From 72d40a85f39eb06bebd60a7de1f5ecaf5b07f2ab Mon Sep 17 00:00:00 2001 From: echo <123984252+xEcho1337@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:24:55 +0000 Subject: [PATCH 1/6] feat(network): implemented connection factory (#3409) --- .../player/PlayerSocketConnection.java | 1 - .../network/socket/ConnectionFactory.java | 21 +++++++++++++++++++ .../server/network/socket/Server.java | 19 +++++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/minestom/server/network/socket/ConnectionFactory.java diff --git a/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java b/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java index 1d34cd09460..07020c6a2ba 100644 --- a/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java +++ b/src/main/java/net/minestom/server/network/player/PlayerSocketConnection.java @@ -59,7 +59,6 @@ *

* It is the implementation used for all network client. */ -@ApiStatus.Internal public class PlayerSocketConnection extends PlayerConnection { private static final Set> IMMEDIATE_PROCESS_PACKETS = Set.of( ClientHandshakePacket.class, // First received packet diff --git a/src/main/java/net/minestom/server/network/socket/ConnectionFactory.java b/src/main/java/net/minestom/server/network/socket/ConnectionFactory.java new file mode 100644 index 00000000000..25d9b908ec7 --- /dev/null +++ b/src/main/java/net/minestom/server/network/socket/ConnectionFactory.java @@ -0,0 +1,21 @@ +package net.minestom.server.network.socket; + +import net.minestom.server.network.player.PlayerSocketConnection; + +import org.jetbrains.annotations.Nullable; + +import java.net.SocketAddress; +import java.nio.channels.SocketChannel; + +@FunctionalInterface +public interface ConnectionFactory { + /** + * Creates the connection for an accepted client. + * The given threads are unstarted; they will be started by the server after this call. + * + * @return the connection to use, or {@code null} to reject the client (its channel will be closed) + */ + @Nullable PlayerSocketConnection create(SocketChannel channel, SocketAddress remoteAddress, + Thread readThread, Thread writeThread); +} + diff --git a/src/main/java/net/minestom/server/network/socket/Server.java b/src/main/java/net/minestom/server/network/socket/Server.java index 35d0680bb79..358f58586d6 100644 --- a/src/main/java/net/minestom/server/network/socket/Server.java +++ b/src/main/java/net/minestom/server/network/socket/Server.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicReference; public final class Server { + private volatile ConnectionFactory connectionFactory = PlayerSocketConnection::new; private volatile boolean stop; private final PacketParser.Client packetParser; @@ -93,11 +94,17 @@ public void start() { configureSocket(client); Thread readThread = readBuilder.unstarted(() -> playerReadLoop(reference.get())); Thread writeThread = writeBuilder.unstarted(() -> playerWriteLoop(reference.get())); - PlayerSocketConnection connection = new PlayerSocketConnection(client, client.getRemoteAddress(), readThread, writeThread); + PlayerSocketConnection connection = connectionFactory.create(client, client.getRemoteAddress(), readThread, writeThread); + + if (connection == null) { + client.close(); // Rejected by the factory + continue; + } + reference.set(connection); readThread.start(); writeThread.start(); - } catch (IOException e) { + } catch (IOException | RuntimeException e) { if (!ServerProperties.SUPPRESS_CONNECTION_ACCEPT_ERRORS.get()) MinecraftServer.getExceptionManager().handleException(e); try { @@ -198,6 +205,14 @@ public void stop() { } } + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = Objects.requireNonNull(connectionFactory); + } + + public ConnectionFactory connectionFactory() { + return connectionFactory; + } + @ApiStatus.Internal public PacketParser.Client packetParser() { return packetParser; From 43c23a9278a3ee522b644109ee4c49d6979dc014 Mon Sep 17 00:00:00 2001 From: iceBear67 <48877375+iceBear67@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:25:15 +0000 Subject: [PATCH 2/6] fix: update SmithingTrim to use Holder for trimPattern (#3407) * fix: update SmithingTrim to use Holder for trimPattern * adopt pattern matching for `pattern` --- .../minestom/server/recipe/display/SlotDisplay.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/minestom/server/recipe/display/SlotDisplay.java b/src/main/java/net/minestom/server/recipe/display/SlotDisplay.java index c1f7ba9644f..a32412edf23 100644 --- a/src/main/java/net/minestom/server/recipe/display/SlotDisplay.java +++ b/src/main/java/net/minestom/server/recipe/display/SlotDisplay.java @@ -5,8 +5,10 @@ import net.minestom.server.component.DataComponent; import net.minestom.server.item.ItemStackTemplate; import net.minestom.server.item.Material; +import net.minestom.server.item.armor.TrimPattern; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.NetworkBufferTemplate; +import net.minestom.server.registry.Holder; import net.minestom.server.registry.TagKey; import java.util.ArrayList; @@ -88,19 +90,20 @@ record Dyed(SlotDisplay dye, SlotDisplay target) implements SlotDisplay { record SmithingTrim( SlotDisplay base, SlotDisplay trimMaterial, - SlotDisplay trimPattern + Holder trimPattern ) implements SlotDisplay { public static final NetworkBuffer.Type NETWORK_TYPE = NetworkBufferTemplate.template( SlotDisplay.NETWORK_TYPE, SmithingTrim::base, SlotDisplay.NETWORK_TYPE, SmithingTrim::trimMaterial, - SlotDisplay.NETWORK_TYPE, SmithingTrim::trimPattern, + TrimPattern.NETWORK_TYPE, SmithingTrim::trimPattern, SmithingTrim::new); @Override public List components() { final var components = new ArrayList<>(base.components()); components.addAll(trimMaterial.components()); - components.addAll(trimPattern.components()); + final TrimPattern pattern = trimPattern.asValue(); + if (pattern != null) components.add(pattern.description()); return List.copyOf(components); } @@ -108,7 +111,9 @@ public List components() { public SlotDisplay copyWithOperator(UnaryOperator operator) { return new SmithingTrim(base.copyWithOperator(operator), trimMaterial.copyWithOperator(operator), - trimPattern.copyWithOperator(operator)); + trimPattern instanceof TrimPattern pattern + ? TrimPattern.create(pattern.assetId(), operator.apply(pattern.description()), pattern.isDecal()) + : trimPattern); } } From a92b8b81e33356deab5ddd71f253dc20108cf467 Mon Sep 17 00:00:00 2001 From: mudkip Date: Thu, 8 Oct 2026 21:30:47 +0000 Subject: [PATCH 3/6] rename resource location argument (#3316) --- .../java/net/minestom/demo/commands/CookieCommand.java | 4 ++-- .../server/command/builder/arguments/ArgumentType.java | 10 +++++----- ...{ArgumentResourceLocation.java => ArgumentKey.java} | 10 +++++----- .../server/command/builder/parser/ArgumentParser.java | 4 ++-- .../minestom/server/command/ArgumentParserTest.java | 2 +- .../net/minestom/server/command/ArgumentTypeTest.java | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) rename src/main/java/net/minestom/server/command/builder/arguments/minecraft/{ArgumentResourceLocation.java => ArgumentKey.java} (72%) diff --git a/demo/src/main/java/net/minestom/demo/commands/CookieCommand.java b/demo/src/main/java/net/minestom/demo/commands/CookieCommand.java index 01a71745549..b62309faf91 100644 --- a/demo/src/main/java/net/minestom/demo/commands/CookieCommand.java +++ b/demo/src/main/java/net/minestom/demo/commands/CookieCommand.java @@ -19,7 +19,7 @@ public CookieCommand() { } public static class Store extends Command { - private final Argument keyArg = ArgumentType.ResourceLocation("key"); + private final Argument keyArg = ArgumentType.Key("key"); private final Argument valueArg = ArgumentType.StringArray("value"); public Store() { @@ -40,7 +40,7 @@ private void store(CommandSender sender, CommandContext context) { } public static class Fetch extends Command { - private final Argument keyArg = ArgumentType.ResourceLocation("key"); + private final Argument keyArg = ArgumentType.Key("key"); public Fetch() { super("fetch"); diff --git a/src/main/java/net/minestom/server/command/builder/arguments/ArgumentType.java b/src/main/java/net/minestom/server/command/builder/arguments/ArgumentType.java index ac4e5c9c176..534c8fdcd8d 100644 --- a/src/main/java/net/minestom/server/command/builder/arguments/ArgumentType.java +++ b/src/main/java/net/minestom/server/command/builder/arguments/ArgumentType.java @@ -6,10 +6,10 @@ import net.minestom.server.command.builder.arguments.minecraft.ArgumentFloatRange; import net.minestom.server.command.builder.arguments.minecraft.ArgumentIntRange; import net.minestom.server.command.builder.arguments.minecraft.ArgumentItemStack; +import net.minestom.server.command.builder.arguments.minecraft.ArgumentKey; import net.minestom.server.command.builder.arguments.minecraft.ArgumentNbtCompoundTag; import net.minestom.server.command.builder.arguments.minecraft.ArgumentNbtTag; import net.minestom.server.command.builder.arguments.minecraft.ArgumentResource; -import net.minestom.server.command.builder.arguments.minecraft.ArgumentResourceLocation; import net.minestom.server.command.builder.arguments.minecraft.ArgumentResourceOrTag; import net.minestom.server.command.builder.arguments.minecraft.ArgumentTeamColor; import net.minestom.server.command.builder.arguments.minecraft.ArgumentTime; @@ -183,12 +183,12 @@ public static ArgumentResource Resource(String id, String identifier) { } /** - * Creates a new {@link ArgumentResourceLocation}. + * Creates a new {@link ArgumentKey}. * - * @see ArgumentResourceLocation + * @see ArgumentKey */ - public static ArgumentResourceLocation ResourceLocation(String id) { - return new ArgumentResourceLocation(id); + public static ArgumentKey Key(String id) { + return new ArgumentKey(id); } /** diff --git a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentResourceLocation.java b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentKey.java similarity index 72% rename from src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentResourceLocation.java rename to src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentKey.java index e30c757b627..55fc6c71f35 100644 --- a/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentResourceLocation.java +++ b/src/main/java/net/minestom/server/command/builder/arguments/minecraft/ArgumentKey.java @@ -8,23 +8,23 @@ import net.minestom.server.command.builder.exception.ArgumentSyntaxException; /** - * Represents a resource location (namespaced identifier) value. + * Represents a {@link Key} value. *

* Example: {@code minecraft:air} *

*/ -public class ArgumentResourceLocation extends Argument { +public class ArgumentKey extends Argument { public static final int PARSE_ERROR = 1; - public ArgumentResourceLocation(String id) { + public ArgumentKey(String id) { super(id); } @Override public Key parse(CommandSender sender, @KeyPattern String input) throws ArgumentSyntaxException { if (!Key.parseable(input)) - throw new ArgumentSyntaxException("Invalid resource location", input, PARSE_ERROR); + throw new ArgumentSyntaxException("Invalid key", input, PARSE_ERROR); return Key.key(input); } @@ -36,6 +36,6 @@ public ArgumentParserType parser() { @Override public String toString() { - return String.format("ResourceLocation<%s>", getId()); + return String.format("Key<%s>", getId()); } } diff --git a/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java b/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java index 9e5d57abac0..be3750a54b8 100644 --- a/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java +++ b/src/main/java/net/minestom/server/command/builder/parser/ArgumentParser.java @@ -14,9 +14,9 @@ import net.minestom.server.command.builder.arguments.minecraft.ArgumentFloatRange; import net.minestom.server.command.builder.arguments.minecraft.ArgumentIntRange; import net.minestom.server.command.builder.arguments.minecraft.ArgumentItemStack; +import net.minestom.server.command.builder.arguments.minecraft.ArgumentKey; import net.minestom.server.command.builder.arguments.minecraft.ArgumentNbtCompoundTag; import net.minestom.server.command.builder.arguments.minecraft.ArgumentNbtTag; -import net.minestom.server.command.builder.arguments.minecraft.ArgumentResourceLocation; import net.minestom.server.command.builder.arguments.minecraft.ArgumentTeamColor; import net.minestom.server.command.builder.arguments.minecraft.ArgumentTime; import net.minestom.server.command.builder.arguments.minecraft.ArgumentUUID; @@ -58,7 +58,7 @@ public class ArgumentParser { ARGUMENT_FUNCTION_MAP.put("color", ArgumentTeamColor::new); ARGUMENT_FUNCTION_MAP.put("time", ArgumentTime::new); ARGUMENT_FUNCTION_MAP.put("particle", ArgumentParticle::new); - ARGUMENT_FUNCTION_MAP.put("resourcelocation", ArgumentResourceLocation::new); + ARGUMENT_FUNCTION_MAP.put("key", ArgumentKey::new); ARGUMENT_FUNCTION_MAP.put("entitytype", ArgumentEntityType::new); ARGUMENT_FUNCTION_MAP.put("blockstate", ArgumentBlockState::new); ARGUMENT_FUNCTION_MAP.put("intrange", ArgumentIntRange::new); diff --git a/src/test/java/net/minestom/server/command/ArgumentParserTest.java b/src/test/java/net/minestom/server/command/ArgumentParserTest.java index a20fff7d6c0..f8008a8cae4 100644 --- a/src/test/java/net/minestom/server/command/ArgumentParserTest.java +++ b/src/test/java/net/minestom/server/command/ArgumentParserTest.java @@ -27,7 +27,7 @@ public void testArgumentParser() { assertParserEquals("Color", ArgumentType.TeamColor("example")); assertParserEquals("Time", ArgumentType.Time("example")); assertParserEquals("Particle", ArgumentType.Particle("example")); - assertParserEquals("ResourceLocation", ArgumentType.ResourceLocation("example")); + assertParserEquals("Key", ArgumentType.Key("example")); assertParserEquals("EntityType", ArgumentType.EntityType("example")); assertParserEquals("BlockState", ArgumentType.BlockState("example")); assertParserEquals("IntRange", ArgumentType.IntRange("example")); diff --git a/src/test/java/net/minestom/server/command/ArgumentTypeTest.java b/src/test/java/net/minestom/server/command/ArgumentTypeTest.java index 57bb7db6842..91a5e120f64 100644 --- a/src/test/java/net/minestom/server/command/ArgumentTypeTest.java +++ b/src/test/java/net/minestom/server/command/ArgumentTypeTest.java @@ -224,14 +224,14 @@ public void testArgumentResource() { } @Test - public void testArgumentResourceLocation() { - var arg = ArgumentType.ResourceLocation("resource_location"); + public void testArgumentKey() { + var arg = ArgumentType.Key("key"); assertArg(arg, Key.key("foo:bar"), "foo:bar"); assertArg(arg, Key.key("minecraft:air"), "air"); assertArg(arg, Key.key("minecraft:foo/bar"), "foo/bar"); - assertInvalidArg(arg, "minecraft:invalid resource location"); + assertInvalidArg(arg, "minecraft:invalid key"); assertInvalidArg(arg, "!"); assertInvalidArg(arg, "a/b:empty"); } From a4e5ee3904a1a72b4da0407adc25a6c8144a84ff Mon Sep 17 00:00:00 2001 From: Kerman <46640204+kermandev@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:31:31 +0000 Subject: [PATCH 4/6] chore: add .gitattributes (#3410) Checkouts use LF on every platform, and committed generated sources collapse in GitHub diffs. --- .gitattributes | 18 ++++ gradlew.bat | 224 ++++++++++++++++++++++++------------------------- 2 files changed, 130 insertions(+), 112 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..d823c105c1a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +* text=auto eol=lf + +*.bat text eol=crlf + +*.jar binary +*.png binary +*.der binary +*.dat binary +*.mca binary + +*.java diff=java +*.java.peb diff=java +*.kt diff=kotlin +*.kts diff=kotlin +*.md diff=markdown + +/src/autogenerated/** linguist-generated +/src/main/resources/META-INF/native-image/** linguist-generated diff --git a/gradlew.bat b/gradlew.bat index 03754c4c3d9..3185a43f78f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,112 +1,112 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem gradlew startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -@rem Catch executions from older scripts and ensure they exit cleanly. -@rem This can be removed once we can be reasonably confident that few people -@rem will be migrating directly to this new wrapper. -goto afterSafetyNet -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -goto exitWithErrorLevel -:afterSafetyNet - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -1>&2 echo. -1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -1>&2 echo. -1>&2 echo Please set the JAVA_HOME variable in your environment to match the -1>&2 echo location of your Java installation. - -"%COMSPEC%" /c exit 1 -goto exitWithErrorLevel - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -1>&2 echo. -1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -1>&2 echo. -1>&2 echo Please set the JAVA_HOME variable in your environment to match the -1>&2 echo location of your Java installation. - -"%COMSPEC%" /c exit 1 -goto exitWithErrorLevel - -:execute -@rem Setup the command line - - - -@rem Execute gradlew -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel - -@rem This label must not be changed. We rely on old scripts being able to jump to this point. -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% From 8e1427091e47eb6a744006e7fe4eebc73b29f855 Mon Sep 17 00:00:00 2001 From: Kerman <46640204+kermandev@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:17:34 +0000 Subject: [PATCH 5/6] fix: cancel a pending setInstance when a later call replaces it (#3411) --- .../net/minestom/server/entity/Player.java | 11 +++++ .../entity/player/PlayerIntegrationTest.java | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 237caed3896..19758e9d8c7 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -159,6 +159,7 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -203,6 +204,8 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou private PlayerSkin skin; private @Nullable Instance pendingInstance = null; + // Counts the calls to setInstance, so a spawn waiting for chunks knows when a later call replaces it + private final AtomicInteger instanceRequests = new AtomicInteger(); private int dimensionTypeId; private volatile GameMode gameMode; private WorldPos deathLocation; @@ -664,6 +667,9 @@ public void sendPacketToViewersAndSelf(SendablePacket packet) { *

* Be aware that because chunk operations are expensive, * it is possible for this method to be non-blocking when retrieving chunks is required. + *

+ * A future that waits for chunks completes with a {@link CancellationException} when a later call to this method + * replaces it, and the player stays wherever that later call moves it. * * @param instance the new player instance * @param spawnPosition the new position of the player @@ -673,6 +679,7 @@ public void sendPacketToViewersAndSelf(SendablePacket packet) { public CompletableFuture setInstance(Instance instance, Pos spawnPosition) { final Instance currentInstance = this.instance; Check.argCondition(currentInstance == instance, "Instance should be different than the current one"); + final int request = instanceRequests.incrementAndGet(); if (SharedInstance.areLinked(currentInstance, instance) && spawnPosition.sameChunk(this.position)) { // The player already has the good version of all the chunks. // We just need to refresh his entity viewing list and add him to the instance @@ -723,6 +730,10 @@ public Void join() { var _ = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) .thenRun(() -> { scheduler.scheduleNextProcess(() -> { + if (instanceRequests.get() != request) { + future.completeExceptionally(new CancellationException("Another setInstance call replaced this one")); + return; + } runnable.accept(instance); future.complete(null); }); diff --git a/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java b/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java index f94619a55ca..2a020187e1c 100644 --- a/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java @@ -18,6 +18,9 @@ import net.minestom.server.event.player.PlayerChunkUnloadEvent; import net.minestom.server.event.player.PlayerGameModeChangeEvent; import net.minestom.server.event.player.PlayerInputEvent; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.ChunkLoader; +import net.minestom.server.instance.Instance; import net.minestom.server.listener.PlayerInputListener; import net.minestom.server.message.ChatMessageType; import net.minestom.server.network.packet.client.common.ClientSettingsPacket; @@ -41,6 +44,7 @@ import net.minestom.testing.Collector; import net.minestom.testing.Env; import net.minestom.testing.EnvTest; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -49,6 +53,8 @@ import java.util.EnumSet; import java.util.List; import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.function.Predicate; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -386,6 +392,43 @@ public void lookAt(Env env) { assertEquals(startingPlayerPos, player.getPosition()); } + @Test + public void laterSetInstanceCancelsSpawnWaitingForChunks(Env env) { + final Player player = env.createPlayer(env.createFlatInstance(), new Pos(0, 40, 0)); + final CountDownLatch release = new CountDownLatch(1); + final Instance slow = env.process().instance().createInstanceContainer(new ChunkLoader() { + @Override + public @Nullable Chunk loadChunk(Instance instance, int chunkX, int chunkZ) { + try { + release.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return null; + } + + @Override + public void saveChunk(Chunk chunk) { + } + + @Override + public boolean supportsParallelLoading() { + return true; + } + }); + final Instance target = env.createFlatInstance(); + + final CompletableFuture waiting = player.setInstance(slow, new Pos(0, 40, 0)); + final CompletableFuture moved = player.setInstance(target, new Pos(0, 40, 0)); + assertTrue(env.tickWhile(() -> !moved.isDone(), Duration.ofSeconds(5))); + moved.join(); + release.countDown(); + assertTrue(env.tickWhile(() -> !waiting.isDone(), Duration.ofSeconds(5))); + + assertTrue(waiting.isCancelled()); + assertEquals(target, player.getInstance()); + } + @Test public void removePlayer(Env env) { var instance = env.createFlatInstance(); From dd2ddbc72d09f1f6b1ff6778104030c00e780231 Mon Sep 17 00:00:00 2001 From: Kerman <46640204+kermandev@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:18:36 +0000 Subject: [PATCH 6/6] refactor: cache CachedPacket packets separately from framed bodies (#3412) Reading the packet of a CachedPacket does not frame it. --- .../network/packet/server/CachedPacket.java | 95 +++++++++++-------- .../SendablePacketIntegrationTest.java | 47 +++++++++ 2 files changed, 101 insertions(+), 41 deletions(-) diff --git a/src/main/java/net/minestom/server/network/packet/server/CachedPacket.java b/src/main/java/net/minestom/server/network/packet/server/CachedPacket.java index 5f2e2fa71e7..ecd36f69cad 100644 --- a/src/main/java/net/minestom/server/network/packet/server/CachedPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/CachedPacket.java @@ -41,7 +41,7 @@ public final class CachedPacket implements SendablePacket { static { try { - PACKET = MethodHandles.lookup().findVarHandle(CachedPacket.class, "packet", SoftReference.class); + PACKET = MethodHandles.lookup().findVarHandle(CachedPacket.class, "cache", SoftReference.class); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } @@ -51,7 +51,7 @@ public final class CachedPacket implements SendablePacket { // Accessed through PACKET using acquire/release semantics. @SuppressWarnings("unused") - private @Nullable SoftReference packet; + private @Nullable SoftReference cache; /** * Creates a cached packet backed by the given packet supplier. @@ -101,7 +101,7 @@ public void invalidate() { * Returns the packet represented by this cached value. * *

When caching is enabled, this method may initialize or reuse the cached - * framed packet. Supplier invocation and cache computation are serialized. + * packet. Supplier invocation and cache computation are serialized. * *

When caching is disabled, the packet supplier is invoked directly and * may be invoked concurrently by multiple callers. @@ -110,15 +110,17 @@ public void invalidate() { * @return the packet value */ public ServerPacket packet(ConnectionState state) { - final FramedPacket cache = updatedCache(state); - return cache != null ? cache.packet() : packetSupplier.get(); + if (!ServerProperties.CACHED_PACKET.get()) return packetSupplier.get(); + final Cache cache = cache(); + return cache != null ? cache.packet() : computePacket(); } /** * Returns the cached framed packet body for the given connection state. * - *

If caching is enabled, this method initializes the cache when necessary. - * Cache computation and supplier invocation are serialized. + *

If caching is enabled, this method frames the packet for + * {@code state} when no body is cached yet, reusing the cached packet when + * one exists. Cache computation and supplier invocation are serialized. * *

If caching is disabled, no framed body is created and {@code null} is * returned. @@ -127,67 +129,73 @@ public ServerPacket packet(ConnectionState state) { * @return the framed packet body, or {@code null} when caching is disabled */ public @Nullable NetworkBuffer body(ConnectionState state) { - final FramedPacket cache = updatedCache(state); - return cache != null ? cache.body() : null; + if (!ServerProperties.CACHED_PACKET.get()) return null; + final Cache cache = cache(); + final NetworkBuffer body = cache != null ? cache.body() : null; + return body != null ? body : computeBody(state); } /** - * Returns the current cached value, computing it when absent. + * Returns the cached packet, computing it when absent. * - * @param state the connection state used when framing the packet - * @return the cached framed packet, or {@code null} when caching is disabled + *

This method is synchronized so that only one thread invokes the packet + * supplier at a time. The cache is checked again after acquiring this + * object's monitor because another thread may have populated it while the + * current thread was waiting. + * + * @return the existing or newly computed packet */ - private @Nullable FramedPacket updatedCache(ConnectionState state) { - if (!ServerProperties.CACHED_PACKET.get()) return null; - - final FramedPacket cache = cachedPacket(); - return cache != null ? cache : computeCache(state); + private synchronized ServerPacket computePacket() { + final Cache cache = cache(); + if (cache != null) return cache.packet(); + final ServerPacket packet = packetSupplier.get(); + PACKET.setRelease(this, new SoftReference<>(new Cache(packet, null))); + return packet; } /** - * Returns an existing cached value or computes and publishes a replacement. + * Returns the existing framed body or computes and publishes one. * *

This method is synchronized so that only one thread invokes the packet - * supplier and computes a replacement cache entry at a time. The cache is - * checked again after acquiring this object's monitor because another thread - * may have populated it while the current thread was waiting. + * supplier and frames a replacement at a time. The cache is checked again + * after acquiring this object's monitor because another thread may have + * populated it while the current thread was waiting. * *

Invalidation does not acquire this monitor. An invalidation occurring * during computation may therefore be followed by publication of the * in progress result. * * @param state the connection state used when framing the packet - * @return the existing or newly computed framed packet + * @return the existing or newly framed body */ - private synchronized FramedPacket computeCache(ConnectionState state) { - final FramedPacket cache = cachedPacket(); - if (cache != null) return cache; - - final ServerPacket packet = packetSupplier.get(); - final NetworkBuffer buffer = PacketWriting.allocateTrimmedPacket(state, packet, MinecraftServer.getCompressionThreshold()); - - final FramedPacket updated = new FramedPacket(packet, buffer); - PACKET.setRelease(this, new SoftReference<>(updated)); - return updated; + private synchronized NetworkBuffer computeBody(ConnectionState state) { + final Cache cache = cache(); + final NetworkBuffer cached = cache != null ? cache.body() : null; + if (cached != null) return cached; + + final ServerPacket packet = cache != null ? cache.packet() : packetSupplier.get(); + final NetworkBuffer body = PacketWriting.allocateTrimmedPacket(state, packet, MinecraftServer.getCompressionThreshold()); + PACKET.setRelease(this, new SoftReference<>(new Cache(packet, body))); + return body; } /** - * Returns the currently referenced cached packet. + * Returns the currently referenced cache. * *

The cached reference is read using acquire semantics. * - * @return the cached framed packet, or {@code null} if no cache exists or - * the soft reference has been cleared + * @return the cache, or {@code null} if no cache exists or the soft + * reference has been cleared */ @SuppressWarnings("unchecked") - private @Nullable FramedPacket cachedPacket() { - final SoftReference ref = (SoftReference) PACKET.getAcquire(this); + private @Nullable Cache cache() { + final SoftReference ref = (SoftReference) PACKET.getAcquire(this); return ref != null ? ref.get() : null; } /** - * Returns whether a framed packet is currently cached and has not been - * reclaimed. + * Returns whether a packet is currently cached and has not been + * reclaimed. The cached packet may not have been framed yet. * *

This result is inherently transient because the underlying soft * reference may be cleared or the cache may be invalidated immediately @@ -197,7 +205,7 @@ private synchronized FramedPacket computeCache(ConnectionState state) { * available */ public boolean isValid() { - return ServerProperties.CACHED_PACKET.get() && cachedPacket() != null; + return ServerProperties.CACHED_PACKET.get() && cache() != null; } /** @@ -211,6 +219,11 @@ public boolean isValid() { */ @Override public String toString() { - return String.format("CachedPacket{cache=%s}", cachedPacket()); + return String.format("CachedPacket{cache=%s}", cache()); + } + + // The packet, plus its framed body once a connection requested it. + // Requires identity + private record Cache(ServerPacket packet, @Nullable NetworkBuffer body) { } } \ No newline at end of file diff --git a/src/test/java/net/minestom/server/network/SendablePacketIntegrationTest.java b/src/test/java/net/minestom/server/network/SendablePacketIntegrationTest.java index 47b8dff2606..adbd3483583 100644 --- a/src/test/java/net/minestom/server/network/SendablePacketIntegrationTest.java +++ b/src/test/java/net/minestom/server/network/SendablePacketIntegrationTest.java @@ -9,6 +9,10 @@ import net.minestom.testing.EnvTest; import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -31,4 +35,47 @@ public void cached(Env env) { assertSame(packet, cached.packet(ConnectionState.PLAY)); } + + @Test + public void cachedBodyReusesCachedPacket(Env env) { + var packet = new SystemChatPacket(Component.text("Hello World!"), false); + var calls = new AtomicInteger(); + var cached = new CachedPacket(() -> { + calls.incrementAndGet(); + return packet; + }); + assertFalse(cached.isValid()); + + // May fail in the very unlikely case where soft references are cleared + assertSame(packet, cached.packet(ConnectionState.PLAY)); + assertSame(packet, cached.packet(ConnectionState.PLAY)); + assertEquals(1, calls.get()); + assertTrue(cached.isValid()); + + var buffer = PacketWriting.allocateTrimmedPacket(ConnectionState.PLAY, packet, + MinecraftServer.getCompressionThreshold(), env.process().registries()); + var cachedBuffer = cached.body(ConnectionState.PLAY); + assertTrue(NetworkBuffer.equals(buffer, cachedBuffer)); + assertSame(cachedBuffer, cached.body(ConnectionState.PLAY)); + assertSame(packet, cached.packet(ConnectionState.PLAY)); + assertEquals(1, calls.get()); + + cached.invalidate(); + assertFalse(cached.isValid()); + assertTrue(NetworkBuffer.equals(buffer, cached.body(ConnectionState.PLAY))); + assertSame(packet, cached.packet(ConnectionState.PLAY)); + assertEquals(2, calls.get()); + } + + @Test + public void cachedPacketIsNotFramed(Env env) { + // System chat has no packet ID in the login state, so framing it there fails + var packet = new SystemChatPacket(Component.text("Hello World!"), false); + var cached = new CachedPacket(packet); + assertSame(packet, cached.packet(ConnectionState.LOGIN)); + assertTrue(cached.isValid()); + assertTrue(NetworkBuffer.equals(PacketWriting.allocateTrimmedPacket(ConnectionState.PLAY, packet, + MinecraftServer.getCompressionThreshold(), env.process().registries()), + cached.body(ConnectionState.PLAY))); + } }