├── .editorconfig ├── .github └── workflows │ ├── build_test.yml │ ├── jitpack_build_trigger.yml │ ├── sonar.yml │ └── sync_docs.yml ├── .gitignore ├── .profileconfig.json ├── LICENSE ├── README.md ├── build.gradle.kts ├── docker-compose.yml ├── docs ├── Home.md └── mql │ └── Basic Usage.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── modules ├── block-placement │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ └── block │ │ │ └── placement │ │ │ ├── BlockPlaceMechanicAxis.java │ │ │ ├── BlockPlaceMechanicButton.java │ │ │ ├── BlockPlaceMechanicChestType.java │ │ │ ├── BlockPlaceMechanicFence.java │ │ │ ├── BlockPlaceMechanicGlowLichen.java │ │ │ ├── BlockPlaceMechanicHalf.java │ │ │ ├── BlockPlaceMechanicPointedDripstone.java │ │ │ ├── BlockPlaceMechanicRotation.java │ │ │ ├── BlockPlaceMechanicSlab.java │ │ │ ├── BlockPlaceMechanicStairShape.java │ │ │ ├── BlockPlaceMechanicVine.java │ │ │ ├── BlockPlaceMechanicWall.java │ │ │ ├── BlockPlaceMechanicWallReplacement.java │ │ │ └── HCPlacementRules.java │ │ └── test │ │ └── java │ │ └── net │ │ └── hollowcube │ │ └── test │ │ └── TestServer.java ├── build.gradle.kts ├── common │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ ├── Env.java │ │ │ ├── config │ │ │ └── ConfigProvider.java │ │ │ ├── data │ │ │ ├── NumberSource.java │ │ │ └── number │ │ │ │ ├── ConstantNumberProvider.java │ │ │ │ └── NumberProvider.java │ │ │ ├── dfu │ │ │ ├── DFUUtil.java │ │ │ ├── EnvVarOps.java │ │ │ └── ExtraCodecs.java │ │ │ ├── lang │ │ │ └── LanguageProvider.java │ │ │ ├── logging │ │ │ ├── CustomJsonWriter.java │ │ │ ├── Logger.java │ │ │ ├── LoggerFactory.java │ │ │ └── LoggerImpl.java │ │ │ ├── mongo │ │ │ ├── BsonOps.java │ │ │ └── package-info.java │ │ │ ├── registry │ │ │ ├── MapRegistry.java │ │ │ ├── MissingEntryException.java │ │ │ ├── Registry.java │ │ │ ├── Resource.java │ │ │ └── ResourceFactory.java │ │ │ ├── server │ │ │ ├── Facet.java │ │ │ ├── IsolatedServerWrapper.java │ │ │ ├── ServerWrapper.java │ │ │ └── instance │ │ │ │ └── TickTrackingInstance.java │ │ │ └── util │ │ │ ├── BlockUtil.java │ │ │ ├── ComponentUtil.java │ │ │ ├── EventUtil.java │ │ │ ├── FutureUtil.java │ │ │ ├── JsonUtil.java │ │ │ └── ParticleUtils.java │ │ └── test │ │ ├── java │ │ └── net │ │ │ └── hollowcube │ │ │ ├── data │ │ │ └── number │ │ │ │ └── TestConstantNumberProvider.java │ │ │ ├── dfu │ │ │ └── TestEnvVarOps.java │ │ │ ├── registry │ │ │ ├── TestMissingEntry.java │ │ │ └── TestResource.java │ │ │ └── util │ │ │ └── TestJsonUtil.java │ │ └── resources │ │ ├── lang │ │ └── en_US.properties │ │ └── test.json ├── dev │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── net │ │ └── hollowcube │ │ └── dev │ │ └── Main.java ├── instances │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── net │ │ └── hollowcube │ │ └── world │ │ ├── BaseWorld.java │ │ ├── World.java │ │ ├── WorldManager.java │ │ ├── compression │ │ ├── CompressedWorldData.java │ │ └── WorldCompressor.java │ │ ├── decompression │ │ ├── RegionFileData.java │ │ └── WorldDecompressor.java │ │ ├── dimension │ │ └── DimensionTypes.java │ │ ├── event │ │ ├── PlayerInstanceLeaveEvent.java │ │ └── PlayerSpawnInInstanceEvent.java │ │ ├── generation │ │ ├── FlatGenerator.java │ │ ├── MapGenerators.java │ │ ├── StoneGenerator.java │ │ └── VoidGenerator.java │ │ ├── storage │ │ ├── FileStorage.java │ │ ├── FileStorageMemory.java │ │ ├── FileStorageS3.java │ │ ├── PostgreSQLManager.java │ │ └── SeaweedFS.java │ │ └── util │ │ └── FileUtil.java ├── mql │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ └── mql │ │ │ ├── MqlScript.java │ │ │ ├── foreign │ │ │ ├── MqlForeignFunctions.java │ │ │ ├── MqlForeignTypes.java │ │ │ └── Query.java │ │ │ ├── jit │ │ │ ├── AsmUtil.java │ │ │ ├── MqlCompiler.java │ │ │ ├── MqlEnv.java │ │ │ ├── MqlQueryScript.java │ │ │ └── MqlRuntime.java │ │ │ ├── parser │ │ │ ├── MqlLexer.java │ │ │ ├── MqlParseError.java │ │ │ ├── MqlParser.java │ │ │ └── MqlToken.java │ │ │ ├── runtime │ │ │ ├── MqlMath.java │ │ │ ├── MqlRuntimeError.java │ │ │ ├── MqlScope.java │ │ │ ├── MqlScopeImpl.java │ │ │ └── MqlScriptScope.java │ │ │ ├── tree │ │ │ ├── MqlAccessExpr.java │ │ │ ├── MqlArgListExpr.java │ │ │ ├── MqlBinaryExpr.java │ │ │ ├── MqlCallExpr.java │ │ │ ├── MqlExpr.java │ │ │ ├── MqlIdentExpr.java │ │ │ ├── MqlNumberExpr.java │ │ │ ├── MqlTernaryExpr.java │ │ │ ├── MqlUnaryExpr.java │ │ │ └── MqlVisitor.java │ │ │ ├── util │ │ │ ├── MqlPrinter.java │ │ │ └── StringUtil.java │ │ │ └── value │ │ │ ├── MqlCallable.java │ │ │ ├── MqlHolder.java │ │ │ ├── MqlIdentValue.java │ │ │ ├── MqlNumberValue.java │ │ │ └── MqlValue.java │ │ └── test │ │ └── java │ │ └── net │ │ └── hollowcube │ │ └── mql │ │ ├── TestMql.java │ │ ├── foreign │ │ └── TestMqlForeignFunctions.java │ │ ├── jit │ │ ├── BaseScript.java │ │ ├── QueryScript.java │ │ ├── QueryTest.java │ │ ├── TestCompilation.java │ │ ├── TestExecution.java │ │ └── TestRegression.java │ │ ├── parser │ │ ├── TestMqlLexer.java │ │ └── TestMqlParser.java │ │ └── runtime │ │ └── TestMqlMath.java ├── schem │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── jmh │ │ ├── java │ │ │ └── net │ │ │ │ └── hollowcube │ │ │ │ └── util │ │ │ │ └── schem │ │ │ │ ├── BenchSchematicBuilder.java │ │ │ │ └── BenchSchematicReader.java │ │ └── resources │ │ │ └── mm-spawn-1-27.schem │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ └── util │ │ │ └── schem │ │ │ ├── BlockUtil.java │ │ │ ├── CoordinateUtil.java │ │ │ ├── Rotation.java │ │ │ ├── Schematic.java │ │ │ ├── SchematicBuilder.java │ │ │ ├── SchematicReadException.java │ │ │ ├── SchematicReader.java │ │ │ └── SchematicWriter.java │ │ └── test │ │ ├── java │ │ └── net │ │ │ └── hollowcube │ │ │ └── util │ │ │ └── schem │ │ │ ├── TestRegressionIssue44.java │ │ │ ├── TestRegressionIssue46.java │ │ │ ├── TestSchematicBuilder.java │ │ │ └── TestSchematicWriter.java │ │ └── resources │ │ ├── big.schem │ │ ├── big2.schem │ │ ├── minmax345.schem │ │ ├── minmax345_2.schem │ │ ├── minmaxcube.schem │ │ └── minmaxrect.schem └── test │ ├── README.md │ ├── build.gradle.kts │ └── src │ └── main │ └── java │ └── net │ ├── hollowcube │ └── test │ │ ├── ComponentUtil.java │ │ └── TestUtil.java │ └── minestom │ └── server │ └── test │ ├── Collector.java │ ├── Env.java │ ├── EnvBefore.java │ ├── EnvCleaner.java │ ├── EnvImpl.java │ ├── EnvParameterResolver.java │ ├── EnvTest.java │ ├── FlexibleListener.java │ ├── TestConnection.java │ ├── TestConnectionImpl.java │ ├── TestUtils.java │ └── truth │ ├── AbstractInventorySubject.java │ ├── EntitySubject.java │ ├── ItemStackSubject.java │ └── MinestomTruth.java └── settings.gradle.kts /.github/workflows/build_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v2 17 | with: 18 | distribution: 'zulu' 19 | java-version: 17 20 | - name: Grant execute permission for gradlew 21 | run: chmod +x gradlew 22 | - name: Setup gradle cache 23 | uses: burrunan/gradle-cache-action@v1 24 | with: 25 | save-generated-gradle-jars: false 26 | save-local-build-cache: false 27 | save-gradle-dependencies-cache: true 28 | save-maven-dependencies-cache: true 29 | # Ignore some of the paths when caching Maven Local repository 30 | maven-local-ignore-paths: | 31 | starlight/mmo/ 32 | - name: Build 33 | run: ./gradlew classes testClasses 34 | - name: Run tests 35 | run: ./gradlew test 36 | -------------------------------------------------------------------------------- /.github/workflows/jitpack_build_trigger.yml: -------------------------------------------------------------------------------- 1 | name: Trigger Jitpack Build 2 | on: 3 | push: 4 | branches: [ main ] 5 | 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Trigger Jitpack Build 12 | run: curl "https://jitpack.io/com/github/hollow-cube/common/${GITHUB_SHA:0:10}/build.log" -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main_disabled 7 | pull_request: 8 | branches: 9 | - main_disabled 10 | 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 20 | - name: Set up JDK 17 21 | uses: actions/setup-java@v1 22 | with: 23 | java-version: 17 24 | - name: Cache SonarQube packages 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/.sonar/cache 28 | key: ${{ runner.os }}-sonar 29 | restore-keys: ${{ runner.os }}-sonar 30 | - name: Cache Gradle packages 31 | uses: actions/cache@v1 32 | with: 33 | path: ~/.gradle/caches 34 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 35 | restore-keys: ${{ runner.os }}-gradle 36 | - name: Build and analyze 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 39 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 40 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 41 | run: ./gradlew build sonar --info -------------------------------------------------------------------------------- /.github/workflows/sync_docs.yml: -------------------------------------------------------------------------------- 1 | name: Sync Docs 2 | 3 | on: 4 | push: 5 | paths: 6 | - docs/** 7 | branches: 8 | - main 9 | 10 | jobs: 11 | deploy-wiki: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Sync Wiki Changes 16 | uses: Andrew-Chen-Wang/github-wiki-action@v3 17 | env: 18 | WIKI_DIR: docs/ 19 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | GH_MAIL: ${{ github.triggering_actor }} 21 | GH_NAME: ${{ github.repository_owner }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | .idea 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store -------------------------------------------------------------------------------- /.profileconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "jfrConfig": { 3 | "settings": "profile" 4 | }, 5 | "asyncProfilerConfig": { 6 | "jfrsync": true, 7 | "alloc": true, 8 | "event": "wall", 9 | "misc": "" 10 | }, 11 | "file": "$PROJECT_DIR/profile.jfr", 12 | "conversionConfig": { 13 | "nonProjectPackagePrefixes": [ 14 | "java.", 15 | "javax.", 16 | "kotlin.", 17 | "jdk.", 18 | "com.google.", 19 | "org.apache.", 20 | "org.spring.", 21 | "sun.", 22 | "scala." 23 | ], 24 | "enableMarkers": true, 25 | "initialVisibleThreads": 10, 26 | "initialSelectedThreads": 10, 27 | "includeGCThreads": false, 28 | "includeInitialSystemProperty": false, 29 | "includeInitialEnvironmentVariables": false, 30 | "includeSystemProcesses": false, 31 | "ignoredEvents": [ 32 | "jdk.ActiveSetting", 33 | "jdk.ActiveRecording", 34 | "jdk.BooleanFlag", 35 | "jdk.IntFlag", 36 | "jdk.DoubleFlag", 37 | "jdk.LongFlag", 38 | "jdk.NativeLibrary", 39 | "jdk.StringFlag", 40 | "jdk.UnsignedIntFlag", 41 | "jdk.UnsignedLongFlag", 42 | "jdk.InitialSystemProperty", 43 | "jdk.InitialEnvironmentVariable", 44 | "jdk.SystemProcess", 45 | "jdk.ModuleExport", 46 | "jdk.ModuleRequire" 47 | ], 48 | "minRequiredItemsPerThread": 3 49 | } 50 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 HollowCube Contibutors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repository is not being updated and should not be used going forward. 3 | > 4 | > The schematic module has been moved to its own repository [here](https://github.com/hollow-cube/schem), 5 | > so has mql [here](https://github.com/hollow-cube/mql). 6 | 7 | # HollowCube Common 8 | A common set of libraries for Minestom servers. 9 | 10 | todo 11 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("net.ltgt.errorprone") version "2.0.2" apply false 3 | id("org.sonarqube") version "3.5.0.2730" 4 | } 5 | 6 | sonarqube{ 7 | properties{ 8 | property("sonar.host.url", "https://sonarqube.hollowcube.net") 9 | property("sonar.sourceEncoding", "UTF-8") 10 | property("sonar.projectName", "common") 11 | property("sonar.projectKey", "common") 12 | } 13 | } 14 | 15 | subprojects { 16 | sonarqube { 17 | properties { 18 | property("sonar.sources", "src/main/") 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: 'postgres:latest' 6 | container_name: 'hollowcube' 7 | ports: 8 | - '5432:5432' 9 | environment: 10 | - 'POSTGRES_PASSWORD=starlight' -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | # HollowCube Common 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/mql/Basic Usage.md: -------------------------------------------------------------------------------- 1 | MQL may be used either in an interpreted mode, which may support features not present in compiled mode. 2 | 3 | ### Interpreted 4 | Interpreted mode uses a tree-walk interpreter to evaluate the script. The API consists of `MqlScript` and `MqlScope`s. 5 | A script is created using `MqlScript#parse(String)`, and then evaluated using `MqlScript#evaluate(MqlScope)double`. 6 | 7 | The simplest example of interpreting a script is as follows: 8 | ``` 9 | var script = MqlScript.parse("1.234"); 10 | var result = script.evaluate(new MqlScopeImpl()); 11 | System.out.println(result); // 1.234 12 | ``` 13 | 14 | The scope may be used to provide variables and functions to the script. `MqlMath.INSTANCE` itself is a scope, which 15 | provides the language spec math queries. In interpreted mode, math is not available unless provided explicitly. 16 | 17 | ``` 18 | var script = MqlScript.parse("math.pi + 1"); 19 | var result = script.evaluate(new MqlScopeImpl(){{ 20 | put("math", MqlMath.INSTANCE); 21 | }}); 22 | System.out.println(result); // 4.141592653589793 23 | ``` 24 | 25 | `MqlForeignFunctions` provides an interface for calling Java functions from MQL. The main function is 26 | `MqlForeignFunctions.create(Class, T)MqlScope`, which creates a scope from the provided class, 27 | exposing all `@Query` methods to the script. See the example below: 28 | 29 | ``` 30 | public class MyQuery { 31 | @Query 32 | public double add(double a, double b) { 33 | return a + b; 34 | } 35 | } 36 | 37 | var script = MqlScript.parse("my.add(1, 2)"); 38 | var result = script.evaluate(new MqlScopeImpl(){{ 39 | data.put("my", MqlForeignFunctions.create(MyQuery.class, new MyQuery())); 40 | }}); 41 | System.out.println(result); // 3.0 42 | ``` 43 | 44 | ### Compiled 45 | Compiled (JIT) mode compiles the script to JVM bytecode at runtime. Compiled mode is significantly faster than 46 | interpreted mode (solid benchmarks wip), however it is not as flexible. 47 | 48 | Due to Java security, the following vm argument must be provided to allow dynamic class loading: 49 | - `--add-opens java.base/java.lang=ALL-UNNAMED` 50 | 51 | The simple interpreted example can be rewritten as follows: 52 | ``` 53 | public interface MyScript { 54 | double evaluate(); 55 | } 56 | 57 | var compiler = new MqlCompiler<>(MyScript.class); 58 | var scriptClass = compiler.compile("1.234"); // Class 59 | var script = scriptClass.newInstance(); // Deprecated and unsafe, handle errors in real usage 60 | var result = script.evaluate(); 61 | System.out.println(result); // 1.234 62 | ``` 63 | 64 | Unlike interpreted mode, compiled mode provides math functions implicitly (for now): 65 | 66 | ``` 67 | // ... 68 | var script = compiler.compile("math.pi + 1").newInstance(); 69 | var result = script.evaluate(); 70 | System.out.println(result); // 4.141592653589793 71 | ``` 72 | 73 | Compiled mode requires all types to be known at compile time, particularly, they must be present in the 74 | script interface given to the compiler. The third example can be rewritten as follows: 75 | 76 | ``` 77 | public class MyQuery { 78 | @Query 79 | public double add(double a, double b) { 80 | return a + b; 81 | } 82 | } 83 | 84 | public interface MyScript { 85 | double evaluate(@MqlEnv("my") MyQuery my); 86 | } 87 | 88 | var compiler = new MqlCompiler<>(MyScript.class); 89 | var script = compiler.compile("my.add(1, 2)").newInstance(); 90 | var result = script.evaluate(new MyQuery()); 91 | System.out.println(result); // 3.0 92 | ``` 93 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - source "$HOME/.sdkman/bin/sdkman-init.sh" 3 | - sdk update 4 | - sdk install java 17-open 5 | - sdk use java 17-open -------------------------------------------------------------------------------- /modules/block-placement/README.md: -------------------------------------------------------------------------------- 1 | # Placement Rules 2 | A (wip) set of block placement rules for Minestom based on [Moulberry's PR](https://github.com/Minestom/Minestom/pull/554) to Minestom. 3 | 4 | > ⚠️ **Requires using the [HollowCube Minestom fork](https://github.com/hollow-cube/Minestom)** 5 | 6 | Minestom has not yet implemented the required behavior (click location) to implement these rules, nor accepted a PR 7 | which implements it and/or these rules. Once that happens, this module will no longer be required. 8 | 9 | ## Install 10 | 11 | Artifacts are published on Jitpack for now. Add the following to your `build.gradle(.kts)`: 12 | 13 | > Note: {VERSION} should be replaced with the latest version on Jitpack, you can find 14 | > this [here](https://jitpack.io/#hollow-cube/common). 15 | 16 | ```kotlin 17 | dependencies { 18 | implementation("com.github.hollow-cube.common:block-placement:{VERSION}") 19 | } 20 | ``` 21 | 22 | ## Usage 23 | 24 | To use, simply call `HCPlacementRules.init()` during server initialization. 25 | 26 | ## Contributing 27 | 28 | Issues and PRs are welcome! Please refer to [CONTRIBUTING.md](../../CONTRIBUTING.md) for more information. 29 | 30 | ## License 31 | 32 | This project is licensed under the [MIT License](../../LICENSE). 33 | -------------------------------------------------------------------------------- /modules/block-placement/build.gradle.kts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/block-placement/build.gradle.kts -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicAxis.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 4 | import net.minestom.server.instance.block.Block; 5 | 6 | public class BlockPlaceMechanicAxis { 7 | 8 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 9 | block = event.getBlock(); 10 | 11 | switch (event.getBlockFace()) { 12 | case EAST, WEST -> event.setBlock(block.withProperty("axis", "x")); 13 | case NORTH, SOUTH -> event.setBlock(block.withProperty("axis", "z")); 14 | default -> event.setBlock(block.withProperty("axis", "y")); 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicButton.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.coordinate.Vec; 4 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 5 | import net.minestom.server.instance.block.Block; 6 | import net.minestom.server.instance.block.BlockFace; 7 | 8 | public class BlockPlaceMechanicButton { 9 | 10 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 11 | BlockFace face = event.getBlockFace(); 12 | 13 | if (face == BlockFace.BOTTOM) { 14 | block = block.withProperty("face", "ceiling"); 15 | } else if (face == BlockFace.TOP) { 16 | block = block.withProperty("face", "floor"); 17 | } else { 18 | block = block.withProperty("face", "wall"); 19 | block = block.withProperty("facing", face.name().toLowerCase()); 20 | event.setBlock(block); 21 | return; 22 | } 23 | 24 | // When placing on the top/bottom of a block, set facing 25 | Vec playerDir = event.getPlayer().getPosition().direction(); 26 | double absX = Math.abs(playerDir.x()); 27 | double absZ = Math.abs(playerDir.z()); 28 | if (absX > absZ) { 29 | if (playerDir.x() > 0) { 30 | block = block.withProperty("facing", "east"); 31 | } else { 32 | block = block.withProperty("facing", "west"); 33 | } 34 | } else { 35 | if (playerDir.z() > 0) { 36 | block = block.withProperty("facing", "south"); 37 | } else { 38 | block = block.withProperty("facing", "north"); 39 | } 40 | } 41 | 42 | event.setBlock(block); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicChestType.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.coordinate.Vec; 5 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 6 | import net.minestom.server.event.player.PlayerBlockUpdateNeighborEvent; 7 | import net.minestom.server.instance.Instance; 8 | import net.minestom.server.instance.block.Block; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public class BlockPlaceMechanicChestType { 12 | 13 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 14 | event.setBlock(updateFromNeighbors(event.getBlock(), event.getInstance(), event.getBlockPosition())); 15 | } 16 | 17 | public static void onNeighbor(Block block, PlayerBlockUpdateNeighborEvent event) { 18 | event.setBlock(updateFromNeighbors(event.getBlock(), event.getInstance(), event.getBlockPosition())); 19 | } 20 | 21 | private static @NotNull Block updateFromNeighbors(Block state, Instance instance, Point position) { 22 | String oldState = state.getProperty("type"); 23 | boolean isSingle = "single".equals(oldState); 24 | boolean isLeft = "left".equals(oldState); 25 | boolean isRight = "right".equals(oldState); 26 | 27 | state = state.withProperty("type", "single"); 28 | 29 | String facing = state.getProperty("facing"); 30 | 31 | Point left; 32 | Point right; 33 | 34 | switch (facing) { 35 | case "east": { 36 | left = new Vec(0, 0, 1); 37 | right = new Vec(0, 0, -1); 38 | break; 39 | } 40 | case "west": { 41 | left = new Vec(0, 0, -1); 42 | right = new Vec(0, 0, 1); 43 | break; 44 | } 45 | case "north": { 46 | left = new Vec(1, 0, 0); 47 | right = new Vec(-1, 0, 0); 48 | break; 49 | } 50 | case "south": { 51 | left = new Vec(-1, 0, 0); 52 | right = new Vec(1, 0, 0); 53 | break; 54 | } 55 | default: { 56 | return state; 57 | } 58 | } 59 | 60 | Block leftBlock = instance.getBlock( 61 | position.blockX() + left.blockX(), 62 | position.blockY() + left.blockY(), 63 | position.blockZ() + left.blockZ() 64 | ); 65 | 66 | if (leftBlock.compare(state)) { 67 | if (isSingle || isLeft) { 68 | String leftType = leftBlock.getProperty("type"); 69 | 70 | if ("single".equals(leftType) || "right".equals(leftType)) { 71 | state = state.withProperty("type", "left"); 72 | return state; 73 | } 74 | } 75 | } else if (isLeft) { 76 | state = state.withProperty("type", "single"); 77 | return state; 78 | } 79 | 80 | Block rightBlock = instance.getBlock( 81 | position.blockX() + right.blockX(), 82 | position.blockY() + right.blockY(), 83 | position.blockZ() + right.blockZ() 84 | ); 85 | 86 | if (rightBlock.compare(state)) { 87 | if (isSingle || isRight) { 88 | String rightLeft = rightBlock.getProperty("type"); 89 | 90 | if ("single".equals(rightLeft) || "left".equals(rightLeft)) { 91 | state = state.withProperty("type", "right"); 92 | return state; 93 | } 94 | } 95 | } else if (isRight) { 96 | state = state.withProperty("type", "single"); 97 | return state; 98 | } 99 | 100 | return state; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicFence.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 5 | import net.minestom.server.event.player.PlayerBlockUpdateNeighborEvent; 6 | import net.minestom.server.instance.Instance; 7 | import net.minestom.server.instance.block.Block; 8 | 9 | public class BlockPlaceMechanicFence { 10 | 11 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 12 | event.setBlock(update(event.getBlock(), event.getInstance(), event.getBlockPosition())); 13 | } 14 | 15 | public static void onNeighbor(Block block, PlayerBlockUpdateNeighborEvent event) { 16 | event.setBlock(update(event.getBlock(), event.getInstance(), event.getBlockPosition())); 17 | } 18 | 19 | private static Block update(Block state, Instance instance, Point position) { 20 | boolean northNeighbor = instance.getBlock(position.blockX(), position.blockY(), position.blockZ()-1).isSolid(); 21 | boolean southNeighbor = instance.getBlock(position.blockX(), position.blockY(), position.blockZ()+1).isSolid(); 22 | boolean eastNeighbor = instance.getBlock(position.blockX()+1, position.blockY(), position.blockZ()).isSolid(); 23 | boolean westNeighbor = instance.getBlock(position.blockX()-1, position.blockY(), position.blockZ()).isSolid(); 24 | 25 | state = state.withProperty("north", northNeighbor ? "true" : "false"); 26 | state = state.withProperty("south", southNeighbor ? "true" : "false"); 27 | state = state.withProperty("east", eastNeighbor ? "true" : "false"); 28 | state = state.withProperty("west", westNeighbor ? "true" : "false"); 29 | 30 | return state; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicGlowLichen.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 4 | import net.minestom.server.instance.block.Block; 5 | import net.minestom.server.instance.block.BlockFace; 6 | 7 | public class BlockPlaceMechanicGlowLichen { 8 | 9 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 10 | BlockFace face = event.getBlockFace().getOppositeFace(); 11 | 12 | String faceName = face.name().toLowerCase(); 13 | if (face == BlockFace.TOP) faceName = "up"; 14 | if (face == BlockFace.BOTTOM) faceName = "down"; 15 | 16 | Block oldBlock = event.getInstance().getBlock(event.getBlockPosition()); 17 | if (oldBlock.compare(block)) { 18 | block = block.withProperties(oldBlock.properties()); 19 | } 20 | 21 | block = block.withProperty(faceName, "true"); 22 | 23 | event.setBlock(block); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicHalf.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 4 | import net.minestom.server.instance.block.Block; 5 | import net.minestom.server.instance.block.BlockFace; 6 | 7 | public class BlockPlaceMechanicHalf { 8 | 9 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 10 | onPlace(block, event, "half", "top", "bottom"); 11 | } 12 | 13 | public static void onPlace(Block block, PlayerBlockPlaceEvent event, 14 | String propertyName) { 15 | onPlace(block, event, propertyName, "top", "bottom"); 16 | } 17 | 18 | public static void onPlace(Block block, PlayerBlockPlaceEvent event, 19 | String propertyName, String propertyValTop, String propertyValBot) { 20 | block = event.getBlock(); 21 | 22 | BlockFace face = event.getBlockFace(); 23 | if (face == BlockFace.TOP) { 24 | event.setBlock(block.withProperty(propertyName, propertyValBot)); 25 | } else if (face == BlockFace.BOTTOM) { 26 | event.setBlock(block.withProperty(propertyName, propertyValTop)); 27 | } else if (event.getCursorPosition().y() % 1 > 0.5f) { 28 | event.setBlock(block.withProperty(propertyName, propertyValTop)); 29 | } else { 30 | event.setBlock(block.withProperty(propertyName, propertyValBot)); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicPointedDripstone.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 5 | import net.minestom.server.event.player.PlayerBlockUpdateNeighborEvent; 6 | import net.minestom.server.instance.Instance; 7 | import net.minestom.server.instance.block.Block; 8 | 9 | public class BlockPlaceMechanicPointedDripstone { 10 | 11 | public static void onNeighbor(Block block, PlayerBlockUpdateNeighborEvent event) { 12 | String direction = block.getProperty("vertical_direction"); 13 | if (direction == null) return; 14 | 15 | Instance instance = event.getInstance(); 16 | Point position = event.getBlockPosition(); 17 | 18 | Block aboveState = instance.getBlock(position.blockX(), position.blockY() + (direction.equals("down") ? -1 : 1), position.blockZ()); 19 | 20 | if (Block.POINTED_DRIPSTONE.compare(aboveState, Block.Comparator.ID)) { 21 | String thickness = aboveState.getProperty("thickness"); 22 | 23 | if (aboveState.getProperty("vertical_direction").equals(direction)) { 24 | switch (thickness) { 25 | case "tip": 26 | case "tip_merge": 27 | block = block.withProperty("thickness", "frustum"); 28 | break; 29 | case "frustum": 30 | case "middle": 31 | Block blockOpposite = instance.getBlock(position.blockX(), position.blockY() + (direction.equals("down") ? 1 : -1), position.blockZ()); 32 | if (!Block.POINTED_DRIPSTONE.compare(blockOpposite, Block.Comparator.ID)) { 33 | block = block.withProperty("thickness", "base"); 34 | } else { 35 | block = block.withProperty("thickness", "middle"); 36 | } 37 | break; 38 | } 39 | } else { 40 | switch (thickness) { 41 | case "tip": 42 | case "tip_merge": 43 | block = block.withProperty("thickness", "tip_merge"); 44 | break; 45 | } 46 | } 47 | } else { 48 | block = block.withProperty("thickness", "tip"); 49 | } 50 | 51 | event.setShouldUpdateNeighbors(true); 52 | event.setBlock(block); 53 | } 54 | 55 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 56 | BlockPlaceMechanicHalf.onPlace(block, event, "vertical_direction", "down", "up"); 57 | block = event.getBlock(); 58 | 59 | String direction = block.getProperty("vertical_direction"); 60 | if (direction == null) return; 61 | 62 | Instance instance = event.getInstance(); 63 | Point position = event.getBlockPosition(); 64 | 65 | Block aboveState = instance.getBlock(position.blockX(), position.blockY() + (direction.equals("down") ? -1 : 1), position.blockZ()); 66 | 67 | if (Block.POINTED_DRIPSTONE.compare(aboveState, Block.Comparator.ID)) { 68 | String thickness = aboveState.getProperty("thickness"); 69 | 70 | if (!aboveState.getProperty("vertical_direction").equals(direction)) { 71 | switch (thickness) { 72 | case "tip": 73 | case "tip_merge": 74 | block = block.withProperty("thickness", "tip_merge"); 75 | break; 76 | } 77 | } 78 | } 79 | 80 | event.setBlock(block); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicSlab.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 5 | import net.minestom.server.instance.Instance; 6 | import net.minestom.server.instance.block.Block; 7 | import net.minestom.server.instance.block.BlockFace; 8 | 9 | public class BlockPlaceMechanicSlab { 10 | 11 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 12 | BlockPlaceMechanicHalf.onPlace(block, event, "type"); 13 | block = event.getBlock(); 14 | 15 | BlockFace face = event.getBlockFace(); 16 | Point position = event.getBlockPosition(); 17 | Instance instance = event.getInstance(); 18 | 19 | if (face == BlockFace.TOP || face == BlockFace.BOTTOM) { 20 | Point placedOn = position.add(0, face == BlockFace.BOTTOM ? 1 : -1, 0); 21 | 22 | Block placedOnState = instance.getBlock(placedOn); 23 | 24 | if (placedOnState.compare(block)) { 25 | String oldType = placedOnState.getProperty("type"); 26 | String newType = block.getProperty("type"); 27 | 28 | if (!oldType.equalsIgnoreCase("double") && 29 | oldType.equalsIgnoreCase(newType)) { 30 | placedOnState = placedOnState.withProperty("type", "double"); 31 | instance.setBlock(placedOn, placedOnState); 32 | event.setCancelled(true); 33 | return; 34 | } 35 | } 36 | } 37 | 38 | Block blockAt = instance.getBlock(position); 39 | if (blockAt.compare(block)) { 40 | event.setBlock(block.withProperty("type", "double")); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicVine.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.coordinate.Vec; 4 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 5 | import net.minestom.server.instance.block.Block; 6 | import net.minestom.server.instance.block.BlockFace; 7 | 8 | public class BlockPlaceMechanicVine { 9 | 10 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 11 | BlockFace face = event.getBlockFace().getOppositeFace(); 12 | Vec playerDir = event.getPlayer().getPosition().direction(); 13 | 14 | // Find direction if placed on bottom 15 | if(face == BlockFace.BOTTOM) { 16 | if(Math.abs(playerDir.x()) > Math.abs(playerDir.z())) { 17 | if(playerDir.x() > 0) { 18 | face = BlockFace.EAST; 19 | } else { 20 | face = BlockFace.WEST; 21 | } 22 | } else { 23 | if(playerDir.z() > 0) { 24 | face = BlockFace.SOUTH; 25 | } else { 26 | face = BlockFace.NORTH; 27 | } 28 | } 29 | } 30 | 31 | String faceName = face.name().toLowerCase(); 32 | if(face == BlockFace.TOP) faceName = "up"; 33 | 34 | // Combine with previous vine states 35 | Block oldBlock = event.getInstance().getBlock(event.getBlockPosition()); 36 | if(oldBlock.compare(block)) { 37 | block = block.withProperties(oldBlock.properties()); 38 | } 39 | 40 | block = block.withProperty(faceName, "true"); 41 | 42 | event.setBlock(block); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicWall.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 5 | import net.minestom.server.event.player.PlayerBlockUpdateNeighborEvent; 6 | import net.minestom.server.instance.Instance; 7 | import net.minestom.server.instance.block.Block; 8 | import net.minestom.server.utils.NamespaceID; 9 | 10 | public class BlockPlaceMechanicWall { 11 | 12 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 13 | event.setBlock(update(event.getBlock(), event.getBlockPosition(), event.getInstance())); 14 | } 15 | 16 | public static void onNeighbor(Block block, PlayerBlockUpdateNeighborEvent event) { 17 | event.setBlock(update(event.getBlock(), event.getBlockPosition(), event.getInstance())); 18 | event.setShouldUpdateNeighbors(true); 19 | } 20 | 21 | private static Block update(Block block, Point position, Instance instance) { 22 | boolean northNeighbor = instance.getBlock(position.blockX(), position.blockY(), position.blockZ() - 1).isSolid(); 23 | boolean southNeighbor = instance.getBlock(position.blockX(), position.blockY(), position.blockZ() + 1).isSolid(); 24 | boolean eastNeighbor = instance.getBlock(position.blockX() + 1, position.blockY(), position.blockZ()).isSolid(); 25 | boolean westNeighbor = instance.getBlock(position.blockX() - 1, position.blockY(), position.blockZ()).isSolid(); 26 | 27 | block = block.withProperty("up", "" + !((northNeighbor && southNeighbor) || (eastNeighbor && westNeighbor))); 28 | 29 | Block aboveBlock = instance.getBlock(position.blockX(), position.blockY() + 1, position.blockZ()); 30 | 31 | if (HCPlacementRules.isWall(aboveBlock)) { 32 | if ("true".equals(aboveBlock.getProperty("up"))) { 33 | block = block.withProperty("up", "true"); 34 | } else { 35 | block = block.withProperty("up", "" + !((northNeighbor && southNeighbor && 36 | !"none".equals(aboveBlock.getProperty("north")) && !"none".equals(aboveBlock.getProperty("south"))) 37 | || (eastNeighbor && westNeighbor && !"none".equals(aboveBlock.getProperty("east")) && !"none".equals(aboveBlock.getProperty("west"))))); 38 | } 39 | } else { 40 | block = block.withProperty("up", "" + !((northNeighbor && southNeighbor && eastNeighbor && westNeighbor) || 41 | (!northNeighbor && !southNeighbor && eastNeighbor && westNeighbor) || 42 | (northNeighbor && southNeighbor && !eastNeighbor && !westNeighbor))); 43 | } 44 | 45 | boolean above = aboveBlock.isSolid(); 46 | 47 | if (above) { 48 | block = block.withProperty("north", northNeighbor ? (instance.getBlock(position.blockX(), position.blockY() + 1, position.blockZ() - 1).isSolid() ? "tall" : "low") : "none"); 49 | block = block.withProperty("south", southNeighbor ? (instance.getBlock(position.blockX(), position.blockY() + 1, position.blockZ() + 1).isSolid() ? "tall" : "low") : "none"); 50 | block = block.withProperty("east", eastNeighbor ? (instance.getBlock(position.blockX() + 1, position.blockY() + 1, position.blockZ()).isSolid() ? "tall" : "low") : "none"); 51 | block = block.withProperty("west", westNeighbor ? (instance.getBlock(position.blockX() - 1, position.blockY() + 1, position.blockZ()).isSolid() ? "tall" : "low") : "none"); 52 | } else { 53 | block = block.withProperty("north", northNeighbor ? "low" : "none"); 54 | block = block.withProperty("south", southNeighbor ? "low" : "none"); 55 | block = block.withProperty("east", eastNeighbor ? "low" : "none"); 56 | block = block.withProperty("west", westNeighbor ? "low" : "none"); 57 | } 58 | 59 | return block; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/block-placement/src/main/java/net/hollowcube/block/placement/BlockPlaceMechanicWallReplacement.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.block.placement; 2 | 3 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 4 | import net.minestom.server.instance.block.Block; 5 | import net.minestom.server.utils.NamespaceID; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | public class BlockPlaceMechanicWallReplacement { 11 | 12 | private static final Map WALL_REPLACEMENTS = new HashMap<>(); 13 | 14 | static { 15 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:dead_tube_coral_fan"), Block.DEAD_TUBE_CORAL_WALL_FAN); 16 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:dead_brain_coral_fan"), Block.DEAD_BRAIN_CORAL_WALL_FAN); 17 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:dead_bubble_coral_fan"), Block.DEAD_BUBBLE_CORAL_WALL_FAN); 18 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:dead_fire_coral_fan"), Block.DEAD_FIRE_CORAL_WALL_FAN); 19 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:dead_horn_coral_fan"), Block.DEAD_HORN_CORAL_WALL_FAN); 20 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:tube_coral_fan"), Block.TUBE_CORAL_WALL_FAN); 21 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:brain_coral_fan"), Block.BRAIN_CORAL_WALL_FAN); 22 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:bubble_coral_fan"), Block.BUBBLE_CORAL_WALL_FAN); 23 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:fire_coral_fan"), Block.FIRE_CORAL_WALL_FAN); 24 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:horn_coral_fan"), Block.HORN_CORAL_WALL_FAN); 25 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:torch"), Block.WALL_TORCH); 26 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:redstone_torch"), Block.REDSTONE_WALL_TORCH); 27 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:soul_torch"), Block.SOUL_WALL_TORCH); 28 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:skeleton_skull"), Block.SKELETON_WALL_SKULL); 29 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:wither_skeleton_skull"), Block.WITHER_SKELETON_WALL_SKULL); 30 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:zombie_head"), Block.ZOMBIE_WALL_HEAD); 31 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:player_head"), Block.PLAYER_WALL_HEAD); 32 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:creeper_head"), Block.CREEPER_WALL_HEAD); 33 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:dragon_head"), Block.DRAGON_WALL_HEAD); 34 | 35 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:acacia_sign"), Block.ACACIA_WALL_SIGN); 36 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:warped_sign"), Block.WARPED_WALL_SIGN); 37 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:birch_sign"), Block.BIRCH_WALL_SIGN); 38 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:oak_sign"), Block.OAK_WALL_SIGN); 39 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:crimson_sign"), Block.CRIMSON_WALL_SIGN); 40 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:dark_oak_sign"), Block.DARK_OAK_WALL_SIGN); 41 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:jungle_sign"), Block.JUNGLE_WALL_SIGN); 42 | WALL_REPLACEMENTS.put(NamespaceID.from("minecraft:spruce_sign"), Block.SPRUCE_WALL_SIGN); 43 | } 44 | 45 | public static boolean shouldReplace(Block block) { 46 | return WALL_REPLACEMENTS.containsKey(block.namespace()); 47 | } 48 | 49 | public static void onPlace(Block block, PlayerBlockPlaceEvent event) { 50 | Block replacement = WALL_REPLACEMENTS.get(block.namespace()); 51 | 52 | if (replacement != null && event.getBlockFace().toDirection().normalY() == 0) { 53 | event.setBlock(replacement); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /modules/block-placement/src/test/java/net/hollowcube/test/TestServer.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.test; 2 | 3 | import net.hollowcube.block.placement.HCPlacementRules; 4 | import net.minestom.server.MinecraftServer; 5 | import net.minestom.server.coordinate.Pos; 6 | import net.minestom.server.entity.GameMode; 7 | import net.minestom.server.event.player.PlayerLoginEvent; 8 | import net.minestom.server.event.player.PlayerSpawnEvent; 9 | import net.minestom.server.instance.block.Block; 10 | 11 | public class TestServer { 12 | public static void main(String[] args) { 13 | var server = MinecraftServer.init(); 14 | 15 | var instanceManager = MinecraftServer.getInstanceManager(); 16 | var instance = instanceManager.createInstanceContainer(); 17 | instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); 18 | 19 | var eventHandler = MinecraftServer.getGlobalEventHandler(); 20 | eventHandler.addListener(PlayerLoginEvent.class, event -> { 21 | event.setSpawningInstance(instance); 22 | event.getPlayer().setRespawnPoint(new Pos(0, 40, 0)); 23 | }); 24 | eventHandler.addListener(PlayerSpawnEvent.class, event -> { 25 | var player = event.getPlayer(); 26 | player.setGameMode(GameMode.CREATIVE); 27 | }); 28 | 29 | HCPlacementRules.init(); 30 | 31 | server.start("localhost", 25565); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import net.ltgt.gradle.errorprone.CheckSeverity 2 | import net.ltgt.gradle.errorprone.errorprone 3 | 4 | subprojects { 5 | apply(plugin = "java") 6 | apply(plugin = "net.ltgt.errorprone") 7 | apply(plugin = "maven-publish") 8 | 9 | group = "net.hollowcube.common" 10 | 11 | repositories { 12 | mavenCentral() 13 | maven(url = "https://jitpack.io") 14 | } 15 | 16 | 17 | dependencies { 18 | // A bug with kotlin dsl 19 | val compileOnly by configurations 20 | val implementation by configurations 21 | val annotationProcessor by configurations 22 | val testImplementation by configurations 23 | val testAnnotationProcessor by configurations 24 | val errorprone by configurations 25 | 26 | errorprone("com.google.errorprone:error_prone_core:2.16") 27 | errorprone("com.uber.nullaway:nullaway:0.10.6") 28 | 29 | // Auto service (SPI) 30 | annotationProcessor("com.google.auto.service:auto-service:1.0.1") 31 | testAnnotationProcessor("com.google.auto.service:auto-service:1.0.1") 32 | implementation("com.google.auto.service:auto-service-annotations:1.0.1") 33 | 34 | // Minestom 35 | compileOnly("com.github.hollow-cube:Minestom:a9535e5d29") 36 | 37 | // Testing 38 | testImplementation(project(":modules:test")) 39 | } 40 | 41 | tasks.getByName("test") { 42 | useJUnitPlatform() 43 | } 44 | 45 | tasks.withType { 46 | options.errorprone.disableWarningsInGeneratedCode.set(true) 47 | options.errorprone { 48 | check("NullAway", CheckSeverity.ERROR) 49 | option("NullAway:AnnotatedPackages", "com.uber") 50 | } 51 | } 52 | 53 | configure { 54 | withSourcesJar() 55 | } 56 | 57 | configure { 58 | publications { 59 | create("mavenJava") { 60 | from(components["java"]) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /modules/common/README.md: -------------------------------------------------------------------------------- 1 | ## common 2 | 3 | Common libraries for other modules in the project. 4 | 5 | Included by all other projects, so this must be kept small with minimal dependencies. 6 | 7 | ``` 8 | data: Data parsing abstraction 9 | registry: Resource registry for various data driven modules 10 | util: Common utilities 11 | ``` 12 | -------------------------------------------------------------------------------- /modules/common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | dependencies { 6 | api("com.github.minestommmo:DataFixerUpper:cf58e926a6") 7 | api("net.kyori:adventure-text-minimessage:4.11.0") 8 | 9 | implementation("org.tinylog:tinylog-impl:2.4.1") 10 | 11 | implementation("io.github.cdimascio:dotenv-java:2.2.4") 12 | 13 | // Optional components 14 | compileOnly("org.mongodb:mongodb-driver-sync:4.7.0") 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/Env.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.util.function.Supplier; 8 | 9 | public class Env { 10 | /** 11 | * Strict mode is enabled in production, but may be disabled during tests. 12 | *

