├── .github ├── img │ ├── end.png │ ├── icon128.png │ ├── icon512.png │ └── persistence-nbt.png ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── build.yml │ ├── publish.yml │ └── release.yml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── settings.gradle.kts ├── paper └── src │ └── main │ ├── java │ └── com │ │ └── jtprince │ │ └── coordinateoffset │ │ └── paper │ │ ├── lib │ │ └── org │ │ │ └── geysermc │ │ │ └── hurricane │ │ │ ├── README-HURRICANE.md │ │ │ └── NMSReflection.java │ │ ├── adapter │ │ ├── PaperLocation.java │ │ ├── PaperOffsetPlayer.java │ │ ├── PaperWorld.java │ │ └── PaperAdapter.java │ │ ├── CoordinateOffsetPaperPlugin.java │ │ └── MetricsWrapper.java │ └── resources │ └── paper-plugin.yml ├── gradle.properties ├── .gitattributes ├── api ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── jtprince │ │ │ └── coordinateoffset │ │ │ ├── command │ │ │ ├── OffsetCommand.java │ │ │ ├── OffsetCommandSender.java │ │ │ └── OffsetSetCommand.java │ │ │ ├── provider │ │ │ ├── OffsetProviderConfig.java │ │ │ └── OffsetProviderContext.java │ │ │ ├── adapter │ │ │ ├── OffsetPlayer.java │ │ │ ├── OffsetWorld.java │ │ │ └── OffsetLocation.java │ │ │ ├── OffsetChange.java │ │ │ ├── config │ │ │ ├── CoordinateOffsetProviderConfig.java │ │ │ ├── CoordinateOffsetConfig.java │ │ │ └── OffsetProviderOverrideConfig.java │ │ │ ├── api │ │ │ └── CoordinateOffset.java │ │ │ ├── OffsetData.java │ │ │ └── FixedOffset.java │ └── test │ │ └── java │ │ └── com │ │ └── jtprince │ │ └── coordinateoffset │ │ └── TestOffset.java └── build.gradle.kts ├── scripts ├── configure_server.sh └── setup_server.sh ├── core ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── jtprince │ │ │ └── coordinateoffset │ │ │ ├── command │ │ │ ├── OffsetReloadCommand.java │ │ │ ├── OffsetRegenerateCommand.java │ │ │ └── OffsetQueryCommand.java │ │ │ ├── adapter │ │ │ ├── OffsetSwapper.java │ │ │ ├── OffsetPersistenceAdapter.java │ │ │ └── CoordinateOffsetAdapter.java │ │ │ ├── provider │ │ │ ├── CoreOffsetProvider.java │ │ │ ├── util │ │ │ │ └── ProviderOffsetStore.java │ │ │ ├── DefaultOffsetProviders.java │ │ │ ├── ConstantOffsetProvider.java │ │ │ ├── ZeroAtLocationOffsetProvider.java │ │ │ └── RandomOffsetProvider.java │ │ │ ├── offsetter │ │ │ ├── server │ │ │ │ ├── OffsetterServerEffect.java │ │ │ │ ├── OffsetterServerChunkData.java │ │ │ │ ├── OffsetterServerSpawnEntity.java │ │ │ │ ├── OffsetterServerVehicleMove.java │ │ │ │ ├── OffsetterServerFacePlayer.java │ │ │ │ ├── OffsetterServerBlockAction.java │ │ │ │ ├── OffsetterServerBlockChange.java │ │ │ │ ├── OffsetterServerSpawnPosition.java │ │ │ │ ├── OffsetterServerEntityTeleport.java │ │ │ │ ├── OffsetterServerOpenSignEditor.java │ │ │ │ ├── OffsetterServerSpawnPlayer.java │ │ │ │ ├── OffsetterServerSpawnPainting.java │ │ │ │ ├── OffsetterServerMultiBlockChange.java │ │ │ │ ├── OffsetterServerUpdateLight.java │ │ │ │ ├── OffsetterServerRespawn.java │ │ │ │ ├── OffsetterServerDebugSample.java │ │ │ │ ├── OffsetterServerJoinGame.java │ │ │ │ ├── OffsetterServerSoundEffect.java │ │ │ │ ├── OffsetterServerBlockBreakAnimation.java │ │ │ │ ├── OffsetterServerDebugEvent.java │ │ │ │ ├── OffsetterServerUnloadChunk.java │ │ │ │ ├── OffsetterServerEntityPositionSync.java │ │ │ │ ├── OffsetterServerSpawnLivingEntity.java │ │ │ │ ├── OffsetterServerNamedSoundEffect.java │ │ │ │ ├── OffsetterServerSpawnExperienceOrb.java │ │ │ │ ├── OffsetterServerDebugEntityValue.java │ │ │ │ ├── OffsetterServerUpdateViewPosition.java │ │ │ │ ├── OffsetterServerGameTestHighlightPos.java │ │ │ │ ├── OffsetterServerSetSlot.java │ │ │ │ ├── OffsetterServerExplosion.java │ │ │ │ ├── OffsetterServerDebugBlockValue.java │ │ │ │ ├── OffsetterServerDebugChunkValue.java │ │ │ │ ├── OffsetterServerAcknowledgePlayerDigging.java │ │ │ │ ├── OffsetterServerSetCursorItem.java │ │ │ │ ├── OffsetterServerMoveMinecart.java │ │ │ │ ├── OffsetterServerSetPlayerInventory.java │ │ │ │ ├── OffsetterServerPlayerPositionAndLook.java │ │ │ │ ├── OffsetterServerEntityEquipment.java │ │ │ │ ├── OffsetterServerWindowItems.java │ │ │ │ ├── OffsetterServerBlockEntityData.java │ │ │ │ ├── OffsetterServerWaypoint.java │ │ │ │ ├── OffsetterServerEntityMetadata.java │ │ │ │ └── OffsetterServerParticle.java │ │ │ ├── client │ │ │ │ ├── OffsetterClientVehicleMove.java │ │ │ │ ├── OffsetterClientUpdateSign.java │ │ │ │ ├── OffsetterClientSetTestBlock.java │ │ │ │ ├── OffsetterClientPickItemFromBlock.java │ │ │ │ ├── OffsetterClientUpdateJigsawBlock.java │ │ │ │ ├── OffsetterClientSetStructureBlock.java │ │ │ │ ├── OffsetterClientGenerateStructure.java │ │ │ │ ├── OffsetterClientUpdateCommandBlock.java │ │ │ │ ├── OffsetterClientPlayerBlockPlacement.java │ │ │ │ ├── OffsetterClientPlayerPosition.java │ │ │ │ ├── OffsetterClientTestInstanceBlockAction.java │ │ │ │ ├── OffsetterClientCreativeInventoryAction.java │ │ │ │ ├── OffsetterClientPlayerDigging.java │ │ │ │ └── OffsetterClientClickWindow.java │ │ │ ├── wrapper │ │ │ │ ├── WrapperPlayServerEffect.java │ │ │ │ └── WrapperPlayServerNamedSoundEffect.java │ │ │ └── OffsettedColumn.java │ │ │ ├── config │ │ │ ├── OffsetProviderConfigImpl.java │ │ │ ├── ConfigVersion.java │ │ │ ├── OffsetMultipleConfig.java │ │ │ ├── OffsetProviderOverrideConfigImpl.java │ │ │ ├── OffsetProviderListSerializer.java │ │ │ ├── CoordinateOffsetConfigBase.java │ │ │ └── CoordinateOffsetConfigFull.java │ │ │ ├── OffsetProviderClassRegistry.java │ │ │ ├── CoordinateOffsetPermission.java │ │ │ └── api │ │ │ └── CoordinateOffsetAPIImpl.java │ └── test │ │ └── java │ │ └── com │ │ └── jtprince │ │ └── coordinateoffset │ │ └── config │ │ └── TestConfigVersion.java └── build.gradle.kts ├── CONTRIBUTING.md ├── example-api-plugin ├── build.gradle.kts └── src │ └── main │ └── resources │ ├── plugin.yml │ └── paper-plugin.yml ├── .gitignore ├── gradlew.bat └── docs └── Updating.md /.github/img/end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuaprince/CoordinateOffset/HEAD/.github/img/end.png -------------------------------------------------------------------------------- /.github/img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuaprince/CoordinateOffset/HEAD/.github/img/icon128.png -------------------------------------------------------------------------------- /.github/img/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuaprince/CoordinateOffset/HEAD/.github/img/icon512.png -------------------------------------------------------------------------------- /.github/img/persistence-nbt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuaprince/CoordinateOffset/HEAD/.github/img/persistence-nbt.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuaprince/CoordinateOffset/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "CoordinateOffset" 2 | 3 | include("api") 4 | include("core") 5 | include("paper") 6 | 7 | include("example-api-plugin") 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question not related to a bug or new feature 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/lib/org/geysermc/hurricane/README-HURRICANE.md: -------------------------------------------------------------------------------- 1 | This library is used from https://github.com/GeyserMC/Hurricane as of commit 2 | `de9ade8`. It is used unmodified. 3 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # This file was generated by the Gradle 'init' task. 2 | # https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties 3 | 4 | org.gradle.configuration-cache=true 5 | org.gradle.parallel=true 6 | org.gradle.caching=true 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | # Binary files should be left untouched 11 | *.jar binary 12 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/command/OffsetCommand.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.command; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | @NullMarked 6 | public interface OffsetCommand { 7 | /** 8 | * Get who sent this command. Can be used to respond to the command. 9 | */ 10 | OffsetCommandSender getCommandSender(); 11 | } 12 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/provider/OffsetProviderConfig.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | import java.util.SequencedMap; 6 | 7 | @NullMarked 8 | public interface OffsetProviderConfig { 9 | String getUserDefinedProviderName(); 10 | 11 | String getProviderClassName(); 12 | 13 | SequencedMap getConfigSection(); 14 | } 15 | -------------------------------------------------------------------------------- /paper/src/main/resources/paper-plugin.yml: -------------------------------------------------------------------------------- 1 | name: CoordinateOffset 2 | version: "${version}" 3 | main: com.jtprince.coordinateoffset.paper.CoordinateOffsetPaperPlugin 4 | api-version: "${apiVersion}" 5 | authors: [ joshuaprince ] 6 | contributors: [ Cavallium ] 7 | website: https://github.com/joshuaprince/CoordinateOffset 8 | description: Configurably obfuscate players' coordinates. 9 | dependencies: 10 | server: 11 | packetevents: 12 | load: BEFORE 13 | required: true 14 | GrimAC: 15 | load: AFTER 16 | required: false 17 | -------------------------------------------------------------------------------- /scripts/configure_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | if [[ $# -lt 1 ]]; then 5 | echo "Usage: $0 " 6 | echo "Example: $0 ./run-1.21.6" 7 | exit 1 8 | fi 9 | 10 | cd "$1" 11 | 12 | # Requires https://github.com/mikefarah/yq/ (go-yq in Arch repos) 13 | 14 | set -v 15 | # Configure server.properties 16 | yq -i '.enable-command-block=true' server.properties 17 | 18 | # Configure CoordinateOffset 19 | yq -i '.bypassByPermission=false' plugins/CoordinateOffset/config.yml 20 | yq -i '.verbose=true' plugins/CoordinateOffset/config.yml 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/command/OffsetReloadCommand.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.command; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | @NullMarked 6 | public class OffsetReloadCommand implements OffsetCommand { 7 | private final OffsetCommandSender commandSender; 8 | 9 | public OffsetReloadCommand(OffsetCommandSender commandSender) { 10 | this.commandSender = commandSender; 11 | } 12 | 13 | @Override 14 | public OffsetCommandSender getCommandSender() { 15 | return commandSender; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Pull requests are welcome! Try to maintain the existing code style, but I don't have much else in the way of guidelines. 5 | Please reach out on Discord to @jtchips if you'd like to discuss changes you want to make. 6 | 7 | I'm trying to document as much as I can in the `docs` folder. 8 | 9 | Updates 10 | ------- 11 | I hope to make this plugin as easy as possible to update to future Minecraft versions in case I no longer want to 12 | maintain it. Please see [Updating.md](docs/Updating.md) where I have outlined what I roughly expect to be the standard 13 | updating process. 14 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compileOnly(libs.adventure.api) 3 | compileOnly(libs.adventure.minimessage) 4 | compileOnly(libs.geyser.api) 5 | compileOnly(libs.jspecify) 6 | compileOnly(libs.packetevents.api) 7 | 8 | implementation(project(":api")) 9 | implementation(libs.configlib.paper) // TODO: Minor leak of Paper platform into core 10 | implementation(libs.netty.buffer) 11 | 12 | testCompileOnly(libs.jspecify) 13 | testImplementation(libs.test.junit.jupiter) 14 | testRuntimeOnly(libs.test.junit.platform) 15 | } 16 | 17 | tasks.test { 18 | useJUnitPlatform() 19 | } 20 | -------------------------------------------------------------------------------- /example-api-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | group = "com.jtprince.coordinateoffset.example" 2 | version = "0.0.1" 3 | 4 | dependencies { 5 | /* 6 | * Use the following dependencies in your own plugin: 7 | * shadow("io.papermc.paper:paper-api:-R0.1-SNAPSHOT") 8 | * shadow("com.jtprince.coordinateoffset:coordinateoffset-api:") 9 | * CoordinateOffset API version examples: 10 | * - 6.1.1 11 | * - 6.1.2-SNAPSHOT 12 | */ 13 | compileOnly(libs.paper.api) 14 | compileOnly(project(":api")) 15 | } 16 | 17 | tasks { 18 | jar { 19 | archiveBaseName.set("CoordinateOffsetAPIExamplePlugin") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example-api-plugin/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | # You may depend on CoordinateOffset as a Bukkit plugin. Add CoordinateOffset in the "depend" list. 2 | name: ExampleCoordinateOffsetAPIPlugin 3 | version: 0.0.1 4 | main: com.jtprince.coordinateoffset.example.ExampleCoordinateOffsetAPIPlugin 5 | description: Example Coordinate Offset API Plugin 6 | author: joshuaprince 7 | website: https://github.com/joshuaprince/CoordinateOffset 8 | api-version: 1.21 9 | softdepend: 10 | - CoordinateOffset 11 | # Add to the "depend" list to disable your plugin if CoordinateOffset is not present. 12 | # If including as a soft dependency, be sure to handle the case where CoordinateOffset is not present. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v4 12 | - name: Setup Java 13 | uses: actions/setup-java@v4 14 | with: 15 | distribution: 'temurin' 16 | java-version: '21' 17 | - name: Setup Gradle 18 | uses: gradle/actions/setup-gradle@v5 19 | - name: Build and test 20 | run: ./gradlew build 21 | - name: Upload artifacts 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: CoordinateOffset-Package 25 | path: paper/build/libs/*.jar 26 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/adapter/OffsetPlayer.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.adapter; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | import java.util.Set; 6 | import java.util.UUID; 7 | 8 | /** 9 | * Adapter interface representing a player in a Minecraft server. 10 | */ 11 | @NullMarked 12 | public interface OffsetPlayer { 13 | UUID getUuid(); 14 | String getName(); 15 | boolean hasPermission(String permission); 16 | Set getAllPermissions(); 17 | OffsetLocation getLocation(); 18 | 19 | /** 20 | * Get the underlying platform-specific player object, for example a Bukkit Player. 21 | */ 22 | Object getPlatformPlayerObject(); 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/adapter/OffsetSwapper.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.adapter; 2 | 3 | import com.jtprince.coordinateoffset.OffsetHolder; 4 | 5 | public interface OffsetSwapper { 6 | /** 7 | * Forcibly swap a player's next offset into their current offset in-place, sending any packets necessary to 8 | * simulate a teleport. 9 | * 10 | *

This may be called after {@link OffsetHolder#generateNextOffset} to apply an offset change immediately.

11 | * 12 | *

This must only be called on the main server thread.

13 | * 14 | * @param player Player to swap the offset for. 15 | */ 16 | void forceOffsetSwap(OffsetPlayer player); 17 | } 18 | -------------------------------------------------------------------------------- /example-api-plugin/src/main/resources/paper-plugin.yml: -------------------------------------------------------------------------------- 1 | # You may depend on CoordinateOffset as a Paper plugin. Add CoordinateOffset as a BEFORE dependency. 2 | name: ExampleCoordinateOffsetAPIPlugin 3 | version: 0.0.1 4 | main: com.jtprince.coordinateoffset.example.ExampleCoordinateOffsetAPIPlugin 5 | api-version: "1.21" 6 | authors: [ joshuaprince ] 7 | website: https://github.com/joshuaprince/CoordinateOffset 8 | description: Example Coordinate Offset API Plugin 9 | dependencies: 10 | server: 11 | CoordinateOffset: 12 | load: BEFORE 13 | # Set "required" to true to disable your plugin if CoordinateOffset is not present. 14 | # If required is "false", be sure to handle the case where CoordinateOffset is not present. 15 | required: false 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Environment details** 14 | CoordinateOffset version: 15 | Output of `/version`: 16 | Output of `/plugins`: 17 | Upload `config.yml` to https://mclo.gs/ : 18 | 19 | **Log errors** 20 | ``` 21 | Paste logs here. Include any stack traces, exceptions, or error messages printed when the bug occurs. 22 | ``` 23 | **Steps to reproduce** 24 | Please give any additional details that might help us to recreate the bug in a test server. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/OffsetChange.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset; 2 | 3 | import org.jspecify.annotations.Nullable; 4 | 5 | /** 6 | * Container for all data associated with a player's offset changing. 7 | * 8 | *

This record is immutable and represents the final result of an offset change; i.e., it is created after the 9 | * new offset has already been determined.

