├── .editorconfig ├── .env ├── .github └── workflows │ ├── build_test.yml │ └── sync_docs.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── docs └── Home.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── modules ├── block-interactions │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ └── blocks │ │ │ ├── BlockInteracter.java │ │ │ ├── BlockInteractionUtils.java │ │ │ ├── data │ │ │ ├── CropBlockData.java │ │ │ └── CropBlockDataSerializer.java │ │ │ ├── handlers │ │ │ ├── CropHandler.java │ │ │ ├── FarmlandHandler.java │ │ │ └── TillHandler.java │ │ │ ├── ore │ │ │ ├── MiningFacet.java │ │ │ ├── Ore.java │ │ │ ├── event │ │ │ │ └── PlayerOreBreakEvent.java │ │ │ ├── handler │ │ │ │ └── OreBlockHandler.java │ │ │ ├── item │ │ │ │ ├── Pickaxe.java │ │ │ │ └── PickaxeHandler.java │ │ │ └── quest │ │ │ │ └── MineOreObjective.java │ │ │ └── util │ │ │ └── BlockUtil.java │ │ └── test │ │ └── java │ │ └── net │ │ └── hollowcube │ │ └── blocks │ │ └── util │ │ └── TestBlockUtil.java ├── build.gradle.kts ├── chat │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ └── chat │ │ │ ├── ChatFacet.java │ │ │ ├── ChatMessage.java │ │ │ ├── ChatQuery.java │ │ │ ├── command │ │ │ └── LogCommand.java │ │ │ └── storage │ │ │ ├── ChatStorage.java │ │ │ ├── MongoChatStorage.java │ │ │ └── NoopChatStorage.java │ │ └── test │ │ ├── java │ │ └── net │ │ │ └── hollowcube │ │ │ └── chat │ │ │ ├── TestChatManager.java │ │ │ ├── TestChatQuery.java │ │ │ ├── command │ │ │ └── TestLogCommand.java │ │ │ └── storage │ │ │ ├── MockChatStorage.java │ │ │ ├── TestMongoChatStorage.java │ │ │ └── TestMongoChatStorageIntegration.java │ │ └── resources │ │ └── fixtures │ │ └── chat_messages.json ├── common │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ ├── Env.java │ │ │ ├── command │ │ │ └── ExtraArguments.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 │ │ │ ├── MongoConfig.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 ├── development │ ├── .env.sample │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── java │ │ └── net │ │ │ └── hollowcube │ │ │ └── server │ │ │ └── dev │ │ │ ├── Main.java │ │ │ ├── blocks │ │ │ └── ore │ │ │ │ └── OreCreatorTool.java │ │ │ ├── command │ │ │ ├── BaseCommandRegister.java │ │ │ ├── CraftCommand.java │ │ │ ├── GameModeCommand.java │ │ │ ├── GiveCommand.java │ │ │ ├── ModifierCommand.java │ │ │ └── StopCommand.java │ │ │ └── tool │ │ │ ├── DebugTool.java │ │ │ ├── DebugToolManager.java │ │ │ └── HelloDebugTool.java │ │ └── resources │ │ ├── data │ │ ├── items.json │ │ ├── loot_table.json │ │ ├── ore.json │ │ └── quest.json │ │ ├── lang │ │ └── en_US.properties │ │ └── tinylog.properties ├── item │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ └── item │ │ │ ├── Item.java │ │ │ ├── ItemComponent.java │ │ │ ├── ItemComponentHandler.java │ │ │ ├── ItemComponentRegistry.java │ │ │ ├── ItemImpl.java │ │ │ ├── ItemManager.java │ │ │ ├── ItemRegistry.java │ │ │ ├── crafting │ │ │ ├── CraftingInventory.java │ │ │ ├── CraftingRecipe.java │ │ │ ├── RecipeList.java │ │ │ ├── ShapedCraftingRecipe.java │ │ │ ├── ShapelessCraftingRecipe.java │ │ │ ├── ToolCraftingInventory.java │ │ │ └── ToolShapedCraftingRecipe.java │ │ │ ├── entity │ │ │ └── OwnedItemEntity.java │ │ │ ├── impl │ │ │ ├── Rarity.java │ │ │ └── RarityHandler.java │ │ │ └── loot │ │ │ ├── ItemDistributor.java │ │ │ └── ItemEntry.java │ │ └── test │ │ ├── java │ │ └── net │ │ │ └── hollowcube │ │ │ ├── crafting │ │ │ ├── TestCraftingIntegration.java │ │ │ ├── TestCraftingRecipe.java │ │ │ └── TestToolCraftingIntegration.java │ │ │ └── item │ │ │ ├── TestItem.java │ │ │ ├── TestItemRegistry.java │ │ │ ├── entity │ │ │ └── TestOwnedItemEntityIntegration.java │ │ │ ├── loot │ │ │ ├── TestItemDistributorIntegration.java │ │ │ └── TestItemEntry.java │ │ │ └── test │ │ │ ├── MockItem.java │ │ │ ├── TestComponent.java │ │ │ └── TestComponentHandler.java │ │ └── resources │ │ └── data │ │ └── items.json ├── loot-table │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── net │ │ │ └── hollowcube │ │ │ └── loot │ │ │ ├── LootContext.java │ │ │ ├── LootEntry.java │ │ │ ├── LootModifier.java │ │ │ ├── LootPool.java │ │ │ ├── LootPredicate.java │ │ │ ├── LootResult.java │ │ │ ├── LootTable.java │ │ │ └── impl │ │ │ ├── GroupLootEntry.java │ │ │ ├── LootContextImpl.java │ │ │ └── LootResultImpl.java │ │ └── test │ │ └── java │ │ └── net │ │ └── hollowcube │ │ └── loot │ │ ├── TestLootPool.java │ │ ├── TestLootTable.java │ │ ├── impl │ │ ├── TestGroupLootEntry.java │ │ ├── TestLootContextImpl.java │ │ └── TestLootResultImpl.java │ │ └── test │ │ ├── LootTableUtil.java │ │ └── StringLootType.java ├── player │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── java │ │ │ └── net │ │ │ │ └── hollowcube │ │ │ │ ├── CombatFacet.java │ │ │ │ ├── DamageTagList.java │ │ │ │ ├── damage │ │ │ │ ├── AttackCooldown.java │ │ │ │ ├── DamageInfo.java │ │ │ │ ├── DamageProcessor.java │ │ │ │ ├── EntityKilledByEntityEvent.java │ │ │ │ ├── MultiPartValue.java │ │ │ │ ├── iticks │ │ │ │ │ ├── ImmunityTickImpl.java │ │ │ │ │ ├── ImmunityTicks.java │ │ │ │ │ └── ImmunityTicksPlayerImpl.java │ │ │ │ └── weapon │ │ │ │ │ ├── AppliedPotionEffect.java │ │ │ │ │ ├── Weapon.java │ │ │ │ │ ├── WeaponHandler.java │ │ │ │ │ └── WeaponWeight.java │ │ │ │ ├── modifiers │ │ │ │ ├── Modifier.java │ │ │ │ ├── ModifierList.java │ │ │ │ ├── ModifierOperation.java │ │ │ │ ├── ModifierType.java │ │ │ │ ├── PermanentModifier.java │ │ │ │ └── TemporaryModifier.java │ │ │ │ └── player │ │ │ │ ├── PlayerImpl.java │ │ │ │ └── event │ │ │ │ └── PlayerLongDiggingStartEvent.java │ │ └── resources │ │ │ └── data │ │ │ └── modifiers.json │ │ └── test │ │ ├── java │ │ └── net │ │ │ └── hollowcube │ │ │ └── player │ │ │ ├── TestLongDiggingIntegration.java │ │ │ ├── TestModifierList.java │ │ │ ├── TestModifierRegistry.java │ │ │ └── damage │ │ │ └── TestDamageIntegration.java │ │ └── resources │ │ └── data │ │ └── modifiers.json ├── quest │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── java │ │ │ └── net │ │ │ │ └── hollowcube │ │ │ │ └── quest │ │ │ │ ├── Quest.java │ │ │ │ ├── QuestContext.java │ │ │ │ ├── QuestContextImpl.java │ │ │ │ ├── QuestFacet.java │ │ │ │ ├── QuestState.java │ │ │ │ ├── event │ │ │ │ ├── QuestCompleteEvent.java │ │ │ │ ├── QuestObjectiveChangeEvent.java │ │ │ │ └── QuestObjectiveCompleteEvent.java │ │ │ │ ├── objective │ │ │ │ ├── Objective.java │ │ │ │ ├── ObjectiveData.java │ │ │ │ ├── ParallelObjective.java │ │ │ │ └── SequenceObjective.java │ │ │ │ ├── player │ │ │ │ └── QuestManager.java │ │ │ │ └── storage │ │ │ │ ├── MemoryQuestStorage.java │ │ │ │ ├── QuestData.java │ │ │ │ └── QuestStorage.java │ │ └── resources │ │ │ ├── example-quest.json │ │ │ └── quest-spec.txt │ │ └── test │ │ └── java │ │ └── net │ │ └── hollowcube │ │ └── quest │ │ ├── objective │ │ ├── TestParallelObjective.java │ │ └── TestSequenceObjective.java │ │ └── test │ │ ├── MockObjective.java │ │ └── MockQuestContext.java └── 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 /.env: -------------------------------------------------------------------------------- 1 | TEST_VALUE=hello -------------------------------------------------------------------------------- /.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/sync_docs.yml: -------------------------------------------------------------------------------- 1 | name: Sync Docs 2 | 3 | on: 4 | push: 5 | paths: 6 | - docs/** 7 | branches: 8 | - master 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@v2 17 | env: 18 | WIKI_DIR: docs/ 19 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | GH_MAIL: ${{ github.triggering_actor }} 21 | GH_NAME: ${{ github.repository_owner }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/.gitignore 10 | .idea/gradle.xml 11 | .idea/misc.xml 12 | .idea/uiDesigner.xml 13 | .idea/vcs.xml 14 | .idea/jarRepositories.xml 15 | .idea/compiler.xml 16 | .idea/libraries/ 17 | *.iws 18 | *.iml 19 | *.ipr 20 | out/ 21 | !**/src/main/**/out/ 22 | !**/src/test/**/out/ 23 | 24 | ### Eclipse ### 25 | .apt_generated 26 | .classpath 27 | .factorypath 28 | .project 29 | .settings 30 | .springBeans 31 | .sts4-cache 32 | bin/ 33 | !**/src/main/**/bin/ 34 | !**/src/test/**/bin/ 35 | 36 | ### NetBeans ### 37 | /nbproject/private/ 38 | /nbbuild/ 39 | /dist/ 40 | /nbdist/ 41 | /.nb-gradle/ 42 | 43 | ### VS Code ### 44 | .vscode/ 45 | 46 | ### Mac OS ### 47 | .DS_Store 48 | 49 | !.env.sample 50 | !.github 51 | 52 | log.txt 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | todo 4 | 5 | ## Code Style 6 | 7 | There is an attached `.editorconfig` file for basic formatting rules. 8 | 9 | We also make use of [Error Prone](https://errorprone.info) and [NullAway](https://github.com/uber/NullAway), 10 | these warnings should be respected or explicitly ignored if there is a good reason. Parameters and return types 11 | should have nullability annotations using Jetbrains Annotations. Other annotations should be used where they 12 | make sense. 13 | 14 | ## Testing 15 | 16 | All contributions are expected to be reasonably tested. Unit test classes should be named `TestX`, integration 17 | tests should be named `TestXIntegrataion`. Any test using the Minestom test framework (any test annotated with 18 | `@EnvTest`), the test should be appropriately marked as an integration test. 19 | 20 | We use [Truth](https://truth.dev) for assertions. Subjects should be added in repeated cases where an error 21 | would become more clear. There is a guide on this [here](https://truth.dev/extension). 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 HollowCube 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 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("net.ltgt.errorprone") version "2.0.2" apply false 3 | } 4 | -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | # Starlight MMO 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollow-cube/libmmo/2c4926776aa21bbdef8c3c549c9c082e07230128/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 | -------------------------------------------------------------------------------- /modules/block-interactions/README.md: -------------------------------------------------------------------------------- 1 | # Block Interactions 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /modules/block-interactions/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | dependencies { 6 | implementation(project(":modules:common")) 7 | implementation(project(":modules:item")) 8 | implementation(project(":modules:loot-table")) 9 | implementation(project(":modules:player")) 10 | 11 | compileOnly(project(":modules:quest")) 12 | } -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/BlockInteracter.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.event.player.PlayerBlockInteractEvent; 6 | import net.minestom.server.event.player.PlayerBlockPlaceEvent; 7 | import net.minestom.server.instance.block.Block; 8 | import net.minestom.server.item.Material; 9 | import net.hollowcube.blocks.handlers.TillHandler; 10 | 11 | public class BlockInteracter { 12 | 13 | public static void registerEvents() { 14 | MinecraftServer.getGlobalEventHandler().addListener(PlayerBlockPlaceEvent.class, event -> { 15 | if (event.getBlock().registry().material() == Material.DIRT) { 16 | event.setBlock(event.getBlock().withHandler(new TillHandler())); 17 | } 18 | }); 19 | MinecraftServer.getGlobalEventHandler().addListener(PlayerBlockInteractEvent.class, event -> { 20 | Player player = event.getPlayer(); 21 | if (player.getItemInMainHand().material() == Material.WATER_BUCKET) { 22 | player.getInstance().setBlock(event.getBlockPosition().relative(event.getBlockFace()), Block.WATER); 23 | player.setItemInMainHand(player.getItemInMainHand().withMaterial(Material.BUCKET)); 24 | } 25 | }); 26 | BlockInteractionUtils.registerHandlers(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/BlockInteractionUtils.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.instance.block.Block; 5 | import net.minestom.server.tag.Tag; 6 | import net.minestom.server.utils.NamespaceID; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | import net.hollowcube.blocks.data.CropBlockData; 10 | import net.hollowcube.blocks.data.CropBlockDataSerializer; 11 | import net.hollowcube.blocks.handlers.CropHandler; 12 | import net.hollowcube.blocks.handlers.FarmlandHandler; 13 | import net.hollowcube.blocks.handlers.TillHandler; 14 | 15 | public class BlockInteractionUtils { 16 | private static final String DOMAIN_NAME = "unnammedmmo"; 17 | 18 | public static final NamespaceID TILL_HANDLER_ID = NamespaceID.from(DOMAIN_NAME, "tillhandler"); 19 | public static final NamespaceID FARMLAND_HANDLER_ID = NamespaceID.from(DOMAIN_NAME, "farmlandhandler"); 20 | public static final NamespaceID CROP_HANDLER_ID = NamespaceID.from(DOMAIN_NAME, "crophandler"); 21 | 22 | 23 | public static void registerHandlers() { 24 | MinecraftServer.getBlockManager().registerHandler(TILL_HANDLER_ID, TillHandler::new); 25 | MinecraftServer.getBlockManager().registerHandler(FARMLAND_HANDLER_ID, FarmlandHandler::new); 26 | MinecraftServer.getBlockManager().registerHandler(CROP_HANDLER_ID, CropHandler::new); 27 | } 28 | 29 | 30 | private static final Tag cropBlockDataTag = Tag.Structure("CropBlockData", new CropBlockDataSerializer()); 31 | 32 | public static @NotNull Block storeDataOntoBlock(@NotNull Block block, @NotNull CropBlockData data) { 33 | return block.withTag(cropBlockDataTag, data); 34 | } 35 | 36 | /** 37 | * Reads the stored CropBlockData from a block 38 | * 39 | * @param block - the block to read the data from 40 | * @return The stored CropBlockData, or null if the block does not have the proper data 41 | */ 42 | public static @Nullable CropBlockData readDataFromBlock(@NotNull Block block) { 43 | if (block.hasTag(cropBlockDataTag)) { 44 | return block.getTag(cropBlockDataTag); 45 | } else { 46 | return null; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/data/CropBlockData.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.data; 2 | 3 | import net.minestom.server.instance.block.Block; 4 | import net.minestom.server.item.Material; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | // createAnotherBlock boolean is a flag set true for pumpkins and melons, means this seed creates another block when fully grown 8 | public record CropBlockData(@NotNull Material seedMaterial, @NotNull Material cropGrownMaterial, 9 | @NotNull Block cropBlock, int maximumAge, boolean createAnotherBlock) {} 10 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/data/CropBlockDataSerializer.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.data; 2 | 3 | import net.minestom.server.instance.block.Block; 4 | import net.minestom.server.item.Material; 5 | import net.minestom.server.tag.Tag; 6 | import net.minestom.server.tag.TagReadable; 7 | import net.minestom.server.tag.TagSerializer; 8 | import net.minestom.server.tag.TagWritable; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import java.util.Objects; 13 | 14 | public class CropBlockDataSerializer implements TagSerializer { 15 | 16 | private final Tag seedMaterialTag = Tag.Integer("seedMaterial"); 17 | private final Tag cropGrownMaterialTag = Tag.Integer("cropGrownMaterial"); 18 | private final Tag cropBlockTag = Tag.Integer("cropBlock"); 19 | private final Tag maxAgeTag = Tag.Integer("maxAge"); 20 | private final Tag createsOtherTag = Tag.Boolean("createsOther"); 21 | 22 | @Override 23 | public @Nullable CropBlockData read(@NotNull TagReadable reader) { 24 | if (reader.hasTag(seedMaterialTag) && reader.hasTag(cropGrownMaterialTag) && 25 | reader.hasTag(cropBlockTag) && reader.hasTag(maxAgeTag) && reader.hasTag(createsOtherTag)) { 26 | return new CropBlockData( 27 | Objects.requireNonNullElse(Material.fromId(reader.getTag(seedMaterialTag)), Material.AIR), 28 | Objects.requireNonNullElse(Material.fromId(reader.getTag(cropGrownMaterialTag)), Material.AIR), 29 | Objects.requireNonNullElse(Block.fromBlockId(reader.getTag(cropBlockTag)), Block.AIR), 30 | reader.getTag(maxAgeTag), 31 | reader.getTag(createsOtherTag) 32 | ); 33 | } else { 34 | return null; 35 | } 36 | } 37 | 38 | @Override 39 | public void write(@NotNull TagWritable writer, @NotNull CropBlockData value) { 40 | writer.setTag(seedMaterialTag, value.seedMaterial().id()); 41 | writer.setTag(cropGrownMaterialTag, value.cropGrownMaterial().id()); 42 | writer.setTag(cropBlockTag, value.cropBlock().id()); 43 | writer.setTag(maxAgeTag, value.maximumAge()); 44 | writer.setTag(createsOtherTag, value.createAnotherBlock()); 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/handlers/TillHandler.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.handlers; 2 | 3 | import net.hollowcube.blocks.BlockInteractionUtils; 4 | import net.kyori.adventure.sound.Sound; 5 | import net.minestom.server.coordinate.Point; 6 | import net.minestom.server.entity.Player; 7 | import net.minestom.server.instance.Instance; 8 | import net.minestom.server.instance.block.Block; 9 | import net.minestom.server.instance.block.BlockHandler; 10 | import net.minestom.server.item.Material; 11 | import net.minestom.server.sound.SoundEvent; 12 | import net.minestom.server.utils.NamespaceID; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | import java.util.Set; 16 | 17 | public class TillHandler implements BlockHandler { 18 | 19 | private static final Set hoes = Set.of(Material.WOODEN_HOE, Material.STONE_HOE, Material.IRON_HOE, Material.GOLDEN_HOE, Material.DIAMOND_HOE, Material.NETHERITE_HOE); 20 | 21 | @Override 22 | public boolean onInteract(@NotNull Interaction interaction) { 23 | // Till if you have a hoe 24 | Player player = interaction.getPlayer(); 25 | Instance instance = interaction.getInstance(); 26 | Point point = interaction.getBlockPosition(); 27 | if (hoes.contains(player.getItemInMainHand().material())) { 28 | // convert block to farmland 29 | Block block = Block.FARMLAND.withHandler(new FarmlandHandler()); 30 | instance.setBlock(point, block); 31 | instance.playSound(Sound.sound(SoundEvent.ITEM_HOE_TILL, Sound.Source.BLOCK, 1f, 1f), point.blockX(), point.blockY(), point.blockZ()); 32 | // TODO: Damage item in hand? 33 | } 34 | return true; 35 | } 36 | 37 | @Override 38 | public boolean isTickable() { 39 | return false; 40 | } 41 | 42 | @Override 43 | public @NotNull NamespaceID getNamespaceId() { 44 | return BlockInteractionUtils.TILL_HANDLER_ID; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/MiningFacet.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.ore; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.hollowcube.blocks.ore.handler.OreBlockHandler; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.server.Facet; 7 | import net.hollowcube.server.ServerWrapper; 8 | 9 | @AutoService(Facet.class) 10 | public class MiningFacet implements Facet { 11 | 12 | @Override 13 | public void hook(@NotNull ServerWrapper server) { 14 | server.registerBlockHandler(OreBlockHandler::instance); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/event/PlayerOreBreakEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.ore.event; 2 | 3 | import net.minestom.server.entity.Player; 4 | import net.minestom.server.event.trait.BlockEvent; 5 | import net.minestom.server.event.trait.PlayerInstanceEvent; 6 | import net.minestom.server.instance.block.Block; 7 | import org.jetbrains.annotations.NotNull; 8 | import net.hollowcube.blocks.ore.Ore; 9 | 10 | public class PlayerOreBreakEvent implements PlayerInstanceEvent, BlockEvent { 11 | private final Player player; 12 | private final Block block; 13 | private final Ore ore; 14 | 15 | public PlayerOreBreakEvent(Player player, Block block, Ore ore) { 16 | this.player = player; 17 | this.block = block; 18 | this.ore = ore; 19 | } 20 | 21 | @Override 22 | public @NotNull Player getPlayer() { 23 | return player; 24 | } 25 | 26 | @Override 27 | public @NotNull Block getBlock() { 28 | return block; 29 | } 30 | 31 | public @NotNull Ore getOre() { 32 | return ore; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/item/Pickaxe.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.ore.item; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.hollowcube.item.ItemComponent; 6 | 7 | public record Pickaxe( 8 | int miningSpeed 9 | ) implements ItemComponent { 10 | 11 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 12 | Codec.INT.fieldOf("miningSpeed").forGetter(Pickaxe::miningSpeed) 13 | ).apply(i, Pickaxe::new)); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/ore/item/PickaxeHandler.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.ore.item; 2 | 3 | import com.google.auto.service.AutoService; 4 | import com.mojang.serialization.Codec; 5 | import net.hollowcube.blocks.ore.handler.OreBlockHandler; 6 | import net.minestom.server.event.Event; 7 | import net.minestom.server.event.EventNode; 8 | import net.minestom.server.instance.block.Block; 9 | import net.minestom.server.utils.NamespaceID; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | import net.hollowcube.blocks.ore.Ore; 13 | import net.hollowcube.item.Item; 14 | import net.hollowcube.item.ItemComponentHandler; 15 | import net.hollowcube.player.event.PlayerLongDiggingStartEvent; 16 | 17 | @AutoService(ItemComponentHandler.class) 18 | public class PickaxeHandler implements ItemComponentHandler { 19 | 20 | private final EventNode eventNode = EventNode.all("starlight:pickaxe/item_component_handler"); 21 | 22 | public PickaxeHandler() { 23 | eventNode.addListener(PlayerLongDiggingStartEvent.class, this::handleLongDiggingStart); 24 | } 25 | 26 | @Override 27 | public @NotNull NamespaceID namespace() { 28 | return NamespaceID.from("starlight:pickaxe"); 29 | } 30 | 31 | @Override 32 | public @NotNull Class componentType() { 33 | return Pickaxe.class; 34 | } 35 | 36 | @Override 37 | public @NotNull Codec<@NotNull Pickaxe> codec() { 38 | return Pickaxe.CODEC; 39 | } 40 | 41 | @Override 42 | public @Nullable EventNode eventNode() { 43 | return eventNode; 44 | } 45 | 46 | private void handleLongDiggingStart(PlayerLongDiggingStartEvent event) { 47 | var ore = Ore.fromBlock(event.getBlock()); 48 | if (ore == null || event.getBlock().compare(OreBlockHandler.REPLACEMENT_BLOCK, Block.Comparator.STATE)) return; 49 | 50 | // Ensure they have a pickaxe in hand and get the pickaxe 51 | var item = Item.fromItemStack(event.getPlayer().getItemInMainHand()); 52 | //todo will currently fail on any non-custom item 53 | 54 | var pickaxe = item.getComponent(Pickaxe.class); 55 | if (pickaxe == null) return; // Not holding a pickaxe 56 | 57 | // Start mining the block 58 | event.setDiggingBlock( 59 | ore.health(), 60 | pickaxe::miningSpeed 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /modules/block-interactions/src/main/java/net/hollowcube/blocks/util/BlockUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.util; 2 | 3 | import net.minestom.server.instance.block.Block; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public class BlockUtil { 7 | public static boolean isWater(@NotNull Block block) { 8 | if (block.id() == Block.WATER.id()) 9 | return true; 10 | return "true".equals(block.getProperty("waterlogged")); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/block-interactions/src/test/java/net/hollowcube/blocks/util/TestBlockUtil.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.blocks.util; 2 | 3 | import net.minestom.server.instance.block.Block; 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 com.google.common.truth.Truth.assertThat; 11 | 12 | public class TestBlockUtil { 13 | 14 | private static Stream isWaterCases() { 15 | return Stream.of( 16 | Arguments.of(Block.WATER, true), 17 | Arguments.of(Block.fromStateId((short) (Block.WATER.stateId() + 1)), true), 18 | Arguments.of(Block.OAK_STAIRS.withProperty("waterlogged", "true"), true), 19 | Arguments.of(Block.OAK_STAIRS, false), 20 | Arguments.of(Block.AIR, false) 21 | ); 22 | } 23 | 24 | @ParameterizedTest 25 | @MethodSource("isWaterCases") 26 | public void testIsWater(Block block, boolean expected) { 27 | var result = BlockUtil.isWater(block); 28 | assertThat(result).isEqualTo(expected); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import net.ltgt.gradle.errorprone.errorprone 2 | 3 | subprojects { 4 | apply(plugin = "java") 5 | apply(plugin = "net.ltgt.errorprone") 6 | 7 | group = "net.hollowcube.libmmo" 8 | 9 | repositories { 10 | mavenCentral() 11 | maven(url = "https://jitpack.io") 12 | } 13 | 14 | 15 | dependencies { 16 | // A bug with kotlin dsl 17 | val compileOnly by configurations 18 | val implementation by configurations 19 | val annotationProcessor by configurations 20 | val testImplementation by configurations 21 | val testAnnotationProcessor by configurations 22 | val errorprone by configurations 23 | 24 | errorprone("com.google.errorprone:error_prone_core:2.14.0") 25 | errorprone("com.uber.nullaway:nullaway:0.9.8") 26 | 27 | // Auto service (SPI) 28 | annotationProcessor("com.google.auto.service:auto-service:1.0.1") 29 | testAnnotationProcessor("com.google.auto.service:auto-service:1.0.1") 30 | implementation("com.google.auto.service:auto-service-annotations:1.0.1") 31 | 32 | // Minestom 33 | compileOnly("com.github.hollow-cube:Minestom:e84114b752") 34 | 35 | // Testing 36 | testImplementation(project(":modules:test")) 37 | } 38 | 39 | tasks.getByName("test") { 40 | useJUnitPlatform() 41 | } 42 | 43 | tasks.withType { 44 | options.errorprone.disableWarningsInGeneratedCode.set(true) 45 | options.errorprone { 46 | check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) 47 | option("NullAway:AnnotatedPackages", "com.uber") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/chat/README.md: -------------------------------------------------------------------------------- 1 | ## Chat 2 | 3 | General chat module for starlight mmo. 4 | 5 | -------------------------------------------------------------------------------- /modules/chat/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":modules:common")) 3 | 4 | implementation("org.mongodb:mongodb-driver-sync:4.7.0") 5 | } 6 | 7 | tasks.test { 8 | if (project.hasProperty("excludeTests")) { 9 | filter { 10 | excludeTestsMatching(project.property("excludeTests") as String) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /modules/chat/src/main/java/net/hollowcube/chat/ChatFacet.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.minestom.server.event.Event; 5 | import net.minestom.server.event.EventNode; 6 | import net.minestom.server.event.player.PlayerChatEvent; 7 | import net.minestom.server.event.player.PlayerCommandEvent; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.TestOnly; 10 | import net.hollowcube.chat.command.LogCommand; 11 | import net.hollowcube.chat.storage.ChatStorage; 12 | import net.hollowcube.server.Facet; 13 | import net.hollowcube.server.ServerWrapper; 14 | import net.hollowcube.util.EventUtil; 15 | import net.hollowcube.util.FutureUtil; 16 | 17 | import java.time.Instant; 18 | 19 | @AutoService(Facet.class) 20 | public class ChatFacet implements Facet { 21 | 22 | //todo where does this come from? 23 | private static final String SERVER_NAME = "test_server"; 24 | private static final String DEFAULT_CHANNEL = "global"; 25 | private static final String COMMAND_CHANNEL = "command"; 26 | 27 | private final EventNode eventNode = EventUtil 28 | .notCancelledNode("chat") 29 | // Very low priority to run other events which might cancel these beforehand 30 | .setPriority(-10); 31 | 32 | private final ChatStorage storage; 33 | 34 | public ChatFacet() { 35 | //todo this should be based on a config param, which should be loaded before loading managers 36 | this(ChatStorage.noop()); 37 | } 38 | 39 | @TestOnly 40 | public ChatFacet(@NotNull ChatStorage storage) { 41 | this.storage = storage; 42 | 43 | eventNode.addListener(PlayerChatEvent.class, this::handleChatEvent); 44 | eventNode.addListener(PlayerCommandEvent.class, this::handleCommandEvent); 45 | } 46 | 47 | @Override 48 | public void hook(@NotNull ServerWrapper server) { 49 | server.addEventNode(eventNode); 50 | server.registerCommand(new LogCommand(storage)); 51 | } 52 | 53 | public EventNode eventNode() { 54 | return eventNode; 55 | } 56 | 57 | private void handleChatEvent(PlayerChatEvent event) { 58 | // Record message, ignore response. 59 | storage.recordChatMessage(new ChatMessage( 60 | Instant.now(), 61 | SERVER_NAME, 62 | DEFAULT_CHANNEL, 63 | event.getPlayer().getUuid(), 64 | event.getMessage() 65 | )).exceptionally(FutureUtil::handleException); 66 | } 67 | 68 | private void handleCommandEvent(PlayerCommandEvent event) { 69 | // Record command, ignore response. 70 | storage.recordChatMessage(new ChatMessage( 71 | Instant.now(), 72 | SERVER_NAME, 73 | COMMAND_CHANNEL, 74 | event.getPlayer().getUuid(), 75 | event.getCommand() 76 | )).exceptionally(FutureUtil::handleException); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /modules/chat/src/main/java/net/hollowcube/chat/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.time.Instant; 6 | import java.util.UUID; 7 | 8 | public record ChatMessage( 9 | @NotNull Instant timestamp, 10 | @NotNull String serverId, 11 | @NotNull String context, 12 | @NotNull UUID sender, 13 | @NotNull String message 14 | ) { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /modules/chat/src/main/java/net/hollowcube/chat/storage/ChatStorage.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat.storage; 2 | 3 | import com.mongodb.ConnectionString; 4 | import com.mongodb.MongoClientSettings; 5 | import com.mongodb.client.MongoClient; 6 | import com.mongodb.client.MongoClients; 7 | import net.hollowcube.chat.ChatMessage; 8 | import net.hollowcube.chat.ChatQuery; 9 | import org.bson.UuidRepresentation; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import net.hollowcube.config.ConfigProvider; 14 | import net.hollowcube.dfu.ExtraCodecs; 15 | import net.hollowcube.mongo.MongoConfig; 16 | 17 | import java.util.List; 18 | import java.util.concurrent.CompletableFuture; 19 | 20 | public interface ChatStorage { 21 | static @NotNull ChatStorage noop() { 22 | return new NoopChatStorage(); 23 | } 24 | 25 | static @NotNull ChatStorage mongo(MongoClient client) { 26 | return new MongoChatStorage(client); 27 | } 28 | 29 | static @NotNull ChatStorage fromConfig() { 30 | Logger logger = LoggerFactory.getLogger(ChatStorage.class); 31 | 32 | enum Type {NOOP, MONGO} 33 | Type type = ConfigProvider.load("chat_storage_type", ExtraCodecs.forEnum(Type.class).orElse(Type.NOOP)); 34 | logger.info("Using {} chat storage", type); 35 | 36 | return switch (type) { 37 | case NOOP -> noop(); 38 | case MONGO -> { 39 | //todo need to have a common mongo client somewhere, not worth recreating every time 40 | MongoConfig config = ConfigProvider.load("mongo", MongoConfig.CODEC); 41 | MongoClient client = MongoClients.create(MongoClientSettings.builder() 42 | .applyConnectionString(new ConnectionString(config.uri())) 43 | .uuidRepresentation(UuidRepresentation.STANDARD) 44 | .build()); 45 | yield mongo(client); 46 | } 47 | }; 48 | } 49 | 50 | 51 | /** 52 | * Record a chat message to storage. How/when/if the message is written is up to the implementing class, but may not 53 | * block the current thread to do so. 54 | * 55 | * @param message The chat message to save 56 | */ 57 | CompletableFuture recordChatMessage(@NotNull ChatMessage message); 58 | 59 | CompletableFuture> queryChatMessages(@NotNull ChatQuery query); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /modules/chat/src/main/java/net/hollowcube/chat/storage/NoopChatStorage.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat.storage; 2 | 3 | import net.hollowcube.chat.ChatMessage; 4 | import net.hollowcube.chat.ChatQuery; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.List; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | /** 11 | * Dummy chat storage which does not do anything with the messages. 12 | */ 13 | class NoopChatStorage implements ChatStorage { 14 | 15 | @Override 16 | public CompletableFuture recordChatMessage(@NotNull ChatMessage message) { 17 | return CompletableFuture.completedFuture(null); 18 | } 19 | 20 | @Override 21 | public CompletableFuture> queryChatMessages(@NotNull ChatQuery query) { 22 | return CompletableFuture.completedFuture(List.of()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/chat/src/test/java/net/hollowcube/chat/TestChatManager.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat; 2 | 3 | import net.kyori.adventure.text.Component; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.event.player.PlayerChatEvent; 6 | import org.junit.jupiter.api.Test; 7 | import net.hollowcube.chat.storage.MockChatStorage; 8 | import net.hollowcube.test.TestUtil; 9 | 10 | import java.util.Collections; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertFalse; 14 | 15 | public class TestChatManager { 16 | private final MockChatStorage storage = new MockChatStorage(); 17 | private final ChatFacet manager = new ChatFacet(storage); 18 | private final Player player = TestUtil.headlessPlayer(); 19 | 20 | @Test 21 | public void testAddChatMessage() { 22 | 23 | PlayerChatEvent event = new PlayerChatEvent( 24 | player, 25 | Collections.emptyList(), 26 | () -> Component.text(""), 27 | "test message 1" 28 | ); 29 | 30 | // Call without cancellation even though it is cancellable. 31 | manager.eventNode().call(event); 32 | 33 | assertFalse(event.isCancelled()); 34 | ChatMessage message = storage.assertOneMessage(); 35 | assertEquals(player.getUuid(), message.sender()); 36 | assertEquals("global", message.context()); 37 | assertEquals("test message 1", message.message()); 38 | } 39 | 40 | @Test 41 | public void testIgnoreCancelledChatEvents() { 42 | 43 | PlayerChatEvent event = new PlayerChatEvent( 44 | player, 45 | Collections.emptyList(), 46 | () -> Component.text(""), 47 | "test message 1" 48 | ); 49 | 50 | // Call without cancellation even though it is cancellable. 51 | event.setCancelled(true); 52 | manager.eventNode().call(event); 53 | 54 | storage.assertEmpty(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /modules/chat/src/test/java/net/hollowcube/chat/TestChatQuery.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | import static com.google.common.truth.Truth.assertThat; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | public class TestChatQuery { 12 | 13 | @Test 14 | public void testBuilderMethods() { 15 | UUID[] uuids = new UUID[]{new UUID(0, 0), new UUID(0, 1), new UUID(0, 2), new UUID(0, 3)}; 16 | ChatQuery query = ChatQuery.builder() 17 | .serverId("a", "b") 18 | .serverIds(List.of("c", "d")) 19 | .context("1", "2") 20 | .contexts(List.of("3", "4")) 21 | .sender(uuids[0], uuids[1]) 22 | .senders(List.of(uuids[2], uuids[3])) 23 | .message("test message") 24 | .build(); 25 | 26 | assertThat(query.serverIds()).containsExactly("a", "b", "c", "d"); 27 | assertThat(query.contexts()).containsExactly("1", "2", "3", "4"); 28 | assertThat(query.senders()).containsExactly((Object[]) uuids); 29 | assertThat(query.message()).isEqualTo("test message"); 30 | } 31 | 32 | @Test 33 | public void testInvalidServerId() { 34 | ChatQuery.Builder query = ChatQuery.builder() 35 | .serverId("*"); 36 | 37 | var exc = assertThrows(IllegalArgumentException.class, query::build); 38 | assertThat(exc.getMessage()).isEqualTo("Illegal character in serverId '*'"); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /modules/chat/src/test/java/net/hollowcube/chat/command/TestLogCommand.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat.command; 2 | 3 | import net.hollowcube.chat.ChatQuery; 4 | import net.minestom.server.MinecraftServer; 5 | import net.minestom.server.command.builder.CommandDispatcher; 6 | import net.minestom.server.command.builder.CommandResult; 7 | import net.minestom.server.entity.Player; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | import net.hollowcube.chat.storage.MockChatStorage; 13 | import net.hollowcube.test.TestUtil; 14 | 15 | import java.util.stream.Stream; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | public class TestLogCommand { 20 | private final CommandDispatcher dispatcher = new CommandDispatcher(); 21 | // Fine to use headless player since this command does not affect the world or anything 22 | private final Player player = TestUtil.headlessPlayer(); 23 | private final MockChatStorage storage = new MockChatStorage(); 24 | 25 | @BeforeEach 26 | public void setup() { 27 | dispatcher.register(new LogCommand(storage)); 28 | } 29 | 30 | private static Stream commandToQueryMappings() { 31 | return Stream.of( 32 | Arguments.of("log in global", ChatQuery.builder().context("global").build()), 33 | Arguments.of("log context global", ChatQuery.builder().context("global").build()), 34 | Arguments.of("log in global in local", ChatQuery.builder().context("global", "local").build()), 35 | Arguments.of("log on build", ChatQuery.builder().serverId("build").build()), 36 | Arguments.of("log on build in global", ChatQuery.builder().context("global").serverId("build").build()) 37 | ); 38 | } 39 | 40 | @ParameterizedTest 41 | @MethodSource("commandToQueryMappings") 42 | public void basicExecution(String command, ChatQuery expectedQuery) { 43 | MinecraftServer.init(); 44 | CommandResult result = dispatcher.execute(player, command); 45 | 46 | assertThat(result.getType()).isEqualTo(CommandResult.Type.SUCCESS); 47 | assertThat(storage.queries()).containsExactly(expectedQuery); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /modules/chat/src/test/java/net/hollowcube/chat/storage/MockChatStorage.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.chat.storage; 2 | 3 | import net.hollowcube.chat.ChatMessage; 4 | import net.hollowcube.chat.ChatQuery; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public record MockChatStorage( 14 | @NotNull List messages, 15 | @NotNull List queries 16 | ) implements ChatStorage { 17 | 18 | public MockChatStorage() { 19 | this(new ArrayList<>(), new ArrayList<>()); 20 | } 21 | 22 | @Override 23 | public CompletableFuture recordChatMessage(@NotNull ChatMessage message) { 24 | messages.add(message); 25 | return CompletableFuture.completedFuture(null); 26 | } 27 | 28 | @Override 29 | public CompletableFuture> queryChatMessages(@NotNull ChatQuery query) { 30 | queries.add(query); 31 | return CompletableFuture.completedFuture(List.of()); 32 | } 33 | 34 | public ChatMessage assertOneMessage() { 35 | assertEquals(1, messages.size()); 36 | return messages.get(0); 37 | } 38 | 39 | public void assertEmpty() { 40 | assertEquals(0, messages.size()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /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.hollow-cube:DataFixerUpper:cf58e926a6") 7 | api("net.kyori:adventure-text-minimessage:4.11.0") 8 | 9 | api("com.github.mworzala.mc_debug_renderer:minestom:1.19.2-rv1") 10 | 11 | 12 | implementation("org.tinylog:tinylog-impl:2.4.1") 13 | 14 | implementation("io.github.cdimascio:dotenv-java:2.2.4") 15 | 16 | // Optional components 17 | compileOnly("org.mongodb:mongodb-driver-sync:4.7.0") 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /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/command/ExtraArguments.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.command; 2 | import net.hollowcube.registry.Registry; 3 | import net.hollowcube.registry.Resource; 4 | import net.minestom.server.command.builder.arguments.Argument; 5 | import net.minestom.server.command.builder.arguments.ArgumentType; 6 | import net.minestom.server.command.builder.exception.ArgumentSyntaxException; 7 | import net.minestom.server.command.builder.suggestion.SuggestionEntry; 8 | import net.minestom.server.utils.NamespaceID; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public final class ExtraArguments { 12 | private ExtraArguments() {} 13 | 14 | /** 15 | * Returns an argument that targets a Resource within a Registry. The argument will suggest the resource names, 16 | * including just the path. 17 | *