13 | * It should be used to check cases which are fine during development but are a fatal problem in production. For 14 | * example, if a registry is empty for any reason in production the server should not be allowed to start. 15 | */ 16 | public static final Boolean STRICT_MODE = Boolean.valueOf(System.getProperty("starlight.strict", "false")); 17 | 18 | 19 | private static final Logger STRICT_LOGGER = LoggerFactory.getLogger("STRICT"); 20 | 21 | public static void strictValidation(@NotNull String message, @NotNull Supplier predicate) { 22 | if (STRICT_MODE && predicate.get()) { 23 | STRICT_LOGGER.error(message); 24 | System.exit(1); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/config/ConfigProvider.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.config; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.hollowcube.dfu.EnvVarOps; 5 | import net.minestom.server.utils.validate.Check; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.Locale; 9 | 10 | public final class ConfigProvider { 11 | 12 | public static @NotNull T load(@NotNull String prefix, @NotNull Codec codec) { 13 | var result = EnvVarOps.DOTENV.withDecoder(codec) 14 | .apply(prefix.toUpperCase(Locale.ROOT)) 15 | .result() 16 | .orElse(null); 17 | Check.notNull(result, "Config unable to load"); 18 | return result.getFirst(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/data/NumberSource.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.data; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.concurrent.ThreadLocalRandom; 6 | 7 | /** 8 | * A source of numbers, implementations decide where the numbers come from 9 | */ 10 | @FunctionalInterface 11 | public interface NumberSource { 12 | 13 | static @NotNull NumberSource constant(double value) { 14 | return () -> value; 15 | } 16 | 17 | static @NotNull NumberSource threadLocalRandom() { 18 | return () -> ThreadLocalRandom.current().nextDouble(); 19 | } 20 | 21 | 22 | double random(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/data/number/ConstantNumberProvider.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.data.number; 2 | 3 | import com.google.auto.service.AutoService; 4 | import com.mojang.serialization.Codec; 5 | import com.mojang.serialization.codecs.RecordCodecBuilder; 6 | import net.hollowcube.data.NumberSource; 7 | import net.hollowcube.dfu.ExtraCodecs; 8 | import net.minestom.server.utils.NamespaceID; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | record ConstantNumberProvider( 12 | @NotNull Number value 13 | ) implements NumberProvider { 14 | 15 | public static Codec CODEC = RecordCodecBuilder.create(i -> i.group( 16 | ExtraCodecs.NUMBER.fieldOf("value").forGetter(ConstantNumberProvider::value) 17 | ).apply(i, ConstantNumberProvider::new)); 18 | 19 | 20 | @Override 21 | public long nextLong(@NotNull NumberSource numbers) { 22 | return value().longValue(); 23 | } 24 | 25 | @Override 26 | public double nextDouble(@NotNull NumberSource numbers) { 27 | return value().doubleValue(); 28 | } 29 | 30 | 31 | @AutoService(NumberProvider.Factory.class) 32 | public static final class Factory extends NumberProvider.Factory { 33 | public Factory() { 34 | super( 35 | NamespaceID.from("starlight:constant"), 36 | ConstantNumberProvider.class, 37 | ConstantNumberProvider.CODEC 38 | ); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/data/number/NumberProvider.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.data.number; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import com.mojang.serialization.Codec; 5 | import net.hollowcube.data.NumberSource; 6 | import net.hollowcube.dfu.DFUUtil; 7 | import net.hollowcube.dfu.ExtraCodecs; 8 | import net.hollowcube.registry.Registry; 9 | import net.hollowcube.registry.ResourceFactory; 10 | import net.minestom.server.utils.NamespaceID; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | /** 14 | * A source of numbers, see Number 15 | * Providers. 16 | *

17 | * Implementing a new number source requires a few steps: 18 | *

    19 | *
  1. Create a class inheriting from {@link NumberProvider}
  2. 20 | *
  3. Write a {@link Codec} for the class
  4. 21 | *
  5. Create an inner factory class inheriting from {@link NumberProvider.Factory}, filling in the required constructor params
  6. 22 | *
  7. Annotate the factory class with {@link com.google.auto.service.AutoService} for {@link NumberProvider.Factory}
  8. 23 | *
24 | *

25 | * Would like to look into a simplification of this process & removal of the Factory class. I would prefer if it was something like the following 26 | *

{@code
27 |  *     // This, where the annotation generates an entry for the superclass
28 |  *     @BasicRegistryItem("minecraft:constant")
29 |  *     public record ConstantNumberProvider(
30 |  *         Number value
31 |  *     ) implements NumberProvider {
32 |  *
33 |  *         Codec CODEC = ...;
34 |  *
35 |  *         // Or alternatively something like this, where it finds the descriptor element
36 |  *         Descriptor DESCRIPTOR = new Descriptor("minecraft:constant", CODEC){}
37 |  *
38 |  *     }
39 |  * }
40 | * 41 | * @see ConstantNumberProvider 42 | */ 43 | public interface NumberProvider { 44 | 45 | static @NotNull NumberProvider constant(@NotNull Number value) { 46 | return new ConstantNumberProvider(value); 47 | } 48 | 49 | Codec CODEC = Codec.either( 50 | Factory.CODEC.dispatch(Factory::from, Factory::codec), 51 | // Handle the case of providing a single value value. If serialized back it will 52 | // come out as a full {"type": "constant", "value": ...} 53 | ExtraCodecs.NUMBER.xmap(ConstantNumberProvider::new, ConstantNumberProvider::value) 54 | ).xmap(DFUUtil::value, Either::left); 55 | 56 | 57 | // Impl 58 | 59 | long nextLong(@NotNull NumberSource numbers); 60 | 61 | double nextDouble(@NotNull NumberSource numbers); 62 | 63 | 64 | abstract class Factory extends ResourceFactory { 65 | static Registry REGISTRY = Registry.service("number_providers", NumberProvider.Factory.class); 66 | static Registry.Index, NumberProvider.Factory> TYPE_REGISTRY = REGISTRY.index(NumberProvider.Factory::type); 67 | 68 | public static final Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.get(ns), Factory::name); 69 | 70 | public Factory(NamespaceID namespace, Class type, Codec codec) { 71 | super(namespace, type, codec); 72 | } 73 | 74 | static @NotNull Factory from(@NotNull NumberProvider provider) { 75 | return TYPE_REGISTRY.get(provider.getClass()); 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/dfu/DFUUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.dfu; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import com.mojang.datafixers.util.Pair; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | 12 | public final class DFUUtil { 13 | 14 | public static Map pairListToMap(List> pairList) { 15 | return pairList.stream().collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); 16 | } 17 | 18 | public static List> mapToPairList(Map map) { 19 | return map.entrySet().stream().map(entry -> new Pair<>(entry.getKey(), entry.getValue())).toList(); 20 | } 21 | 22 | public static @NotNull T value(@NotNull Either either) { 23 | return either.map(Function.identity(), Function.identity()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/dfu/ExtraCodecs.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.dfu; 2 | 3 | import com.mojang.datafixers.util.Pair; 4 | import com.mojang.serialization.Codec; 5 | import com.mojang.serialization.DataResult; 6 | import com.mojang.serialization.DynamicOps; 7 | import com.mojang.serialization.MapCodec; 8 | import com.mojang.serialization.codecs.PrimitiveCodec; 9 | import net.minestom.server.instance.block.Block; 10 | import net.minestom.server.item.Material; 11 | import net.minestom.server.potion.PotionEffect; 12 | import net.minestom.server.utils.NamespaceID; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import java.util.Locale; 17 | import java.util.function.Supplier; 18 | 19 | public final class ExtraCodecs { 20 | private ExtraCodecs() { 21 | } 22 | 23 | public static final PrimitiveCodec NUMBER = new PrimitiveCodec<>() { 24 | @Override 25 | public DataResult read(DynamicOps ops, T input) { 26 | return ops.getNumberValue(input); 27 | } 28 | 29 | @Override 30 | public T write(DynamicOps ops, Number value) { 31 | return ops.createNumeric(value); 32 | } 33 | }; 34 | 35 | public static final Codec NAMESPACE_ID = Codec.STRING.xmap(NamespaceID::from, NamespaceID::asString); 36 | 37 | public static final Codec MATERIAL = Codec.STRING.xmap(Material::fromNamespaceId, Material::name); 38 | 39 | public static final Codec POTION_EFFECT = Codec.STRING.xmap(PotionEffect::fromNamespaceId, PotionEffect::name); 40 | 41 | public static final Codec BLOCK = Codec.STRING.xmap(Block::fromNamespaceId, Block::name); 42 | 43 | public static @NotNull MapCodec string(@NotNull String name, @Nullable String defaultValue) { 44 | return Codec.STRING.optionalFieldOf(name, defaultValue); 45 | } 46 | 47 | public static > @NotNull Codec forEnum(Class type) { 48 | return Codec.STRING.xmap(s -> Enum.valueOf(type, s.toUpperCase(Locale.ROOT)), e -> e.name().toLowerCase(Locale.ROOT)); 49 | } 50 | 51 | public static @NotNull Codec lazy(Supplier> init) { 52 | return new LazyCodec<>(init); 53 | } 54 | 55 | 56 | public static class LazyCodec implements Codec { 57 | private final Supplier> init; 58 | private Codec value = null; 59 | 60 | LazyCodec(Supplier> init) { 61 | this.init = init; 62 | } 63 | 64 | @Override 65 | public DataResult> decode(DynamicOps ops, T1 input) { 66 | if (value == null) value = init.get(); 67 | return value.decode(ops, input); 68 | } 69 | 70 | @Override 71 | public DataResult encode(T input, DynamicOps ops, T1 prefix) { 72 | if (value == null) value = init.get(); 73 | return value.encode(input, ops, prefix); 74 | } 75 | } 76 | 77 | 78 | } 79 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/lang/LanguageProvider.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.lang; 2 | 3 | import net.hollowcube.util.ComponentUtil; 4 | import net.kyori.adventure.text.Component; 5 | import net.kyori.adventure.text.TextReplacementConfig; 6 | import net.kyori.adventure.text.TranslatableComponent; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.util.List; 12 | import java.util.Properties; 13 | import java.util.regex.Pattern; 14 | 15 | /** 16 | * Naive component translation system. 17 | *

18 | * Should be replaced with adventure translation or something else in the future. 19 | */ 20 | public class LanguageProvider { 21 | private static final Properties properties = new Properties(); 22 | 23 | static { 24 | try (InputStream is = LanguageProvider.class.getResourceAsStream("/lang/en_US.properties")) { 25 | if (is != null) { 26 | properties.load(is); 27 | } 28 | } catch (IOException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | 33 | private static final Pattern ARG_PATTERN = Pattern.compile("\\{[0-9]+}"); 34 | 35 | /** 36 | * Translates a component (if possible, see below). 37 | *

38 | * If the component is a {@link TranslatableComponent}, it will attempt to be translated. Any arguments in the 39 | * component will also be templated into the translation using the {@link java.text.MessageFormat} syntax of `{0}`, 40 | * `{1}`, etc. Translations are parsed using MiniMessage, and may contain styling as such. 41 | *

42 | * Translations are always (for now) loaded from `/lang/en_US.properties` within the classpath. This system is 43 | * temporary, and will be replaced with either a proxy translation system or using the Adventure translation system. 44 | * The problem with the adventure translation system is that it does not support MiniMessage in translation strings 45 | * as far as I can tell. 46 | * 47 | * @param component The component to translate 48 | * @return The component, or a component holding just the translation key if not found 49 | */ 50 | public static @NotNull Component get(@NotNull Component component) { 51 | if (!(component instanceof TranslatableComponent translatable)) { 52 | return component; 53 | } 54 | String value = properties.getProperty(translatable.key()); 55 | if (value == null) return Component.text(translatable.key()); 56 | Component translated = ComponentUtil.fromStringSafe(value); 57 | List args = translatable.args(); 58 | if (args.size() != 0) { 59 | //todo this seems like it could be wildly slow... 60 | translated = translated.replaceText(TextReplacementConfig.builder() 61 | .match(ARG_PATTERN) 62 | .replacement((result, builder) -> { 63 | var group = result.group(); 64 | int index = Integer.parseInt(group.substring(1, group.length() - 1)); 65 | return index < args.size() ? 66 | args.get(index) : 67 | Component.text("$$" + index); 68 | }).build()); 69 | } 70 | return translated; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/logging/Logger.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.logging; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * SLF4J wrapper with a sane API for MDC. 9 | *

10 | * Context variables are important for searching within log tools such as Loki (grafana) and the default usage of MDC is 11 | * extremely verbose and error-prone. 12 | *

13 | * Create using {@link LoggerFactory}. 14 | */ 15 | public interface Logger { 16 | 17 | void debug(@NotNull String message); 18 | 19 | void debug(@NotNull String message, @NotNull Map context); 20 | 21 | void info(@NotNull String message); 22 | 23 | void info(@NotNull String message, @NotNull Map context); 24 | 25 | void warn(@NotNull String message); 26 | 27 | void warn(@NotNull String message, @NotNull Map context); 28 | 29 | void error(@NotNull String message); 30 | 31 | void error(@NotNull String message, @NotNull Map context); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/logging/LoggerFactory.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.logging; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public final class LoggerFactory { 6 | private LoggerFactory() { 7 | } 8 | 9 | public static @NotNull Logger getLogger(@NotNull Class clazz) { 10 | return new LoggerImpl(org.slf4j.LoggerFactory.getLogger(clazz)); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/logging/LoggerImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.logging; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.slf4j.MDC; 5 | 6 | import java.util.Map; 7 | 8 | record LoggerImpl(org.slf4j.Logger delegate) implements Logger { 9 | 10 | @Override 11 | public void debug(@NotNull String message) { 12 | debug(message, Map.of()); 13 | } 14 | 15 | @Override 16 | public void debug(@NotNull String message, @NotNull Map context) { 17 | if (!delegate.isDebugEnabled()) return; 18 | 19 | for (var entry : context.entrySet()) { 20 | put(entry.getKey(), entry.getValue()); 21 | } 22 | 23 | delegate.debug(message); 24 | 25 | for (var key : context.keySet()) { 26 | remove(key); 27 | } 28 | } 29 | 30 | 31 | @Override 32 | public void info(@NotNull String message) { 33 | info(message, Map.of()); 34 | } 35 | 36 | @Override 37 | public void info(@NotNull String message, @NotNull Map context) { 38 | if (!delegate.isInfoEnabled()) return; 39 | 40 | for (var entry : context.entrySet()) { 41 | put(entry.getKey(), entry.getValue()); 42 | } 43 | 44 | delegate.info(message); 45 | 46 | for (var key : context.keySet()) { 47 | remove(key); 48 | } 49 | } 50 | 51 | @Override 52 | public void warn(@NotNull String message) { 53 | warn(message, Map.of()); 54 | } 55 | 56 | @Override 57 | public void warn(@NotNull String message, @NotNull Map context) { 58 | if (!delegate.isWarnEnabled()) return; 59 | 60 | for (var entry : context.entrySet()) { 61 | put(entry.getKey(), entry.getValue()); 62 | } 63 | 64 | delegate.warn(message); 65 | 66 | for (var key : context.keySet()) { 67 | remove(key); 68 | } 69 | } 70 | 71 | @Override 72 | public void error(@NotNull String message) { 73 | error(message, Map.of()); 74 | } 75 | 76 | @Override 77 | public void error(@NotNull String message, @NotNull Map context) { 78 | if (!delegate.isErrorEnabled()) return; 79 | 80 | for (var entry : context.entrySet()) { 81 | put(entry.getKey(), entry.getValue()); 82 | } 83 | 84 | delegate.error(message); 85 | 86 | for (var key : context.keySet()) { 87 | remove(key); 88 | } 89 | } 90 | 91 | private void put(String key, Object value) { 92 | MDC.put(key, value == null ? null : value.toString()); 93 | } 94 | 95 | private void remove(String key) { 96 | MDC.remove(key); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/mongo/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * MongoDB utilities to work with the other systems present. 3 | *

4 | * `common` does not have a runtime dependency on mongodb, so this package will not bring along any extra baggage. 5 | * However, any package using this module must bring along mongodb-driver-sync. 6 | */ 7 | package net.hollowcube.mongo; -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/registry/MapRegistry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.registry; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.jetbrains.annotations.UnknownNullability; 6 | 7 | import java.util.Collection; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | 12 | class MapRegistry implements Registry { 13 | private final Map delegate; 14 | 15 | public MapRegistry(Map resources) { 16 | this.delegate = resources; 17 | } 18 | 19 | @Override 20 | public @Nullable T getRaw(String namespace) { 21 | return delegate.get(namespace); 22 | } 23 | 24 | @Override 25 | public @NotNull Collection values() { 26 | return delegate.values(); 27 | } 28 | 29 | @Override 30 | public int size() { 31 | return delegate.size(); 32 | } 33 | 34 | @Override 35 | public @NotNull Index index(Function mapper) { 36 | Map index = values().stream().collect(Collectors.toMap(mapper, i -> i)); 37 | return new MapIndex<>(index); 38 | } 39 | 40 | 41 | static class MapIndex implements Index { 42 | private final Map delegate; 43 | 44 | MapIndex(Map delegate) { 45 | this.delegate = delegate; 46 | } 47 | 48 | @Override 49 | public @UnknownNullability T get(K key) { 50 | return delegate.get(key); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/registry/MissingEntryException.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.registry; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class MissingEntryException extends RuntimeException { 6 | private final @NotNull Registry registry; 7 | private final @NotNull String key; 8 | 9 | public MissingEntryException(@NotNull Registry registry, @NotNull String key) { 10 | super("Missing registry entry: " + key + " in " + registry + "!"); 11 | this.registry = registry; 12 | this.key = key; 13 | } 14 | 15 | public @NotNull Registry registry() { 16 | return registry; 17 | } 18 | 19 | public @NotNull String key() { 20 | return key; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/registry/Resource.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.registry; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonParser; 5 | import net.kyori.adventure.key.Key; 6 | import net.kyori.adventure.key.Keyed; 7 | import net.minestom.server.utils.NamespaceID; 8 | import org.jetbrains.annotations.Contract; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.io.BufferedReader; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.InputStreamReader; 18 | import java.nio.charset.Charset; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.stream.Collectors; 22 | 23 | public interface Resource extends Keyed { 24 | 25 | @Contract(pure = true) 26 | @NotNull NamespaceID namespace(); 27 | 28 | @Override 29 | default @NotNull Key key() { 30 | return namespace(); 31 | } 32 | 33 | @Contract(pure = true) 34 | default @NotNull String name() { 35 | return namespace().asString(); 36 | } 37 | 38 | 39 | interface Id extends Resource { 40 | 41 | @Contract(pure = true) 42 | int id(); 43 | 44 | } 45 | 46 | 47 | Path DATA_PATH = Path.of(System.getProperty("starlight.data.dir", "data")); 48 | 49 | /** 50 | * Attempts to load a resource file with the following priorities 51 | *

    52 | *
  1. Read from `jar://data/{name}`
  2. 53 | *
  3. Read from `{DATA_PATH}/{name}`
  4. 54 | *
  5. null
  6. 55 | *
56 | * 57 | * @param name The resource name and extension to load 58 | * @return The resource file content, or null if missing. 59 | */ 60 | static @Nullable String load(@NotNull String name) { 61 | InputStream packaged = Resource.class.getClassLoader().getResourceAsStream("data/" + name); 62 | if (packaged != null) { 63 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(packaged, Charset.defaultCharset()))) { 64 | return reader.lines().collect(Collectors.joining("\n")); 65 | } catch (IOException e) { 66 | Logger logger = LoggerFactory.getLogger(Resource.class); 67 | logger.warn("Found resource {} in classpath, but failed to load it", name, e); 68 | } 69 | } 70 | 71 | Path external = DATA_PATH.resolve(name); 72 | if (Files.exists(external) && Files.isRegularFile(external)) { 73 | try (BufferedReader reader = Files.newBufferedReader(external)) { 74 | return reader.lines().collect(Collectors.joining("\n")); 75 | } catch (IOException e) { 76 | Logger logger = LoggerFactory.getLogger(Resource.class); 77 | logger.warn("Found resource {} in external data, but failed to load it", name, e); 78 | } 79 | } 80 | 81 | return null; 82 | } 83 | 84 | /** 85 | * Loads a resource using the semantics of {@link #load(String)}, but the resource must be a json file with an array 86 | * at the root. If the file cannot be found, an empty array will be returned. 87 | */ 88 | static @NotNull JsonArray loadJsonArray(@NotNull String resource) { 89 | String content = load(resource); 90 | if (content == null) 91 | return new JsonArray(); 92 | 93 | return JsonParser.parseString(content).getAsJsonArray(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/registry/ResourceFactory.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.registry; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.minestom.server.utils.NamespaceID; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public abstract class ResourceFactory implements Resource { 8 | private final NamespaceID namespace; 9 | private final Class type; 10 | private final Codec codec; 11 | 12 | public ResourceFactory(NamespaceID namespace, Class type, Codec codec) { 13 | this.namespace = namespace; 14 | this.type = type; 15 | this.codec = codec; 16 | } 17 | 18 | @Override 19 | public @NotNull NamespaceID namespace() { 20 | return this.namespace; 21 | } 22 | 23 | public @NotNull Class type() { 24 | return this.type; 25 | } 26 | 27 | public @NotNull Codec codec() { 28 | return this.codec; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/server/Facet.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server; 2 | 3 | import net.minestom.server.ServerProcess; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * A concept for loading modules of the server, similar-ish to extensions. 8 | *

9 | * Facets are defined, then loaded using the service provider interface by the controlling server (development, 10 | * production, etc). 11 | *

12 | * All facets must have a public no-args constructor. There may be other constructors present (eg for use in 13 | * tests). 14 | *

15 | * Common registration functions are provided on {@link ServerWrapper}. These should be used over accessing the 16 | * {@link ServerProcess} directly, because they can be transparently extended to support unloading facets if this ever 17 | * becomes a desired behavior (and they have some utilities). 18 | */ 19 | public interface Facet { 20 | 21 | void hook(@NotNull ServerWrapper server); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/server/IsolatedServerWrapper.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server; 2 | 3 | import net.minestom.server.ServerProcess; 4 | import net.minestom.server.command.builder.Command; 5 | import net.minestom.server.event.EventNode; 6 | import net.minestom.server.instance.block.BlockHandler; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.function.Supplier; 11 | 12 | @SuppressWarnings("UnstableApiUsage") 13 | record IsolatedServerWrapper(@NotNull ServerProcess process) implements ServerWrapper { 14 | 15 | @Override 16 | public @Nullable F getFacet(@NotNull Class type) { 17 | return null; 18 | } 19 | 20 | @Override 21 | public void addEventNode(@NotNull EventNode node) { 22 | process.eventHandler().addChild(node); 23 | } 24 | 25 | @Override 26 | public void registerCommand(@NotNull Command command) { 27 | process.command().register(command); 28 | } 29 | 30 | @Override 31 | public void registerBlockHandler(@NotNull Supplier handlerSupplier) { 32 | process.block().registerHandler(handlerSupplier.get().getNamespaceId(), handlerSupplier); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/server/ServerWrapper.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server; 2 | 3 | import net.minestom.server.ServerProcess; 4 | import net.minestom.server.command.builder.Command; 5 | import net.minestom.server.event.EventNode; 6 | import net.minestom.server.instance.block.BlockHandler; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.function.Supplier; 11 | 12 | @SuppressWarnings("UnstableApiUsage") 13 | public interface ServerWrapper { 14 | 15 | static @NotNull ServerWrapper isolated(@NotNull ServerProcess process) { 16 | return new IsolatedServerWrapper(process); 17 | } 18 | 19 | /** 20 | * Access to the underlying server process. Should not be used if there is an alternative api present in this 21 | * class. 22 | */ 23 | @NotNull ServerProcess process(); 24 | 25 | /** 26 | * Fetches a {@link Facet} loaded on the server. 27 | *

28 | * Note: Load order is _not_ guaranteed. If this method is accessed during the facet loading phase of server start, 29 | * the target may not have been loaded yet. It will still be returned in this case. 30 | * 31 | * @return The facet if it is present on the server, otherwise null 32 | */ 33 | @Nullable F getFacet(@NotNull Class type); 34 | 35 | 36 | // Utility functions 37 | 38 | void addEventNode(@NotNull EventNode node); 39 | 40 | void registerCommand(@NotNull Command command); 41 | 42 | void registerBlockHandler(@NotNull Supplier handlerSupplier); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/server/instance/TickTrackingInstance.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.instance; 2 | 3 | import net.minestom.server.instance.IChunkLoader; 4 | import net.minestom.server.instance.InstanceContainer; 5 | import net.minestom.server.world.DimensionType; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.util.UUID; 10 | 11 | public class TickTrackingInstance extends InstanceContainer { 12 | //todo i (matt) really dont think this should be in common, but not sure where 13 | 14 | private long tick = 0; 15 | 16 | public TickTrackingInstance(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @Nullable IChunkLoader loader) { 17 | super(uniqueId, dimensionType, loader); 18 | } 19 | 20 | public TickTrackingInstance(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType) { 21 | super(uniqueId, dimensionType); 22 | } 23 | 24 | @Override 25 | public void tick(long time) { 26 | tick++; 27 | 28 | super.tick(time); 29 | } 30 | 31 | public long getTick() { 32 | return tick; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/util/BlockUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util; 2 | 3 | import net.minestom.server.instance.block.Block; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public final class BlockUtil { 7 | 8 | public static @NotNull Block withType(@NotNull Block block, @NotNull Block type) { 9 | return type.withHandler(block.handler()).withNbt(block.nbt()); 10 | } 11 | 12 | public static @NotNull String toStateString(@NotNull Block block) { 13 | if (block.properties().isEmpty()) 14 | return block.name(); 15 | 16 | var sb = new StringBuilder(); 17 | sb.append(block.name()).append("["); 18 | for (var entry : block.properties().entrySet()) { 19 | sb.append(entry.getKey()).append("=").append(entry.getValue()).append(","); 20 | } 21 | sb.deleteCharAt(sb.length() - 1); 22 | sb.append("]"); 23 | return sb.toString(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/util/ComponentUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util; 2 | 3 | import net.kyori.adventure.text.Component; 4 | import net.kyori.adventure.text.format.TextDecoration; 5 | import net.kyori.adventure.text.minimessage.MiniMessage; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class ComponentUtil { 9 | 10 | public static @NotNull Component fromStringSafe(@NotNull String content) { 11 | Component mm = MiniMessage.miniMessage().deserialize(content); 12 | return Component.text().decoration(TextDecoration.ITALIC, false).append(mm).asComponent(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/util/EventUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util; 2 | 3 | import net.hollowcube.Env; 4 | import net.minestom.server.MinecraftServer; 5 | import net.minestom.server.ServerProcess; 6 | import net.minestom.server.event.Event; 7 | import net.minestom.server.event.EventFilter; 8 | import net.minestom.server.event.EventNode; 9 | import net.minestom.server.event.trait.CancellableEvent; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.function.Predicate; 13 | 14 | public class EventUtil { 15 | 16 | /** 17 | * Creates an event node with the given name for any event which has not yet been cancelled. 18 | */ 19 | public static EventNode notCancelledNode(@NotNull String name) { 20 | return EventNode.event(name, EventFilter.ALL, Predicate.not(EventUtil::isCancelled)); 21 | } 22 | 23 | public static boolean isCancelled(Event event) { 24 | return event instanceof CancellableEvent c && c.isCancelled(); 25 | } 26 | 27 | /** 28 | * Call an event in a test-safe manner. 29 | */ 30 | public static void safeDispatch(@NotNull Event event) { 31 | ServerProcess process = MinecraftServer.process(); 32 | if (process == null && !Env.STRICT_MODE) 33 | return; 34 | process.eventHandler().call(event); 35 | } 36 | 37 | /** 38 | * Call an event in a test-safe manner. 39 | */ 40 | public static void safeDispatchCancellable(@NotNull Event event, @NotNull Runnable onSuccess) { 41 | ServerProcess process = MinecraftServer.process(); 42 | if (process == null && !Env.STRICT_MODE) 43 | return; 44 | process.eventHandler().callCancellable(event, onSuccess); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/util/FutureUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util; 2 | 3 | import org.jetbrains.annotations.Contract; 4 | 5 | public class FutureUtil { 6 | 7 | @Contract("_ -> null") 8 | @SuppressWarnings("TypeParameterUnusedInFormals") 9 | public static T handleException(Throwable throwable) { 10 | //todo log to sentry or something 11 | throwable.printStackTrace(); 12 | return null; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import org.jetbrains.annotations.Contract; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.jetbrains.annotations.UnknownNullability; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class JsonUtil { 15 | 16 | /** 17 | * Merge two {@link JsonElement}s recursively, combining arrays based on the "type" field. 18 | *

19 | * The resulting json element is not necessarily a deep copy if the returned item has not been modified. 20 | */ 21 | @Contract("_, _ -> new") 22 | public static @UnknownNullability JsonElement merge(@Nullable JsonElement left, @Nullable JsonElement right) { 23 | if (left == null) return right; 24 | if (right == null) return left; 25 | 26 | if (left.isJsonObject() && right.isJsonObject()) 27 | return merge(left.getAsJsonObject(), right.getAsJsonObject()); 28 | if (left.isJsonArray() && right.isJsonArray()) 29 | return merge(left.getAsJsonArray(), right.getAsJsonArray()); 30 | 31 | return right; 32 | } 33 | 34 | public static @NotNull JsonObject merge(@NotNull JsonObject left, @NotNull JsonObject right) { 35 | JsonObject merged = new JsonObject(); 36 | for (var entry : left.entrySet()) { 37 | merged.add(entry.getKey(), entry.getValue()); 38 | } 39 | for (var entry : right.entrySet()) { 40 | final String name = entry.getKey(); 41 | // If left had the value, merge the two. Otherwise, just add the new entry. 42 | if (merged.has(entry.getKey())) { 43 | merged.add(name, merge(merged.get(name), entry.getValue())); 44 | } else { 45 | merged.add(entry.getKey(), entry.getValue()); 46 | } 47 | } 48 | return merged; 49 | } 50 | 51 | public static @NotNull JsonArray merge(@NotNull JsonArray left, @NotNull JsonArray right) { 52 | JsonArray merged = new JsonArray(); 53 | 54 | //todo lots of duplication, very yikes 55 | Map temp = new HashMap<>(); 56 | for (JsonElement elem : left) { 57 | // If it isnt an object, we cannot merge. Move on 58 | if (!elem.isJsonObject()) { 59 | merged.add(elem); 60 | continue; 61 | } 62 | JsonObject obj = elem.getAsJsonObject(); 63 | // If it has a type field, we can merge. Otherwise, just add it. 64 | if (obj.has("type")) { 65 | temp.put(obj.get("type").getAsString(), elem); 66 | } else { 67 | merged.add(elem); 68 | } 69 | } 70 | 71 | for (JsonElement elem : right) { 72 | // If it isnt an object, we cannot merge. Move on 73 | if (!elem.isJsonObject()) { 74 | merged.add(elem); 75 | continue; 76 | } 77 | JsonObject obj = elem.getAsJsonObject(); 78 | 79 | // If there is not a string type, we cannot merge. Move on 80 | JsonElement type = obj.get("type"); 81 | if (!type.isJsonPrimitive()) { 82 | merged.add(elem); 83 | continue; 84 | } 85 | 86 | // If there is already an element of the same type, merge them. 87 | // Otherwise just add the element to temp map 88 | if (temp.containsKey(type.getAsString())) { 89 | temp.put(type.getAsString(), merge(temp.get(type.getAsString()), elem)); 90 | } else { 91 | temp.put(type.getAsString(), elem); 92 | } 93 | } 94 | 95 | // Copy all entries from temp map into temp 96 | for (var entry : temp.entrySet()) { 97 | merged.add(entry.getValue()); 98 | } 99 | 100 | return merged; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /modules/common/src/main/java/net/hollowcube/util/ParticleUtils.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.item.Material; 6 | import net.minestom.server.network.packet.server.ServerPacket; 7 | import net.minestom.server.particle.Particle; 8 | import net.minestom.server.particle.ParticleCreator; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public class ParticleUtils { 12 | 13 | public static void spawnBlockBreakParticles(@NotNull Player player, @NotNull Point point, @NotNull Material material) { 14 | ServerPacket packet = ParticleCreator.createParticlePacket( 15 | Particle.BLOCK, false, point.x(), point.y(), point.z(), 0.01f, 0.01f, 0.01f, 16 | 0.05f, 12, binaryWriter -> binaryWriter.writeVarInt(material.id())); 17 | player.sendPacketToViewersAndSelf(packet); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules/common/src/test/java/net/hollowcube/data/number/TestConstantNumberProvider.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.data.number; 2 | 3 | import net.hollowcube.data.NumberSource; 4 | import org.junit.jupiter.api.Test; 5 | import org.opentest4j.AssertionFailedError; 6 | 7 | import static com.google.common.truth.Truth.assertThat; 8 | 9 | public class TestConstantNumberProvider { 10 | 11 | @Test 12 | public void testHappyCase() { 13 | var source = NumberSource.constant(0); 14 | var provider = new ConstantNumberProvider(1.0); 15 | 16 | assertThat(provider.nextLong(source)).isEqualTo(1); 17 | } 18 | 19 | @Test 20 | public void testIgnoresSource() { 21 | var source = new NumberSource() { 22 | @Override 23 | public double random() { 24 | throw new AssertionFailedError("Should not be called."); 25 | } 26 | }; 27 | var provider = new ConstantNumberProvider(1.0); 28 | 29 | provider.nextLong(source); 30 | provider.nextDouble(source); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules/common/src/test/java/net/hollowcube/registry/TestMissingEntry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.registry; 2 | 3 | import com.google.gson.JsonParser; 4 | import com.mojang.serialization.JsonOps; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | public class TestMissingEntry { 10 | 11 | @Test 12 | public void testMissingRegistryEntry() { 13 | var json = JsonParser.parseString("{\"type\": \"missing\"}"); 14 | assertThrows(MissingEntryException.class, () -> JsonOps.INSTANCE.withDecoder(TestResource.CODEC).apply(json)); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /modules/common/src/test/java/net/hollowcube/registry/TestResource.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.registry; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.minestom.server.utils.NamespaceID; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public interface TestResource extends Resource { 8 | 9 | Codec CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); 10 | 11 | abstract class Factory extends ResourceFactory { 12 | static Registry REGISTRY = Registry.service("test_resource", Factory.class); 13 | static Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); 14 | 15 | static Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.required(ns), Factory::name); 16 | 17 | public Factory(NamespaceID namespace, Class type, Codec codec) { 18 | super(namespace, type, codec); 19 | } 20 | 21 | public static @NotNull Factory from(@NotNull TestResource resource) { 22 | return TYPE_REGISTRY.get(resource.getClass()); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /modules/common/src/test/resources/lang/en_US.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/common/src/test/resources/lang/en_US.properties -------------------------------------------------------------------------------- /modules/common/src/test/resources/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "test:one": { 3 | "string": "hello" 4 | } 5 | } -------------------------------------------------------------------------------- /modules/dev/README.md: -------------------------------------------------------------------------------- 1 | ## Background 2 | This is a common package for libraries which will be used by more than a single game/mode on the HollowCube network. 3 | 4 | ## Setup 5 | 6 | * Install Docker 7 | * Navigate to https://docs.docker.com/engine/install/ and follow the steps for your operating system 8 | 9 | ## Testing with databases 10 | 11 | ### Intellij 12 | * Navigate to /common/docker-compose.yml 13 | * Press the start icon next to services 14 | 15 | ### Terminal 16 | * Navigate to /common/ 17 | * Run `docker compose up -d` to start the docker container 18 | * Remove the `-d` flag if you'd like the output in the terminal, otherwise it will run in the background 19 | * Run `docker compose down` to kill the docker container 20 | # common -------------------------------------------------------------------------------- /modules/dev/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation("org.postgresql:postgresql:42.5.0") 3 | implementation(project(":modules:instances")) 4 | implementation("com.github.hollow-cube:Minestom:c6c97162a6") 5 | } -------------------------------------------------------------------------------- /modules/dev/src/main/java/net/hollowcube/dev/Main.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.dev; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.coordinate.Pos; 5 | import net.minestom.server.entity.Player; 6 | import net.minestom.server.event.GlobalEventHandler; 7 | import net.minestom.server.event.player.PlayerLoginEvent; 8 | import net.minestom.server.extras.MojangAuth; 9 | import net.minestom.server.instance.InstanceContainer; 10 | import net.minestom.server.instance.InstanceManager; 11 | import net.minestom.server.instance.block.Block; 12 | 13 | import java.sql.*; 14 | 15 | public class Main { 16 | 17 | public static void main(String[] args) throws SQLException { 18 | MinecraftServer server = MinecraftServer.init(); 19 | MojangAuth.init(); 20 | server.start("0.0.0.0", 25565); 21 | 22 | InstanceManager instanceManager = MinecraftServer.getInstanceManager(); 23 | // Create the instance 24 | InstanceContainer instanceContainer = instanceManager.createInstanceContainer(); 25 | // Set the ChunkGenerator 26 | instanceContainer.setGenerator(unit -> 27 | unit.modifier().fillHeight(0, 40, Block.GRASS_BLOCK)); 28 | // Add an event callback to specify the spawning instance (and the spawn position) 29 | GlobalEventHandler globalEventHandler = MinecraftServer.getGlobalEventHandler(); 30 | globalEventHandler.addListener(PlayerLoginEvent.class, event -> { 31 | final Player player = event.getPlayer(); 32 | event.setSpawningInstance(instanceContainer); 33 | player.setRespawnPoint(new Pos(0, 42, 0)); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/instances/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | implementation("com.github.luben:zstd-jni:1.5.2-2") 4 | 5 | implementation("com.amazonaws:aws-java-sdk-s3:1.12.364") 6 | implementation("javax.xml.bind:jaxb-api:2.3.1") // Performance improvement for s3 7 | 8 | implementation("net.anumbrella.seaweedfs:seaweedfs-java-client:0.0.2.RELEASE") 9 | 10 | implementation("org.postgresql:postgresql:42.5.1") 11 | 12 | } 13 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/World.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world; 2 | 3 | import net.minestom.server.instance.Instance; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface World { 9 | 10 | @NotNull String id(); 11 | 12 | @NotNull Instance instance(); 13 | 14 | // Management 15 | 16 | @NotNull CompletableFuture<@NotNull String> saveWorld(); 17 | 18 | @NotNull CompletableFuture unloadWorld(); 19 | 20 | default @NotNull CompletableFuture saveAndUnloadWorld() { 21 | return saveWorld().thenCompose(unused -> unloadWorld()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/WorldManager.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world; 2 | 3 | import net.hollowcube.world.storage.FileStorage; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public class WorldManager { 7 | 8 | private final FileStorage fileStorage; 9 | 10 | public WorldManager(@NotNull FileStorage fileStorage) { 11 | this.fileStorage = fileStorage; 12 | } 13 | 14 | public @NotNull FileStorage fileStorage() { 15 | return fileStorage; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/compression/CompressedWorldData.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.compression; 2 | 3 | public class CompressedWorldData { 4 | //the byte array that contains all compressed world files 5 | private final byte[] compressedData; 6 | //the original world data size; needed for decompression 7 | private final int originalSize; 8 | 9 | public CompressedWorldData(byte[] compressedData, int originalSize) { 10 | this.compressedData = compressedData; 11 | this.originalSize = originalSize; 12 | } 13 | 14 | public byte[] getCompressedData() { 15 | return this.compressedData; 16 | } 17 | 18 | public int getOriginalSize() { 19 | return this.originalSize; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/compression/WorldCompressor.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.compression; 2 | 3 | import com.github.luben.zstd.Zstd; 4 | import net.hollowcube.world.util.FileUtil; 5 | import net.minestom.server.world.DimensionType; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.File; 9 | import java.io.FileInputStream; 10 | import java.io.IOException; 11 | import java.util.List; 12 | import java.util.zip.Deflater; 13 | import java.util.zip.ZipEntry; 14 | import java.util.zip.ZipOutputStream; 15 | 16 | public class WorldCompressor { 17 | // valid values: 0-22 (0 = no compression; 22 = highest compression) 18 | private static final int COMPRESSION_LEVEL = 22; 19 | /** 20 | * Compresses the region files of the given world using the Zstandard algorithm 21 | * 22 | * @param worldname The worlds name whose files you want to compress 23 | * @param dimensionType The worlds dimension type 24 | * @return A CompressedWorldData object that contains the compressed world data 25 | * and original data size (needed for decompression) 26 | */ 27 | public static CompressedWorldData compressWorldRegionFiles(final String worldname, final DimensionType dimensionType) { 28 | // get correct path to region files 29 | String path = worldname + "/region"; //TODO figure out correct world file path 30 | /**switch (dimensionType) { 31 | case NORMAL: 32 | path = Bukkit.getWorldContainer().toPath() + "//" + worldname + "//region"; 33 | break; 34 | case NETHER: 35 | path = Bukkit.getWorldContainer().toPath() + "//" + worldname + "//DIM-1//region"; 36 | break; 37 | case THE_END: 38 | path = Bukkit.getWorldContainer().toPath() + "//" + worldname + "//DIM1//region"; 39 | break; 40 | }**/ 41 | // get the region folder 42 | File folder = new File(path); 43 | // put all .mca region files into a list 44 | List fileList = FileUtil.getFileList(folder); 45 | // create ByteArrayOutputStream and ZipOutputStream to put all region files into an uncompressed zip file 46 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 47 | ZipOutputStream zipFile = new ZipOutputStream(bos); 48 | // set compression level of the zipping process to 0, zstandard will do the real compression 49 | zipFile.setLevel(Deflater.NO_COMPRESSION); 50 | // byte array that will contain the zipped data 51 | byte[] zippedData = null; 52 | try { 53 | // loop through all region files 54 | for (File file : fileList) { 55 | // create FileInputStream that reads in the region file 56 | FileInputStream fis = new FileInputStream(file); 57 | // get exact file path of the region file 58 | String zipFilePath = file.getCanonicalPath().substring(folder.getCanonicalPath().length() + 1, file.getCanonicalPath().length()); 59 | // put it into the zip archive 60 | ZipEntry zipEntry = new ZipEntry(zipFilePath); 61 | zipFile.putNextEntry(zipEntry); 62 | // write the file into the zip archive 63 | byte[] bytes = new byte[1024]; 64 | int length; 65 | while ((length = fis.read(bytes)) >= 0) { 66 | zipFile.write(bytes, 0, length); 67 | } 68 | // close entry and FileInputStream (important: we dont want unreleased ressources! not closing it will cause memory leaks!) 69 | zipFile.closeEntry(); 70 | fis.close(); 71 | } 72 | // close zip output stream 73 | zipFile.close(); 74 | // put zip data into the byte array 75 | zippedData = bos.toByteArray(); 76 | // close ByteArrayOutputStream 77 | bos.close(); 78 | } catch (IOException ex) { 79 | ex.printStackTrace(); 80 | } 81 | // placeholder value 82 | int zippedSize = zippedData.length; 83 | // return a new CompressedWorldData object that contains the zstandard 84 | // compressed zip data and the original zipped data size 85 | 86 | return new CompressedWorldData(Zstd.compress(zippedData, COMPRESSION_LEVEL), zippedSize); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/decompression/RegionFileData.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.decompression; 2 | 3 | public class RegionFileData { 4 | //contents of the region file 5 | private final byte[] data; 6 | //name of the region file 7 | private final String fileName; 8 | 9 | public RegionFileData(byte[] data, String fileName) { 10 | this.data = data; 11 | this.fileName = fileName; 12 | } 13 | 14 | public byte[] getData() { 15 | return this.data; 16 | } 17 | 18 | public String getFileName() { 19 | return this.fileName; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/dimension/DimensionTypes.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.dimension; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.utils.NamespaceID; 5 | import net.minestom.server.world.DimensionType; 6 | 7 | public final class DimensionTypes { 8 | private DimensionTypes() {} 9 | 10 | public static final DimensionType FULL_BRIGHT = DimensionType.builder(NamespaceID.from("bright_dim")) 11 | .ultrawarm(false) 12 | .natural(true) 13 | .piglinSafe(false) 14 | .respawnAnchorSafe(false) 15 | .bedSafe(true) 16 | .raidCapable(true) 17 | .skylightEnabled(true) 18 | .ceilingEnabled(false) 19 | .fixedTime(null) 20 | .ambientLight(2.0f) 21 | .height(384) 22 | .minY(-64) 23 | .logicalHeight(384) 24 | .infiniburn(NamespaceID.from("minecraft:infiniburn_overworld")) 25 | .build(); 26 | 27 | static { 28 | MinecraftServer.getDimensionTypeManager().addDimension(FULL_BRIGHT); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/event/PlayerInstanceLeaveEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.event; 2 | 3 | import net.minestom.server.entity.Player; 4 | import net.minestom.server.event.trait.PlayerInstanceEvent; 5 | import net.minestom.server.instance.Instance; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | /** 9 | * Triggered when a player leaves an instance 10 | */ 11 | public record PlayerInstanceLeaveEvent( 12 | @NotNull Player player, 13 | @NotNull Instance instance 14 | ) implements PlayerInstanceEvent { 15 | 16 | @Override 17 | public @NotNull Player getPlayer() { 18 | return player; 19 | } 20 | 21 | @Override 22 | public @NotNull Instance getInstance() { 23 | return instance; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/event/PlayerSpawnInInstanceEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.event; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.event.EventDispatcher; 6 | import net.minestom.server.event.player.PlayerSpawnEvent; 7 | import net.minestom.server.event.trait.PlayerInstanceEvent; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | /** 11 | * Like PlayerSpawnEvent, but implements InstanceEvent. 12 | */ 13 | public record PlayerSpawnInInstanceEvent( 14 | @NotNull Player player 15 | ) implements PlayerInstanceEvent { 16 | 17 | static { 18 | // Register a handler for this event globally, always. 19 | MinecraftServer.getGlobalEventHandler().addListener(PlayerSpawnEvent.class, event -> { 20 | EventDispatcher.call(new PlayerSpawnInInstanceEvent(event.getPlayer())); 21 | }); 22 | } 23 | 24 | @Override 25 | public @NotNull Player getPlayer() { 26 | return player; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/generation/FlatGenerator.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.generation; 2 | 3 | import net.minestom.server.instance.block.Block; 4 | import net.minestom.server.instance.generator.GenerationUnit; 5 | import net.minestom.server.instance.generator.Generator; 6 | import net.minestom.server.instance.generator.UnitModifier; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | class FlatGenerator implements Generator { 10 | 11 | @Override 12 | public void generate(@NotNull GenerationUnit unit) { 13 | UnitModifier modifier = unit.modifier(); 14 | modifier.fillHeight(0, 1, Block.BEDROCK); 15 | modifier.fillHeight(1, 5, Block.DIRT); 16 | modifier.fillHeight(5, 6, Block.GRASS_BLOCK); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/generation/MapGenerators.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.generation; 2 | 3 | import net.minestom.server.instance.generator.Generator; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public final class MapGenerators { 7 | private MapGenerators() {} 8 | 9 | private static final Generator VOID = new VoidGenerator(); 10 | private static final Generator FLAT = new FlatGenerator(); 11 | private static final Generator STONE = new StoneGenerator(); 12 | 13 | public static @NotNull Generator voidWorld() { 14 | return VOID; 15 | } 16 | 17 | public static @NotNull Generator flatWorld() { 18 | return FLAT; 19 | } 20 | 21 | public static @NotNull Generator stoneWorld() { 22 | return STONE; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/generation/StoneGenerator.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.generation; 2 | 3 | import net.minestom.server.instance.block.Block; 4 | import net.minestom.server.instance.generator.GenerationUnit; 5 | import net.minestom.server.instance.generator.Generator; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | class StoneGenerator implements Generator { 9 | 10 | @Override 11 | public void generate(@NotNull GenerationUnit unit) { 12 | unit.modifier().fillHeight(0, 4, Block.STONE); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/generation/VoidGenerator.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.generation; 2 | 3 | import net.minestom.server.instance.generator.GenerationUnit; 4 | import net.minestom.server.instance.generator.Generator; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class VoidGenerator implements Generator { 8 | @Override 9 | public void generate(@NotNull GenerationUnit unit) { 10 | // Minestom worlds are void by default, so don't do anything 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/storage/FileStorage.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.storage; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.InputStream; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface FileStorage { 9 | 10 | /** 11 | * Uploads the given data to the storage under the given path. Returns the unique identifier of the file. 12 | * Not necessarily the same as the path. 13 | */ 14 | @NotNull CompletableFuture<@NotNull String> uploadFile(@NotNull String path, @NotNull InputStream data, int size); 15 | 16 | // @NotNull CompletableFuture updateFile(@NotNull String path, @NotNull InputStream data, int size); 17 | 18 | @NotNull CompletableFuture downloadFile(@NotNull String path); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/storage/FileStorageMemory.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.storage; 2 | 3 | import net.hollowcube.world.storage.FileStorage; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.InputStream; 8 | import java.util.Map; 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | public class FileStorageMemory implements FileStorage { 13 | private final Map files = new ConcurrentHashMap<>(); 14 | 15 | @Override 16 | public @NotNull CompletableFuture<@NotNull String> uploadFile(@NotNull String path, @NotNull InputStream data, int size) { 17 | try { 18 | files.put(path, data.readAllBytes()); 19 | return CompletableFuture.completedFuture(path); 20 | } catch (Exception e) { 21 | return CompletableFuture.failedFuture(e); 22 | } 23 | } 24 | 25 | @Override 26 | public @NotNull CompletableFuture downloadFile(@NotNull String path) { 27 | if (!files.containsKey(path)) 28 | return CompletableFuture.failedFuture(new Exception("File not found")); 29 | return CompletableFuture.completedFuture(new ByteArrayInputStream(files.get(path))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/storage/FileStorageS3.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.storage; 2 | 3 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 4 | import com.amazonaws.auth.BasicAWSCredentials; 5 | import com.amazonaws.client.builder.AwsClientBuilder; 6 | import com.amazonaws.services.s3.AmazonS3; 7 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 8 | import com.amazonaws.services.s3.model.ObjectMetadata; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.io.InputStream; 12 | import java.util.concurrent.CompletableFuture; 13 | 14 | public record FileStorageS3( 15 | @NotNull AmazonS3 s3 16 | ) implements FileStorage { 17 | 18 | public static @NotNull FileStorage connect(@NotNull String address, @NotNull String accessKey, @NotNull String secretKey) { 19 | 20 | 21 | 22 | // s3://accessKey:secretKey@address/bucket 23 | var credentials = new BasicAWSCredentials(accessKey, secretKey); 24 | var s3 = AmazonS3ClientBuilder.standard() 25 | .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(address, "us-east-1")) 26 | .withCredentials(new AWSStaticCredentialsProvider(credentials)) 27 | .enablePathStyleAccess() 28 | .build(); 29 | System.out.println(s3.listBuckets()); 30 | return new FileStorageS3(s3); 31 | } 32 | 33 | 34 | public static void main(String[] args) { 35 | // var storage = connect("http://localhost:9000/", "DTprdE3DBZ7vG8wQ", "qByxgkPV7rO7zo12KmRUkikSBMwYJCRj"); 36 | // var data = "Hello World!".getBytes(); 37 | // storage.uploadFile("test.txt", new ByteArrayInputStream(data), data.length).join(); 38 | } 39 | 40 | @Override 41 | public @NotNull CompletableFuture<@NotNull String> uploadFile(@NotNull String path, @NotNull InputStream data, int size) { 42 | var metadata = new ObjectMetadata(); 43 | metadata.setContentLength(size); 44 | s3.putObject("mapmaker", path, data, metadata); 45 | return CompletableFuture.completedFuture(path); //todo do this properly (in the future, handle errors) 46 | } 47 | 48 | @Override 49 | public @NotNull CompletableFuture downloadFile(@NotNull String path) { 50 | var object = s3.getObject("mapmaker", path); 51 | return CompletableFuture.completedFuture(object.getObjectContent()); //todo make async 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/instances/src/main/java/net/hollowcube/world/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.world.util; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.nio.file.FileVisitResult; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.SimpleFileVisitor; 11 | import java.nio.file.attribute.BasicFileAttributes; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public final class FileUtil { 16 | private FileUtil() { 17 | } 18 | 19 | public static void deleteDirectory(@NotNull Path directory) throws IOException { 20 | Files.walkFileTree(directory, new SimpleFileVisitor<>() { 21 | @Override 22 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 23 | Files.delete(dir); 24 | return FileVisitResult.CONTINUE; 25 | } 26 | 27 | @Override 28 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 29 | Files.delete(file); 30 | return FileVisitResult.CONTINUE; 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * Get an ArrayList that contains all the files from the specified folder 37 | * 38 | * @param folder The folder you want all the files from 39 | * @return An ArrayList containing all the files of the specified folder 40 | */ 41 | public static @NotNull List getFileList(final File folder) { 42 | // create a new empty arraylist 43 | ArrayList fileList = new ArrayList<>(); 44 | // create an array containing all the files from the folder 45 | File[] files = folder.listFiles(); 46 | // loop through all the files 47 | for (File file : files) { 48 | // check if the file is not another folder (only files get added) 49 | if (!file.isDirectory()) 50 | fileList.add(file); // add the file to the arraylist 51 | } 52 | return fileList; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modules/mql/README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repository is not being updated and should not be used going forward. 3 | > 4 | > This module has been moved to its own repository [here](https://github.com/hollow-cube/mql). 5 | 6 | # Minecraft Query Language (mql) 7 | 8 | A subset of MoLang (may eventually be a full implementation). Available as an interpreter or a JIT compiled mode. 9 | 10 | ## Background 11 | 12 | ## Install 13 | 14 | Artifacts are published on Jitpack for now. Add the following to your `build.gradle(.kts)`: 15 | 16 | > Note: {VERSION} should be replaced with the latest version on Jitpack, you can find 17 | > this [here](https://jitpack.io/#hollow-cube/common). 18 | 19 | ```kotlin 20 | dependencies { 21 | implementation("com.github.hollow-cube.common:mql:{VERSION}") 22 | } 23 | ``` 24 | 25 | ## Syntax 26 | 27 | `mql` supports the following syntax 28 | 29 | * Query functions 30 | * Math & Comparison operators (`+`, `*`, `==`, etc) 31 | 32 | ## Usage 33 | 34 | See the [docs](https://github.com/hollow-cube/common/wiki/Basic-Usage). 35 | 36 | ## Future Plans 37 | 38 | * Unify the interpreter and compiler apis 39 | * Allows for fallback if using unsupported JIT features, permission issues, etc. 40 | * Temp variables 41 | * Public variables/querying other scripts 42 | * Other data types & functions 43 | 44 | ## Contributing 45 | 46 | Issues and PRs are welcome! Please refer to [CONTRIBUTING.md](../../CONTRIBUTING.md) for more information. 47 | 48 | ## License 49 | 50 | This project is licensed under the [MIT License](../../LICENSE). 51 | -------------------------------------------------------------------------------- /modules/mql/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | dependencies { 6 | implementation("org.ow2.asm:asm:9.4") 7 | implementation("org.ow2.asm:asm-util:9.4") 8 | } -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/MqlScript.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql; 2 | 3 | import net.hollowcube.mql.parser.MqlParser; 4 | import net.hollowcube.mql.runtime.MqlScope; 5 | import net.hollowcube.mql.tree.MqlExpr; 6 | import net.hollowcube.mql.tree.MqlNumberExpr; 7 | import net.hollowcube.mql.value.MqlNumberValue; 8 | import net.hollowcube.mql.value.MqlValue; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public record MqlScript(@NotNull MqlExpr expr) { 12 | 13 | // public static final Codec CODEC = Codec.either(Codec.STRING, Codec.STRING.listOf()) 14 | // .xmap(either -> DFUUtil.value(either.mapRight(lines -> String.join("\n", lines))), Either::left) 15 | // .xmap(MqlScript::parse, unused -> {throw new RuntimeException("cannot serialize an mql script");}); 16 | 17 | public static @NotNull MqlScript parse(@NotNull String script) { 18 | if (script.trim().isEmpty()) // Empty script is always the number 1 19 | return new MqlScript(new MqlNumberExpr(1)); 20 | MqlExpr expr = new MqlParser(script).parse(); 21 | return new MqlScript(expr); 22 | } 23 | 24 | public double evaluate(@NotNull MqlScope scope) { 25 | MqlValue result = expr().evaluate(scope); 26 | if (result instanceof MqlNumberValue num) 27 | return num.value(); 28 | return 0.0; 29 | } 30 | 31 | public boolean evaluateToBool(@NotNull MqlScope scope) { 32 | MqlValue result = expr().evaluate(scope); 33 | if (result instanceof MqlNumberValue num) 34 | return num.value() != 0; 35 | return result != MqlValue.NULL; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/foreign/MqlForeignTypes.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.foreign; 2 | 3 | import net.hollowcube.mql.value.MqlNumberValue; 4 | import net.hollowcube.mql.value.MqlValue; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.UnknownNullability; 7 | 8 | /** 9 | * Conversion utility between java types and MQL types. 10 | *

11 | * todo not sure if other types should be convertable. I think just doubles is fine. 12 | */ 13 | public class MqlForeignTypes { 14 | 15 | public static @NotNull MqlValue toMql(@UnknownNullability Object javaValue) { 16 | if (javaValue == null) return MqlValue.NULL; 17 | if (javaValue instanceof MqlValue value) 18 | return value; 19 | if (javaValue instanceof Double value) 20 | return new MqlNumberValue(value); 21 | // if (javaValue instanceof Float value) 22 | // return new MqlNumberValue(value); 23 | // if (javaValue instanceof Long value) 24 | // return new MqlNumberValue(value); 25 | // if (javaValue instanceof Integer value) 26 | // return new MqlNumberValue(value); 27 | // if (javaValue instanceof Short value) 28 | // return new MqlNumberValue(value); 29 | // if (javaValue instanceof Byte value) 30 | // return new MqlNumberValue(value); 31 | if (javaValue instanceof Boolean value) 32 | return MqlValue.from(value); 33 | throw new RuntimeException("cannot convert " + javaValue.getClass().getSimpleName() + " to mql value"); 34 | } 35 | 36 | public static @UnknownNullability Object fromMql(@NotNull MqlValue value, @NotNull Class targetType) { 37 | if (value instanceof MqlNumberValue numberValue) { 38 | if (Double.class.equals(targetType) || double.class.equals(targetType)) { 39 | return numberValue.value(); 40 | // } else if (Float.class.equals(targetType) || float.class.equals(targetType)) { 41 | // return (float) numberValue.value(); 42 | // } else if (Long.class.equals(targetType) || long.class.equals(targetType)) { 43 | // return (long) numberValue.value(); 44 | // } else if (Integer.class.equals(targetType) || int.class.equals(targetType)) { 45 | // return (int) numberValue.value(); 46 | // } else if (Short.class.equals(targetType) || short.class.equals(targetType)) { 47 | // return (short) numberValue.value(); 48 | // } else if (Byte.class.equals(targetType) || byte.class.equals(targetType)) { 49 | // return (byte) numberValue.value(); 50 | } else if (Boolean.class.equals(targetType) || boolean.class.equals(targetType)) { 51 | return numberValue.value() != 0; 52 | } 53 | throw new RuntimeException("cannot convert number " + targetType.getSimpleName()); 54 | } 55 | throw new RuntimeException("cannot convert " + value.getClass().getSimpleName() + " to " + targetType.getSimpleName()); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/foreign/Query.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.foreign; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.METHOD) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Query { 11 | String value() default ""; 12 | } 13 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/jit/AsmUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.objectweb.asm.ClassReader; 5 | import org.objectweb.asm.MethodVisitor; 6 | import org.objectweb.asm.Opcodes; 7 | import org.objectweb.asm.tree.ClassNode; 8 | import org.objectweb.asm.tree.InsnList; 9 | import org.objectweb.asm.util.Textifier; 10 | import org.objectweb.asm.util.TraceMethodVisitor; 11 | 12 | import java.io.PrintWriter; 13 | import java.io.StringWriter; 14 | import java.lang.reflect.Method; 15 | 16 | public final class AsmUtil { 17 | private AsmUtil() { 18 | } 19 | 20 | public static @NotNull String toDescriptor(@NotNull Class clazz) { 21 | if (boolean.class.equals(clazz)) { 22 | return "Z"; 23 | } else if (byte.class.equals(clazz)) { 24 | return "B"; 25 | } else if (char.class.equals(clazz)) { 26 | return "C"; 27 | } else if (short.class.equals(clazz)) { 28 | return "S"; 29 | } else if (int.class.equals(clazz)) { 30 | return "I"; 31 | } else if (long.class.equals(clazz)) { 32 | return "J"; 33 | } else if (float.class.equals(clazz)) { 34 | return "F"; 35 | } else if (double.class.equals(clazz)) { 36 | return "D"; 37 | } else if (void.class.equals(clazz)) { 38 | return "V"; 39 | } 40 | return "L" + toName(clazz) + ";"; 41 | } 42 | 43 | public static @NotNull String toDescriptor(@NotNull Method method) { 44 | var sb = new StringBuilder("("); 45 | for (var param : method.getParameterTypes()) { 46 | sb.append(toDescriptor(param)); 47 | } 48 | sb.append(")"); 49 | sb.append(toDescriptor(method.getReturnType())); 50 | return sb.toString(); 51 | } 52 | 53 | public static @NotNull String toName(@NotNull Class clazz) { 54 | return clazz.getName().replace(".", "/"); 55 | } 56 | 57 | public static void convert(Class to, Class from, MethodVisitor mv) { 58 | if (to.isAssignableFrom(from)) { 59 | return; 60 | } 61 | //todo handle other conversions 62 | throw new UnsupportedOperationException("Cannot convert from " + from + " to " + to); 63 | } 64 | 65 | @SuppressWarnings("all") 66 | public static Class loadClass(String className, byte[] b) { 67 | // Override defineClass (as it is protected) and define the class. 68 | Class clazz = null; 69 | try { 70 | ClassLoader loader = ClassLoader.getSystemClassLoader(); 71 | Class cls = (Class) Class.forName("java.lang.ClassLoader"); 72 | java.lang.reflect.Method method = 73 | cls.getDeclaredMethod( 74 | "defineClass", 75 | new Class[]{String.class, byte[].class, int.class, int.class}); 76 | 77 | // Protected method invocation. 78 | method.setAccessible(true); 79 | try { 80 | Object[] args = 81 | new Object[]{className, b, 0, b.length}; 82 | clazz = (Class) method.invoke(loader, args); 83 | } finally { 84 | method.setAccessible(false); 85 | } 86 | } catch (Exception e) { 87 | e.printStackTrace(); 88 | System.exit(1); 89 | } 90 | return clazz; 91 | } 92 | 93 | public static @NotNull String prettyPrintEvalMethod(byte[] bytecode) { 94 | var str = new StringBuilder(); 95 | var printer = new Textifier(); 96 | var mp = new TraceMethodVisitor(printer); 97 | 98 | var cr = new ClassReader(bytecode); 99 | var cn = new ClassNode(); 100 | cr.accept(cn, 0); 101 | for (var method : cn.methods) { 102 | // Ignore any method that isnt evaluate and not a bridge (we want the concrete impl) 103 | if (!"evaluate".equals(method.name) || (method.access & Opcodes.ACC_BRIDGE) != 0) 104 | continue; 105 | InsnList insns = method.instructions; 106 | for (var insn : insns) { 107 | insn.accept(mp); 108 | StringWriter sw = new StringWriter(); 109 | printer.print(new PrintWriter(sw)); 110 | printer.getText().clear(); 111 | str.append(sw.toString().trim()).append("\n"); 112 | } 113 | } 114 | 115 | return str.toString(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/jit/MqlEnv.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.PARAMETER) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface MqlEnv { 11 | String[] value(); 12 | } 13 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/jit/MqlQueryScript.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | /** 4 | * An example MQL script interface. May be used if only a query object is required, otherwise serves as documentation. 5 | *

6 | * A script interface is a java interface with the following properties: 7 | *

    8 | *
  • Must be public, non-sealed.
  • 9 | *
  • Must have a single abstract method which returns a double ({@link FunctionalInterface} can enforce this at compile time, but is not required)
  • 10 | *
  • The abstract method may have 1 or more parameters, but all must be annotated with {@link MqlEnv}.
  • 11 | *
  • The abstract method may not have generic parameters.
  • 12 | *
  • The interface may use generic parameters, but they must be passed to the {@link MqlCompiler}
  • 13 | *
14 | */ 15 | @FunctionalInterface 16 | public interface MqlQueryScript { 17 | double evaluate(@MqlEnv({"query", "q"}) Query query); 18 | } 19 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/jit/MqlRuntime.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | public final class MqlRuntime { 4 | private MqlRuntime() { 5 | } 6 | 7 | public static double ternary(double condition, double ifTrue, double ifFalse) { 8 | return condition != 0 ? ifTrue : ifFalse; 9 | } 10 | 11 | public static double gte(double lhs, double rhs) { 12 | return lhs >= rhs ? 1 : 0; 13 | } 14 | 15 | public static double ge(double lhs, double rhs) { 16 | return lhs > rhs ? 1 : 0; 17 | } 18 | 19 | public static double lte(double lhs, double rhs) { 20 | return lhs <= rhs ? 1 : 0; 21 | } 22 | 23 | public static double le(double lhs, double rhs) { 24 | return lhs < rhs ? 1 : 0; 25 | } 26 | 27 | public static double eq(double lhs, double rhs) { 28 | return lhs == rhs ? 1 : 0; 29 | } 30 | 31 | public static double neq(double lhs, double rhs) { 32 | return lhs != rhs ? 1 : 0; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/parser/MqlParseError.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.parser; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class MqlParseError extends RuntimeException { 6 | 7 | public MqlParseError(@NotNull String message) { 8 | super(message); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/parser/MqlToken.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.parser; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | record MqlToken(@NotNull Type type, int start, int end) { 6 | 7 | enum Type { 8 | PLUS, MINUS, STAR, SLASH, 9 | LPAREN, RPAREN, 10 | DOT, COMMA, COLON, QUESTION, QUESTIONQUESTION, 11 | GTE, GE, LTE, LE, EQ, NEQ, 12 | NUMBER, IDENT; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/runtime/MqlRuntimeError.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.runtime; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class MqlRuntimeError extends RuntimeException { 6 | public MqlRuntimeError(@NotNull String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/runtime/MqlScope.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.runtime; 2 | 3 | import net.hollowcube.mql.value.MqlHolder; 4 | import net.hollowcube.mql.value.MqlValue; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public interface MqlScope extends MqlHolder { 8 | 9 | MqlScope EMPTY = unused -> MqlValue.NULL; 10 | 11 | @NotNull MqlValue get(@NotNull String name); 12 | 13 | interface Mutable extends MqlScope { 14 | 15 | void set(@NotNull String name, @NotNull MqlValue value); 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/runtime/MqlScopeImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.runtime; 2 | 3 | import net.hollowcube.mql.value.MqlValue; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class MqlScopeImpl implements MqlScope { 10 | protected final Map data = new HashMap<>(); 11 | 12 | @Override 13 | public @NotNull MqlValue get(@NotNull String name) { 14 | return data.getOrDefault(name, MqlValue.NULL); 15 | } 16 | 17 | 18 | public static class Mutable extends MqlScopeImpl implements MqlScope.Mutable { 19 | 20 | @Override 21 | public void set(@NotNull String name, @NotNull MqlValue value) { 22 | data.put(name, value); 23 | } 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/runtime/MqlScriptScope.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.runtime; 2 | 3 | import net.hollowcube.mql.value.MqlValue; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public class MqlScriptScope implements MqlScope { 7 | 8 | private final MqlScope query; 9 | private final MqlScope.Mutable actor; 10 | private final MqlScope context; 11 | private final MqlScope.Mutable temp = new MqlScopeImpl.Mutable(); 12 | 13 | public MqlScriptScope(@NotNull MqlScope query, @NotNull Mutable actor, @NotNull MqlScope context) { 14 | this.query = query; 15 | this.actor = actor; 16 | this.context = context; 17 | } 18 | 19 | @Override 20 | public @NotNull MqlValue get(@NotNull String name) { 21 | return switch (name) { 22 | case "math", "m", "Math" -> MqlMath.INSTANCE; 23 | case "query", "q" -> query; 24 | case "temp", "t" -> temp; 25 | case "variable", "v" -> actor; 26 | case "context", "c" -> context; 27 | default -> throw new MqlRuntimeError("unknown environment object: " + name); 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlAccessExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlHolder; 5 | import net.hollowcube.mql.value.MqlValue; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public record MqlAccessExpr( 9 | @NotNull MqlExpr lhs, String target) implements MqlExpr { 10 | 11 | @Override 12 | public MqlValue evaluate(@NotNull MqlScope scope) { 13 | var lhs = lhs().evaluate(scope).cast(MqlHolder.class); 14 | return lhs.get(target()); 15 | } 16 | 17 | @Override 18 | public R visit(@NotNull MqlVisitor visitor, P p) { 19 | return visitor.visitAccessExpr(this, p); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlArgListExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlValue; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.List; 8 | 9 | public record MqlArgListExpr(List args) implements MqlExpr { 10 | @Override 11 | public MqlValue evaluate(@NotNull MqlScope scope) { 12 | return null; 13 | } 14 | 15 | @Override 16 | public R visit(@NotNull MqlVisitor visitor, P p) { 17 | return visitor.visitArgListExpr(this, p); 18 | } 19 | 20 | public int size() { 21 | return args().size(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlBinaryExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlNumberValue; 5 | import net.hollowcube.mql.value.MqlValue; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public record MqlBinaryExpr( 9 | @NotNull Op operator, 10 | @NotNull MqlExpr lhs, 11 | @NotNull MqlExpr rhs 12 | ) implements MqlExpr { 13 | public enum Op { 14 | PLUS, 15 | MINUS, 16 | DIV, 17 | MUL, 18 | NULL_COALESCE, 19 | GTE, 20 | GE, 21 | LTE, 22 | LE, 23 | EQ, 24 | NEQ 25 | } 26 | 27 | @Override 28 | public MqlValue evaluate(@NotNull MqlScope scope) { 29 | // Handle lazy evaluation of null coalescing lhs and rhs 30 | if (operator() == Op.NULL_COALESCE) { 31 | var lhs = lhs().evaluate(scope).cast(MqlNumberValue.class); 32 | if (lhs.value() != 0) { 33 | return lhs; 34 | } 35 | 36 | return rhs().evaluate(scope); 37 | } 38 | 39 | // The rest use both lhs and rhs always 40 | MqlNumberValue lhs = lhs().evaluate(scope).cast(MqlNumberValue.class); 41 | MqlNumberValue rhs = rhs().evaluate(scope).cast(MqlNumberValue.class); 42 | 43 | return switch (operator()) { 44 | case PLUS -> new MqlNumberValue(lhs.value() + rhs.value()); 45 | case MINUS -> new MqlNumberValue(lhs.value() - rhs.value()); 46 | case DIV -> new MqlNumberValue(lhs.value() / rhs.value()); 47 | case MUL -> new MqlNumberValue(lhs.value() * rhs.value()); 48 | case GTE -> new MqlNumberValue(lhs.value() >= rhs.value() ? 1 : 0); 49 | case GE -> new MqlNumberValue(lhs.value() > rhs.value() ? 1 : 0); 50 | case LTE -> new MqlNumberValue(lhs.value() <= rhs.value() ? 1 : 0); 51 | case LE -> new MqlNumberValue(lhs.value() < rhs.value() ? 1 : 0); 52 | case EQ -> new MqlNumberValue(lhs.value() == rhs.value() ? 1 : 0); 53 | case NEQ -> new MqlNumberValue(lhs.value() != rhs.value() ? 1 : 0); 54 | case NULL_COALESCE -> throw new RuntimeException("unreachable"); 55 | }; 56 | } 57 | 58 | @Override 59 | public R visit(@NotNull MqlVisitor visitor, P p) { 60 | return visitor.visitBinaryExpr(this, p); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlCallExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlCallable; 5 | import net.hollowcube.mql.value.MqlNumberValue; 6 | import net.hollowcube.mql.value.MqlValue; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public record MqlCallExpr(@NotNull MqlAccessExpr access, @NotNull MqlArgListExpr argList) implements MqlExpr { 10 | @Override 11 | public MqlValue evaluate(@NotNull MqlScope scope) { 12 | var callable = access().evaluate(scope).cast(MqlCallable.class); 13 | return callable.call(argList.args(), scope).cast(MqlNumberValue.class); 14 | } 15 | 16 | @Override 17 | public R visit(@NotNull MqlVisitor visitor, P p) { 18 | return visitor.visitCallExpr(this, p); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlValue; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public sealed interface MqlExpr permits MqlAccessExpr, MqlArgListExpr, MqlBinaryExpr, MqlCallExpr, MqlIdentExpr, MqlNumberExpr, MqlTernaryExpr, MqlUnaryExpr { 8 | 9 | MqlValue evaluate(@NotNull MqlScope scope); 10 | 11 | R visit(@NotNull MqlVisitor visitor, P p); 12 | } 13 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlIdentExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlValue; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public record MqlIdentExpr(@NotNull String value) implements MqlExpr { 8 | 9 | @Override 10 | public MqlValue evaluate(@NotNull MqlScope scope) { 11 | return scope.get(value); 12 | } 13 | 14 | @Override 15 | public R visit(@NotNull MqlVisitor visitor, P p) { 16 | return visitor.visitRefExpr(this, p); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlNumberExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlNumberValue; 5 | import net.hollowcube.mql.value.MqlValue; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public record MqlNumberExpr(@NotNull MqlNumberValue value) implements MqlExpr { 9 | 10 | public MqlNumberExpr(double value) { 11 | this(new MqlNumberValue(value)); 12 | } 13 | 14 | @Override 15 | public MqlValue evaluate(@NotNull MqlScope scope) { 16 | return value; 17 | } 18 | 19 | @Override 20 | public R visit(@NotNull MqlVisitor visitor, P p) { 21 | return visitor.visitNumberExpr(this, p); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlTernaryExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlNumberValue; 5 | import net.hollowcube.mql.value.MqlValue; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public record MqlTernaryExpr( 9 | @NotNull MqlExpr condition, 10 | @NotNull MqlExpr trueCase, 11 | @NotNull MqlExpr falseCase 12 | ) implements MqlExpr { 13 | 14 | @Override 15 | public MqlValue evaluate(@NotNull MqlScope scope) { 16 | var condition = condition().evaluate(scope).cast(MqlNumberValue.class); 17 | return condition.value() != 0 ? trueCase().evaluate(scope) : falseCase().evaluate(scope); 18 | } 19 | 20 | @Override 21 | public R visit(@NotNull MqlVisitor visitor, P p) { 22 | return visitor.visitTernaryExpr(this, p); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlUnaryExpr.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.value.MqlNumberValue; 5 | import net.hollowcube.mql.value.MqlValue; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public record MqlUnaryExpr( 9 | @NotNull Op operator, 10 | @NotNull MqlExpr rhs 11 | ) implements MqlExpr { 12 | public enum Op { 13 | NEGATE, 14 | } 15 | 16 | @Override 17 | public MqlValue evaluate(@NotNull MqlScope scope) { 18 | MqlNumberValue rhs = rhs().evaluate(scope).cast(MqlNumberValue.class); 19 | 20 | return switch (operator()) { 21 | case NEGATE -> new MqlNumberValue(-rhs.value()); 22 | }; 23 | } 24 | 25 | @Override 26 | public R visit(@NotNull MqlVisitor visitor, P p) { 27 | return visitor.visitUnaryExpr(this, p); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/tree/MqlVisitor.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.tree; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | // @formatter:off 6 | public interface MqlVisitor { 7 | 8 | default R visitBinaryExpr(@NotNull MqlBinaryExpr expr, P p) { 9 | return defaultValue(); 10 | } 11 | 12 | default R visitUnaryExpr(@NotNull MqlUnaryExpr expr, P p) { 13 | return defaultValue(); 14 | } 15 | 16 | default R visitAccessExpr(@NotNull MqlAccessExpr expr, P p) { 17 | return defaultValue(); 18 | } 19 | 20 | default R visitNumberExpr(@NotNull MqlNumberExpr expr, P p) { 21 | return defaultValue(); 22 | } 23 | 24 | default R visitRefExpr(@NotNull MqlIdentExpr expr, P p) { 25 | return defaultValue(); 26 | } 27 | 28 | default R visitArgListExpr(MqlArgListExpr mqlArgListExpr, P p) { 29 | return defaultValue(); 30 | } 31 | 32 | ; 33 | 34 | default R visitTernaryExpr(MqlTernaryExpr expr, P p) { 35 | return defaultValue(); 36 | } 37 | 38 | default R visitCallExpr(MqlCallExpr expr, P p) { 39 | return defaultValue(); 40 | } 41 | 42 | default R visit(@NotNull MqlExpr expr, P p) { 43 | return expr.visit(this, p); 44 | } 45 | 46 | default R defaultValue() { 47 | return null; 48 | } 49 | } 50 | // @formatter:on 51 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/util/MqlPrinter.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.util; 2 | 3 | import net.hollowcube.mql.tree.*; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public class MqlPrinter implements MqlVisitor { 7 | 8 | @Override 9 | public String visitBinaryExpr(@NotNull MqlBinaryExpr expr, Void unused) { 10 | return String.format( 11 | "(%s %s %s)", 12 | switch (expr.operator()) { 13 | case PLUS -> "+"; 14 | case MINUS -> "-"; 15 | case MUL -> "*"; 16 | case DIV -> "/"; 17 | case NULL_COALESCE -> "??"; 18 | case GTE -> ">="; 19 | case GE -> ">"; 20 | case LTE -> "<="; 21 | case LE -> "<"; 22 | case EQ -> "=="; 23 | case NEQ -> "!="; 24 | }, 25 | visit(expr.lhs(), null), 26 | visit(expr.rhs(), null) 27 | ); 28 | } 29 | 30 | @Override 31 | public String visitUnaryExpr(@NotNull MqlUnaryExpr expr, Void unused) { 32 | return String.format( 33 | "(%s %s)", 34 | switch (expr.operator()) { 35 | case NEGATE -> "-"; 36 | }, 37 | visit(expr.rhs(), null) 38 | ); 39 | } 40 | 41 | @Override 42 | public String visitAccessExpr(@NotNull MqlAccessExpr expr, Void unused) { 43 | return String.format( 44 | "(. %s %s)", 45 | visit(expr.lhs(), null), 46 | expr.target() 47 | ); 48 | } 49 | 50 | @Override 51 | public String visitNumberExpr(@NotNull MqlNumberExpr expr, Void unused) { 52 | return String.valueOf(expr.value()); 53 | } 54 | 55 | @Override 56 | public String visitRefExpr(@NotNull MqlIdentExpr expr, Void unused) { 57 | return expr.value(); 58 | } 59 | 60 | @Override 61 | public String visitArgListExpr(@NotNull MqlArgListExpr expr, Void unused) { 62 | StringBuilder sb = new StringBuilder(); 63 | sb.append("("); 64 | 65 | for (int i = 0; i < expr.args().size(); i++) { 66 | sb.append(visit(expr.args().get(i), null)); 67 | if (i != expr.args().size() - 1) { 68 | sb.append(" "); 69 | } 70 | } 71 | 72 | sb.append(")"); 73 | return sb.toString(); 74 | } 75 | 76 | @Override 77 | public String visitTernaryExpr(MqlTernaryExpr expr, Void unused) { 78 | return String.format( 79 | "(? %s %s %s)", 80 | visit(expr.condition(), null), 81 | visit(expr.trueCase(), null), 82 | visit(expr.falseCase(), null) 83 | ); 84 | } 85 | 86 | @Override 87 | public String visitCallExpr(MqlCallExpr expr, Void unused) { 88 | return String.format( 89 | "(? %s %s)", 90 | visit(expr.access(), null), 91 | visit(expr.argList(), null) 92 | ); 93 | } 94 | 95 | @Override 96 | public String defaultValue() { 97 | return "##Error"; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/util/StringUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.util; 2 | 3 | import org.intellij.lang.annotations.Language; 4 | 5 | public final class StringUtil { 6 | private StringUtil() { 7 | } 8 | 9 | private static final @Language("regexp") String CAMEL_TO_SNAKE_CASE_REGEX = "([a-z])([A-Z]+)"; 10 | private static final String CAMEL_TO_SNAKE_CASE_REPLACEMENT = "$1_$2"; 11 | 12 | public static String camelCaseToSnakeCase(String str) { 13 | return str.replaceAll(CAMEL_TO_SNAKE_CASE_REGEX, CAMEL_TO_SNAKE_CASE_REPLACEMENT).toLowerCase(); 14 | } 15 | 16 | public static String snakeCaseToCamelCase(String str) { 17 | while (str.contains("_")) { 18 | str = str.replaceFirst("_[a-z]", String.valueOf(Character.toUpperCase(str.charAt(str.indexOf("_") + 1)))); 19 | } 20 | return str; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/value/MqlCallable.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.value; 2 | 3 | import net.hollowcube.mql.runtime.MqlScope; 4 | import net.hollowcube.mql.tree.MqlExpr; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.util.List; 9 | 10 | @FunctionalInterface 11 | public non-sealed interface MqlCallable extends MqlValue { 12 | 13 | /** 14 | * Returns the arity of the function, or -1 if it is variadic/otherwise unknown 15 | */ 16 | default int arity() { 17 | return -1; 18 | } 19 | 20 | @NotNull MqlValue call(@NotNull List args, @Nullable MqlScope scope); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/value/MqlHolder.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.value; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public non-sealed interface MqlHolder extends MqlValue { 6 | 7 | @NotNull MqlValue get(@NotNull String name); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/value/MqlIdentValue.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.value; 2 | 3 | import org.jetbrains.annotations.ApiStatus; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | @ApiStatus.Internal 7 | public record MqlIdentValue(@NotNull String value) implements MqlValue { 8 | } 9 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/value/MqlNumberValue.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.value; 2 | 3 | public record MqlNumberValue(double value) implements MqlValue { 4 | 5 | @Override 6 | public String toString() { 7 | return String.valueOf(value); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/mql/src/main/java/net/hollowcube/mql/value/MqlValue.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.value; 2 | 3 | import net.hollowcube.mql.runtime.MqlRuntimeError; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * Mutable marker for any possible mql value. 8 | */ 9 | public sealed interface MqlValue permits MqlCallable, MqlHolder, MqlIdentValue, MqlNumberValue { 10 | MqlValue NULL = new MqlNumberValue(0.0); 11 | 12 | static @NotNull MqlValue from(boolean bool) { 13 | return new MqlNumberValue(bool ? 1 : 0); 14 | } 15 | 16 | static @NotNull MqlValue from(double dbl) { 17 | return new MqlNumberValue(dbl); 18 | } 19 | 20 | default Target cast(@NotNull Class targetType) { 21 | if (targetType.isInstance(this)) 22 | //noinspection unchecked 23 | return (Target) this; 24 | throw new MqlRuntimeError("cannot cast " + this.getClass().getSimpleName() + " to " + targetType.getSimpleName()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/foreign/TestMqlForeignFunctions.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.foreign; 2 | 3 | import net.hollowcube.mql.tree.MqlNumberExpr; 4 | import net.hollowcube.mql.value.MqlCallable; 5 | import net.hollowcube.mql.value.MqlValue; 6 | import org.junit.jupiter.api.Test; 7 | import org.testcontainers.shaded.com.google.common.util.concurrent.AtomicDouble; 8 | 9 | import java.lang.reflect.Method; 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | import static com.google.common.truth.Truth.assertThat; 14 | 15 | public class TestMqlForeignFunctions { 16 | 17 | private static final AtomicBoolean test1Called = new AtomicBoolean(false); 18 | 19 | public static void test1() { 20 | test1Called.set(true); 21 | } 22 | 23 | @Test 24 | public void emptyVoidFunction() throws Exception { 25 | Method method = getClass().getMethod("test1"); 26 | MqlCallable function = MqlForeignFunctions.createForeign(method, null); 27 | assertThat(function.arity()).isEqualTo(0); 28 | assertThat(function.call(List.of(), null)).isEqualTo(MqlValue.NULL); 29 | assertThat(test1Called.get()).isTrue(); 30 | } 31 | 32 | private static final AtomicDouble test2Value = new AtomicDouble(0); 33 | 34 | public static void test2(double value) { 35 | test2Value.set(value); 36 | } 37 | 38 | @Test 39 | public void singleArgVoidFunction() throws Exception { 40 | Method method = getClass().getMethod("test2", double.class); 41 | MqlCallable function = MqlForeignFunctions.createForeign(method, null); 42 | MqlValue result = function.call(List.of(new MqlNumberExpr(10.5)), null); 43 | 44 | assertThat(function.arity()).isEqualTo(1); 45 | assertThat(result).isEqualTo(MqlValue.NULL); 46 | assertThat(test2Value.get()).isEqualTo(10.5); 47 | } 48 | 49 | public static double test3() { 50 | return 10.5; 51 | } 52 | 53 | @Test 54 | public void emptyNonVoidFunction() throws Exception { 55 | Method method = getClass().getMethod("test3"); 56 | MqlCallable function = MqlForeignFunctions.createForeign(method, null); 57 | MqlValue result = function.call(List.of(), null); 58 | 59 | assertThat(function.arity()).isEqualTo(0); 60 | assertThat(result).isEqualTo(MqlValue.from(10.5)); 61 | } 62 | 63 | public static double test4(double a, double b) { 64 | return a + b; 65 | } 66 | 67 | @Test 68 | public void multiParamNonVoidFunction() throws Exception { 69 | Method method = getClass().getMethod("test4", double.class, double.class); 70 | MqlCallable function = MqlForeignFunctions.createForeign(method, null); 71 | MqlValue result = function.call(List.of(new MqlNumberExpr(10.5), new MqlNumberExpr(20.5)), null); 72 | 73 | assertThat(function.arity()).isEqualTo(2); 74 | assertThat(result).isEqualTo(MqlValue.from(31)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/jit/BaseScript.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | public interface BaseScript { 4 | double evaluate(); 5 | } 6 | -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/jit/QueryScript.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | public interface QueryScript { 4 | double evaluate(@MqlEnv({"query", "q"}) QueryTest query); 5 | } 6 | -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/jit/QueryTest.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | import net.hollowcube.mql.foreign.Query; 4 | 5 | public class QueryTest { 6 | 7 | @Query 8 | public double dbl(double value) { 9 | return value * 2; 10 | } 11 | 12 | @Query 13 | public double mul(double a, double b) { 14 | return a * b; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/jit/TestCompilation.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | public class TestCompilation { 9 | 10 | @Test 11 | public void singleNumber() { 12 | check(BaseScript.class, "0", """ 13 | DCONST_0 14 | DRETURN 15 | """); 16 | check(BaseScript.class, "1", """ 17 | DCONST_1 18 | DRETURN 19 | """); 20 | check(BaseScript.class, "1.234", """ 21 | LDC 1.234 22 | DRETURN 23 | """); 24 | } 25 | 26 | @Test 27 | public void simpleAddition() { 28 | check(BaseScript.class, "1 + 1", """ 29 | DCONST_1 30 | DCONST_1 31 | DADD 32 | DRETURN 33 | """); 34 | } 35 | 36 | @Test 37 | public void callQuerySingleArg() { 38 | check(QueryScript.class, "q.dbl(1.0)", """ 39 | ALOAD 1 40 | DCONST_1 41 | INVOKEVIRTUAL net/hollowcube/mql/jit/QueryTest.dbl (D)D 42 | DRETURN 43 | """); 44 | } 45 | 46 | @Test 47 | public void callQueryMultiArg() { 48 | check(QueryScript.class, "q.mul(1.0, 2.0)", """ 49 | ALOAD 1 50 | DCONST_1 51 | LDC 2.0 52 | INVOKEVIRTUAL net/hollowcube/mql/jit/QueryTest.mul (DD)D 53 | DRETURN 54 | """); 55 | } 56 | 57 | @Test 58 | public void callQueryInnerCall() { 59 | check(QueryScript.class, "q.mul(2.0, q.dbl(2.0))", """ 60 | ALOAD 1 61 | LDC 2.0 62 | ALOAD 1 63 | LDC 2.0 64 | INVOKEVIRTUAL net/hollowcube/mql/jit/QueryTest.dbl (D)D 65 | INVOKEVIRTUAL net/hollowcube/mql/jit/QueryTest.mul (DD)D 66 | DRETURN 67 | """); 68 | } 69 | 70 | private void check(@NotNull Class script, @NotNull String source, @NotNull String expected) { 71 | var compiler = new MqlCompiler<>(script); 72 | byte[] bytecode = compiler.compileBytecode("mql$test", source); 73 | 74 | var str = AsmUtil.prettyPrintEvalMethod(bytecode); 75 | assertEquals(expected, str); 76 | } 77 | } -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/jit/TestExecution.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | public class TestExecution { 9 | 10 | @Test 11 | public void singleNumber() { 12 | var script = compile(BaseScript.class, "0"); 13 | assertEquals(0, script.evaluate()); 14 | } 15 | 16 | @Test 17 | public void simpleAddition() { 18 | var script = compile(BaseScript.class, "1+1"); 19 | assertEquals(2, script.evaluate()); 20 | } 21 | 22 | @Test 23 | public void simpleQueryCall() { 24 | var script = compile(QueryScript.class, "q.dbl(2.0)"); 25 | assertEquals(4, script.evaluate(new QueryTest())); 26 | } 27 | 28 | @Test 29 | public void ternarySimple() { 30 | var script = compile(BaseScript.class, "1.0 ? 2.0 : 3.0"); 31 | assertEquals(2, script.evaluate()); 32 | } 33 | 34 | @Test 35 | public void gte() { 36 | var script = compile(BaseScript.class, "1.0 >= 2.0"); 37 | assertEquals(0, script.evaluate()); 38 | } 39 | 40 | @Test 41 | public void math() { 42 | var script = compile(BaseScript.class, "math.abs(-100)"); 43 | assertEquals(100, script.evaluate()); 44 | } 45 | 46 | private T compile(@NotNull Class scriptInterface, @NotNull String source) { 47 | var compiler = new MqlCompiler<>(scriptInterface); 48 | Class scriptClass = compiler.compile(source); 49 | 50 | try { 51 | return scriptClass.newInstance(); 52 | } catch (InstantiationException | IllegalAccessException e) { 53 | throw new RuntimeException(e); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/jit/TestRegression.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.jit; 2 | 3 | import net.hollowcube.mql.foreign.Query; 4 | import net.hollowcube.mql.parser.MqlParser; 5 | import net.hollowcube.mql.util.MqlPrinter; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | public class TestRegression { 12 | 13 | @Test 14 | public void clampParse() { 15 | parse("math.clamp(1, 10, 20)", "(? (. math clamp) (1.0 10.0 20.0))"); 16 | } 17 | 18 | @Test 19 | public void mathAndClampParse() { 20 | parse("1 + math.clamp(1, 10, 20)", "(+ 1.0 (? (. math clamp) (1.0 10.0 20.0)))"); 21 | } 22 | 23 | @Test 24 | public void mathAndClampCompile() { 25 | compile(BaseScript.class, "1 + 1 + math.clamp(1, 10, 20)", """ 26 | DCONST_1 27 | DCONST_1 28 | DADD 29 | DCONST_1 30 | LDC 10.0 31 | LDC 20.0 32 | INVOKESTATIC net/hollowcube/mql/runtime/MqlMath.clamp (DDD)D 33 | DADD 34 | DRETURN 35 | """); 36 | } 37 | 38 | @Test 39 | public void mathAndClampExec() { 40 | var script = execute(BaseScript.class, "1 + 1 + math.clamp(1, 10, 20)"); 41 | assertEquals(12, script.evaluate()); 42 | } 43 | 44 | public static class TestClass { 45 | @Query 46 | public double variable() { 47 | return 12; 48 | } 49 | } 50 | 51 | @FunctionalInterface 52 | public interface TestScript { 53 | double evaluate(@MqlEnv({"variable", "v"}) TestClass emitter); 54 | } 55 | 56 | @Test 57 | public void variableVariableParse() { 58 | parse("variable.variable", "(. variable variable)"); 59 | } 60 | 61 | @Test 62 | public void variableVariable() { 63 | var script = execute(TestScript.class, "variable.variable"); 64 | assertEquals(12, script.evaluate(new TestClass())); 65 | } 66 | 67 | @Test 68 | public void sameScriptTwice() { 69 | var script = execute(TestScript.class, "variable.variable + 1"); 70 | assertEquals(13, script.evaluate(new TestClass())); 71 | script = execute(TestScript.class, "variable.variable + 1"); 72 | assertEquals(13, script.evaluate(new TestClass())); 73 | } 74 | 75 | 76 | private void parse(String input, String expected) { 77 | var expr = new MqlParser(input).parse(); 78 | var actual = new MqlPrinter().visit(expr, null); 79 | 80 | assertEquals(expected, actual); 81 | } 82 | 83 | private void compile(@NotNull Class script, @NotNull String source, @NotNull String expected) { 84 | var compiler = new MqlCompiler<>(script); 85 | byte[] bytecode = compiler.compileBytecode("mql$test", source); 86 | 87 | var str = AsmUtil.prettyPrintEvalMethod(bytecode); 88 | assertEquals(expected, str); 89 | } 90 | 91 | private T execute(@NotNull Class scriptInterface, @NotNull String source) { 92 | var compiler = new MqlCompiler<>(scriptInterface); 93 | Class scriptClass = compiler.compile(source); 94 | 95 | try { 96 | return scriptClass.newInstance(); 97 | } catch (InstantiationException | IllegalAccessException e) { 98 | throw new RuntimeException(e); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/parser/TestMqlLexer.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.parser; 2 | 3 | import org.junit.jupiter.params.ParameterizedTest; 4 | import org.junit.jupiter.params.provider.Arguments; 5 | import org.junit.jupiter.params.provider.MethodSource; 6 | 7 | import java.util.stream.Stream; 8 | 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | public class TestMqlLexer { 12 | 13 | @ParameterizedTest 14 | @MethodSource("individualSymbols") 15 | public void testIndividualSymbols(String input, MqlToken.Type expected) { 16 | var lexer = new MqlLexer(input); 17 | 18 | var token = lexer.next(); 19 | assertNotNull(token); 20 | assertEquals(expected, token.type()); 21 | 22 | var eof = lexer.next(); 23 | assertNull(eof); 24 | } 25 | 26 | private static Stream individualSymbols() { 27 | return Stream.of( 28 | Arguments.of("+", MqlToken.Type.PLUS), 29 | Arguments.of("-", MqlToken.Type.MINUS), 30 | Arguments.of("*", MqlToken.Type.STAR), 31 | Arguments.of("/", MqlToken.Type.SLASH), 32 | Arguments.of(".", MqlToken.Type.DOT), 33 | Arguments.of(",", MqlToken.Type.COMMA), 34 | Arguments.of("?", MqlToken.Type.QUESTION), 35 | Arguments.of("??", MqlToken.Type.QUESTIONQUESTION), 36 | Arguments.of("(", MqlToken.Type.LPAREN), 37 | Arguments.of(")", MqlToken.Type.RPAREN), 38 | 39 | Arguments.of("123", MqlToken.Type.NUMBER), 40 | Arguments.of("123.", MqlToken.Type.NUMBER), 41 | Arguments.of("123.456", MqlToken.Type.NUMBER), 42 | 43 | Arguments.of("abc", MqlToken.Type.IDENT), 44 | Arguments.of("aBc", MqlToken.Type.IDENT), 45 | Arguments.of("aBc1", MqlToken.Type.IDENT) 46 | ); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/parser/TestMqlParser.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.parser; 2 | 3 | import net.hollowcube.mql.util.MqlPrinter; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.Arguments; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | 8 | import java.util.stream.Stream; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | public class TestMqlParser { 13 | 14 | @MethodSource("inputPairs") 15 | @ParameterizedTest(name = "{0}") 16 | public void testInputPairs(String name, String input, String expected) { 17 | var expr = new MqlParser(input).parse(); 18 | var actual = new MqlPrinter().visit(expr, null); 19 | 20 | assertEquals(expected, actual); 21 | } 22 | 23 | private static Stream inputPairs() { 24 | return Stream.of( 25 | Arguments.of("basic number", 26 | "1", "1.0"), 27 | Arguments.of("basic ref", 28 | "abc", "abc"), 29 | Arguments.of("basic add", 30 | "1 + 2", "(+ 1.0 2.0)"), 31 | Arguments.of("nested add", 32 | "1 + 2 + 3", "(+ (+ 1.0 2.0) 3.0)"), 33 | Arguments.of("negate simple", 34 | "-1", "(- 1.0)"), 35 | Arguments.of("negate nested", 36 | "---1", "(- (- (- 1.0)))"), 37 | Arguments.of("negate precedence", 38 | "-2 + 1", "(+ (- 2.0) 1.0)"), 39 | Arguments.of("basic access", 40 | "a.b", "(. a b)"), 41 | Arguments.of("access/add precedence", 42 | "a.b + 1", "(+ (. a b) 1.0)"), 43 | Arguments.of("null coalesce precedence", 44 | "a.b ?? 1", "(?? (. a b) 1.0)"), 45 | Arguments.of("null coalesce precedence 2", 46 | "1 + 2 ?? 1", "(?? (+ 1.0 2.0) 1.0)"), //todo is this correct? should it be (+ 1.0 (?? 2.0 1.0))? 47 | Arguments.of("normalize case 1", 48 | "q.is_alive()", "(. q is_alive)"), 49 | Arguments.of("normalize case 2", 50 | "q.is_alive", "(. q is_alive)"), 51 | Arguments.of("single ternary simple", 52 | "1 ? 2 : 3", "(? 1.0 2.0 3.0)"), 53 | Arguments.of("nested ternary", 54 | "1 ? 2 : 3 ? 4 : 5", "(? 1.0 2.0 (? 3.0 4.0 5.0))"), 55 | Arguments.of("ternary add precedence", 56 | "1 ? 2 + 3 : 4", "(? 1.0 (+ 2.0 3.0) 4.0)") 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/mql/src/test/java/net/hollowcube/mql/runtime/TestMqlMath.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mql.runtime; 2 | 3 | import net.hollowcube.mql.tree.MqlNumberExpr; 4 | import net.hollowcube.mql.value.MqlCallable; 5 | import net.hollowcube.mql.value.MqlValue; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.List; 9 | 10 | import static com.google.common.truth.Truth.assertThat; 11 | 12 | public class TestMqlMath { 13 | 14 | @Test 15 | public void testForeignMath() { 16 | MqlValue result = MqlMath.INSTANCE.get("sqrt").cast(MqlCallable.class) 17 | .call(List.of(new MqlNumberExpr(4)), null); 18 | 19 | assertThat(result).isEqualTo(MqlValue.from(2)); 20 | } 21 | 22 | // hermiteBlend 23 | // lerp 24 | // lerprotate 25 | 26 | } 27 | -------------------------------------------------------------------------------- /modules/schem/README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repository is not being updated and should not be used going forward. 3 | > 4 | > This module has been moved to its own repository [here](https://github.com/hollow-cube/schem). 5 | 6 | # Schematic Loader 7 | 8 | A simple schematic file loader. 9 | 10 | # Install 11 | 12 | Artifacts are published on Jitpack for now. Add the following to your `build.gradle(.kts)`: 13 | 14 | > Note: {VERSION} should be replaced with the latest version on Jitpack, you can find 15 | > this [here](https://jitpack.io/#hollow-cube/common). 16 | 17 | ```kotlin 18 | dependencies { 19 | implementation("com.github.hollow-cube.common:schem:{VERSION}") 20 | } 21 | ``` 22 | 23 | # Usage 24 | 25 | The simplest usage is as follows, which will load a schematic file and place it at 0, 0, 0: 26 | 27 | ``` 28 | var schematic = SchematicReader.read(Path.of("path/to/schematic.schem")); 29 | schem.build(Rotation.NONE, null).apply(instance, 0, 0, 0, null); 30 | ``` 31 | -------------------------------------------------------------------------------- /modules/schem/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | `maven-publish` 4 | id("me.champeau.jmh") version "0.7.0" 5 | } 6 | 7 | dependencies { 8 | testImplementation("org.openjdk.jmh:jmh-core:1.35") 9 | testAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.35") 10 | } 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /modules/schem/src/jmh/java/net/hollowcube/util/schem/BenchSchematicBuilder.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.annotations.BenchmarkMode; 5 | import org.openjdk.jmh.annotations.Fork; 6 | import org.openjdk.jmh.annotations.Mode; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | 12 | public class BenchSchematicBuilder { 13 | 14 | private static Schematic schematic; 15 | private static SchematicBuilder builder; 16 | 17 | static { 18 | try { 19 | var data = Files.readAllBytes(Path.of("todo")); 20 | schematic = SchematicReader.read(new ByteArrayInputStream(data)); 21 | 22 | builder = new SchematicBuilder(); 23 | schematic.apply(Rotation.NONE, builder::addBlock); 24 | } catch (Exception e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | 29 | @Fork(value = 1, warmups = 1) 30 | @Benchmark 31 | @BenchmarkMode(Mode.AverageTime) 32 | public void benchBuildToSchematic() { 33 | builder.toSchematic(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /modules/schem/src/jmh/java/net/hollowcube/util/schem/BenchSchematicReader.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.annotations.BenchmarkMode; 5 | import org.openjdk.jmh.annotations.Fork; 6 | import org.openjdk.jmh.annotations.Mode; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | 12 | public class BenchSchematicReader { 13 | 14 | private static byte[] data; 15 | 16 | static { 17 | try { 18 | data = Files.readAllBytes(Path.of("todo")); 19 | } catch (Exception e) { 20 | throw new RuntimeException(e); 21 | } 22 | // try (var is = BenchSchematicReader.class.getResourceAsStream("mm-spawn-1-27.schem")) { 23 | // assert is != null; 24 | // data = is.readAllBytes(); 25 | // } catch (Exception e) { 26 | // throw new RuntimeException(e); 27 | // } 28 | } 29 | 30 | @Fork(value = 1, warmups = 1) 31 | @Benchmark 32 | @BenchmarkMode(Mode.AverageTime) 33 | public void benchReadFromFile() throws Exception { 34 | SchematicReader.read(new ByteArrayInputStream(data)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /modules/schem/src/jmh/resources/mm-spawn-1-27.schem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/schem/src/jmh/resources/mm-spawn-1-27.schem -------------------------------------------------------------------------------- /modules/schem/src/main/java/net/hollowcube/util/schem/BlockUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import net.minestom.server.instance.block.Block; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | final class BlockUtil { 7 | private BlockUtil() {} 8 | 9 | public static @NotNull String toStateString(@NotNull Block block) { 10 | if (block.properties().isEmpty()) 11 | return block.name(); 12 | 13 | var sb = new StringBuilder(); 14 | sb.append(block.name()).append("["); 15 | for (var entry : block.properties().entrySet()) { 16 | sb.append(entry.getKey()).append("=").append(entry.getValue()).append(","); 17 | } 18 | sb.deleteCharAt(sb.length() - 1); 19 | sb.append("]"); 20 | return sb.toString(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /modules/schem/src/main/java/net/hollowcube/util/schem/Rotation.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * Represents a 90 degree rotation around the Y axis. 7 | */ 8 | public enum Rotation { 9 | NONE, 10 | CLOCKWISE_90, 11 | CLOCKWISE_180, 12 | CLOCKWISE_270; 13 | 14 | public Rotation rotate(Rotation rotation) { 15 | return values()[(ordinal() + rotation.ordinal()) % 4]; 16 | } 17 | 18 | /** 19 | * Converts a Minestom {@link net.minestom.server.utils.Rotation} to a rotation usable in a schematic. 20 | *

21 | * Minestom rotation supports 45 degree angles, if passed to this function they will be rounded down to the nearest 90 degree angle. 22 | */ 23 | public static @NotNull Rotation from(@NotNull net.minestom.server.utils.Rotation rotation) { 24 | return values()[rotation.ordinal() / 2]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/schem/src/main/java/net/hollowcube/util/schem/SchematicReadException.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | public class SchematicReadException extends RuntimeException { 4 | 5 | public SchematicReadException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /modules/schem/src/main/java/net/hollowcube/util/schem/SchematicReader.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import net.minestom.server.command.builder.arguments.minecraft.ArgumentBlockState; 4 | import net.minestom.server.coordinate.Vec; 5 | import net.minestom.server.instance.block.Block; 6 | import net.minestom.server.utils.validate.Check; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jglrxavpok.hephaistos.collections.ImmutableByteArray; 9 | import org.jglrxavpok.hephaistos.nbt.CompressedProcesser; 10 | import org.jglrxavpok.hephaistos.nbt.NBTCompound; 11 | import org.jglrxavpok.hephaistos.nbt.NBTInt; 12 | import org.jglrxavpok.hephaistos.nbt.NBTReader; 13 | 14 | import java.io.InputStream; 15 | import java.nio.file.Path; 16 | 17 | /** 18 | * Simple schematic file reader. 19 | */ 20 | public final class SchematicReader { 21 | 22 | private SchematicReader() {} 23 | 24 | public static @NotNull Schematic read(@NotNull InputStream stream) { 25 | try (var reader = new NBTReader(stream, CompressedProcesser.GZIP)) { 26 | return read(reader); 27 | } catch (Exception e) { 28 | throw new SchematicReadException("failed to read schematic NBT", e); 29 | } 30 | } 31 | 32 | public static @NotNull Schematic read(@NotNull Path path) { 33 | try (var reader = new NBTReader(path, CompressedProcesser.GZIP)) { 34 | return read(reader); 35 | } catch (Exception e) { 36 | throw new SchematicReadException("failed to read schematic NBT", e); 37 | } 38 | } 39 | 40 | public static @NotNull Schematic read(@NotNull NBTReader reader) { 41 | try { 42 | NBTCompound tag = (NBTCompound) reader.read(); 43 | 44 | Short width = tag.getShort("Width"); 45 | Check.notNull(width, "Missing required field 'Width'"); 46 | Short height = tag.getShort("Height"); 47 | Check.notNull(height, "Missing required field 'Height'"); 48 | Short length = tag.getShort("Length"); 49 | Check.notNull(length, "Missing required field 'Length'"); 50 | 51 | NBTCompound metadata = tag.getCompound("Metadata"); 52 | Check.notNull(metadata, "Missing required field 'Metadata'"); 53 | 54 | Integer offsetX = metadata.getInt("WEOffsetX"); 55 | Check.notNull(offsetX, "Missing required field 'Metadata.WEOffsetX'"); 56 | Integer offsetY = metadata.getInt("WEOffsetY"); 57 | Check.notNull(offsetY, "Missing required field 'Metadata.WEOffsetY'"); 58 | Integer offsetZ = metadata.getInt("WEOffsetZ"); 59 | Check.notNull(offsetZ, "Missing required field 'Metadata.WEOffsetZ'"); 60 | 61 | NBTCompound palette = tag.getCompound("Palette"); 62 | Check.notNull(palette, "Missing required field 'Palette'"); 63 | ImmutableByteArray blockArray = tag.getByteArray("BlockData"); 64 | Check.notNull(blockArray, "Missing required field 'BlockData'"); 65 | 66 | Integer paletteSize = tag.getInt("PaletteMax"); 67 | Check.notNull(paletteSize, "Missing required field 'PaletteMax'"); 68 | 69 | Block[] paletteBlocks = new Block[paletteSize]; 70 | 71 | palette.forEach((key, value) -> { 72 | int assigned = ((NBTInt) value).getValue(); 73 | Block block = ArgumentBlockState.staticParse(key); 74 | paletteBlocks[assigned] = block; 75 | }); 76 | 77 | return new Schematic( 78 | new Vec(width, height, length), 79 | new Vec(offsetX, offsetY, offsetZ), 80 | paletteBlocks, 81 | blockArray.copyArray() 82 | ); 83 | } catch (Exception e) { 84 | throw new SchematicReadException("Invalid schematic file", e); 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /modules/schem/src/main/java/net/hollowcube/util/schem/SchematicWriter.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.instance.block.Block; 5 | import net.minestom.server.utils.block.BlockUtils; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jglrxavpok.hephaistos.nbt.CompressedProcesser; 8 | import org.jglrxavpok.hephaistos.nbt.NBTWriter; 9 | import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; 10 | 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | 16 | public class SchematicWriter { 17 | 18 | public static @NotNull byte[] write(@NotNull Schematic schematic) { 19 | MutableNBTCompound schematicNBT = new MutableNBTCompound(); 20 | Point size = schematic.size(); 21 | schematicNBT.setShort("Width", (short) size.x()); 22 | schematicNBT.setShort("Height", (short) size.y()); 23 | schematicNBT.setShort("Length", (short) size.z()); 24 | 25 | Point offset = schematic.offset(); 26 | MutableNBTCompound schematicMetadata = new MutableNBTCompound(); 27 | schematicMetadata.setInt("WEOffsetX", offset.blockX()); 28 | schematicMetadata.setInt("WEOffsetY", offset.blockY()); 29 | schematicMetadata.setInt("WEOffsetZ", offset.blockZ()); 30 | 31 | schematicNBT.set("Metadata", schematicMetadata.toCompound()); 32 | 33 | schematicNBT.setByteArray("BlockData", schematic.blocks()); 34 | Block[] blocks = schematic.palette(); 35 | 36 | schematicNBT.setInt("PaletteMax", blocks.length); 37 | 38 | MutableNBTCompound palette = new MutableNBTCompound(); 39 | for (int i = 0; i < blocks.length; i++) { 40 | palette.setInt(BlockUtil.toStateString(blocks[i]), i); 41 | } 42 | schematicNBT.set("Palette", palette.toCompound()); 43 | 44 | var out = new ByteArrayOutputStream(); 45 | try (NBTWriter writer = new NBTWriter(out, CompressedProcesser.GZIP)) { 46 | writer.writeNamed("Schematic", schematicNBT.toCompound()); 47 | } catch (IOException e) { 48 | // No exceptions when writing to a byte array 49 | throw new RuntimeException(e); 50 | } 51 | 52 | return out.toByteArray(); 53 | } 54 | 55 | public static void write(@NotNull Schematic schematic, @NotNull Path schemPath) throws IOException { 56 | Files.write(schemPath, write(schematic)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /modules/schem/src/test/java/net/hollowcube/util/schem/TestRegressionIssue44.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.coordinate.Vec; 5 | import net.minestom.server.instance.block.Block; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.HashMap; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | /** 13 | * #44 14 | */ 15 | class TestRegressionIssue44 { 16 | 17 | @Test 18 | void testNegative3x3() { 19 | SchematicBuilder builder = new SchematicBuilder(); 20 | 21 | int startX = -12, startY = 0, startZ = -12; 22 | int endX = -10, endY = 2, endZ = -10; 23 | for (int y = startY; y <= endY; y++) { 24 | for (int z = startZ; z <= endZ; z++) { 25 | for (int x = startX; x <= endX; x++) { 26 | builder.addBlock(new Vec(x, y, z), Block.STONE); 27 | } 28 | } 29 | } 30 | 31 | Schematic result = builder.build(); 32 | assertEquals(new Vec(3, 3, 3), result.size()); 33 | assertEquals(new Vec(-12, 0, -12), result.offset()); 34 | 35 | var appliedBlocks = new HashMap(); 36 | result.apply(Rotation.NONE, appliedBlocks::put); 37 | 38 | for (int y = startY; y <= endY; y++) { 39 | for (int z = startZ; z <= endZ; z++) { 40 | for (int x = startX; x <= endX; x++) { 41 | assertEquals(Block.STONE, appliedBlocks.get(new Vec(x, y, z))); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /modules/schem/src/test/java/net/hollowcube/util/schem/TestRegressionIssue46.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.coordinate.Vec; 5 | import net.minestom.server.instance.block.Block; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.HashMap; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | 13 | class TestRegressionIssue46 { 14 | 15 | @Test 16 | void test1x2x3SingleBlock() { 17 | var builder = new SchematicBuilder(); 18 | builder.addBlock(0, 0, 0, Block.STONE); // Min point 19 | builder.addBlock(0, 1, 2, Block.STONE); // Max point 20 | 21 | var result = builder.build(); 22 | assertEquals(new Vec(1, 2, 3), result.size()); 23 | 24 | var blocks = new HashMap(); 25 | result.apply(Rotation.NONE, blocks::put); 26 | 27 | assertEquals(Block.STONE, blocks.get(new Vec(0, 0, 0))); 28 | assertEquals(Block.STONE, blocks.get(new Vec(0, 1, 2))); 29 | } 30 | 31 | @Test 32 | void testReadBuild() { 33 | // Reading, building, and comparing the built version to the original should yield the same thing... in theory 34 | 35 | var schemFile = getClass().getClassLoader().getResourceAsStream("big2.schem"); 36 | assertNotNull(schemFile); 37 | var schem = SchematicReader.read(schemFile); 38 | 39 | var builder = new SchematicBuilder(); 40 | schem.apply(Rotation.NONE, builder::addBlock); 41 | var result = builder.build(); 42 | 43 | var expectedBlocks = new HashMap<>(); 44 | schem.apply(Rotation.NONE, expectedBlocks::put); 45 | var actualBlocks = new HashMap<>(); 46 | result.apply(Rotation.NONE, actualBlocks::put); 47 | 48 | for (var entry : expectedBlocks.entrySet()) { 49 | var expectedBlock = entry.getValue(); 50 | var actualBlock = actualBlocks.get(entry.getKey()); 51 | assertEquals(expectedBlock, actualBlock); 52 | } 53 | 54 | // assertEquals(schem, result); 55 | assertEquals(expectedBlocks, actualBlocks); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /modules/schem/src/test/java/net/hollowcube/util/schem/TestSchematicBuilder.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import net.minestom.server.coordinate.Vec; 4 | import net.minestom.server.instance.block.Block; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | class TestSchematicBuilder { 10 | 11 | @Test 12 | void testSchematicBuild() { 13 | SchematicBuilder builder = new SchematicBuilder(); 14 | 15 | builder.addBlock(new Vec(5, 2, 5), Block.STONE); 16 | builder.addBlock(new Vec(5, 3, 5), Block.STONE); 17 | builder.addBlock(new Vec(5, 4, 5), Block.STONE); 18 | builder.addBlock(new Vec(4, 2, 5), Block.DIORITE); 19 | builder.addBlock(new Vec(4, 3, 5), Block.DIORITE); 20 | builder.addBlock(new Vec(4, 4, 5), Block.DIORITE); 21 | 22 | Schematic result = builder.build(); 23 | // Since we should have perfect encapsulation in the schematic because it is rectangular, we shouldn't have any air entries 24 | assertEquals(2, result.palette().length); 25 | assertEquals(new Vec(4, 2, 5), result.offset()); 26 | 27 | builder.addBlock(new Vec(3, 2, 5), Block.COAL_BLOCK); 28 | 29 | // Should have air and coal in the schematic now 30 | result = builder.build(); 31 | assertEquals(4, result.palette().length); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/schem/src/test/java/net/hollowcube/util/schem/TestSchematicWriter.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.util.schem; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.nio.file.Files; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNotNull; 9 | 10 | class TestSchematicWriter { 11 | 12 | @Test 13 | void readWriteReadTest() throws Exception { 14 | var schemFile = getClass().getClassLoader().getResourceAsStream("big.schem"); 15 | assertNotNull(schemFile); 16 | var schem = SchematicReader.read(schemFile); 17 | 18 | var tempFile = Files.createTempDirectory("schem-test").resolve("big.schem"); 19 | SchematicWriter.write(schem, tempFile); 20 | 21 | var schem2 = SchematicReader.read(tempFile); 22 | assertEquals(schem, schem2); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/schem/src/test/resources/big.schem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/schem/src/test/resources/big.schem -------------------------------------------------------------------------------- /modules/schem/src/test/resources/big2.schem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/schem/src/test/resources/big2.schem -------------------------------------------------------------------------------- /modules/schem/src/test/resources/minmax345.schem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/schem/src/test/resources/minmax345.schem -------------------------------------------------------------------------------- /modules/schem/src/test/resources/minmax345_2.schem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/schem/src/test/resources/minmax345_2.schem -------------------------------------------------------------------------------- /modules/schem/src/test/resources/minmaxcube.schem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/schem/src/test/resources/minmaxcube.schem -------------------------------------------------------------------------------- /modules/schem/src/test/resources/minmaxrect.schem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/common/97cfc65565c877346f2ef8c1f6cbe90721e3b6e8/modules/schem/src/test/resources/minmaxrect.schem -------------------------------------------------------------------------------- /modules/test/README.md: -------------------------------------------------------------------------------- 1 | ## test 2 | 3 | A module for test utilities used by the others. Only ever included for tests. 4 | 5 | Contains a copy of the Minestom internal test framework until that has been extracted to its own module. 6 | -------------------------------------------------------------------------------- /modules/test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | dependencies { 6 | // Minestom 7 | api("com.github.minestommmo:Minestom:675b3170a1") 8 | 9 | // JUnit 10 | api("org.junit.jupiter:junit-jupiter-api:5.9.0") 11 | api("org.junit.jupiter:junit-jupiter-params:5.9.0") 12 | runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.0") 13 | 14 | // Truth 15 | api("com.google.truth:truth:1.1.3") 16 | 17 | // TestContainers 18 | fun testContainersApi(name: String) { 19 | api("org.testcontainers:$name:1.17.3") { 20 | exclude(group = "junit", module = "junit") 21 | } 22 | } 23 | 24 | testContainersApi("testcontainers") 25 | testContainersApi("junit-jupiter") 26 | testContainersApi("mongodb") 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/hollowcube/test/ComponentUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.test; 2 | 3 | import net.kyori.adventure.text.Component; 4 | import net.kyori.adventure.text.TranslatableComponent; 5 | import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class ComponentUtil { 9 | 10 | public static @NotNull String toString(@NotNull Component component) { 11 | if (component instanceof TranslatableComponent comp) 12 | return comp.key(); 13 | return PlainTextComponentSerializer.plainText().serialize(component); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/hollowcube/test/TestUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.test; 2 | 3 | import net.minestom.server.entity.Player; 4 | import net.minestom.server.network.packet.server.SendablePacket; 5 | import net.minestom.server.network.player.PlayerConnection; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.net.InetSocketAddress; 9 | import java.net.SocketAddress; 10 | import java.time.Instant; 11 | import java.util.UUID; 12 | 13 | //todo Moved straight from chat module, needs some refactoring 14 | public class TestUtil { 15 | 16 | public static Instant instantNow() { 17 | return Instant.ofEpochMilli(1659127729952L); 18 | } 19 | 20 | public static @NotNull Player headlessPlayer(@NotNull String name) { 21 | return new Player(UUID.randomUUID(), name, EMPTY_PLAYER_CONNECTION); 22 | } 23 | 24 | public static @NotNull Player headlessPlayer() { 25 | return headlessPlayer("test0"); 26 | } 27 | 28 | private static final PlayerConnection EMPTY_PLAYER_CONNECTION = new PlayerConnection() { 29 | @Override 30 | public void sendPacket(@NotNull SendablePacket packet) { 31 | 32 | } 33 | 34 | @Override 35 | public @NotNull SocketAddress getRemoteAddress() { 36 | return new InetSocketAddress(0); 37 | } 38 | }; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/Collector.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.List; 6 | import java.util.function.Consumer; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; 10 | 11 | public interface Collector { 12 | @NotNull List<@NotNull T> collect(); 13 | 14 | default

void assertSingle(@NotNull Class

type, @NotNull Consumer

consumer) { 15 | List elements = collect(); 16 | assertEquals(1, elements.size(), "Expected 1 element, got " + elements); 17 | var element = elements.get(0); 18 | assertInstanceOf(type, element, "Expected type " + type.getSimpleName() + ", got " + element.getClass().getSimpleName()); 19 | consumer.accept((P) element); 20 | } 21 | 22 | default void assertSingle(@NotNull Consumer consumer) { 23 | List elements = collect(); 24 | assertEquals(1, elements.size(), "Expected 1 element, got " + elements); 25 | consumer.accept(elements.get(0)); 26 | } 27 | 28 | default void assertCount(int count) { 29 | List elements = collect(); 30 | assertEquals(count, elements.size(), "Expected " + count + " element(s), got " + elements.size() + ": " + elements); 31 | } 32 | 33 | default void assertSingle() { 34 | assertCount(1); 35 | } 36 | 37 | default void assertEmpty() { 38 | assertCount(0); 39 | } 40 | } -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/Env.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import net.minestom.server.ServerProcess; 4 | import net.minestom.server.coordinate.Pos; 5 | import net.minestom.server.entity.Player; 6 | import net.minestom.server.event.Event; 7 | import net.minestom.server.event.EventFilter; 8 | import net.minestom.server.instance.Instance; 9 | import net.minestom.server.instance.block.Block; 10 | import net.minestom.server.network.PlayerProvider; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.time.Duration; 14 | import java.util.function.BooleanSupplier; 15 | 16 | public interface Env { 17 | 18 | @NotNull ServerProcess process(); 19 | 20 | @NotNull TestConnection createConnection(); 21 | 22 | @NotNull Collector trackEvent(@NotNull Class eventType, @NotNull EventFilter filter, @NotNull H actor); 23 | 24 | @NotNull FlexibleListener listen(@NotNull Class eventType); 25 | 26 | default void tick() { 27 | process().ticker().tick(System.nanoTime()); 28 | } 29 | 30 | default boolean tickWhile(BooleanSupplier condition, Duration timeout) { 31 | var ticker = process().ticker(); 32 | final long start = System.nanoTime(); 33 | while (condition.getAsBoolean()) { 34 | final long tick = System.nanoTime(); 35 | ticker.tick(tick); 36 | if (timeout != null && System.nanoTime() - start > timeout.toNanos()) { 37 | return false; 38 | } 39 | } 40 | return true; 41 | } 42 | 43 | default @NotNull Player createPlayer(@NotNull Instance instance, @NotNull Pos pos) { 44 | return createConnection().connect(Player::new, instance, pos).join(); 45 | } 46 | 47 | default @NotNull Player createPlayer(@NotNull PlayerProvider playerProvider, @NotNull Instance instance, @NotNull Pos pos) { 48 | return createConnection().connect(playerProvider, instance, pos).join(); 49 | } 50 | 51 | default @NotNull Instance createFlatInstance() { 52 | var instance = process().instance().createInstanceContainer(); 53 | instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); 54 | // instance.loadChunk(0, 0).join(); //todo this line breaks tests that need a player... somehow... 55 | return instance; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/EnvBefore.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import org.junit.jupiter.api.extension.BeforeEachCallback; 4 | import org.junit.jupiter.api.extension.ExtensionContext; 5 | 6 | final class EnvBefore implements BeforeEachCallback { 7 | @Override 8 | public void beforeEach(ExtensionContext context) { 9 | System.setProperty("minestom.viewable-packet", "false"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/EnvCleaner.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext; 4 | import org.junit.jupiter.api.extension.InvocationInterceptor; 5 | import org.junit.jupiter.api.extension.ReflectiveInvocationContext; 6 | 7 | import java.lang.reflect.Method; 8 | 9 | final class EnvCleaner implements InvocationInterceptor { 10 | @Override 11 | public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { 12 | invocation.proceed(); 13 | EnvImpl env = (EnvImpl) invocationContext.getArguments().get(0); 14 | env.cleanup(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/EnvImpl.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import net.minestom.server.ServerProcess; 4 | import net.minestom.server.event.Event; 5 | import net.minestom.server.event.EventFilter; 6 | import net.minestom.server.event.EventListener; 7 | import net.minestom.server.utils.debug.DebugUtils; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.CopyOnWriteArrayList; 12 | import java.util.function.Consumer; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | import static org.junit.jupiter.api.Assertions.fail; 16 | 17 | final class EnvImpl implements Env { 18 | 19 | static { 20 | DebugUtils.INSIDE_TEST = true; 21 | } 22 | 23 | private final ServerProcess process; 24 | private final List> listeners = new CopyOnWriteArrayList<>(); 25 | 26 | public EnvImpl(ServerProcess process) { 27 | this.process = process; 28 | } 29 | 30 | @Override 31 | public @NotNull ServerProcess process() { 32 | return process; 33 | } 34 | 35 | @Override 36 | public @NotNull TestConnection createConnection() { 37 | return new TestConnectionImpl(this); 38 | } 39 | 40 | @Override 41 | public @NotNull Collector trackEvent(@NotNull Class eventType, @NotNull EventFilter filter, @NotNull H actor) { 42 | var tracker = new EventCollector(actor); 43 | this.process.eventHandler().map(actor, filter).addListener(eventType, tracker.events::add); 44 | return tracker; 45 | } 46 | 47 | @Override 48 | public @NotNull FlexibleListener listen(@NotNull Class eventType) { 49 | var handler = process.eventHandler(); 50 | var flexible = new FlexibleListenerImpl<>(eventType); 51 | var listener = EventListener.of(eventType, e -> flexible.handler.accept(e)); 52 | handler.addListener(listener); 53 | this.listeners.add(flexible); 54 | return flexible; 55 | } 56 | 57 | void cleanup() { 58 | this.listeners.forEach(FlexibleListenerImpl::check); 59 | } 60 | 61 | final class EventCollector implements Collector { 62 | private final Object handler; 63 | private final List events = new CopyOnWriteArrayList<>(); 64 | 65 | public EventCollector(Object handler) { 66 | this.handler = handler; 67 | } 68 | 69 | @Override 70 | public @NotNull List collect() { 71 | process.eventHandler().unmap(handler); 72 | return List.copyOf(events); 73 | } 74 | } 75 | 76 | static final class FlexibleListenerImpl implements FlexibleListener { 77 | private final Class eventType; 78 | private Consumer handler = e -> { 79 | }; 80 | private boolean initialized; 81 | private boolean called; 82 | 83 | FlexibleListenerImpl(Class eventType) { 84 | this.eventType = eventType; 85 | } 86 | 87 | @Override 88 | public void followup(@NotNull Consumer handler) { 89 | updateHandler(handler); 90 | } 91 | 92 | @Override 93 | public void failFollowup() { 94 | updateHandler(e -> fail("Event " + e.getClass().getSimpleName() + " was not expected")); 95 | } 96 | 97 | void updateHandler(@NotNull Consumer handler) { 98 | check(); 99 | this.initialized = true; 100 | this.called = false; 101 | this.handler = e -> { 102 | handler.accept(e); 103 | this.called = true; 104 | }; 105 | } 106 | 107 | void check() { 108 | assertTrue(!initialized || called, "Last listener has not been called: " + eventType.getSimpleName()); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/EnvParameterResolver.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import org.junit.jupiter.api.extension.ExtensionContext; 5 | import org.junit.jupiter.api.extension.ParameterContext; 6 | import org.junit.jupiter.api.extension.ParameterResolutionException; 7 | import org.junit.jupiter.api.extension.support.TypeBasedParameterResolver; 8 | 9 | final class EnvParameterResolver extends TypeBasedParameterResolver { 10 | @Override 11 | public Env resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) 12 | throws ParameterResolutionException { 13 | return new EnvImpl(MinecraftServer.updateProcess()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/EnvTest.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import org.junit.jupiter.api.extension.ExtendWith; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @ExtendWith(EnvParameterResolver.class) 11 | @ExtendWith(EnvBefore.class) 12 | @ExtendWith(EnvCleaner.class) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.TYPE) 15 | public @interface EnvTest { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/FlexibleListener.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import net.minestom.server.event.Event; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.util.function.Consumer; 7 | 8 | public interface FlexibleListener { 9 | /** 10 | * Updates the handler. Fails if the previous followup has not been called. 11 | */ 12 | void followup(@NotNull Consumer handler); 13 | 14 | default void followup() { 15 | followup(event -> { 16 | // Empty 17 | }); 18 | } 19 | 20 | /** 21 | * Fails if an event is received. Valid until the next followup call. 22 | */ 23 | void failFollowup(); 24 | } 25 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/TestConnection.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import net.minestom.server.coordinate.Pos; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.instance.Instance; 6 | import net.minestom.server.network.PlayerProvider; 7 | import net.minestom.server.network.packet.server.ServerPacket; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | public interface TestConnection { 13 | @NotNull CompletableFuture<@NotNull Player> connect(@NotNull PlayerProvider playerProvider, @NotNull Instance instance, @NotNull Pos pos); 14 | 15 | @NotNull Collector trackIncoming(@NotNull Class type); 16 | 17 | default @NotNull Collector trackIncoming() { 18 | return trackIncoming(ServerPacket.class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/TestConnectionImpl.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import net.minestom.server.ServerProcess; 4 | import net.minestom.server.coordinate.Pos; 5 | import net.minestom.server.entity.Player; 6 | import net.minestom.server.event.player.PlayerLoginEvent; 7 | import net.minestom.server.instance.Instance; 8 | import net.minestom.server.network.PlayerProvider; 9 | import net.minestom.server.network.packet.server.SendablePacket; 10 | import net.minestom.server.network.packet.server.ServerPacket; 11 | import net.minestom.server.network.player.PlayerConnection; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.net.InetSocketAddress; 15 | import java.net.SocketAddress; 16 | import java.util.List; 17 | import java.util.UUID; 18 | import java.util.concurrent.CompletableFuture; 19 | import java.util.concurrent.CopyOnWriteArrayList; 20 | 21 | final class TestConnectionImpl implements TestConnection { 22 | private final ServerProcess process; 23 | private final PlayerConnectionImpl playerConnection = new PlayerConnectionImpl(); 24 | 25 | private final List> incomingTrackers = new CopyOnWriteArrayList<>(); 26 | 27 | TestConnectionImpl(Env env) { 28 | this.process = env.process(); 29 | } 30 | 31 | @Override 32 | public @NotNull CompletableFuture connect(@NotNull PlayerProvider playerProvider, @NotNull Instance instance, @NotNull Pos pos) { 33 | Player player = playerProvider.createPlayer(UUID.randomUUID(), "RandName", playerConnection); 34 | player.eventNode().addListener(PlayerLoginEvent.class, event -> { 35 | event.setSpawningInstance(instance); 36 | event.getPlayer().setRespawnPoint(pos); 37 | }); 38 | 39 | return process.connection().startPlayState(player, true) 40 | .thenApply(unused -> { 41 | process.connection().updateWaitingPlayers(); 42 | return player; 43 | }); 44 | } 45 | 46 | @Override 47 | public @NotNull Collector trackIncoming(@NotNull Class type) { 48 | var tracker = new IncomingCollector<>(type); 49 | this.incomingTrackers.add((IncomingCollector) tracker); 50 | return tracker; 51 | } 52 | 53 | final class PlayerConnectionImpl extends PlayerConnection { 54 | @Override 55 | public void sendPacket(@NotNull SendablePacket packet) { 56 | for (var tracker : incomingTrackers) { 57 | final var serverPacket = SendablePacket.extractServerPacket(packet); 58 | if (tracker.type.isAssignableFrom(serverPacket.getClass())) tracker.packets.add(serverPacket); 59 | } 60 | } 61 | 62 | @Override 63 | public @NotNull SocketAddress getRemoteAddress() { 64 | return new InetSocketAddress("localhost", 25565); 65 | } 66 | 67 | @Override 68 | public void disconnect() { 69 | 70 | } 71 | } 72 | 73 | final class IncomingCollector implements Collector { 74 | private final Class type; 75 | private final List packets = new CopyOnWriteArrayList<>(); 76 | 77 | public IncomingCollector(Class type) { 78 | this.type = type; 79 | } 80 | 81 | @Override 82 | public @NotNull List collect() { 83 | incomingTrackers.remove(this); 84 | return List.copyOf(packets); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/TestUtils.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test; 2 | 3 | import org.jglrxavpok.hephaistos.nbt.NBTCompound; 4 | import org.jglrxavpok.hephaistos.nbt.NBTException; 5 | import org.jglrxavpok.hephaistos.parser.SNBTParser; 6 | 7 | import java.io.StringReader; 8 | import java.lang.ref.WeakReference; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | public class TestUtils { 13 | public static void waitUntilCleared(WeakReference ref) { 14 | while (ref.get() != null) { 15 | System.gc(); 16 | try { 17 | Thread.sleep(50); 18 | } catch (InterruptedException e) { 19 | throw new RuntimeException(e); 20 | } 21 | } 22 | } 23 | 24 | public static void assertEqualsSNBT(String snbt, NBTCompound compound) { 25 | try { 26 | final var converted = (NBTCompound) new SNBTParser(new StringReader(snbt)).parse(); 27 | assertEquals(converted, compound); 28 | } catch (NBTException e) { 29 | fail(e); 30 | } 31 | } 32 | 33 | public static void assertEqualsIgnoreSpace(String s1, String s2, boolean matchCase) { 34 | final String val1 = stripExtraSpaces(s1); 35 | final String val2 = stripExtraSpaces(s2); 36 | if (matchCase) { 37 | assertEquals(val1, val2); 38 | } else { 39 | assertTrue(val1.equalsIgnoreCase(val2)); 40 | } 41 | } 42 | 43 | public static void assertEqualsIgnoreSpace(String s1, String s2) { 44 | assertEqualsIgnoreSpace(s1, s2, true); 45 | } 46 | 47 | private static String stripExtraSpaces(String s) { 48 | StringBuilder formattedString = new StringBuilder(); 49 | java.util.StringTokenizer st = new java.util.StringTokenizer(s); 50 | while (st.hasMoreTokens()) { 51 | formattedString.append(st.nextToken()); 52 | } 53 | return formattedString.toString().trim(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/truth/AbstractInventorySubject.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test.truth; 2 | 3 | import com.google.common.truth.Fact; 4 | import com.google.common.truth.FailureMetadata; 5 | import com.google.common.truth.Subject; 6 | import com.google.common.truth.Truth; 7 | import net.minestom.server.inventory.AbstractInventory; 8 | import net.minestom.server.item.ItemStack; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import java.util.function.Predicate; 15 | 16 | import static com.google.common.truth.Truth.assertAbout; 17 | 18 | public class AbstractInventorySubject extends Subject { 19 | private final AbstractInventory actual; 20 | 21 | public static AbstractInventorySubject assertThat(AbstractInventory actual) { 22 | return assertAbout(abstractInventories()).that(actual); 23 | } 24 | 25 | protected AbstractInventorySubject(FailureMetadata metadata, @Nullable AbstractInventory actual) { 26 | super(metadata, actual); 27 | this.actual = actual; 28 | } 29 | 30 | public void isEmpty() { 31 | if (!itemStacks().isEmpty()) { 32 | failWithActual(Fact.simpleFact("expected to be empty")); 33 | } 34 | } 35 | 36 | public void isNotEmpty() { 37 | if (itemStacks().isEmpty()) { 38 | failWithActual(Fact.simpleFact("expected not to be empty")); 39 | } 40 | } 41 | 42 | public void containsExactly(@NotNull ItemStack... itemStacks) { 43 | Truth.assertThat(itemStacks()).containsExactly((Object[]) itemStacks); 44 | } 45 | 46 | public void doesNotContain(@NotNull ItemStack itemStack) { 47 | Truth.assertThat(itemStacks()).doesNotContain(itemStack); 48 | } 49 | 50 | private List itemStacks() { 51 | return Arrays.stream(actual.getItemStacks()) 52 | .filter(Predicate.not(ItemStack::isAir)) 53 | .toList(); 54 | } 55 | 56 | private static Factory abstractInventories() { 57 | return AbstractInventorySubject::new; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/truth/EntitySubject.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test.truth; 2 | 3 | import com.google.common.truth.Fact; 4 | import com.google.common.truth.FailureMetadata; 5 | import com.google.common.truth.Subject; 6 | import com.google.common.truth.Truth; 7 | import net.minestom.server.entity.Entity; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | @SuppressWarnings("ConstantConditions") 11 | public class EntitySubject extends Subject { 12 | private final Entity actual; 13 | 14 | public static EntitySubject assertThat(@Nullable Entity entity) { 15 | return Truth.assertAbout(entities()).that(entity); 16 | } 17 | 18 | protected EntitySubject(FailureMetadata metadata, @Nullable Entity actual) { 19 | super(metadata, actual); 20 | this.actual = actual; 21 | } 22 | 23 | public void isRemoved() { 24 | if (!actual.isRemoved()) { 25 | failWithActual(Fact.simpleFact("expected to be removed")); 26 | } 27 | } 28 | 29 | public void isNotRemoved() { 30 | if (actual.isRemoved()) { 31 | failWithActual(Fact.simpleFact("expected not to be removed")); 32 | } 33 | } 34 | 35 | public static Factory entities() { 36 | return EntitySubject::new; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/truth/ItemStackSubject.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test.truth; 2 | 3 | import com.google.common.truth.FailureMetadata; 4 | import com.google.common.truth.Subject; 5 | import com.google.common.truth.Truth; 6 | import net.minestom.server.item.ItemStack; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | @SuppressWarnings("ConstantConditions") 10 | public class ItemStackSubject extends Subject { 11 | private final ItemStack actual; 12 | 13 | public static ItemStackSubject assertThat(@Nullable ItemStack actual) { 14 | return Truth.assertAbout(itemStacks()).that(actual); 15 | } 16 | 17 | protected ItemStackSubject(FailureMetadata metadata, @Nullable ItemStack actual) { 18 | super(metadata, actual); 19 | this.actual = actual; 20 | } 21 | 22 | public void hasAmount(int expectedAmount) { 23 | check("amount()").that(actual.amount()).isEqualTo(expectedAmount); 24 | } 25 | 26 | 27 | public static Factory itemStacks() { 28 | return ItemStackSubject::new; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /modules/test/src/main/java/net/minestom/server/test/truth/MinestomTruth.java: -------------------------------------------------------------------------------- 1 | package net.minestom.server.test.truth; 2 | 3 | import net.minestom.server.entity.Entity; 4 | import net.minestom.server.inventory.AbstractInventory; 5 | import net.minestom.server.item.ItemStack; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | public final class MinestomTruth { 10 | private MinestomTruth() { 11 | } 12 | 13 | public static @NotNull EntitySubject assertThat(@Nullable Entity actual) { 14 | return EntitySubject.assertThat(actual); 15 | } 16 | 17 | public static @NotNull AbstractInventorySubject assertThat(@Nullable AbstractInventory actual) { 18 | return AbstractInventorySubject.assertThat(actual); 19 | } 20 | 21 | public static @NotNull ItemStackSubject assertThat(@Nullable ItemStack actual) { 22 | return ItemStackSubject.assertThat(actual); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "common" 2 | 3 | include(":modules") 4 | // Common + Modules which depend on it 5 | include(":modules:common") 6 | include(":modules:dev") 7 | // Standalone modules 8 | include("modules:instances") 9 | include(":modules:mql") 10 | include(":modules:schem") 11 | include(":modules:block-placement") 12 | include(":modules:test") 13 | --------------------------------------------------------------------------------