10 | * 11 | * @param previousOffsetData The previous offset data for the player, or null if the player is joining the server. 12 | * @param newOffsetData The new offset data for the player. 13 | */ 14 | public record OffsetChange( 15 | @Nullable OffsetData previousOffsetData, 16 | OffsetData newOffsetData 17 | ) { 18 | public boolean offsetChanged() { 19 | return previousOffsetData == null || !previousOffsetData.offset().equals(newOffsetData.offset()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | 3 | on: 4 | push: 5 | branches: 6 | - ver/* 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v4 15 | - name: Setup Java 16 | uses: actions/setup-java@v4 17 | with: 18 | distribution: 'temurin' 19 | java-version: '21' 20 | - name: Setup Gradle 21 | uses: gradle/actions/setup-gradle@v5 22 | - name: Publish snapshot 23 | run: ./gradlew publishToMavenCentral 24 | env: 25 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.CENTRAL_TOKEN_USERNAME }} 26 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }} 28 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/provider/CoreOffsetProvider.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider; 2 | 3 | public abstract sealed class CoreOffsetProvider 4 | extends OffsetProvider 5 | permits ConstantOffsetProvider, PermissionOffsetProvider, RandomOffsetProvider, ZeroAtLocationOffsetProvider { 6 | 7 | public CoreOffsetProvider(String userDefinedProviderName) { 8 | super(userDefinedProviderName); 9 | } 10 | 11 | /** 12 | * Class name shown in the main pie chart on the bStats metrics page. 13 | * @return A String like "RandomOffsetProvider" 14 | */ 15 | public abstract String getMetricsClassName(); 16 | 17 | /** 18 | * Extra details shown when drilling down into a pie chart on the bStats metrics page. 19 | * @return Variable strings per offset provider. 20 | */ 21 | public abstract String getMetricsDetails(); 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/command/OffsetRegenerateCommand.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.command; 2 | 3 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 4 | import org.jspecify.annotations.NullMarked; 5 | 6 | import java.util.List; 7 | 8 | @NullMarked 9 | public class OffsetRegenerateCommand implements OffsetCommand { 10 | private final OffsetCommandSender commandSender; 11 | private final List targets; 12 | 13 | public OffsetRegenerateCommand(OffsetCommandSender commandSender, List targets) { 14 | this.commandSender = commandSender; 15 | this.targets = List.copyOf(targets); 16 | } 17 | 18 | @Override 19 | public OffsetCommandSender getCommandSender() { 20 | return commandSender; 21 | } 22 | 23 | public List getTargets() { 24 | return targets; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/config/CoordinateOffsetProviderConfig.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 4 | import org.jspecify.annotations.NullMarked; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * Configuration interface for Offset Providers in CoordinateOffset. 11 | * 12 | *

See {@link CoordinateOffsetConfig} for general configuration access.

13 | * 14 | *

See Configuration Guide 15 | * for information about how providers work.

16 | */ 17 | @NullMarked 18 | public interface CoordinateOffsetProviderConfig { 19 | OffsetProvider getDefaultOffsetProviderConfig(); 20 | List getOffsetProviderOverrides(); 21 | Map getAllOffsetProviderConfigs(); 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerEffect.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.jtprince.coordinateoffset.FixedOffset; 6 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 7 | import com.jtprince.coordinateoffset.offsetter.wrapper.WrapperPlayServerEffect; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerEffect extends PacketOffsetter { 12 | public OffsetterServerEffect() { 13 | super(WrapperPlayServerEffect.class, PacketType.Play.Server.EFFECT); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerEffect packet, FixedOffset offset, User user) { 18 | packet.setPosition(apply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/command/OffsetQueryCommand.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.command; 2 | 3 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 4 | import org.jspecify.annotations.NullMarked; 5 | 6 | @NullMarked 7 | public class OffsetQueryCommand implements OffsetCommand { 8 | private final OffsetCommandSender commandSender; 9 | private final OffsetPlayer target; 10 | private final boolean verbose; 11 | 12 | public OffsetQueryCommand(OffsetCommandSender commandSender, OffsetPlayer target, boolean verbose) { 13 | this.commandSender = commandSender; 14 | this.target = target; 15 | this.verbose = verbose; 16 | } 17 | 18 | @Override 19 | public OffsetCommandSender getCommandSender() { 20 | return commandSender; 21 | } 22 | 23 | public OffsetPlayer getTarget() { 24 | return target; 25 | } 26 | 27 | public boolean isVerbose() { 28 | return verbose; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/test/java/com/jtprince/coordinateoffset/config/TestConfigVersion.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.nio.file.Path; 7 | 8 | public class TestConfigVersion { 9 | @Test 10 | void testGetBackupPath() { 11 | Path p1 = Path.of("/abc/def/config.yml"); 12 | Assertions.assertEquals(Path.of("/abc/def/config.v10.old.yml"), ConfigVersion.getBackupPath(p1, 10)); 13 | 14 | Path p2 = Path.of("/abc/def/my.config.yml"); 15 | Assertions.assertEquals(Path.of("/abc/def/my.config.v1.old.yml"), ConfigVersion.getBackupPath(p2, 1)); 16 | 17 | Path p3 = Path.of("config.yml"); 18 | Assertions.assertEquals(Path.of("config.v999.old.yml"), ConfigVersion.getBackupPath(p3, 999)); 19 | 20 | Path p4 = Path.of("some.config.yml"); 21 | Assertions.assertEquals(Path.of("some.config.v0.old.yml"), ConfigVersion.getBackupPath(p4, 0)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerChunkData.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerChunkData extends PacketOffsetter { 12 | public OffsetterServerChunkData() { 13 | super(WrapperPlayServerChunkData.class, PacketType.Play.Server.CHUNK_DATA); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerChunkData packet, FixedOffset offset, User user) { 18 | packet.setColumn(applyColumn(packet.getColumn(), offset, user)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientVehicleMove.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientVehicleMove; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientVehicleMove extends PacketOffsetter { 12 | public OffsetterClientVehicleMove() { 13 | super(WrapperPlayClientVehicleMove.class, PacketType.Play.Client.VEHICLE_MOVE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientVehicleMove packet, FixedOffset offset, User user) { 18 | packet.setPosition(unapply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSpawnEntity.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerSpawnEntity extends PacketOffsetter { 12 | public OffsetterServerSpawnEntity() { 13 | super(WrapperPlayServerSpawnEntity.class, PacketType.Play.Server.SPAWN_ENTITY); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerSpawnEntity packet, FixedOffset offset, User user) { 18 | packet.setPosition(apply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerVehicleMove.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerVehicleMove; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerVehicleMove extends PacketOffsetter { 12 | public OffsetterServerVehicleMove() { 13 | super(WrapperPlayServerVehicleMove.class, PacketType.Play.Server.VEHICLE_MOVE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerVehicleMove packet, FixedOffset offset, User user) { 18 | packet.setPosition(apply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientUpdateSign.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientUpdateSign; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientUpdateSign extends PacketOffsetter { 12 | public OffsetterClientUpdateSign() { 13 | super(WrapperPlayClientUpdateSign.class, PacketType.Play.Client.UPDATE_SIGN); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientUpdateSign packet, FixedOffset offset, User user) { 18 | packet.setBlockPosition(unapply(packet.getBlockPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerFacePlayer.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerFacePlayer; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerFacePlayer extends PacketOffsetter { 12 | public OffsetterServerFacePlayer() { 13 | super(WrapperPlayServerFacePlayer.class, PacketType.Play.Server.FACE_PLAYER); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerFacePlayer packet, FixedOffset offset, User user) { 18 | packet.setTargetPosition(apply(packet.getTargetPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientSetTestBlock.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientSetTestBlock; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientSetTestBlock extends PacketOffsetter { 12 | public OffsetterClientSetTestBlock() { 13 | super(WrapperPlayClientSetTestBlock.class, PacketType.Play.Client.SET_TEST_BLOCK); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientSetTestBlock packet, FixedOffset offset, User user) { 18 | packet.setPosition(unapply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerBlockAction.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockAction; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerBlockAction extends PacketOffsetter { 12 | public OffsetterServerBlockAction() { 13 | super(WrapperPlayServerBlockAction.class, PacketType.Play.Server.BLOCK_ACTION); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerBlockAction packet, FixedOffset offset, User user) { 18 | packet.setBlockPosition(apply(packet.getBlockPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerBlockChange.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockChange; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerBlockChange extends PacketOffsetter { 12 | public OffsetterServerBlockChange() { 13 | super(WrapperPlayServerBlockChange.class, PacketType.Play.Server.BLOCK_CHANGE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerBlockChange packet, FixedOffset offset, User user) { 18 | packet.setBlockPosition(apply(packet.getBlockPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSpawnPosition.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnPosition; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerSpawnPosition extends PacketOffsetter { 12 | public OffsetterServerSpawnPosition() { 13 | super(WrapperPlayServerSpawnPosition.class, PacketType.Play.Server.SPAWN_POSITION); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerSpawnPosition packet, FixedOffset offset, User user) { 18 | packet.setPosition(apply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerEntityTeleport.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityTeleport; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerEntityTeleport extends PacketOffsetter { 12 | public OffsetterServerEntityTeleport() { 13 | super(WrapperPlayServerEntityTeleport.class, PacketType.Play.Server.ENTITY_TELEPORT); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerEntityTeleport packet, FixedOffset offset, User user) { 18 | packet.setPosition(apply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerOpenSignEditor.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenSignEditor; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerOpenSignEditor extends PacketOffsetter { 12 | public OffsetterServerOpenSignEditor() { 13 | super(WrapperPlayServerOpenSignEditor.class, PacketType.Play.Server.OPEN_SIGN_EDITOR); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerOpenSignEditor packet, FixedOffset offset, User user) { 18 | packet.setPosition(apply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSpawnPlayer.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnPlayer; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerSpawnPlayer extends PacketOffsetter { 12 | public OffsetterServerSpawnPlayer() { 13 | // Removed in 1.19.3 14 | super(WrapperPlayServerSpawnPlayer.class, PacketType.Play.Server.SPAWN_PLAYER); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerSpawnPlayer packet, FixedOffset offset, User user) { 19 | packet.setPosition(apply(packet.getPosition(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientPickItemFromBlock.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPickItemFromBlock; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientPickItemFromBlock extends PacketOffsetter { 12 | public OffsetterClientPickItemFromBlock() { 13 | super(WrapperPlayClientPickItemFromBlock.class, PacketType.Play.Client.PICK_ITEM_FROM_BLOCK); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientPickItemFromBlock packet, FixedOffset offset, User user) { 18 | packet.setBlockPos(unapply(packet.getBlockPos(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientUpdateJigsawBlock.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientUpdateJigsawBlock; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientUpdateJigsawBlock extends PacketOffsetter { 12 | public OffsetterClientUpdateJigsawBlock() { 13 | super(WrapperPlayClientUpdateJigsawBlock.class, PacketType.Play.Client.UPDATE_JIGSAW_BLOCK); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientUpdateJigsawBlock packet, FixedOffset offset, User user) { 18 | packet.setPosition(unapply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSpawnPainting.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnPainting; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerSpawnPainting extends PacketOffsetter { 12 | public OffsetterServerSpawnPainting() { 13 | // Removed in 1.19 14 | super(WrapperPlayServerSpawnPainting.class, PacketType.Play.Server.SPAWN_PAINTING); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerSpawnPainting packet, FixedOffset offset, User user) { 19 | packet.setPosition(apply(packet.getPosition(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientSetStructureBlock.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientSetStructureBlock; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientSetStructureBlock extends PacketOffsetter { 12 | public OffsetterClientSetStructureBlock() { 13 | super(WrapperPlayClientSetStructureBlock.class, PacketType.Play.Client.UPDATE_STRUCTURE_BLOCK); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientSetStructureBlock packet, FixedOffset offset, User user) { 18 | packet.setPosition(unapply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerMultiBlockChange.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerMultiBlockChange extends PacketOffsetter { 12 | public OffsetterServerMultiBlockChange() { 13 | super(WrapperPlayServerMultiBlockChange.class, PacketType.Play.Server.MULTI_BLOCK_CHANGE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerMultiBlockChange packet, FixedOffset offset, User user) { 18 | packet.setChunkPosition(applyChunk(packet.getChunkPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerUpdateLight.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateLight; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerUpdateLight extends PacketOffsetter { 12 | public OffsetterServerUpdateLight() { 13 | super(WrapperPlayServerUpdateLight.class, PacketType.Play.Server.UPDATE_LIGHT); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerUpdateLight packet, FixedOffset offset, User user) { 18 | packet.setX(applyChunkX(packet.getX(), offset)); 19 | packet.setZ(applyChunkZ(packet.getZ(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientGenerateStructure.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientGenerateStructure; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientGenerateStructure extends PacketOffsetter { 12 | public OffsetterClientGenerateStructure() { 13 | super(WrapperPlayClientGenerateStructure.class, PacketType.Play.Client.GENERATE_STRUCTURE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientGenerateStructure packet, FixedOffset offset, User user) { 18 | packet.setBlockPosition(unapply(packet.getBlockPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientUpdateCommandBlock.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientUpdateCommandBlock; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientUpdateCommandBlock extends PacketOffsetter { 12 | public OffsetterClientUpdateCommandBlock() { 13 | super(WrapperPlayClientUpdateCommandBlock.class, PacketType.Play.Client.UPDATE_COMMAND_BLOCK); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientUpdateCommandBlock packet, FixedOffset offset, User user) { 18 | packet.setPosition(unapply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerRespawn.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerRespawn; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerRespawn extends PacketOffsetter { 12 | public OffsetterServerRespawn() { 13 | super(WrapperPlayServerRespawn.class, PacketType.Play.Server.RESPAWN); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerRespawn packet, FixedOffset offset, User user) { 18 | if (packet.getLastDeathPosition() != null) { 19 | packet.setLastDeathPosition(apply(packet.getLastDeathPosition(), offset)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerDebugSample.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDebugSample; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerDebugSample extends PacketOffsetter { 12 | public OffsetterServerDebugSample() { 13 | super(WrapperPlayServerDebugSample.class, PacketType.Play.Server.DEBUG_SAMPLE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerDebugSample packet, FixedOffset offset, User user) { 18 | // TODO: Drill into Sample and offset any positions found there. 19 | // Currently disabled entirely by default. 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerJoinGame.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerJoinGame; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerJoinGame extends PacketOffsetter { 12 | public OffsetterServerJoinGame() { 13 | super(WrapperPlayServerJoinGame.class, PacketType.Play.Server.JOIN_GAME); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerJoinGame packet, FixedOffset offset, User user) { 18 | if (packet.getLastDeathPosition() != null) { 19 | packet.setLastDeathPosition(apply(packet.getLastDeathPosition(), offset)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSoundEffect.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.jtprince.coordinateoffset.FixedOffset; 6 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 7 | import com.jtprince.coordinateoffset.offsetter.wrapper.WrapperPlayServerSoundEffect_WithIdentifier; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerSoundEffect extends PacketOffsetter { 12 | public OffsetterServerSoundEffect() { 13 | super(WrapperPlayServerSoundEffect_WithIdentifier.class, PacketType.Play.Server.SOUND_EFFECT); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerSoundEffect_WithIdentifier packet, FixedOffset offset, User user) { 18 | packet.setEffectPosition(applyTimes8(packet.getEffectPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerBlockBreakAnimation.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockBreakAnimation; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerBlockBreakAnimation extends PacketOffsetter { 12 | public OffsetterServerBlockBreakAnimation() { 13 | super(WrapperPlayServerBlockBreakAnimation.class, PacketType.Play.Server.BLOCK_BREAK_ANIMATION); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerBlockBreakAnimation packet, FixedOffset offset, User user) { 18 | packet.setBlockPosition(apply(packet.getBlockPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerDebugEvent.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDebugEvent; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerDebugEvent extends PacketOffsetter { 12 | public OffsetterServerDebugEvent() { 13 | super(WrapperPlayServerDebugEvent.class, PacketType.Play.Server.DEBUG_EVENT); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerDebugEvent packet, FixedOffset offset, User user) { 18 | // TODO: Drill into DebugSubscription.Event and offset any positions found there. 19 | // Currently disabled entirely by default. 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerUnloadChunk.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUnloadChunk; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerUnloadChunk extends PacketOffsetter { 12 | public OffsetterServerUnloadChunk() { 13 | super(WrapperPlayServerUnloadChunk.class, PacketType.Play.Server.UNLOAD_CHUNK); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerUnloadChunk packet, FixedOffset offset, User user) { 18 | packet.setChunkX(applyChunkX(packet.getChunkX(), offset)); 19 | packet.setChunkZ(applyChunkZ(packet.getChunkZ(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerEntityPositionSync.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityPositionSync; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerEntityPositionSync extends PacketOffsetter { 12 | public OffsetterServerEntityPositionSync() { 13 | super(WrapperPlayServerEntityPositionSync.class, PacketType.Play.Server.ENTITY_POSITION_SYNC); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerEntityPositionSync packet, FixedOffset offset, User user) { 18 | packet.getValues().setPosition(apply(packet.getValues().getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSpawnLivingEntity.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnLivingEntity; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerSpawnLivingEntity extends PacketOffsetter { 12 | public OffsetterServerSpawnLivingEntity() { 13 | // Removed in 1.19 14 | super(WrapperPlayServerSpawnLivingEntity.class, PacketType.Play.Server.SPAWN_LIVING_ENTITY); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerSpawnLivingEntity packet, FixedOffset offset, User user) { 19 | packet.setPosition(apply(packet.getPosition(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientPlayerBlockPlacement.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerBlockPlacement; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientPlayerBlockPlacement extends PacketOffsetter { 12 | public OffsetterClientPlayerBlockPlacement() { 13 | super(WrapperPlayClientPlayerBlockPlacement.class, PacketType.Play.Client.PLAYER_BLOCK_PLACEMENT); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientPlayerBlockPlacement packet, FixedOffset offset, User user) { 18 | packet.setBlockPosition(unapply(packet.getBlockPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientPlayerPosition.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientPlayerPosition extends PacketOffsetter { 12 | public OffsetterClientPlayerPosition() { 13 | super(WrapperPlayClientPlayerFlying.class, 14 | PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayClientPlayerFlying packet, FixedOffset offset, User user) { 19 | packet.setLocation(unapply(packet.getLocation(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerNamedSoundEffect.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.jtprince.coordinateoffset.FixedOffset; 6 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 7 | import com.jtprince.coordinateoffset.offsetter.wrapper.WrapperPlayServerNamedSoundEffect; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerNamedSoundEffect extends PacketOffsetter { 12 | public OffsetterServerNamedSoundEffect() { 13 | // Removed around 1.19.2ish 14 | super(WrapperPlayServerNamedSoundEffect.class, PacketType.Play.Server.NAMED_SOUND_EFFECT); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerNamedSoundEffect packet, FixedOffset offset, User user) { 19 | packet.setEffectPosition(applyTimes8(packet.getEffectPosition(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientTestInstanceBlockAction.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientTestInstanceBlockAction; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterClientTestInstanceBlockAction extends PacketOffsetter { 12 | public OffsetterClientTestInstanceBlockAction() { 13 | super(WrapperPlayClientTestInstanceBlockAction.class, PacketType.Play.Client.TEST_INSTANCE_BLOCK_ACTION); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayClientTestInstanceBlockAction packet, FixedOffset offset, User user) { 18 | packet.setPosition(unapply(packet.getPosition(), offset)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSpawnExperienceOrb.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnExperienceOrb; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerSpawnExperienceOrb extends PacketOffsetter { 12 | public OffsetterServerSpawnExperienceOrb() { 13 | super(WrapperPlayServerSpawnExperienceOrb.class, PacketType.Play.Server.SPAWN_EXPERIENCE_ORB); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerSpawnExperienceOrb packet, FixedOffset offset, User user) { 18 | packet.setX(applyX(packet.getX(), offset)); 19 | packet.setZ(applyZ(packet.getZ(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerDebugEntityValue.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDebugEntityValue; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerDebugEntityValue extends PacketOffsetter { 12 | public OffsetterServerDebugEntityValue() { 13 | super(WrapperPlayServerDebugEntityValue.class, PacketType.Play.Server.DEBUG_ENTITY_VALUE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerDebugEntityValue packet, FixedOffset offset, User user) { 18 | // TODO: Drill into DebugSubscription.Update and offset any positions found there. 19 | // Currently disabled entirely by default. 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerUpdateViewPosition.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateViewPosition; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerUpdateViewPosition extends PacketOffsetter { 12 | public OffsetterServerUpdateViewPosition() { 13 | super(WrapperPlayServerUpdateViewPosition.class, PacketType.Play.Server.UPDATE_VIEW_POSITION); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerUpdateViewPosition packet, FixedOffset offset, User user) { 18 | packet.setChunkX(applyChunkX(packet.getChunkX(), offset)); 19 | packet.setChunkZ(applyChunkZ(packet.getChunkZ(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerGameTestHighlightPos.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerGameTestHighlightPos; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerGameTestHighlightPos extends PacketOffsetter { 12 | public OffsetterServerGameTestHighlightPos() { 13 | super(WrapperPlayServerGameTestHighlightPos.class, PacketType.Play.Server.GAME_TEST_HIGHLIGHT_POS); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerGameTestHighlightPos packet, FixedOffset offset, User user) { 18 | /* NB: Untested. Not clear how to trigger this packet. */ 19 | packet.setAbsolutePos(apply(packet.getAbsolutePos(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSetSlot.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetSlot; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | @NullMarked 12 | public class OffsetterServerSetSlot extends PacketOffsetter { 13 | public OffsetterServerSetSlot() { 14 | super(WrapperPlayServerSetSlot.class, PacketType.Play.Server.SET_SLOT); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerSetSlot packet, FixedOffset offset, User user) { 19 | ItemStack modifiedItemStack = applyItemStack(packet.getItem(), offset); 20 | if (modifiedItemStack != null) { 21 | packet.setItem(modifiedItemStack); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/api/CoordinateOffset.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.api; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | import org.jspecify.annotations.Nullable; 5 | 6 | /** 7 | * Singleton holder for the CoordinateOffsetAPI instance. 8 | * 9 | *

To get the API instance as an API consumer, call {@link #api()}.

10 | */ 11 | @NullMarked 12 | public class CoordinateOffset { 13 | private static @Nullable CoordinateOffsetAPI apiSingleton = null; 14 | 15 | /** 16 | * Get the singleton instance of the CoordinateOffsetAPI. 17 | * 18 | * @throws IllegalStateException if the API has not been initialized yet. 19 | */ 20 | public static CoordinateOffsetAPI api() { 21 | if (apiSingleton == null) { 22 | throw new IllegalStateException("CoordinateOffset API is not yet initialized."); 23 | } 24 | return apiSingleton; 25 | } 26 | 27 | static void set(CoordinateOffsetAPI api) { 28 | if (apiSingleton != null) { 29 | throw new IllegalStateException("CoordinateOffset API is already initialized."); 30 | } 31 | apiSingleton = api; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerExplosion.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerExplosion; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerExplosion extends PacketOffsetter { 12 | public OffsetterServerExplosion() { 13 | super(WrapperPlayServerExplosion.class, PacketType.Play.Server.EXPLOSION); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerExplosion packet, FixedOffset offset, User user) { 18 | packet.setPosition(apply(packet.getPosition(), offset)); 19 | if (packet.getRecords() != null) { // Can be null >=1.21.2 20 | packet.setRecords(packet.getRecords().stream().map(v -> apply(v, offset)).toList()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/config/OffsetProviderConfigImpl.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.provider.OffsetProviderConfig; 4 | import org.jspecify.annotations.NullMarked; 5 | 6 | import java.util.SequencedMap; 7 | 8 | @NullMarked 9 | public class OffsetProviderConfigImpl implements OffsetProviderConfig { 10 | private final String providerName; 11 | private final String providerClassName; 12 | private final SequencedMap configSection; 13 | 14 | public OffsetProviderConfigImpl(String providerName, String providerClassName, SequencedMap configSection) { 15 | this.providerName = providerName; 16 | this.providerClassName = providerClassName; 17 | this.configSection = configSection; 18 | } 19 | 20 | @Override 21 | public String getUserDefinedProviderName() { 22 | return providerName; 23 | } 24 | 25 | @Override 26 | public String getProviderClassName() { 27 | return providerClassName; 28 | } 29 | 30 | @Override 31 | public SequencedMap getConfigSection() { 32 | return configSection; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerDebugBlockValue.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDebugBlockValue; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerDebugBlockValue extends PacketOffsetter { 12 | public OffsetterServerDebugBlockValue() { 13 | super(WrapperPlayServerDebugBlockValue.class, PacketType.Play.Server.DEBUG_BLOCK_VALUE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerDebugBlockValue packet, FixedOffset offset, User user) { 18 | packet.setBlockPos(apply(packet.getBlockPos(), offset)); 19 | // TODO: Look closer at fields like getUpdate() which probably can have entity locations in them. 20 | // Currently disabled entirely by default. 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerDebugChunkValue.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDebugChunkValue; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerDebugChunkValue extends PacketOffsetter { 12 | public OffsetterServerDebugChunkValue() { 13 | super(WrapperPlayServerDebugChunkValue.class, PacketType.Play.Server.DEBUG_CHUNK_VALUE); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerDebugChunkValue packet, FixedOffset offset, User user) { 18 | packet.setChunkPos(applyChunk(packet.getChunkPos(), offset)); 19 | // TODO: Look closer at fields like getUpdate() which probably can have entity locations in them. 20 | // Currently disabled entirely by default. 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerAcknowledgePlayerDigging.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerAcknowledgePlayerDigging; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerAcknowledgePlayerDigging extends PacketOffsetter { 12 | public OffsetterServerAcknowledgePlayerDigging() { 13 | // Removed in 1.19 and replaced with ACKNOWLEDGE_BLOCK_CHANGES (which has no position) 14 | super(WrapperPlayServerAcknowledgePlayerDigging.class, PacketType.Play.Server.ACKNOWLEDGE_PLAYER_DIGGING); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerAcknowledgePlayerDigging packet, FixedOffset offset, User user) { 19 | packet.setBlockPosition(apply(packet.getBlockPosition(), offset)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSetCursorItem.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetCursorItem; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | @NullMarked 12 | public class OffsetterServerSetCursorItem extends PacketOffsetter { 13 | public OffsetterServerSetCursorItem() { 14 | super(WrapperPlayServerSetCursorItem.class, PacketType.Play.Server.SET_CURSOR_ITEM); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerSetCursorItem packet, FixedOffset offset, User user) { 19 | ItemStack modifiedItemStack = applyItemStack(packet.getStack(), offset); 20 | if (modifiedItemStack != null) { 21 | packet.setStack(modifiedItemStack); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerMoveMinecart.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMoveMinecart; 6 | import com.jtprince.coordinateoffset.FixedOffset; 7 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | @NullMarked 11 | public class OffsetterServerMoveMinecart extends PacketOffsetter { 12 | public OffsetterServerMoveMinecart() { 13 | super(WrapperPlayServerMoveMinecart.class, PacketType.Play.Server.MOVE_MINECART); 14 | } 15 | 16 | @Override 17 | public void offset(WrapperPlayServerMoveMinecart packet, FixedOffset offset, User user) { 18 | // Note: As of 1.21.3, this packet is only used when the experimental minecart_improvements datapack is applied 19 | for (WrapperPlayServerMoveMinecart.MinecartStep step : packet.getLerpSteps()) { 20 | step.setPosition(apply(step.getPosition(), offset)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/command/OffsetCommandSender.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.command; 2 | 3 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 4 | import net.kyori.adventure.audience.Audience; 5 | import net.kyori.adventure.audience.ForwardingAudience; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | import java.util.Collections; 10 | 11 | /** 12 | * Adapter interface representing a command sender in a Minecraft server. 13 | * 14 | * @param audience An audience that can be used to respond to this sender. 15 | * @param name String to represent the sender in various verbose messages. 16 | * @param player The player who sent this command, or null if the sender is not a player (console, command 17 | * block, etc.) 18 | */ 19 | @NullMarked 20 | public record OffsetCommandSender(Audience audience, String name, @Nullable OffsetPlayer player) implements ForwardingAudience { 21 | @Override 22 | public Iterable audiences() { 23 | return Collections.singleton(audience); 24 | } 25 | 26 | public boolean isPlayer(OffsetPlayer player) { 27 | return this.player != null && this.player.equals(player); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerSetPlayerInventory.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPlayerInventory; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | @NullMarked 12 | public class OffsetterServerSetPlayerInventory extends PacketOffsetter { 13 | public OffsetterServerSetPlayerInventory() { 14 | super(WrapperPlayServerSetPlayerInventory.class, PacketType.Play.Server.SET_PLAYER_INVENTORY); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerSetPlayerInventory packet, FixedOffset offset, User user) { 19 | ItemStack modifiedItemStack = applyItemStack(packet.getStack(), offset); 20 | if (modifiedItemStack != null) { 21 | packet.setStack(modifiedItemStack); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/config/CoordinateOffsetConfig.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | import org.jspecify.annotations.Nullable; 5 | 6 | import java.util.SequencedMap; 7 | 8 | /** 9 | * Configuration interface for CoordinateOffset. Provides access to various settings configured in the CoordinateOffset 10 | * configuration file. 11 | * 12 | *

Configured Offset Providers are NOT accessible here because they load after the main CoordinateOffset config. 13 | * See {@link CoordinateOffsetProviderConfig} for provider-specific configuration access.

14 | * 15 | *

See Configuration Guide 16 | * for information about individual settings.

17 | */ 18 | @NullMarked 19 | public interface CoordinateOffsetConfig { 20 | @Nullable Integer getConfigVersion(); 21 | boolean getBypassByPermission(); 22 | boolean getFixCollisionBamboo(); 23 | boolean getFixCollisionDripstone(); 24 | boolean getObfuscateWorldBorder(); 25 | boolean getObfuscateDebugPropertySubscriptions(); 26 | int getOffsetsAreMultiplesOfBlocks(); 27 | boolean getVerbose(); 28 | SequencedMap getWorldCoordinateScaleOverrides(); 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientCreativeInventoryAction.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientCreativeInventoryAction; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | @NullMarked 12 | public class OffsetterClientCreativeInventoryAction extends PacketOffsetter { 13 | public OffsetterClientCreativeInventoryAction() { 14 | super(WrapperPlayClientCreativeInventoryAction.class, PacketType.Play.Client.CREATIVE_INVENTORY_ACTION); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayClientCreativeInventoryAction packet, FixedOffset offset, User user) { 19 | ItemStack modifiedItemStack = unapplyItemStack(packet.getItemStack(), offset); 20 | if (modifiedItemStack != null) { 21 | packet.setItemStack(modifiedItemStack); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/adapter/OffsetWorld.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.adapter; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * Adapter interface representing a world in a Minecraft server. 9 | * 10 | *

The name does NOT imply that an offset was already applied. It refers to any World object in the different 11 | * platforms.

12 | */ 13 | @NullMarked 14 | public interface OffsetWorld { 15 | UUID getUuid(); 16 | 17 | /** 18 | * Get the name of the world. 19 | * @return Example world or world_nether. 20 | */ 21 | String getName(); 22 | 23 | /** 24 | * Get the key of the world. 25 | * @return Example minecraft:overworld or minecraft:the_nether. 26 | */ 27 | String getKey(); 28 | 29 | /** 30 | * Get the scaling factor for this world. This is used to scale the offset by the world's size. 31 | * Offsets are divided by this factor. For example, the default nether has a scaling factor of 8. 32 | * @return Scaling factor for this world. 33 | */ 34 | Double getCoordinateScale(); 35 | 36 | /** 37 | * Get the underlying platform-specific player object, for example a Bukkit World. 38 | */ 39 | Object getPlatformPlayerObject(); 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerPlayerPositionAndLook.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.protocol.teleport.RelativeFlag; 6 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerPositionAndLook; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | @NullMarked 12 | public class OffsetterServerPlayerPositionAndLook extends PacketOffsetter { 13 | public OffsetterServerPlayerPositionAndLook() { 14 | super(WrapperPlayServerPlayerPositionAndLook.class, PacketType.Play.Server.PLAYER_POSITION_AND_LOOK); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerPlayerPositionAndLook packet, FixedOffset offset, User user) { 19 | if (!packet.isRelativeFlag(RelativeFlag.X)) { 20 | packet.setX(applyX(packet.getX(), offset)); 21 | } 22 | if (!packet.isRelativeFlag(RelativeFlag.Z)) { 23 | packet.setZ(applyZ(packet.getZ(), offset)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/provider/util/ProviderOffsetStore.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider.util; 2 | 3 | import com.jtprince.coordinateoffset.ScalableOffset; 4 | import com.jtprince.coordinateoffset.adapter.OffsetPersistenceAdapter; 5 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | import java.util.UUID; 10 | 11 | @NullMarked 12 | public class ProviderOffsetStore { 13 | private final OffsetPersistenceAdapter adapter; 14 | private final OffsetPersistenceAdapter.Key persistenceKey; 15 | 16 | public ProviderOffsetStore( 17 | OffsetPersistenceAdapter adapter, 18 | String providerName, 19 | @Nullable String persistenceKeyOverride 20 | ) { 21 | this.adapter = adapter; 22 | this.persistenceKey = new OffsetPersistenceAdapter.Key(providerName, persistenceKeyOverride); 23 | } 24 | 25 | public @Nullable ScalableOffset get(OffsetPlayer player) { 26 | return adapter.get(player, persistenceKey); 27 | } 28 | 29 | public void put(OffsetPlayer player, ScalableOffset offset) { 30 | adapter.put(player, persistenceKey, offset); 31 | } 32 | 33 | public void clear(UUID playerUuid) { 34 | adapter.clear(playerUuid, persistenceKey); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerEntityEquipment.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.player.Equipment; 6 | import com.github.retrooper.packetevents.protocol.player.User; 7 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityEquipment; 8 | import com.jtprince.coordinateoffset.FixedOffset; 9 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 10 | import org.jspecify.annotations.NullMarked; 11 | 12 | @NullMarked 13 | public class OffsetterServerEntityEquipment extends PacketOffsetter { 14 | public OffsetterServerEntityEquipment() { 15 | super(WrapperPlayServerEntityEquipment.class, PacketType.Play.Server.ENTITY_EQUIPMENT); 16 | } 17 | 18 | @Override 19 | public void offset(WrapperPlayServerEntityEquipment packet, FixedOffset offset, User user) { 20 | for (Equipment equipment : packet.getEquipment()) { 21 | ItemStack modifiedItemStack = applyItemStack(equipment.getItem(), offset); 22 | if (modifiedItemStack != null) { 23 | equipment.setItem(modifiedItemStack); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /scripts/setup_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | if [[ $# -lt 1 ]]; then 5 | echo "Usage: $0 []" 6 | echo "Example: $0 1.21.11 2.11.0" 7 | exit 1 8 | fi 9 | 10 | if [[ ! -d example-api-plugin ]]; then 11 | echo "Please run this from the CoordinateOffset root directory." 12 | exit 1 13 | fi 14 | 15 | mkdir -p "run-$1" 16 | cd "run-$1" 17 | 18 | # EULA agree 19 | echo "eula=true" > eula.txt 20 | 21 | # Disable bStats 22 | mkdir -p "plugins/bStats" 23 | echo "enabled: false" > plugins/bStats/config.yml 24 | 25 | # Link CoordinateOffset snapshot jar 26 | if [[ ! -e "plugins/CoordinateOffset-SNAPSHOT.jar" ]]; then 27 | ln -s ../../paper/build/CoordinateOffset-Paper-SNAPSHOT.jar plugins/CoordinateOffset-Paper-SNAPSHOT.jar 28 | fi 29 | 30 | # Download packetevents 31 | if [[ -n "${2:-}" ]]; then 32 | echo Downloading PacketEvents v$2 ... 33 | curl -fLO --output-dir "plugins" "https://github.com/retrooper/packetevents/releases/download/v$2/packetevents-spigot-$2.jar" 34 | else 35 | echo "No PacketEvents version specified. Be sure to install it in the plugins folder." 36 | fi 37 | 38 | # Verify CoordinateOffset is built 39 | if [[ ! -e ../paper/build/CoordinateOffset-Paper-SNAPSHOT.jar ]]; then 40 | echo "No plugin build at paper/build/CoordinateOffset-Paper-SNAPSHOT.jar. Be sure to run './gradlew build' before testing." 41 | fi 42 | 43 | echo "Server is ready at $(realpath .). Download a server JAR there and run it once." 44 | echo "Configure with configure_server.sh" 45 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerWindowItems.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowItems; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | @NullMarked 12 | public class OffsetterServerWindowItems extends PacketOffsetter { 13 | public OffsetterServerWindowItems() { 14 | super(WrapperPlayServerWindowItems.class, PacketType.Play.Server.WINDOW_ITEMS); 15 | } 16 | 17 | @Override 18 | public void offset(WrapperPlayServerWindowItems packet, FixedOffset offset, User user) { 19 | packet.setItems(packet.getItems().stream().map(it -> { 20 | ItemStack modifiedItemStack = applyItemStack(it, offset); 21 | if (modifiedItemStack != null) { 22 | return modifiedItemStack; 23 | } else { 24 | return it; 25 | } 26 | }).toList()); 27 | if (packet.getCarriedItem().isPresent()) { 28 | ItemStack modifiedItemStack = applyItemStack(packet.getCarriedItem().get(), offset); 29 | if (modifiedItemStack != null) { 30 | packet.setCarriedItem(modifiedItemStack); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/adapter/OffsetPersistenceAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.adapter; 2 | 3 | import com.jtprince.coordinateoffset.ScalableOffset; 4 | import org.jspecify.annotations.NullMarked; 5 | import org.jspecify.annotations.Nullable; 6 | 7 | import java.util.UUID; 8 | 9 | @NullMarked 10 | public interface OffsetPersistenceAdapter { 11 | @Nullable ScalableOffset get(OffsetPlayer player, Key persistenceKey); 12 | void put(OffsetPlayer player, Key persistenceKey, ScalableOffset offset); 13 | void clear(UUID playerUuid, Key persistenceKey); 14 | 15 | /** 16 | * A key to index persistent player data, for example provider.persistence.default 17 | * 18 | * @param providerName User-defined name of the provider, for example random 19 | * @param persistenceKeyOverride An override that allows the user to specify a custom key for any persistent storage 20 | * on players. Takes precedence over the provider name. 21 | */ 22 | record Key( 23 | String providerName, 24 | @Nullable String persistenceKeyOverride 25 | ) { 26 | public Key(String providerName) { 27 | this(providerName, null); 28 | } 29 | 30 | /** 31 | * Get the effective key to use for persistence. This may be the user-defined provider name (e.g. "random") 32 | * or the override set by the user (e.g. "default" for legacy configs). 33 | * @return The effective key to use for persistence. 34 | */ 35 | public String getPersistenceKey() { 36 | return "provider." + (persistenceKeyOverride != null ? persistenceKeyOverride : providerName); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | packetevents = "2.11.0-SNAPSHOT" 3 | paper-api = "1.21.4-R0.1-SNAPSHOT" 4 | paper-apiversion = "1.21.4" # plugin.yml apiVersion 5 | 6 | [libraries] 7 | adventure-api = { group = "net.kyori", name = "adventure-api", version = "4.25.0" } 8 | adventure-minimessage = { group = "net.kyori", name = "adventure-text-minimessage", version = "4.25.0" } 9 | bstats-bukkit = { group = "org.bstats", name = "bstats-bukkit", version = "3.1.0" } 10 | configlib-core = { group = "de.exlll", name = "configlib-core", version = "4.6.4" } 11 | configlib-paper = { group = "de.exlll", name = "configlib-paper", version = "4.6.4" } 12 | geyser-api = { group = "org.geysermc.geyser", name = "api", version = "2.9.0-SNAPSHOT" } 13 | jspecify = { group = "org.jspecify", name = "jspecify", version = "1.0.0" } 14 | morepdt = { group = "com.jeff-media", name = "MorePersistentDataTypes", version = "2.4.0" } 15 | netty-buffer = { group = "io.netty", name = "netty-buffer", version = "4.2.8.Final" } 16 | packetevents-api = { group = "com.github.retrooper", name = "packetevents-api", version.ref = "packetevents" } 17 | paper-api = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper-api" } 18 | test-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version = "5.11.4" } 19 | test-junit-platform = { group = "org.junit.platform", name = "junit-platform-launcher" } 20 | 21 | [plugins] 22 | hangar-publish = { id = "io.papermc.hangar-publish-plugin", version = "0.1.4" } 23 | maven-publish = { id = "com.vanniktech.maven.publish", version = "0.35.0" } 24 | modrinth-minotaur = { id = "com.modrinth.minotaur", version = "2.8.10" } 25 | paperweight-userdev = { id = "io.papermc.paperweight.userdev", version = "2.0.0-beta.19" } 26 | shadow = { id = "com.gradleup.shadow", version = "9.3.0" } 27 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerBlockEntityData.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; 4 | import com.github.retrooper.packetevents.protocol.nbt.NBTInt; 5 | import com.github.retrooper.packetevents.protocol.nbt.NBTNumber; 6 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 7 | import com.github.retrooper.packetevents.protocol.player.User; 8 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockEntityData; 9 | import com.jtprince.coordinateoffset.FixedOffset; 10 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 11 | import org.jspecify.annotations.NullMarked; 12 | 13 | @NullMarked 14 | public class OffsetterServerBlockEntityData extends PacketOffsetter { 15 | public OffsetterServerBlockEntityData() { 16 | super(WrapperPlayServerBlockEntityData.class, PacketType.Play.Server.BLOCK_ENTITY_DATA); 17 | } 18 | 19 | @Override 20 | public void offset(WrapperPlayServerBlockEntityData packet, FixedOffset offset, User user) { 21 | packet.setPosition(apply(packet.getPosition(), offset)); 22 | 23 | // TBD: I'm not sure which tile entity these are used for, but I'm keeping them from upstream just in case. 24 | if (packet.getNBT() != null) { 25 | NBTCompound nbt = packet.getNBT(); 26 | NBTNumber x = nbt.getNumberTagOrNull("x"); 27 | NBTNumber z = nbt.getNumberTagOrNull("z"); 28 | if (x != null && z != null) { 29 | nbt.setTag("x", new NBTInt(x.getAsInt() - offset.x())); 30 | nbt.setTag("z", new NBTInt(z.getAsInt() - offset.z())); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/lib/org/geysermc/hurricane/NMSReflection.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.paper.lib.org.geysermc.hurricane; 2 | 3 | import org.bukkit.Bukkit; 4 | 5 | // From ViaRewind Legacy Support 6 | public final class NMSReflection { 7 | private static String version; 8 | /** 9 | * Cheap hack to allow different fields. 10 | */ 11 | public static boolean mojmap = true; 12 | 13 | public static String getVersion() { 14 | return version == null ? version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3] : version; 15 | } 16 | 17 | /** 18 | * 1.17+ 19 | */ 20 | public static Class getMojmapNMSClass(String name) { 21 | try { 22 | return Class.forName("net.minecraft." + name); 23 | } catch (ClassNotFoundException e) { 24 | return null; 25 | } 26 | } 27 | 28 | public static Class getNMSClass(String post1_16Prefix, String... names) { 29 | for (String name : names) { 30 | Class newNMSClass = getMojmapNMSClass(post1_16Prefix + "." + name); 31 | if (newNMSClass != null) { 32 | return newNMSClass; 33 | } 34 | } 35 | 36 | // Else Mojmap/post-1.17 is not in effect 37 | mojmap = false; 38 | Exception ex = null; 39 | for (String name : names) { 40 | try { 41 | return Class.forName("net.minecraft.server." + getVersion() + "." + name); 42 | } catch (ClassNotFoundException e) { 43 | if (ex == null) { 44 | ex = e; 45 | } 46 | } 47 | } 48 | if (ex != null) { 49 | ex.printStackTrace(); 50 | } 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.maven.publish) 3 | idea 4 | } 5 | 6 | dependencies { 7 | compileOnly(libs.jspecify) 8 | compileOnly(libs.configlib.core) 9 | compileOnly(libs.paper.api) 10 | 11 | testImplementation(libs.paper.api) 12 | testImplementation(libs.test.junit.jupiter) 13 | testRuntimeOnly(libs.test.junit.platform) 14 | } 15 | 16 | tasks { 17 | test { 18 | useJUnitPlatform() 19 | } 20 | } 21 | 22 | mavenPublishing { 23 | publishToMavenCentral() 24 | signAllPublications() 25 | 26 | coordinates(project.group.toString(), "coordinateoffset-api", project.version.toString()) 27 | pom { 28 | name = "CoordinateOffset API" 29 | description = "API for CoordinateOffset, a Minecraft server plugin that configurably obfuscates players' coordinates." 30 | url = "https://github.com/joshuaprince/CoordinateOffset" 31 | developers { 32 | developer { 33 | id = "joshuaprince" 34 | name = "Joshua Prince" 35 | email = "joshua@jtprince.com" 36 | url = "https://github.com/joshuaprince" 37 | } 38 | } 39 | licenses { 40 | license { 41 | name = "GNU Affero General Public License v3.0" 42 | url = "https://www.gnu.org/licenses/agpl-3.0.en.html" 43 | } 44 | } 45 | scm { 46 | connection = "scm:git:git://github.com/joshuaprince/CoordinateOffset.git" 47 | developerConnection = "scm:git:ssh://github.com:joshuaprince/CoordinateOffset.git" 48 | url = "https://github.com/joshuaprince/CoordinateOffset" 49 | } 50 | } 51 | } 52 | 53 | idea { 54 | module { 55 | isDownloadJavadoc = true 56 | isDownloadSources = true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientPlayerDigging.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.DiggingAction; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | import java.util.Set; 12 | 13 | @NullMarked 14 | public class OffsetterClientPlayerDigging extends PacketOffsetter { 15 | /** 16 | * Despite containing a block position, these actions do not actually refer to a block in the world. Minecraft 17 | * protocol documentation specifies that the position included in packets for these actions should be 0/0/0. 18 | * Offsetting these positions causes issues with anticheats that expect 0/0/0. 19 | */ 20 | private static final Set ACTIONS_WITH_ZERO_POSITION = Set.of( 21 | DiggingAction.DROP_ITEM_STACK, 22 | DiggingAction.DROP_ITEM, 23 | DiggingAction.RELEASE_USE_ITEM, 24 | DiggingAction.SWAP_ITEM_WITH_OFFHAND 25 | ); 26 | 27 | public OffsetterClientPlayerDigging() { 28 | super(WrapperPlayClientPlayerDigging.class, PacketType.Play.Client.PLAYER_DIGGING); 29 | } 30 | 31 | @Override 32 | public void offset(WrapperPlayClientPlayerDigging packet, FixedOffset offset, User user) { 33 | if (!ACTIONS_WITH_ZERO_POSITION.contains(packet.getAction())) { 34 | packet.setBlockPosition(unapply(packet.getBlockPosition(), offset)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/adapter/PaperLocation.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.paper.adapter; 2 | 3 | import com.jtprince.coordinateoffset.FixedOffset; 4 | import com.jtprince.coordinateoffset.adapter.OffsetLocation; 5 | import com.jtprince.coordinateoffset.adapter.OffsetWorld; 6 | import org.bukkit.Location; 7 | import org.jspecify.annotations.NullMarked; 8 | import org.jspecify.annotations.Nullable; 9 | 10 | @NullMarked 11 | public class PaperLocation implements OffsetLocation { 12 | private final Location location; 13 | public PaperLocation(Location location) { 14 | this.location = location; 15 | } 16 | 17 | @Override 18 | public @Nullable OffsetWorld getWorld() { 19 | if (location.getWorld() == null) return null; 20 | return new PaperWorld(location.getWorld().getUID()); 21 | } 22 | 23 | @Override 24 | public double getX() { 25 | return location.getX(); 26 | } 27 | 28 | @Override 29 | public double getY() { 30 | return location.getY(); 31 | } 32 | 33 | @Override 34 | public double getZ() { 35 | return location.getZ(); 36 | } 37 | 38 | @Override 39 | public OffsetLocation apply(FixedOffset offset) { 40 | return new PaperLocation(location.clone().subtract(offset.x(), 0, offset.z())); 41 | } 42 | 43 | @Override 44 | public OffsetLocation unapply(FixedOffset offset) { 45 | return new PaperLocation(location.clone().add(offset.x(), 0, offset.z())); 46 | } 47 | 48 | @Override 49 | public Location getPlatformLocationObject() { 50 | return location; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return location.toString(); 56 | } 57 | 58 | @Override 59 | public boolean equals(Object obj) { 60 | return (obj instanceof PaperLocation p) && location.equals(p.location); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/OffsetProviderClassRegistry.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset; 2 | 3 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 4 | import com.jtprince.coordinateoffset.provider.OffsetProviderConfig; 5 | import org.jspecify.annotations.NullMarked; 6 | import org.jspecify.annotations.Nullable; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.function.Function; 11 | 12 | @NullMarked 13 | public class OffsetProviderClassRegistry { 14 | /** 15 | * Record for storing a registered provider class. 16 | * @param className The class name of the provider, e.g. "RandomOffsetProvider". 17 | * @param isCore If true, the provider is built-in to CoordinateOffset. If false, the provider was added by an 18 | * API consumer. 19 | * @param deserializeFunction A function that can deserialize an OffsetProviderConfig into an OffsetProvider. 20 | */ 21 | public record RegisteredProviderClass( 22 | String className, 23 | boolean isCore, 24 | Function deserializeFunction 25 | ) {} 26 | private final Map registeredProviders = new HashMap<>(); 27 | 28 | public void registerProviderClass( 29 | String className, 30 | boolean isCore, 31 | Function deserializeFunction 32 | ) { 33 | if (registeredProviders.containsKey(className)) { 34 | throw new IllegalArgumentException("Attempted to re-register Offset provider class: " + className); 35 | } 36 | registeredProviders.put(className, new RegisteredProviderClass(className, isCore, deserializeFunction)); 37 | } 38 | 39 | public @Nullable RegisteredProviderClass getRegisteredProviderClass(String className) { 40 | return registeredProviders.get(className); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/adapter/PaperOffsetPlayer.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.paper.adapter; 2 | 3 | import com.jtprince.coordinateoffset.adapter.OffsetLocation; 4 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.permissions.PermissionAttachmentInfo; 7 | import org.jspecify.annotations.NullMarked; 8 | 9 | import java.util.Set; 10 | import java.util.UUID; 11 | 12 | @NullMarked 13 | public class PaperOffsetPlayer implements OffsetPlayer { 14 | private final Player player; 15 | public PaperOffsetPlayer(Player player) { 16 | this.player = player; 17 | } 18 | 19 | public Player getPlayer() { 20 | return player; 21 | } 22 | 23 | @Override 24 | public UUID getUuid() { 25 | return player.getUniqueId(); 26 | } 27 | 28 | @Override 29 | public String getName() { 30 | return player.getName(); 31 | } 32 | 33 | @Override 34 | public boolean hasPermission(String permission) { 35 | return player.hasPermission(permission); 36 | } 37 | 38 | @Override 39 | public Set getAllPermissions() { 40 | return player.getEffectivePermissions().stream() 41 | .filter(PermissionAttachmentInfo::getValue) // only "true" permissions 42 | .map(PermissionAttachmentInfo::getPermission) 43 | .collect(java.util.stream.Collectors.toSet()); 44 | } 45 | 46 | @Override 47 | public OffsetLocation getLocation() { 48 | return new PaperLocation(player.getLocation()); 49 | } 50 | 51 | @Override 52 | public Player getPlatformPlayerObject() { 53 | return player; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return player.toString(); 59 | } 60 | 61 | @Override 62 | public boolean equals(Object obj) { 63 | return (obj instanceof PaperOffsetPlayer p) && player.equals(p.player); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerWaypoint.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 4 | import com.github.retrooper.packetevents.protocol.player.User; 5 | import com.github.retrooper.packetevents.protocol.world.waypoint.*; 6 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWaypoint; 7 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 8 | import com.jtprince.coordinateoffset.FixedOffset; 9 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 10 | import org.jspecify.annotations.NullMarked; 11 | 12 | @NullMarked 13 | public class OffsetterServerWaypoint extends PacketOffsetter { 14 | public OffsetterServerWaypoint() { 15 | super(WrapperPlayServerWaypoint.class, PacketType.Play.Server.WAYPOINT); 16 | } 17 | 18 | @Override 19 | public void offset(WrapperPlayServerWaypoint packet, FixedOffset offset, User user) { 20 | TrackedWaypoint waypoint = packet.getWaypoint(); 21 | 22 | WaypointInfo oldInfo = waypoint.getInfo(); 23 | WaypointInfo newInfo; 24 | if (oldInfo instanceof Vec3iWaypointInfo info) { 25 | newInfo = new Vec3iWaypointInfo(apply(info.getPosition(), offset)); 26 | } else if (oldInfo instanceof ChunkWaypointInfo info) { 27 | newInfo = new ChunkWaypointInfo(applyChunkX(info.getChunkX(), offset), applyChunkZ(info.getChunkZ(), offset)); 28 | } else if (oldInfo instanceof AzimuthWaypointInfo || oldInfo instanceof EmptyWaypointInfo) { 29 | // No offset needed for these types 30 | return; 31 | } else { 32 | CoordinateOffsetCore.get().getLogger().warning("Unknown waypoint type: " + oldInfo.getClass().getName()); 33 | return; 34 | } 35 | 36 | packet.setWaypoint(new TrackedWaypoint(waypoint.getIdentifier(), waypoint.getIcon(), newInfo)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/OffsetData.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset; 2 | 3 | import com.jtprince.coordinateoffset.command.OffsetSetCommand; 4 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 5 | import com.jtprince.coordinateoffset.provider.OffsetProviderContext; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | /** 10 | * Container for an offset which was generated for a specific context, and where the offset came from. 11 | * 12 | * @param offset The offset that was generated. 13 | * @param source Source of the offset. The offset may have come from an offset provider, or the offset may have been 14 | * generated in response to an offset bypass or a command. 15 | * @param context Context in which the offset was generated. 16 | */ 17 | @NullMarked 18 | public record OffsetData( 19 | FixedOffset offset, 20 | Source source, 21 | OffsetProviderContext context 22 | ) { 23 | public sealed interface Source 24 | permits Source.PermissionBypass, Source.BedrockBypass, Source.Provider, Source.SetCommand, Source.PluginSet { 25 | 26 | /** This offset is zero and was generated because the player has permission to bypass offsets. */ 27 | record PermissionBypass() implements Source {} 28 | /** This offset is zero and was generated because the player is using Bedrock edition through Geyser. */ 29 | record BedrockBypass() implements Source {} 30 | /** This offset was generated by a configured OffsetProvider class. */ 31 | record Provider(OffsetProvider provider, @Nullable Integer overrideRuleIndex) implements Source {} 32 | /** This offset was explicitly set by a command. */ 33 | record SetCommand(OffsetSetCommand command, @Nullable OffsetProvider affectedProvider) implements Source {} 34 | /** This offset was directly set by a third-party plugin (NOT a provider defined by the plugin). */ 35 | record PluginSet() implements Source {} 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/wrapper/WrapperPlayServerEffect.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.wrapper; 2 | 3 | import com.github.retrooper.packetevents.event.PacketSendEvent; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Server; 5 | import com.github.retrooper.packetevents.util.Vector3i; 6 | import com.github.retrooper.packetevents.wrapper.PacketWrapper; 7 | import org.jspecify.annotations.NonNull; 8 | 9 | @SuppressWarnings("unused") // Constructors are called reflectively 10 | public class WrapperPlayServerEffect extends PacketWrapper<@NonNull WrapperPlayServerEffect> { 11 | private int eventId; 12 | private Vector3i position; 13 | private byte[] remainingData; // Lazily ignoring everything after position, since only position matters for this plugin 14 | 15 | public WrapperPlayServerEffect(PacketSendEvent event) { 16 | super(event); 17 | } 18 | 19 | public WrapperPlayServerEffect(int eventId, Vector3i position, byte[] remainingData) { 20 | super(Server.EFFECT); 21 | this.eventId = eventId; 22 | this.position = position; 23 | this.remainingData = remainingData; 24 | } 25 | 26 | public void read() { 27 | this.eventId = this.readInt(); 28 | this.position = new Vector3i(this.readLong()); 29 | this.remainingData = this.readRemainingBytes(); 30 | } 31 | 32 | public void write() { 33 | this.writeInt(this.eventId); 34 | long positionVector = this.position.getSerializedPosition(); 35 | this.writeLong(positionVector); 36 | this.writeBytes(this.remainingData); 37 | } 38 | 39 | public int getEventId() { 40 | return eventId; 41 | } 42 | 43 | public void setEventId(int eventId) { 44 | this.eventId = eventId; 45 | } 46 | 47 | public Vector3i getPosition() { 48 | return position; 49 | } 50 | 51 | public void setPosition(Vector3i position) { 52 | this.position = position; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/adapter/PaperWorld.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.paper.adapter; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.adapter.OffsetWorld; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.World; 7 | import org.jspecify.annotations.NullMarked; 8 | 9 | import java.util.Objects; 10 | import java.util.SequencedMap; 11 | import java.util.UUID; 12 | 13 | @NullMarked 14 | public class PaperWorld implements OffsetWorld { 15 | UUID uuid; 16 | public PaperWorld(UUID worldUuid) { 17 | this.uuid = worldUuid; 18 | } 19 | 20 | @Override 21 | public UUID getUuid() { 22 | return uuid; 23 | } 24 | 25 | @Override 26 | public String getName() { 27 | return getPlatformPlayerObject().getName(); 28 | } 29 | 30 | @Override 31 | public String getKey() { 32 | return getPlatformPlayerObject().getKey().asString(); 33 | } 34 | 35 | @Override 36 | public Double getCoordinateScale() { 37 | World world = getPlatformPlayerObject(); 38 | 39 | SequencedMap overrides = CoordinateOffsetCore.get().getConfig().getWorldCoordinateScaleOverrides(); 40 | if (overrides.containsKey(world.getUID().toString())) return overrides.get(world.getUID().toString()); 41 | if (overrides.containsKey(world.getName())) return overrides.get(world.getName()); 42 | if (overrides.containsKey(world.getKey().asString())) return overrides.get(world.getKey().asString()); 43 | 44 | return getPlatformPlayerObject().getCoordinateScale(); 45 | } 46 | 47 | @Override 48 | public World getPlatformPlayerObject() { 49 | return Objects.requireNonNull(Bukkit.getWorld(uuid)); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return getPlatformPlayerObject().toString(); 55 | } 56 | 57 | @Override 58 | public boolean equals(Object obj) { 59 | return (obj instanceof PaperWorld w) && uuid.equals(w.uuid); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/config/OffsetProviderOverrideConfig.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 4 | import com.jtprince.coordinateoffset.provider.OffsetProviderContext; 5 | import org.jspecify.annotations.NullMarked; 6 | import org.jspecify.annotations.Nullable; 7 | 8 | /** 9 | * Container for a single line in the offsetProviderOverrides list in the CoordinateOffset configuration. 10 | * 11 | *

See 12 | * Applying Offset Providers 13 | * for more information about how overrides work.

14 | */ 15 | @NullMarked 16 | public interface OffsetProviderOverrideConfig { 17 | /** The offset provider configuration to use when this override matches. */ 18 | OffsetProvider getOffsetProvider(); 19 | 20 | /** The name, key, or UUID of a world to match, or null to match any world. */ 21 | @Nullable String getWorld(); 22 | 23 | /** The permission node to match, or null to match any player. */ 24 | @Nullable String getPermission(); 25 | 26 | /** The name or UUID of a specific player to match, or null to match any player. */ 27 | @Nullable String getPlayer(); 28 | 29 | default boolean appliesTo(OffsetProviderContext context) { 30 | if (getPlayer() != null 31 | && !getPlayer().equalsIgnoreCase(context.player().getName()) 32 | && !getPlayer().equalsIgnoreCase(context.player().getUuid().toString())) return false; 33 | if (getWorld() != null 34 | && !getWorld().equalsIgnoreCase(context.playerLocation().getWorld().getName()) 35 | && !getWorld().equalsIgnoreCase(context.playerLocation().getWorld().getKey()) 36 | && !getWorld().equalsIgnoreCase(context.playerLocation().getWorld().getUuid().toString())) return false; 37 | if (getPermission() != null && !context.player().hasPermission(getPermission())) return false; 38 | 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/adapter/CoordinateOffsetAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.adapter; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | import org.jspecify.annotations.Nullable; 5 | 6 | import java.nio.file.Path; 7 | import java.util.UUID; 8 | import java.util.logging.Logger; 9 | 10 | @NullMarked 11 | public interface CoordinateOffsetAdapter { 12 | Path getConfigDir(); 13 | 14 | Logger getLogger(); 15 | 16 | @Nullable OffsetPlayer getPlayer(UUID playerUuid); 17 | OffsetPlayer adaptPlayer(Object platformPlayerObject) throws ClassCastException; 18 | OffsetLocation adaptLocation(Object platformLocationObject) throws ClassCastException; 19 | 20 | /** 21 | * Get a platform interface into storing persistent offset data on Players. 22 | */ 23 | OffsetPersistenceAdapter getPersistenceAdapter(); 24 | 25 | /** 26 | * Get a platform interface into applying immediate offset changes. 27 | */ 28 | OffsetSwapper getOffsetSwapper(); 29 | 30 | /** 31 | * Get the minimum offset multiple that is compatible with all installed plugins. CoordinateOffset core will 32 | * "best-effort" attempt to align offset components to this multiple, but API consumers may circumvent it. 33 | * 34 | *

Vanilla Minecraft servers have an offset multiple of 16 (the size of a chunk). Other mods and plugins may 35 | * push this higher, such as Distant Horizons (64).

36 | * 37 | * @return A power of 2 that is at least 16. 38 | */ 39 | int getMinimumOffsetMultiple(); 40 | 41 | /** 42 | * Assert that the current thread is the main server thread, or throw an {@link IllegalStateException} if it is not. 43 | * 44 | * @param methodName Name of the method being called. Printed in the exception message for help tracking. 45 | */ 46 | void assertMainThread(String methodName) throws IllegalStateException; 47 | 48 | /** 49 | * Initiate internal shutdown of CoordinateOffset due to an error. Should not be called as part of server shutdown. 50 | */ 51 | void shutdown(); 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | /run*/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/provider/DefaultOffsetProviders.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider; 2 | 3 | import com.jtprince.coordinateoffset.Offset; 4 | import com.jtprince.coordinateoffset.config.OffsetProviderOverrideConfigImpl; 5 | import com.jtprince.coordinateoffset.provider.util.RegenerateConfig; 6 | import org.jspecify.annotations.NullMarked; 7 | 8 | import java.util.LinkedHashMap; 9 | import java.util.List; 10 | import java.util.SequencedMap; 11 | 12 | @NullMarked 13 | public class DefaultOffsetProviders { 14 | static final ConstantOffsetProvider CONSTANT_DISABLED = new ConstantOffsetProvider( 15 | "disabled", 16 | null 17 | ); 18 | static final ConstantOffsetProvider CONSTANT_1024 = new ConstantOffsetProvider( 19 | "constant", 20 | Offset.scalable(1024, 1024) 21 | ); 22 | 23 | static final RandomOffsetProvider RANDOM = new RandomOffsetProvider( 24 | "random", 25 | 100_000, 26 | new RegenerateConfig(false, false, false, false, RegenerateConfig.DEFAULT_MINIMUM_TELEPORT_DISTANCE, null) 27 | ); 28 | 29 | static final ZeroAtLocationOffsetProvider ZERO_LOC = new ZeroAtLocationOffsetProvider( 30 | "zeroAtLocation", 31 | new RegenerateConfig(false, false, false, false, RegenerateConfig.DEFAULT_MINIMUM_TELEPORT_DISTANCE, null) 32 | ); 33 | 34 | static final PermissionOffsetProvider PERMISSION = new PermissionOffsetProvider( 35 | "permission", 36 | "coordinateoffset.offset" 37 | ); 38 | 39 | public static SequencedMap PROVIDERS = new LinkedHashMap<>(); 40 | static { 41 | try { 42 | PROVIDERS.put(CONSTANT_DISABLED.name, CONSTANT_DISABLED); 43 | PROVIDERS.put(CONSTANT_1024.name, CONSTANT_1024); 44 | PROVIDERS.put(RANDOM.name, RANDOM); 45 | PROVIDERS.put(RANDOM.name, RANDOM); 46 | PROVIDERS.put(ZERO_LOC.name, ZERO_LOC); 47 | PROVIDERS.put(PERMISSION.name, PERMISSION); 48 | } catch (Exception e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | 53 | public static final List DEFAULT_OVERRIDES = List.of( 54 | new OffsetProviderOverrideConfigImpl(CONSTANT_DISABLED.name, "world_the_end", null, null), 55 | new OffsetProviderOverrideConfigImpl(ZERO_LOC.name, "world_example", 56 | "coordinateoffset.provider.my_custom_permission", "ExamplePlayerName") 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerEntityMetadata.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.entity.data.EntityData; 4 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 5 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 6 | import com.github.retrooper.packetevents.protocol.player.User; 7 | import com.github.retrooper.packetevents.util.Vector3i; 8 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; 9 | import com.jtprince.coordinateoffset.FixedOffset; 10 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 11 | import org.jspecify.annotations.NullMarked; 12 | 13 | import java.util.Optional; 14 | 15 | @NullMarked 16 | public class OffsetterServerEntityMetadata extends PacketOffsetter { 17 | public OffsetterServerEntityMetadata() { 18 | super(WrapperPlayServerEntityMetadata.class, PacketType.Play.Server.ENTITY_METADATA); 19 | } 20 | 21 | @Override 22 | public void offset(WrapperPlayServerEntityMetadata packet, FixedOffset offset, User user) { 23 | for (EntityData data : packet.getEntityMetadata()) { 24 | Object value = data.getValue(); 25 | if (value == null) continue; 26 | if (value instanceof Optional optional) { 27 | if (optional.isPresent()) { 28 | data.setValue(Optional.of(applyOffsetToEntityMeta(optional.get(), offset))); 29 | } 30 | } else { 31 | data.setValue(applyOffsetToEntityMeta(value, offset)); 32 | } 33 | } 34 | } 35 | 36 | private static Object applyOffsetToEntityMeta(Object object, FixedOffset offset) { 37 | /* 38 | * Warning: Beware of adding Vector3d/Vector3f here as they are also used in display entity translation and 39 | * scale values (and probably other ones that should not be offsetted) 40 | */ 41 | if (object instanceof Vector3i blockPosition) { 42 | return apply(blockPosition, offset); 43 | } 44 | if (object instanceof ItemStack) { 45 | ItemStack modifiedItemStack = applyItemStack((ItemStack) object, offset); 46 | if (modifiedItemStack != null) { 47 | return modifiedItemStack; 48 | } else { 49 | return object; 50 | } 51 | } 52 | return object; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/config/ConfigVersion.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import org.jspecify.annotations.NullMarked; 5 | 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | 9 | @NullMarked 10 | public class ConfigVersion { 11 | public static final int CURRENT = 6; 12 | public static final int ASSUMED_VERSION_IF_MISSING = 4; 13 | 14 | /** 15 | * @return true if after running this function, it is safe to write to the configPath; false if something went 16 | * wrong and we should avoid writing 17 | */ 18 | static boolean onLoadBaseConfig(Path configPath, CoordinateOffsetConfigBase config) { 19 | if (config.configVersion == null) { 20 | config.configVersion = ASSUMED_VERSION_IF_MISSING; 21 | } 22 | 23 | if (config.configVersion < CURRENT) { 24 | // Outdated config; Make backup of previous config before allowing any file writes 25 | try { 26 | Path dst = getBackupPath(configPath, config.configVersion); 27 | CoordinateOffsetCore.get().getLogger().info("Detected new configuration version " + CURRENT + 28 | "; backing up " + configPath.getFileName() + " to " + dst.getFileName()); 29 | Files.copy(configPath, dst); 30 | } catch (Exception e) { 31 | CoordinateOffsetCore.get().getLogger().severe("Failed to back up CoordinateOffset config; leaving file untouched"); 32 | return false; 33 | } 34 | } 35 | 36 | if (config.configVersion > CURRENT) { 37 | CoordinateOffsetCore.get().getLogger().warning("Found config version " + config.configVersion + 38 | " which is higher than the version of the plugin; I'll try my best to interpret it but I won't try to" + 39 | " upgrade it to the latest config standard."); 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | static Path getBackupPath(Path configPath, int version) { 47 | String newName; 48 | String fileName = configPath.getFileName().toString(); 49 | int dotIndex = fileName.lastIndexOf('.'); 50 | if (dotIndex == -1) { 51 | newName = "v" + version + ".old." + fileName; 52 | } else { 53 | newName = fileName.substring(0, dotIndex) + ".v" + version + ".old" + fileName.substring(dotIndex); 54 | } 55 | return configPath.resolveSibling(newName); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/wrapper/WrapperPlayServerNamedSoundEffect.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.wrapper; 2 | 3 | import com.github.retrooper.packetevents.event.PacketSendEvent; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Server; 5 | import com.github.retrooper.packetevents.protocol.sound.SoundCategory; 6 | import com.github.retrooper.packetevents.resources.ResourceLocation; 7 | import com.github.retrooper.packetevents.util.Vector3i; 8 | import com.github.retrooper.packetevents.wrapper.PacketWrapper; 9 | import org.jspecify.annotations.NonNull; 10 | 11 | @SuppressWarnings("unused") // Constructors are called reflectively 12 | public class WrapperPlayServerNamedSoundEffect extends PacketWrapper<@NonNull WrapperPlayServerNamedSoundEffect> { 13 | private ResourceLocation soundName; 14 | private SoundCategory soundCategory; 15 | private Vector3i effectPosition; 16 | private byte[] remainingData; // Lazily ignoring everything after position, since only position matters for this plugin 17 | 18 | public WrapperPlayServerNamedSoundEffect(PacketSendEvent event) { 19 | super(event); 20 | } 21 | 22 | public WrapperPlayServerNamedSoundEffect(ResourceLocation soundName, SoundCategory soundCategory, 23 | Vector3i effectPosition, byte[] remainingData) { 24 | // Removed around 1.19.2ish 25 | super(Server.NAMED_SOUND_EFFECT); 26 | this.soundName = soundName; 27 | this.soundCategory = soundCategory; 28 | this.effectPosition = effectPosition; 29 | this.remainingData = remainingData; 30 | } 31 | 32 | public void read() { 33 | // CO: Heavily based on PacketEvents builtin WrapperPlayServerSoundEffect 34 | this.soundName = this.readIdentifier(); 35 | this.soundCategory = SoundCategory.fromId(readVarInt()); 36 | effectPosition = new Vector3i(readInt(), readInt(), readInt()); 37 | this.remainingData = this.readRemainingBytes(); 38 | } 39 | 40 | public void write() { 41 | this.writeIdentifier(this.soundName); 42 | this.writeVarInt(soundCategory.ordinal()); 43 | writeInt(effectPosition.x); 44 | writeInt(effectPosition.y); 45 | writeInt(effectPosition.z); 46 | this.writeBytes(this.remainingData); 47 | } 48 | 49 | public Vector3i getEffectPosition() { 50 | return effectPosition; 51 | } 52 | 53 | public void setEffectPosition(Vector3i effectPosition) { 54 | this.effectPosition = effectPosition; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/server/OffsetterServerParticle.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.server; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.particle.data.ParticleItemStackData; 6 | import com.github.retrooper.packetevents.protocol.particle.data.ParticleTrailData; 7 | import com.github.retrooper.packetevents.protocol.particle.data.ParticleVibrationData; 8 | import com.github.retrooper.packetevents.protocol.player.User; 9 | import com.github.retrooper.packetevents.util.Vector3i; 10 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerParticle; 11 | import com.jtprince.coordinateoffset.FixedOffset; 12 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 13 | import org.jspecify.annotations.NullMarked; 14 | 15 | @NullMarked 16 | public class OffsetterServerParticle extends PacketOffsetter { 17 | public OffsetterServerParticle() { 18 | super(WrapperPlayServerParticle.class, PacketType.Play.Server.PARTICLE); 19 | } 20 | 21 | @Override 22 | public void offset(WrapperPlayServerParticle packet, FixedOffset offset, User user) { 23 | packet.setPosition(apply(packet.getPosition(), offset)); 24 | 25 | if (packet.getParticle().getData() instanceof ParticleVibrationData vibrationData) { 26 | // startingPosition was only part of packet data up to 1.19.4. PE reports >1.19.4 with a zero vector. 27 | // Make sure not to offset this zero vector, or it will leak offsets. 28 | if (!vibrationData.getStartingPosition().equals(Vector3i.zero())) { 29 | vibrationData.setStartingPosition(apply(vibrationData.getStartingPosition(), offset)); 30 | } 31 | 32 | if (vibrationData.getBlockPosition().isPresent()) { 33 | vibrationData.setBlockPosition(apply(vibrationData.getBlockPosition().get(), offset)); 34 | } 35 | } 36 | 37 | if (packet.getParticle().getData() instanceof ParticleItemStackData itemStackData) { 38 | ItemStack modifiedItemStack = applyItemStack(itemStackData.getItemStack(), offset); 39 | if (modifiedItemStack != null) { 40 | itemStackData.setItemStack(modifiedItemStack); 41 | } 42 | } 43 | 44 | if (packet.getParticle().getData() instanceof ParticleTrailData trailData) { 45 | trailData.setTarget(apply(trailData.getTarget(), offset)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/config/OffsetMultipleConfig.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import de.exlll.configlib.Configuration; 5 | import org.jspecify.annotations.NullMarked; 6 | import org.jspecify.annotations.Nullable; 7 | 8 | @NullMarked 9 | @Configuration 10 | public class OffsetMultipleConfig { 11 | public static final OffsetMultipleConfig AUTO = new OffsetMultipleConfig(null); 12 | 13 | private final @Nullable Integer valueConfigured; // null for "auto" 14 | 15 | private OffsetMultipleConfig(@Nullable Integer valueConfigured) { 16 | this.valueConfigured = valueConfigured; 17 | } 18 | 19 | public int getMultiple() { 20 | if (valueConfigured == null) { 21 | // "auto" - use server minimum 22 | return CoordinateOffsetCore.get().getAdapter().getMinimumOffsetMultiple(); 23 | } else { 24 | return valueConfigured; 25 | } 26 | } 27 | 28 | public static class Serializer implements de.exlll.configlib.Serializer { 29 | @Override 30 | public Object serialize(OffsetMultipleConfig config) { 31 | if (config.valueConfigured == null) return "auto"; 32 | return config.valueConfigured.longValue(); 33 | } 34 | 35 | @Override 36 | public OffsetMultipleConfig deserialize(Object serialized) { 37 | if (serialized.toString().equals("auto")) return new OffsetMultipleConfig(null); 38 | 39 | if (serialized instanceof Number n) { 40 | int i = n.intValue(); 41 | 42 | if (i <= 0) { 43 | throw new IllegalArgumentException(i + " is not a positive integer"); 44 | } 45 | 46 | if ((i & (i - 1)) != 0) { 47 | throw new IllegalArgumentException(i + " is not a power of 2 (16, 32, 64, etc.)"); 48 | } 49 | 50 | if (i < 16) { 51 | throw new IllegalArgumentException(i + " is too small for a chunk size of 16x16 blocks"); 52 | } 53 | 54 | return new OffsetMultipleConfig(n.intValue()); 55 | } 56 | 57 | throw new IllegalArgumentException("Could not parse value " + serialized + " as an integer or \"auto\"."); 58 | } 59 | } 60 | 61 | public String getMetricsString() { 62 | if (valueConfigured == null) { 63 | return "auto (" + CoordinateOffsetCore.get().getAdapter().getMinimumOffsetMultiple() + ")"; 64 | } else { 65 | return valueConfigured.toString(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/CoordinateOffsetPermission.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | import java.util.Arrays; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | 9 | @NullMarked 10 | public enum CoordinateOffsetPermission { 11 | ALL("coordinateoffset.*", 12 | "Players with this permission will have all other permissions granted."), 13 | BYPASS("coordinateoffset.bypass", 14 | "Players with this permission will never have their coordinates offsetted."), 15 | QUERY_SELF("coordinateoffset.query", 16 | "Allows use of commands /offset and /offset query, which prints a player's own offset and coordinates."), 17 | QUERY_OTHERS("coordinateoffset.query.others", 18 | "Allows use of command /offset query , which prints another player's offset and coordinates."), 19 | QUERY_VERBOSE("coordinateoffset.query.verbose", 20 | "Players with this permission see the name of the offset provider which generated an offset in /offset query."), 21 | RELOAD("coordinateoffset.reload", 22 | "Players with this permission can use the /offset reload command to reload the plugin."), 23 | REGENERATE_SELF("coordinateoffset.regenerate", 24 | "Allows use of the command /offset regenerate, which regenerates the player's offset using configured offset providers."), 25 | REGENERATE_OTHERS("coordinateoffset.regenerate.others", 26 | "Allows use of the command /offset regenerate , which regenerates another player's offset using configured offset providers."), 27 | SET_SELF("coordinateoffset.set", 28 | "Allows use of the command /offset set, which sets the player's offset to a specific value."), 29 | SET_OTHERS("coordinateoffset.set.others", 30 | "Allows use of the command /offset set , which sets another player's offset to a specific value."); 31 | 32 | public final String node; 33 | public final String description; 34 | 35 | CoordinateOffsetPermission(String node, String description) { 36 | // All permissions default to Operators only 37 | this.node = node; 38 | this.description = description; 39 | } 40 | 41 | public Set getChildren() { 42 | return switch (this) { 43 | case ALL -> Arrays.stream(values()).filter(p -> p != ALL).collect(Collectors.toSet()); 44 | case QUERY_OTHERS -> Set.of(QUERY_SELF); 45 | case REGENERATE_OTHERS -> Set.of(REGENERATE_SELF); 46 | case SET_OTHERS -> Set.of(SET_SELF); 47 | default -> Set.of(); 48 | }; 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return node; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/adapter/OffsetLocation.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.adapter; 2 | 3 | import com.jtprince.coordinateoffset.FixedOffset; 4 | import org.checkerframework.dataflow.qual.Pure; 5 | import org.jspecify.annotations.NullMarked; 6 | import org.jspecify.annotations.Nullable; 7 | 8 | /** 9 | * Adapter interface representing a location in a Minecraft world with X, Y, Z coordinates. 10 | * 11 | *

The name does NOT imply that an offset was already applied. It refers to any Location object in the different 12 | * platforms.

13 | */ 14 | @NullMarked 15 | public interface OffsetLocation { 16 | OffsetWorld getWorld(); 17 | double getX(); 18 | double getY(); 19 | double getZ(); 20 | 21 | /** @deprecated Use {@link #getWorld()} instead. */ 22 | @Deprecated 23 | @Nullable default String getWorldName() { 24 | return getWorld().getName(); 25 | } 26 | 27 | /** 28 | * Apply an offset to this location. This location should refer to coordinates in "real" space, i.e. not yet 29 | * transformed by an offset. 30 | * 31 | *

Be careful not to use the returned Location for anything internal to the server, such as getting the Block 32 | * at that Location. The returned Location is primarily intended to be sent to a Player who this Offset is applied 33 | * to, such as in a message.

34 | * 35 | * @param offset The offset to apply. 36 | * @return A new {@code OffsetLocation} with the offset applied and all other data matching the original. 37 | */ 38 | @Pure 39 | OffsetLocation apply(FixedOffset offset); 40 | 41 | /** 42 | * Reverses the application of the given offset from this location, effectively restoring the "real" coordinates 43 | * before the offset was applied. This location should refer to coordinates in a player's offsetted space. 44 | * 45 | * @param offset The offset to reverse. 46 | * @return A new {@code OffsetLocation} with the offset unapplied and all other data matching the original. 47 | */ 48 | @Pure 49 | OffsetLocation unapply(FixedOffset offset); 50 | 51 | /** 52 | * Get the distance between two locations. 53 | * @param other Second location to compare against. 54 | * @return A distance in blocks, or null if the locations are in different worlds. 55 | */ 56 | default @Nullable Double getDistance(OffsetLocation other) { 57 | if (!getWorld().equals(other.getWorld())) { 58 | return null; 59 | } 60 | return Math.sqrt(Math.pow(getX() - other.getX(), 2) + Math.pow(getY() - other.getY(), 2) + Math.pow(getZ() - other.getZ(), 2)); 61 | } 62 | 63 | /** 64 | * Get the underlying platform-specific location object, for example a Bukkit Location. 65 | */ 66 | Object getPlatformLocationObject(); 67 | } 68 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/client/OffsetterClientClickWindow.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter.client; 2 | 3 | import com.github.retrooper.packetevents.protocol.item.ItemStack; 4 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientClickWindow; 7 | import com.jtprince.coordinateoffset.FixedOffset; 8 | import com.jtprince.coordinateoffset.offsetter.PacketOffsetter; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.stream.Collectors; 14 | 15 | @NullMarked 16 | public class OffsetterClientClickWindow extends PacketOffsetter { 17 | public OffsetterClientClickWindow() { 18 | super(WrapperPlayClientClickWindow.class, PacketType.Play.Client.CLICK_WINDOW); 19 | } 20 | 21 | @Override 22 | public void offset(WrapperPlayClientClickWindow packet, FixedOffset offset, User user) { 23 | if (packet.getSlots().isPresent()) { 24 | Map clientItems = packet.getSlots().get(); 25 | Map serverItems = clientItems.entrySet().stream() 26 | .collect(Collectors.toMap(Map.Entry::getKey, v -> { 27 | ItemStack modifiedItem = unapplyItemStack(v.getValue(), offset); 28 | if (modifiedItem != null) { 29 | return modifiedItem; 30 | } else { 31 | return v.getValue(); 32 | } 33 | })); 34 | packet.setSlots(Optional.of(serverItems)); 35 | } 36 | 37 | /* 38 | * TODO: In 1.21.5, this packet was changed to contain a hashed ItemStack instead of the 39 | * full ItemStack component data. 40 | * More info: https://minecraft.wiki/w/Java_Edition_protocol/Slot_data#Hashed_Format 41 | * It is impossible to get the ItemStack back from the hash, so we cannot simply unapply 42 | * an offset here and make the offset fully transparent to the server. 43 | * The result is that the server thinks the client has the wrong ItemStack. The server 44 | * responds with a SetCursorItem packet with the correct ItemStack. This results in the 45 | * client seeing a ghost item on the cursor. 46 | * For now, the null check below ensures that this only affects items that need to be 47 | * offsetted (i.e. lodestone compasses). 48 | * The easiest way to reproduce this is to put a lodestone compass on the cursor and 49 | * click-drag in the inventory window. 50 | */ 51 | ItemStack modifiedCarriedItemStack = unapplyItemStack(packet.getCarriedItemStack(), offset); 52 | if (modifiedCarriedItemStack != null) { 53 | packet.setCarriedItemStack(modifiedCarriedItemStack); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /api/src/test/java/com/jtprince/coordinateoffset/TestOffset.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public class TestOffset { 7 | @Test 8 | void testNonAlignedThrowsException() { 9 | Assertions.assertThrows(IllegalArgumentException.class, () -> Offset.fixed(-64, 24)); 10 | Assertions.assertThrows(IllegalArgumentException.class, () -> Offset.fixed(100, 32)); 11 | Assertions.assertThrows(IllegalArgumentException.class, () -> Offset.fixed(-1000, -100)); 12 | } 13 | 14 | @Test 15 | void testAlignComponent() { 16 | Assertions.assertEquals(0, Offset.alignComponent(0, 0)); 17 | Assertions.assertEquals(16, Offset.alignComponent(15, 0)); // 0, 16, 32... 18 | 19 | Assertions.assertEquals(0, Offset.alignComponent(15, 1)); // 0, 32, 64... 20 | Assertions.assertEquals(0, Offset.alignComponent(-16, 1)); // -32, 0, 32... 21 | Assertions.assertEquals(32, Offset.alignComponent(16, 1)); // 0, 32, 64... 22 | 23 | Assertions.assertEquals(0, Offset.alignComponent(1, 3)); // 0, 128, 256... 24 | Assertions.assertEquals(0, Offset.alignComponent(63, 3)); // 0, 128, 256... 25 | Assertions.assertEquals(128, Offset.alignComponent(64, 3)); // 0, 128, 256... 26 | Assertions.assertEquals(128, Offset.alignComponent(65, 3)); // 0, 128, 256... 27 | Assertions.assertEquals(-128, Offset.alignComponent(-65, 3)); // -128, 0, 128... 28 | Assertions.assertEquals(0, Offset.alignComponent(-64, 3)); // -128, 0, 128... 29 | Assertions.assertEquals(0, Offset.alignComponent(-63, 3)); // -128, 0, 128... 30 | 31 | Assertions.assertEquals(16, Offset.alignComponent(17, -1)); // same as 0 32 | } 33 | 34 | @Test 35 | void testAlignToMultipleChunks() { 36 | // Align to 16 chunks (nearest 256 blocks) 37 | Assertions.assertEquals(new ScalableOffset(0, 0), Offset.align(-65, 65, 4)); 38 | Assertions.assertEquals(new ScalableOffset(0, 0), Offset.align(-128, 127, 4)); 39 | Assertions.assertEquals(new ScalableOffset(-256, 256), Offset.align(-129, 128, 4)); 40 | } 41 | 42 | @Test 43 | void testChunkValues() { 44 | Assertions.assertEquals(5, Offset.fixed(80, 0).chunkX()); 45 | Assertions.assertEquals(-3, Offset.fixed(0, -48).chunkZ()); 46 | } 47 | 48 | @Test 49 | void testScale() { 50 | Assertions.assertEquals(new FixedOffset(64, -128), Offset.scalable(16, -32).scaleDownBy(0.25)); 51 | Assertions.assertEquals(new FixedOffset(-48, -128), Offset.scalable(-48, -128).scaleDownBy(1.0)); 52 | 53 | // Clean scaling (dividing inputs by 8 still results in a multiple of 16) 54 | Assertions.assertEquals(new FixedOffset(96, -176), Offset.scalable(768, -1408).scaleDownBy(8.0)); 55 | // Truncated scaling (dividing inputs by 8 does NOT result in a multiple of 16, so they must be aligned) 56 | Assertions.assertEquals(new FixedOffset(-48, 16), Offset.scalable(-432, 144).scaleDownBy(8.0)); 57 | Assertions.assertEquals(new FixedOffset(-64, 32), Offset.scalable(-464, 192).scaleDownBy(8.0)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/FixedOffset.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset; 2 | 3 | import com.jtprince.coordinateoffset.adapter.OffsetLocation; 4 | import com.jtprince.coordinateoffset.api.CoordinateOffset; 5 | import org.jspecify.annotations.NullMarked; 6 | 7 | /** 8 | * Amount by which a player's clientside X and Z coordinates will appear shifted compared to their real position in a 9 | * world. 10 | * 11 | *

Fixed offsets are absolute in any coordinate space. For example, a fixed offset of (800, 800) 12 | * will always subtract 800 from the player's coordinates. This may break coordinate-based alignment between 13 | * nether portals and make it possible to reverse-engineer offsets through clever use of nether portals.

14 | * 15 | *

A fixed offset is fully resolved and may be applied to coordinates directly.

16 | * 17 | * @param x X offset value in blocks. Will be subtracted from the player's real X coordinate. 18 | * @param z Z offset value in blocks. Will be subtracted from the player's real Z coordinate. 19 | */ 20 | @NullMarked 21 | public record FixedOffset(int x, int z) implements Offset { 22 | public FixedOffset { 23 | if (x % 16 != 0) { 24 | throw new IllegalArgumentException("Offset x=" + x + " is not chunk-aligned! (must be a multiple of 16)"); 25 | } 26 | if (z % 16 != 0) { 27 | throw new IllegalArgumentException("Offset z=" + z + " is not chunk-aligned! (must be a multiple of 16)"); 28 | } 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "[x=" + x + ", z=" + z + "]"; 34 | } 35 | 36 | @Override 37 | public FixedOffset negate() { 38 | return new FixedOffset(-x, -z); 39 | } 40 | 41 | @Override 42 | public boolean isZero() { 43 | return x == 0 && z == 0; 44 | } 45 | 46 | public int chunkX() { 47 | return x >> 4; 48 | } 49 | 50 | public int chunkZ() { 51 | return z >> 4; 52 | } 53 | 54 | @Override 55 | public T apply(T location) throws ClassCastException { 56 | OffsetLocation l; 57 | if (location instanceof OffsetLocation) { 58 | l = (OffsetLocation) location; 59 | } else { 60 | l = CoordinateOffset.api().adaptLocation(location); 61 | } 62 | 63 | OffsetLocation applied = l.apply(this); 64 | 65 | if (location instanceof OffsetLocation) { 66 | return (T) applied; 67 | } else { 68 | return (T) applied.getPlatformLocationObject(); 69 | } 70 | } 71 | 72 | @Override 73 | public T unapply(T location) throws ClassCastException { 74 | OffsetLocation l; 75 | if (location instanceof OffsetLocation) { 76 | l = (OffsetLocation) location; 77 | } else { 78 | l = CoordinateOffset.api().adaptLocation(location); 79 | } 80 | 81 | OffsetLocation unapplied = l.unapply(this); 82 | 83 | if (location instanceof OffsetLocation) { 84 | return (T) unapplied; 85 | } else { 86 | return (T) unapplied.getPlatformLocationObject(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/Updating.md: -------------------------------------------------------------------------------- 1 | Updating 2 | ======== 3 | 4 | This is a guide for porting CoordinateOffset to a new Minecraft version. 5 | 6 | This plugin is heavily dependent on the Minecraft protocol and its details. Luckily, PacketEvents abstracts away most of 7 | the changes made to the protocol in each new Minecraft version. The important changes to be made in CoordinateOffset 8 | are: 9 | * When a new packet is added to support a new feature 10 | * When an existing packet involving coordinates is changed 11 | 12 | All interaction with PacketEvents is contained within the `core` module in the package 13 | `com.jtprince.coordinateoffset.offsetter`. Logic for how to apply an offset to each packet type are contained within 14 | the subpackages `client` and `server` for packets sent by the client and server respectively. 15 | 16 | PacketEvents does not provide wrappers for every packet type, and the provided wrappers for some packet types do not 17 | work with every protocol version. The preferred way to handle these packets is to define our own wrapper in the 18 | `wrappers` subpackage, and potentially pull request it to PacketEvents if it would be of widespread utility. 19 | 20 | Step 0: Wait 21 | ------------ 22 | Wait for the dependencies to be updated. That typically means PacketEvents. It must have at least released a development 23 | build that works with the new Minecraft version. 24 | 25 | When dependencies are fully updated, bump any dependency versions in `gradle/libs.versions.toml`. You 26 | should not need to bump the API version or Paper version in `gradle.properties`, and if you do, be sure to maintain 27 | backwards compatibility with previous server versions. 28 | 29 | Step 1: Fix Existing Offsetters 30 | ------------------------------- 31 | Starting at the top of the list in [Testing.md](./Testing.md), execute each existing test in the new version. If a test 32 | case does not work or results in a console error, investigate changes in the protocol related to that feature. Some 33 | resources which might be helpful: 34 | * [Minecraft Wiki protocol documentation](https://minecraft.wiki/w/Java_Edition_protocol) 35 | * [PacketEvents Git history](https://github.com/retrooper/packetevents/commits/2.0/) 36 | * [Prismarine minecraft-data protocol docs](https://prismarinejs.github.io/minecraft-data/protocol/) 37 | 38 | The easiest way to debug wrappers from PacketEvents is to use a debugger in IntelliJ or similar. There are plenty of 39 | guides online for how to attach a debugger to the running Minecraft server and place breakpoints within plugin code. 40 | 41 | PacketEvents should provide backwards compatibility for most changes, but if needed, use version checks. 42 | 43 | Step 2: Add New Packets 44 | ----------------------- 45 | New protocol versions likely added some new packet types. It is critical that every packet containing an absolute 46 | coordinate is registered in an Offsetter so that the plugin can obfuscate that coordinate. The easiest way to do this 47 | is to look at the Minecraft changelog, and try out all of the new features in that version to make sure that none of 48 | them are broken. Do this while confirming that your coordinates displayed in F3 are **not** the world's real 49 | coordinates. 50 | 51 | Step 3: Testing 52 | --------------- 53 | Now it's time to test everything. I summarized everything that tends to exhibit problems in [Testing.md](Testing.md). 54 | It's best to run through the list on some older versions if possible. 55 | 56 | Step 4: Done! 57 | ------------- 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | packages: write 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v4 17 | - name: Setup Java 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: '21' 22 | - name: Setup Gradle 23 | uses: gradle/actions/setup-gradle@v5 24 | - name: Extract version from tag 25 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 26 | - name: Extract changelog 27 | # Find the section titled with the release version in CHANGELOG.md and extract it. 28 | run: | 29 | VERSION=$RELEASE_VERSION 30 | awk "/^# $VERSION\$/{flag=1; next} /^# /{flag=0} flag" CHANGELOG.md > release_changelog.md 31 | cat release_changelog.md 32 | if [ ! -s release_changelog.md ]; then 33 | echo "ERROR: CHANGELOG.md has no entry for $RELEASE_VERSION" 34 | exit 1 35 | fi 36 | echo "RELEASE_CHANGELOG<> $GITHUB_ENV 37 | cat release_changelog.md >> $GITHUB_ENV 38 | echo "EOF" >> $GITHUB_ENV 39 | - name: Extract supported Minecraft versions 40 | # Extract the supported Minecraft versions from the extracted changelog. 41 | # Changelog must include a line right under the title between asterisks and with a colon, like: 42 | # *Supported Minecraft servers: Paper 1.21.4-1.21.10* 43 | run: | 44 | SUPPORTED_MC_LINE=$(echo "$RELEASE_CHANGELOG" | awk '/^\*/{print; exit}') 45 | # MC_SERVERS="Paper 1.21.4-1.21.10" 46 | MC_SERVERS=$(echo "$SUPPORTED_MC_LINE" | sed -E 's/^[*][^:]+:[[:space:]]*(.*)[*]$/\1/') 47 | if [ -z "$MC_SERVERS" ]; then 48 | echo "ERROR: Could not extract supported Minecraft servers string from CHANGELOG.md" 49 | exit 1 50 | fi 51 | # MC_VERSIONS="1.21.4-1.21.10" 52 | MC_VERSIONS=$(printf '%s\n' "$MC_SERVERS" | sed 's/.* //') 53 | echo "SUPPORTED_MC_SERVERS=$MC_SERVERS" >> $GITHUB_ENV 54 | echo "SUPPORTED_MC_VERSIONS=$MC_VERSIONS" >> $GITHUB_ENV 55 | echo "Minecraft versions extracted: $SUPPORTED_MC_SERVERS" 56 | - name: Build and test 57 | run: ./gradlew build 58 | - name: Create GitHub release 59 | uses: softprops/action-gh-release@v2 60 | with: 61 | tag_name: ${{ env.RELEASE_VERSION }} 62 | name: ${{ env.RELEASE_VERSION }} (${{ env.SUPPORTED_MC_SERVERS }}) 63 | body: ${{ env.RELEASE_CHANGELOG }} 64 | draft: false 65 | prerelease: false 66 | make_latest: true 67 | files: paper/build/libs/*.jar 68 | - name: Create Hangar release 69 | # TODO: Re-enable configuration cache, https://github.com/HangarMC/hangar-publish-plugin/issues/40 70 | run: ./gradlew publishPluginPublicationToHangar --no-configuration-cache 71 | env: 72 | HANGAR_TOKEN: ${{ secrets.HANGAR_TOKEN }} 73 | - name: Create Modrinth release 74 | # TODO: Re-enable configuration cache, https://github.com/HangarMC/hangar-publish-plugin/issues/40 75 | run: ./gradlew modrinth --no-configuration-cache 76 | env: 77 | MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} 78 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/offsetter/OffsettedColumn.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.offsetter; 2 | 3 | import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; 4 | import com.github.retrooper.packetevents.protocol.player.ClientVersion; 5 | import com.github.retrooper.packetevents.protocol.player.User; 6 | import com.github.retrooper.packetevents.protocol.world.chunk.BaseChunk; 7 | import com.github.retrooper.packetevents.protocol.world.chunk.Column; 8 | import com.github.retrooper.packetevents.protocol.world.chunk.HeightmapType; 9 | import com.github.retrooper.packetevents.protocol.world.chunk.TileEntity; 10 | import com.jtprince.coordinateoffset.FixedOffset; 11 | import org.jspecify.annotations.NullMarked; 12 | 13 | import java.util.Map; 14 | 15 | /** 16 | * Wrapper for a PacketEvents Column (vertical slice of chunk sections) that returns offsetted 17 | * coordinates. (This was the cleanest way to wrap Column since it is not an interface) 18 | */ 19 | @NullMarked 20 | public class OffsettedColumn extends Column { 21 | private final Column inner; 22 | private final FixedOffset offset; 23 | private final User user; 24 | 25 | private static final BaseChunk[] emptyChunkList = {}; 26 | 27 | public OffsettedColumn(Column column, FixedOffset offset, User user) { 28 | super(0, 0, false, emptyChunkList, null); 29 | this.inner = column; 30 | this.offset = offset; 31 | this.user = user; 32 | } 33 | 34 | @Override 35 | public int getX() { 36 | return inner.getX() - offset.chunkX(); 37 | } 38 | 39 | @Override 40 | public int getZ() { 41 | return inner.getZ() - offset.chunkZ(); 42 | } 43 | 44 | @Override 45 | public boolean isFullChunk() { 46 | return inner.isFullChunk(); 47 | } 48 | 49 | @Override 50 | public BaseChunk[] getChunks() { 51 | return inner.getChunks(); 52 | } 53 | 54 | @Override 55 | public TileEntity[] getTileEntities() { 56 | // Tile entities are only absolutely positioned up to 1.17.1 57 | if (user.getClientVersion().isOlderThan(ClientVersion.V_1_18)) { 58 | TileEntity[] entities = inner.getTileEntities(); 59 | for (TileEntity entity : entities) { 60 | entity.setX(entity.getX() - offset.x()); 61 | entity.setZ(entity.getZ() - offset.z()); 62 | } 63 | return entities; 64 | } else { 65 | return inner.getTileEntities(); 66 | } 67 | } 68 | 69 | @Override 70 | public boolean hasHeightMaps() { 71 | return inner.hasHeightMaps(); 72 | } 73 | 74 | /** 75 | * @deprecated in 1.21.5, use {@link #getHeightmaps()} instead 76 | */ 77 | @SuppressWarnings("deprecation") 78 | @Override 79 | public NBTCompound getHeightMaps() { 80 | return inner.getHeightMaps(); 81 | } 82 | 83 | /** 84 | * Only used in 1.21.5+. 85 | */ 86 | @Override 87 | public Map getHeightmaps() { 88 | return inner.getHeightmaps(); 89 | } 90 | 91 | @Override 92 | public boolean hasBiomeData() { 93 | return inner.hasBiomeData(); 94 | } 95 | 96 | @Override 97 | public int[] getBiomeDataInts() { 98 | return inner.getBiomeDataInts(); 99 | } 100 | 101 | @Override 102 | public byte[] getBiomeDataBytes() { 103 | return inner.getBiomeDataBytes(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/provider/OffsetProviderContext.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider; 2 | 3 | import com.jtprince.coordinateoffset.Offset; 4 | import com.jtprince.coordinateoffset.adapter.OffsetLocation; 5 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | /** 10 | * Container for relevant information when calculating a new {@link Offset}. 11 | * 12 | * @param player The Player that will receive this new Offset. 13 | * @param previousLocation The previous location of the Player before this Offset begins to take effect. If the player 14 | * is joining the server or the death location is unknown for a respawn, this will be 15 | * null. 16 | * @param playerLocation The real location that the Player will be at as soon as this Offset begins to take effect. 17 | * Note that this may be different from player.getLocation() because the Provider is 18 | * called before a teleport completes. 19 | * @param reason The reason that a new Offset is being requested. 20 | */ 21 | @NullMarked 22 | public record OffsetProviderContext( 23 | OffsetPlayer player, 24 | @Nullable OffsetLocation previousLocation, 25 | OffsetLocation playerLocation, 26 | @Nullable Offset previousOffset, 27 | ProvideReason reason 28 | ) { 29 | public enum ProvideReason { 30 | /** 31 | * A player's offset is being generated because they are joining the server. They may or may not have played 32 | * on the server previously. 33 | */ 34 | JOIN, 35 | 36 | /** 37 | * A player's offset is being generated because they died and are about to respawn. 38 | */ 39 | DEATH_RESPAWN, 40 | 41 | /** 42 | * A player's offset is being generated because they are changing worlds, either through a Nether portal, End 43 | * portal, or any teleport across worlds. 44 | */ 45 | WORLD_CHANGE, 46 | 47 | /** 48 | * A player's offset is being generated because they are teleporting within the same world. 49 | */ 50 | TELEPORT, 51 | 52 | /** 53 | * A player's offset is being regenerated immediately because someone executed a command (such as 54 | * /offset regenerate). 55 | */ 56 | COMMAND_REGENERATE, 57 | 58 | /** 59 | * A player's offset is being set immediately because someone executed a command (such as 60 | * /offset set). Note that offset providers are never called to provide an offset for this 61 | * reason since the offset is predetermined by the command. 62 | * 63 | * @see OffsetProvider#onOffsetSetByCommand 64 | */ 65 | COMMAND_SET, 66 | 67 | /** 68 | * A player's offset is being generated immediately because a third-party plugin requested it. 69 | */ 70 | PLUGIN_REGENERATE, 71 | 72 | /** 73 | * A player's offset is being set immediately because a third-party plugin called a method to set it. 74 | * Note that offset providers are never called to provide an offset for this reason since the offset is 75 | * predetermined by the plugin. 76 | */ 77 | PLUGIN_SET 78 | } 79 | 80 | /** 81 | * @deprecated Use {@link #playerLocation()} - {@link OffsetLocation#getWorld()} instead. 82 | */ 83 | @Deprecated 84 | public String worldName() { 85 | return playerLocation.getWorld().getName(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/CoordinateOffsetPaperPlugin.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.paper; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.CoordinateOffsetPermission; 5 | import com.jtprince.coordinateoffset.paper.adapter.PaperAdapter; 6 | import com.jtprince.coordinateoffset.paper.lib.org.geysermc.hurricane.CollisionFix; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.permissions.Permission; 9 | import org.bukkit.permissions.PermissionDefault; 10 | import org.bukkit.plugin.java.JavaPlugin; 11 | import org.jspecify.annotations.NullMarked; 12 | import org.jspecify.annotations.Nullable; 13 | 14 | import java.util.stream.Collectors; 15 | 16 | @NullMarked 17 | public final class CoordinateOffsetPaperPlugin extends JavaPlugin { 18 | private static @Nullable CoordinateOffsetPaperPlugin instance; 19 | private @Nullable PaperAdapter adapter; 20 | 21 | private @Nullable CoordinateOffsetCore core; 22 | private @Nullable WorldBorderObfuscator worldBorderObfuscator; 23 | private @Nullable PacketOffsetAdapter packetOffsetAdapter; 24 | private @Nullable CollisionFix collisionFix; 25 | 26 | @Override 27 | public void onEnable() { 28 | instance = this; 29 | 30 | adapter = new PaperAdapter(this); 31 | core = CoordinateOffsetCore.bootstrap(adapter); 32 | 33 | worldBorderObfuscator = new WorldBorderObfuscator(this); 34 | 35 | new BukkitEventListener(this, core, worldBorderObfuscator).registerListeners(); 36 | 37 | new PaperOffsetCommand(this, core).registerCommands(); 38 | 39 | packetOffsetAdapter = new PacketOffsetAdapter(this); 40 | packetOffsetAdapter.registerAdapters(); 41 | 42 | if (core.isDebugEnabled()) { 43 | new PacketEventSequencer(this).install(); 44 | } 45 | 46 | for (CoordinateOffsetPermission p : CoordinateOffsetPermission.values()) { 47 | Bukkit.getPluginManager().addPermission(new Permission(p.node, p.description, PermissionDefault.OP, 48 | p.getChildren().stream().collect(Collectors.toMap(p1 -> p1.node, p1 -> true)))); 49 | } 50 | 51 | if (core.getConfig().getFixCollisionBamboo() || core.getConfig().getFixCollisionDripstone()) { 52 | try { 53 | collisionFix = new CollisionFix(this, core.getConfig().getFixCollisionBamboo(), core.getConfig().getFixCollisionDripstone()); 54 | } catch (Exception e) { 55 | getLogger().severe("Failed to enable bamboo/dripstone collision fix: " + e.getMessage()); 56 | if (core.getConfig().getVerbose()) { 57 | //noinspection CallToPrintStackTrace 58 | e.printStackTrace(); 59 | } 60 | } 61 | } 62 | } 63 | 64 | void onAllPluginsEnabled() { 65 | CoordinateOffsetCore.get().signalCompletedLoading(); 66 | 67 | // bStats Metrics 68 | if (this.isEnabled()) { // signalCompletedLoading may have disabled the plugin 69 | MetricsWrapper.reportMetrics(this); 70 | } 71 | } 72 | 73 | @Override 74 | public void onDisable() { 75 | if (packetOffsetAdapter != null) { 76 | packetOffsetAdapter.onDisable(); 77 | packetOffsetAdapter = null; 78 | } 79 | } 80 | 81 | /** 82 | * Get the loaded instance of the CoordinateOffset plugin on the server. 83 | * @return The instance of the plugin, or null if the plugin is not loaded. 84 | */ 85 | @SuppressWarnings("unused") 86 | public static @Nullable CoordinateOffsetPaperPlugin getInstance() { 87 | return instance; 88 | } 89 | 90 | @Nullable WorldBorderObfuscator getWorldBorderObfuscator() { 91 | return worldBorderObfuscator; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/config/OffsetProviderOverrideConfigImpl.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 5 | import de.exlll.configlib.Configuration; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | import java.util.Objects; 10 | import java.util.SequencedMap; 11 | import java.util.UUID; 12 | 13 | @NullMarked 14 | @Configuration 15 | public class OffsetProviderOverrideConfigImpl implements OffsetProviderOverrideConfig { 16 | @SuppressWarnings("unused") 17 | OffsetProviderOverrideConfigImpl() {} // required by ConfigLib 18 | 19 | public OffsetProviderOverrideConfigImpl( 20 | String provider, 21 | @Nullable String world, 22 | @Nullable String permission, 23 | @Nullable String player 24 | ) { 25 | this.provider = provider; 26 | this.world = world; 27 | this.permission = permission; 28 | this.player = player; 29 | } 30 | 31 | private @Nullable String provider; // nullable so that we don't wipe half-configs; validated elsewhere 32 | @Override 33 | public OffsetProvider getOffsetProvider() { 34 | CoordinateOffsetProviderConfig providers = CoordinateOffsetCore.get().getProviderConfig(); 35 | return Objects.requireNonNull(providers.getAllOffsetProviderConfigs().get(Objects.requireNonNull(provider))); 36 | } 37 | 38 | private @Nullable String world; 39 | @Override 40 | public @Nullable String getWorld() { 41 | return world; 42 | } 43 | 44 | private @Nullable String permission; 45 | @Override 46 | public @Nullable String getPermission() { 47 | return permission; 48 | } 49 | 50 | private @Nullable String player; 51 | @Override 52 | public @Nullable String getPlayer() { 53 | return player; 54 | } 55 | 56 | @Deprecated 57 | private @Nullable UUID playerUuid; 58 | 59 | /** 60 | * Validate that the override is valid. Logs warning messages for any errors found. 61 | * 62 | * @param allProviders All known offset providers mapped by user-defined name. 63 | * @param logWarning If true, logs a warning to the logger indicating why. 64 | * 65 | * @return true if the configuration is acceptable to use, false if the configuration is invalid and should be 66 | * ignored. 67 | */ 68 | public boolean validate(SequencedMap allProviders, boolean logWarning) { 69 | if (playerUuid != null) { 70 | // Migrate old `playerUuid` field (UUID only) to new `player` field (supports player names and UUIDs) 71 | if (player == null) { 72 | player = playerUuid.toString(); 73 | } 74 | playerUuid = null; 75 | } 76 | 77 | StringBuilder b = new StringBuilder(); 78 | if (provider != null) b.append(" provider=").append(provider); 79 | if (world != null) b.append(" world=").append(world); 80 | if (permission != null) b.append(" permission=").append(permission); 81 | if (player != null) b.append(" player=").append(player); 82 | 83 | if (provider == null) { 84 | if (logWarning) { 85 | CoordinateOffsetCore.get().getLogger().warning("Ignoring an offset provider override with no provider set:" + b); 86 | } 87 | return false; 88 | } 89 | 90 | if (!allProviders.containsKey(provider)) { 91 | if (logWarning) { 92 | CoordinateOffsetCore.get().getLogger().warning("Ignoring an offset provider override with unknown provider:" + b); 93 | } 94 | return false; 95 | } 96 | 97 | return true; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/api/CoordinateOffsetAPIImpl.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.api; 2 | 3 | import com.jtprince.coordinateoffset.*; 4 | import com.jtprince.coordinateoffset.adapter.OffsetLocation; 5 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 6 | import com.jtprince.coordinateoffset.config.CoordinateOffsetConfig; 7 | import com.jtprince.coordinateoffset.config.CoordinateOffsetProviderConfig; 8 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 9 | import com.jtprince.coordinateoffset.provider.OffsetProviderConfig; 10 | import com.jtprince.coordinateoffset.provider.OffsetProviderContext; 11 | import org.jspecify.annotations.NullMarked; 12 | import org.jspecify.annotations.Nullable; 13 | 14 | import java.util.UUID; 15 | import java.util.function.Function; 16 | 17 | @NullMarked 18 | public class CoordinateOffsetAPIImpl implements CoordinateOffsetAPI { 19 | private final CoordinateOffsetCore core; 20 | public CoordinateOffsetAPIImpl(CoordinateOffsetCore core) { 21 | this.core = core; 22 | } 23 | 24 | @Override 25 | public FixedOffset getOffset(OffsetPlayer player) { 26 | return core.getOffsetHolder().getOffset(player).offset(); 27 | } 28 | 29 | @Override 30 | public OffsetData getOffsetData(OffsetPlayer player) { 31 | return core.getOffsetHolder().getOffset(player); 32 | } 33 | 34 | @Override 35 | public OffsetChange regenerateOffset(OffsetPlayer player) { 36 | CoordinateOffsetCore.get().getAdapter().assertMainThread("regenerateOffset"); // throws IllegalStateException 37 | 38 | OffsetChange result = core.getOffsetHolder().generateNextOffset( 39 | player, player.getLocation(), player.getLocation(), OffsetProviderContext.ProvideReason.PLUGIN_REGENERATE); 40 | if (result.offsetChanged()) { 41 | core.getAdapter().getOffsetSwapper().forceOffsetSwap(player); 42 | } 43 | return result; 44 | } 45 | 46 | @Override 47 | public OffsetChange setOffset(OffsetPlayer player, Offset offset) { 48 | CoordinateOffsetCore.get().getAdapter().assertMainThread("setOffset"); // throws IllegalStateException 49 | 50 | OffsetChange result = core.getOffsetHolder().setNextOffsetByPlugin(player, offset); 51 | if (result.offsetChanged()) { 52 | core.getAdapter().getOffsetSwapper().forceOffsetSwap(player); 53 | } 54 | return result; 55 | } 56 | 57 | @Override 58 | public @Nullable OffsetPlayer getPlayer(UUID playerUuid) { 59 | return core.getAdapter().getPlayer(playerUuid); 60 | } 61 | 62 | @Override 63 | public OffsetPlayer adaptPlayer(Object platformPlayerObject) throws ClassCastException { 64 | return core.getAdapter().adaptPlayer(platformPlayerObject); 65 | } 66 | 67 | @Override 68 | public OffsetLocation adaptLocation(Object platformLocationObject) throws ClassCastException { 69 | return core.getAdapter().adaptLocation(platformLocationObject); 70 | } 71 | 72 | @Override 73 | public CoordinateOffsetConfig getConfig() { 74 | return core.getConfig(); 75 | } 76 | 77 | @Override 78 | public CoordinateOffsetProviderConfig getProviderConfig() { 79 | return core.getProviderConfig(); 80 | } 81 | 82 | @Override 83 | public void registerOffsetProviderClass( 84 | String className, 85 | Function deserializeFunction 86 | ) { 87 | boolean isCore = false; // Only built-in providers are considered core. This is always false for API providers. 88 | core.getProviderRegistry().registerProviderClass(className, isCore, deserializeFunction); 89 | } 90 | 91 | public static void set(CoordinateOffsetAPI api) { 92 | // Helper function to allow setting the singleton from CoordinateOffsetCore, but not expose it as a public API 93 | CoordinateOffset.set(api); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/adapter/PaperAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.paper.adapter; 2 | 3 | import com.jtprince.coordinateoffset.adapter.CoordinateOffsetAdapter; 4 | import com.jtprince.coordinateoffset.paper.CoordinateOffsetPaperPlugin; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.Location; 7 | import org.bukkit.entity.Player; 8 | import org.jspecify.annotations.NullMarked; 9 | import org.jspecify.annotations.Nullable; 10 | 11 | import java.nio.file.Path; 12 | import java.util.UUID; 13 | import java.util.logging.Logger; 14 | 15 | @NullMarked 16 | public class PaperAdapter implements CoordinateOffsetAdapter { 17 | private final CoordinateOffsetPaperPlugin plugin; 18 | private final PaperPlayerOffsetPersistence offsetPersistence; 19 | private final PaperOffsetSwapper offsetSwapper; 20 | 21 | public PaperAdapter(CoordinateOffsetPaperPlugin plugin) { 22 | this.plugin = plugin; 23 | offsetPersistence = new PaperPlayerOffsetPersistence(plugin); 24 | offsetSwapper = new PaperOffsetSwapper(plugin); 25 | offsetSwapper.initialize(); 26 | } 27 | 28 | @Override 29 | public Path getConfigDir() { 30 | return plugin.getDataFolder().toPath(); 31 | } 32 | 33 | @Override 34 | public Logger getLogger() { 35 | return plugin.getLogger(); 36 | } 37 | 38 | @Override 39 | public @Nullable PaperOffsetPlayer getPlayer(UUID playerUuid) { 40 | Player bukkitPlayer = Bukkit.getPlayer(playerUuid); 41 | if (bukkitPlayer == null) { 42 | return null; 43 | } 44 | return new PaperOffsetPlayer(bukkitPlayer); 45 | } 46 | 47 | @Override 48 | public PaperOffsetPlayer adaptPlayer(Object platformPlayerObject) throws ClassCastException { 49 | if (!(platformPlayerObject instanceof Player bukkitPlayer)) { 50 | throw new ClassCastException("Object \"" + platformPlayerObject + "\" of class " + 51 | platformPlayerObject.getClass().getName() + " is not a valid Bukkit Player."); 52 | } 53 | return new PaperOffsetPlayer(bukkitPlayer); 54 | } 55 | 56 | @Override 57 | public PaperLocation adaptLocation(Object platformLocationObject) throws ClassCastException { 58 | if (!(platformLocationObject instanceof Location bukkitLocation)) { 59 | throw new ClassCastException("Object \"" + platformLocationObject + "\" of class " + 60 | platformLocationObject.getClass().getName() + " is not a valid Bukkit Location."); 61 | } 62 | return new PaperLocation(bukkitLocation); 63 | } 64 | 65 | @Override 66 | public PaperPlayerOffsetPersistence getPersistenceAdapter() { 67 | return offsetPersistence; 68 | } 69 | 70 | @Override 71 | public PaperOffsetSwapper getOffsetSwapper() { 72 | return offsetSwapper; 73 | } 74 | 75 | private static boolean printedDHSupportWarning = false; 76 | @Override 77 | public int getMinimumOffsetMultiple() { 78 | if (Bukkit.getPluginManager().getPlugin("DHSupport") != null) { 79 | if (!printedDHSupportWarning) { 80 | getLogger().info("DHSupport plugin is detected. Offset X and Z values must be divisible by 64 " + 81 | "blocks for offsets to be compatible with Distant Horizons LODs."); 82 | printedDHSupportWarning = true; 83 | } 84 | return 64; 85 | } 86 | 87 | return 16; 88 | } 89 | 90 | @Override 91 | public void assertMainThread(String methodName) throws IllegalStateException { 92 | if (!Bukkit.isPrimaryThread()) { 93 | throw new IllegalStateException("Method \"" + methodName + "\" must be called on the main server thread."); 94 | } 95 | } 96 | 97 | @Override 98 | public void shutdown() { 99 | Bukkit.getPluginManager().disablePlugin(plugin); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /api/src/main/java/com/jtprince/coordinateoffset/command/OffsetSetCommand.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.command; 2 | 3 | import com.jtprince.coordinateoffset.ScalableOffset; 4 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 5 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | import java.util.*; 10 | 11 | /** 12 | * Container for data and response methods for "/offset set" commands. 13 | */ 14 | @NullMarked 15 | public class OffsetSetCommand implements OffsetCommand { 16 | private final OffsetCommandSender commandSender; 17 | private final List targets; 18 | private final ScalableOffset offset; 19 | 20 | private @Nullable OffsetProvider notPersistentForProvider = null; 21 | private final Set notPersistentInProviderTargets = new HashSet<>(); 22 | private final Map scalingForTargets = new HashMap<>(); 23 | 24 | public OffsetSetCommand(OffsetCommandSender commandSender, List targets, ScalableOffset offset) { 25 | this.commandSender = commandSender; 26 | this.targets = List.copyOf(targets); 27 | this.offset = offset; 28 | } 29 | 30 | @Override 31 | public OffsetCommandSender getCommandSender() { 32 | return commandSender; 33 | } 34 | 35 | /** 36 | * Get an immutable list of the player(s) whose offset is being set by a command. 37 | */ 38 | public List getTargets() { 39 | return targets; 40 | } 41 | 42 | /** 43 | * Get the offset that was specified in the command. 44 | */ 45 | public ScalableOffset getOffset() { 46 | return offset; 47 | } 48 | 49 | /** 50 | * Print a warning message to the sender that the specified offset will be lost the next time an offset can change 51 | * for a given player. 52 | * 53 | * @param provider Offset provider that is not persisting the offset. The name is printed in the warning message. 54 | * @param target Player whose offset will be lost. 55 | */ 56 | public void warnOffsetIsNotPersistentInProvider(OffsetProvider provider, OffsetPlayer target) { 57 | notPersistentForProvider = provider; 58 | notPersistentInProviderTargets.add(target); 59 | } 60 | 61 | /** 62 | * Check if the offset provider affected by this command has called {@link #warnOffsetIsNotPersistentInProvider}. 63 | * 64 | * @param target Player whose offset is being set. 65 | * @return The offset provider that indicated it is not storing the specified offset, or null if no offset provider 66 | * indicated it is not storing the offset. 67 | */ 68 | public @Nullable OffsetProvider getOffsetIsNotPersistentForProvider(OffsetPlayer target) { 69 | if (notPersistentInProviderTargets.contains(target)) { 70 | return notPersistentForProvider; 71 | } else { 72 | return null; 73 | } 74 | } 75 | 76 | /** 77 | * Print a warning message to the sender that the specified offset will be scaled by the specified factor since the 78 | * targeted player is in a world with a non-unit scaling factor. 79 | * 80 | * @param target Player whose offset will be scaled. 81 | * @param scalingFactor The scaling factor (divisor) that will be applied to the offset. 82 | */ 83 | public void warnScaling(OffsetPlayer target, double scalingFactor) { 84 | scalingForTargets.put(target, scalingFactor); 85 | } 86 | 87 | /** 88 | * Retrieve the scaling factor that will be applied to the offset for the specified player. 89 | * 90 | * @param target Player whose offset scaling factor is being retrieved. 91 | * @return The scaling factor (divisor) that will be applied to the offset, or null if no scaling warning is set. 92 | */ 93 | public @Nullable Double getWarnScaling(OffsetPlayer target) { 94 | return scalingForTargets.get(target); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/provider/ConstantOffsetProvider.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.Offset; 5 | import com.jtprince.coordinateoffset.ScalableOffset; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | import java.util.LinkedHashMap; 10 | import java.util.Objects; 11 | import java.util.SequencedMap; 12 | 13 | @NullMarked 14 | public final class ConstantOffsetProvider extends CoreOffsetProvider { 15 | private final @Nullable ScalableOffset offset; 16 | 17 | ConstantOffsetProvider(String name, @Nullable ScalableOffset offset) { 18 | super(name); 19 | this.offset = offset; 20 | } 21 | 22 | @Override 23 | public Offset provideOffset(OffsetProviderContext context) { 24 | return Objects.requireNonNullElse(offset, Offset.ZERO); 25 | } 26 | 27 | @Override 28 | public SequencedMap serialize() { 29 | SequencedMap map = new LinkedHashMap<>(); 30 | if (offset == null) { 31 | map.put("offsetX", (long) 0); 32 | map.put("offsetZ", (long) 0); 33 | } else { 34 | map.put("offsetX", (long) offset.x()); 35 | map.put("offsetZ", (long) offset.z()); 36 | } 37 | return map; 38 | } 39 | 40 | public static ConstantOffsetProvider deserialize(OffsetProviderConfig config) throws IllegalArgumentException { 41 | SequencedMap s = config.getConfigSection(); 42 | 43 | if (!s.containsKey("offsetX") || !(s.get("offsetX") instanceof Number offsetXNum)) { 44 | throw new IllegalArgumentException("Provider \"" + config.getUserDefinedProviderName() + 45 | "\": Required key offsetX for ConstantOffsetProvider is missing or invalid."); 46 | } 47 | if (!s.containsKey("offsetZ") || !(s.get("offsetZ") instanceof Number offsetZNum)) { 48 | throw new IllegalArgumentException("Provider \"" + config.getUserDefinedProviderName() + 49 | "\": Required key offsetZ for ConstantOffsetProvider is missing or invalid."); 50 | } 51 | 52 | int offsetX = offsetXNum.intValue(); 53 | int offsetZ = offsetZNum.intValue(); 54 | 55 | if (Math.abs(offsetX) > OffsetProvider.OFFSET_MAX) { 56 | throw new IllegalArgumentException("Provider \"" + config.getUserDefinedProviderName() + 57 | "\": offsetX value " + offsetX + " is too large! (Max 30M)"); 58 | } 59 | if (Math.abs(offsetZ) > OffsetProvider.OFFSET_MAX) { 60 | throw new IllegalArgumentException("Provider \"" + config.getUserDefinedProviderName() + 61 | "\": offsetZ value " + offsetZ + " is too large! (Max 30M)"); 62 | } 63 | 64 | if (offsetX == 0 && offsetZ == 0) { 65 | return new ConstantOffsetProvider(config.getUserDefinedProviderName(), null); 66 | } else { 67 | ScalableOffset directOffset = Offset.scalable(offsetX, offsetZ); 68 | ScalableOffset alignedOffset = Offset.align(offsetX, offsetZ); 69 | if (!directOffset.equals(alignedOffset)) { 70 | CoordinateOffsetCore.get().getLogger().warning("Provider \"" + config.getUserDefinedProviderName() + 71 | "\": Constant offset " + directOffset + " contains a component which is not a multiple of " + 72 | CoordinateOffsetCore.get().getConfig().getOffsetsAreMultiplesOfBlocks() + 73 | " blocks; the offset will be rounded to " + alignedOffset + " to match the configured " + 74 | "offsetsAreMultiplesOfBlocks setting."); 75 | } 76 | return new ConstantOffsetProvider(config.getUserDefinedProviderName(), alignedOffset); 77 | } 78 | } 79 | 80 | @Override 81 | public String getMetricsClassName() { 82 | return "ConstantOffsetProvider"; 83 | } 84 | 85 | @Override 86 | public String getMetricsDetails() { 87 | // No details reported for constant providers. 88 | return getMetricsClassName(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /paper/src/main/java/com/jtprince/coordinateoffset/paper/MetricsWrapper.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.paper; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.config.CoordinateOffsetConfigBase; 5 | import com.jtprince.coordinateoffset.provider.CoreOffsetProvider; 6 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 7 | import org.bstats.bukkit.Metrics; 8 | import org.bstats.charts.DrilldownPie; 9 | import org.bstats.charts.SimplePie; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.plugin.Plugin; 12 | import org.jspecify.annotations.NullMarked; 13 | 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | @NullMarked 18 | public class MetricsWrapper { 19 | // https://bstats.org/plugin/bukkit/CoordinateOffset/19988 20 | private static final int BSTATS_PLUGIN_METRICS_ID = 19988; 21 | 22 | public static void reportMetrics(CoordinateOffsetPaperPlugin plugin) { 23 | Metrics metrics = new Metrics(plugin, BSTATS_PLUGIN_METRICS_ID); 24 | 25 | CoordinateOffsetCore core = CoordinateOffsetCore.get(); 26 | 27 | metrics.addCustomChart(new DrilldownPie("default_offset_provider", () -> { 28 | Map> result = new HashMap<>(); 29 | OffsetProvider defaultProvider = core.getProviderConfig().getDefaultOffsetProviderConfig(); 30 | if (defaultProvider instanceof CoreOffsetProvider coreOffsetProvider) { 31 | // Only report full metrics for built-in ("core") offset providers. 32 | result.put(coreOffsetProvider.getMetricsClassName(), Map.of(coreOffsetProvider.getMetricsDetails(), 1)); 33 | } else { 34 | // Intentionally obfuscate the name of any extensions made to CoordinateOffset. 35 | result.put("Custom Provider", Map.of("Unknown Offset Provider", 1)); 36 | } 37 | return result; 38 | })); 39 | 40 | metrics.addCustomChart(new SimplePie("world_border_obfuscation", () -> 41 | enabledDisabledStr(core.getConfig().getObfuscateWorldBorder()))); 42 | 43 | metrics.addCustomChart(new SimplePie("debug_packet_obfuscation", () -> 44 | enabledDisabledStr(core.getConfig().getObfuscateDebugPropertySubscriptions()))); 45 | 46 | metrics.addCustomChart(new SimplePie("fix_collision", () -> { 47 | boolean enabled = false; 48 | StringBuilder sb = new StringBuilder(); 49 | if (core.getConfig().getFixCollisionBamboo()) { 50 | enabled = true; 51 | sb.append("B"); 52 | } else { 53 | sb.append("x"); 54 | } 55 | if (core.getConfig().getFixCollisionDripstone()) { 56 | enabled = true; 57 | sb.append("D"); 58 | } else { 59 | sb.append("x"); 60 | } 61 | if (enabled) { 62 | return "enabled (" + sb + ")"; 63 | } else { 64 | return "disabled"; 65 | } 66 | })); 67 | 68 | metrics.addCustomChart(new SimplePie("verbose", () -> 69 | enabledDisabledStr(core.getConfig().getVerbose()))); 70 | 71 | metrics.addCustomChart(new SimplePie("offset_provider_override_count", () -> 72 | String.valueOf(core.getProviderConfig().getOffsetProviderOverrides().size()))); 73 | 74 | metrics.addCustomChart(new SimplePie("coord_scale_override_count", () -> 75 | String.valueOf(core.getConfig().getWorldCoordinateScaleOverrides().size()))); 76 | 77 | metrics.addCustomChart(new SimplePie("offsets_are_multiples_of_blocks", () -> 78 | ((CoordinateOffsetConfigBase) core.getConfig()).offsetsAreMultiplesOfBlocks.getMetricsString())); 79 | 80 | // Distant Horizons Support plugin 81 | metrics.addCustomChart(new SimplePie("dhsupport_version", () -> { 82 | Plugin dhs = Bukkit.getPluginManager().getPlugin("DHSupport"); 83 | if (dhs == null) { 84 | return "none"; 85 | } else { 86 | return dhs.getPluginMeta().getVersion(); 87 | } 88 | })); 89 | } 90 | 91 | private static String enabledDisabledStr(boolean enabled) { 92 | return enabled ? "enabled" : "disabled"; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/config/OffsetProviderListSerializer.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.OffsetProviderClassRegistry; 5 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 6 | import de.exlll.configlib.Serializer; 7 | import org.jspecify.annotations.NullMarked; 8 | 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import java.util.SequencedMap; 12 | 13 | @NullMarked 14 | public class OffsetProviderListSerializer implements Serializer, SequencedMap> { 15 | @Override 16 | public SequencedMap serialize(SequencedMap map) { 17 | SequencedMap providers = new LinkedHashMap<>(); 18 | for (Map.Entry entry : map.entrySet()) { 19 | SequencedMap serialized = new LinkedHashMap<>(); 20 | serialized.put("class", entry.getValue().getClass().getSimpleName()); 21 | serialized.putAll(entry.getValue().serialize()); 22 | providers.put(entry.getKey(), serialized); 23 | } 24 | return providers; 25 | } 26 | 27 | @Override 28 | public SequencedMap deserialize(SequencedMap element) throws IllegalArgumentException { 29 | if (!CoordinateOffsetCore.get().areAllProvidersLoaded()) { 30 | throw new IllegalStateException("Cannot deserialize OffsetProvider list until all providers are registered."); 31 | } 32 | 33 | SequencedMap providers = new LinkedHashMap<>(); 34 | for (Map.Entry entry : element.entrySet()) { 35 | if (!(entry.getValue() instanceof Map providerMap)) { 36 | throw new IllegalArgumentException("Invalid provider config for key " + entry.getKey()); 37 | } 38 | if (!providerMap.containsKey("class") || !(providerMap.get("class") instanceof String className)) { 39 | throw new IllegalArgumentException("Missing or invalid field 'class' for provider \"" + entry.getKey() + "\""); 40 | } 41 | 42 | OffsetProviderClassRegistry.RegisteredProviderClass clazz = 43 | CoordinateOffsetCore.get().getProviderRegistry().getRegisteredProviderClass(className); 44 | if (clazz == null) { 45 | // Unknown provider class 46 | // TODO: Replace full failure with best-effort loading for the remaining known provider classes 47 | // Can't do this now because ConfigLib will just delete any unknown sections when doing update() 48 | throw new IllegalArgumentException("Unknown provider class " + className + " for provider \"" + entry.getKey() + "\""); 49 | } 50 | 51 | OffsetProviderConfigImpl providerConfig = new OffsetProviderConfigImpl( 52 | entry.getKey(), 53 | className, 54 | new LinkedHashMap<>((Map) providerMap) 55 | ); 56 | try { 57 | OffsetProvider provider = clazz.deserializeFunction().apply(providerConfig); 58 | providers.put(entry.getKey(), provider); 59 | } catch (IllegalArgumentException e) { 60 | String msg = "Failed to read configured offset provider \"" + entry.getKey() + "\" with class " + 61 | clazz.className() + ". Check your configuration and look at the error below."; 62 | throw new IllegalArgumentException(msg, e); 63 | } catch (Exception e) { 64 | StringBuilder msg = new StringBuilder(); 65 | msg.append("Failed to read configured offset provider \"").append(entry.getKey()) 66 | .append("\" with class ").append(clazz.className()).append(". "); 67 | if (clazz.isCore()) { 68 | msg.append("This is a built-in offset provider, please report this as a bug."); 69 | } else { 70 | msg.append("This is NOT a CoordinateOffset bug! Check with the author of ") 71 | .append(clazz.className()).append(" before reporting this to CoordinateOffset."); 72 | } 73 | throw new IllegalArgumentException(msg.toString(), e); 74 | } 75 | } 76 | return providers; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/provider/ZeroAtLocationOffsetProvider.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.Offset; 5 | import com.jtprince.coordinateoffset.ScalableOffset; 6 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 7 | import com.jtprince.coordinateoffset.command.OffsetSetCommand; 8 | import com.jtprince.coordinateoffset.provider.util.ProviderOffsetStore; 9 | import com.jtprince.coordinateoffset.provider.util.RegenerateConfig; 10 | import org.jspecify.annotations.NullMarked; 11 | 12 | import java.util.LinkedHashMap; 13 | import java.util.Objects; 14 | import java.util.SequencedMap; 15 | 16 | @NullMarked 17 | public final class ZeroAtLocationOffsetProvider extends CoreOffsetProvider { 18 | private final RegenerateConfig regenerateConfig; 19 | private final ProviderOffsetStore offsetStore; 20 | 21 | ZeroAtLocationOffsetProvider(String name, RegenerateConfig regenerateConfig) { 22 | super(name); 23 | this.regenerateConfig = regenerateConfig; 24 | this.offsetStore = new ProviderOffsetStore( 25 | CoordinateOffsetCore.get().getAdapter().getPersistenceAdapter(), 26 | name, 27 | regenerateConfig.persistenceKeyOverride() 28 | ); 29 | } 30 | 31 | @Override 32 | public Offset provideOffset(OffsetProviderContext context) { 33 | //noinspection DuplicatedCode (with RandomOffsetProvider) 34 | boolean willRegenerate = switch (context.reason()) { 35 | case JOIN -> regenerateConfig.isRegenOnJoin(); 36 | case DEATH_RESPAWN -> regenerateConfig.isRegenOnDeath(); 37 | case WORLD_CHANGE -> regenerateConfig.isRegenOnWorldChange(); 38 | case COMMAND_REGENERATE, PLUGIN_REGENERATE -> true; /* Always regenerate when explicitly called */ 39 | case TELEPORT -> { 40 | Objects.requireNonNull(context.previousLocation()); 41 | Double distanceTeleported = context.playerLocation().getDistance(context.previousLocation()); 42 | Objects.requireNonNull(distanceTeleported); 43 | yield regenerateConfig.isRegenOnDistantTeleport(distanceTeleported); 44 | } 45 | case COMMAND_SET, PLUGIN_SET -> false; /* Should be unreachable - offset providers are not called for this reason */ 46 | }; 47 | if (willRegenerate) { 48 | offsetStore.clear(context.player().getUuid()); 49 | } 50 | 51 | // Check if the provider already has an offset calculated that was not cleared for a regenerate 52 | ScalableOffset offset = offsetStore.get(context.player()); 53 | double coordinateScale = context.playerLocation().getWorld().getCoordinateScale(); // 8 for nether e.g. 54 | if (offset == null) { 55 | // Generate a new offset if we don't already have one for this player 56 | offset = Offset.align( 57 | (int) (context.playerLocation().getX() * coordinateScale), 58 | (int) (context.playerLocation().getZ() * coordinateScale) 59 | ); 60 | offsetStore.put(context.player(), offset); 61 | } 62 | 63 | return offset; 64 | } 65 | 66 | @Override 67 | public void onOffsetSetByCommand(OffsetSetCommand command, OffsetPlayer target) { 68 | if (CoordinateOffsetCore.get().getConfig().getVerbose()) { 69 | CoordinateOffsetCore.get().getLogger().info("Provider \"" + name + "\": Updating offset " + 70 | "for " + target.getName() + " to " + command.getOffset()); 71 | } 72 | offsetStore.put(target, command.getOffset()); 73 | } 74 | 75 | 76 | @Override 77 | public SequencedMap serialize() { 78 | SequencedMap map = new LinkedHashMap<>(); 79 | 80 | regenerateConfig.serializeTo(map); 81 | 82 | return map; 83 | } 84 | 85 | public static ZeroAtLocationOffsetProvider deserialize(OffsetProviderConfig config) throws IllegalArgumentException { 86 | SequencedMap s = config.getConfigSection(); 87 | 88 | RegenerateConfig regenerateConfig = RegenerateConfig.deserialize(config.getUserDefinedProviderName(), s); 89 | 90 | return new ZeroAtLocationOffsetProvider(config.getUserDefinedProviderName(), regenerateConfig); 91 | } 92 | 93 | @Override 94 | public String getMetricsClassName() { 95 | return "ZeroAtLocationOffsetProvider"; 96 | } 97 | 98 | @Override 99 | public String getMetricsDetails() { 100 | return "Regenerate on " + regenerateConfig.getMetricsCharacterString(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/config/CoordinateOffsetConfigBase.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import de.exlll.configlib.Comment; 4 | import de.exlll.configlib.Configuration; 5 | import de.exlll.configlib.SerializeWith; 6 | import org.jspecify.annotations.NullMarked; 7 | import org.jspecify.annotations.Nullable; 8 | 9 | import java.util.LinkedHashMap; 10 | import java.util.Objects; 11 | import java.util.SequencedMap; 12 | 13 | @NullMarked 14 | @Configuration 15 | public class CoordinateOffsetConfigBase implements CoordinateOffsetConfig { 16 | @Comment("Do not change this.") 17 | @Nullable Integer configVersion = null; 18 | public @Nullable Integer getConfigVersion() { 19 | return configVersion; 20 | } 21 | 22 | @Comment({ 23 | "", 24 | "############################################################################ #", // keep this header at the top 25 | "################### General CoordinateOffset Configuration ################# #", 26 | "############################################################################ #", 27 | "", 28 | "If true, players with the `coordinateoffset.bypass` permission will always", 29 | " see their real coordinates (no offsets). Disable this to test the plugin." 30 | }) 31 | boolean bypassByPermission = true; 32 | public boolean getBypassByPermission() { 33 | return bypassByPermission; 34 | } 35 | 36 | @Configuration 37 | public static class FixCollision { 38 | boolean bamboo = true; 39 | boolean dripstone = true; 40 | } 41 | @Comment({ 42 | "", 43 | "Disable server-side collision checks for the listed blocks.", 44 | " If collision checks are left enabled, movement near these blocks will be", 45 | " extremely glitchy for all players with an offset applied.", 46 | " More info: https://github.com/joshuaprince/CoordinateOffset/issues/8", 47 | "Note: Requires a server restart for changes to take effect." 48 | }) 49 | FixCollision fixCollision = new FixCollision(); 50 | 51 | public boolean getFixCollisionBamboo() { 52 | return fixCollision.bamboo; 53 | } 54 | public boolean getFixCollisionDripstone() { 55 | return fixCollision.dripstone; 56 | } 57 | 58 | @Comment({ 59 | "", 60 | "Don't send world border packets to players who are far from the world border.", 61 | " Disable if your world border moves, but beware that it may leak coordinates:", 62 | " https://github.com/joshuaprince/CoordinateOffset/wiki/Implications-and-Limitations#world-border" 63 | }) 64 | boolean obfuscateWorldBorder = true; 65 | public boolean getObfuscateWorldBorder() { 66 | return obfuscateWorldBorder; 67 | } 68 | 69 | @Comment({ 70 | "", 71 | "Don't send any \"debug\" packets to players with an applied offset.", 72 | " Debug information reveals real coordinates if this is disabled.", 73 | " More info: https://minecraft.wiki/w/Debug_property" 74 | }) 75 | boolean obfuscateDebugPropertySubscriptions = true; 76 | public boolean getObfuscateDebugPropertySubscriptions() { 77 | return obfuscateDebugPropertySubscriptions; 78 | } 79 | 80 | @Comment({ 81 | "", 82 | "Round generated offsets to the nearest multiple of this number of blocks.", 83 | " Must be at least 16 and a power of 2 (16, 32, 64, 128, etc.).", 84 | " \"auto\" selects the lowest value compatible with other installed plugins,", 85 | " e.g. the Distant Horizons plugin requires 64+ for offsets to be compatible.", 86 | }) 87 | @SerializeWith(serializer = OffsetMultipleConfig.Serializer.class) 88 | public OffsetMultipleConfig offsetsAreMultiplesOfBlocks = OffsetMultipleConfig.AUTO; 89 | public int getOffsetsAreMultiplesOfBlocks() { 90 | return offsetsAreMultiplesOfBlocks.getMultiple(); 91 | } 92 | 93 | @Comment({ 94 | "", 95 | "Enable a log message when a player's offset changes." 96 | }) 97 | boolean verbose = false; 98 | public boolean getVerbose() { 99 | return verbose; 100 | } 101 | 102 | @Comment({ 103 | "", 104 | "Custom scaling for coordinates between worlds. Default overworld/end scale is", 105 | " 1.0 and default nether scale is 8.0. Offsets are divided by this value." 106 | }) 107 | @Nullable SequencedMap worldCoordinateScaleOverrides = null; // Not in default config 108 | public SequencedMap getWorldCoordinateScaleOverrides() { 109 | return Objects.requireNonNullElseGet(worldCoordinateScaleOverrides, LinkedHashMap::new); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/config/CoordinateOffsetConfigFull.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.config; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.provider.DefaultOffsetProviders; 5 | import com.jtprince.coordinateoffset.provider.OffsetProvider; 6 | import de.exlll.configlib.Comment; 7 | import de.exlll.configlib.Configuration; 8 | import de.exlll.configlib.SerializeWith; 9 | import org.jspecify.annotations.NullMarked; 10 | 11 | import java.util.List; 12 | import java.util.Objects; 13 | import java.util.SequencedMap; 14 | import java.util.logging.Logger; 15 | import java.util.stream.Collectors; 16 | 17 | @NullMarked 18 | @Configuration 19 | public class CoordinateOffsetConfigFull extends CoordinateOffsetConfigBase implements CoordinateOffsetProviderConfig { 20 | @Comment({ 21 | "", 22 | "############################################################################ #", // keep this header at the top 23 | "####################### Offset Provider Configuration ###################### #", 24 | "############################################################################ #", 25 | "", 26 | "Specify the method used to apply coordinate offsets to players.", 27 | "Options are any key under `offsetProviders` below (e.g. constant, random...)" 28 | }) 29 | String defaultOffsetProvider = "random"; 30 | public OffsetProvider getDefaultOffsetProviderConfig() { 31 | return Objects.requireNonNull(offsetProviders.get(defaultOffsetProvider)); 32 | } 33 | 34 | @Comment({ 35 | "", 36 | "List of overrides to the default offset provider. The first item has the", 37 | " highest priority. `provider` is a required key. Optional keys are", 38 | " `world`, `player`, and `permission` which, if present, must ALL match", 39 | " for the override to apply." 40 | }) 41 | List offsetProviderOverrides = DefaultOffsetProviders.DEFAULT_OVERRIDES; 42 | public List getOffsetProviderOverrides() { 43 | return offsetProviderOverrides.stream() 44 | .filter(o -> o.validate(offsetProviders, false)) 45 | .collect(Collectors.toUnmodifiableList()); 46 | } 47 | 48 | @Comment({ 49 | "", 50 | "Configuration for all available offset providers. Each provider must have a", 51 | " unique key (e.g. \"constant\"), which is used in `defaultOffsetProvider` and", 52 | " `offsetProviderOverrides`. You may add your own keys to define as many", 53 | " providers as you need.", 54 | "See the configuration guide for details about which options are available for", 55 | " each provider class.", 56 | "https://github.com/joshuaprince/CoordinateOffset/wiki/Configuration-Guide" 57 | }) 58 | @SerializeWith(serializer = OffsetProviderListSerializer.class) 59 | SequencedMap offsetProviders = DefaultOffsetProviders.PROVIDERS; 60 | public SequencedMap getAllOffsetProviderConfigs() { 61 | return offsetProviders; 62 | } 63 | 64 | /** 65 | * Validate that the fully-loaded configuration is valid. 66 | * 67 | * @return true if the configuration is acceptable to proceed, false if there is a problem that prevents 68 | * the plugin from functioning correctly. 69 | */ 70 | public boolean validateBaseAndFullConfig() { 71 | Logger logger = CoordinateOffsetCore.get().getLogger(); 72 | 73 | // Default provider must exist 74 | try { 75 | getDefaultOffsetProviderConfig(); 76 | } catch (NullPointerException e) { 77 | logger.severe("Failed to load offset providers from config."); 78 | logger.severe("An offset provider named \"" + defaultOffsetProvider + "\" was configured as the default provider, but no such provider exists."); 79 | logger.severe("Check your configuration and make sure that:"); 80 | logger.severe(" 1) The provider name is spelled correctly."); 81 | logger.severe(" 2) A provider whose name matches exactly is defined in the 'offsetProviders' section."); 82 | logger.severe(" 3) There are no other errors in the log that may indicate why the provider failed to load."); 83 | logger.severe("If the problem persists, please contact the plugin author for assistance."); 84 | return false; 85 | } 86 | 87 | // Override rules must be valid, but not fatal 88 | for (OffsetProviderOverrideConfigImpl override : offsetProviderOverrides) { 89 | override.validate(offsetProviders, true); 90 | // No early return; they'll be excluded in calls to getOffsetProviderOverrides 91 | } 92 | 93 | return true; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/src/main/java/com/jtprince/coordinateoffset/provider/RandomOffsetProvider.java: -------------------------------------------------------------------------------- 1 | package com.jtprince.coordinateoffset.provider; 2 | 3 | import com.jtprince.coordinateoffset.CoordinateOffsetCore; 4 | import com.jtprince.coordinateoffset.Offset; 5 | import com.jtprince.coordinateoffset.ScalableOffset; 6 | import com.jtprince.coordinateoffset.adapter.OffsetPlayer; 7 | import com.jtprince.coordinateoffset.command.OffsetSetCommand; 8 | import com.jtprince.coordinateoffset.provider.util.ProviderOffsetStore; 9 | import com.jtprince.coordinateoffset.provider.util.RegenerateConfig; 10 | import org.jspecify.annotations.NullMarked; 11 | 12 | import java.util.LinkedHashMap; 13 | import java.util.Objects; 14 | import java.util.SequencedMap; 15 | 16 | @NullMarked 17 | public final class RandomOffsetProvider extends CoreOffsetProvider { 18 | private final int randomBound; 19 | private final RegenerateConfig regenerateConfig; 20 | private final ProviderOffsetStore offsetStore; 21 | 22 | RandomOffsetProvider( 23 | String name, 24 | int randomBound, 25 | RegenerateConfig regenerateConfig 26 | ) { 27 | super(name); 28 | this.randomBound = randomBound; 29 | this.regenerateConfig = regenerateConfig; 30 | this.offsetStore = new ProviderOffsetStore( 31 | CoordinateOffsetCore.get().getAdapter().getPersistenceAdapter(), 32 | name, 33 | regenerateConfig.persistenceKeyOverride() 34 | ); 35 | } 36 | 37 | @Override 38 | public Offset provideOffset(OffsetProviderContext context) { 39 | //noinspection DuplicatedCode (with ZeroAtLocationOffsetProvider) 40 | boolean willRegenerate = switch (context.reason()) { 41 | case JOIN -> regenerateConfig.isRegenOnJoin(); 42 | case DEATH_RESPAWN -> regenerateConfig.isRegenOnDeath(); 43 | case WORLD_CHANGE -> regenerateConfig.isRegenOnWorldChange(); 44 | case COMMAND_REGENERATE, PLUGIN_REGENERATE -> true; /* Always regenerate when explicitly called */ 45 | case TELEPORT -> { 46 | Objects.requireNonNull(context.previousLocation()); 47 | Double distanceTeleported = context.playerLocation().getDistance(context.previousLocation()); 48 | Objects.requireNonNull(distanceTeleported); 49 | yield regenerateConfig.isRegenOnDistantTeleport(distanceTeleported); 50 | } 51 | case COMMAND_SET, PLUGIN_SET -> false; /* Should be unreachable - offset providers are not called for this reason */ 52 | }; 53 | if (willRegenerate) { 54 | offsetStore.clear(context.player().getUuid()); 55 | } 56 | 57 | // Check if the provider already has an offset calculated that was not cleared for a regenerate 58 | ScalableOffset offset = offsetStore.get(context.player()); 59 | if (offset == null) { 60 | // Generate a new offset if we don't already have one for this player 61 | offset = Offset.random(randomBound); 62 | offsetStore.put(context.player(), offset); 63 | } 64 | 65 | return offset; 66 | } 67 | 68 | @Override 69 | public void onOffsetSetByCommand(OffsetSetCommand command, OffsetPlayer target) { 70 | if (CoordinateOffsetCore.get().getConfig().getVerbose()) { 71 | CoordinateOffsetCore.get().getLogger().info("Provider \"" + name + "\": Updating offset " + 72 | "for " + target.getName() + " to " + command.getOffset()); 73 | } 74 | offsetStore.put(target, command.getOffset()); 75 | } 76 | 77 | @Override 78 | public SequencedMap serialize() { 79 | SequencedMap map = new LinkedHashMap<>(); 80 | 81 | map.put("randomBound", (long) randomBound); 82 | 83 | regenerateConfig.serializeTo(map); 84 | 85 | return map; 86 | } 87 | 88 | public static RandomOffsetProvider deserialize(OffsetProviderConfig config) throws IllegalArgumentException { 89 | SequencedMap s = config.getConfigSection(); 90 | 91 | if (!s.containsKey("randomBound") || !(s.get("randomBound") instanceof Number randomBoundNum)) { 92 | throw new IllegalArgumentException("Provider \"" + config.getUserDefinedProviderName() + 93 | ": Required key randomBound for RandomOffsetProvider is missing or invalid."); 94 | } 95 | int randomBound = randomBoundNum.intValue(); 96 | 97 | RegenerateConfig regenerateConfig = RegenerateConfig.deserialize(config.getUserDefinedProviderName(), s); 98 | 99 | return new RandomOffsetProvider( 100 | config.getUserDefinedProviderName(), 101 | randomBound, 102 | regenerateConfig 103 | ); 104 | } 105 | 106 | @Override 107 | public String getMetricsClassName() { 108 | return "RandomOffsetProvider"; 109 | } 110 | 111 | @Override 112 | public String getMetricsDetails() { 113 | return "Regenerate on " + regenerateConfig.getMetricsCharacterString(); 114 | } 115 | } 116 | --------------------------------------------------------------------------------