├── tests_e2e ├── docker-compose.yml ├── jar │ └── .gitkeep ├── bot │ ├── .prettierrc.json │ ├── .gitignore │ ├── .prettierignore │ ├── .vscode │ │ └── settings.json │ ├── tests │ │ ├── xaero │ │ │ ├── minimap.mjs │ │ │ ├── worldmap.mjs │ │ │ └── common.mjs │ │ └── world_id │ │ │ ├── modern.mjs │ │ │ ├── legacy.mjs │ │ │ ├── forge.mjs │ │ │ └── common.mjs │ ├── Dockerfile │ ├── package.json │ ├── index.mjs │ └── testClient.mjs ├── proxy │ ├── velocity │ │ ├── forwarding.secret │ │ ├── plugins │ │ │ ├── bStats │ │ │ │ └── config.txt │ │ │ └── mapmodcompanion │ │ │ │ └── config.toml │ │ ├── log4j2.xml │ │ └── velocity.toml │ ├── bungeecord │ │ ├── config.yml │ │ └── plugins │ │ │ ├── bStats │ │ │ └── config.yml │ │ │ └── MapModCompanion │ │ │ └── config.yml │ └── Dockerfile ├── server │ ├── spigot.yml │ ├── server.properties │ ├── plugins │ │ ├── bStats │ │ │ └── config.yml │ │ └── MapModCompanion │ │ │ └── config.yml │ ├── Dockerfile │ └── log4j2.xml ├── .gitignore ├── saves │ ├── 1.8.9 │ │ ├── red.dat │ │ └── blue.dat │ ├── 1.13.2 │ │ ├── blue.dat │ │ └── red.dat │ └── 1.17.1 │ │ ├── blue.dat │ │ └── red.dat ├── requirements.txt ├── run.sh ├── README.md └── run.py ├── .github ├── FUNDING.yml ├── workflows │ ├── modrinth.yml │ ├── update-gradle.yml │ ├── hangar.yml │ ├── update-pages.yml │ ├── nightly.yml │ ├── github.yml │ ├── e2e_all.yml │ ├── e2e_test.yml │ ├── e2e_notable.yml │ ├── version.yml │ └── build.yml ├── actions │ └── setup_gradle │ │ └── action.yml └── dependabot.yml ├── packages ├── build.gradle.kts └── single │ └── build.gradle.kts ├── common ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── turikhay │ │ └── mc │ │ └── mapmodcompanion │ │ ├── Disposable.java │ │ ├── package-info.java │ │ ├── Channels.java │ │ ├── LightweightException.java │ │ ├── InitializationException.java │ │ ├── MalformedPacketException.java │ │ ├── IdBlender.java │ │ ├── Id.java │ │ ├── StandardId.java │ │ ├── VerboseLogger.java │ │ ├── IdLookup.java │ │ ├── ILogger.java │ │ ├── PrefixLogger.java │ │ ├── DaemonThreadFactory.java │ │ ├── LevelMapProperties.java │ │ ├── Handler.java │ │ ├── FileChangeWatchdog.java │ │ └── PrefixedId.java │ └── test │ └── java │ ├── IdBlenderTest.java │ ├── LevelMapPropertiesPacketSerializerTest.java │ ├── LevelMapPropertiesPacketDeserializerTest.java │ ├── PrefixedIdPacketDeserializerTest.java │ └── FileChangeWatchdogTest.java ├── gradle.properties ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .vscode ├── settings.json └── launch.json ├── .devcontainer └── devcontainer.json ├── settings.gradle.kts ├── spigot ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── turikhay │ │ │ │ └── mc │ │ │ │ └── mapmodcompanion │ │ │ │ └── spigot │ │ │ │ ├── PluginScheduler.java │ │ │ │ ├── ProtocolVersion.java │ │ │ │ ├── package-info.java │ │ │ │ ├── FoliaSupport.java │ │ │ │ ├── BukkitScheduler.java │ │ │ │ ├── SingleThreadScheduler.java │ │ │ │ ├── ProtocolLib.java │ │ │ │ ├── IdRegistry.java │ │ │ │ ├── LevelIdHandler.java │ │ │ │ ├── PrefixedIdRequest.java │ │ │ │ ├── XaeroHandler.java │ │ │ │ └── MapModCompanion.java │ │ └── resources │ │ │ └── config.yml │ └── test │ │ └── java │ │ └── com │ │ └── turikhay │ │ └── mc │ │ └── mapmodcompanion │ │ └── spigot │ │ ├── PrefixedIdRequestTest.java │ │ └── PrefixedIdRequestParserTest.java ├── log4j2-debug.xml └── build.gradle.kts ├── bungee ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── turikhay │ │ │ └── mc │ │ │ └── mapmodcompanion │ │ │ └── bungee │ │ │ ├── package-info.java │ │ │ ├── PacketHandler.java │ │ │ └── MapModCompanion.java │ │ └── resources │ │ └── config_bungee.yml └── build.gradle.kts ├── velocity ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── turikhay │ │ │ └── mc │ │ │ └── mapmodcompanion │ │ │ └── velocity │ │ │ ├── package-info.java │ │ │ ├── Channels.java │ │ │ ├── MessageHandler.java │ │ │ └── MapModCompanion.java │ │ └── resources │ │ └── config_velocity.toml ├── log4j2-debug.xml └── build.gradle.kts ├── VERSIONS.txt ├── .gitignore ├── LICENSE.txt ├── gradlew.bat ├── README.md └── gradlew /tests_e2e/docker-compose.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests_e2e/jar/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests_e2e/bot/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests_e2e/bot/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /tests_e2e/bot/.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: turikhay 2 | -------------------------------------------------------------------------------- /packages/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | base 3 | } -------------------------------------------------------------------------------- /tests_e2e/proxy/velocity/forwarding.secret: -------------------------------------------------------------------------------- 1 | dc4Fq995oJXx -------------------------------------------------------------------------------- /tests_e2e/server/spigot.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | bungeecord: true 3 | -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-convention") 3 | } 4 | -------------------------------------------------------------------------------- /tests_e2e/.gitignore: -------------------------------------------------------------------------------- 1 | test_env/ 2 | #jar/ 3 | debug.log 4 | 5 | .venv 6 | venv 7 | -------------------------------------------------------------------------------- /tests_e2e/bot/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /tests_e2e/saves/1.8.9/red.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turikhay/MapModCompanion/HEAD/tests_e2e/saves/1.8.9/red.dat -------------------------------------------------------------------------------- /tests_e2e/saves/1.13.2/blue.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turikhay/MapModCompanion/HEAD/tests_e2e/saves/1.13.2/blue.dat -------------------------------------------------------------------------------- /tests_e2e/saves/1.13.2/red.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turikhay/MapModCompanion/HEAD/tests_e2e/saves/1.13.2/red.dat -------------------------------------------------------------------------------- /tests_e2e/saves/1.17.1/blue.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turikhay/MapModCompanion/HEAD/tests_e2e/saves/1.17.1/blue.dat -------------------------------------------------------------------------------- /tests_e2e/saves/1.17.1/red.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turikhay/MapModCompanion/HEAD/tests_e2e/saves/1.17.1/red.dat -------------------------------------------------------------------------------- /tests_e2e/saves/1.8.9/blue.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turikhay/MapModCompanion/HEAD/tests_e2e/saves/1.8.9/blue.dat -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=com.turikhay.mc 2 | version=0.0.0-SNAPSHOT 3 | spigot_version=1.20.1 4 | protocolLib_version=5.3.0 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turikhay/MapModCompanion/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tests_e2e/server/server.properties: -------------------------------------------------------------------------------- 1 | online-mode=false 2 | level-type=flat 3 | view-distance=3 4 | simulation-distance=3 5 | difficulty=0 6 | -------------------------------------------------------------------------------- /tests_e2e/proxy/bungeecord/config.yml: -------------------------------------------------------------------------------- 1 | ip_forward: true 2 | online_mode: false 3 | listeners: 4 | - priorities: 5 | - red 6 | host: 0.0.0.0:25565 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic", 3 | "java.compile.nullAnalysis.mode": "automatic" 4 | } 5 | -------------------------------------------------------------------------------- /tests_e2e/server/plugins/bStats/config.yml: -------------------------------------------------------------------------------- 1 | enabled: false 2 | serverUuid: 00000000-0000-0000-0000-000000000000 3 | logFailedRequests: true 4 | logSentData: true 5 | -------------------------------------------------------------------------------- /tests_e2e/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi>=2023.11.17 2 | charset-normalizer>=3.3.2 3 | idna>=3.7 4 | PyYAML>=6.0.1 5 | requests>=2.32.0 6 | toml>=0.10.2 7 | urllib3>=2.1.0 8 | -------------------------------------------------------------------------------- /tests_e2e/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | python3 -m venv venv 5 | source venv/bin/activate 6 | pip install -r requirements.txt 7 | python run.py "$@" 8 | -------------------------------------------------------------------------------- /tests_e2e/server/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG TAG 2 | 3 | FROM itzg/minecraft-server:${TAG} 4 | 5 | ENV EULA=true TYPE=paper 6 | 7 | COPY --chown=minecraft:minecraft . /data 8 | 9 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/Disposable.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | public interface Disposable { 4 | void cleanUp(); 5 | } 6 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "hostRequirements": { "memory": "8gb" }, 4 | "onCreateCommand": "./gradlew" 5 | } 6 | -------------------------------------------------------------------------------- /tests_e2e/proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM itzg/bungeecord 2 | 3 | ENV SERVER_PORT=25565 4 | 5 | ENV JVM_OPTS="-Dlog4j2.configurationFile=log4j2.xml" 6 | 7 | COPY --chown=bungeecord:bungeecord . /server 8 | -------------------------------------------------------------------------------- /tests_e2e/proxy/velocity/plugins/bStats/config.txt: -------------------------------------------------------------------------------- 1 | enabled=false 2 | server-uuid=00000000-0000-0000-0000-000000000000 3 | log-errors=true 4 | log-sent-data=true 5 | log-response-status-text=true 6 | -------------------------------------------------------------------------------- /tests_e2e/proxy/bungeecord/plugins/bStats/config.yml: -------------------------------------------------------------------------------- 1 | enabled: false 2 | serverUuid: 00000000-0000-0000-0000-000000000000 3 | logFailedRequests: true 4 | logSentData: true 5 | logResponseStatusText: true -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "mapmodcompanion" 2 | 3 | include(":common") 4 | include(":bungee") 5 | include(":spigot") 6 | include(":velocity") 7 | include(":packages") 8 | include(":packages:single") 9 | -------------------------------------------------------------------------------- /tests_e2e/proxy/velocity/plugins/mapmodcompanion/config.toml: -------------------------------------------------------------------------------- 1 | [world_id.modern] 2 | enabled = true 3 | 4 | [world_id.legacy] 5 | enabled = true 6 | 7 | [xaero.world_map] 8 | enabled = true 9 | 10 | [xaero.mini_map] 11 | enabled = true 12 | -------------------------------------------------------------------------------- /tests_e2e/bot/tests/xaero/minimap.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from "minecraft-protocol"; 2 | import test from "./common.mjs"; 3 | 4 | export default { 5 | groups: ["xaerominimap"], 6 | test: (/** @type Client */ client) => test(client, "xaerominimap:main"), 7 | }; 8 | -------------------------------------------------------------------------------- /tests_e2e/bot/tests/xaero/worldmap.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from "minecraft-protocol"; 2 | import test from "./common.mjs"; 3 | 4 | export default { 5 | groups: ["xaeroworldmap"], 6 | test: (/** @type Client */ client) => test(client, "xaeroworldmap:main"), 7 | }; 8 | -------------------------------------------------------------------------------- /tests_e2e/bot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | 3 | ENV NODE_ENV=production 4 | 5 | WORKDIR /usr/src/app 6 | 7 | COPY package.json package-lock.json ./ 8 | 9 | RUN npm ci 10 | 11 | COPY . . 12 | 13 | ARG DEBUG="" 14 | ENV DEBUG=${DEBUG} 15 | 16 | CMD [ "node", "index.mjs" ] 17 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/PluginScheduler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.Disposable; 4 | 5 | public interface PluginScheduler extends Disposable { 6 | void schedule(Runnable r); 7 | } 8 | -------------------------------------------------------------------------------- /tests_e2e/proxy/bungeecord/plugins/MapModCompanion/config.yml: -------------------------------------------------------------------------------- 1 | verbose: true 2 | 3 | overrides: 4 | 1984: 1337 5 | 6 | world_id: 7 | modern: 8 | enabled: true 9 | legacy: 10 | enabled: true 11 | 12 | xaero: 13 | world_map: 14 | enabled: true 15 | mini_map: 16 | enabled: true 17 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | @ReturnValuesAreNonnullByDefault 3 | package com.turikhay.mc.mapmodcompanion; 4 | 5 | import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; 6 | 7 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/ProtocolVersion.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | public interface ProtocolVersion { 4 | int MINECRAFT_1_13_2 = 404; 5 | int MINECRAFT_1_14_4 = 498; 6 | int MINECRAFT_1_15_2 = 578; 7 | int MINECRAFT_1_16_3 = 753; 8 | } 9 | -------------------------------------------------------------------------------- /bungee/src/main/java/com/turikhay/mc/mapmodcompanion/bungee/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | @ReturnValuesAreNonnullByDefault 3 | package com.turikhay.mc.mapmodcompanion.bungee; 4 | 5 | import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; 6 | 7 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | @ReturnValuesAreNonnullByDefault 3 | package com.turikhay.mc.mapmodcompanion.spigot; 4 | 5 | import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; 6 | 7 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /velocity/src/main/java/com/turikhay/mc/mapmodcompanion/velocity/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | @ReturnValuesAreNonnullByDefault 3 | package com.turikhay.mc.mapmodcompanion.velocity; 4 | 5 | import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; 6 | 7 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/Channels.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | public interface Channels { 4 | String XAERO_MINIMAP_CHANNEL = "xaerominimap:main"; 5 | String XAERO_WORLDMAP_CHANNEL = "xaeroworldmap:main"; 6 | String WORLDID_CHANNEL = "worldinfo:world_id"; 7 | String WORLDID_LEGACY_CHANNEL = "world_id"; 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /common/src/test/java/IdBlenderTest.java: -------------------------------------------------------------------------------- 1 | import com.turikhay.mc.mapmodcompanion.IdBlender; 2 | import com.turikhay.mc.mapmodcompanion.StandardId; 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class IdBlenderTest { 8 | 9 | IdBlender blender = IdBlender.DEFAULT; 10 | 11 | @Test 12 | void test() { 13 | assertEquals(new StandardId(3600), blender.blend(new StandardId(42), 1337)); 14 | } 15 | } -------------------------------------------------------------------------------- /.github/workflows/modrinth.yml: -------------------------------------------------------------------------------- 1 | name: Modrinth release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | version: 7 | name: Detect version 8 | uses: ./.github/workflows/version.yml 9 | upload: 10 | name: Build and upload 11 | needs: version 12 | uses: ./.github/workflows/build.yml 13 | with: 14 | version: ${{ needs.version.outputs.version }} 15 | task: modrinth 16 | secrets: 17 | modrinth-token: ${{ secrets.MODRINTH_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "41 18 * * 1" 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v6 14 | 15 | - name: Update Gradle Wrapper 16 | uses: gradle-update/update-gradle-wrapper-action@v2 17 | with: 18 | repo-token: ${{ secrets.GRADLE_UPDATE_TOKEN }} 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/LightweightException.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public class LightweightException extends Exception { 6 | public LightweightException(String message, @Nullable Throwable cause) { 7 | super(message, cause, false, false); 8 | } 9 | 10 | public LightweightException(String message) { 11 | this(message, null); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/InitializationException.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public class InitializationException extends LightweightException { 6 | public InitializationException(String message, @Nullable Throwable cause) { 7 | super(message, cause); 8 | } 9 | 10 | public InitializationException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/MalformedPacketException.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public class MalformedPacketException extends LightweightException { 6 | public MalformedPacketException(String message, @Nullable Throwable cause) { 7 | super(message, cause); 8 | } 9 | 10 | public MalformedPacketException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/hangar.yml: -------------------------------------------------------------------------------- 1 | name: Hangar release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | version: 7 | name: Detect version 8 | uses: ./.github/workflows/version.yml 9 | upload: 10 | name: Build and upload 11 | needs: version 12 | uses: ./.github/workflows/build.yml 13 | with: 14 | version: ${{ needs.version.outputs.version }} 15 | task: publishPluginPublicationToHangar 16 | secrets: 17 | hangar-token: ${{ secrets.HANGAR_TOKEN }} 18 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/IdBlender.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.util.Objects; 4 | 5 | public interface IdBlender { 6 | IdBlender DEFAULT = new IdBlender() { 7 | @Override 8 | public IdType blend(IdType id, int anotherId) { 9 | return id.withId(Objects.hash(id.getId(), anotherId)); 10 | } 11 | }; 12 | 13 | IdType blend(IdType id, int anotherId); 14 | } 15 | -------------------------------------------------------------------------------- /.github/actions/setup_gradle/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Gradle 2 | 3 | inputs: 4 | dependency-graph: 5 | required: false 6 | default: generate-and-upload 7 | 8 | runs: 9 | using: composite 10 | steps: 11 | - name: Setup Java 12 | uses: actions/setup-java@v4 13 | with: 14 | distribution: temurin 15 | java-version: 21 16 | - name: Setup Gradle 17 | uses: gradle/actions/setup-gradle@v4 18 | with: 19 | dependency-graph: ${{ inputs.dependency-graph }} 20 | -------------------------------------------------------------------------------- /velocity/log4j2-debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests_e2e/bot/tests/world_id/modern.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from "minecraft-protocol"; 2 | import test, { expectedResponseBytes } from "./common.mjs"; 3 | 4 | export default { 5 | groups: ["voxelmap", "journeymap"], 6 | test: (/** @type Client */ client, /** @type number */ protocolVersion) => 7 | protocolVersion >= 754 // >= 1.16.4 8 | ? test(client, "worldinfo:world_id", { 9 | request: [0, 42, 0], 10 | response: [0, 42, ...expectedResponseBytes], 11 | }) 12 | : undefined, 13 | }; 14 | -------------------------------------------------------------------------------- /tests_e2e/server/plugins/MapModCompanion/config.yml: -------------------------------------------------------------------------------- 1 | verbose: true 2 | 3 | world_id: 4 | modern: 5 | enabled: true 6 | legacy: 7 | enabled: true 8 | 9 | xaero: 10 | world_map: 11 | enabled: true 12 | events: 13 | join: 14 | enabled: true 15 | repeat_times: 3 16 | world_change: 17 | enabled: true 18 | mini_map: 19 | enabled: true 20 | events: 21 | join: 22 | enabled: true 23 | repeat_times: 3 24 | world_change: 25 | enabled: true 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: npm 8 | directory: "/tests_e2e/bot" 9 | schedule: 10 | interval: monthly 11 | groups: 12 | minecraft: 13 | patterns: 14 | - "minecraft-data" 15 | - "minecraft-protocol" 16 | - package-ecosystem: github-actions 17 | directory: "/" 18 | schedule: 19 | interval: monthly 20 | groups: 21 | actions: 22 | patterns: 23 | - "actions/*" 24 | -------------------------------------------------------------------------------- /tests_e2e/bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmc-bot", 3 | "private": "true", 4 | "module": "true", 5 | "version": "1.0.0", 6 | "description": "", 7 | "main": "index.mjs", 8 | "scripts": { 9 | "debug": "DEBUG=minecraft-protocol node index.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "turikhay", 14 | "license": "MIT", 15 | "dependencies": { 16 | "args-parser": "^1.3.0", 17 | "minecraft-data": "^3.100.0", 18 | "minecraft-protocol": "^1.62.0" 19 | }, 20 | "devDependencies": { 21 | "prettier": "^3.7.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/Id.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | public interface Id { 4 | int MAGIC_MARKER = 42; 5 | 6 | int getId(); 7 | 8 | Id withIdUnchecked(int id); 9 | 10 | @SuppressWarnings("unchecked") 11 | default IdType withId(int id) { 12 | return (IdType) withIdUnchecked(id); 13 | } 14 | 15 | interface Deserializer { 16 | IdType deserialize(byte[] data) throws MalformedPacketException; 17 | } 18 | 19 | interface Serializer { 20 | byte[] serialize(IdType id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/FoliaSupport.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | public class FoliaSupport { 4 | private static Boolean IS_FOLIA_SERVER; 5 | 6 | public static boolean isFoliaServer() { 7 | if (IS_FOLIA_SERVER == null) { 8 | boolean isIt = true; 9 | try { 10 | Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); 11 | } catch (Throwable t) { 12 | isIt = false; 13 | } 14 | IS_FOLIA_SERVER = isIt; 15 | } 16 | return IS_FOLIA_SERVER; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/update-pages.yml: -------------------------------------------------------------------------------- 1 | name: Upload pages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | upload: 11 | name: Upload pages 12 | env: 13 | MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} 14 | HANGAR_TOKEN: ${{ secrets.HANGAR_TOKEN }} 15 | UPDATE_PAGES: true 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Setup Gradle 20 | uses: ./.github/actions/setup_gradle 21 | - name: Run tasks 22 | run: | 23 | ./gradlew \ 24 | modrinthSyncBody \ 25 | syncAllPluginPublicationPagesToHangar 26 | -------------------------------------------------------------------------------- /common/src/test/java/LevelMapPropertiesPacketSerializerTest.java: -------------------------------------------------------------------------------- 1 | import com.turikhay.mc.mapmodcompanion.LevelMapProperties; 2 | import com.turikhay.mc.mapmodcompanion.StandardId; 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class LevelMapPropertiesPacketSerializerTest { 8 | 9 | final LevelMapProperties.Serializer writer = LevelMapProperties.Serializer.instance(); 10 | 11 | @Test 12 | void regularTest() { 13 | assertArrayEquals(new byte[]{0, 0, 0, 0, 1}, writer.serialize(new StandardId(1))); 14 | assertArrayEquals(new byte[]{0, 0, 0, 5, 57}, writer.serialize(new StandardId(1337))); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests_e2e/bot/tests/world_id/legacy.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from "minecraft-protocol"; 2 | import test, { expectedResponseBytes } from "./common.mjs"; 3 | 4 | export default { 5 | groups: ["voxelmap"], 6 | test: (/** @type Client */ client, /** @type number */ protocolVersion) => 7 | protocolVersion >= 761 8 | ? // 1.19.3+ uses modern protocol 9 | undefined 10 | : test( 11 | client, 12 | // use non-prefixed version when < 1.13 13 | protocolVersion <= 340 ? "world_id" : "worldinfo:world_id", 14 | { 15 | request: [0, 0, 0, 42], 16 | response: [42, ...expectedResponseBytes], 17 | }, 18 | ), 19 | }; 20 | -------------------------------------------------------------------------------- /VERSIONS.txt: -------------------------------------------------------------------------------- 1 | 1.21.8 2 | 1.21.7 3 | 1.21.6 4 | 1.21.5 5 | 1.21.4 6 | 1.21.3 7 | 1.21.2 8 | 1.21.1 9 | 1.21 10 | 1.20.6 11 | 1.20.5 12 | 1.20.4 13 | 1.20.3 14 | 1.20.2 15 | 1.20.1 16 | 1.20 17 | 1.19.4 18 | 1.19.3 19 | 1.19.2 20 | 1.19.1 21 | 1.19 22 | 1.18.2 23 | 1.18.1 24 | 1.18 25 | 1.17.1 26 | 1.17 27 | 1.16.5 28 | 1.16.4 29 | 1.16.3 30 | 1.16.2 31 | 1.16.1 32 | 1.16 33 | 1.15.2 34 | 1.15.1 35 | 1.15 36 | 1.14.4 37 | 1.14.3 38 | 1.14.2 39 | 1.14.1 40 | 1.14 41 | 1.13.2 42 | 1.13.1 43 | 1.13 44 | 1.12.2 45 | 1.12.1 46 | 1.12 47 | 1.11.2 48 | 1.11.1 49 | 1.11 50 | 1.10.2 51 | 1.10.1 52 | 1.10 53 | 1.9.4 54 | 1.9.3 55 | 1.9.2 56 | 1.9.1 57 | 1.9 58 | 1.8.9 59 | 1.8.8 60 | 1.8.7 61 | 1.8.6 62 | 1.8.5 63 | 1.8.4 64 | 1.8.3 65 | 1.8.2 66 | 1.8.1 67 | 1.8 68 | -------------------------------------------------------------------------------- /velocity/src/main/resources/config_velocity.toml: -------------------------------------------------------------------------------- 1 | # 2 | # MapModCompanion (Velocity) 3 | # https://github.com/turikhay/MapModCompanion 4 | # 5 | 6 | # Modern world_id channel; supports: 7 | # - VoxelMap since 1.12.0 (Minecraft 1.19.3) 8 | # - JourneyMap since 5.7.1 (Minecraft 1.16.5) 9 | [world_id.modern] 10 | enabled = true 11 | 12 | # Legacy world_id channel; supports: 13 | # - VoxelMap 1.11.0 and older (Minecraft 1.19.2) 14 | [world_id.legacy] 15 | enabled = true 16 | 17 | # Xaero's World Map is supported since 20.20.0 (Minecraft 1.8.9) 18 | [xaero.world_map] 19 | enabled = true 20 | 21 | # Xaero's Minimap is supported since 1.10.0 (Minecraft 1.8.9) 22 | [xaero.mini_map] 23 | enabled = true 24 | 25 | # ID overrides 26 | [overrides] 27 | 42 = 1337 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | debug*/ 7 | htmlReport/ 8 | .kotlin/ 9 | 10 | ### IntelliJ IDEA ### 11 | .idea/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### Mac OS ### 39 | .DS_Store 40 | 41 | ### Python ### 42 | /venv/ 43 | /.venv/ 44 | 45 | ### GitHub Gradle Actions ### 46 | dependency-graph-reports/ 47 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - 'v*.*.*' 9 | workflow_call: 10 | 11 | jobs: 12 | prepare: 13 | name: Prepare 14 | outputs: 15 | version: ${{ steps.sha8.outputs.version }} 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Generate version string 19 | id: sha8 20 | run: | 21 | SHA=${{ github.sha }} 22 | SHA8=${SHA:0:8} 23 | echo "SHA8 = $SHA8" 24 | echo "version=0.0.0+$SHA8" >> $GITHUB_OUTPUT 25 | build: 26 | name: Build 27 | needs: prepare 28 | uses: ./.github/workflows/build.yml 29 | with: 30 | version: ${{ needs.prepare.outputs.version }} 31 | dependency-graph: generate-and-submit 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "run.py", 6 | "type": "python", 7 | "request": "launch", 8 | "program": "run.py", 9 | "args": [ 10 | "velocity", 11 | "1.20.1", 12 | "test" 13 | ], 14 | "cwd": "${workspaceFolder}/tests_e2e/", 15 | "console": "integratedTerminal", 16 | "justMyCode": true 17 | }, 18 | { 19 | "type": "java", 20 | "name": "debug proxy (in Docker)", 21 | "request": "attach", 22 | "hostName": "localhost", 23 | "port": 9010 24 | }, 25 | { 26 | "type": "java", 27 | "name": "debug server (in Docker)", 28 | "request": "attach", 29 | "hostName": "localhost", 30 | "port": 9011 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /bungee/src/main/resources/config_bungee.yml: -------------------------------------------------------------------------------- 1 | # 2 | # MapModCompanion (BungeeCord) 3 | # https://github.com/turikhay/MapModCompanion 4 | # 5 | 6 | # Enable verbose logging 7 | verbose: false 8 | 9 | # world_id packet channel support 10 | world_id: 11 | # Modern; supports: 12 | # - VoxelMap since 1.12.0 (Minecraft 1.19.3) 13 | # - JourneyMap since 5.7.1 (Minecraft 1.16.5) 14 | modern: 15 | enabled: true 16 | # Legacy; supports: 17 | # - VoxelMap 1.11.0 and older (Minecraft 1.19.2) 18 | legacy: 19 | enabled: true 20 | 21 | # Xaero's mod family support 22 | xaero: 23 | # Supported since 20.20.0 (Minecraft 1.8.9) 24 | world_map: 25 | enabled: true 26 | # Supported since 1.10.0 (Minecraft 1.8.9) 27 | mini_map: 28 | enabled: true 29 | 30 | # ID overrides 31 | overrides: 32 | 42: 1337 33 | -------------------------------------------------------------------------------- /spigot/src/test/java/com/turikhay/mc/mapmodcompanion/spigot/PrefixedIdRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.PrefixedId; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | public class PrefixedIdRequestTest { 9 | 10 | @Test 11 | void constructIdTest() { 12 | assertEquals(new PrefixedId(0, true, 1337), new PrefixedIdRequest(0, true).constructId(1337)); 13 | assertEquals(new PrefixedId(1, true, 1337), new PrefixedIdRequest(1, true).constructId(1337)); 14 | assertEquals(new PrefixedId(3, true, 1337), new PrefixedIdRequest(3, true).constructId(1337)); 15 | assertEquals(new PrefixedId(4, false, 1337), new PrefixedIdRequest(4, false).constructId(1337)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests_e2e/bot/tests/xaero/common.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from "minecraft-protocol"; 2 | 3 | const expectedResponseBuffer = Buffer.alloc(5); 4 | expectedResponseBuffer.writeInt32BE(1337, 1); 5 | 6 | export default function test( 7 | /** @type Client */ client, 8 | /** @type string */ channel, 9 | ) { 10 | // register channel 11 | client.once("login", () => { 12 | client.registerChannel(channel, ["restBuffer", []], true); 13 | }); 14 | 15 | return new Promise((resolve, reject) => { 16 | // validate incoming packets in our channel 17 | client.on(channel, (/** @type Buffer */ buffer) => { 18 | const isExpected = buffer.equals(expectedResponseBuffer); 19 | if (isExpected) { 20 | resolve(); 21 | } else { 22 | reject(`unexpected data: ${buffer}`); 23 | } 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/turikhay/mc/mapmodcompanion/velocity/Channels.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.velocity; 2 | 3 | import com.velocitypowered.api.proxy.messages.ChannelIdentifier; 4 | import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; 5 | import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; 6 | 7 | import static com.turikhay.mc.mapmodcompanion.Channels.*; 8 | 9 | public interface Channels { 10 | ChannelIdentifier WORLD_ID = MinecraftChannelIdentifier.from(WORLDID_CHANNEL); 11 | ChannelIdentifier WORLD_ID_LEGACY = new LegacyChannelIdentifier(WORLDID_LEGACY_CHANNEL); 12 | ChannelIdentifier XAERO_MINIMAP = MinecraftChannelIdentifier.from(XAERO_MINIMAP_CHANNEL); 13 | ChannelIdentifier XAERO_WORLDMAP = MinecraftChannelIdentifier.from(XAERO_WORLDMAP_CHANNEL); 14 | } 15 | -------------------------------------------------------------------------------- /tests_e2e/bot/tests/world_id/forge.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from "minecraft-protocol"; 2 | import test, { expectedResponseBytes } from "./common.mjs"; 3 | 4 | export default { 5 | groups: ["voxelmap"], 6 | test: (/** @type Client */ client, /** @type number */ protocolVersion) => 7 | protocolVersion < 340 || protocolVersion > 754 // Forge VoxelMap 1.12.2 - 1.16.5 8 | ? undefined 9 | : test( 10 | client, 11 | // non-prefixed channel on <= 1.12.2 12 | protocolVersion <= 340 ? "world_id" : "worldinfo:world_id", 13 | { 14 | request: protocolVersion == 340 ? [0, 0] : [0, 42, 0], 15 | response: 16 | protocolVersion <= 753 17 | ? [0, ...expectedResponseBytes] 18 | : [0, 42, ...expectedResponseBytes], 19 | }, 20 | ), 21 | }; 22 | -------------------------------------------------------------------------------- /velocity/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-shadow") 3 | } 4 | 5 | java { 6 | sourceCompatibility = JavaVersion.VERSION_11 7 | targetCompatibility = JavaVersion.VERSION_11 8 | } 9 | 10 | repositories { 11 | maven { 12 | name = "Paper" 13 | url = uri("https://repo.papermc.io/repository/maven-public/") 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation(project(":common")) 19 | implementation(libs.bstats.velocity) 20 | 21 | compileOnly(libs.velocity.api) 22 | annotationProcessor(libs.velocity.api) 23 | } 24 | 25 | tasks { 26 | val rewriteVelocityPluginJson by creating(PluginDescriptorTask::class) { 27 | dependsOn(compileJava) 28 | descriptorFile = project.layout.buildDirectory.file("classes/java/main/velocity-plugin.json") 29 | format = PluginDescriptorFormat.JSON 30 | append = true 31 | content.putAll(mapOf( 32 | "version" to project.version 33 | )) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bungee/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-shadow") 3 | } 4 | 5 | repositories { 6 | maven { 7 | name = "Sonatype" 8 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 9 | } 10 | maven { 11 | name = "Minecraft" 12 | url = uri("https://libraries.minecraft.net") 13 | content { 14 | includeGroup("com.mojang") 15 | } 16 | } 17 | } 18 | 19 | tasks { 20 | val writeBungeeYml by creating(PluginDescriptorTask::class) { 21 | descriptor = "bungee.yml" 22 | content.putAll(mapOf( 23 | "name" to "MapModCompanion", 24 | "version" to project.version, 25 | "author" to "turikhay", 26 | "main" to "com.turikhay.mc.mapmodcompanion.bungee.MapModCompanion" 27 | )) 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation(project(":common")) 33 | implementation(libs.bstats.bungeecord) 34 | compileOnly(libs.bungeecord.api) 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/StandardId.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.util.Objects; 4 | 5 | public class StandardId implements Id { 6 | private final int id; 7 | 8 | public StandardId(int id) { 9 | this.id = id; 10 | } 11 | 12 | public int getId() { 13 | return id; 14 | } 15 | 16 | @Override 17 | public StandardId withIdUnchecked(int id) { 18 | return new StandardId(id); 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | StandardId that = (StandardId) o; 26 | return id == that.id; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(id); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "StandardId{" + 37 | "id=" + id + 38 | '}'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/github.yml: -------------------------------------------------------------------------------- 1 | name: GitHub release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | version: 13 | name: Detect version 14 | uses: ./.github/workflows/version.yml 15 | build: 16 | name: Build 17 | needs: version 18 | uses: ./.github/workflows/build.yml 19 | with: 20 | version: ${{ needs.version.outputs.version }} 21 | release: 22 | name: Prepare release 23 | needs: 24 | - version 25 | - build 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Download build artifacts 29 | uses: actions/download-artifact@v6 30 | with: 31 | name: gradle-build 32 | - uses: softprops/action-gh-release@v2 33 | with: 34 | name: ${{ needs.version.outputs.version }} 35 | prerelease: ${{ needs.version.outputs.pre-release }} 36 | generate_release_notes: true 37 | draft: true 38 | files: ${{ needs.build.outputs.artifact }} 39 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/VerboseLogger.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.LogRecord; 5 | import java.util.logging.Logger; 6 | 7 | public class VerboseLogger extends Logger { 8 | private boolean verbose; 9 | 10 | public VerboseLogger(Logger logger) { 11 | super(logger.getName(), null); 12 | setParent(logger); 13 | } 14 | 15 | public boolean isVerbose() { 16 | return verbose; 17 | } 18 | 19 | public void setVerbose(boolean verbose) { 20 | this.verbose = verbose; 21 | } 22 | 23 | @Override 24 | public boolean isLoggable(Level level) { 25 | return verbose || super.isLoggable(level); 26 | } 27 | 28 | @Override 29 | public void log(LogRecord logRecord) { 30 | if (verbose && logRecord.getLevel().intValue() < Level.INFO.intValue()) { 31 | logRecord.setLevel(Level.INFO); 32 | } 33 | super.log(logRecord); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/IdLookup.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.util.Optional; 4 | 5 | public interface IdLookup { 6 | Optional findMatch(String id); 7 | 8 | default Optional findMatch(int id) { 9 | return findMatch(String.valueOf(id)); 10 | } 11 | 12 | class ConfigBased implements IdLookup { 13 | public static final String PATH_PREFIX = "overrides."; 14 | 15 | private final ConfigAccessor accessor; 16 | 17 | public ConfigBased(ConfigAccessor accessor) { 18 | this.accessor = accessor; 19 | } 20 | 21 | @Override 22 | public Optional findMatch(String id) { 23 | int value = accessor.getInt(PATH_PREFIX + id, Integer.MIN_VALUE); 24 | return value == Integer.MIN_VALUE ? Optional.empty() : Optional.of(value); 25 | } 26 | 27 | public interface ConfigAccessor { 28 | int getInt(String path, int def); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/e2e_all.yml: -------------------------------------------------------------------------------- 1 | name: Full E2E test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*.*.*' 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | uses: ./.github/workflows/nightly.yml 13 | test: 14 | name: E2E test 15 | needs: build 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | version: 20 | - 1.21.6 21 | - 1.20.6 22 | - 1.20.4 23 | - 1.19.4 24 | - 1.18.2 25 | - 1.17.1 26 | - 1.16.5 27 | - 1.16.3 28 | - 1.15.2 29 | - 1.14.4 30 | - 1.13.2 31 | - 1.12.2 32 | - 1.11.2 33 | - 1.10.2 34 | - 1.9.4 35 | - 1.8.9 36 | proxy: 37 | - velocity 38 | - bungeecord 39 | - waterfall 40 | server: 41 | - paper 42 | - folia 43 | uses: ./.github/workflows/e2e_test.yml 44 | with: 45 | proxy: ${{ matrix.proxy }} 46 | version: ${{ matrix.version }} 47 | server: ${{ matrix.server }} 48 | -------------------------------------------------------------------------------- /.github/workflows/e2e_test.yml: -------------------------------------------------------------------------------- 1 | name: E2E test (common) 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | proxy: 7 | required: true 8 | type: string 9 | server: 10 | default: paper 11 | type: string 12 | version: 13 | required: true 14 | type: string 15 | 16 | jobs: 17 | test: 18 | name: E2E (${{ inputs.version }}, ${{ inputs.proxy }}, ${{ inputs.server }}) 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | defaults: 22 | run: 23 | working-directory: ./tests_e2e 24 | env: 25 | # Legacy builder doesn't seem to properly mount /data folder inside server container for some reason 26 | DOCKER_BUILDKIT: 1 27 | SERVER_TYPE: "${{ inputs.server }}" 28 | steps: 29 | - uses: actions/checkout@v6 30 | - uses: actions/download-artifact@v6 31 | with: 32 | name: gradle-build 33 | - name: Build 34 | run: ./run.sh ${{ inputs.proxy }} ${{ inputs.version }} build 35 | - name: Test 36 | run: ./run.sh ${{ inputs.proxy }} ${{ inputs.version }} test 37 | -------------------------------------------------------------------------------- /spigot/log4j2-debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/ILogger.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.util.logging.Level; 4 | 5 | public interface ILogger { 6 | void fine(String message); 7 | 8 | void info(String message); 9 | 10 | void warn(String message, Throwable t); 11 | 12 | void error(String message, Throwable t); 13 | 14 | static ILogger ofJava(java.util.logging.Logger logger) { 15 | return new ILogger() { 16 | @Override 17 | public void fine(String message) { 18 | logger.fine(message); 19 | } 20 | 21 | @Override 22 | public void info(String message) { 23 | logger.info(message); 24 | } 25 | 26 | @Override 27 | public void warn(String message, Throwable t) { 28 | logger.log(Level.WARNING, message, t); 29 | } 30 | 31 | @Override 32 | public void error(String message, Throwable t) { 33 | logger.log(Level.SEVERE, message, t); 34 | } 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/BukkitScheduler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import org.bukkit.plugin.Plugin; 4 | 5 | import java.util.logging.Level; 6 | 7 | public class BukkitScheduler implements PluginScheduler { 8 | private final Plugin plugin; 9 | 10 | public BukkitScheduler(Plugin plugin) { 11 | this.plugin = plugin; 12 | } 13 | 14 | @Override 15 | public void cleanUp() { 16 | } 17 | 18 | @Override 19 | public void schedule(Runnable r) { 20 | if (plugin.getServer().isPrimaryThread()) { 21 | executeTask(r); 22 | } else { 23 | plugin.getServer().getScheduler().scheduleSyncDelayedTask(this.plugin, () -> executeTask(r)); 24 | } 25 | } 26 | 27 | private void executeTask(Runnable r) { 28 | try { 29 | r.run(); 30 | } catch (Throwable t) { 31 | plugin.getLogger().log(Level.SEVERE, "Failed to execute the task", t); 32 | } 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "BukkitScheduler{}"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Artur Khusainov 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 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/SingleThreadScheduler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.DaemonThreadFactory; 4 | import com.turikhay.mc.mapmodcompanion.ILogger; 5 | 6 | import java.util.concurrent.ScheduledThreadPoolExecutor; 7 | 8 | public class SingleThreadScheduler implements PluginScheduler { 9 | 10 | private final ScheduledThreadPoolExecutor service; 11 | 12 | public SingleThreadScheduler(ILogger logger) { 13 | this.service = new ScheduledThreadPoolExecutor( 14 | 1, 15 | new DaemonThreadFactory(logger, SingleThreadScheduler.class) 16 | ); 17 | service.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); 18 | } 19 | 20 | @Override 21 | public void cleanUp() { 22 | service.shutdown(); 23 | } 24 | 25 | @Override 26 | public void schedule(Runnable r) { 27 | service.submit(r); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "SingleThreadScheduler{" + 33 | "service=" + service + 34 | '}'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/src/test/java/LevelMapPropertiesPacketDeserializerTest.java: -------------------------------------------------------------------------------- 1 | import com.turikhay.mc.mapmodcompanion.LevelMapProperties; 2 | import com.turikhay.mc.mapmodcompanion.MalformedPacketException; 3 | import com.turikhay.mc.mapmodcompanion.StandardId; 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.assertThrows; 8 | 9 | class LevelMapPropertiesPacketDeserializerTest { 10 | 11 | final LevelMapProperties.Deserializer reader = LevelMapProperties.Deserializer.instance(); 12 | 13 | @Test 14 | void regularTest() throws MalformedPacketException { 15 | assertEquals(new StandardId(1), reader.deserialize(new byte[]{ 0, 0, 0, 0, 1 })); 16 | assertEquals(new StandardId(1337), reader.deserialize(new byte[]{ 0, 0, 0, 5, 57 })); 17 | } 18 | 19 | @Test 20 | void noMarkerTest() { 21 | assertThrows(MalformedPacketException.class, () -> reader.deserialize(new byte[]{ 5, 57 })); 22 | } 23 | 24 | @Test 25 | void emptyTest() { 26 | assertThrows(MalformedPacketException.class, () -> reader.deserialize(new byte[]{})); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests_e2e/bot/tests/world_id/common.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from "minecraft-protocol"; 2 | 3 | const expectedResponseBuffer = Buffer.from("1337"); 4 | export const expectedResponseBytes = [ 5 | expectedResponseBuffer.byteLength, 6 | ...expectedResponseBuffer, 7 | ]; 8 | 9 | /** 10 | * @typedef {Object} Options 11 | * @property {number[]} request 12 | * @property {number[]} response 13 | */ 14 | 15 | export default function test( 16 | /** @type Client */ client, 17 | /** @type string */ channel, 18 | /** @type Options */ options, 19 | ) { 20 | const request = Buffer.from(options.request); 21 | const response = Buffer.from(options.response); 22 | 23 | client.on("login", function () { 24 | client.registerChannel(channel, ["restBuffer", []], true); 25 | client.writeChannel(channel, request); 26 | }); 27 | 28 | return new Promise((resolve, reject) => { 29 | // validate response 30 | client.on(channel, (/** @type Buffer */ buffer) => { 31 | const isValid = buffer.equals(response); 32 | if (isValid) { 33 | resolve(); 34 | } else { 35 | reject( 36 | `unexpected response: ${[...buffer]}; expected: ${[...response]}`, 37 | ); 38 | } 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/e2e_notable.yml: -------------------------------------------------------------------------------- 1 | name: E2E test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | uses: ./.github/workflows/nightly.yml 16 | paper: 17 | name: E2E 18 | needs: build 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | version: 23 | - 1.16.5 24 | - 1.16.3 25 | - 1.13.2 26 | - 1.12.2 27 | - 1.8.9 28 | proxy: 29 | - velocity 30 | - bungeecord 31 | uses: ./.github/workflows/e2e_test.yml 32 | with: 33 | proxy: ${{ matrix.proxy }} 34 | version: ${{ matrix.version }} 35 | paper-folia: 36 | name: E2E w/Folia 37 | needs: build 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | version: 42 | - 1.21.6 43 | - 1.20.6 44 | proxy: 45 | - velocity 46 | - bungeecord 47 | server: 48 | - paper 49 | - folia 50 | uses: ./.github/workflows/e2e_test.yml 51 | with: 52 | proxy: ${{ matrix.proxy }} 53 | version: ${{ matrix.version }} 54 | server: ${{ matrix.server }} 55 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/PrefixLogger.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.LogRecord; 5 | import java.util.logging.Logger; 6 | 7 | public class PrefixLogger extends Logger { 8 | public static boolean INCLUDE_PREFIX = false; 9 | 10 | private final VerboseLogger parent; 11 | private final String formattedPrefix; 12 | 13 | public PrefixLogger(VerboseLogger parent, String prefix) { 14 | super(parent.getName() + " - " + prefix, null); 15 | this.parent = parent; 16 | this.formattedPrefix = "[" + prefix + "] "; 17 | setParent(parent); 18 | setLevel(Level.ALL); 19 | } 20 | 21 | @Override 22 | public boolean isLoggable(Level level) { 23 | return parent.isVerbose() || super.isLoggable(level); 24 | } 25 | 26 | @Override 27 | public void log(LogRecord record) { 28 | if (parent.isVerbose() && record.getLevel().intValue() < Level.INFO.intValue()) { 29 | record.setLevel(Level.INFO); 30 | } 31 | if (INCLUDE_PREFIX) { 32 | record.setMessage(this.formattedPrefix + record.getMessage()); 33 | } 34 | super.log(record); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/src/test/java/PrefixedIdPacketDeserializerTest.java: -------------------------------------------------------------------------------- 1 | import com.turikhay.mc.mapmodcompanion.*; 2 | import org.junit.jupiter.api.Test; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | class PrefixedIdPacketDeserializerTest { 7 | 8 | PrefixedId.Deserializer deserializer = PrefixedId.Deserializer.instance(); 9 | 10 | @Test 11 | void voxelMapForge1_12_2UpTo1_16_3() throws MalformedPacketException { 12 | test(new PrefixedId(1, false, 1), new byte[] { 0, 1, 49 }); 13 | } 14 | 15 | @Test 16 | void standardFormTest() throws MalformedPacketException { 17 | // JourneyMap 1.16.5, VoxelMap 1.19.2+ 18 | test(new PrefixedId(1, true, 1), new byte[] { 0, 42, 1, 49 }); 19 | } 20 | 21 | @Test 22 | void voxelMapFixTest() throws MalformedPacketException { 23 | // VoxelMap LiteLoader 1.8.9 - 1.12.2, VoxelMap Fabric 1.14.4 - 1.19.x 24 | test(new PrefixedId(0, true, 1), new byte[] { 42, 1, 49 }); 25 | } 26 | 27 | @Test 28 | void zeroFilledArrayTest() { 29 | assertThrows(MalformedPacketException.class, 30 | () -> deserializer.deserialize(new byte[] { 0, 0, 0 })); 31 | } 32 | 33 | private void test(PrefixedId expected, byte[] data) throws MalformedPacketException { 34 | assertEquals(expected, deserializer.deserialize(data)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Detect version 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | version: 7 | description: Version (in SemVer format) 8 | value: ${{ jobs.detect.outputs.version }} 9 | release-type: 10 | description: Release type (alpha, beta or release) 11 | value: ${{ jobs.detect.outputs.release-type }} 12 | pre-release: 13 | description: Whether is this version a pre-release (boolean) 14 | value: ${{ jobs.detect.outputs.pre-release }} 15 | 16 | jobs: 17 | detect: 18 | name: Detect version 19 | runs-on: ubuntu-latest 20 | outputs: 21 | version: ${{ steps.version.outputs.version }} 22 | release-type: ${{ steps.version.outputs.release-type }} 23 | pre-release: ${{ steps.version.outputs.pre-release }} 24 | steps: 25 | - id: version 26 | run: | 27 | TAG=${{ github.ref }} 28 | VERSION=${TAG#refs/tags/v} 29 | echo "version=$VERSION" >> $GITHUB_OUTPUT 30 | IS_PRE_RELEASE="false" 31 | RELEASE_TYPE="release" 32 | [[ $VERSION =~ .+(beta|pre|rc).* ]] && RELEASE_TYPE="beta" && IS_PRE_RELEASE="true" 33 | [[ $VERSION =~ .+(alpha).* ]] && RELEASE_TYPE="alpha" && IS_PRE_RELEASE="true" 34 | echo "release-type=$RELEASE_TYPE" >> $GITHUB_OUTPUT 35 | echo "pre-release=$IS_PRE_RELEASE" >> $GITHUB_OUTPUT 36 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/ProtocolLib.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.comphenix.protocol.ProtocolLibrary; 4 | import com.comphenix.protocol.ProtocolManager; 5 | import com.turikhay.mc.mapmodcompanion.Handler; 6 | import com.turikhay.mc.mapmodcompanion.InitializationException; 7 | import org.bukkit.entity.Player; 8 | 9 | public class ProtocolLib implements Handler { 10 | private final ProtocolManager manager; 11 | 12 | public ProtocolLib(ProtocolManager manager) { 13 | this.manager = manager; 14 | } 15 | 16 | public ProtocolLib() { 17 | this(ProtocolLibrary.getProtocolManager()); 18 | } 19 | 20 | public int getProtocolVersion(Player player) { 21 | return manager.getProtocolVersion(player); 22 | } 23 | 24 | @Override 25 | public void cleanUp() { 26 | } 27 | 28 | public static class Factory implements Handler.Factory { 29 | @Override 30 | public String getName() { 31 | return "ProtocolLib"; 32 | } 33 | 34 | @Override 35 | public Handler create(MapModCompanion plugin) throws InitializationException { 36 | try { 37 | return new ProtocolLib(); 38 | } catch (NoClassDefFoundError e) { 39 | throw new InitializationException("missing dependency", e); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spigot/src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # 2 | # MapModCompanion (Spigot) 3 | # https://github.com/turikhay/MapModCompanion 4 | # 5 | 6 | # Enable verbose logging 7 | verbose: false 8 | 9 | # Send only default world ID to the clients 10 | # Mods are (mostly) capable of distinguishing between different dimensions 11 | preferDefaultWorld: true 12 | 13 | # World id channel support 14 | world_id: 15 | # Modern; supports: 16 | # - VoxelMap since 1.12.0 (Minecraft 1.19.3) 17 | # - JourneyMap since 5.7.1 (Minecraft 1.16.5) 18 | modern: 19 | enabled: true 20 | # Legacy; supports: 21 | # - VoxelMap 1.11.0 and older (Minecraft 1.19.2) 22 | legacy: 23 | enabled: true 24 | 25 | # Xaero's mod family support 26 | xaero: 27 | # Supported since 20.20.0 (Minecraft 1.8.9) 28 | world_map: 29 | enabled: true 30 | events: 31 | join: 32 | enabled: true 33 | # Sometimes the mod doesn't pick up the initial packet 34 | repeat_times: 3 35 | world_change: 36 | enabled: true 37 | # Supported since 1.10.0 (Minecraft 1.8.9) 38 | mini_map: 39 | enabled: true 40 | events: 41 | join: 42 | enabled: true 43 | # Sometimes the mod doesn't pick up the initial packet 44 | repeat_times: 3 45 | world_change: 46 | enabled: true 47 | 48 | # ID overrides 49 | overrides: 50 | # Override by the world name 51 | custom_world_8_800: 5553535 52 | # Override by its derived ID 53 | 42: 1337 54 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/DaemonThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | public class DaemonThreadFactory implements ThreadFactory { 7 | private final AtomicInteger counter = new AtomicInteger(); 8 | private final ILogger logger; 9 | private final String name; 10 | private final Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() { 11 | @Override 12 | public void uncaughtException(Thread thread, Throwable throwable) { 13 | logger.error("Error executing the task inside " + thread.getName(), throwable); 14 | } 15 | }; 16 | 17 | public DaemonThreadFactory(ILogger logger, String name) { 18 | this.logger = logger; 19 | this.name = name; 20 | } 21 | 22 | public DaemonThreadFactory(ILogger logger, Class cl) { 23 | this(logger, cl.getSimpleName()); 24 | } 25 | 26 | @Override 27 | public Thread newThread(Runnable runnable) { 28 | Thread t = new Thread(runnable, computeNextName()); 29 | t.setDaemon(true); 30 | t.setUncaughtExceptionHandler(handler); 31 | return t; 32 | } 33 | 34 | private String computeNextName() { 35 | int i = counter.getAndIncrement(); 36 | if (i == 0) { 37 | return name; 38 | } 39 | return name + "#" + i; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | spotbugs = "4.8.3" 3 | shadow = "9.0.0" 4 | junit = "5.13.4" 5 | junit-platform-launcher = "1.13.4" 6 | bstats = "3.1.0" 7 | velocity-api = "3.1.1" 8 | jackson = "2.16.1" 9 | semver = "1.3.0" 10 | hangar = "0.1.3" 11 | bungeecord-api = "1.20-R0.2" 12 | 13 | [libraries] 14 | spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" } 15 | junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } 16 | junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } 17 | junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform-launcher" } 18 | bstats-bungeecord = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats" } 19 | bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } 20 | bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" } 21 | velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity-api" } 22 | jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" } 23 | semver = { module = "net.swiftzer.semver:semver", version.ref = "semver" } 24 | bungeecord-api = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord-api" } 25 | 26 | 27 | [plugins] 28 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } 29 | hangar = { id = "io.papermc.hangar-publish-plugin", version.ref = "hangar" } 30 | -------------------------------------------------------------------------------- /spigot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-shadow") 3 | } 4 | 5 | repositories { 6 | maven { 7 | name = "Sonatype" 8 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 9 | } 10 | maven { 11 | name = "Spigot" 12 | url = uri("https://hub.spigotmc.org/nexus/content/repositories/public/") 13 | } 14 | } 15 | 16 | tasks { 17 | val writePluginYml by creating(PluginDescriptorTask::class) { 18 | descriptor = "plugin.yml" 19 | content.putAll(mapOf( 20 | "name" to "MapModCompanion", 21 | "version" to project.version, 22 | "main" to "com.turikhay.mc.mapmodcompanion.spigot.MapModCompanion", 23 | "description" to "Plugin that fixes Multi-world detection for Xaero's Minimap, VoxelMap and JourneyMap", 24 | "authors" to listOf("turikhay"), 25 | "website" to "https://github.com/turikhay/MapModCompanion", 26 | "api-version" to "1.13", 27 | "softdepend" to listOf("ProtocolLib"), 28 | "folia-supported" to true, 29 | )) 30 | } 31 | } 32 | 33 | // From gradle.properties 34 | val spigot_version: String by project 35 | val protocolLib_version: String by project 36 | 37 | dependencies { 38 | implementation(project(":common")) 39 | implementation(libs.bstats.bukkit) 40 | 41 | // These dependencies are intentionally not present in libs.version.toml 42 | compileOnly("org.spigotmc:spigot-api:${spigot_version}-R0.1-SNAPSHOT") 43 | compileOnly("net.dmulloy2:ProtocolLib:${protocolLib_version}") 44 | } 45 | -------------------------------------------------------------------------------- /common/src/test/java/FileChangeWatchdogTest.java: -------------------------------------------------------------------------------- 1 | import com.turikhay.mc.mapmodcompanion.FileChangeWatchdog; 2 | import com.turikhay.mc.mapmodcompanion.ILogger; 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.lang.reflect.Method; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | class FileChangeWatchdogTest { 15 | 16 | @Test 17 | void callbackRunsOnceWhenFileChanges() throws Exception { 18 | Path file = Files.createTempFile("watchdog", ".tmp"); 19 | ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); 20 | try { 21 | AtomicInteger counter = new AtomicInteger(); 22 | FileChangeWatchdog watchdog = new FileChangeWatchdog( 23 | ILogger.ofJava(java.util.logging.Logger.getAnonymousLogger()), 24 | scheduler, 25 | file, 26 | counter::incrementAndGet 27 | ); 28 | Method tick = FileChangeWatchdog.class.getDeclaredMethod("tick"); 29 | tick.setAccessible(true); 30 | 31 | tick.invoke(watchdog); // initialize lastTime 32 | 33 | Thread.sleep(1000); 34 | Files.writeString(file, "a"); 35 | 36 | tick.invoke(watchdog); // should trigger callback 37 | tick.invoke(watchdog); // should not trigger again 38 | 39 | assertEquals(1, counter.get()); 40 | } finally { 41 | scheduler.shutdownNow(); 42 | Files.deleteIfExists(file); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests_e2e/README.md: -------------------------------------------------------------------------------- 1 | # E2E tests for MapModCompanion 2 | 3 | ⚠️ In order to run E2E tests you'll need at least 2 GiB of _available_ RAM. Check it by running: 4 | 5 | ```shell 6 | $ free -h 7 | ``` 8 | 9 | ## Why? 10 | 11 | I wanted to verify if the plugin actually works on all Minecraft versions we claim it supports. Setting up 30+ Paper, BungeeCord and Velocity instances manually was just too much work for me. 12 | 13 | ## How? 14 | 15 | Thanks to [@itzg](https://github.com/itzg), who probably spent countless hours on [Docker image for hosting Minecraft server](https://github.com/itzg/docker-minecraft-server), it was easy. We just spin up a test server using Docker Compose and log into the game. The bot (which is built using [node-minecraft-protocol](https://github.com/PrismarineJS/node-minecraft-protocol)) only needs to join the server and listen to specific plugin channels. 16 | 17 | Automatic bot test is not _that_ useful because it doesn't behave like real Minecraft client. Now I can easily debug different combinations of Minecraft server versions and proxies. 18 | 19 | ## Let me try 20 | 21 | ```shell 22 | $ DEBUG=1 ./run.sh 23 | ``` 24 | 25 | Proxy can be one of the following: 26 | * `bungeecord` 27 | * `waterfall` 28 | * `velocity` 29 | 30 | Version can be `1.8.9`, `1.12.2`, `1.16.5`, `1.19.3` etc. 31 | 32 | Command: 33 | * `test` – perform automatic (E2E) tests 34 | * `manual` – spin up specified server and proxy 35 | * * Use env `JAVA_DEBUG=1` to enable Java debugging. Local port `9010` goes for the proxy, `9011` for the server. Example: `JAVA_DEBUG=1 ./run.sh waterfall 1.16.5 manual` 36 | * * Use env `BLUE=1` to enable second server (e.g. to debug map persistence). You'll be able to switch between servers with `/server red/blue`. 37 | * * Use Env `SERVER_TYPE=folia` if you want to use Folia as a backend Minecraft server. Note that the script will just exit if there is no Folia support for the selected version. 38 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Gradle build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | version: 7 | required: true 8 | type: string 9 | spigot-target: 10 | type: string 11 | task: 12 | type: string 13 | default: build 14 | dependency-graph: 15 | type: string 16 | default: generate-and-upload 17 | outputs: 18 | artifact: 19 | description: Path to the jar artifact 20 | value: ${{ jobs.build.outputs.artifact }} 21 | secrets: 22 | hangar-token: 23 | required: false 24 | modrinth-token: 25 | required: false 26 | 27 | 28 | permissions: 29 | contents: write 30 | 31 | jobs: 32 | build: 33 | name: Run Gradle 34 | env: 35 | ARTIFACT: packages/single/build/libs/MapModCompanion.jar 36 | outputs: 37 | artifact: ${{ env.ARTIFACT }} 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v6 41 | - name: Setup Gradle 42 | uses: ./.github/actions/setup_gradle 43 | with: 44 | dependency-graph: ${{ inputs.dependency-graph }} 45 | - name: Determine target Spigot version 46 | id: target 47 | run: | 48 | TARGET=${{ inputs.spigot-target }} 49 | [[ -z "$TARGET" ]] && TARGET=$(tail -n1 VERSIONS.txt) 50 | echo "spigot-version=$TARGET" >> $GITHUB_OUTPUT 51 | - name: Run the task 52 | run: | 53 | ./gradlew \ 54 | ${{ inputs.task }} \ 55 | -Pversion=${{ inputs.version }} \ 56 | -Pspigot_version=${{ steps.target.outputs.spigot-version }} 57 | env: 58 | HANGAR_TOKEN: ${{ secrets.hangar-token }} 59 | MODRINTH_TOKEN: ${{ secrets.modrinth-token }} 60 | - name: Force original path preservation 61 | run: touch .empty 62 | - uses: actions/upload-artifact@v5 63 | with: 64 | name: gradle-build 65 | path: | 66 | .empty 67 | ${{ env.ARTIFACT }} 68 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/LevelMapProperties.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.io.*; 4 | 5 | public interface LevelMapProperties { 6 | class Deserializer implements Id.Deserializer { 7 | private static Deserializer INSTANCE; 8 | 9 | @Override 10 | public StandardId deserialize(byte[] data) throws MalformedPacketException { 11 | DataInputStream in = new DataInputStream(new ByteArrayInputStream(data)); 12 | try { 13 | int marker = in.readByte(); 14 | if (marker == 0) { 15 | return new StandardId(in.readInt()); 16 | } 17 | throw new MalformedPacketException("invalid marker byte (0x" + Integer.toHexString(marker) + ") in the level map properties packet"); 18 | } catch (IOException e) { 19 | throw new MalformedPacketException("unexpected error reading level map properties packet", e); 20 | } 21 | } 22 | 23 | public static Deserializer instance() { 24 | return INSTANCE == null ? INSTANCE = new Deserializer() : INSTANCE; 25 | } 26 | } 27 | 28 | class Serializer implements Id.Serializer { 29 | private static Serializer INSTANCE; 30 | 31 | @Override 32 | public byte[] serialize(StandardId id) { 33 | return serialize(id.getId()); 34 | } 35 | 36 | public byte[] serialize(int id) { 37 | ByteArrayOutputStream array = new ByteArrayOutputStream(); 38 | try(DataOutputStream out = new DataOutputStream(array)) { 39 | out.write(0); // LevelMapProperties { 40 | out.writeInt(id); // id } 41 | } catch (IOException e) { 42 | throw new RuntimeException("unexpected error", e); 43 | } 44 | return array.toByteArray(); 45 | } 46 | 47 | public static Serializer instance() { 48 | return INSTANCE == null ? INSTANCE = new Serializer() : INSTANCE; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests_e2e/proxy/velocity/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 29 | 30 | 31 | 32 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests_e2e/bot/index.mjs: -------------------------------------------------------------------------------- 1 | import argsParser from "args-parser"; 2 | import dataProvider from "minecraft-data"; 3 | 4 | import xaeroMinimapTest from "./tests/xaero/minimap.mjs"; 5 | import xaeroWorldMapTest from "./tests/xaero/worldmap.mjs"; 6 | import voxelMapLegacy from "./tests/world_id/legacy.mjs"; 7 | import voxelMapForge from "./tests/world_id/forge.mjs"; 8 | import worldIdModern from "./tests/world_id/modern.mjs"; 9 | import startTestClient from "./testClient.mjs"; 10 | 11 | const tests = [ 12 | { name: "Xaero's Minimap", test: xaeroMinimapTest }, 13 | { name: "Xaero's Worldmap", test: xaeroWorldMapTest }, 14 | { name: "VoxelMap (legacy)", test: voxelMapLegacy }, 15 | { name: "Forge VoxelMap", test: voxelMapForge }, 16 | { name: "worldinfo:world_id (modern)", test: worldIdModern }, 17 | ]; 18 | 19 | const args = argsParser(process.argv); 20 | 21 | const version = 22 | args.version ?? 23 | process.env.BOT_VERSION ?? 24 | (() => { 25 | throw new Error("version not defined"); 26 | })(); 27 | 28 | const data = dataProvider(version); 29 | 30 | if (!data) { 31 | throw `${version} not supported or unknown`; 32 | } 33 | 34 | const clientOptions = { 35 | host: args.host ?? process.env.BOT_HOST ?? "127.0.0.1", 36 | port: args.port ?? process.env.BOT_PORT ?? 25565, 37 | version, 38 | protocolVersion: data.version.version, 39 | }; 40 | 41 | const clientGroups = []; 42 | 43 | tests.forEach((tf) => { 44 | for (const testsArray of clientGroups) { 45 | const hasConflict = testsArray.some((tf1) => 46 | tf1.test.groups.some((group) => tf.test.groups.includes(group)), 47 | ); 48 | if (hasConflict) { 49 | continue; 50 | } 51 | testsArray.push(tf); 52 | return; 53 | } 54 | clientGroups.push([tf]); 55 | }); 56 | 57 | console.log(`⏳ Testing MapModCompanion on Minecraft ${version}`); 58 | 59 | const results = clientGroups.map((group, index) => 60 | startTestClient({ 61 | ...clientOptions, 62 | name: `Client#${index}`, 63 | username: `test${index}`, 64 | tests: group, 65 | }), 66 | ); 67 | 68 | let success = true; 69 | try { 70 | await Promise.all(results); 71 | } catch (e) { 72 | success = false; 73 | } 74 | 75 | if (!success) { 76 | console.warn(`😢 There were failing tests`); 77 | process.exit(1); 78 | } 79 | 80 | console.log(`🎉 Success`); 81 | process.exit(0); 82 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/Handler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public interface Handler extends Disposable { 8 | static List initialize(ILogger logger, PluginType plugin, List> factories) { 9 | ArrayList handlers = new ArrayList<>(); 10 | for (Factory factory : factories) { 11 | Handler handler = initialize(logger, plugin, factory); 12 | if (handler != null) { 13 | handlers.add(handler); 14 | } 15 | } 16 | return handlers; 17 | } 18 | 19 | @SuppressWarnings({"unchecked"}) 20 | static @Nullable HandlerType initialize(ILogger logger, PluginType plugin, Factory factory) { 21 | logger.fine("Calling the handler factory: " + factory.getName()); 22 | Handler handler; 23 | try { 24 | handler = factory.create(plugin); 25 | } catch (InitializationException e) { 26 | logger.info(factory.getName() + " handler will not be available (" + e.getMessage() + ")"); 27 | return null; 28 | } 29 | logger.fine("Handler has been initialized: " + handler); 30 | return (HandlerType) handler; 31 | } 32 | 33 | static List initialize(java.util.logging.Logger logger, PluginType plugin, List> factories) { 34 | return initialize(ILogger.ofJava(logger), plugin, factories); 35 | } 36 | 37 | static @Nullable HandlerType initialize(java.util.logging.Logger logger, PluginType plugin, Factory factory) { 38 | return initialize(ILogger.ofJava(logger), plugin, factory); 39 | } 40 | 41 | static void cleanUp(ILogger logger, List handlers) { 42 | logger.fine("Cleaning up " + handlers.size() + " handlers"); 43 | handlers.forEach(Handler::cleanUp); 44 | } 45 | 46 | static void cleanUp(java.util.logging.Logger logger, List handlers) { 47 | cleanUp(ILogger.ofJava(logger), handlers); 48 | } 49 | 50 | interface Factory { 51 | String getName(); 52 | Handler create(PluginType plugin) throws InitializationException; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /spigot/src/test/java/com/turikhay/mc/mapmodcompanion/spigot/PrefixedIdRequestParserTest.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import static com.turikhay.mc.mapmodcompanion.spigot.PrefixedIdRequest.parse; 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class PrefixedIdRequestParserTest { 13 | 14 | @Test 15 | void voxelMapForge1_12_2() throws MalformedPacketException { 16 | test(new PrefixedIdRequest(1, false), new byte[] { 0, 0 }); 17 | } 18 | 19 | @Test 20 | void voxelMapForge1_13_2UpTo1_16_3() throws MalformedPacketException { 21 | test( 22 | Arrays.asList( 23 | ProtocolVersion.MINECRAFT_1_13_2, 24 | ProtocolVersion.MINECRAFT_1_14_4, 25 | ProtocolVersion.MINECRAFT_1_15_2, 26 | ProtocolVersion.MINECRAFT_1_16_3 27 | ), 28 | new PrefixedIdRequest(1, false), 29 | new byte[] { 0, 42, 0 } 30 | ); 31 | } 32 | 33 | @Test 34 | void standardFormTest() throws MalformedPacketException { 35 | // JourneyMap 1.16.5, VoxelMap 1.19.2+ 36 | test( 37 | new PrefixedIdRequest(1, true), 38 | new byte[] { 0, 42, 0 } 39 | ); 40 | } 41 | 42 | @Test 43 | void voxelMapFixTest() throws MalformedPacketException { 44 | // VoxelMap LiteLoader 1.8.9 - 1.12.2, VoxelMap Fabric 1.14.4 - 1.19.x 45 | test( 46 | new PrefixedIdRequest(0, true), 47 | new byte[] { 0, 0, 0, 42 } 48 | ); 49 | } 50 | 51 | private void test(List protocolVersions, PrefixedIdRequest expected, byte[] data) throws MalformedPacketException { 52 | for (Integer protocolVersion : protocolVersions) { 53 | assertEquals(expected, parse(data, protocolVersion), "protocolVersion: " + protocolVersion.toString()); 54 | } 55 | } 56 | 57 | private void test(PrefixedIdRequest expected, byte[] data) throws MalformedPacketException { 58 | assertEquals(expected, parse(data, null)); 59 | } 60 | 61 | @Test 62 | void zeroTest() { 63 | assertThrows(MalformedPacketException.class, () -> parse(new byte[]{ 0 }, null)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/FileChangeWatchdog.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.attribute.FileTime; 7 | import java.util.concurrent.*; 8 | 9 | public class FileChangeWatchdog implements Disposable { 10 | private final ILogger logger; 11 | private final ScheduledExecutorService scheduler; 12 | private final Path path; 13 | private final Runnable callback; 14 | 15 | public FileChangeWatchdog(ILogger logger, ScheduledExecutorService scheduler, Path path, Runnable callback) { 16 | this.logger = logger; 17 | this.scheduler = scheduler; 18 | this.path = path; 19 | this.callback = callback; 20 | } 21 | 22 | public FileChangeWatchdog(VerboseLogger parent, ScheduledExecutorService scheduler, Path path, Runnable callback) { 23 | this( 24 | ILogger.ofJava(new PrefixLogger(parent, FileChangeWatchdog.class.getSimpleName())), 25 | scheduler, 26 | path, 27 | callback 28 | ); 29 | } 30 | 31 | private ScheduledFuture task; 32 | 33 | public void start() { 34 | logger.fine("Starting watchdog task"); 35 | task = scheduler.scheduleWithFixedDelay(this::tick, 5L, 5L, TimeUnit.SECONDS); 36 | } 37 | 38 | private FileTime lastTime; 39 | 40 | private void tick() { 41 | FileTime time; 42 | try { 43 | time = Files.getLastModifiedTime(path); 44 | } catch (IOException e) { 45 | logger.warn("Couldn't poll last modification time of " + path, e); 46 | return; 47 | } 48 | if (lastTime == null) { 49 | lastTime = time; 50 | return; 51 | } 52 | if (!time.equals(lastTime)) { 53 | lastTime = time; 54 | logger.info("File has been changed: " + path); 55 | try { 56 | callback.run(); 57 | } catch (RuntimeException e) { 58 | logger.error("File change callback error", e); 59 | } 60 | } 61 | } 62 | 63 | @Override 64 | public void cleanUp() { 65 | logger.fine("Cleaning up the task"); 66 | task.cancel(true); 67 | } 68 | 69 | public static ScheduledThreadPoolExecutor createScheduler(ILogger logger) { 70 | ScheduledThreadPoolExecutor service = new ScheduledThreadPoolExecutor( 71 | 1, 72 | new DaemonThreadFactory(logger, "MapModCompanion-" + FileChangeWatchdog.class.getSimpleName()) 73 | ); 74 | service.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); 75 | return service; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests_e2e/server/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /tests_e2e/bot/testClient.mjs: -------------------------------------------------------------------------------- 1 | import { createClient } from "minecraft-protocol"; 2 | 3 | const RETRY_SECONDS = 3; 4 | const RETRY_MAX_COUNT = 60; 5 | const skipped = new Set(); 6 | 7 | export default async function startTestClient(clientOptions) { 8 | const { name } = clientOptions; 9 | let connectRetries = 0; 10 | let tryAgain = false; 11 | do { 12 | try { 13 | await performTests(clientOptions); 14 | } catch (e) { 15 | if (e === "connect error" && ++connectRetries < RETRY_MAX_COUNT) { 16 | // console.log( 17 | // `😒 ${name} failed to connect to the server. Retrying in ${RETRY_SECONDS} seconds...` 18 | // ); 19 | await new Promise((resolve) => 20 | setTimeout(resolve, RETRY_SECONDS * 1000), 21 | ); 22 | tryAgain = true; 23 | continue; 24 | } 25 | console.error(`⚠️ ${name} error:`, e); 26 | throw e; 27 | } 28 | return; 29 | } while (tryAgain); 30 | } 31 | 32 | function performTests({ 33 | name: clientName, 34 | host, 35 | port, 36 | username, 37 | version, 38 | protocolVersion, 39 | tests, 40 | }) { 41 | const client = createClient({ 42 | host, 43 | port, 44 | username, 45 | version, 46 | }); 47 | client.on("login", () => { 48 | client.registerChannel("minecraft:register", ["registerarr", []]); 49 | client.registerChannel("minecraft:unregister", ["registerarr", []]); 50 | }); 51 | const results = tests.map(({ name, test }) => ({ 52 | name, 53 | result: test.test ? test.test(client, protocolVersion) : undefined, 54 | })); 55 | let allCompleted = false, 56 | connected = false; 57 | return new Promise((resolve, reject) => { 58 | client.on("error", (e) => { 59 | if (connected) { 60 | console.warn(`Error in ${clientName}: ${e}`); 61 | reject(e); 62 | } 63 | }); 64 | client.once("end", () => { 65 | if (allCompleted) { 66 | return; 67 | } 68 | if (connected) { 69 | reject(`disconnected while running tests`); 70 | } else { 71 | reject(`connect error`); 72 | } 73 | }); 74 | const promises = results.map(async ({ name: testName, result }) => { 75 | if (!result) { 76 | if (!skipped.has(testName)) { 77 | skipped.add(testName); 78 | console.log(`↘️ Skipped: ${testName}`); 79 | } 80 | return; 81 | } 82 | // console.log(`⏳ ${clientName} waits for ${name}`); 83 | try { 84 | await result; 85 | } catch (e) { 86 | console.warn(`${clientName} 💀 failed: ${testName}: ${e}`); 87 | reject(e); 88 | return; 89 | } 90 | console.log(`${clientName} ✅ OK: ${testName}`); 91 | }); 92 | Promise.all(promises) 93 | .then(() => resolve()) 94 | .catch((e) => reject(e)); 95 | client.once("login", () => { 96 | console.log(`✅ ${clientName} has connected to the server`); 97 | connected = true; 98 | setTimeout(() => { 99 | if (!allCompleted) { 100 | reject("timed out"); 101 | } 102 | }, 15000); 103 | }); 104 | }) 105 | .then(() => { 106 | allCompleted = true; 107 | }) 108 | .finally(() => client.end()); 109 | } 110 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/IdRegistry.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.IdLookup; 4 | import com.turikhay.mc.mapmodcompanion.PrefixLogger; 5 | import com.turikhay.mc.mapmodcompanion.VerboseLogger; 6 | import org.bukkit.World; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.logging.Logger; 12 | 13 | public interface IdRegistry { 14 | int getId(World world); 15 | 16 | class CacheableRegistry implements IdRegistry { 17 | private final Map cache = new HashMap<>(); 18 | private final IdRegistry delegate; 19 | 20 | public CacheableRegistry(IdRegistry delegate) { 21 | this.delegate = delegate; 22 | } 23 | 24 | @Override 25 | public int getId(World world) { 26 | return cache.computeIfAbsent(world.getName(), s -> delegate.getId(world)); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "CacheableRegistry{" + 32 | "delegate=" + delegate + 33 | '}'; 34 | } 35 | } 36 | 37 | class ConvertingRegistry implements IdRegistry { 38 | private final Logger logger; 39 | private final IdLookup lookup; 40 | private final IdRegistry delegate; 41 | 42 | public ConvertingRegistry(VerboseLogger parent, IdLookup lookup, IdRegistry delegate) { 43 | this.logger = new PrefixLogger(parent, ConvertingRegistry.class.getSimpleName()); 44 | this.lookup = lookup; 45 | this.delegate = delegate; 46 | } 47 | 48 | @Override 49 | public int getId(World world) { 50 | Optional byName = lookup.findMatch(world.getName()); 51 | if (byName.isPresent()) { 52 | logger.fine("Found override: " + world.getName() + " -> " + byName.get()); 53 | return byName.get(); 54 | } 55 | int processedId = delegate.getId(world); 56 | Optional byId = lookup.findMatch(processedId); 57 | if (byId.isPresent()) { 58 | logger.fine("Found override: " + processedId + " (" + world.getName() + ") -> " + byId.get()); 59 | return byId.get(); 60 | } 61 | return processedId; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "ConvertingRegistry{" + 67 | "delegate=" + delegate + 68 | '}'; 69 | } 70 | } 71 | 72 | class ConstantRegistry implements IdRegistry { 73 | private final int id; 74 | 75 | public ConstantRegistry(int id) { 76 | this.id = id; 77 | } 78 | 79 | @Override 80 | public int getId(World world) { 81 | return id; 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return "ConstantRegistry{" + 87 | "id=" + id + 88 | '}'; 89 | } 90 | } 91 | 92 | class DynamicUUIDRegistry implements IdRegistry { 93 | @Override 94 | public int getId(World world) { 95 | return world.getUID().hashCode(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/LevelIdHandler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | import org.bukkit.entity.Player; 5 | import org.bukkit.plugin.messaging.PluginMessageListener; 6 | 7 | import java.util.Arrays; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | public class LevelIdHandler implements Handler, PluginMessageListener { 12 | private final Logger logger; 13 | private final String channelName; 14 | private final boolean legacyChannel; 15 | private final MapModCompanion plugin; 16 | 17 | public LevelIdHandler(Logger logger, String channelName, boolean legacyChannel, MapModCompanion plugin) { 18 | this.logger = logger; 19 | this.channelName = channelName; 20 | this.legacyChannel = legacyChannel; 21 | this.plugin = plugin; 22 | } 23 | 24 | public void init() throws InitializationException { 25 | plugin.registerIncomingChannel(channelName, legacyChannel, this); 26 | plugin.registerOutgoingChannel(channelName); 27 | } 28 | 29 | @Override 30 | public void cleanUp() { 31 | plugin.unregisterIncomingChannel(channelName, this); 32 | plugin.unregisterOutgoingChannel(channelName); 33 | } 34 | 35 | @Override 36 | public void onPluginMessageReceived(String channel, Player player, byte[] requestBytes) { 37 | Integer protocolVersion = plugin.getProtocolLib().map(lib -> lib.getProtocolVersion(player)).orElse(null); 38 | int id = plugin.getRegistry().getId(player.getWorld()); 39 | PrefixedIdRequest request; 40 | try { 41 | request = PrefixedIdRequest.parse(requestBytes, protocolVersion); 42 | } catch (MalformedPacketException e) { 43 | logger.log(Level.WARNING, "world_id request from " + player.getName() + " might be corrupted", e); 44 | logger.fine(() -> "Payload: " + Arrays.toString(requestBytes)); 45 | return; 46 | } 47 | PrefixedId prefixedId = request.constructId(id); 48 | byte[] responseBytes = PrefixedId.Serializer.instance().serialize(prefixedId); 49 | logger.fine(() -> "Sending world_id packet to " + player.getName() + ": " + Arrays.toString(responseBytes)); 50 | player.sendPluginMessage(plugin, channelName, responseBytes); 51 | } 52 | 53 | public static class Factory implements Handler.Factory { 54 | private final String configPath; 55 | private final String channelName; 56 | private final boolean legacyChannel; 57 | 58 | public Factory(String configPath, String channelName, boolean legacyChannel) { 59 | this.configPath = configPath; 60 | this.channelName = channelName; 61 | this.legacyChannel = legacyChannel; 62 | } 63 | 64 | @Override 65 | public String getName() { 66 | return channelName; 67 | } 68 | 69 | @Override 70 | public LevelIdHandler create(MapModCompanion plugin) throws InitializationException { 71 | plugin.checkEnabled(configPath); 72 | LevelIdHandler handler = new LevelIdHandler( 73 | new PrefixLogger(plugin.getVerboseLogger(), channelName), 74 | channelName, legacyChannel, plugin 75 | ); 76 | handler.init(); 77 | return handler; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/PrefixedIdRequest.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | 5 | import javax.annotation.Nullable; 6 | import java.io.ByteArrayInputStream; 7 | import java.io.DataInputStream; 8 | import java.io.EOFException; 9 | import java.io.IOException; 10 | import java.util.Objects; 11 | 12 | import static com.turikhay.mc.mapmodcompanion.Id.MAGIC_MARKER; 13 | 14 | public class PrefixedIdRequest { 15 | private final int padding; 16 | private final boolean usesMagicByte; 17 | 18 | public PrefixedIdRequest(int padding, boolean usesMagicByte) { 19 | this.padding = padding; 20 | this.usesMagicByte = usesMagicByte; 21 | } 22 | 23 | public PrefixedId constructId(int id) { 24 | return new PrefixedId(padding, usesMagicByte, id); 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | PrefixedIdRequest that = (PrefixedIdRequest) o; 32 | return padding == that.padding && usesMagicByte == that.usesMagicByte; 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(padding, usesMagicByte); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "PrefixedIdRequest{" + 43 | "padding=" + padding + 44 | ", usesMagicByte=" + usesMagicByte + 45 | '}'; 46 | } 47 | 48 | public static PrefixedIdRequest parse(byte[] payload, @Nullable Integer protocolVersion) throws MalformedPacketException { 49 | int padding = -1; 50 | try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload))) { 51 | int c; 52 | try { 53 | do { 54 | padding++; 55 | c = in.readByte(); 56 | } while (c == 0); 57 | if (c != MAGIC_MARKER) { 58 | throw new MalformedPacketException("first byte after zero padding in the request is not a magic byte"); 59 | } 60 | } catch (EOFException e) { 61 | if (padding == 2) { 62 | // VoxelMap Forge 1.12.2 63 | return new PrefixedIdRequest(1, false); 64 | } else { 65 | throw new MalformedPacketException("ambiguous zero-filled request packet"); 66 | } 67 | } 68 | } catch (IOException e) { 69 | throw new MalformedPacketException("unexpected exception reading the request packet", e); 70 | } 71 | switch (padding) { 72 | case 1: 73 | if (protocolVersion != null && protocolVersion <= ProtocolVersion.MINECRAFT_1_16_3) { 74 | // VoxelMap Forge 1.13.2 - 1.16.3 75 | return new PrefixedIdRequest(1, false); 76 | } 77 | // VoxelMap Forge 1.16.4+ 78 | // VoxelMap Fabric 1.19.3+ 79 | // JourneyMap 1.16.5+ 80 | return new PrefixedIdRequest(padding, true); 81 | case 3: 82 | // VoxelMap LiteLoader 1.18.9 - 1.12.2 83 | // VoxelMap Fabric 1.14.4 - 1.19.x 84 | return new PrefixedIdRequest(0, true); 85 | default: 86 | throw new MalformedPacketException("unexpected prefix length in the request packet"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/single/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import net.swiftzer.semver.SemVer 2 | 3 | plugins { 4 | id("java-shadow") 5 | id("com.modrinth.minotaur") 6 | id("io.papermc.hangar-publish-plugin") 7 | id("platform-readme") 8 | } 9 | 10 | // There is no source code in this project, but Gradle 9.0+ will 11 | // still complain about compatibility with ':velocity' project 12 | java { 13 | sourceCompatibility = JavaVersion.VERSION_11 14 | targetCompatibility = JavaVersion.VERSION_11 15 | } 16 | 17 | dependencies { 18 | implementation(project(":bungee")) 19 | implementation(project(":spigot")) 20 | implementation(project(":velocity")) 21 | } 22 | 23 | val dedupShadowJar = tasks.named("dedupShadowJar") 24 | val semVer = SemVer.parse(project.version as String) 25 | val isRelease = semVer.preRelease == null 26 | val commonChangelog = """ 27 | Changelog is available on 28 | [GitHub](https://github.com/turikhay/MapModCompanion/releases/tag/v${project.version}) 29 | """.trimIndent() 30 | val allVersions = provider { rootProject.file("VERSIONS.txt").readLines() } 31 | 32 | modrinth { 33 | token = System.getenv("MODRINTH_TOKEN") 34 | projectId = "UO7aDcrF" 35 | versionNumber = project.version as String 36 | changelog = commonChangelog 37 | versionType = run { 38 | val preRelease = semVer.preRelease 39 | if (preRelease != null) { 40 | if (preRelease.contains("beta")) { 41 | "beta" 42 | } else { 43 | "alpha" 44 | } 45 | } else { 46 | "release" 47 | } 48 | } 49 | syncBodyFrom = platformReadme.contents 50 | file = dedupShadowJar.singleFile 51 | gameVersions = allVersions 52 | loaders.addAll(listOf( 53 | "bukkit", 54 | "bungeecord", 55 | "folia", 56 | "paper", 57 | "spigot", 58 | "velocity", 59 | "waterfall", 60 | )) 61 | } 62 | 63 | hangarPublish { 64 | publications.register("plugin") { 65 | version = project.version as String 66 | id = "MapModCompanion" 67 | channel = run { 68 | val preRelease = semVer.preRelease 69 | if (preRelease != null) { 70 | "Beta" 71 | } else { 72 | "Release" 73 | } 74 | } 75 | changelog = commonChangelog 76 | apiKey = System.getenv("HANGAR_TOKEN") 77 | platforms { 78 | val singleJar = dedupShadowJar.singleFile 79 | paper { 80 | jar = singleJar 81 | platformVersions = allVersions.map { 82 | // (VERSIONS.txt uses reverse order) 83 | listOf("${it.last()}-${it.first()}") // 1.8 - latest 84 | } 85 | dependencies { 86 | hangar("ProtocolLib") { 87 | required = false 88 | } 89 | } 90 | } 91 | waterfall { 92 | jar = singleJar 93 | platformVersions = allVersions.map { list -> 94 | list.map { 95 | val split = it.split(".") // -> 1, 20[, 4] 96 | assert(split.size > 1) 97 | assert(split.first() == "1") // will Minecraft 2.0 ever come out? 98 | Integer.parseInt(split[1]) // "1.20.4" -> 20 99 | }.sorted() 100 | }.map { 101 | f -> f.filter { it >= 11 } // Waterfall is only available >= 1.11 102 | }.map { 103 | listOf("1.${it.first()}-1.${it.last()}") 104 | } 105 | } 106 | velocity { 107 | val velocityFamily = libs.versions.velocity.api.map { 108 | val split = it.split(".") 109 | "${split[0]}.${split[1]}" 110 | } 111 | jar = singleJar 112 | platformVersions = velocityFamily.map { listOf(it) } 113 | } 114 | } 115 | pages { 116 | resourcePage(platformReadme.contents) 117 | } 118 | } 119 | } 120 | 121 | tasks { 122 | shadowJar { 123 | archiveFileName = "MapModCompanion-shadow.jar" 124 | } 125 | getByName("modrinth") { 126 | dependsOn(dedupShadowJar) 127 | } 128 | getByName("publishPluginPublicationToHangar") { 129 | dependsOn(dedupShadowJar) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/turikhay/mc/mapmodcompanion/velocity/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.velocity; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | import com.velocitypowered.api.event.Subscribe; 5 | import com.velocitypowered.api.event.connection.PluginMessageEvent; 6 | import com.velocitypowered.api.proxy.Player; 7 | import com.velocitypowered.api.proxy.ServerConnection; 8 | import com.velocitypowered.api.proxy.messages.ChannelIdentifier; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.Arrays; 13 | 14 | public class MessageHandler implements Handler { 15 | private final Logger logger; 16 | private final MapModCompanion plugin; 17 | private final ChannelIdentifier channelId; 18 | private final Id.Deserializer deserializer; 19 | private final Id.Serializer serializer; 20 | 21 | public MessageHandler(Logger logger, MapModCompanion plugin, ChannelIdentifier channelId, Id.Deserializer deserializer, Id.Serializer serializer) { 22 | this.logger = logger; 23 | this.plugin = plugin; 24 | this.channelId = channelId; 25 | this.deserializer = deserializer; 26 | this.serializer = serializer; 27 | } 28 | 29 | public void init() { 30 | logger.debug("Registering the channel: {}", channelId); 31 | plugin.getServer().getChannelRegistrar().register(channelId); 32 | plugin.getServer().getEventManager().register(plugin, this); 33 | } 34 | 35 | @Subscribe 36 | public void onPluginMessage(PluginMessageEvent event) { 37 | if (!event.getIdentifier().equals(channelId)) { 38 | return; 39 | } 40 | 41 | if (logger.isDebugEnabled()) { 42 | logger.debug("Data sent from {} to {} (channel {}): {}", event.getSource(), event.getTarget(), 43 | channelId, Arrays.toString(event.getData())); 44 | } 45 | 46 | if (!(event.getSource() instanceof ServerConnection)) { 47 | return; 48 | } 49 | var server = (ServerConnection) event.getSource(); 50 | 51 | if (!(event.getTarget() instanceof Player)) { 52 | return; 53 | } 54 | var player = (Player) event.getTarget(); 55 | 56 | IdType id; 57 | try { 58 | id = deserializer.deserialize(event.getData()); 59 | } catch (MalformedPacketException e) { 60 | if (logger.isDebugEnabled()) { 61 | logger.debug("Received possibly malformed packet in {}", channelId, e); 62 | logger.debug("Packet data: {}", Arrays.toString(event.getData())); 63 | } 64 | return; 65 | } 66 | 67 | event.setResult(PluginMessageEvent.ForwardResult.handled()); 68 | 69 | var newId = plugin.getConverter() 70 | .findMatch(id.getId()) 71 | .map(id::withId) 72 | .orElseGet(() -> IdBlender.DEFAULT.blend(id, server.getServerInfo().getName().hashCode())); 73 | 74 | logger.debug("Intercepting world_id packet sent to {} (channel {}): {} -> {}", 75 | player.getGameProfile().getName(), channelId.getId(), id, newId); 76 | 77 | player.sendPluginMessage(channelId, serializer.serialize(newId)); 78 | } 79 | 80 | @Override 81 | public void cleanUp() { 82 | logger.debug("Unregistering the channel: {}", channelId); 83 | plugin.getServer().getChannelRegistrar().unregister(channelId); 84 | plugin.getServer().getEventManager().unregisterListener(plugin, this); 85 | } 86 | 87 | public static class Factory implements Handler.Factory { 88 | private final String configPath; 89 | private final ChannelIdentifier channelId; 90 | private final Id.Deserializer deserializer; 91 | private final Id.Serializer serializer; 92 | 93 | public Factory(String configPath, ChannelIdentifier channelId, Id.Deserializer deserializer, Id.Serializer serializer) { 94 | this.configPath = configPath; 95 | this.channelId = channelId; 96 | this.deserializer = deserializer; 97 | this.serializer = serializer; 98 | } 99 | 100 | @Override 101 | public String getName() { 102 | return channelId.getId(); 103 | } 104 | 105 | @Override 106 | public MessageHandler create(MapModCompanion plugin) throws InitializationException { 107 | if (!plugin.getConfig().getBoolean(configPath + ".enabled", true)) { 108 | throw new InitializationException("disabled in the config"); 109 | } 110 | var handler = new MessageHandler<>( 111 | LoggerFactory.getLogger(plugin.getClass().getName() + ":" + channelId.getId()), 112 | plugin, 113 | channelId, 114 | deserializer, 115 | serializer 116 | ); 117 | handler.init(); 118 | return handler; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /common/src/main/java/com/turikhay/mc/mapmodcompanion/PrefixedId.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion; 2 | 3 | import java.io.*; 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.Objects; 6 | 7 | public class PrefixedId implements Id { 8 | private final int padding; 9 | private final boolean usesMagicByte; 10 | private final int id; 11 | 12 | public PrefixedId(int padding, boolean usesMagicByte, int id) { 13 | this.padding = padding; 14 | this.usesMagicByte = usesMagicByte; 15 | this.id = id; 16 | } 17 | 18 | @Deprecated 19 | public PrefixedId(int padding, int id) { 20 | this(padding, true, id); 21 | } 22 | 23 | @Override 24 | public int getId() { 25 | return id; 26 | } 27 | 28 | public int getPadding() { 29 | return padding; 30 | } 31 | 32 | @Override 33 | public PrefixedId withIdUnchecked(int id) { 34 | return new PrefixedId(this.padding, this.usesMagicByte, id); 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | PrefixedId that = (PrefixedId) o; 42 | return padding == that.padding && usesMagicByte == that.usesMagicByte && id == that.id; 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | return Objects.hash(padding, usesMagicByte, id); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "PrefixedId{" + 53 | "padding=" + padding + 54 | ", usesMagicByte=" + usesMagicByte + 55 | ", id=" + id + 56 | '}'; 57 | } 58 | 59 | public static class Deserializer implements Id.Deserializer { 60 | private static Deserializer INSTANCE; 61 | 62 | @Override 63 | public PrefixedId deserialize(byte[] data) throws MalformedPacketException { 64 | DataInputStream in = new DataInputStream(new ByteArrayInputStream(data)); 65 | try { 66 | int length; 67 | int prefixSize = -1; 68 | int c; 69 | try { 70 | do { 71 | prefixSize++; 72 | c = in.readByte(); 73 | } while (c == 0); 74 | } catch (EOFException e) { 75 | // zero-filled response 76 | throw new MalformedPacketException("zero-filled prefixed id packet"); 77 | } 78 | boolean usesMagicNumber = true; 79 | if (c != MAGIC_MARKER) { 80 | // 1.12.2 <= VoxelMap Forge <= 1.16.3 doesn't use MAGIC_MARKER in the response 81 | usesMagicNumber = false; 82 | length = c; 83 | } else { 84 | length = in.readByte(); 85 | } 86 | byte[] buf = new byte[length]; 87 | int read = in.read(buf, 0, length); 88 | if (read < length) { 89 | throw new MalformedPacketException("incorrect length (" + read + " < " + length + ") in the prefixed id packet"); 90 | } 91 | String id = new String(buf, StandardCharsets.UTF_8); 92 | int numeric; 93 | try { 94 | numeric = Integer.parseInt(id); 95 | } catch (NumberFormatException e) { 96 | throw new MalformedPacketException("couldn't parse an integer from prefixed id packet", e); 97 | } 98 | return new PrefixedId(prefixSize, usesMagicNumber, numeric); 99 | } catch (IOException e) { 100 | throw new MalformedPacketException("unexpected error reading prefixed id packet", e); 101 | } 102 | } 103 | 104 | public static Deserializer instance() { 105 | return INSTANCE == null ? INSTANCE = new Deserializer() : INSTANCE; 106 | } 107 | } 108 | 109 | public static class Serializer implements Id.Serializer { 110 | private static Serializer INSTANCE; 111 | 112 | @Override 113 | public byte[] serialize(PrefixedId id) { 114 | ByteArrayOutputStream array = new ByteArrayOutputStream(); 115 | try (DataOutputStream out = new DataOutputStream(array)) { 116 | for (int i = 0; i < id.getPadding(); i++) { 117 | out.writeByte(0); // packetId, or prefix 118 | } 119 | if (id.usesMagicByte) { 120 | out.writeByte(MAGIC_MARKER); // 42 (literally) 121 | } 122 | byte[] data = String.valueOf(id.getId()).getBytes(StandardCharsets.UTF_8); 123 | out.write(data.length); // length 124 | out.write(data); // UTF 125 | } catch (IOException e) { 126 | throw new RuntimeException("unexpected error", e); 127 | } 128 | return array.toByteArray(); 129 | } 130 | 131 | public static Serializer instance() { 132 | return INSTANCE == null ? INSTANCE = new Serializer() : INSTANCE; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /bungee/src/main/java/com/turikhay/mc/mapmodcompanion/bungee/PacketHandler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.bungee; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | import net.md_5.bungee.api.connection.ProxiedPlayer; 5 | import net.md_5.bungee.api.connection.Server; 6 | import net.md_5.bungee.api.event.PluginMessageEvent; 7 | import net.md_5.bungee.api.plugin.Listener; 8 | import net.md_5.bungee.event.EventHandler; 9 | 10 | import java.util.Arrays; 11 | import java.util.Locale; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | 15 | public class PacketHandler implements Handler, Listener { 16 | private final MapModCompanion plugin; 17 | private final Logger logger; 18 | private final String channelName; 19 | private final Id.Deserializer deserializer; 20 | private final Id.Serializer serializer; 21 | 22 | public PacketHandler(MapModCompanion plugin, Logger logger, String channelName, 23 | Id.Deserializer deserializer, Id.Serializer serializer) { 24 | this.plugin = plugin; 25 | this.logger = logger; 26 | this.channelName = channelName; 27 | this.deserializer = deserializer; 28 | this.serializer = serializer; 29 | } 30 | 31 | public void init() { 32 | logger.fine("Registering channel " + channelName); 33 | plugin.getProxy().registerChannel(channelName); 34 | plugin.getProxy().getPluginManager().registerListener(plugin, this); 35 | } 36 | 37 | @Override 38 | public void cleanUp() { 39 | logger.fine("Unregistering channel " + channelName); 40 | plugin.getProxy().unregisterChannel(channelName); 41 | plugin.getProxy().getPluginManager().unregisterListener(this); 42 | } 43 | 44 | @EventHandler 45 | public void onPluginMessageSentToPlayer(PluginMessageEvent event) { 46 | if (!event.getTag().equals(channelName)) { 47 | return; 48 | } 49 | 50 | if (logger.isLoggable(Level.FINEST)) { 51 | logger.finest(String.format(Locale.ROOT, 52 | "Data sent from %s to %s (channel %s):", 53 | event.getSender(), event.getReceiver(), channelName 54 | )); 55 | logger.finest("Data (0): " + Arrays.toString(event.getData())); 56 | } 57 | 58 | if (!(event.getSender() instanceof Server)) { 59 | return; 60 | } 61 | Server server = (Server) event.getSender(); 62 | 63 | if (!(event.getReceiver() instanceof ProxiedPlayer)) { 64 | return; 65 | } 66 | ProxiedPlayer player = (ProxiedPlayer) event.getReceiver(); 67 | 68 | IdType id; 69 | try { 70 | id = deserializer.deserialize(event.getData()); 71 | } catch (MalformedPacketException e) { 72 | if (logger.isLoggable(Level.FINE)) { 73 | logger.log(Level.FINE, "Received possibly malformed packet in " + channelName, e); 74 | logger.fine("Packet data: " + Arrays.toString(event.getData())); 75 | } 76 | return; 77 | } 78 | 79 | event.setCancelled(true); 80 | 81 | IdType newId = plugin.getConverter() 82 | .findMatch(id.getId()) 83 | .map(id::withId) 84 | .orElseGet(() -> IdBlender.DEFAULT.blend(id, server.getInfo().getName().hashCode())); 85 | 86 | logger.fine(String.format(Locale.ROOT, 87 | "Intercepting world_id packet sent to %s (channel %s): %s -> %s", 88 | player.getName(), channelName, id, newId 89 | )); 90 | 91 | player.sendData(channelName, serializer.serialize(newId)); 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "PacketHandler{" + 97 | "channelName='" + channelName + '\'' + 98 | '}'; 99 | } 100 | 101 | public static class Factory implements Handler.Factory { 102 | private final String configPath; 103 | private final String channelName; 104 | private final Id.Deserializer deserializer; 105 | private final Id.Serializer serializer; 106 | 107 | public Factory(String configPath, String channelName, 108 | Id.Deserializer deserializer, Id.Serializer serializer) { 109 | this.configPath = configPath; 110 | this.channelName = channelName; 111 | this.deserializer = deserializer; 112 | this.serializer = serializer; 113 | } 114 | 115 | @Override 116 | public String getName() { 117 | return channelName; 118 | } 119 | 120 | @Override 121 | public PacketHandler create(MapModCompanion plugin) throws InitializationException { 122 | if (!plugin.getConfig().getBoolean(configPath + ".enabled", true)) { 123 | throw new InitializationException("disabled in the config"); 124 | } 125 | PacketHandler handler = new PacketHandler<>( 126 | plugin, 127 | new PrefixLogger(plugin.getVerboseLogger(), channelName), 128 | channelName, 129 | deserializer, 130 | serializer 131 | ); 132 | handler.init(); 133 | return handler; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /bungee/src/main/java/com/turikhay/mc/mapmodcompanion/bungee/MapModCompanion.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.bungee; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | import net.md_5.bungee.api.plugin.Plugin; 5 | import net.md_5.bungee.config.Configuration; 6 | import net.md_5.bungee.config.ConfigurationProvider; 7 | import net.md_5.bungee.config.YamlConfiguration; 8 | import org.bstats.bungeecord.Metrics; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.InputStreamReader; 13 | import java.io.OutputStream; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.util.*; 17 | import java.util.concurrent.ScheduledExecutorService; 18 | 19 | public class MapModCompanion extends Plugin { 20 | private static final int BSTATS_ID = 17976; 21 | 22 | private final List> factories = Arrays.asList( 23 | new PacketHandler.Factory<>( 24 | "world_id.modern", 25 | Channels.WORLDID_CHANNEL, 26 | PrefixedId.Deserializer.instance(), 27 | PrefixedId.Serializer.instance() 28 | ), 29 | new PacketHandler.Factory<>( 30 | "world_id.legacy", 31 | Channels.WORLDID_LEGACY_CHANNEL, 32 | PrefixedId.Deserializer.instance(), 33 | PrefixedId.Serializer.instance() 34 | ), 35 | new PacketHandler.Factory<>( 36 | "xaero.mini_map", 37 | Channels.XAERO_MINIMAP_CHANNEL, 38 | LevelMapProperties.Deserializer.instance(), 39 | LevelMapProperties.Serializer.instance() 40 | ), 41 | new PacketHandler.Factory<>( 42 | "xaero.world_map", 43 | Channels.XAERO_WORLDMAP_CHANNEL, 44 | LevelMapProperties.Deserializer.instance(), 45 | LevelMapProperties.Serializer.instance() 46 | ) 47 | ); 48 | private final IdLookup converter = new IdLookup.ConfigBased((path, def) -> getConfig().getInt(path, def)); 49 | 50 | private VerboseLogger logger; 51 | private ScheduledExecutorService fileChangeWatchdogScheduler; 52 | private Configuration configuration; 53 | private List handlers = Collections.emptyList(); 54 | private FileChangeWatchdog fileChangeWatchdog; 55 | 56 | public IdLookup getConverter() { 57 | return converter; 58 | } 59 | 60 | public VerboseLogger getVerboseLogger() { 61 | return logger; 62 | } 63 | 64 | public Configuration getConfig() { 65 | return configuration; 66 | } 67 | 68 | @Override 69 | public void onLoad() { 70 | PrefixLogger.INCLUDE_PREFIX = true; 71 | logger = new VerboseLogger(getLogger()); 72 | } 73 | 74 | @Override 75 | public void onEnable() { 76 | fileChangeWatchdogScheduler = FileChangeWatchdog.createScheduler(ILogger.ofJava(logger)); 77 | new Metrics(this, BSTATS_ID); 78 | load(); 79 | } 80 | 81 | @Override 82 | public void onDisable() { 83 | unload(); 84 | fileChangeWatchdogScheduler.shutdown(); 85 | } 86 | 87 | private void load() { 88 | logger.fine("Loading"); 89 | 90 | boolean reload = configuration != null; 91 | try { 92 | configuration = readConfig(); 93 | } catch (IOException e) { 94 | throw new RuntimeException("Couldn't read or save configuration", e); 95 | } 96 | logger.info("Configuration has been " + (reload ? "reloaded" : "loaded")); 97 | 98 | logger.setVerbose(configuration.getBoolean("verbose", false)); 99 | logger.fine("Verbose logging enabled"); 100 | 101 | handlers = Handler.initialize(logger, this, factories); 102 | 103 | fileChangeWatchdog = new FileChangeWatchdog( 104 | logger, 105 | fileChangeWatchdogScheduler, 106 | getDataFolder().toPath().resolve("config.yml"), 107 | this::reload 108 | ); 109 | fileChangeWatchdog.start(); 110 | } 111 | 112 | private void unload() { 113 | logger.fine("Unloading"); 114 | Handler.cleanUp(logger, handlers); 115 | handlers = Collections.emptyList(); 116 | fileChangeWatchdog.cleanUp(); 117 | } 118 | 119 | private void reload() { 120 | unload(); 121 | load(); 122 | } 123 | 124 | private Configuration readConfig() throws IOException { 125 | Path configFile = getConfigFile(); 126 | if (!Files.exists(configFile)) { 127 | logger.fine("Creating new config file"); 128 | Files.createDirectories(configFile.getParent()); 129 | try (InputStream in = getResourceAsStream("config_bungee.yml"); 130 | OutputStream out = Files.newOutputStream(configFile)) { 131 | byte[] buffer = new byte[1024]; 132 | int read; 133 | while ((read = in.read(buffer)) >= 0) { 134 | out.write(buffer, 0, read); 135 | } 136 | } 137 | } else { 138 | logger.fine("Reading existing config file"); 139 | } 140 | try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(configFile))) { 141 | return ConfigurationProvider.getProvider(YamlConfiguration.class).load(reader); 142 | } 143 | } 144 | 145 | private Path getConfigFile() { 146 | return getDataFolder().toPath().resolve("config.yml"); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/XaeroHandler.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | import org.bukkit.World; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.EventHandler; 7 | import org.bukkit.event.EventPriority; 8 | import org.bukkit.event.HandlerList; 9 | import org.bukkit.event.Listener; 10 | import org.bukkit.event.player.PlayerChangedWorldEvent; 11 | import org.bukkit.event.player.PlayerEvent; 12 | import org.bukkit.event.player.PlayerJoinEvent; 13 | 14 | import java.util.Arrays; 15 | import java.util.Locale; 16 | import java.util.UUID; 17 | import java.util.concurrent.Executors; 18 | import java.util.concurrent.ScheduledExecutorService; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.logging.Logger; 21 | 22 | public class XaeroHandler implements Handler, Listener { 23 | private final Logger logger; 24 | private final String configPath; 25 | private final String channelName; 26 | private final MapModCompanion plugin; 27 | private final ScheduledExecutorService scheduler; 28 | 29 | public XaeroHandler(Logger logger, String configPath, String channelName, MapModCompanion plugin) { 30 | this.logger = logger; 31 | this.configPath = configPath; 32 | this.channelName = channelName; 33 | this.plugin = plugin; 34 | 35 | this.scheduler = Executors.newSingleThreadScheduledExecutor( 36 | new DaemonThreadFactory(ILogger.ofJava(logger), XaeroHandler.class) 37 | ); 38 | } 39 | 40 | public void init() throws InitializationException { 41 | plugin.registerOutgoingChannel(channelName); 42 | plugin.getServer().getPluginManager().registerEvents(this, plugin); 43 | logger.fine("Event listener has been registered"); 44 | } 45 | 46 | @Override 47 | public void cleanUp() { 48 | plugin.unregisterOutgoingChannel(channelName); 49 | HandlerList.unregisterAll(this); 50 | logger.fine("Event listener has been unregistered"); 51 | scheduler.shutdown(); 52 | } 53 | 54 | @EventHandler(priority = EventPriority.MONITOR) 55 | public void onPlayerJoined(PlayerJoinEvent event) { 56 | sendPacket(event, Type.JOIN); 57 | } 58 | 59 | @EventHandler(priority = EventPriority.MONITOR) 60 | public void onWorldChanged(PlayerChangedWorldEvent event) { 61 | sendPacket(event, Type.WORLD_CHANGE); 62 | } 63 | 64 | private void sendPacket(PlayerEvent event, Type type) { 65 | Player p = event.getPlayer(); 66 | World world = p.getWorld(); 67 | int id = plugin.getRegistry().getId(world); 68 | byte[] payload = LevelMapProperties.Serializer.instance().serialize(id); 69 | SendPayloadTask task = new SendPayloadTask(logger, plugin, p.getUniqueId(), channelName, payload, world.getUID()); 70 | int repeatTimes = plugin.getConfig().getInt( 71 | configPath + ".events." + type.name().toLowerCase(Locale.ROOT) + ".repeat_times", 72 | 1 73 | ); 74 | if (repeatTimes > 1) { 75 | for (int i = 0; i < repeatTimes; i++) { 76 | scheduler.schedule(task, i, TimeUnit.SECONDS); 77 | } 78 | } else { 79 | task.run(); 80 | } 81 | } 82 | 83 | private enum Type { 84 | JOIN, 85 | WORLD_CHANGE, 86 | } 87 | 88 | public static class Factory implements Handler.Factory { 89 | private final String configPath; 90 | private final String channelName; 91 | 92 | public Factory(String configPath, String channelName) { 93 | this.configPath = configPath; 94 | this.channelName = channelName; 95 | } 96 | 97 | @Override 98 | public String getName() { 99 | return channelName; 100 | } 101 | 102 | @Override 103 | public XaeroHandler create(MapModCompanion plugin) throws InitializationException { 104 | plugin.checkEnabled(configPath); 105 | XaeroHandler handler = new XaeroHandler( 106 | new PrefixLogger(plugin.getVerboseLogger(), channelName), 107 | configPath, channelName, plugin 108 | ); 109 | handler.init(); 110 | return handler; 111 | } 112 | } 113 | 114 | private static class SendPayloadTask implements Runnable { 115 | private final Logger logger; 116 | private final MapModCompanion plugin; 117 | private final UUID playerId; 118 | private final String channelName; 119 | private final byte[] payload; 120 | private final UUID expectedWorld; 121 | 122 | public SendPayloadTask(Logger logger, MapModCompanion plugin, UUID playerId, String channelName, byte[] payload, 123 | UUID expectedWorld) { 124 | this.logger = logger; 125 | this.plugin = plugin; 126 | this.playerId = playerId; 127 | this.channelName = channelName; 128 | this.payload = payload; 129 | this.expectedWorld = expectedWorld; 130 | } 131 | 132 | @Override 133 | public void run() { 134 | Player player = plugin.getServer().getPlayer(playerId); 135 | if (player == null) { 136 | return; 137 | } 138 | UUID world = player.getWorld().getUID(); 139 | if (!world.equals(expectedWorld)) { 140 | logger.fine("Skipping sending Xaero's LevelMapProperties to " + player.getName() + ": unexpected world"); 141 | return; 142 | } 143 | logger.fine(() -> "Sending Xaero's LevelMapProperties to " + player.getName() + ": " + Arrays.toString(payload)); 144 | player.sendPluginMessage(plugin, channelName, payload); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests_e2e/proxy/velocity/velocity.toml: -------------------------------------------------------------------------------- 1 | # Config version. Do not change this 2 | config-version = "2.6" 3 | 4 | # What port should the proxy be bound to? By default, we'll bind to all addresses on port 25577. 5 | bind = "0.0.0.0:25565" 6 | 7 | # What should be the MOTD? This gets displayed when the player adds your server to 8 | # their server list. Only MiniMessage format is accepted. 9 | motd = "<#09add3>A Velocity Server" 10 | 11 | # What should we display for the maximum number of players? (Velocity does not support a cap 12 | # on the number of players online.) 13 | show-max-players = 500 14 | 15 | # Should we authenticate players with Mojang? By default, this is on. 16 | online-mode = false 17 | 18 | # Should the proxy enforce the new public key security standard? By default, this is on. 19 | force-key-authentication = true 20 | 21 | # If client's ISP/AS sent from this proxy is different from the one from Mojang's 22 | # authentication server, the player is kicked. This disallows some VPN and proxy 23 | # connections but is a weak form of protection. 24 | prevent-client-proxy-connections = false 25 | 26 | # Should we forward IP addresses and other data to backend servers? 27 | # Available options: 28 | # - "none": No forwarding will be done. All players will appear to be connecting 29 | # from the proxy and will have offline-mode UUIDs. 30 | # - "legacy": Forward player IPs and UUIDs in a BungeeCord-compatible format. Use this 31 | # if you run servers using Minecraft 1.12 or lower. 32 | # - "bungeeguard": Forward player IPs and UUIDs in a format supported by the BungeeGuard 33 | # plugin. Use this if you run servers using Minecraft 1.12 or lower, and are 34 | # unable to implement network level firewalling (on a shared host). 35 | # - "modern": Forward player IPs and UUIDs as part of the login process using 36 | # Velocity's native forwarding. Only applicable for Minecraft 1.13 or higher. 37 | player-info-forwarding-mode = "legacy" 38 | 39 | # If you are using modern or BungeeGuard IP forwarding, configure a file that contains a unique secret here. 40 | # The file is expected to be UTF-8 encoded and not empty. 41 | forwarding-secret-file = "forwarding.secret" 42 | 43 | # Announce whether or not your server supports Forge. If you run a modded server, we 44 | # suggest turning this on. 45 | # 46 | # If your network runs one modpack consistently, consider using ping-passthrough = "mods" 47 | # instead for a nicer display in the server list. 48 | announce-forge = false 49 | 50 | # If enabled (default is false) and the proxy is in online mode, Velocity will kick 51 | # any existing player who is online if a duplicate connection attempt is made. 52 | kick-existing-players = false 53 | 54 | # Should Velocity pass server list ping requests to a backend server? 55 | # Available options: 56 | # - "disabled": No pass-through will be done. The velocity.toml and server-icon.png 57 | # will determine the initial server list ping response. 58 | # - "mods": Passes only the mod list from your backend server into the response. 59 | # The first server in your try list (or forced host) with a mod list will be 60 | # used. If no backend servers can be contacted, Velocity won't display any 61 | # mod information. 62 | # - "description": Uses the description and mod list from the backend server. The first 63 | # server in the try (or forced host) list that responds is used for the 64 | # description and mod list. 65 | # - "all": Uses the backend server's response as the proxy response. The Velocity 66 | # configuration is used if no servers could be contacted. 67 | ping-passthrough = "DISABLED" 68 | 69 | # If not enabled (default is true) player IP addresses will be replaced by in logs 70 | enable-player-address-logging = true 71 | 72 | [forced-hosts] 73 | 74 | [advanced] 75 | # How large a Minecraft packet has to be before we compress it. Setting this to zero will 76 | # compress all packets, and setting it to -1 will disable compression entirely. 77 | compression-threshold = 256 78 | 79 | # How much compression should be done (from 0-9). The default is -1, which uses the 80 | # default level of 6. 81 | compression-level = -1 82 | 83 | # How fast (in milliseconds) are clients allowed to connect after the last connection? By 84 | # default, this is three seconds. Disable this by setting this to 0. 85 | login-ratelimit = 0 86 | 87 | # Specify a custom timeout for connection timeouts here. The default is five seconds. 88 | connection-timeout = 5000 89 | 90 | # Specify a read timeout for connections here. The default is 30 seconds. 91 | read-timeout = 30000 92 | 93 | # Enables compatibility with HAProxy's PROXY protocol. If you don't know what this is for, then 94 | # don't enable it. 95 | haproxy-protocol = false 96 | 97 | # Enables TCP fast open support on the proxy. Requires the proxy to run on Linux. 98 | tcp-fast-open = false 99 | 100 | # Enables BungeeCord plugin messaging channel support on Velocity. 101 | bungee-plugin-message-channel = true 102 | 103 | # Shows ping requests to the proxy from clients. 104 | show-ping-requests = false 105 | 106 | # By default, Velocity will attempt to gracefully handle situations where the user unexpectedly 107 | # loses connection to the server without an explicit disconnect message by attempting to fall the 108 | # user back, except in the case of read timeouts. BungeeCord will disconnect the user instead. You 109 | # can disable this setting to use the BungeeCord behavior. 110 | failover-on-unexpected-server-disconnect = true 111 | 112 | # Declares the proxy commands to 1.13+ clients. 113 | announce-proxy-commands = true 114 | 115 | # Enables the logging of commands 116 | log-command-executions = false 117 | 118 | # Enables logging of player connections when connecting to the proxy, switching servers 119 | # and disconnecting from the proxy. 120 | log-player-connections = true 121 | 122 | [query] 123 | # Whether to enable responding to GameSpy 4 query responses or not. 124 | enabled = false 125 | 126 | # If query is enabled, on what port should the query protocol listen on? 127 | port = 25577 128 | 129 | # This is the map name that is reported to the query services. 130 | map = "Velocity" 131 | 132 | # Whether plugins should be shown in query response by default or not 133 | show-plugins = false 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Companion for map mods 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 | 30 | Allay from Minecraft holding a compass and waving with their other hand at the viewer 37 | 38 | 39 | **With this plugin your minimap will never be confused which world you're in. [A more in-depth explanation can be found in the wiki](https://github.com/turikhay/MapModCompanion/wiki/How-it-works).** 40 | 41 |
How it should look like 42 | 43 | | Mod | Screenshot | 44 | | ----|------------| 45 | | Xaero's World Map | Screenshot of Xaero's WorldMap menu | 46 | | VoxelMap | Screenshot of the game with a minimap on the top-right corner Screenshot of a map | 47 | | Xaero's Minimap | See Xaero's WorldMap | 48 | | JourneyMap | It just works 😄 | 49 | 50 |
51 | 52 | Companion plugin for 53 | [Xaero's Minimap] 54 | (and their [World Map][Xaero's World Map]), 55 | [JourneyMap] and 56 | VoxelMap (both [old][VoxelMap (old)] and [updated][VoxelMap-Updated]). 57 | Provides a way for these mods to identify worlds on BungeeCord/Velocity servers. 58 | 59 | It's recommended to install this plugin on a fresh server, otherwise **existing map data** 60 | (waypoints, map cache, etc.) **may no longer be visible to some players**. Fortunately, 61 | [there are ways to restore it](https://github.com/turikhay/MapModCompanion/wiki/Restore-map-data). 62 | It's worth mentioning that the plugin doesn't affect in-game progress. 63 | 64 | This plugin was inspired by @kosma's [worldnamepacket], 65 | which supported Velocity, Fabric and Spigot at the time of writing. 66 | 67 | If you have any questions, please [join my Discord][Discord]. 68 | 69 | [![](https://bstats.org/signatures/bukkit/MapModCompanion.svg)](https://bstats.org/plugin/bukkit/MapModCompanion/16539 "MapModCompanion on bStats") 70 | 71 | ## Support table 72 | | Mod | Oldest version | Latest version | Status | 73 | |------------------------------------------------------------------------------------|----------------------------|--------------------------------------------------------------|-------------| 74 | | [Xaero's Minimap] | v20.20.0 / Minecraft 1.8.9 | v25.2.14 / Minecraft 1.21.9 | ✅ Supported | 75 | | [Xaero's World Map] | v1.10.0 / Minecraft 1.8.9 | v1.39.16 / Minecraft 1.21.9 | ✅ Supported[[1]](https://github.com/turikhay/MapModCompanion/issues/62) | 76 | | [JourneyMap] | v5.7.1 / Minecraft 1.16.5 | v6.0.0-beta.52 / Minecraft 1.21.9 | ✅ Supported | 77 | | VoxelMap | [v1.7.10][VoxelMap (old)] / Minecraft 1.8 | [v1.15.7][VoxelMap-Updated] / Minecraft 1.21.8 | ✅ Supported[[2]](https://github.com/turikhay/MapModCompanion/issues/8) | 78 | 79 | [Folia](https://papermc.io/software/folia) is supported, but isn't tested thoroughly. Please report if the support is broken. 80 | 81 | ## Installation 82 | 83 | ℹ️ Plugin must be installed on every downstream (backend) server in your network. Simply installing it on the proxy side (BungeeCord/Velocity) isn't enough. To ensure compatibility, you need to install the plugin on both the proxy server (BungeeCord/Velocity) and each of the backend servers (Spigot/Paper). 84 | 85 | 1. Download the latest release 86 | 2. Put each file into the corresponding plugins folder 87 | 3. That's it. No configuration is required. You can restart your servers now. 88 | 89 | ## Configuration 90 | The configuration file is stored at `plugins/MapModCompanion/config.yml` for both Spigot and BungeeCord. 91 | Velocity uses `plugins/mapmodcompanion/config.toml`. 92 | 93 | The configuration file reloads automatically if it's modified. 94 | 95 | 96 | ## Alternatives 97 | - If you're running Forge or Fabric server, just install the map mod on your server: this will unlock all its 98 | features. 99 | - [worldnamepacket] (Velocity, Fabric, Spigot) 100 | - [journeymap-bukkit](https://github.com/TeamJM/journeymap-bukkit) (Spigot) 101 | - [JourneyMap Server](https://www.curseforge.com/minecraft/mc-mods/journeymap-server) (Spigot) 102 | 103 | 104 | [Discord]: https://discord.gg/H9ACHEqBrg 105 | [Xaero's Minimap]: https://modrinth.com/mod/xaeros-minimap 106 | [Xaero's World Map]: https://modrinth.com/mod/xaeros-world-map 107 | [JourneyMap]: https://modrinth.com/mod/journeymap 108 | [VoxelMap (old)]: https://www.curseforge.com/minecraft/mc-mods/voxelmap 109 | [VoxelMap-Updated]: https://modrinth.com/mod/voxelmap-updated 110 | [worldnamepacket]: https://github.com/kosma/worldnamepacket 111 | -------------------------------------------------------------------------------- /velocity/src/main/java/com/turikhay/mc/mapmodcompanion/velocity/MapModCompanion.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.velocity; 2 | 3 | import com.google.inject.Inject; 4 | import com.moandjiezana.toml.Toml; 5 | import com.turikhay.mc.mapmodcompanion.*; 6 | import com.velocitypowered.api.event.Subscribe; 7 | import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; 8 | import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; 9 | import com.velocitypowered.api.plugin.Plugin; 10 | import com.velocitypowered.api.plugin.annotation.DataDirectory; 11 | import com.velocitypowered.api.proxy.ProxyServer; 12 | import org.bstats.velocity.Metrics; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.concurrent.ScheduledExecutorService; 24 | 25 | import static com.turikhay.mc.mapmodcompanion.velocity.Channels.*; 26 | 27 | @Plugin( 28 | id = "mapmodcompanion", 29 | name = "MapModCompanion", 30 | version = "to be filled by the build script", 31 | url = "https://github.com/turikhay/MapModCompanion", 32 | authors = {"turikhay"} 33 | ) 34 | public class MapModCompanion { 35 | private static final int BSTATS_ID = 17977; 36 | 37 | private final List> factories = List.of( 38 | new MessageHandler.Factory<>( 39 | "world_id.modern", 40 | WORLD_ID, 41 | PrefixedId.Deserializer.instance(), 42 | PrefixedId.Serializer.instance() 43 | ), 44 | new MessageHandler.Factory<>( 45 | "world_id.legacy", 46 | WORLD_ID_LEGACY, 47 | PrefixedId.Deserializer.instance(), 48 | PrefixedId.Serializer.instance() 49 | ), 50 | new MessageHandler.Factory<>( 51 | "xaero.mini_map", 52 | XAERO_MINIMAP, 53 | LevelMapProperties.Deserializer.instance(), 54 | LevelMapProperties.Serializer.instance() 55 | ), 56 | new MessageHandler.Factory<>( 57 | "xaero.world_map", 58 | XAERO_WORLDMAP, 59 | LevelMapProperties.Deserializer.instance(), 60 | LevelMapProperties.Serializer.instance() 61 | ) 62 | ); 63 | 64 | private final ProxyServer server; 65 | private final Logger logger; 66 | private final Path dataDirectory; 67 | private final Metrics.Factory metricsFactory; 68 | 69 | private final IdLookup converter = new IdLookup.ConfigBased((path, def) -> 70 | getConfig().getLong(path, (long) def).intValue() 71 | ); 72 | 73 | private ScheduledExecutorService fileChangeWatchdogScheduler; 74 | private Toml config; 75 | private List handlers; 76 | private FileChangeWatchdog fileChangeWatchdog; 77 | 78 | @Inject 79 | public MapModCompanion(ProxyServer server, Logger logger, 80 | @DataDirectory Path dataDirectory, 81 | Metrics.Factory metricsFactory 82 | ) { 83 | this.server = server; 84 | this.logger = logger; 85 | this.dataDirectory = dataDirectory; 86 | this.metricsFactory = metricsFactory; 87 | } 88 | 89 | public Toml getConfig() { 90 | return this.config; 91 | } 92 | 93 | public ProxyServer getServer() { 94 | return server; 95 | } 96 | 97 | public IdLookup getConverter() { 98 | return converter; 99 | } 100 | 101 | @Subscribe 102 | public void onProxyInitialization(ProxyInitializeEvent event) { 103 | fileChangeWatchdogScheduler = FileChangeWatchdog.createScheduler(ofSlf4j(logger)); 104 | metricsFactory.make(this, BSTATS_ID); 105 | load(); 106 | } 107 | 108 | @Subscribe 109 | public void onProxyShutdown(ProxyShutdownEvent event) { 110 | fileChangeWatchdogScheduler.shutdown(); 111 | unload(); 112 | } 113 | 114 | private void load() { 115 | logger.debug("Loading"); 116 | 117 | boolean reload = config != null; 118 | try { 119 | this.config = this.reloadConfig(); 120 | } catch (IOException e) { 121 | throw new RuntimeException("error loading config file", e); 122 | } 123 | logger.info("Configuration has been " + (reload ? "reloaded" : "loaded")); 124 | 125 | handlers = Handler.initialize(ofSlf4j(logger), this, factories); 126 | 127 | fileChangeWatchdog = new FileChangeWatchdog( 128 | ofSlf4j(LoggerFactory.getLogger(FileChangeWatchdog.class)), 129 | fileChangeWatchdogScheduler, 130 | getConfigFile(), 131 | this::reload 132 | ); 133 | fileChangeWatchdog.start(); 134 | } 135 | 136 | private void unload() { 137 | logger.debug("Unloading"); 138 | fileChangeWatchdog.cleanUp(); 139 | Handler.cleanUp(ofSlf4j(logger), handlers); 140 | handlers = null; 141 | } 142 | 143 | private void reload() { 144 | unload(); 145 | load(); 146 | } 147 | 148 | private Toml reloadConfig() throws IOException { 149 | logger.debug("Creating new config file"); 150 | var configFile = getConfigFile(); 151 | if (!Files.exists(configFile)) { 152 | Files.createDirectories(dataDirectory); 153 | try (InputStream in = getClass().getResourceAsStream(CONFIG_PATH); 154 | OutputStream out = Files.newOutputStream(configFile) 155 | ) { 156 | Objects.requireNonNull(in, "missing " + CONFIG_PATH).transferTo(out); 157 | } 158 | } 159 | var config = new Toml(); 160 | try { 161 | config.read(configFile.toFile()); 162 | } catch (RuntimeException e) { 163 | throw new IOException(e); 164 | } 165 | return config; 166 | } 167 | 168 | private Path getConfigFile() { 169 | return dataDirectory.resolve("config.toml"); 170 | } 171 | 172 | private static final String CONFIG_PATH = "/config_velocity.toml"; 173 | 174 | private static ILogger ofSlf4j(Logger logger) { 175 | return new ILogger() { 176 | @Override 177 | public void fine(String message) { 178 | logger.debug("{}", message); 179 | } 180 | 181 | @Override 182 | public void info(String message) { 183 | logger.info("{}", message); 184 | } 185 | 186 | @Override 187 | public void warn(String message, Throwable t) { 188 | logger.warn("{}", message, t); 189 | } 190 | 191 | @Override 192 | public void error(String message, Throwable t) { 193 | logger.error("{}", message, t); 194 | } 195 | }; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /spigot/src/main/java/com/turikhay/mc/mapmodcompanion/spigot/MapModCompanion.java: -------------------------------------------------------------------------------- 1 | package com.turikhay.mc.mapmodcompanion.spigot; 2 | 3 | import com.turikhay.mc.mapmodcompanion.*; 4 | import org.bstats.bukkit.Metrics; 5 | import org.bukkit.World; 6 | import org.bukkit.plugin.java.JavaPlugin; 7 | import org.bukkit.plugin.messaging.PluginMessageListener; 8 | 9 | import javax.annotation.Nullable; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | 16 | public class MapModCompanion extends JavaPlugin { 17 | private static final int BSTATS_ID = 16539; 18 | 19 | private final List> factories = Arrays.asList( 20 | new XaeroHandler.Factory( 21 | "xaero.mini_map", 22 | Channels.XAERO_MINIMAP_CHANNEL 23 | ), 24 | new XaeroHandler.Factory( 25 | "xaero.world_map", 26 | Channels.XAERO_WORLDMAP_CHANNEL 27 | ), 28 | new LevelIdHandler.Factory( 29 | "world_id.modern", 30 | Channels.WORLDID_CHANNEL, 31 | false 32 | ), 33 | new LevelIdHandler.Factory( 34 | "world_id.legacy", 35 | Channels.WORLDID_LEGACY_CHANNEL, 36 | true 37 | ) 38 | ); 39 | 40 | private VerboseLogger logger; 41 | private PluginScheduler scheduler; 42 | private ScheduledExecutorService fileChangeWatchdogScheduler; 43 | private IdRegistry registry; 44 | private @Nullable ProtocolLib protocolLib; 45 | private List handlers = Collections.emptyList(); 46 | private FileChangeWatchdog fileChangeWatchdog; 47 | 48 | public VerboseLogger getVerboseLogger() { 49 | return logger; 50 | } 51 | 52 | public IdRegistry getRegistry() { 53 | return registry; 54 | } 55 | 56 | public Optional getProtocolLib() { 57 | return Optional.ofNullable(protocolLib); 58 | } 59 | 60 | @Override 61 | public void onLoad() { 62 | logger = new VerboseLogger(getLogger()); 63 | } 64 | 65 | @Override 66 | public void onEnable() { 67 | scheduler = initScheduler(); 68 | fileChangeWatchdogScheduler = FileChangeWatchdog.createScheduler(ILogger.ofJava(logger)); 69 | new Metrics(this, BSTATS_ID); 70 | saveDefaultConfig(); 71 | scheduler.schedule(this::load); 72 | } 73 | 74 | @Override 75 | public void onDisable() { 76 | scheduler.schedule(this::unload); 77 | fileChangeWatchdogScheduler.shutdown(); 78 | scheduler.cleanUp(); 79 | } 80 | 81 | private void load() { 82 | logger.fine("Loading"); 83 | 84 | reloadConfig(); 85 | 86 | logger.setVerbose(getConfig().getBoolean("verbose", false)); 87 | logger.fine("Verbose logging enabled"); 88 | 89 | registry = initRegistry(); 90 | protocolLib = Handler.initialize(logger, this, new ProtocolLib.Factory()); 91 | handlers = Handler.initialize(logger, this, factories); 92 | fileChangeWatchdog = new FileChangeWatchdog( 93 | logger, 94 | fileChangeWatchdogScheduler, 95 | getDataFolder().toPath().resolve("config.yml"), 96 | () -> scheduler.schedule(this::reload) 97 | ); 98 | fileChangeWatchdog.start(); 99 | } 100 | 101 | private void unload() { 102 | logger.fine("Unloading"); 103 | Handler.cleanUp(logger, handlers); 104 | handlers = Collections.emptyList(); 105 | getProtocolLib().ifPresent(Handler::cleanUp); 106 | fileChangeWatchdog.cleanUp(); 107 | } 108 | 109 | private void reload() { 110 | unload(); 111 | load(); 112 | } 113 | 114 | private PluginScheduler initScheduler() { 115 | PluginScheduler selected; 116 | if (FoliaSupport.isFoliaServer()) { 117 | logger.info("Folia server support enabled"); 118 | selected = new SingleThreadScheduler(ILogger.ofJava(logger)); 119 | } else { 120 | selected = new BukkitScheduler(this); 121 | } 122 | logger.fine("Scheduler: " + selected); 123 | return selected; 124 | } 125 | 126 | private IdRegistry initRegistry() { 127 | World world = null; 128 | if (getConfig().getBoolean("preferDefaultWorld", true)) { 129 | world = detectDefaultWorld(); 130 | } 131 | IdRegistry registry; 132 | if (world == null) { 133 | logger.info("For every world plugin will now send their unique IDs"); 134 | registry = new IdRegistry.DynamicUUIDRegistry(); 135 | } else { 136 | int id = world.getUID().hashCode(); 137 | registry = new IdRegistry.ConstantRegistry(id); 138 | } 139 | registry = new IdRegistry.ConvertingRegistry( 140 | logger, 141 | new IdLookup.ConfigBased((path, def) -> getConfig().getInt(path, def)), 142 | registry 143 | ); 144 | return new IdRegistry.CacheableRegistry(registry); 145 | } 146 | 147 | private @Nullable World detectDefaultWorld() { 148 | List worlds = getServer().getWorlds(); 149 | if (worlds.isEmpty()) { 150 | throw new RuntimeException("world list is empty"); 151 | } 152 | World defaultWorld = null; 153 | for (World world : worlds) { 154 | World.Environment env = world.getEnvironment(); 155 | if (env == World.Environment.NORMAL) { 156 | if (defaultWorld != null) { 157 | // Non-default server configuration 158 | logger.info("Unexpected world: " + world); 159 | return null; 160 | } 161 | defaultWorld = world; 162 | } 163 | } 164 | if (defaultWorld == null) { 165 | logger.info("Default world not detected"); 166 | return null; 167 | } 168 | logger.fine("Selected default world: " + defaultWorld + " (" + defaultWorld.getUID() + ")"); 169 | return defaultWorld; 170 | } 171 | 172 | void registerOutgoingChannel(String channelName) throws InitializationException { 173 | logger.fine("Registering outgoing plugin channel: " + channelName); 174 | try { 175 | getServer().getMessenger().registerOutgoingPluginChannel(this, channelName); 176 | } catch (Exception e) { 177 | throw new InitializationException("couldn't register outgoing plugin channel: " + channelName, e); 178 | } 179 | } 180 | 181 | void unregisterOutgoingChannel(String channelName) { 182 | logger.fine("Unregistering outgoing plugin channel: " + channelName); 183 | getServer().getMessenger().unregisterOutgoingPluginChannel(this, channelName); 184 | } 185 | 186 | void registerIncomingChannel(String channelName, boolean legacyChannel, PluginMessageListener listener) throws InitializationException { 187 | logger.fine("Registering incoming plugin channel: " + channelName); 188 | try { 189 | getServer().getMessenger().registerIncomingPluginChannel(this, channelName, listener); 190 | } catch (Exception e) { 191 | String message = "couldn't register incoming plugin channel: " + channelName; 192 | if (legacyChannel) { 193 | message += " (can be safely ignored on 1.13+)"; 194 | } 195 | throw new InitializationException(message, e); 196 | } 197 | } 198 | 199 | void registerIncomingChannel(String channelName, PluginMessageListener listener) throws InitializationException { 200 | registerIncomingChannel(channelName, false, listener); 201 | } 202 | 203 | void unregisterIncomingChannel(String channelName, PluginMessageListener listener) { 204 | logger.fine("Unregistering incoming plugin channel: " + channelName); 205 | getServer().getMessenger().unregisterIncomingPluginChannel(this, channelName, listener); 206 | } 207 | 208 | void checkEnabled(String configPath) throws InitializationException { 209 | if (!getConfig().getBoolean(configPath + ".enabled", true)) { 210 | throw new InitializationException("disabled in the config"); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /tests_e2e/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from logging import DEBUG, INFO, basicConfig, getLogger 4 | from pathlib import Path 5 | from signal import SIGINT 6 | from sys import argv, stdout 7 | from os import environ, makedirs, path 8 | from subprocess import PIPE, STDOUT, run, Popen 9 | from shutil import copytree, rmtree, copyfile 10 | import requests 11 | from yaml import load as yaml_load, dump as yaml_dump, Loader as YamlLoader, Dumper as YamlDumper 12 | from toml import load as toml_load, dump as toml_dump 13 | 14 | logger = getLogger("tests_e2e") 15 | 16 | PARENT_DIR = Path(path.realpath(__file__)).parent 17 | 18 | SERVER_OVERRIDES = { 19 | 'red': 42, 20 | 'blue': 2000, 21 | } 22 | 23 | PROXY_OVERRIDES = { 24 | '42': 1337, 25 | '2000': 3000, 26 | } 27 | 28 | JAVA_DEBUG = { 29 | 'proxy': 9010, 30 | 'red': 9011, 31 | 'blue': 9012, 32 | } 33 | 34 | VERSIONS = { 35 | '1.8.9': { 36 | 'server': '1.8.8', 37 | 'java': 8, 38 | 'world': '1.8.9', 39 | }, 40 | **( 41 | dict(( 42 | version, 43 | { 44 | 'java': 8, 45 | 'world': '1.8.9', 46 | }, 47 | ) for version in ( 48 | '1.9.4', 49 | '1.10.2', 50 | '1.11.2', 51 | '1.12.2', 52 | )) 53 | ), 54 | **( 55 | dict(( 56 | version, 57 | { 58 | 'java': 8, 59 | 'protocollib': True, 60 | 'world': '1.13.2', 61 | }, 62 | ) for version in ( 63 | '1.13.2', 64 | '1.14.4', 65 | '1.15.2', 66 | '1.16.1', 67 | '1.16.2', 68 | '1.16.3', 69 | )) 70 | ), 71 | **( 72 | dict(( 73 | version, 74 | { 75 | 'java': 8, 76 | 'world': '1.13.2', 77 | }, 78 | ) for version in ( 79 | '1.16.4', 80 | '1.16.5', 81 | )) 82 | ), 83 | **( 84 | dict((version, { 85 | 'java': 17, 86 | }) for version in ( 87 | '1.17.1', 88 | '1.18.2', 89 | '1.19.3', 90 | '1.19.4', 91 | '1.20.1', 92 | '1.20.2', 93 | '1.20.3', 94 | )) 95 | ), 96 | **( 97 | dict(( 98 | version, 99 | { 100 | 'java': 17, 101 | 'folia': True, 102 | }, 103 | ) for version in ( 104 | '1.20.4', 105 | )) 106 | ), 107 | **( 108 | dict(( 109 | version, 110 | { 111 | 'java': 21, 112 | 'folia': True, 113 | }, 114 | ) for version in ( 115 | '1.20.6', 116 | '1.21.4', 117 | )) 118 | ), 119 | **( 120 | dict(( 121 | version, 122 | { 123 | 'java': 21, 124 | }, 125 | ) for version in ( 126 | '1.21.1', 127 | '1.21.3', 128 | '1.21.6', 129 | )) 130 | ), 131 | **( 132 | dict(( 133 | version, 134 | { 135 | 'java': 21, 136 | 'bot': False, 137 | 'paper_channel': 'experimental', 138 | }, 139 | ) for version in ( 140 | '1.21.9', 141 | )) 142 | ), 143 | } 144 | 145 | 146 | def gradle_build(): 147 | logger.debug("Running gradle build") 148 | run( 149 | ['./gradlew', 'build'], 150 | check=True, 151 | cwd=str(PARENT_DIR.parent), 152 | ) 153 | 154 | 155 | def docker(l: list[str], **kwargs): 156 | logger.debug(f"Running docker with: {l}") 157 | p = Popen( 158 | ['docker', *l], 159 | cwd=str(PARENT_DIR), 160 | **kwargs, 161 | ) 162 | p._sigint_wait_secs = 60.0 163 | return p 164 | 165 | 166 | def copy_clean(_from: Path, _to: Path): 167 | logger.debug(f"Copying {_from} -> {_to}") 168 | if _to.is_dir(): 169 | rmtree(_to) 170 | copytree(_from, _to, symlinks=True) 171 | 172 | 173 | def files_dir_of(entity: str): 174 | return test_env_dir / f"{entity}" 175 | 176 | 177 | def copy_plugin(into: Path): 178 | copyfile( 179 | PARENT_DIR.parent / "packages" / "single" / 180 | "build" / "libs" / "MapModCompanion.jar", 181 | into / "MapModCompanion.jar", 182 | ) 183 | 184 | 185 | def copy_server_files(): 186 | logger.debug("Copying server files") 187 | for server_name in servers: 188 | _to = files_dir_of(server_name) 189 | copy_clean( 190 | PARENT_DIR / "server", 191 | _to, 192 | ) 193 | copy_plugin(_to / "plugins") 194 | mmc_config_path = _to / "plugins" / "MapModCompanion" / "config.yml" 195 | with open(mmc_config_path, "r") as f: 196 | config = yaml_load(f, Loader=YamlLoader) 197 | config["overrides"] = { 198 | 'world': SERVER_OVERRIDES[server_name], 199 | } 200 | logger.debug(f"Writing config {mmc_config_path}: {config}") 201 | with open(mmc_config_path, "w") as f: 202 | yaml_dump(config, f, Dumper=YamlDumper) 203 | 204 | 205 | def copy_proxy_files(): 206 | logger.debug("Copying proxy files") 207 | if proxy_type == "waterfall": 208 | _proxy_dir = "bungeecord" 209 | else: 210 | _proxy_dir = proxy_type 211 | _from = PARENT_DIR / "proxy" / _proxy_dir 212 | _to = files_dir_of("proxy") 213 | copy_clean( 214 | _from, 215 | _to, 216 | ) 217 | if proxy_type == "velocity": 218 | mmc_config_path = _to / "plugins" / "mapmodcompanion" / "config.toml" 219 | config = toml_load(mmc_config_path) 220 | def save(data): 221 | with open(mmc_config_path, "w") as f: 222 | toml_dump(data, f) 223 | else: 224 | mmc_config_path = _to / "plugins" / "MapModCompanion" / "config.yml" 225 | with open(mmc_config_path, "r") as f: 226 | config = yaml_load(f, Loader=YamlLoader) 227 | def save(data): 228 | with open(mmc_config_path, "w") as f: 229 | yaml_dump(data, f, Dumper=YamlDumper) 230 | config["overrides"] = PROXY_OVERRIDES 231 | logger.debug(f"Writing config {mmc_config_path}: {config}") 232 | save(config) 233 | copy_plugin(_to / "plugins") 234 | copyfile( 235 | PARENT_DIR / "proxy" / "Dockerfile", 236 | _to / "Dockerfile", 237 | ) 238 | 239 | 240 | if __name__ == "__main__": 241 | servers = [ 242 | 'red', 243 | 'blue', 244 | ] 245 | 246 | debug_level = int(environ.get("DEBUG")) if environ.get("DEBUG") else 0 247 | debug = debug_level > 0 248 | basicConfig( 249 | level=DEBUG if debug else INFO 250 | ) 251 | 252 | enable_blue = environ.get("BLUE") == "1" 253 | if not enable_blue: 254 | servers.remove('blue') 255 | 256 | java_debug = debug_level or environ.get("JAVA_DEBUG") 257 | if java_debug: 258 | logger.info("Java debugging enabled.") 259 | logger.info("Use 127.0.0.1:9010 for proxy") 260 | logger.info("Use 127.0.0.1:9011 for red server") 261 | if enable_blue: 262 | logger.info("Use 127.0.0.1:9011 for blue server") 263 | 264 | server_type = environ.get("SERVER_TYPE") 265 | if not server_type: 266 | server_type = "paper" 267 | 268 | proxy_type, client_version, action = argv[1:] 269 | 270 | version_info = VERSIONS[client_version] 271 | 272 | if server_type == 'folia' and ("folia" not in version_info or not version_info["folia"]): 273 | logger.info(f"Skipping: Folia is not supported on this version ({client_version})") 274 | exit(0) 275 | 276 | test_name_suffix = "" 277 | if server_type == 'folia': 278 | test_name_suffix += "folia_" 279 | test_name_suffix += f"{proxy_type}_{client_version}" 280 | 281 | test_name = f"mmc_test_{test_name_suffix}" 282 | test_env_dir = PARENT_DIR / "test_env" / test_name 283 | makedirs(test_env_dir, exist_ok=True) 284 | 285 | if debug_level: 286 | gradle_build() 287 | 288 | copy_server_files() 289 | copy_proxy_files() 290 | 291 | docker_compose_file_contents = { 292 | 'services': { 293 | } 294 | } 295 | 296 | if "bot" not in version_info or version_info["bot"] == True: 297 | bot_container = test_name 298 | bot_desc = { 299 | 'container_name': test_name, 300 | 'build': { 301 | 'context': str(PARENT_DIR / "bot"), 302 | 'args': [ 303 | 'DEBUG=minecraft-protocol' 304 | ] if debug_level > 1 else [ 305 | ] 306 | }, 307 | 'environment': [ 308 | 'BOT_HOST=proxy', 309 | f'BOT_VERSION={client_version}', 310 | ], 311 | 'depends_on': [ 312 | 'proxy' 313 | ], 314 | } 315 | docker_compose_file_contents["services"]["bot"] = bot_desc 316 | else: 317 | logger.warning(f"Bot doesn't support {client_version}") 318 | bot_container = None 319 | assert action not in ("test",) 320 | 321 | if "server" in version_info: 322 | server_version = version_info["server"] 323 | else: 324 | server_version = client_version 325 | 326 | assert "java" in version_info, f"java version for {server_version} is not defined" 327 | server_java_version = version_info["java"] 328 | 329 | if "world" in version_info: 330 | world_version = version_info["world"] 331 | else: 332 | world_version = "1.17.1" 333 | 334 | if server_type in ("folia"): 335 | paper_channel = "experimental" 336 | elif "paper_channel" in version_info: 337 | paper_channel = version_info["paper_channel"] 338 | else: 339 | paper_channel = None 340 | 341 | logger.info(f"Server type: {server_type}") 342 | 343 | for server_name in servers: 344 | server_desc = { 345 | 'build': { 346 | 'context': server_name, 347 | 'args': [ 348 | f'TAG=java{server_java_version}' 349 | ], 350 | 'tags': [ 351 | f'mmc-e2e-server-{server_name}:java{server_java_version}' 352 | ], 353 | }, 354 | 'environment': [ 355 | f'VERSION={server_version}', 356 | f'TYPE={server_type.upper()}', 357 | *([ 358 | f'PAPER_CHANNEL={paper_channel}', 359 | ] if paper_channel else []), 360 | ], 361 | 'ports': [ 362 | ] 363 | } 364 | if java_debug: 365 | server_desc["ports"] += [ 366 | f'{JAVA_DEBUG[server_name]}:9001' 367 | ] 368 | server_desc["environment"] += [ 369 | 'JVM_XX_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=' + 370 | '*:9001' if server_java_version > 8 else '9001' 371 | ] 372 | docker_compose_file_contents["services"][server_name] = server_desc 373 | if "protocollib" in version_info and version_info["protocollib"] == True: 374 | protocollib_source_file = test_env_dir / "ProtocolLib.jar" 375 | if not protocollib_source_file.is_file(): 376 | logger.info("Downloading ProtocolLib") 377 | data = requests.get("https://github.com/dmulloy2/ProtocolLib/releases/download/4.8.0/ProtocolLib.jar").content 378 | try: 379 | with open(protocollib_source_file, "wb") as f: 380 | f.write(data) 381 | except Exception as e: 382 | protocollib_source_file.unlink(missing_ok=True) 383 | raise e 384 | logger.info("Copying ProtocolLib") 385 | copyfile( 386 | protocollib_source_file, 387 | files_dir_of(server_name) / "plugins" / "ProtocolLib.jar", 388 | ) 389 | world_dir = files_dir_of(server_name) / "world" 390 | makedirs(world_dir, exist_ok=True) 391 | copyfile( 392 | PARENT_DIR / "saves" / world_version / f"{server_name}.dat", 393 | world_dir / "level.dat" 394 | ) 395 | 396 | proxy_desc = { 397 | 'build': { 398 | 'context': 'proxy', 399 | }, 400 | 'depends_on': [ 401 | *servers, 402 | ], 403 | 'environment': [ 404 | f'TYPE={proxy_type}', 405 | ], 406 | 'ports': [ 407 | ], 408 | } 409 | if debug_level: 410 | proxy_desc["ports"] += [ 411 | '25565:25565', 412 | ] 413 | if java_debug: 414 | proxy_desc["ports"] += [ 415 | f'{JAVA_DEBUG["proxy"]}:9001' 416 | ] 417 | proxy_desc["environment"] += [ 418 | 'JVM_XX_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:9001' 419 | ] 420 | 421 | if proxy_type in ("waterfall", "bungeecord"): 422 | bungee_config_path = test_env_dir / "proxy" / "config.yml" 423 | with open(bungee_config_path, "r") as f: 424 | bungee_config = yaml_load(f, Loader=YamlLoader) 425 | bungee_config["servers"] = dict( 426 | (server_name, { 427 | 'address': f'{server_name}:25565' 428 | }) for server_name in servers 429 | ) 430 | with open(bungee_config_path, "w") as f: 431 | yaml_dump(bungee_config, f, Dumper=YamlDumper) 432 | 433 | if proxy_type in ("velocity"): 434 | velocity_config_path = test_env_dir / "proxy" / "velocity.toml" 435 | velocity_config = toml_load(velocity_config_path) 436 | velocity_config["servers"] = { 437 | **dict( 438 | ( 439 | server_name, 440 | f"{server_name}:25565" 441 | ) for server_name in servers 442 | ), 443 | "try": [ 444 | "red", 445 | ], 446 | } 447 | with open(velocity_config_path, "w") as f: 448 | toml_dump(velocity_config, f) 449 | 450 | docker_compose_file_contents["services"]["proxy"] = proxy_desc 451 | 452 | docker_compose_path = test_env_dir / "docker-compose.yml" 453 | 454 | logger.debug( 455 | f"Writing {docker_compose_path}: {docker_compose_file_contents}") 456 | with open(docker_compose_path, "w") as f: 457 | yaml_dump(docker_compose_file_contents, f, Dumper=YamlDumper) 458 | 459 | auto = bot_container and action in ("test") 460 | 461 | compose = [ 462 | 'compose', 463 | '-f', 464 | str(docker_compose_path), 465 | ] 466 | 467 | if action == "build": 468 | exit_code = docker([ 469 | *compose, 470 | 'build', 471 | '--no-cache', 472 | ]).wait() 473 | exit(exit_code) 474 | 475 | docker_proc = docker([ 476 | *compose, 477 | 'up', 478 | '--force-recreate', 479 | '--build', 480 | *(['-V'] if debug_level else []), 481 | *(['--detach'] if auto else []), 482 | ]) 483 | 484 | try: 485 | exit_code = docker_proc.wait() 486 | except KeyboardInterrupt: 487 | docker_proc.send_signal(SIGINT) 488 | exit(1) 489 | 490 | if auto: 491 | logger.info(f"Waiting for {bot_container}") 492 | docker_logs = docker( 493 | [ 494 | *compose, 495 | 'logs', 496 | *([ 497 | '-f', 498 | ] if debug else [ 499 | '-f', 500 | 'bot', 501 | *servers, 502 | ]) 503 | ], 504 | ) 505 | docker_wait = docker( 506 | [ 507 | 'wait', 508 | bot_container, 509 | ], 510 | stdout=PIPE, 511 | stderr=STDOUT, 512 | text=True 513 | ) 514 | 515 | try: 516 | docker_wait.wait() 517 | except KeyboardInterrupt: 518 | docker_logs.kill() 519 | 520 | docker([ 521 | *compose, 522 | 'down', 523 | ]).wait() 524 | 525 | if docker_wait.returncode != 0 or int(docker_wait.stdout.read()) != 0: 526 | logger.error("Failed") 527 | exit(1) 528 | 529 | logger.info("OK") 530 | --------------------------------------------------------------------------------