From 3c8fcdff09cde97cc22a492927c5c83921c00a2c Mon Sep 17 00:00:00 2001 From: Eclipse Date: Fri, 2 May 2025 11:41:16 +0000 Subject: [PATCH] Initial work on using Minecraft gametests to test component hashing --- .../GeyserComponentHashTestInstance.java | 94 +++++++++++++++++++ .../GeyserFabricGametestBootstrap.java | 39 ++++++++ .../fabric/gametest/GeyserTestInstance.java | 60 ++++++++++++ .../gametest/mixin/GameTestServerMixin.java | 85 +++++++++++++++++ .../data/geyser/test_instance/test.json | 15 +++ .../src/gametest/resources/fabric.mod.json | 25 ++++- .../resources/geyser-fabric-tests.mixins.json | 14 +++ .../platform/fabric/GeyserFabricPlatform.java | 3 + .../item/hashing/DataComponentHashers.java | 4 + core/src/main/resources/languages | 2 +- 10 files changed, 336 insertions(+), 5 deletions(-) create mode 100644 bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserComponentHashTestInstance.java create mode 100644 bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserFabricGametestBootstrap.java create mode 100644 bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserTestInstance.java create mode 100644 bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/mixin/GameTestServerMixin.java create mode 100644 bootstrap/mod/fabric/src/gametest/resources/data/geyser/test_instance/test.json create mode 100644 bootstrap/mod/fabric/src/gametest/resources/geyser-fabric-tests.mixins.json diff --git a/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserComponentHashTestInstance.java b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserComponentHashTestInstance.java new file mode 100644 index 000000000..dfcb80af9 --- /dev/null +++ b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserComponentHashTestInstance.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.fabric.gametest; + +import com.google.common.hash.HashCode; +import com.mojang.serialization.MapCodec; +import io.netty.buffer.Unpooled; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.TypedDataComponent; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.gametest.framework.GameTestInstance; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.RegistryOps; +import net.minecraft.util.HashOps; +import org.geysermc.geyser.item.hashing.DataComponentHashers; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponent; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.jetbrains.annotations.NotNull; + +public class GeyserComponentHashTestInstance extends GeyserTestInstance { + + private static final MapCodec> TYPED_COMPONENT_CODEC = DataComponentType.PERSISTENT_CODEC + .dispatchMap("component", TypedDataComponent::type, GeyserComponentHashTestInstance::typedComponentCodec); + public static final MapCodec> CODEC = TYPED_COMPONENT_CODEC.xmap(GeyserComponentHashTestInstance::new, + instance -> instance.testValue); + + private final TypedDataComponent testValue; + + protected GeyserComponentHashTestInstance(TypedDataComponent testValue) { + this.testValue = testValue; + } + + @Override + protected void run(@NotNull GameTestHelper helper, GeyserSession session) { + // Encode vanilla component to buffer + RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), helper.getLevel().registryAccess()); + TypedDataComponent.STREAM_CODEC.encode(buffer, testValue); + + // Read with MCPL + int id = MinecraftTypes.readVarInt(buffer); + DataComponent mcplComponent = DataComponentTypes.from(id).readDataComponent(buffer); + + // Hash both and compare + RegistryOps ops = RegistryOps.create(HashOps.CRC32C_INSTANCE, helper.getLevel().registryAccess()); + int expected = testValue.encodeValue(ops).getOrThrow().asInt(); + int geyser = DataComponentHashers.hash(session, mcplComponent).asInt(); + + helper.assertValueEqual(expected, geyser, Component.literal("Hash for component " + testValue)); + + // Succeed if nothing was thrown + helper.succeed(); + } + + @Override + public @NotNull MapCodec codec() { + return CODEC; + } + + @Override + protected @NotNull MutableComponent typeDescription() { + return Component.literal("Geyser Data Component Hash Test"); + } + + private static MapCodec> typedComponentCodec(DataComponentType component) { + return component.codecOrThrow().fieldOf("value").xmap(value -> new TypedDataComponent<>(component, value), TypedDataComponent::value); + } +} diff --git a/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserFabricGametestBootstrap.java b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserFabricGametestBootstrap.java new file mode 100644 index 000000000..7befc4bc8 --- /dev/null +++ b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserFabricGametestBootstrap.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.fabric.gametest; + +import net.fabricmc.api.ModInitializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; + +public class GeyserFabricGametestBootstrap implements ModInitializer { + + @Override + public void onInitialize() { + Registry.register(BuiltInRegistries.TEST_INSTANCE_TYPE, ResourceLocation.fromNamespaceAndPath("geyser", "component_hash"), GeyserComponentHashTestInstance.CODEC); + } +} diff --git a/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserTestInstance.java b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserTestInstance.java new file mode 100644 index 000000000..ed5ba2118 --- /dev/null +++ b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/GeyserTestInstance.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.fabric.gametest; + +import net.minecraft.core.Holder; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.gametest.framework.GameTestInstance; +import net.minecraft.gametest.framework.TestData; +import net.minecraft.gametest.framework.TestEnvironmentDefinition; +import net.minecraft.resources.ResourceLocation; +import org.geysermc.geyser.session.GeyserSession; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public abstract class GeyserTestInstance extends GameTestInstance { + + protected GeyserTestInstance() { + // TODO use default vanilla test environment + super(new TestData<>(Holder.direct(new TestEnvironmentDefinition.AllOf(List.of())), + ResourceLocation.withDefaultNamespace("empty"), 1, 1, true)); + } + + @Override + public void run(@NotNull GameTestHelper helper) { + /*Map sessions = GeyserImpl.getInstance().getSessionManager().getSessions(); + while (sessions.isEmpty()) { + try { + Thread.sleep(100L); + } catch (InterruptedException ignored) {} + } + GeyserSession session = sessions.values().stream().findAny().orElseThrow();*/ + run(helper, null); + } + + protected abstract void run(@NotNull GameTestHelper helper, GeyserSession session); +} diff --git a/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/mixin/GameTestServerMixin.java b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/mixin/GameTestServerMixin.java new file mode 100644 index 000000000..aac475bec --- /dev/null +++ b/bootstrap/mod/fabric/src/gametest/java/org/geysermc/geyser/platform/fabric/gametest/mixin/GameTestServerMixin.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.fabric.gametest.mixin; + +import com.mojang.datafixers.DataFixer; +import net.minecraft.gametest.framework.GameTestServer; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.Services; +import net.minecraft.server.WorldStem; +import net.minecraft.server.level.progress.ChunkProgressListenerFactory; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.geysermc.geyser.platform.mod.GeyserServerPortGetter; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Proxy; + +@Mixin(GameTestServer.class) +public abstract class GameTestServerMixin extends MinecraftServer implements GeyserServerPortGetter { + + @Shadow + @Final + private static Logger LOGGER; + + public GameTestServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, + Proxy proxy, DataFixer dataFixer, Services services, ChunkProgressListenerFactory chunkProgressListenerFactory) { + super(thread, levelStorageAccess, packRepository, worldStem, proxy, dataFixer, services, chunkProgressListenerFactory); + } + + @Override + public int geyser$getServerPort() { + return this.getPort(); + } + + @Inject(method = "initServer", at = @At("HEAD"), cancellable = true) + public void startTcpServer(CallbackInfoReturnable callbackInfoReturnable) { + // A bit of copying from dedicated server code + InetAddress address = InetAddress.getLoopbackAddress(); + if (getPort() < 0) { + setPort(25565); + } + LOGGER.info("Starting gametest server on {}:{}", address, getPort()); + LOGGER.info("Geyser's tests will start once a bedrock player connects!"); + + try { + this.getConnection().startTcpServerListener(address, getPort()); + } catch (IOException exception) { + LOGGER.warn("**** FAILED TO BIND TO PORT!"); + LOGGER.warn("The exception was: {}", exception.toString()); + LOGGER.warn("Perhaps a server is already running on that port?"); + callbackInfoReturnable.setReturnValue(false); + } + } +} diff --git a/bootstrap/mod/fabric/src/gametest/resources/data/geyser/test_instance/test.json b/bootstrap/mod/fabric/src/gametest/resources/data/geyser/test_instance/test.json new file mode 100644 index 000000000..06cf7e483 --- /dev/null +++ b/bootstrap/mod/fabric/src/gametest/resources/data/geyser/test_instance/test.json @@ -0,0 +1,15 @@ +{ + "type": "geyser:component_hash", + "component": "minecraft:custom_data", + "value": { + "hello": "g'day", + "nice?": false, + "coolness": 100, + "geyser": { + "is": "very cool" + }, + "a list": [ + ["in a list"] + ] + } +} diff --git a/bootstrap/mod/fabric/src/gametest/resources/fabric.mod.json b/bootstrap/mod/fabric/src/gametest/resources/fabric.mod.json index 246352046..e64198f90 100644 --- a/bootstrap/mod/fabric/src/gametest/resources/fabric.mod.json +++ b/bootstrap/mod/fabric/src/gametest/resources/fabric.mod.json @@ -1,13 +1,30 @@ { "schemaVersion": 1, - "id": "geyser-gametest", - "version": "1.0.0", - "name": "Geyser-GameTest", + "id": "${id}-fabric-tests", + "version": "${version}", + "name": "${name}-Fabric-gametest", + "description": "A bridge/proxy allowing you to connect to Minecraft: Java Edition servers with Minecraft: Bedrock Edition. ", + "authors": [ + "${author}" + ], + "contact": { + "website": "${url}", + "repo": "https://github.com/GeyserMC/Geyser" + }, + "license": "MIT", "icon": "assets/geyser/icon.png", "environment": "*", "entrypoints": { "fabric-gametest": [ - "org.geysermc.geyser.gametest.GeyserGameTest" + "org.geysermc.geyser.platform.fabric.gametest.GeyserFabricGametestBootstrap" ] + }, + "mixins": [ + "geyser-fabric-tests.mixins.json" + ], + "depends": { + "fabricloader": ">=0.16.7", + "fabric-api": "*", + "minecraft": ">=1.21.5" } } diff --git a/bootstrap/mod/fabric/src/gametest/resources/geyser-fabric-tests.mixins.json b/bootstrap/mod/fabric/src/gametest/resources/geyser-fabric-tests.mixins.json new file mode 100644 index 000000000..17dc3df4e --- /dev/null +++ b/bootstrap/mod/fabric/src/gametest/resources/geyser-fabric-tests.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.geysermc.geyser.platform.fabric.gametest.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "GameTestServerMixin" + ], + "server": [], + "client": [], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java index 4631ab493..2ebfeb0a9 100644 --- a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java +++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java @@ -82,6 +82,9 @@ public class GeyserFabricPlatform implements GeyserModPlatform { @Override public @Nullable InputStream resolveResource(@NonNull String resource) { + if (true) { + return this.getClass().getClassLoader().getResourceAsStream(resource); + } // We need to handle this differently, because Fabric shares the classloader across multiple mods Path path = this.mod.findPath(resource).orElse(null); if (path == null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java index d45ea5ffc..8e5019fb1 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java @@ -317,6 +317,10 @@ public class DataComponentHashers { } } + public static > HashCode hash(GeyserSession session, DataComponent component) { + return hash(session, component.getType(), component.getValue()); + } + public static HashedStack hashStack(GeyserSession session, ItemStack stack) { if (stack == null) { return null; diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index 4ce8ad58e..daa215742 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 4ce8ad58ea7ab779d613a64862956d6d0563a8e3 +Subproject commit daa215742837ca1c16ef34e724bba98b6bc047a9