18 | * The namespace AND id must be provided to match. 19 | */ 20 | public static @NotNull Argument Resource(@NotNull Registry registry, @NotNull String id) { 21 | final var ids = registry.keys().stream().map(NamespaceID::from).toList(); 22 | System.out.println("Create tool with ids: " + ids); 23 | return ArgumentType.ResourceLocation(id) 24 | .setSuggestionCallback((sender, context, suggestions) -> { 25 | String input = suggestions.getInput().substring(suggestions.getStart() - suggestions.getLength()).trim(); 26 | 27 | System.out.println("arg "+ input); 28 | 29 | var i = 0; 30 | for (var namespace : ids) { 31 | if (i++ > 30) break; // Do not send too many suggestions 32 | if (namespace.asString().startsWith(input) || namespace.path().startsWith(input)) { 33 | suggestions.addEntry(new SuggestionEntry(namespace.asString())); 34 | } 35 | } 36 | }) 37 | .map(s -> { 38 | var value = registry.get(s); 39 | if (value == null) throw new ArgumentSyntaxException("Unknown resource: " + s, s, 0); 40 | return value; 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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.minestom.server.utils.validate.Check; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.dfu.EnvVarOps; 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.minestom.server.utils.NamespaceID; 8 | import org.jetbrains.annotations.NotNull; 9 | import net.hollowcube.dfu.ExtraCodecs; 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/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/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 | public static @NotNull Logger getLogger(@NotNull Class clazz) { 9 | return new LoggerImpl(org.slf4j.LoggerFactory.getLogger(clazz)); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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/MongoConfig.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.mongo; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public record MongoConfig( 8 | @NotNull String uri, 9 | boolean useTransactions 10 | ) { 11 | 12 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 13 | Codec.STRING.fieldOf("uri").forGetter(MongoConfig::uri), 14 | Codec.BOOL.optionalFieldOf("use_transactions", false).forGetter(MongoConfig::useTransactions) 15 | ).apply(i, MongoConfig::new)); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /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 keys() { 26 | return delegate.keySet(); 27 | } 28 | 29 | @Override 30 | public @NotNull Collection values() { 31 | return delegate.values(); 32 | } 33 | 34 | @Override 35 | public int size() { 36 | return delegate.size(); 37 | } 38 | 39 | @Override 40 | public @NotNull Index index(Function mapper) { 41 | Map index = values().stream().collect(Collectors.toMap(mapper, i -> i)); 42 | return new MapIndex<>(index); 43 | } 44 | 45 | 46 | static class MapIndex implements Index { 47 | private final Map delegate; 48 | 49 | MapIndex(Map delegate) { 50 | this.delegate = delegate; 51 | } 52 | 53 | @Override 54 | public @UnknownNullability T get(K key) { 55 | return delegate.get(key); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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/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 | } 13 | -------------------------------------------------------------------------------- /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/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/libmmo/2c4926776aa21bbdef8c3c549c9c082e07230128/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/development/.env.sample: -------------------------------------------------------------------------------- 1 | # todo unused currently 2 | 3 | # Chat storage type: noop(default), memory, database 4 | # Chooses which chat storage is used in the dev server. 5 | CHAT_STORAGE_TYPE=noop 6 | -------------------------------------------------------------------------------- /modules/development/README.md: -------------------------------------------------------------------------------- 1 | ## Development server 2 | 3 | Intended to be run both locally and on a remote server. Provides tooling for content creation and debugging. 4 | 5 | Since the server is meant to run locally, services should be optionally stubbed out when relevant. 6 | For example, a noop chat storage system is available in case you are not doing anything related and 7 | do not want to run a database for it to use. 8 | 9 | ## Configuring 10 | 11 | todo 12 | -------------------------------------------------------------------------------- /modules/development/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | application 3 | } 4 | 5 | dependencies { 6 | implementation("com.github.hollow-cube:Minestom:e84114b752") 7 | implementation(project(":modules:common")) 8 | implementation(project(":modules:chat")) 9 | implementation(project(":modules:block-interactions")) 10 | implementation(project(":modules:item")) 11 | implementation(project(":modules:player")) 12 | implementation(project(":modules:quest")) 13 | 14 | implementation("org.mongodb:mongodb-driver-sync:4.7.1") 15 | } 16 | 17 | application { 18 | mainClass.set("net.hollowcube.server.dev.Main") 19 | } 20 | 21 | tasks.named("run", JavaExec::class) { 22 | workingDir("build") 23 | } 24 | -------------------------------------------------------------------------------- /modules/development/src/main/java/net/hollowcube/server/dev/command/BaseCommandRegister.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.dev.command; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.command.CommandManager; 5 | 6 | public class BaseCommandRegister { 7 | 8 | public static void registerCommands() { 9 | CommandManager commandManager = MinecraftServer.getCommandManager(); 10 | commandManager.register(new StopCommand()); 11 | commandManager.register(new GameModeCommand()); 12 | commandManager.register(new GiveCommand()); 13 | commandManager.register(new CraftCommand()); 14 | commandManager.register(new ModifierCommand()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/development/src/main/java/net/hollowcube/server/dev/command/CraftCommand.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.dev.command; 2 | 3 | import net.hollowcube.item.crafting.CraftingInventory; 4 | import net.hollowcube.item.crafting.RecipeList; 5 | import net.hollowcube.item.crafting.ToolCraftingInventory; 6 | import net.kyori.adventure.text.Component; 7 | import net.kyori.adventure.text.format.NamedTextColor; 8 | import net.minestom.server.command.builder.Command; 9 | import net.minestom.server.command.builder.arguments.ArgumentType; 10 | import net.minestom.server.command.builder.arguments.ArgumentWord; 11 | import net.minestom.server.entity.Player; 12 | 13 | import java.util.Locale; 14 | 15 | public class CraftCommand extends Command { 16 | public CraftCommand() { 17 | super("craft"); 18 | 19 | ArgumentWord typeArg = ArgumentType.Word("type").from("normal", "tool"); 20 | 21 | addSyntax((sender, context) -> { 22 | if(sender instanceof Player player) { 23 | String type = context.get(typeArg).toLowerCase(Locale.ROOT); 24 | switch (type) { 25 | case "normal" -> player.openInventory(new CraftingInventory(new RecipeList())); 26 | case "tool" -> player.openInventory(new ToolCraftingInventory(new RecipeList())); 27 | } 28 | } else { 29 | sender.sendMessage(Component.text("Only players can use this command!", NamedTextColor.RED)); 30 | } 31 | }, typeArg); 32 | } 33 | } -------------------------------------------------------------------------------- /modules/development/src/main/java/net/hollowcube/server/dev/command/GameModeCommand.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.dev.command; 2 | 3 | import net.minestom.server.command.builder.Command; 4 | import net.minestom.server.command.builder.arguments.ArgumentEnum; 5 | import net.minestom.server.command.builder.arguments.ArgumentType; 6 | import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity; 7 | import net.minestom.server.entity.Entity; 8 | import net.minestom.server.entity.GameMode; 9 | import net.minestom.server.entity.Player; 10 | 11 | public class GameModeCommand extends Command { 12 | public GameModeCommand() { 13 | super("gamemode", "gm"); 14 | 15 | ArgumentEnum modeArg = ArgumentType.Enum("gamemode", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED); 16 | ArgumentEntity playerArg = ArgumentType.Entity("players").onlyPlayers(true); 17 | 18 | addSyntax((sender, context) -> { 19 | if (sender instanceof Player player) { 20 | player.setGameMode(context.get(modeArg)); 21 | } 22 | }, modeArg); 23 | 24 | addSyntax((sender, context) -> { 25 | GameMode mode = context.get(modeArg); 26 | for (Entity entity : context.get(playerArg).find(sender)) { 27 | if (entity instanceof Player player) { 28 | player.setGameMode(mode); 29 | } 30 | } 31 | }, modeArg, playerArg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/development/src/main/java/net/hollowcube/server/dev/command/GiveCommand.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.dev.command; 2 | 3 | import net.minestom.server.command.builder.Command; 4 | import net.minestom.server.command.builder.arguments.ArgumentType; 5 | import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity; 6 | import net.minestom.server.command.builder.arguments.minecraft.ArgumentItemStack; 7 | import net.minestom.server.command.builder.arguments.number.ArgumentInteger; 8 | import net.minestom.server.entity.Entity; 9 | import net.minestom.server.entity.Player; 10 | import net.minestom.server.item.ItemStack; 11 | 12 | public class GiveCommand extends Command { 13 | 14 | public GiveCommand() { 15 | super("give"); 16 | 17 | ArgumentItemStack itemArg = ArgumentType.ItemStack("item"); 18 | ArgumentInteger amountArg = ArgumentType.Integer("amount"); 19 | ArgumentEntity playerArg = ArgumentType.Entity("players").onlyPlayers(true); 20 | 21 | amountArg.setDefaultValue(1); 22 | amountArg.between(1, 64); 23 | 24 | addSyntax((sender, context) -> { 25 | int amount = context.get(amountArg); 26 | if (sender instanceof Player player) { 27 | player.getInventory().addItemStack(context.get(itemArg).withAmount(amount)); 28 | } 29 | }, itemArg, amountArg); 30 | 31 | addSyntax((sender, context) -> { 32 | int amount = context.get(amountArg); 33 | ItemStack stack = context.get(itemArg).withAmount(amount); 34 | for (Entity entity : context.get(playerArg).find(sender)) { 35 | if (entity instanceof Player player) { 36 | player.getInventory().addItemStack(stack); 37 | } 38 | } 39 | }, itemArg, playerArg, amountArg); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /modules/development/src/main/java/net/hollowcube/server/dev/command/StopCommand.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.dev.command; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.command.builder.Command; 5 | 6 | public class StopCommand extends Command { 7 | public StopCommand() { 8 | super("stop"); 9 | 10 | setDefaultExecutor((sender, context) -> MinecraftServer.stopCleanly()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/development/src/main/java/net/hollowcube/server/dev/tool/DebugTool.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.dev.tool; 2 | 3 | import net.minestom.server.coordinate.Point; 4 | import net.minestom.server.entity.Entity; 5 | import net.minestom.server.entity.Player; 6 | import net.minestom.server.item.ItemStack; 7 | import net.minestom.server.utils.NamespaceID; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | import net.hollowcube.registry.Resource; 11 | 12 | /** 13 | * Debug tool for interacting with certain game features. Exists only in development server. 14 | *

15 | * Known limitations 16 | *

    17 | *
  • Entering/exiting hand is a bit unreliable (but can be improved)
  • 18 | *
  • Left clicked entity is never present (but should be added in the future)
  • 19 | *
  • Only main hand events are supported. This is because hand animation is being 20 | * used for left clicking and we currently do not trigger it for off hand.
  • 21 | *
22 | */ 23 | public interface DebugTool extends Resource { 24 | 25 | @Override 26 | @NotNull NamespaceID namespace(); 27 | 28 | @NotNull ItemStack itemStack(); 29 | 30 | 31 | // Interaction functions 32 | 33 | default void enteredHand(@NotNull Player player, @NotNull ItemStack itemStack) { 34 | } 35 | 36 | default void exitedHand(@NotNull Player player, @NotNull ItemStack itemStack) { 37 | } 38 | 39 | 40 | default @NotNull ItemStack leftClicked(@NotNull Player player, 41 | @NotNull ItemStack itemStack, 42 | @Nullable Point targetBlock, 43 | @Nullable Entity targetEntity) { 44 | return itemStack; 45 | } 46 | 47 | default @NotNull ItemStack rightClicked(@NotNull Player player, 48 | @NotNull ItemStack itemStack, 49 | @Nullable Point targetBlock, 50 | @Nullable Entity targetEntity) { 51 | return itemStack; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/development/src/main/java/net/hollowcube/server/dev/tool/HelloDebugTool.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.server.dev.tool; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.minestom.server.coordinate.Point; 5 | import net.minestom.server.entity.Entity; 6 | import net.minestom.server.entity.Player; 7 | import net.minestom.server.item.ItemStack; 8 | import net.minestom.server.item.Material; 9 | import net.minestom.server.utils.NamespaceID; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | import net.hollowcube.util.ComponentUtil; 13 | 14 | @AutoService(DebugTool.class) 15 | public class HelloDebugTool implements DebugTool { 16 | 17 | @Override 18 | public @NotNull NamespaceID namespace() { 19 | return NamespaceID.from("starlight:hello"); 20 | } 21 | 22 | @Override 23 | public @NotNull ItemStack itemStack() { 24 | return ItemStack.builder(Material.STICK) 25 | .displayName(ComponentUtil.fromStringSafe("Hello Debug Tool")) 26 | .lore(ComponentUtil.fromStringSafe("Capabilities:\na\nb\nc\nd")) 27 | .build(); 28 | } 29 | 30 | @Override 31 | public void enteredHand(@NotNull Player player, @NotNull ItemStack itemStack) { 32 | player.sendMessage("HDT: Entered hand"); 33 | } 34 | 35 | @Override 36 | public void exitedHand(@NotNull Player player, @NotNull ItemStack itemStack) { 37 | player.sendMessage("HDT: Exited hand"); 38 | } 39 | 40 | @Override 41 | public @NotNull ItemStack leftClicked(@NotNull Player player, @NotNull ItemStack itemStack, @Nullable Point targetBlock, @Nullable Entity targetEntity) { 42 | player.sendMessage("HDT: Left clicked"); 43 | return itemStack; 44 | } 45 | 46 | @Override 47 | public @NotNull ItemStack rightClicked(@NotNull Player player, @NotNull ItemStack itemStack, @Nullable Point targetBlock, @Nullable Entity targetEntity) { 48 | player.sendMessage("HDT: Right clicked"); 49 | return itemStack; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/development/src/main/resources/data/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "starlight:diamond_pickaxe", 4 | "id": 0, 5 | "material": "minecraft:diamond_pickaxe", 6 | "components": [ 7 | { 8 | "type": "starlight:pickaxe", 9 | "miningSpeed": 5 10 | }, 11 | { 12 | "type": "starlight:rarity", 13 | "value": "common" 14 | } 15 | ], 16 | "defaultStateId": 0, 17 | "states": { 18 | "[]": { 19 | "stateId": 0 20 | } 21 | } 22 | }, 23 | { 24 | "namespace": "starlight:gold_ingot", 25 | "id": 1, 26 | "material": "minecraft:gold_ingot", 27 | "defaultStateId": 1, 28 | "states": { 29 | "[]": { 30 | "stateId": 1 31 | } 32 | } 33 | } 34 | ] -------------------------------------------------------------------------------- /modules/development/src/main/resources/data/loot_table.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "starlight:gold_ore", 4 | "pools": [ 5 | { 6 | "rolls": 1, 7 | "entries": [ 8 | { 9 | "type": "starlight:item", 10 | "item": "starlight:gold_ingot" 11 | } 12 | ] 13 | } 14 | ] 15 | } 16 | ] -------------------------------------------------------------------------------- /modules/development/src/main/resources/data/ore.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "starlight:gold_ore", 4 | "oreBlock": "minecraft:gold_ore", 5 | "health": 100, 6 | "lootTable": "starlight:gold_ore" 7 | }, 8 | { 9 | "namespace": "starlight:diamond_ore", 10 | "oreBlock": "minecraft:diamond_ore", 11 | "health": 50, 12 | "lootTable": "starlight:gold_ore" 13 | } 14 | ] -------------------------------------------------------------------------------- /modules/development/src/main/resources/data/quest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "starlight:test_1", 4 | "objective": { 5 | "type": "starlight:sequence", 6 | "children": [ 7 | { 8 | "type": "starlight:mine_ore", 9 | "ore": "starlight:gold_ore", 10 | "count": 3 11 | }, 12 | { 13 | "type": "starlight:mine_ore", 14 | "ore": "starlight:diamond_ore", 15 | "count": 2 16 | } 17 | ] 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /modules/development/src/main/resources/lang/en_US.properties: -------------------------------------------------------------------------------- 1 | # Items 2 | item.starlight.diamond_pickaxe.name=Diamond Pickaxe 3 | # Objectives 4 | objective.mine_ore.status=Mine {2} {0} ({1}/{2}) 5 | 6 | # Modifiers 7 | command.modifier.invalid_argument_num=Invalid mode for the amount of arguments supplied (tried the {0} mode, got {1} instead) -------------------------------------------------------------------------------- /modules/development/src/main/resources/tinylog.properties: -------------------------------------------------------------------------------- 1 | #writer=custom json 2 | #writer.level=debug 3 | #writer.file=log.txt 4 | #writer.format=LDJSON 5 | #writer.field.level=level 6 | #writer.field.source={class}.{method}() 7 | #writer.field.message=message 8 | #writer.charset=UTF-8 9 | #writer.append=true 10 | writer=console 11 | writer.format={date: HH:mm:ss.SSS} {level}: {message} 12 | -------------------------------------------------------------------------------- /modules/item/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":modules:common")) 3 | implementation(project(":modules:loot-table")) 4 | } -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/ItemComponent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item; 2 | 3 | import com.mojang.serialization.Codec; 4 | 5 | /** 6 | * Base class for all {@link ItemComponent}s. See {@link ItemComponentHandler} for more information. 7 | */ 8 | public interface ItemComponent { 9 | 10 | Codec CODEC = ItemComponentHandler.CODEC.dispatch("type", ItemComponentHandler::from, ItemComponentHandler::codec); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/ItemComponentRegistry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item; 2 | 3 | import net.hollowcube.registry.Registry; 4 | import org.jetbrains.annotations.ApiStatus; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.ServiceLoader; 9 | 10 | @ApiStatus.Internal 11 | class ItemComponentRegistry { 12 | 13 | static final Registry> REGISTRY = Registry.manual( 14 | "component_handler", 15 | () -> { 16 | List> handlers = new ArrayList<>(); 17 | for (ItemComponentHandler handler : ServiceLoader.load(ItemComponentHandler.class)) 18 | handlers.add(handler); 19 | return handlers; 20 | } 21 | ); 22 | 23 | static final Registry.Index> COMPONENT_ID_INDEX = REGISTRY.index(ItemComponentHandler::name); 24 | 25 | static final Registry.Index, ItemComponentHandler> COMPONENT_CLASS_INDEX = REGISTRY.index(ItemComponentHandler::componentType); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/ItemManager.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.hollowcube.server.Facet; 5 | import net.hollowcube.server.ServerWrapper; 6 | import net.minestom.server.event.Event; 7 | import net.minestom.server.event.EventNode; 8 | import org.jetbrains.annotations.ApiStatus; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | @AutoService(Facet.class) 14 | @ApiStatus.Internal 15 | public class ItemManager implements Facet { 16 | private static final Logger logger = LoggerFactory.getLogger(ItemManager.class); 17 | 18 | @Override 19 | public void hook(@NotNull ServerWrapper server) { 20 | EventNode eventNode = EventNode.all("starlight:item/facet"); 21 | server.addEventNode(eventNode); 22 | 23 | // Component handlers 24 | for (ItemComponentHandler handler : ItemComponentRegistry.REGISTRY.values()) { 25 | 26 | // Register event nodes 27 | final var handlerEventNode = handler.eventNode(); 28 | if (handlerEventNode != null) { 29 | eventNode.addChild(handlerEventNode); 30 | } 31 | } 32 | logger.debug("Loaded {} item components", ItemComponentRegistry.REGISTRY.size()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/crafting/CraftingRecipe.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.crafting; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.minestom.server.item.ItemStack; 6 | import org.jetbrains.annotations.NotNull; 7 | import net.hollowcube.item.Item; 8 | 9 | import java.util.List; 10 | 11 | public interface CraftingRecipe { 12 | /** 13 | * Checks against the list of items in the current crafting menu to see if the recipe matches 14 | * 15 | * @param items A list of items in the crafting menu, ordered from top row (starting from the left), to bottom 16 | * right 17 | * @return true if the recipe matches, false if it does not 18 | */ 19 | boolean doesRecipeMatch(@NotNull List items); 20 | 21 | @NotNull ItemStack getRecipeOutput(); 22 | 23 | boolean containsIngredient(@NotNull ItemStack itemStack); 24 | 25 | boolean requiresTool(); 26 | 27 | record ComponentEntry(Item item, int count) {} 28 | 29 | Codec ENTRY_CODEC = RecordCodecBuilder.create(i -> i.group( 30 | Item.CODEC.fieldOf("item").forGetter(ComponentEntry::item), 31 | Codec.INT.optionalFieldOf("count", 1).forGetter(ComponentEntry::count) 32 | ).apply(i, ComponentEntry::new)); 33 | } 34 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/crafting/RecipeList.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.crafting; 2 | 3 | import net.minestom.server.item.ItemStack; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class RecipeList { 11 | 12 | private final List recipeList = new ArrayList<>(); 13 | 14 | public void addRecipe(CraftingRecipe recipe) {recipeList.add(recipe);} 15 | 16 | public void addRecipes(List list) { 17 | recipeList.addAll(list); 18 | } 19 | 20 | public @NotNull List getRecipeList() { 21 | return recipeList; 22 | } 23 | 24 | public @NotNull List findRecipesWithIngredient(@NotNull ItemStack itemStack) { 25 | ArrayList list = new ArrayList<>(); 26 | for (CraftingRecipe recipe : recipeList) { 27 | if (recipe.containsIngredient(itemStack)) { 28 | list.add(recipe); 29 | } 30 | } 31 | return list; 32 | } 33 | 34 | public @Nullable CraftingRecipe findRecipeForItem(@NotNull ItemStack itemStack) { 35 | for (CraftingRecipe recipe : recipeList) { 36 | if (recipe.getRecipeOutput().equals(itemStack)) { 37 | return recipe; 38 | } 39 | } 40 | return null; 41 | } 42 | } -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/crafting/ShapedCraftingRecipe.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.crafting; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.minestom.server.item.ItemStack; 6 | import org.jetbrains.annotations.NotNull; 7 | import net.hollowcube.dfu.ExtraCodecs; 8 | import net.hollowcube.item.Item; 9 | import net.hollowcube.item.ItemImpl; 10 | 11 | import java.util.List; 12 | 13 | public record ShapedCraftingRecipe(@NotNull List recipe, 14 | @NotNull ItemStack output) implements CraftingRecipe { 15 | 16 | public ShapedCraftingRecipe { 17 | if (recipe.size() != 9) { 18 | throw new IllegalArgumentException("Shaped crafting recipe does not have exactly 9 items (use air for empty slots)!"); 19 | } 20 | } 21 | 22 | @Override 23 | public boolean doesRecipeMatch(@NotNull List items) { 24 | for (int i = 0; i < recipe.size(); i++) { 25 | if (recipe.get(i).item() == ItemImpl.EMPTY_ITEM) continue; 26 | if (recipe.get(i).item().stateId() != Item.fromItemStack(items.get(i)).stateId() || items.get(i).amount() < recipe.get(i).count()) { 27 | return false; 28 | } 29 | } 30 | return true; 31 | } 32 | 33 | @Override 34 | public @NotNull ItemStack getRecipeOutput() { 35 | return output; 36 | } 37 | 38 | @Override 39 | public boolean containsIngredient(@NotNull ItemStack itemStack) { 40 | int stateId = Item.fromItemStack(itemStack).stateId(); 41 | for (ComponentEntry entry : recipe) { 42 | if (entry.item().stateId() != stateId) { 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | 49 | @Override 50 | public boolean requiresTool() { 51 | return false; 52 | } 53 | 54 | 55 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 56 | ENTRY_CODEC.listOf().fieldOf("components").forGetter(ShapedCraftingRecipe::recipe), 57 | ExtraCodecs.MATERIAL.fieldOf("output").xmap(ItemStack::of, ItemStack::material).forGetter(ShapedCraftingRecipe::output) 58 | ).apply(i, ShapedCraftingRecipe::new)); 59 | } 60 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/crafting/ShapelessCraftingRecipe.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.crafting; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.minestom.server.item.ItemStack; 6 | import org.jetbrains.annotations.NotNull; 7 | import net.hollowcube.dfu.ExtraCodecs; 8 | import net.hollowcube.item.Item; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public record ShapelessCraftingRecipe(@NotNull List recipe, 14 | @NotNull ItemStack output) implements CraftingRecipe { 15 | 16 | public ShapelessCraftingRecipe { 17 | if (recipe.size() > 9) { 18 | throw new IllegalArgumentException("Cannot create a shapeless recipe with more than 9 items!"); 19 | } 20 | } 21 | 22 | @Override 23 | public boolean doesRecipeMatch(@NotNull List items) { 24 | ArrayList recipeClone = new ArrayList<>(recipe); 25 | for (ItemStack item : items) { 26 | if (item.isAir()) continue; 27 | // O(n)2 time, :( 28 | // But I don't think there's a better way 29 | 30 | // TODO: This could produce some funny behavior, fix somehow 31 | // Say for instance you needed 4 of a stick in 1 slot, and 8 of that same stick in another 32 | // This method could detect you have the 4 required in the items list, and remove the 8 required in the recipe list 33 | // And then when it checks against the 8 stick itemstack in the recipe, you don't have it in the crafting menu 34 | // But that's a really weird case to go for, we would need to implement some sort of best-fit algorithm, or just not do that 35 | recipeClone.removeIf(entry -> Item.fromItemStack(item).stateId() == entry.item().stateId() && item.amount() >= entry.count()); 36 | } 37 | return recipeClone.isEmpty(); 38 | } 39 | 40 | @Override 41 | public @NotNull ItemStack getRecipeOutput() { 42 | return output; 43 | } 44 | 45 | @Override 46 | public boolean containsIngredient(@NotNull ItemStack itemStack) { 47 | for (ComponentEntry entry : recipe) { 48 | if (Item.fromItemStack(itemStack).stateId() == entry.item().stateId()) { 49 | return true; 50 | } 51 | } 52 | return false; 53 | } 54 | 55 | @Override 56 | public boolean requiresTool() { 57 | return false; 58 | } 59 | 60 | 61 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 62 | ENTRY_CODEC.listOf().fieldOf("components").forGetter(ShapelessCraftingRecipe::recipe), 63 | ExtraCodecs.MATERIAL.fieldOf("output").xmap(ItemStack::of, ItemStack::material).forGetter(ShapelessCraftingRecipe::output) 64 | ).apply(i, ShapelessCraftingRecipe::new)); 65 | } 66 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/crafting/ToolShapedCraftingRecipe.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.crafting; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.minestom.server.item.ItemStack; 6 | import org.jetbrains.annotations.NotNull; 7 | import net.hollowcube.dfu.ExtraCodecs; 8 | import net.hollowcube.item.Item; 9 | import net.hollowcube.item.ItemImpl; 10 | 11 | import java.util.List; 12 | 13 | public record ToolShapedCraftingRecipe(Item toolItem, @NotNull List recipe, 14 | @NotNull ItemStack output) implements CraftingRecipe { 15 | 16 | public ToolShapedCraftingRecipe { 17 | if (recipe.size() != 9) { 18 | throw new IllegalArgumentException("Shaped crafting recipe does not have exactly 9 items (use air for empty slots)!"); 19 | } 20 | } 21 | 22 | @Override 23 | public boolean doesRecipeMatch(@NotNull List items) { 24 | if (toolItem.stateId() != Item.fromItemStack(items.get(0)).stateId()) return false; 25 | for (int i = 1; i < recipe.size(); i++) { 26 | if (recipe.get(i).item() == ItemImpl.EMPTY_ITEM) continue; 27 | // Need to do i + 1 since tool is the first item 28 | if (recipe.get(i).item().stateId() != Item.fromItemStack(items.get(i + 1)).stateId() || items.get(i + 1).amount() < recipe.get(i).count()) { 29 | return false; 30 | } 31 | } 32 | return true; 33 | } 34 | 35 | @Override 36 | public @NotNull ItemStack getRecipeOutput() { 37 | return output; 38 | } 39 | 40 | @Override 41 | public boolean containsIngredient(@NotNull ItemStack itemStack) { 42 | int stackStateId = Item.fromItemStack(itemStack).stateId(); 43 | if (toolItem.stateId() == stackStateId) return true; 44 | for (ComponentEntry entry : recipe) { 45 | if (entry.item().stateId() != stackStateId) { 46 | return true; 47 | } 48 | } 49 | return false; 50 | } 51 | 52 | @Override 53 | public boolean requiresTool() { 54 | return true; 55 | } 56 | 57 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 58 | Item.CODEC.fieldOf("tool").forGetter(ToolShapedCraftingRecipe::toolItem), 59 | ENTRY_CODEC.listOf().fieldOf("components").forGetter(ToolShapedCraftingRecipe::recipe), 60 | ExtraCodecs.MATERIAL.fieldOf("output").xmap(ItemStack::of, ItemStack::material).forGetter(ToolShapedCraftingRecipe::output) 61 | ).apply(i, ToolShapedCraftingRecipe::new)); 62 | } 63 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/entity/OwnedItemEntity.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.entity; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.minestom.server.entity.ItemEntity; 5 | import net.minestom.server.entity.Player; 6 | import net.minestom.server.event.EventNode; 7 | import net.minestom.server.event.entity.EntityItemMergeEvent; 8 | import net.minestom.server.event.item.PickupItemEvent; 9 | import net.minestom.server.item.ItemStack; 10 | import org.jetbrains.annotations.NotNull; 11 | import net.hollowcube.server.Facet; 12 | import net.hollowcube.server.ServerWrapper; 13 | 14 | import java.util.UUID; 15 | 16 | /** 17 | * An {@link OwnedItemEntity} is a regular {@link ItemEntity}, except that it may only be picked up by the defined 18 | * owner. This should be used for all items currently. 19 | */ 20 | public class OwnedItemEntity extends ItemEntity { 21 | private final UUID owner; 22 | 23 | /** 24 | * @param owner The uuid of the owning entity (player or otherwise) 25 | * @param itemStack The item stack to spawn 26 | */ 27 | public OwnedItemEntity(@NotNull UUID owner, @NotNull ItemStack itemStack) { 28 | super(itemStack); 29 | this.owner = owner; 30 | } 31 | 32 | 33 | @AutoService(Facet.class) 34 | public static class Handler implements Facet { 35 | @Override 36 | public void hook(@NotNull ServerWrapper server) { 37 | var eventNode = EventNode.all("starlight:item_entity/handler"); 38 | eventNode.addListener(PickupItemEvent.class, this::handlePickup); 39 | eventNode.addListener(EntityItemMergeEvent.class, this::handleMerge); 40 | server.addEventNode(eventNode); 41 | } 42 | 43 | private void handlePickup(@NotNull PickupItemEvent event) { 44 | if (!(event.getItemEntity() instanceof OwnedItemEntity itemEntity)) 45 | return; 46 | 47 | // Only players can pick up items for now, eventually entities may be allowed 48 | if (!(event.getEntity() instanceof Player player)) { 49 | event.setCancelled(true); 50 | return; 51 | } 52 | 53 | // Ensure owner is the one picking up the item 54 | if (!player.getUuid().equals(itemEntity.owner)) { 55 | event.setCancelled(true); 56 | return; 57 | } 58 | 59 | // Add item to the player if possible 60 | boolean added = player.getInventory().addItemStack(event.getItemStack()); 61 | if (!added) { 62 | event.setCancelled(true); 63 | } 64 | } 65 | 66 | private void handleMerge(@NotNull EntityItemMergeEvent event) { 67 | if (!(event.getEntity() instanceof OwnedItemEntity entity) || 68 | !(event.getMerged() instanceof OwnedItemEntity merged)) 69 | return; 70 | if (!entity.owner.equals(merged.owner)) 71 | event.setCancelled(true); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/impl/Rarity.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.impl; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.hollowcube.item.ItemComponent; 6 | 7 | public record Rarity( 8 | String value 9 | ) implements ItemComponent { 10 | 11 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 12 | Codec.STRING.fieldOf("value").forGetter(Rarity::value) 13 | ).apply(i, Rarity::new)); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/impl/RarityHandler.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.impl; 2 | 3 | import com.google.auto.service.AutoService; 4 | import com.mojang.serialization.Codec; 5 | import net.hollowcube.util.ComponentUtil; 6 | import net.minestom.server.item.ItemStack; 7 | import net.minestom.server.utils.NamespaceID; 8 | import org.jetbrains.annotations.NotNull; 9 | import net.hollowcube.item.ItemComponentHandler; 10 | 11 | @AutoService(ItemComponentHandler.class) 12 | public class RarityHandler implements ItemComponentHandler { 13 | 14 | @Override 15 | public @NotNull NamespaceID namespace() { 16 | return NamespaceID.from("starlight:rarity"); 17 | } 18 | 19 | @Override 20 | public @NotNull Class componentType() { 21 | return Rarity.class; 22 | } 23 | 24 | @Override 25 | public @NotNull Codec<@NotNull Rarity> codec() { 26 | return Rarity.CODEC; 27 | } 28 | 29 | @Override 30 | public int priority() { 31 | return -1000; 32 | } 33 | 34 | @Override 35 | public void buildItemStack(@NotNull Rarity component, @NotNull ItemStack.Builder builder) { 36 | builder.lore(ComponentUtil.fromStringSafe(component.value())); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/loot/ItemDistributor.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.loot; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.hollowcube.item.entity.OwnedItemEntity; 5 | import net.hollowcube.loot.LootContext; 6 | import net.hollowcube.loot.LootResult; 7 | import net.minestom.server.coordinate.Point; 8 | import net.minestom.server.coordinate.Vec; 9 | import net.minestom.server.entity.Entity; 10 | import net.minestom.server.item.ItemStack; 11 | import net.minestom.server.utils.NamespaceID; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import net.hollowcube.item.Item; 16 | 17 | import java.util.concurrent.CompletableFuture; 18 | 19 | @AutoService(LootResult.DefaultDistributor.class) 20 | public class ItemDistributor implements LootResult.DefaultDistributor { 21 | private static final Logger LOGGER = LoggerFactory.getLogger(ItemDistributor.class); 22 | 23 | @Override 24 | public @NotNull NamespaceID namespace() { 25 | return NamespaceID.from("starlight:item"); 26 | } 27 | 28 | @Override 29 | public @NotNull Class type() { 30 | return Item.class; 31 | } 32 | 33 | @Override 34 | public @NotNull CompletableFuture apply(@NotNull LootContext context, @NotNull Item item) { 35 | final Entity entity = context.get(LootContext.THIS_ENTITY); 36 | if (entity == null) { 37 | LOGGER.error("No `this` entity for item distributor. context={}", context); 38 | return CompletableFuture.completedFuture(null); 39 | } 40 | 41 | final ItemStack itemStack = item.asItemStack(); 42 | final OwnedItemEntity itemEntity = new OwnedItemEntity(entity.getUuid(), itemStack); 43 | 44 | // Spawn at the hinted location, or the entity location if not provided. 45 | Point pos = context.get(LootContext.POSITION); 46 | if (pos == null) pos = entity.getPosition(); 47 | // Set the location to the center of the block 48 | pos = new Vec(pos.blockX() + 0.5f, pos.blockY() + 0.5f, pos.blockZ() + 0.5f); 49 | 50 | // Spawn the entity at the location 51 | return itemEntity.setInstance(entity.getInstance(), pos) 52 | .thenAccept(unused -> { 53 | // Spawn with a velocity in the hinted direction, if present 54 | Vec direction = context.get(LootContext.DIRECTION); 55 | if (direction != null) { 56 | direction = direction.normalize().mul(3f); 57 | itemEntity.setVelocity(direction); 58 | } 59 | }); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/item/src/main/java/net/hollowcube/item/loot/ItemEntry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.loot; 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.dfu.ExtraCodecs; 7 | import net.hollowcube.loot.LootContext; 8 | import net.hollowcube.loot.LootEntry; 9 | import net.minestom.server.utils.NamespaceID; 10 | import net.minestom.server.utils.validate.Check; 11 | import org.jetbrains.annotations.NotNull; 12 | import net.hollowcube.item.Item; 13 | 14 | import java.util.List; 15 | 16 | public record ItemEntry( 17 | @NotNull NamespaceID item 18 | ) implements LootEntry { 19 | //todo this needs to be improved a lot in the future. Weights, item states, amount, etc, etc. 20 | 21 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 22 | ExtraCodecs.NAMESPACE_ID.fieldOf("item").forGetter(ItemEntry::item) 23 | ).apply(i, ItemEntry::new)); 24 | 25 | @Override 26 | public @NotNull List<@NotNull Option> generate(@NotNull LootContext context) { 27 | Item item = Item.fromNamespaceId(this.item); 28 | Check.notNull(item, "Unknown item: " + this.item); 29 | return List.of(new Option<>(List.of(item), 1)); 30 | } 31 | 32 | @AutoService(LootEntry.Factory.class) 33 | public static class Factory extends LootEntry.Factory { 34 | public Factory() { 35 | super( 36 | NamespaceID.from("starlight:item"), 37 | ItemEntry.class, 38 | ItemEntry.CODEC 39 | ); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/crafting/TestCraftingIntegration.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.crafting; 2 | 3 | import net.hollowcube.item.test.MockItem; 4 | import net.minestom.server.coordinate.Pos; 5 | import net.minestom.server.item.ItemStack; 6 | import net.minestom.server.item.Material; 7 | import net.minestom.server.test.Env; 8 | import net.minestom.server.test.EnvTest; 9 | import net.minestom.server.utils.NamespaceID; 10 | import org.junit.jupiter.api.Test; 11 | import net.hollowcube.item.crafting.CraftingInventory; 12 | import net.hollowcube.item.crafting.CraftingRecipe; 13 | import net.hollowcube.item.crafting.RecipeList; 14 | import net.hollowcube.item.crafting.ShapelessCraftingRecipe; 15 | 16 | import java.util.List; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | 20 | @EnvTest 21 | public class TestCraftingIntegration { 22 | 23 | private final MockItem log = new MockItem(NamespaceID.from("test", "log"), 2, 2, null, Material.OAK_LOG, 1); 24 | 25 | @Test 26 | public void testCraftAction(Env env) { 27 | var instance = env.createFlatInstance(); 28 | var player = env.createPlayer(instance, new Pos(0, 42, 0)); 29 | RecipeList list = new RecipeList(); 30 | list.addRecipes(List.of(new ShapelessCraftingRecipe(List.of(new CraftingRecipe.ComponentEntry(log, 1)), ItemStack.of(Material.OAK_PLANKS).withAmount(4)))); 31 | CraftingInventory inventory = new CraftingInventory(list); 32 | inventory.setItemStack(9, log.asItemStack().withAmount(2)); 33 | inventory.refreshCurrentRecipe(); 34 | assertEquals(Material.OAK_PLANKS, inventory.getItemStack(0).material()); 35 | player.openInventory(inventory); 36 | inventory.leftClick(player, 0); 37 | assertEquals(ItemStack.of(Material.OAK_PLANKS, 4), inventory.getCursorItem(player)); 38 | assertEquals(log.asItemStack().withAmount(1), inventory.getItemStack(9)); 39 | inventory.leftClick(player, 0); 40 | assertEquals(ItemStack.of(Material.OAK_PLANKS, 8), inventory.getCursorItem(player)); 41 | assertEquals(inventory.getItemStack(9), ItemStack.AIR); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/crafting/TestCraftingRecipe.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.crafting; 2 | 3 | import net.hollowcube.item.crafting.CraftingRecipe; 4 | import net.hollowcube.item.test.MockItem; 5 | import net.minestom.server.MinecraftServer; 6 | import net.minestom.server.item.ItemStack; 7 | import net.minestom.server.item.Material; 8 | import net.minestom.server.utils.NamespaceID; 9 | import org.junit.jupiter.api.Test; 10 | import net.hollowcube.item.crafting.CraftingInventory; 11 | import net.hollowcube.item.crafting.RecipeList; 12 | import net.hollowcube.item.crafting.ShapelessCraftingRecipe; 13 | 14 | import java.util.List; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 18 | 19 | public class TestCraftingRecipe { 20 | 21 | private final MockItem log = new MockItem(NamespaceID.from("test", "log"), 2, 2, null, Material.OAK_LOG, 1); 22 | 23 | @Test 24 | public void testBlankRecipe() { 25 | MinecraftServer.init(); 26 | RecipeList list = new RecipeList(); 27 | CraftingInventory inventory = new CraftingInventory(list); 28 | inventory.setItemStack(2, ItemStack.of(Material.OAK_LOG)); 29 | inventory.refreshCurrentRecipe(); 30 | assertEquals(inventory.getItemStack(0), ItemStack.AIR); 31 | } 32 | 33 | @Test 34 | public void testPlankRecipe() { 35 | MinecraftServer.init(); 36 | RecipeList list = new RecipeList(); 37 | CraftingInventory inventory = new CraftingInventory(list); 38 | list.addRecipes(List.of(new ShapelessCraftingRecipe(List.of(new CraftingRecipe.ComponentEntry(log, 1)), ItemStack.of(Material.OAK_PLANKS)))); 39 | inventory.setItemStack(2, log.asItemStack()); 40 | inventory.refreshCurrentRecipe(); 41 | assertEquals(inventory.getItemStack(1), ItemStack.AIR); 42 | assertEquals(inventory.getItemStack(2).material(), Material.OAK_LOG); 43 | assertEquals(inventory.getItemStack(0), ItemStack.of(Material.OAK_PLANKS)); 44 | } 45 | 46 | @Test 47 | public void testRecipeRemoval() { 48 | MinecraftServer.init(); 49 | RecipeList list = new RecipeList(); 50 | CraftingInventory inventory = new CraftingInventory(list); 51 | list.addRecipes(List.of(new ShapelessCraftingRecipe(List.of(new CraftingRecipe.ComponentEntry(log, 1)), ItemStack.of(Material.OAK_PLANKS)))); 52 | inventory.setItemStack(2, log.asItemStack()); 53 | inventory.refreshCurrentRecipe(); 54 | assertEquals(inventory.getItemStack(0), ItemStack.of(Material.OAK_PLANKS)); 55 | inventory.setItemStack(2, ItemStack.AIR); 56 | inventory.refreshCurrentRecipe(); 57 | assertNotEquals(inventory.getItemStack(0), ItemStack.of(Material.OAK_PLANKS)); 58 | assertEquals(inventory.getItemStack(0), ItemStack.AIR); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/item/TestItem.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item; 2 | 3 | import net.hollowcube.item.test.MockItem; 4 | import net.kyori.adventure.text.Component; 5 | import net.minestom.server.item.ItemHideFlag; 6 | import net.minestom.server.item.ItemStack; 7 | import net.minestom.server.item.Material; 8 | import net.minestom.server.utils.NamespaceID; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.Map; 12 | 13 | import static com.google.common.truth.Truth.assertThat; 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | 16 | public class TestItem { 17 | // Will need some more tests here as item creation gets more complicated (eg with components editing lore) 18 | 19 | @Test 20 | public void testAsItemStack() { 21 | Item item = new MockItem( 22 | NamespaceID.from("test:item"), 23 | 1, 2, 24 | Map.of(), 25 | Material.GUNPOWDER, 26 | 5 27 | ); 28 | ItemStack expected = ItemStack.builder(Material.GUNPOWDER) 29 | .amount(5) 30 | .meta(builder -> { 31 | builder.hideFlag(ItemHideFlag.HIDE_ATTRIBUTES); 32 | builder.customModelData(2); 33 | builder.displayName(Component.text("item.test.item.name")); 34 | }) 35 | .build(); 36 | 37 | assertEquals(expected, item.asItemStack()); 38 | } 39 | 40 | @Test 41 | public void testFromItemStack() { 42 | var itemStack = ItemStack.of(Material.GOLD_INGOT) 43 | .withMeta(meta -> meta.customModelData(1)); 44 | 45 | var item = Item.fromItemStack(itemStack); 46 | 47 | var expected = Item.fromNamespaceId("test:item"); 48 | assertThat(item).isEqualTo(expected); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/item/TestItemRegistry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item; 2 | 3 | import net.hollowcube.item.test.TestComponent; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | 8 | public class TestItemRegistry { 9 | static { 10 | System.setProperty("starlight.data.dir", "src/test/resources"); 11 | } 12 | 13 | @Test 14 | public void testItemComponentLoading() { 15 | Item item = Item.fromNamespaceId("test:item_with_component"); 16 | assertThat(item).isNotNull(); 17 | 18 | TestComponent component = item.getComponent("test:component"); 19 | assertThat(component).isNotNull(); 20 | 21 | TestComponent component2 = item.getComponent(TestComponent.class); 22 | assertThat(component2).isNotNull(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/item/loot/TestItemEntry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.loot; 2 | 3 | import net.hollowcube.data.NumberSource; 4 | import net.hollowcube.loot.LootContext; 5 | import net.hollowcube.loot.LootEntry; 6 | import net.minestom.server.utils.NamespaceID; 7 | import org.junit.jupiter.api.Test; 8 | import net.hollowcube.item.Item; 9 | 10 | import java.util.List; 11 | 12 | import static com.google.common.truth.Truth.assertThat; 13 | 14 | public class TestItemEntry { 15 | 16 | @Test 17 | public void testGenerateItem() { 18 | var entry = new ItemEntry(NamespaceID.from("test:item")); 19 | var context = LootContext.builder("test") 20 | .numbers(NumberSource.constant(1)) 21 | .build(); 22 | 23 | var result = entry.generate(context); 24 | var expected = new LootEntry.Option<>(List.of(Item.fromNamespaceId("test:item")), 1); 25 | 26 | assertThat(result).containsExactly(expected); 27 | } 28 | 29 | //todo codec tests 30 | } 31 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/item/test/MockItem.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.test; 2 | 3 | import net.minestom.server.item.Material; 4 | import net.minestom.server.utils.NamespaceID; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | import net.hollowcube.item.Item; 8 | import net.hollowcube.item.ItemComponent; 9 | 10 | import java.util.Map; 11 | import java.util.stream.Stream; 12 | 13 | public record MockItem( 14 | NamespaceID namespace, 15 | int id, 16 | int stateId, 17 | Map properties, 18 | Material material, 19 | int amount 20 | ) implements Item { 21 | 22 | @Override 23 | public Item withAmount(int amount) { 24 | return new MockItem( 25 | namespace, 26 | id, stateId, 27 | properties, 28 | material, 29 | amount 30 | ); 31 | } 32 | 33 | @Override 34 | public @NotNull Stream components() { 35 | return Stream.empty(); 36 | } 37 | 38 | @Override 39 | @SuppressWarnings("TypeParameterUnusedInFormals") 40 | public @Nullable C getComponent(@NotNull String namespace) { 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/item/test/TestComponent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.test; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.item.ItemComponent; 7 | 8 | import static net.hollowcube.dfu.ExtraCodecs.string; 9 | 10 | public record TestComponent( 11 | @NotNull String name 12 | ) implements ItemComponent { 13 | 14 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 15 | string("name", "unknown").forGetter(TestComponent::name) 16 | ).apply(i, TestComponent::new)); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /modules/item/src/test/java/net/hollowcube/item/test/TestComponentHandler.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.item.test; 2 | 3 | import com.google.auto.service.AutoService; 4 | import com.mojang.serialization.Codec; 5 | import net.minestom.server.event.Event; 6 | import net.minestom.server.event.EventFilter; 7 | import net.minestom.server.event.EventNode; 8 | import net.minestom.server.event.trait.ItemEvent; 9 | import net.minestom.server.event.trait.PlayerEvent; 10 | import net.minestom.server.item.ItemStack; 11 | import net.minestom.server.utils.NamespaceID; 12 | import org.jetbrains.annotations.NotNull; 13 | import net.hollowcube.item.Item; 14 | import net.hollowcube.item.ItemComponentHandler; 15 | 16 | @AutoService(ItemComponentHandler.class) 17 | public class TestComponentHandler implements ItemComponentHandler { 18 | 19 | @Override 20 | public @NotNull NamespaceID namespace() { 21 | return NamespaceID.from("test:component"); 22 | } 23 | 24 | @Override 25 | public @NotNull Class componentType() { 26 | return TestComponent.class; 27 | } 28 | 29 | @Override 30 | public @NotNull Codec<@NotNull TestComponent> codec() { 31 | return TestComponent.CODEC; 32 | } 33 | 34 | 35 | private final EventNode eventNode = EventNode.all("abc"); 36 | 37 | @Override 38 | public @NotNull EventNode eventNode() { 39 | return eventNode; 40 | } 41 | 42 | 43 | //todo test filter to avoid any events when the player does not have the item in hand. But some components might 44 | // only care if its in inv, or armor, etc. So this wont really work. In the end, probably worth just having some utils. 45 | public static EventFilter itemComponent(Class type) { 46 | return EventFilter.from(PlayerEvent.class, type, event -> { 47 | // final Player player = event.getPlayer(); 48 | 49 | // Find the target item based on the event type 50 | final ItemStack itemStack; 51 | if (event instanceof ItemEvent itemEvent) 52 | itemStack = itemEvent.getItemStack(); 53 | else return null; 54 | 55 | Item item = Item.fromItemStack(itemStack); 56 | return item.getComponent(type); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/item/src/test/resources/data/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "test:item_with_component", 4 | "id": 0, 5 | "material": "minecraft:gold_ingot", 6 | "components": [ 7 | { 8 | "type": "test:component", 9 | "name": "Hello, world" 10 | } 11 | ], 12 | "defaultStateId": 0, 13 | "states": { 14 | "[]": { 15 | "stateId": 0 16 | } 17 | } 18 | }, 19 | { 20 | "namespace": "test:item", 21 | "id": 1, 22 | "material": "minecraft:gold_ingot", 23 | "defaultStateId": 1, 24 | "states": { 25 | "[]": { 26 | "stateId": 1 27 | } 28 | } 29 | }, 30 | { 31 | "namespace": "test:log", 32 | "id": 2, 33 | "material": "minecraft:oak_log", 34 | "defaultStateId": 2, 35 | "states": { 36 | "[]": { 37 | "stateId": 2 38 | } 39 | } 40 | }, 41 | { 42 | "namespace": "test:tool", 43 | "id": 3, 44 | "material": "minecraft:wooden_axe", 45 | "defaultStateId": 3, 46 | "states": { 47 | "[]": { 48 | "stateId": 3 49 | } 50 | } 51 | }, 52 | { 53 | "namespace": "test:stick", 54 | "id": 4, 55 | "material": "minecraft:stick", 56 | "defaultStateId": 4, 57 | "states": { 58 | "[]": { 59 | "stateId": 4 60 | } 61 | } 62 | } 63 | ] -------------------------------------------------------------------------------- /modules/loot-table/README.md: -------------------------------------------------------------------------------- 1 | # Loot Table 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /modules/loot-table/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":modules:common")) 3 | } 4 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/LootEntry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.minestom.server.utils.NamespaceID; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.registry.Registry; 7 | import net.hollowcube.registry.ResourceFactory; 8 | 9 | import java.util.List; 10 | 11 | public interface LootEntry { 12 | 13 | Codec> CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); 14 | 15 | @NotNull List<@NotNull Option> generate(@NotNull LootContext context); 16 | 17 | record Option( 18 | @NotNull List loot, 19 | int weight 20 | ) {} 21 | 22 | 23 | abstract class Factory extends ResourceFactory> { 24 | static Registry REGISTRY = Registry.service("loot_entries", Factory.class); 25 | static Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); 26 | 27 | static Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.required(ns), Factory::name); 28 | 29 | public Factory(NamespaceID namespace, Class> type, Codec> codec) { 30 | super(namespace, type, codec); 31 | } 32 | 33 | public static @NotNull Factory from(@NotNull LootEntry entry) { 34 | return TYPE_REGISTRY.get(entry.getClass()); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/LootModifier.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.minestom.server.utils.NamespaceID; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.registry.Registry; 7 | import net.hollowcube.registry.ResourceFactory; 8 | 9 | public interface LootModifier { 10 | 11 | Codec CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); 12 | 13 | //todo there is a contract here that apply MUST return the same type. 14 | // Perhaps should validate this, or introduce some generic to help perhaps 15 | // Will do in the future 16 | @NotNull Object apply(@NotNull Object input); 17 | 18 | 19 | abstract class Factory extends ResourceFactory { 20 | static Registry REGISTRY = Registry.service("loot_modifiers", LootModifier.Factory.class); 21 | static Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); 22 | 23 | static Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.required(ns), Factory::name); 24 | 25 | public Factory(NamespaceID namespace, Class type, Codec codec) { 26 | super(namespace, type, codec); 27 | } 28 | 29 | public static @NotNull Factory from(@NotNull LootModifier modifier) { 30 | return TYPE_REGISTRY.get(modifier.getClass()); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/LootPool.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.data.number.NumberProvider; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public record LootPool( 12 | @NotNull List conditions, 13 | @NotNull List modifiers, 14 | @NotNull List> entries, 15 | @NotNull NumberProvider rolls 16 | ) { 17 | 18 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 19 | LootPredicate.CODEC.listOf().optionalFieldOf("conditions", List.of()).forGetter(LootPool::conditions), 20 | LootModifier.CODEC.listOf().optionalFieldOf("modifiers", List.of()).forGetter(LootPool::modifiers), 21 | LootEntry.CODEC.listOf().optionalFieldOf("entries", List.of()).forGetter(LootPool::entries), 22 | NumberProvider.CODEC.optionalFieldOf("rolls", NumberProvider.constant(1)).forGetter(LootPool::rolls) 23 | ).apply(i, LootPool::new)); 24 | 25 | public @NotNull List<@NotNull Object> generate(@NotNull LootContext context) { 26 | // Ensure all conditions match 27 | if (!LootPredicate.all(context, conditions())) 28 | return List.of(); 29 | 30 | // Generate available entries 31 | var options = entries() 32 | .stream() 33 | .map(entry -> entry.generate(context)) 34 | .flatMap(List::stream) 35 | .toList(); 36 | int totalWeight = options.stream() 37 | .mapToInt(LootEntry.Option::weight).sum(); 38 | 39 | // Roll for entries 40 | List<@NotNull Object> output = new ArrayList<>(); 41 | for (int i = 0; i < rolls().nextLong(context); i++) { 42 | int roll = (int) (context.random() * totalWeight); 43 | for (LootEntry.Option option : options) { 44 | roll -= option.weight(); 45 | if (roll <= 0) { 46 | output.addAll(option.loot()); 47 | break; 48 | } 49 | } 50 | } 51 | 52 | // Apply modifiers to final entries 53 | return output.stream() 54 | .map(entry -> { 55 | for (LootModifier modifier : modifiers()) 56 | entry = modifier.apply(entry); 57 | return entry; 58 | }) 59 | .toList(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/LootPredicate.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.minestom.server.utils.NamespaceID; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.registry.Registry; 7 | import net.hollowcube.registry.ResourceFactory; 8 | 9 | import java.util.Collection; 10 | 11 | public interface LootPredicate { 12 | Codec CODEC = Factory.CODEC.dispatch(Factory::from, Factory::codec); 13 | 14 | static boolean all(@NotNull LootContext context, @NotNull Collection conditions) { 15 | for (LootPredicate condition : conditions) { 16 | if (!condition.test(context)) return false; 17 | } 18 | return true; 19 | } 20 | 21 | boolean test(@NotNull LootContext context); 22 | 23 | 24 | abstract class Factory extends ResourceFactory { 25 | static Registry REGISTRY = Registry.service("loot_predicates", LootPredicate.Factory.class); 26 | static Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); 27 | 28 | public static final Codec CODEC = Codec.STRING.xmap(ns -> REGISTRY.required(ns), Factory::name); 29 | 30 | public Factory(NamespaceID namespace, Class type, Codec codec) { 31 | super(namespace, type, codec); 32 | } 33 | 34 | static @NotNull Factory from(@NotNull LootPredicate predicate) { 35 | return TYPE_REGISTRY.get(predicate.getClass()); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/LootResult.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot; 2 | 3 | import net.minestom.server.utils.NamespaceID; 4 | import org.jetbrains.annotations.NotNull; 5 | import net.hollowcube.registry.Resource; 6 | 7 | import java.util.Collection; 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | public interface LootResult { 11 | 12 | @NotNull Collection results(); 13 | 14 | int size(); 15 | 16 | void override(@NotNull Class type, @NotNull Distributor distributor); 17 | 18 | @NotNull CompletableFuture apply(@NotNull LootContext context); 19 | 20 | 21 | @FunctionalInterface 22 | interface Distributor { 23 | 24 | @NotNull CompletableFuture apply(@NotNull LootContext context, @NotNull T t); 25 | 26 | } 27 | 28 | interface DefaultDistributor extends Distributor, Resource { 29 | 30 | @Override 31 | @NotNull NamespaceID namespace(); 32 | 33 | @NotNull Class type(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/LootTable.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.minestom.server.utils.NamespaceID; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | import net.hollowcube.dfu.ExtraCodecs; 9 | import net.hollowcube.loot.impl.LootResultImpl; 10 | import net.hollowcube.registry.Registry; 11 | import net.hollowcube.registry.Resource; 12 | 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | public record LootTable( 17 | @NotNull NamespaceID namespace, 18 | @NotNull List modifiers, 19 | @NotNull List pools 20 | ) implements Resource { 21 | 22 | public static final LootTable EMPTY = new LootTable(NamespaceID.from("starlight:empty"), List.of(), List.of()); 23 | 24 | public LootTable { 25 | modifiers = List.copyOf(modifiers); 26 | pools = List.copyOf(pools); 27 | } 28 | 29 | public @NotNull LootResult generate(@NotNull LootContext context) { 30 | return new LootResultImpl(pools.stream() 31 | .map(pool -> pool.generate(context)) 32 | .flatMap(List::stream) 33 | .map(this::applyModifiers) 34 | .collect(Collectors.toList())); 35 | } 36 | 37 | private @NotNull Object applyModifiers(@NotNull Object input) { 38 | for (LootModifier modifier : modifiers()) 39 | input = modifier.apply(input); 40 | return input; 41 | } 42 | 43 | 44 | // Registry 45 | 46 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 47 | ExtraCodecs.NAMESPACE_ID.fieldOf("namespace").forGetter(LootTable::namespace), 48 | LootModifier.CODEC.listOf().optionalFieldOf("modifiers", List.of()).forGetter(LootTable::modifiers), 49 | LootPool.CODEC.listOf().optionalFieldOf("pools", List.of()).forGetter(LootTable::pools) 50 | ).apply(i, LootTable::new)); 51 | 52 | public static final Registry REGISTRY = Registry.codec("loot_table", CODEC); 53 | 54 | public static @Nullable LootTable fromNamespaceId(@NotNull NamespaceID namespace) { 55 | return REGISTRY.get(namespace); 56 | } 57 | 58 | public static @Nullable LootTable fromNamespaceId(@NotNull String namespace) { 59 | return REGISTRY.get(namespace); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/impl/GroupLootEntry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot.impl; 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.loot.LootContext; 7 | import net.hollowcube.loot.LootEntry; 8 | import net.hollowcube.loot.LootPredicate; 9 | import net.minestom.server.utils.NamespaceID; 10 | import org.jetbrains.annotations.NotNull; 11 | import net.hollowcube.dfu.ExtraCodecs; 12 | 13 | import java.util.List; 14 | 15 | public record GroupLootEntry( 16 | @NotNull List conditions, 17 | @NotNull List> children 18 | ) implements LootEntry { 19 | 20 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 21 | LootPredicate.CODEC.listOf().fieldOf("conditions").forGetter(GroupLootEntry::conditions), 22 | // Recursive codec, so must use a lazy version 23 | ExtraCodecs.lazy(() -> LootEntry.CODEC).listOf().fieldOf("children").forGetter(GroupLootEntry::children) 24 | ).apply(i, GroupLootEntry::new)); 25 | 26 | @Override 27 | public @NotNull List<@NotNull Option> generate(@NotNull LootContext context) { 28 | // Ensure all conditions match 29 | if (!LootPredicate.all(context, conditions())) 30 | return List.of(); 31 | 32 | // Collect all results 33 | //noinspection unchecked 34 | return children() 35 | .stream() 36 | .map(entry -> entry.generate(context)) 37 | .flatMap(List::stream) 38 | // This cast is required to make types work out here 39 | .map(it -> (Option) it) 40 | .toList(); 41 | } 42 | 43 | 44 | @AutoService(LootEntry.Factory.class) 45 | public static class Factory extends LootEntry.Factory { 46 | 47 | public Factory() { 48 | super( 49 | NamespaceID.from("starlight:group"), 50 | GroupLootEntry.class, 51 | GroupLootEntry.CODEC 52 | ); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/impl/LootContextImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot.impl; 2 | 3 | import net.hollowcube.loot.LootContext; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | import net.hollowcube.data.NumberSource; 7 | 8 | import java.util.Map; 9 | 10 | public record LootContextImpl( 11 | @NotNull Map dataMap, 12 | @NotNull NumberSource numbers 13 | ) implements LootContext { 14 | 15 | public LootContextImpl { 16 | dataMap = Map.copyOf(dataMap); 17 | } 18 | 19 | @Override 20 | public double random() { 21 | return numbers().random(); 22 | } 23 | 24 | @Override 25 | public @Nullable T get(@NotNull Key key) { 26 | final Object data = dataMap.get(key.name()); 27 | if (data == null || !key.type().isAssignableFrom(data.getClass())) 28 | return null; 29 | //noinspection unchecked 30 | return (T) data; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/loot-table/src/main/java/net/hollowcube/loot/impl/LootResultImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot.impl; 2 | 3 | import net.hollowcube.loot.LootContext; 4 | import net.hollowcube.loot.LootResult; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.UnknownNullability; 7 | import net.hollowcube.registry.Registry; 8 | 9 | import java.util.*; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | public final class LootResultImpl implements LootResult { 13 | private static final Registry> DISTRIBUTOR_REGISTRY = Registry.manual("loot_distributors", () -> { 14 | List> registry = new ArrayList<>(); 15 | for (DefaultDistributor entry : ServiceLoader.load(DefaultDistributor.class)) 16 | registry.add(entry); 17 | return registry; 18 | }); 19 | 20 | private final Map, Distributor> overrides = new HashMap<>(); 21 | private final List results; 22 | 23 | public LootResultImpl(List results) { 24 | this.results = results; 25 | } 26 | 27 | 28 | @Override 29 | public @NotNull Collection results() { 30 | return results; 31 | } 32 | 33 | @Override 34 | public int size() { 35 | return results.size(); 36 | } 37 | 38 | @Override 39 | public void override(@NotNull Class type, @NotNull Distributor distributor) { 40 | overrides.put(type, distributor); 41 | } 42 | 43 | @Override 44 | public @NotNull CompletableFuture apply(@NotNull LootContext context) { 45 | var tasks = new CompletableFuture[size()]; 46 | for (int i = 0; i < tasks.length; i++) { 47 | final Object result = results.get(i); 48 | 49 | //noinspection unchecked 50 | Distributor distributor = (Distributor) findDistributor(result.getClass()); 51 | if (distributor == null) { 52 | //todo better way to handle this? 53 | throw new RuntimeException("No distributor for type " + result.getClass()); 54 | } 55 | 56 | tasks[i] = distributor.apply(context, result); 57 | } 58 | 59 | return CompletableFuture.allOf(tasks); 60 | } 61 | 62 | @SuppressWarnings("unchecked") 63 | private @UnknownNullability Distributor findDistributor(Class type) { 64 | for (var entry : overrides.entrySet()) { 65 | if (entry.getKey().isAssignableFrom(type)) { 66 | return (Distributor) entry.getValue(); 67 | } 68 | } 69 | 70 | for (var entry : DISTRIBUTOR_REGISTRY.values()) { 71 | if (entry.type().isAssignableFrom(type)) { 72 | return (Distributor) entry; 73 | } 74 | } 75 | 76 | return null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /modules/loot-table/src/test/java/net/hollowcube/loot/TestLootTable.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import net.hollowcube.loot.test.StringLootType; 5 | 6 | import static com.google.common.truth.Truth.assertThat; 7 | import static net.hollowcube.loot.test.LootTableUtil.*; 8 | 9 | public class TestLootTable { 10 | 11 | @Test 12 | public void testEmpty() { 13 | var table = table().build(); 14 | 15 | var result = table.generate(context(1)); 16 | 17 | assertThat(result.results()) 18 | .isEmpty(); 19 | } 20 | 21 | @Test 22 | public void testSinglePool() { 23 | var table = table() 24 | .pool(pool() 25 | .entry(StringLootType.entry(1, "a")) 26 | .build()) 27 | .build(); 28 | 29 | var result = table.generate(context(1)); 30 | 31 | assertThat(result.results()) 32 | .containsExactly("a"); 33 | } 34 | 35 | @Test 36 | public void testMultiPool() { 37 | var table = table() 38 | .pool(pool() 39 | .entry(StringLootType.entry(1, "a")) 40 | .build()) 41 | .pool(pool() 42 | .entry(StringLootType.entry(1, "b")) 43 | .build()) 44 | .build(); 45 | 46 | var result = table.generate(context(1)); 47 | 48 | assertThat(result.results()) 49 | .containsExactly("a", "b"); 50 | } 51 | 52 | @Test 53 | public void testModifyAllResults() { 54 | var table = table() 55 | .modifier(StringLootType.rewrite("zzz")) 56 | .pool(pool() 57 | .entry(StringLootType.entry(1, "a")) 58 | .build()) 59 | .pool(pool() 60 | .entry(StringLootType.entry(1, "b")) 61 | .build()) 62 | .build(); 63 | 64 | var result = table.generate(context(1)); 65 | 66 | assertThat(result.results()) 67 | .containsExactly("zzz", "zzz"); 68 | } 69 | 70 | 71 | //todo Codec test i guess 72 | } 73 | -------------------------------------------------------------------------------- /modules/loot-table/src/test/java/net/hollowcube/loot/impl/TestGroupLootEntry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot.impl; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import net.hollowcube.loot.test.StringLootType; 5 | 6 | import java.util.List; 7 | 8 | import static com.google.common.truth.Truth.assertThat; 9 | import static net.hollowcube.loot.test.LootTableUtil.context; 10 | import static net.hollowcube.loot.test.LootTableUtil.failPredicate; 11 | 12 | public class TestGroupLootEntry { 13 | 14 | @Test 15 | public void testEmpty() { 16 | var entry = new GroupLootEntry( 17 | List.of(), 18 | List.of() 19 | ); 20 | 21 | var result = entry.generate(context(1)); 22 | 23 | assertThat(result).isEmpty(); 24 | } 25 | 26 | @Test 27 | public void testConditionFail() { 28 | var entry = new GroupLootEntry( 29 | List.of(failPredicate()), 30 | List.of(StringLootType.entry(1, "")) 31 | ); 32 | 33 | var result = entry.generate(context(1)); 34 | 35 | // Even though there is an entry, it should not be returned 36 | assertThat(result).isEmpty(); 37 | } 38 | 39 | @Test 40 | public void testAllOptionsReturned() { 41 | var entry = new GroupLootEntry( 42 | List.of(), 43 | List.of( 44 | StringLootType.entry(1, "a", "b"), 45 | StringLootType.entries(1, "c", "d") 46 | ) 47 | ); 48 | 49 | var result = entry.generate(context(1)); 50 | var collected = result.stream() 51 | .flatMap(o -> o.loot().stream()) 52 | .toList(); 53 | 54 | assertThat(collected) 55 | .containsExactly("a", "b", "c", "d"); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /modules/loot-table/src/test/java/net/hollowcube/loot/impl/TestLootContextImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot.impl; 2 | 3 | import net.hollowcube.loot.LootContext; 4 | import net.minestom.server.coordinate.Point; 5 | import net.minestom.server.coordinate.Vec; 6 | import org.junit.jupiter.api.Test; 7 | import net.hollowcube.data.NumberSource; 8 | 9 | import java.util.Map; 10 | 11 | import static com.google.common.truth.Truth.assertThat; 12 | 13 | public class TestLootContextImpl { 14 | private static final LootContext.Key TEST_KEY = new LootContext.Key<>("test", Vec.class); 15 | private static final LootContext.Key TEST_KEY_2 = new LootContext.Key<>("test", Point.class); 16 | 17 | @Test 18 | public void testGetMissing() { 19 | var lootContext = new LootContextImpl(Map.of(), NumberSource.constant(1)); 20 | var result = lootContext.get(TEST_KEY); 21 | 22 | assertThat(result).isNull(); 23 | } 24 | 25 | @Test 26 | public void testGetPresent() { 27 | var lootContext = new LootContextImpl(Map.of("test", new Vec(1, 1, 1)), NumberSource.constant(1)); 28 | var result = lootContext.get(TEST_KEY); 29 | 30 | assertThat(result).isEqualTo(new Vec(1, 1, 1)); 31 | } 32 | 33 | @Test 34 | public void testGetSupertype() { 35 | var lootContext = new LootContextImpl(Map.of("test", new Vec(1, 1, 1)), NumberSource.constant(1)); 36 | var result = lootContext.get(TEST_KEY_2); 37 | 38 | assertThat(result).isEqualTo(new Vec(1, 1, 1)); 39 | } 40 | 41 | @Test 42 | public void testGetWrongType() { 43 | var lootContext = new LootContextImpl(Map.of("test", "wrong"), NumberSource.constant(1)); 44 | var result = lootContext.get(TEST_KEY); 45 | 46 | assertThat(result).isNull(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /modules/loot-table/src/test/java/net/hollowcube/loot/impl/TestLootResultImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot.impl; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import net.hollowcube.loot.test.StringLootType; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.CompletableFuture; 8 | 9 | import static com.google.common.truth.Truth.assertThat; 10 | import static net.hollowcube.loot.test.LootTableUtil.context; 11 | 12 | public class TestLootResultImpl { 13 | 14 | @Test 15 | public void testDefaultDistributor() { 16 | LootResultImpl lootResult = new LootResultImpl(List.of("testDefaultDistributor")); 17 | lootResult.apply(context(1)).join(); 18 | 19 | assertThat(StringLootType.StringDistributor.DISTRIBUTED_STRINGS) 20 | .contains("testDefaultDistributor"); 21 | } 22 | 23 | @Test 24 | public void testOverrideDistributor() { 25 | LootResultImpl lootResult = new LootResultImpl(List.of("testOverrideDistributor")); 26 | lootResult.override(String.class, (context, value) -> { 27 | assertThat(value).isEqualTo("testOverrideDistributor"); 28 | StringLootType.StringDistributor.DISTRIBUTED_STRINGS.add(value + "2"); 29 | return CompletableFuture.completedFuture(null); 30 | }); 31 | 32 | lootResult.apply(context(1)).join(); 33 | 34 | assertThat(StringLootType.StringDistributor.DISTRIBUTED_STRINGS) 35 | .doesNotContain("testOverrideDistributor"); 36 | assertThat(StringLootType.StringDistributor.DISTRIBUTED_STRINGS) 37 | .contains("testOverrideDistributor2"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modules/loot-table/src/test/java/net/hollowcube/loot/test/StringLootType.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.loot.test; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.hollowcube.loot.LootContext; 5 | import net.hollowcube.loot.LootEntry; 6 | import net.hollowcube.loot.LootModifier; 7 | import net.hollowcube.loot.LootResult; 8 | import net.minestom.server.utils.NamespaceID; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.concurrent.CompletableFuture; 14 | 15 | import static com.google.common.truth.Truth.assertThat; 16 | 17 | public class StringLootType { 18 | 19 | public static LootEntry entry(int weight, String... values) { 20 | return new StringLootEntry(false, weight, List.of(values)); 21 | } 22 | 23 | public static LootEntry entries(int weight, String... values) { 24 | return new StringLootEntry(true, weight, List.of(values)); 25 | } 26 | 27 | public static LootModifier rewrite(String newValue) { 28 | return new StringRewriteModifier(newValue); 29 | } 30 | 31 | record StringLootEntry( 32 | boolean multiple, 33 | int weight, 34 | List strings 35 | ) implements LootEntry { 36 | 37 | @Override 38 | public @NotNull List<@NotNull Option> generate(@NotNull LootContext context) { 39 | if (multiple) return List.of(new Option<>(strings(), weight)); 40 | return strings().stream().map(s -> new Option<>(List.of(s), weight)).toList(); 41 | } 42 | } 43 | 44 | record StringRewriteModifier( 45 | String newValue 46 | ) implements LootModifier { 47 | 48 | @Override 49 | public @NotNull Object apply(@NotNull Object input) { 50 | if (input instanceof String) 51 | return newValue; 52 | return input; 53 | } 54 | } 55 | 56 | @AutoService(LootResult.DefaultDistributor.class) 57 | public static class StringDistributor implements LootResult.DefaultDistributor { 58 | // Contains all distributed strings since the test started. If a string is applied twice, it is an error. 59 | public static final List DISTRIBUTED_STRINGS = new ArrayList<>(); 60 | 61 | 62 | @Override 63 | public @NotNull NamespaceID namespace() { 64 | return NamespaceID.from("test:string"); 65 | } 66 | 67 | @Override 68 | public @NotNull Class type() { 69 | return String.class; 70 | } 71 | 72 | @Override 73 | public @NotNull CompletableFuture apply(@NotNull LootContext context, @NotNull String s) { 74 | // Cannot use the same string twice 75 | assertThat(DISTRIBUTED_STRINGS).doesNotContain(s); 76 | DISTRIBUTED_STRINGS.add(s); 77 | return CompletableFuture.completedFuture(null); 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /modules/player/README.md: -------------------------------------------------------------------------------- 1 | # Player 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /modules/player/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | dependencies { 6 | implementation(project(":modules:item")) 7 | implementation(project(":modules:common")) 8 | } -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/CombatFacet.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube; 2 | 3 | import com.google.auto.service.AutoService; 4 | import net.hollowcube.damage.DamageProcessor; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.server.Facet; 7 | import net.hollowcube.server.ServerWrapper; 8 | 9 | @AutoService(Facet.class) 10 | public class CombatFacet implements Facet { 11 | @Override 12 | public void hook(@NotNull ServerWrapper server) { 13 | DamageProcessor.init(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/DamageTagList.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube; 2 | 3 | import net.minestom.server.tag.Tag; 4 | 5 | public class DamageTagList { 6 | 7 | public static final Tag ENTITY_WEIGHT_TAG = Tag.Double("mob-weight"); 8 | } 9 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/AttackCooldown.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.attribute.Attribute; 5 | import net.minestom.server.entity.Player; 6 | import net.minestom.server.tag.Tag; 7 | 8 | public class AttackCooldown { 9 | 10 | private final Tag startTimeTag = Tag.Long("cooldown-start-time"); 11 | private final Tag cooldownTickTag = Tag.Integer("cooldown-ticks"); 12 | 13 | public void resetCooldown(Player player) { 14 | // From https://minecraft.fandom.com/wiki/Damage#Attack_cooldown 15 | float attackSpeed = player.getAttributeValue(Attribute.ATTACK_SPEED); 16 | int cooldownTicks = (int) (20f / attackSpeed); 17 | player.setTag(startTimeTag, System.currentTimeMillis()); 18 | player.setTag(cooldownTickTag, cooldownTicks); 19 | } 20 | 21 | public double getCooldownDamageMultiplier(Player player) { 22 | // Formula from https://minecraft.fandom.com/wiki/Damage#Attack_cooldown 23 | if (!player.hasTag(startTimeTag) || !player.hasTag(cooldownTickTag)) { 24 | return 1; 25 | } 26 | int cooldownTicks = player.getTag(cooldownTickTag); 27 | int ticksSinceLastAttacked = (int) ((System.currentTimeMillis() - player.getTag(startTimeTag)) / MinecraftServer.TICK_MS); 28 | return (0.2 + Math.pow(((ticksSinceLastAttacked + 0.5) / cooldownTicks), 2) * 0.8); 29 | } 30 | } -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/DamageInfo.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage; 2 | 3 | import net.minestom.server.entity.LivingEntity; 4 | import net.minestom.server.entity.damage.DamageType; 5 | import net.minestom.server.entity.damage.EntityDamage; 6 | import net.minestom.server.event.EventDispatcher; 7 | import org.jetbrains.annotations.Contract; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | public class DamageInfo { 11 | 12 | private final MultiPartValue damageValue; 13 | private final MultiPartValue knockbackStrength; 14 | private int fireTicks; 15 | private final DamageType type; 16 | private int immunityTicks; 17 | 18 | public DamageInfo(DamageType type) { 19 | this(type, new MultiPartValue(1)); 20 | } 21 | 22 | public DamageInfo(@NotNull DamageType type, @NotNull MultiPartValue damageValue) { 23 | this.type = type; 24 | this.damageValue = damageValue; 25 | knockbackStrength = new MultiPartValue(0.4); 26 | fireTicks = 0; 27 | immunityTicks = 10; 28 | } 29 | 30 | public @NotNull MultiPartValue getDamageValue() { 31 | return damageValue; 32 | } 33 | 34 | public @NotNull MultiPartValue getKnockbackStrength() { 35 | return knockbackStrength; 36 | } 37 | 38 | @Contract(mutates = "this") 39 | public void setFireTicks(int fireTicks) { 40 | this.fireTicks = fireTicks; 41 | } 42 | 43 | @Contract(mutates = "this") 44 | public void setImmunityTicks(int immunityTicks) { 45 | this.immunityTicks = immunityTicks; 46 | } 47 | 48 | public int getImmunityTicks() { 49 | return immunityTicks; 50 | } 51 | 52 | // TODO: Perhaps make Knockback a vector instead of a MultiPartValue to more easily handle cases when there is no attacker? 53 | @Contract(mutates = "param1") 54 | public void apply(@NotNull LivingEntity entity, double attackerYaw) { 55 | // Apply knockback - don't need to handle kb resistance, since that is already done in livingEntity.takeKnockback 56 | double yawRadians = attackerYaw * Math.PI / 180; 57 | entity.takeKnockback((float) knockbackStrength.getFinalValue(), Math.sin(yawRadians), -Math.cos(yawRadians)); 58 | // Apply fire 59 | entity.setFireForDuration(fireTicks); 60 | // Deal damage 61 | 62 | float finalDamage = (float) damageValue.getFinalValue(); 63 | if (entity.getHealth() - finalDamage <= 0.00001 && type instanceof EntityDamage entityDamage) { 64 | EntityKilledByEntityEvent entityKilledByEntityEvent = new EntityKilledByEntityEvent(entity, entityDamage.getSource()); 65 | EventDispatcher.call(entityKilledByEntityEvent); 66 | } 67 | entity.damage(type, finalDamage); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/EntityKilledByEntityEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage; 2 | 3 | import net.minestom.server.entity.Entity; 4 | import net.minestom.server.entity.LivingEntity; 5 | import net.minestom.server.event.trait.EntityEvent; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class EntityKilledByEntityEvent implements EntityEvent { 9 | 10 | private final LivingEntity target; 11 | private final Entity attacker; 12 | 13 | public EntityKilledByEntityEvent(@NotNull LivingEntity target, @NotNull Entity attacker) { 14 | this.target = target; 15 | this.attacker = attacker; 16 | } 17 | 18 | @Override 19 | public @NotNull Entity getEntity() { 20 | return target; 21 | } 22 | 23 | public @NotNull Entity getAttacker() { 24 | return attacker; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/MultiPartValue.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage; 2 | 3 | import net.minestom.server.attribute.AttributeOperation; 4 | import org.jetbrains.annotations.Contract; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | // Mostly ripped from my other projects 8 | public class MultiPartValue { 9 | 10 | private final double base; 11 | private double boost; 12 | private double multiplierBase; 13 | private double multiplierTotal; 14 | 15 | public MultiPartValue(double base) { 16 | this.base = base; 17 | this.boost = 0; 18 | this.multiplierBase = 1; 19 | this.multiplierTotal = 1; 20 | } 21 | 22 | @Contract(mutates = "this") 23 | public void addBase(double amount) { 24 | this.boost += amount; 25 | } 26 | 27 | /** 28 | * Multiplies the total value by the amount 29 | * 30 | * @param amount The amount to multiply the value by 31 | */ 32 | @Contract(mutates = "this") 33 | public void multiply(double amount) { 34 | multiplierTotal *= amount; 35 | } 36 | 37 | /** 38 | * Modifies the current value according to the operation 39 | * 40 | * @param amount The amount to modify by 41 | * @param operation The operation by which to modify the value 42 | */ 43 | @Contract(mutates = "this") 44 | public void modifyValue(double amount, @NotNull AttributeOperation operation) { 45 | switch (operation) { 46 | case ADDITION -> addBase(amount); 47 | case MULTIPLY_BASE -> multiplierBase += amount; 48 | case MULTIPLY_TOTAL -> multiply(amount); 49 | } 50 | } 51 | 52 | /** 53 | * Calculates and returns the value represented bu this object 54 | * 55 | * @return The result of the value calculation 56 | */ 57 | public double getFinalValue() { 58 | return (base + boost) * multiplierBase * multiplierTotal; 59 | } 60 | 61 | @Contract(mutates = "this") 62 | public void combine(@NotNull MultiPartValue other) { 63 | addBase(other.base + other.boost); 64 | this.multiplierBase += other.multiplierBase; 65 | multiply(other.multiplierTotal); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/iticks/ImmunityTickImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage.iticks; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.entity.LivingEntity; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.HashMap; 8 | 9 | public class ImmunityTickImpl implements ImmunityTicks { 10 | 11 | // Map of current time 12 | public HashMap tickMap = new HashMap<>(); 13 | 14 | @Override 15 | public boolean isEntityImmune(@NotNull LivingEntity entity) { 16 | return tickMap.containsKey(entity); 17 | } 18 | 19 | @Override 20 | public void setImmunityTicks(@NotNull LivingEntity entity, int tickCount) { 21 | tickMap.put(entity, new TickEntry(System.currentTimeMillis(), tickCount)); 22 | } 23 | 24 | @Override 25 | public void update() { 26 | tickMap.entrySet().removeIf(entry -> entry.getValue().startTime() + (long) MinecraftServer.TICK_MS * entry.getValue().ticksImmune() < System.currentTimeMillis()); 27 | } 28 | 29 | private record TickEntry(long startTime, int ticksImmune) {} 30 | } 31 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/iticks/ImmunityTicks.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage.iticks; 2 | 3 | import net.minestom.server.entity.LivingEntity; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public interface ImmunityTicks { 7 | /** 8 | * Determines if this entity is currently immune from damage 9 | * 10 | * @param entity The entity to check 11 | * @return True if this entity cannot be damaged, false if it can 12 | */ 13 | boolean isEntityImmune(@NotNull LivingEntity entity); 14 | 15 | /** 16 | * Sets the amount of immunity ticks this entity will have 17 | * 18 | * @param entity The entity to make immune 19 | * @param tickCount The number of ticks to make this entity immune 20 | */ 21 | void setImmunityTicks(@NotNull LivingEntity entity, int tickCount); 22 | 23 | // Called every tick 24 | void update(); 25 | } 26 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/iticks/ImmunityTicksPlayerImpl.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage.iticks; 2 | 3 | import net.minestom.server.MinecraftServer; 4 | import net.minestom.server.entity.LivingEntity; 5 | import net.minestom.server.tag.Tag; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class ImmunityTicksPlayerImpl implements ImmunityTicks { 9 | private final Tag iTickTag = Tag.Long("immunity-ticks"); 10 | 11 | @Override 12 | public boolean isEntityImmune(@NotNull LivingEntity entity) { 13 | if (entity.hasTag(iTickTag)) { 14 | return entity.getTag(iTickTag) > System.currentTimeMillis(); 15 | } else { 16 | return false; 17 | } 18 | } 19 | 20 | @Override 21 | public void setImmunityTicks(@NotNull LivingEntity entity, int tickCount) { 22 | if (tickCount <= 0) { 23 | entity.removeTag(iTickTag); 24 | } else { 25 | entity.setTag(iTickTag, System.currentTimeMillis() + (long) MinecraftServer.TICK_MS * tickCount); 26 | } 27 | } 28 | 29 | @Override 30 | public void update() { 31 | // Nothing 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/weapon/AppliedPotionEffect.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage.weapon; 2 | 3 | import net.minestom.server.potion.PotionEffect; 4 | 5 | public record AppliedPotionEffect( 6 | double chance, 7 | PotionEffect effect, 8 | int tickDuration, 9 | int amplifier 10 | ) {} 11 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/weapon/Weapon.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage.weapon; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.DataResult; 5 | import com.mojang.serialization.DynamicOps; 6 | import com.mojang.serialization.codecs.PrimitiveCodec; 7 | import com.mojang.serialization.codecs.RecordCodecBuilder; 8 | import net.hollowcube.dfu.ExtraCodecs; 9 | import net.hollowcube.item.ItemComponent; 10 | 11 | import java.util.List; 12 | 13 | public record Weapon( 14 | double attackSpeed, 15 | List randomEffects, 16 | WeaponWeight weight 17 | 18 | ) implements ItemComponent { 19 | 20 | private static final Codec APPLIED_POTION_EFFECTS = RecordCodecBuilder.create(i -> i.group( 21 | Codec.DOUBLE.optionalFieldOf("chance", 1d).forGetter(AppliedPotionEffect::chance), 22 | ExtraCodecs.POTION_EFFECT.fieldOf("effect").forGetter(AppliedPotionEffect::effect), 23 | Codec.INT.fieldOf("tickDuration").forGetter(AppliedPotionEffect::tickDuration), 24 | Codec.INT.fieldOf("amplifier").forGetter(AppliedPotionEffect::amplifier) 25 | ).apply(i, AppliedPotionEffect::new)); 26 | 27 | private static final PrimitiveCodec WEAPON_WEIGHT_CODEC = new PrimitiveCodec<>() { 28 | @Override 29 | public DataResult read(DynamicOps ops, T input) { 30 | DataResult number = ops.getNumberValue(input); 31 | Number value = number.get().orThrow(); 32 | WeaponWeight weaponWeight = WeaponWeight.getWeight(value.intValue()); 33 | if (weaponWeight == null) { 34 | return DataResult.error("Number was outside valid bounds!"); 35 | } else { 36 | return DataResult.success(weaponWeight); 37 | } 38 | } 39 | 40 | @Override 41 | public T write(DynamicOps ops, WeaponWeight value) { 42 | return ops.createNumeric(value.ordinal()); 43 | } 44 | }; 45 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 46 | Codec.DOUBLE.fieldOf("attackSpeed").forGetter(Weapon::attackSpeed), 47 | APPLIED_POTION_EFFECTS.listOf().optionalFieldOf("randomEffects", List.of()).forGetter(Weapon::randomEffects), 48 | WEAPON_WEIGHT_CODEC.optionalFieldOf("weight", WeaponWeight.NONE).forGetter(Weapon::weight) 49 | ).apply(i, Weapon::new)); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/weapon/WeaponHandler.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage.weapon; 2 | 3 | import net.minestom.server.event.Event; 4 | import net.minestom.server.event.EventNode; 5 | import net.minestom.server.utils.NamespaceID; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | import net.hollowcube.item.ItemComponentHandler; 9 | 10 | public class WeaponHandler implements ItemComponentHandler { 11 | 12 | private final EventNode eventNode = EventNode.all("starlight:weapon/item_component_handler"); 13 | 14 | public WeaponHandler() { 15 | 16 | } 17 | 18 | @Override 19 | public @NotNull NamespaceID namespace() { 20 | return NamespaceID.from("starlight:weapon"); 21 | } 22 | 23 | @Override 24 | public @NotNull Class componentType() { 25 | return Weapon.class; 26 | } 27 | 28 | @Override 29 | public com.mojang.serialization.@NotNull Codec<@NotNull Weapon> codec() { 30 | return Weapon.CODEC; 31 | } 32 | 33 | @Override 34 | public @Nullable EventNode eventNode() { 35 | return eventNode; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/damage/weapon/WeaponWeight.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.damage.weapon; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | public enum WeaponWeight { 6 | NONE, 7 | LIGHT, 8 | MEDIUM, 9 | HEAVY; 10 | 11 | public static @Nullable WeaponWeight getWeight(int ordinal) { 12 | WeaponWeight[] values = WeaponWeight.values(); 13 | if (ordinal <= 0 || ordinal >= values.length) { 14 | return null; 15 | } 16 | return values[ordinal]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/modifiers/Modifier.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.modifiers; 2 | 3 | public interface Modifier { 4 | 5 | T modifierAmount(); 6 | 7 | ModifierOperation operation(); 8 | 9 | boolean hasExpired(); 10 | } -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/modifiers/ModifierList.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.modifiers; 2 | 3 | import net.hollowcube.damage.MultiPartValue; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class ModifierList { 10 | 11 | private final Map> modifiers = new HashMap<>(); 12 | private final double initialAmount; 13 | 14 | public ModifierList(double initialAmount) { 15 | this.initialAmount = initialAmount; 16 | } 17 | 18 | public void addPermanentModifier(@NotNull String id, double amount) { 19 | addPermanentModifier(id, amount, ModifierOperation.ADD); 20 | } 21 | 22 | public void addPermanentModifier(@NotNull String id, double amount, @NotNull ModifierOperation operation) { 23 | modifiers.put(id, new PermanentModifier<>(amount, operation)); 24 | } 25 | 26 | 27 | public void addTemporaryModifier(String id, double amount, long expiresAt) { 28 | addTemporaryModifier(id, amount, ModifierOperation.ADD, expiresAt); 29 | } 30 | 31 | public void addTemporaryModifier(String id, double amount, ModifierOperation operation, long expiresAt) { 32 | modifiers.put(id, new TemporaryModifier<>(amount, operation, expiresAt)); 33 | } 34 | 35 | public void removeModifier(String id) { 36 | modifiers.remove(id); 37 | } 38 | 39 | public double calculateTotal() { 40 | MultiPartValue value = new MultiPartValue(initialAmount); 41 | // Cleanup - so all current values are not expired 42 | modifiers.values().removeIf(Modifier::hasExpired); 43 | for(var entry : modifiers.entrySet()) { 44 | Modifier modifier = entry.getValue(); 45 | switch (modifier.operation()) { 46 | case ADD -> value.addBase(modifier.modifierAmount()); 47 | case MULTIPLY -> value.multiply(modifier.modifierAmount()); 48 | } 49 | } 50 | return value.getFinalValue(); 51 | } 52 | } -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/modifiers/ModifierOperation.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.modifiers; 2 | 3 | public enum ModifierOperation { 4 | ADD, 5 | MULTIPLY 6 | } 7 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/modifiers/ModifierType.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.modifiers; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.hollowcube.dfu.ExtraCodecs; 6 | import net.hollowcube.registry.Registry; 7 | import net.hollowcube.registry.Resource; 8 | import net.minestom.server.utils.NamespaceID; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public record ModifierType(NamespaceID namespace, double defaultValue) implements Resource { 12 | 13 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 14 | ExtraCodecs.NAMESPACE_ID.fieldOf("namespace").forGetter(ModifierType::namespace), 15 | Codec.DOUBLE.fieldOf("defaultValue").forGetter(ModifierType::defaultValue) 16 | ).apply(i, ModifierType::new)); 17 | 18 | public static final Registry REGISTRY = Registry.codec("modifiers", CODEC); 19 | 20 | 21 | public static boolean doesModifierExist(@NotNull String modifierId) { 22 | if(!modifierId.startsWith("starlight:")) { 23 | return REGISTRY.get("starlight:" + modifierId) != null; 24 | } else { 25 | return REGISTRY.get(modifierId) != null; 26 | } 27 | } 28 | 29 | public static double getBaseValue(@NotNull String modifierId) { 30 | if(!modifierId.startsWith("starlight:")) { 31 | return REGISTRY.required("starlight:" + modifierId).defaultValue; 32 | } else { 33 | return REGISTRY.required(modifierId).defaultValue; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/modifiers/PermanentModifier.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.modifiers; 2 | 3 | public record PermanentModifier(T modifierAmount, ModifierOperation operation) implements Modifier { 4 | 5 | @Override 6 | public boolean hasExpired() { 7 | return false; 8 | } 9 | } -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/modifiers/TemporaryModifier.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.modifiers; 2 | 3 | public record TemporaryModifier(T modifierAmount, ModifierOperation operation, long expiryTimestamp) implements Modifier { 4 | 5 | @Override 6 | public boolean hasExpired() { 7 | return System.currentTimeMillis() > expiryTimestamp; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /modules/player/src/main/java/net/hollowcube/player/event/PlayerLongDiggingStartEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.player.event; 2 | 3 | import net.minestom.server.entity.Player; 4 | import net.minestom.server.event.trait.BlockEvent; 5 | import net.minestom.server.event.trait.PlayerInstanceEvent; 6 | import net.minestom.server.instance.block.Block; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.function.IntSupplier; 11 | 12 | public class PlayerLongDiggingStartEvent implements PlayerInstanceEvent, BlockEvent { 13 | private final Player player; 14 | private final Block block; 15 | 16 | private int maxHealth = 0; 17 | private IntSupplier damageFunction = null; 18 | 19 | public PlayerLongDiggingStartEvent(Player player, Block block) { 20 | this.player = player; 21 | this.block = block; 22 | } 23 | 24 | @Override 25 | public @NotNull Block getBlock() { 26 | return block; 27 | } 28 | 29 | @Override 30 | public @NotNull Player getPlayer() { 31 | return player; 32 | } 33 | 34 | public int getMaxHealth() { 35 | return maxHealth; 36 | } 37 | 38 | public @Nullable IntSupplier getDamageFunction() { 39 | return damageFunction; 40 | } 41 | 42 | public void setDiggingBlock(int maxHealth, IntSupplier damageFunction) { 43 | this.maxHealth = maxHealth; 44 | this.damageFunction = damageFunction; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /modules/player/src/main/resources/data/modifiers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "starlight:luck", 4 | "defaultValue": 0 5 | }, 6 | { 7 | "namespace": "starlight:attack_damage", 8 | "defaultValue": 1.0 9 | }, 10 | { 11 | "namespace": "starlight:mining_speed", 12 | "defaultValue": 1.0 13 | } 14 | ] -------------------------------------------------------------------------------- /modules/player/src/test/java/net/hollowcube/player/TestModifierList.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.player; 2 | 3 | import net.hollowcube.modifiers.ModifierList; 4 | import net.hollowcube.modifiers.ModifierOperation; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class TestModifierList { 10 | 11 | @Test 12 | public void testAddingModifiers() { 13 | ModifierList list = new ModifierList(1); 14 | list.addPermanentModifier("initial", 1.1, ModifierOperation.MULTIPLY); 15 | list.addPermanentModifier("wahoo", 3, ModifierOperation.ADD); 16 | assertEquals(4.4, list.calculateTotal()); 17 | } 18 | 19 | @Test 20 | public void testRemovingModifiers() { 21 | ModifierList list = new ModifierList(1); 22 | list.addPermanentModifier("initial", 1.5, ModifierOperation.MULTIPLY); 23 | list.addPermanentModifier("wahoo", 3, ModifierOperation.ADD); 24 | assertEquals(6, list.calculateTotal()); 25 | list.removeModifier("initial"); 26 | assertEquals(4, list.calculateTotal()); 27 | } 28 | } -------------------------------------------------------------------------------- /modules/player/src/test/java/net/hollowcube/player/TestModifierRegistry.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.player; 2 | 3 | import net.hollowcube.modifiers.ModifierType; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | public class TestModifierRegistry { 10 | 11 | @Test 12 | public void loadRegistry() { 13 | assertTrue(ModifierType.doesModifierExist("luck")); 14 | assertEquals(ModifierType.getBaseValue("potatoes"), 10d); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /modules/player/src/test/java/net/hollowcube/player/damage/TestDamageIntegration.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.player.damage; 2 | 3 | import net.hollowcube.damage.DamageProcessor; 4 | import net.minestom.server.MinecraftServer; 5 | import net.minestom.server.attribute.Attribute; 6 | import net.minestom.server.attribute.AttributeOperation; 7 | import net.minestom.server.entity.EntityType; 8 | import net.minestom.server.entity.LivingEntity; 9 | import net.minestom.server.item.ItemStack; 10 | import net.minestom.server.item.Material; 11 | import net.minestom.server.item.attribute.AttributeSlot; 12 | import net.minestom.server.item.attribute.ItemAttribute; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | 20 | 21 | public class TestDamageIntegration { 22 | 23 | @Test 24 | public void testCanApplyDamage() { 25 | // Required for not NullPointering when applying damage because of an event call 26 | MinecraftServer.init(); 27 | 28 | LivingEntity attacker = new LivingEntity(EntityType.ZOMBIE); 29 | LivingEntity target = new LivingEntity(EntityType.ZOMBIE); 30 | // When creating entities like above, health starts out at 1 ??? 31 | target.setHealth(target.getMaxHealth()); 32 | DamageProcessor.processDamage(attacker, target); 33 | assertEquals(target.getMaxHealth() - 1, target.getHealth()); 34 | } 35 | 36 | @Test 37 | public void testWeaponAttack() { 38 | // Required for not NullPointering when applying damage because of an event call 39 | MinecraftServer.init(); 40 | 41 | LivingEntity attacker = new LivingEntity(EntityType.ZOMBIE); 42 | LivingEntity target = new LivingEntity(EntityType.ZOMBIE); 43 | target.setHealth(target.getMaxHealth()); 44 | attacker.setItemInMainHand(ItemStack.builder( 45 | Material.DIAMOND 46 | ).meta(meta -> meta.attributes( 47 | List.of(new ItemAttribute(UUID.randomUUID(), "damage", Attribute.ATTACK_DAMAGE, AttributeOperation.ADDITION, 10, AttributeSlot.MAINHAND)) 48 | )).build()); 49 | DamageProcessor.processDamage(attacker, target); 50 | // Base damage of 1, +10 from diamond 51 | assertEquals(target.getMaxHealth() - 11, target.getHealth()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/player/src/test/resources/data/modifiers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "starlight:luck", 4 | "defaultValue": 0 5 | }, 6 | { 7 | "namespace": "starlight:potatoes", 8 | "defaultValue": 10 9 | }, 10 | { 11 | "namespace": "starlight:writing_tests", 12 | "defaultValue": -5 13 | }, 14 | { 15 | "namespace": "starlight:mining_speed", 16 | "defaultValue": 1 17 | } 18 | ] -------------------------------------------------------------------------------- /modules/quest/build.gradle.kts: -------------------------------------------------------------------------------- 1 | apply(plugin = "java-library") 2 | 3 | 4 | dependencies { 5 | implementation(project(":modules:common")) 6 | implementation(project(":modules:player")) 7 | } -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/Quest.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.codecs.RecordCodecBuilder; 5 | import net.hollowcube.quest.objective.Objective; 6 | import net.minestom.server.utils.NamespaceID; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | import net.hollowcube.dfu.ExtraCodecs; 10 | import net.hollowcube.registry.Registry; 11 | import net.hollowcube.registry.Resource; 12 | 13 | public record Quest( 14 | @NotNull NamespaceID namespace, 15 | @NotNull Objective objective 16 | ) implements Resource { 17 | 18 | public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( 19 | ExtraCodecs.NAMESPACE_ID.fieldOf("namespace").forGetter(Quest::namespace), 20 | Objective.CODEC.fieldOf("objective").forGetter(Quest::objective) 21 | ).apply(i, Quest::new)); 22 | 23 | public static final Registry REGISTRY = Registry.codec("quest", CODEC); 24 | 25 | public static @Nullable Quest fromNamespaceId(@NotNull String namespace) { 26 | return REGISTRY.get(namespace); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/QuestContext.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.hollowcube.quest.objective.ObjectiveData; 5 | import net.minestom.server.entity.Player; 6 | import org.jetbrains.annotations.ApiStatus; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public interface QuestContext { 10 | 11 | @NotNull T get(Codec codec); 12 | 13 | void set(@NotNull Codec codec, T value); 14 | 15 | @NotNull QuestContext child(@NotNull String name); 16 | 17 | @NotNull Player player(); 18 | 19 | @NotNull Quest quest(); 20 | 21 | 22 | @ApiStatus.Internal 23 | @NotNull ObjectiveData serialize(); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/QuestState.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest; 2 | 3 | public enum QuestState { 4 | NOT_STARTED, 5 | IN_PROGRESS, 6 | COMPLETED 7 | } 8 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/event/QuestCompleteEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.event; 2 | 3 | import net.hollowcube.quest.Quest; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.event.trait.PlayerEvent; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class QuestCompleteEvent implements PlayerEvent { 9 | private final Player player; 10 | private final Quest quest; 11 | 12 | 13 | public QuestCompleteEvent(Player player, Quest quest) { 14 | this.player = player; 15 | this.quest = quest; 16 | } 17 | 18 | @Override 19 | public @NotNull Player getPlayer() { 20 | return player; 21 | } 22 | 23 | public @NotNull Quest getQuest() { 24 | return quest; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/event/QuestObjectiveChangeEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.event; 2 | 3 | import net.hollowcube.quest.objective.Objective; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.event.trait.PlayerEvent; 6 | import org.jetbrains.annotations.NotNull; 7 | import net.hollowcube.quest.Quest; 8 | 9 | public class QuestObjectiveChangeEvent implements PlayerEvent { 10 | private final Player player; 11 | private final Quest quest; 12 | private final Objective objective; 13 | 14 | public QuestObjectiveChangeEvent(Player player, Quest quest, Objective objective) { 15 | this.player = player; 16 | this.quest = quest; 17 | this.objective = objective; 18 | } 19 | 20 | @Override 21 | public @NotNull Player getPlayer() { 22 | return player; 23 | } 24 | 25 | public Quest getQuest() { 26 | return quest; 27 | } 28 | 29 | public Objective getObjective() { 30 | return objective; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/event/QuestObjectiveCompleteEvent.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.event; 2 | 3 | import net.hollowcube.quest.Quest; 4 | import net.hollowcube.quest.objective.Objective; 5 | import net.minestom.server.entity.Player; 6 | import net.minestom.server.event.trait.PlayerEvent; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public class QuestObjectiveCompleteEvent implements PlayerEvent { 10 | private final Player player; 11 | private final Quest quest; 12 | private final Objective objective; 13 | 14 | public QuestObjectiveCompleteEvent(Player player, Quest quest, Objective objective) { 15 | this.player = player; 16 | this.quest = quest; 17 | this.objective = objective; 18 | } 19 | 20 | @Override 21 | public @NotNull Player getPlayer() { 22 | return player; 23 | } 24 | 25 | public Quest getQuest() { 26 | return quest; 27 | } 28 | 29 | public Objective getObjective() { 30 | return objective; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/objective/Objective.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.objective; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.kyori.adventure.text.Component; 5 | import net.minestom.server.utils.NamespaceID; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | import net.hollowcube.quest.QuestContext; 9 | import net.hollowcube.registry.Registry; 10 | import net.hollowcube.registry.ResourceFactory; 11 | 12 | import java.util.concurrent.CompletableFuture; 13 | 14 | import static net.hollowcube.dfu.ExtraCodecs.lazy; 15 | 16 | public interface Objective { 17 | 18 | Codec CODEC = lazy(() -> Factory.CODEC).dispatch(Factory::from, Factory::codec); 19 | 20 | @NotNull CompletableFuture onStart(@NotNull QuestContext context); 21 | 22 | @Nullable Component getCurrentStatus(@NotNull QuestContext context); 23 | 24 | 25 | class Factory extends ResourceFactory { 26 | public static Registry REGISTRY = Registry.service("quest_objective", Factory.class); 27 | 28 | public static Registry.Index, Factory> TYPE_REGISTRY = REGISTRY.index(Factory::type); 29 | 30 | static final Codec CODEC = Codec.STRING.xmap(namespace -> REGISTRY.required(namespace), Factory::name); 31 | 32 | public Factory(NamespaceID namespace, Class type, Codec codec) { 33 | super(namespace, type, codec); 34 | } 35 | 36 | static @Nullable Factory from(@NotNull Objective objective) { 37 | return TYPE_REGISTRY.get(objective.getClass()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/objective/ObjectiveData.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.objective; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Map; 6 | 7 | public record ObjectiveData( 8 | @NotNull Map children, 9 | @NotNull String data 10 | ) { 11 | } 12 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/storage/MemoryQuestStorage.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.storage; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.UUID; 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | class MemoryQuestStorage implements QuestStorage { 12 | private final Map data = new HashMap<>(); 13 | 14 | @Override 15 | public @NotNull CompletableFuture readQuestData(@NotNull UUID playerId) { 16 | final QuestData data = this.data.getOrDefault(playerId, new QuestData(playerId, List.of(), Map.of())); 17 | return CompletableFuture.completedFuture(data); 18 | } 19 | 20 | @Override 21 | public @NotNull CompletableFuture saveQuestData(@NotNull QuestData data) { 22 | this.data.put(data.playerId(), data); 23 | return CompletableFuture.completedFuture(null); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/storage/QuestData.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.storage; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import net.hollowcube.quest.objective.ObjectiveData; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.UUID; 9 | 10 | public record QuestData( 11 | @NotNull UUID playerId, 12 | @NotNull List completed, 13 | @NotNull Map inProgress 14 | ) { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /modules/quest/src/main/java/net/hollowcube/quest/storage/QuestStorage.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.storage; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.UUID; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface QuestStorage { 9 | 10 | static @NotNull QuestStorage memory() { 11 | return new MemoryQuestStorage(); 12 | } 13 | 14 | @NotNull CompletableFuture readQuestData(@NotNull UUID playerId); 15 | 16 | @NotNull CompletableFuture saveQuestData(@NotNull QuestData data); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /modules/quest/src/main/resources/example-quest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "example-quest", 4 | "type": "list", 5 | "objectives": [ 6 | { 7 | "type": "progress", 8 | "id": "start_adventure" 9 | }, 10 | { 11 | "type": "kill_mob", 12 | "id": "zombie", 13 | "count": 10 14 | }, 15 | { 16 | "type": "multi", 17 | "objectives": [ 18 | { 19 | "type": "progress", 20 | "id": "complete-tutorial" 21 | }, 22 | { 23 | "type": "kill_mob", 24 | "id": "pig", 25 | "count": 4 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | ] -------------------------------------------------------------------------------- /modules/quest/src/main/resources/quest-spec.txt: -------------------------------------------------------------------------------- 1 | A quest is defined as follows: 2 | [ 3 | { 4 | "name": "quest-name" 5 | "objective": { 6 | objective-data 7 | } 8 | } 9 | ] 10 | 11 | In a file containing quests to load, each quest should be a JSON object in anrray that has a name key, representing the name of the quest Multiple quests 12 | can be stored in the same file, with unique names as their identifiers. 13 | 14 | A quest definition starts out with a root objective node. Objectives represent a task or tasks to complete, and once 15 | this root objective is complete, the quest is considered complete. 16 | 17 | Each Objective object is defined as follows: 18 | { 19 | "type": Type of Objective 20 | [optional] "parameterName": argument 21 | } 22 | 23 | Current Supported Types: 24 | list - for containing a list of subobjectives that the player must complete in that order 25 | Parameters Required: 26 | "objectives" - array of sub-objectives 27 | multi - for containing a list of subobjectives that the player can complete in any order 28 | Parameters Required: 29 | "objectives" - array of sub-objectives 30 | progress - intended as a flag of story progress 31 | Parameters Required: 32 | id - name of enum that exists in QuestProgress 33 | kill_mob - kill a certain entity x times 34 | Parameters Required 35 | id - name of enum that exists in EntityType 36 | count - number of that mob to kill 37 | block_break - break a certain block x times 38 | Parameters Required: 39 | id - name of Enum (not really an enum but close enough) that exists in Block 40 | count - number of that block to break 41 | If an objective requires certain parameters, those parameters are represented by key-value pairs that map the objective name 42 | to the argument it will be at 43 | 44 | Example of a "Kill 10 Zombies" objective: 45 | { 46 | "type": "kill_mob", 47 | "id": "zombie" 48 | "count": 10 49 | } -------------------------------------------------------------------------------- /modules/quest/src/test/java/net/hollowcube/quest/test/MockObjective.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.test; 2 | 3 | import com.mojang.serialization.Codec; 4 | import net.kyori.adventure.text.Component; 5 | import org.jetbrains.annotations.NotNull; 6 | import net.hollowcube.quest.QuestContext; 7 | import net.hollowcube.quest.objective.Objective; 8 | 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.concurrent.ThreadLocalRandom; 11 | 12 | public class MockObjective implements Objective { 13 | 14 | private final CompletableFuture future = new CompletableFuture<>(); 15 | private final Component status = Component.text(ThreadLocalRandom.current().nextInt()); 16 | 17 | private static final Codec CURRENT = Codec.INT.orElse(0); 18 | 19 | @Override 20 | public @NotNull CompletableFuture onStart(@NotNull QuestContext context) { 21 | context.set(CURRENT, context.get(CURRENT) + 1); 22 | return future; 23 | } 24 | 25 | @Override 26 | public @NotNull Component getCurrentStatus(@NotNull QuestContext context) { 27 | return status; 28 | } 29 | 30 | public void complete() { 31 | future.complete(null); 32 | } 33 | 34 | public Component status() { 35 | return status; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /modules/quest/src/test/java/net/hollowcube/quest/test/MockQuestContext.java: -------------------------------------------------------------------------------- 1 | package net.hollowcube.quest.test; 2 | 3 | import net.minestom.server.entity.Player; 4 | import org.jetbrains.annotations.NotNull; 5 | import net.hollowcube.quest.Quest; 6 | import net.hollowcube.quest.QuestContextImpl; 7 | import net.hollowcube.quest.objective.ObjectiveData; 8 | 9 | public class MockQuestContext extends QuestContextImpl { 10 | 11 | public MockQuestContext(@NotNull Player player, @NotNull Quest quest, @NotNull ObjectiveData data) { 12 | super(player, quest, data); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /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 | 7 | // JUnit 8 | api("org.junit.jupiter:junit-jupiter-api:5.9.0") 9 | api("org.junit.jupiter:junit-jupiter-params:5.9.0") 10 | runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.0") 11 | 12 | // Truth 13 | api("com.google.truth:truth:1.1.3") 14 | 15 | 16 | // TestContainers 17 | fun testContainersApi(name: String) { 18 | api("org.testcontainers:$name:1.17.3") { 19 | exclude(group = "junit", module = "junit") 20 | } 21 | } 22 | 23 | testContainersApi("testcontainers") 24 | testContainersApi("junit-jupiter") 25 | testContainersApi("mongodb") 26 | 27 | 28 | } -------------------------------------------------------------------------------- /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/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/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 | public static @NotNull EntitySubject assertThat(@Nullable Entity actual) { 13 | return EntitySubject.assertThat(actual); 14 | } 15 | 16 | public static @NotNull AbstractInventorySubject assertThat(@Nullable AbstractInventory actual) { 17 | return AbstractInventorySubject.assertThat(actual); 18 | } 19 | 20 | public static @NotNull ItemStackSubject assertThat(@Nullable ItemStack actual) { 21 | return ItemStackSubject.assertThat(actual); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "libmmo" 2 | 3 | include(":modules") 4 | include(":modules:common") 5 | include(":modules:test") 6 | include(":modules:chat") 7 | include(":modules:item") 8 | include(":modules:block-interactions") 9 | include(":modules:loot-table") 10 | include(":modules:player") 11 | include("modules:quest") 12 | include(":modules:development") 13 | --------------------------------------------------------------------------------