From d5b8edea5c27976158ff767175295d5d1c48285a Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:49:00 -0400 Subject: [PATCH 01/11] feat(jvm)!: host sessions through engine-neutral primitives Add SessionHandler, SessionControl, Delivery and ChunkSessions to jvm/runtime so apps decide what a session is and how it is isolated, and add jvm/minestom-primitives with ChunkLogin for upstream Minestom. runtime-minestom now runs on top of these primitives. --- docs/distribution.md | 6 +- .../chunk/runtime/CoinCommandTest.java | 32 +- gradle/libs.versions.toml | 2 + jvm/minestom-primitives/README.md | 43 + jvm/minestom-primitives/build.gradle.kts | 14 + .../chunkzero/chunk/minestom/ChunkLogin.java | 163 ++++ .../chunk/runtime/ChunkLoginTest.java | 216 +++++ .../chunk/runtime/SessionCoroutinesTest.kt | 9 +- .../chunk/runtime/ChunkMinestom.java | 53 +- .../chunk/runtime/ManagedPlayer.java | 23 +- .../chunk/runtime/SessionManager.java | 439 +++------ .../chunk/runtime/SessionRegistration.java | 8 - .../chunkzero/chunk/runtime/SessionScope.java | 46 +- .../minestom/internal/AdmittedPlayer.java | 148 +++ .../minestom/internal/GameplayService.java | 237 +---- .../minestom/internal/PreparedDelivery.java | 259 ------ .../minestom/internal/ProcessService.java | 100 --- .../internal/SessionMethodService.java | 146 --- .../chunk/runtime/AppRegistryTest.java | 9 +- .../chunk/runtime/SessionCreationTest.java | 78 +- .../runtime/SessionMethodServiceTest.java | 121 --- .../chunk/runtime/SessionMethodTest.java | 103 +++ .../chunkzero/chunk/runtime/TestHosts.java | 68 ++ .../chunk/runtime/GameplayLifecycleTest.kt | 35 +- .../chunk/runtime/GameplayServiceTest.kt | 12 +- .../chunk/runtime/SessionEventsTest.kt | 60 +- .../chunk/runtime/SessionManagerTest.kt | 66 +- .../minestom/internal/DeliveryFenceTest.kt | 34 - jvm/runtime/README.md | 63 +- jvm/runtime/build.gradle.kts | 2 +- .../chunkzero/chunk/runtime/ChunkProcess.java | 73 +- .../chunk/runtime/ChunkSessions.java | 846 ++++++++++++++++++ .../com/chunkzero/chunk/runtime/Delivery.java | 153 ++++ .../chunk/runtime}/DeliveryFence.java | 2 +- .../chunk/runtime/PlayerProfile.java | 27 + .../chunk/runtime/SessionControl.java | 129 +++ .../chunk/runtime/SessionHandler.java | 36 + .../chunk/runtime/ChunkSessionsTest.java | 276 ++++++ settings.gradle.kts | 1 + 39 files changed, 2703 insertions(+), 1435 deletions(-) create mode 100644 jvm/minestom-primitives/README.md create mode 100644 jvm/minestom-primitives/build.gradle.kts create mode 100644 jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java create mode 100644 jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java create mode 100644 jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java delete mode 100644 jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/PreparedDelivery.java delete mode 100644 jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ProcessService.java delete mode 100644 jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodService.java delete mode 100644 jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodServiceTest.java create mode 100644 jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java create mode 100644 jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java delete mode 100644 jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/minestom/internal/DeliveryFenceTest.kt create mode 100644 jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java create mode 100644 jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java rename jvm/{runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal => runtime/src/main/java/com/chunkzero/chunk/runtime}/DeliveryFence.java (97%) create mode 100644 jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/PlayerProfile.java create mode 100644 jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java create mode 100644 jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java create mode 100644 jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java diff --git a/docs/distribution.md b/docs/distribution.md index f5dd0075..f687dfbe 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -8,9 +8,9 @@ How the chunk SDK is packaged, installed and published. A release of the SDK has TypeScript SDK, the project templates, the Gradle wrapper and the version pins that `chunk create` writes. - **The JVM artifacts**, published to `https://maven.chunkzero.com` (nightlies to `https://maven.chunkzero.com/nightlies`) under `com.chunkzero.chunk`: `proto`, `backend-api`, `backend-client`, - `backend-client-kotlin`, `runtime`, `runtime-minestom`, `runtime-minestom-kotlin`, the Gradle plugin and its three - plugin markers. Each library has `-sources.jar` and `-javadoc.jar` (Dokka HTML) artifacts. Projects resolve these - through Gradle; the CLI archives contain no JARs. + `backend-client-kotlin`, `runtime`, `runtime-minestom`, `runtime-minestom-kotlin`, `minestom-primitives`, the Gradle + plugin and its three plugin markers. Each library has `-sources.jar` and `-javadoc.jar` (Dokka HTML) artifacts. + Projects resolve these through Gradle; the CLI archives contain no JARs. Linux archives are built on Ubuntu 24.04 and need its glibc and OpenSSL 3; macOS and Windows archives link OpenSSL statically. Users need Java to run Gradle; the Minestom runtime needs Java 25, which the generated projects download diff --git a/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/CoinCommandTest.java b/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/CoinCommandTest.java index 1cec2b22..69933a06 100644 --- a/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/CoinCommandTest.java +++ b/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/CoinCommandTest.java @@ -9,6 +9,7 @@ import chunk.sync.v1.CoreOuterClass.Position; import chunk.sync.v1.CoreOuterClass.SubscribeRequest; import chunk.sync.v1.CoreOuterClass.Update; +import chunk.sync.v1.Gateway.PlayerIdentity; import chunk.sync.v1.Jvm.JvmDelivery; import chunk.sync.v1.Jvm.JvmSession; @@ -112,20 +113,24 @@ public void subscribe( new SessionRegistration( "arena", () -> ExampleSessions.INSTANCE.arena("Arena"))), - (session, app) -> + Map.of()); + var host = + ChunkSessions.detached( + manager, + session -> BackendSession.overCore( channel, "test-credential-with-at-least-32-bytes", "build", new SessionIdentity( - new SessionId(session), app, Optional.empty()), + new SessionId(session), session, Optional.empty()), scheduler, Duration.ofSeconds(5))); var lobby = session("lobby"); var arena = session("arena"); try { - await(ticks, manager.create("lobby", lobby)); - await(ticks, manager.create("arena", arena)); + await(ticks, host.create("lobby", lobby)); + await(ticks, host.create("arena", arena)); var uuid = UUID.randomUUID(); long generation = 0; for (var destination : List.of("lobby", "arena", "arena")) { @@ -142,8 +147,19 @@ public SocketAddress getRemoteAddress() { }; connection.setClientState(ConnectionState.PLAY); var player = new ManagedPlayer(connection, new GameProfile(uuid, "player")); - player.setBinding( - "delivery", JvmDelivery.newBuilder().setGeneration(at(generation)).build()); + player.setDelivery( + new Delivery( + null, + "delivery", + JvmDelivery.newBuilder() + .setSession(destination) + .setGeneration(at(generation)) + .setPlayer( + PlayerIdentity.newBuilder() + .setUuid(uuid.toString()) + .setUsername("player")) + .build(), + 0)); var managed = manager.get(destination); try { await(ticks, managed.join(player)); @@ -165,8 +181,8 @@ public SocketAddress getRemoteAddress() { } } finally { try { - await(ticks, manager.finish("lobby", lobby)); - await(ticks, manager.finish("arena", arena)); + await(ticks, host.finish("lobby", lobby)); + await(ticks, host.finish("arena", arena)); } finally { channel.shutdownNow().awaitTermination(3, TimeUnit.SECONDS); server.shutdownNow().awaitTermination(3, TimeUnit.SECONDS); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7cc5dd76..7668e813 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,8 @@ grpc-api = { module = "io.grpc:grpc-api", version.ref = "grpc" } grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } grpc-netty = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } minestom = { module = "com.chunkzero.multistom:multistom", version.ref = "multistom" } +# Upstream Minestom, which apps that manage their own sessions may use instead of multistom. +upstream-minestom = { module = "net.minestom:minestom", version = "2026.10.05-26.2" } polar = { module = "dev.hollowcube:polar", version = "1.16.0" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } diff --git a/jvm/minestom-primitives/README.md b/jvm/minestom-primitives/README.md new file mode 100644 index 00000000..f9c616a4 --- /dev/null +++ b/jvm/minestom-primitives/README.md @@ -0,0 +1,43 @@ +# Minestom primitives + +Login handling for Minestom apps that run their own sessions through +[`ChunkSessions`](../runtime/README.md#running-your-own-sessions) instead of `runtime-minestom`. It compiles against +upstream [Minestom](https://minestom.net) (`net.minestom:minestom`) and leaves the Minestom dependency to the app, so +the app picks its Minestom build; it must speak the gateway's Minecraft version, Java Edition 26.2. + +`ChunkLogin` answers the gateway's `chunk:delivery` login request, admits only players this JVM was asked to admit, with +the profile the gateway authenticated, and releases each delivery once its player's connection closed. Everything else, +such as what a session is, where a player spawns and when they've arrived, stays with the app: + +```java +try (var chunk = ChunkProcess.connect()) { + MinecraftServer.setCompressionThreshold(0); // the gateway owns compression + var server = MinecraftServer.init(); + var sessions = chunk.host(games); // your SessionHandler + var login = ChunkLogin.create(sessions, (player, delivery) -> games.leave(player)); + var events = MinecraftServer.getGlobalEventHandler(); + events.addChild(login.events()); + events.addListener(AsyncPlayerConfigurationEvent.class, event -> { + var delivery = login.delivery(event.getPlayer()); + event.setSpawningInstance(games.spawn(delivery.session(), event.getPlayer())); + }); + events.addListener(PlayerSpawnEvent.class, event -> { + if (event.isFirstSpawn()) login.delivery(event.getPlayer()).arrived(); + }); + MinecraftServer.getSchedulerManager() + .buildTask(chunk::tick) + .repeat(TaskSchedule.tick(1)) + .schedule(); + server.start(new InetSocketAddress(chunk.playerAddress(), 0)); + chunk.bind(MinecraftServer.getServer().getPort(), MinecraftServer.PROTOCOL_VERSION); + chunk.ready(); + chunk.awaitShutdown(); +} +``` + +- Keep Minestom in offline mode: the gateway authenticates players and owns encryption and compression. +- The leave handler runs once a player's connection closed, whether they left or core withdrew their delivery, and the + delivery is released when it settles. A session ends only after its players are released. +- `login.delivery(player)` also gives `move(destination)` and `operationId(action)` for that player. +- Isolation is the app's choice: several sessions can share instances, own several each, or the app can run one session + per JVM through its machine profile's `max_sessions = 1`. diff --git a/jvm/minestom-primitives/build.gradle.kts b/jvm/minestom-primitives/build.gradle.kts new file mode 100644 index 00000000..230d73b7 --- /dev/null +++ b/jvm/minestom-primitives/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("chunk.java-conventions") + id("chunk.publishing-conventions") +} +java { toolchain.languageVersion = JavaLanguageVersion.of(25) } +tasks.withType().configureEach { options.release = 25 } +dependencies { + api(project(":jvm:runtime")) + // Apps bring their own Minestom; this module only uses API upstream Minestom shares. + compileOnly(libs.upstream.minestom) + compileOnly(libs.jetbrains.annotations) + testImplementation(libs.upstream.minestom) + testImplementation(project(":jvm:proto")) +} diff --git a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java new file mode 100644 index 00000000..78c99965 --- /dev/null +++ b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java @@ -0,0 +1,163 @@ +package com.chunkzero.chunk.minestom; + +import com.chunkzero.chunk.runtime.ChunkSessions; +import com.chunkzero.chunk.runtime.Delivery; +import com.chunkzero.chunk.runtime.PlayerProfile; + +import net.kyori.adventure.text.Component; +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Player; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; +import net.minestom.server.event.player.AsyncPlayerPreLoginEvent; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; +import net.minestom.server.timer.Task; +import net.minestom.server.timer.TaskSchedule; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; + +/** + * Admits the players core delivers to a Minestom server whose app runs its own sessions through + * {@link ChunkSessions}. Add {@link #events()} to the server's event handler: each login answers + * the gateway's {@code chunk:delivery} request, and is refused unless it presents a delivery this + * JVM prepared. Where the player spawns, and when they {@linkplain Delivery#arrived() arrive}, is + * up to the app. + * + *

Once a player's connection closes, whether they left or their delivery was withdrawn, it runs + * the app's leave handler and then releases the delivery. + */ +public final class ChunkLogin implements AutoCloseable { + private static final System.Logger LOG = System.getLogger(ChunkLogin.class.getName()); + + private final ChunkSessions sessions; + private final BiFunction> leave; + private final EventNode events = EventNode.all("chunk-login"); + private final Map admissions = new ConcurrentHashMap<>(); + private final Task sweep; + + private ChunkLogin( + ChunkSessions sessions, BiFunction> leave) { + this.sessions = Objects.requireNonNull(sessions); + this.leave = Objects.requireNonNull(leave); + events.addListener(AsyncPlayerPreLoginEvent.class, this::preLogin); + sweep = + MinecraftServer.getSchedulerManager() + .buildTask(this::sweep) + .repeat(TaskSchedule.tick(1)) + .schedule(); + } + + /** Admits delivered players, and releases each once their connection closes. */ + public static ChunkLogin create(ChunkSessions sessions) { + return new ChunkLogin( + sessions, (player, delivery) -> CompletableFuture.completedFuture(null)); + } + + /** + * Admits delivered players. Once a player's connection closes, {@code leave} runs, and their + * delivery is released once it settles, so the session can still act for them meanwhile. + */ + public static ChunkLogin create( + ChunkSessions sessions, BiFunction> leave) { + return new ChunkLogin(sessions, leave); + } + + /** The login listeners; add them to the server's event handler. */ + public EventNode events() { + return events; + } + + /** + * The delivery {@code player} was admitted under. + * + * @throws IllegalArgumentException if core did not deliver the player, or they were released + */ + public Delivery delivery(Player player) { + var admission = admissions.get(player.getPlayerConnection()); + if (admission == null) throw new IllegalArgumentException("Player was not delivered"); + return admission.delivery; + } + + /** Stops releasing players whose connection closed. */ + @Override + public void close() { + sweep.cancel(); + } + + private void preLogin(AsyncPlayerPreLoginEvent event) { + var connection = event.getConnection(); + try { + var payload = + event.sendPluginRequest("chunk:delivery", new byte[0]) + .get(5, TimeUnit.SECONDS) + .payload(); + var presented = event.getGameProfile(); + var delivery = + sessions.admit( + payload, presented.uuid(), presented.name(), connection::disconnect); + admissions.put(connection, new Admission(delivery)); + event.setGameProfile(profile(delivery.player())); + } catch (Exception ignored) { + connection.kick(Component.text("Delivery rejected")); + } + } + + private void sweep() { + admissions.forEach( + (connection, admission) -> { + if (connection.isOnline() || admission.leaving) return; + admission.leaving = true; + var player = connection.getPlayer(); + CompletionStage left; + try { + left = + player == null + ? CompletableFuture.completedFuture(null) + : leave.apply(player, admission.delivery); + } catch (RuntimeException error) { + left = CompletableFuture.failedFuture(error); + } + left.whenComplete( + (ignored, error) -> { + if (error != null) + LOG.log( + System.Logger.Level.WARNING, + "Player leave did not complete cleanly", + error); + admissions.remove(connection, admission); + admission.delivery.release(); + }); + }); + } + + private static GameProfile profile(PlayerProfile player) { + return new GameProfile( + player.uuid(), + player.name(), + player.properties().stream() + .map( + property -> + new GameProfile.Property( + property.name(), + property.value(), + property.signature())) + .toList()); + } + + private static final class Admission { + final Delivery delivery; + // Only the scheduler thread reads and writes it. + boolean leaving; + + Admission(Delivery delivery) { + this.delivery = delivery; + } + } +} diff --git a/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java b/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java new file mode 100644 index 00000000..5fc663c6 --- /dev/null +++ b/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java @@ -0,0 +1,216 @@ +package com.chunkzero.chunk.runtime; + +import static org.junit.jupiter.api.Assertions.*; + +import chunk.sync.v1.CoreOuterClass.Position; +import chunk.sync.v1.Gateway.PlayerIdentity; +import chunk.sync.v1.Gateway.PlayerProperty; +import chunk.sync.v1.Jvm.JvmDelivery; +import chunk.sync.v1.Jvm.JvmDeliveryPhase; +import chunk.sync.v1.Jvm.JvmSession; +import chunk.sync.v1.Jvm.PlayerSetup; + +import com.chunkzero.chunk.minestom.ChunkLogin; +import com.google.protobuf.ByteString; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.network.ConnectionState; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.network.packet.PacketVanilla; +import net.minestom.server.network.packet.client.handshake.ClientHandshakePacket; +import net.minestom.server.network.packet.client.login.ClientLoginPluginResponsePacket; +import net.minestom.server.network.packet.client.login.ClientLoginStartPacket; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.network.packet.server.login.LoginDisconnectPacket; +import net.minestom.server.network.packet.server.login.LoginPluginRequestPacket; +import net.minestom.server.network.packet.server.login.LoginSuccessPacket; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +class ChunkLoginTest { + private final UUID uuid = UUID.randomUUID(); + private final Map topic = new TreeMap<>(); + + @Test + void upstreamMinestomAdmitsOnlyDeliveredPlayersAndReleasesThemOnceDisconnected() + throws Exception { + MinecraftServer.setCompressionThreshold(0); + var server = MinecraftServer.init(); + var host = + ChunkSessions.detached( + new SessionHandler() { + @Override + public void create(SessionControl session) { + session.ready(); + } + + @Override + public void finish(SessionControl session) { + session.ended(); + } + }, + session -> null); + var sockets = new ArrayList(); + try (var login = ChunkLogin.create(host)) { + MinecraftServer.getGlobalEventHandler().addChild(login.events()); + server.start(new InetSocketAddress("127.0.0.1", 0)); + var port = MinecraftServer.getServer().getPort(); + topic.put( + "session/game", + JvmSession.newBuilder() + .setSessionType("app/default") + .setCapacity(4) + .build() + .toByteString()); + var first = prepare(host, "first", 1); + for (var invalid : + java.util.List.of( + first.toBuilder().setCapability(ByteString.EMPTY).build(), + first.toBuilder().setOperationId("unknown").build())) { + var socket = login(port, "player", invalid); + sockets.add(socket); + assertInstanceOf(LoginDisconnectPacket.class, packet(socket)); + } + var other = login(port, "other", first); + sockets.add(other); + assertInstanceOf(LoginDisconnectPacket.class, packet(other)); + + var admitted = login(port, "player", first); + sockets.add(admitted); + var success = assertInstanceOf(LoginSuccessPacket.class, packet(admitted)); + assertEquals("signature", success.gameProfile().properties().getFirst().signature()); + assertEquals(JvmDeliveryPhase.JVM_DELIVERY_PHASE_ATTACHED, phase(host, "first")); + + // Withdrawal disconnects the player, who is released once their connection closed. + topic.put( + "delivery/first", + delivery(1).toBuilder().setWithdraw(true).build().toByteString()); + host.apply(topic); + await(host, "first", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED); + // The release lets a newer delivery admit the same player. + var next = prepare(host, "next", 2); + var again = login(port, "player", next); + sockets.add(again); + assertInstanceOf(LoginSuccessPacket.class, packet(again)); + } finally { + for (var socket : sockets) socket.close(); + host.close(); + MinecraftServer.stopCleanly(); + } + } + + private PlayerSetup prepare(ChunkSessions host, String operation, long revision) { + topic.put("delivery/" + operation, delivery(revision).toByteString()); + host.apply(topic); + // A delivery listed with its session waits for the next sweep. + host.sweep(); + var status = + host.inventory().getDeliveriesList().stream() + .filter(delivery -> delivery.getOperationId().equals(operation)) + .findFirst() + .orElseThrow(); + assertEquals(JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED, status.getPhase()); + return PlayerSetup.newBuilder() + .setOperationId(operation) + .setCapability(status.getCapability()) + .build(); + } + + private JvmDelivery delivery(long revision) { + return JvmDelivery.newBuilder() + .setSession("game") + .setGeneration(Position.newBuilder().setEpoch(1).setRevision(revision)) + .setPlayer( + PlayerIdentity.newBuilder() + .setUuid(uuid.toString()) + .setUsername("player") + .addProperties( + PlayerProperty.newBuilder() + .setName("textures") + .setValue("value") + .setSignature("signature"))) + .build(); + } + + private static JvmDeliveryPhase phase(ChunkSessions host, String operation) { + return host.inventory().getDeliveriesList().stream() + .filter(delivery -> delivery.getOperationId().equals(operation)) + .findFirst() + .orElseThrow() + .getPhase(); + } + + private static void await(ChunkSessions host, String operation, JvmDeliveryPhase phase) + throws InterruptedException { + var deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (phase(host, operation) != phase) { + assertTrue(System.nanoTime() < deadline, operation + " never reached " + phase); + Thread.sleep(10); + } + } + + /** Starts a login on {@code port}, answering the delivery request with {@code setup}. */ + private Socket login(int port, String name, PlayerSetup setup) throws IOException { + var socket = new Socket("127.0.0.1", port); + socket.setSoTimeout(10_000); + send( + socket, + 0, + ClientHandshakePacket.SERIALIZER, + new ClientHandshakePacket( + MinecraftServer.PROTOCOL_VERSION, + "localhost", + 25565, + ClientHandshakePacket.Intent.LOGIN)); + send(socket, 0, ClientLoginStartPacket.SERIALIZER, new ClientLoginStartPacket(name, uuid)); + var challenge = assertInstanceOf(LoginPluginRequestPacket.class, packet(socket)); + assertEquals("chunk:delivery", challenge.channel()); + send( + socket, + 2, + ClientLoginPluginResponsePacket.SERIALIZER, + new ClientLoginPluginResponsePacket(challenge.messageId(), setup.toByteArray())); + return socket; + } + + private static void send(Socket socket, int id, NetworkBuffer.Type type, T packet) + throws IOException { + var body = + NetworkBuffer.makeArray( + buffer -> { + buffer.write(NetworkBuffer.VAR_INT, id); + buffer.write(type, packet); + }); + socket.getOutputStream() + .write( + NetworkBuffer.makeArray( + buffer -> { + buffer.write(NetworkBuffer.VAR_INT, body.length); + buffer.write(NetworkBuffer.RAW_BYTES, body); + })); + } + + private static ServerPacket packet(Socket socket) throws IOException { + var input = socket.getInputStream(); + var length = 0; + for (var shift = 0; ; shift += 7) { + var next = input.read(); + if (next < 0 || shift >= 21) throw new IOException("Truncated frame"); + length |= (next & 127) << shift; + if ((next & 128) == 0) break; + } + var bytes = input.readNBytes(length); + var buffer = NetworkBuffer.wrap(bytes, 0, bytes.length); + return PacketVanilla.SERVER_PACKET_PARSER.parse( + ConnectionState.LOGIN, buffer.read(NetworkBuffer.VAR_INT), buffer); + } +} diff --git a/jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt b/jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt index c0493020..303f9193 100644 --- a/jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt +++ b/jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt @@ -167,14 +167,15 @@ class SessionCoroutinesTest { } assertTrue(until()) } + val host = ChunkSessions.detached(manager) { null } try { val game = session("game") val other = session("flat") - val created = manager.create("game", game) - val independent = manager.create("other", other) + val created = host.create("game", game) + val independent = host.create("other", other) pump { created.isDone && independent.isDone && observed } assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, created.join().phase) - val ending = manager.finish("game", game) + val ending = host.finish("game", game) pump { mutationStarted.isDone } assertFalse(ending.isDone) assertFalse(watchClosed.isDone) @@ -183,7 +184,7 @@ class SessionCoroutinesTest { assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDED, ending.join().phase) assertTrue(committed) assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, manager.get("other").phase) - val stopOther = manager.finish("other", other) + val stopOther = host.finish("other", other) pump { stopOther.isDone } } finally { result.complete(Unit) diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java index 1c55b3a0..7f632092 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java @@ -1,13 +1,9 @@ package com.chunkzero.chunk.runtime; -import chunk.sync.v1.Jvm.JvmSessionStatus; - import com.chunkzero.chunk.runtime.minestom.internal.AppRegistry; import com.chunkzero.chunk.runtime.minestom.internal.ComponentRegistry; import com.chunkzero.chunk.runtime.minestom.internal.GameplayService; -import com.chunkzero.chunk.runtime.minestom.internal.ProcessService; import com.chunkzero.chunk.runtime.minestom.internal.SessionMethodRegistry; -import com.chunkzero.chunk.runtime.minestom.internal.SessionMethodService; import net.minestom.server.MinecraftConstants; import net.minestom.server.ServerProcess; @@ -16,7 +12,6 @@ import org.jetbrains.annotations.Nullable; -import java.net.Inet6Address; import java.net.InetSocketAddress; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -26,11 +21,9 @@ public final class ChunkMinestom implements AutoCloseable { private final ChunkProcess chunk; private final ServerProcess server; private final TickExecutor ticks = new TickExecutor(); - private final SessionManager sessions; private final ComponentRegistry components; + private final ChunkSessions sessions; private final GameplayService gameplay; - private final SessionMethodService methods; - private final ProcessService service; private final AtomicBoolean closed = new AtomicBoolean(); private final Thread shutdownHook; private @Nullable Task task; @@ -42,28 +35,20 @@ private ChunkMinestom(ChunkProcess chunk, ServerProcess server) { var factories = AppRegistry.load(chunk.app(), Thread.currentThread().getContextClassLoader()); components = ComponentRegistry.load(Thread.currentThread().getContextClassLoader()); - sessions = + var manager = new SessionManager( server, ticks, factories, - (session, appId) -> chunk.backend(session), - components); - sessions.setMover(chunk::move); - server.setCompressionThreshold(0); - server.connectionManager().setPlayerProvider(ManagedPlayer::new); - gameplay = new GameplayService(sessions, System::nanoTime, chunk::isReady); - methods = - new SessionMethodService( - sessions, + components, SessionMethodRegistry.load( chunk.app(), factories.keySet(), - Thread.currentThread().getContextClassLoader()), - gameplay::arrived, - chunk::methodResult, - System::currentTimeMillis); - service = new ProcessService(sessions, chunk::isReady, gameplay, methods); + Thread.currentThread().getContextClassLoader())); + server.setCompressionThreshold(0); + server.connectionManager().setPlayerProvider(ManagedPlayer::new); + sessions = chunk.host(manager); + gameplay = new GameplayService(manager, sessions); shutdownHook = new Thread(this::close, "chunk-minestom-shutdown"); Runtime.getRuntime().addShutdownHook(shutdownHook); } @@ -88,26 +73,12 @@ public synchronized void start() { () -> { ticks.flush(); gameplay.flush(); - chunk.flush(); - var inventory = sessions.inventory(); - chunk.progress( - sessions.activeCount(), - inventory.stream() - .mapToInt(JvmSessionStatus::getAttached) - .sum()); + chunk.tick(); }) .repeat(TaskSchedule.tick(1)) .schedule(); - var address = chunk.playerAddress(); - server.start(new InetSocketAddress(address, 0)); - var host = - address instanceof Inet6Address - ? "[" + address.getHostAddress() + "]" - : address.getHostAddress(); - chunk.bind( - host + ":" + server.server().getPort(), - MinecraftConstants.PROTOCOL_VERSION, - service); + server.start(new InetSocketAddress(chunk.playerAddress(), 0)); + chunk.bind(server.server().getPort(), MinecraftConstants.PROTOCOL_VERSION); started = true; } catch (RuntimeException error) { close(); @@ -125,7 +96,7 @@ public synchronized void close() { () -> { if (task != null) task.cancel(); }, - methods::close, + sessions::close, gameplay::close, components::close, this::removeShutdownHook); diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java index a90e42fe..50ed7709 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java @@ -1,7 +1,5 @@ package com.chunkzero.chunk.runtime; -import chunk.sync.v1.Jvm.JvmDelivery; - import net.minestom.server.entity.Player; import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.PlayerConnection; @@ -14,29 +12,20 @@ @ApiStatus.Internal public final class ManagedPlayer extends Player { - private @Nullable String operation; - private @Nullable JvmDelivery binding; + private @Nullable Delivery delivery; private @Nullable CompletableFuture initialization; ManagedPlayer(PlayerConnection connection, GameProfile profile) { super(connection, profile); } - JvmDelivery getBinding() { - if (binding == null) throw new IllegalStateException("Player delivery has not been bound"); - return binding; - } - - /** The delivery's operation ID, as in its {@code delivery/} key. */ - String getOperation() { - if (operation == null) - throw new IllegalStateException("Player delivery has not been bound"); - return operation; + Delivery getDelivery() { + if (delivery == null) throw new IllegalStateException("Player delivery has not been bound"); + return delivery; } - public void setBinding(String operation, JvmDelivery binding) { - this.operation = Objects.requireNonNull(operation); - this.binding = Objects.requireNonNull(binding); + public void setDelivery(Delivery delivery) { + this.delivery = Objects.requireNonNull(delivery); } @Nullable diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java index 914b4e6a..2db11f5f 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java @@ -1,11 +1,7 @@ package com.chunkzero.chunk.runtime; -import chunk.sync.v1.Jvm.JvmSession; import chunk.sync.v1.Jvm.JvmSessionPhase; -import chunk.sync.v1.Jvm.JvmSessionStatus; -import com.chunkzero.chunk.backend.api.BackendJson; -import com.chunkzero.chunk.backend.client.BackendSession; import com.chunkzero.chunk.runtime.minestom.event.SessionCreateEvent; import com.chunkzero.chunk.runtime.minestom.event.SessionJoinEvent; import com.chunkzero.chunk.runtime.minestom.event.SessionLeaveEvent; @@ -17,10 +13,6 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -import tools.jackson.databind.JsonNode; - -import java.time.Duration; -import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; @@ -30,36 +22,24 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; -import java.util.regex.Pattern; import java.util.stream.Collectors; +/** + * Runs each session as a {@link Session} with its own {@link SessionScope} in the app's shared + * Minestom process. Hooks start on the tick thread. + */ @ApiStatus.Internal -public final class SessionManager { - private static final Pattern SESSION_ID = Pattern.compile("[A-Za-z0-9_-]{1,128}"); - - /** Matches how long control waits for a created session to become ready. */ - private static final Duration CREATE_DEADLINE = Duration.ofSeconds(10); - +public final class SessionManager implements SessionHandler { + private static final System.Logger LOG = System.getLogger(SessionManager.class.getName()); private final ServerProcess process; private final TickExecutor ticks; private final Map factories; - private final @Nullable BiFunction backend; private final ComponentRegistry components; + private final Map> methods; private final Map sessions = new ConcurrentHashMap<>(); - /** Sessions that failed to start or were never created, as reported to control. */ - private final Map outcomes = new ConcurrentHashMap<>(); - - private Function> withdraw = - ignored -> CompletableFuture.completedFuture(null); - private SessionScope.Mover mover = SessionScope.Mover.UNAVAILABLE; - private Duration createDeadline = CREATE_DEADLINE; - SessionManager( ServerProcess process, TickExecutor ticks, Map> factories) { this( @@ -72,35 +52,28 @@ public final class SessionManager { entry -> new SessionRegistration( entry.getKey(), entry.getValue()))), - null); + Map.of()); } SessionManager( ServerProcess process, TickExecutor ticks, Map factories, - @Nullable BiFunction backend) { - this(process, ticks, factories, backend, new ComponentRegistry(List.of())); + Map> methods) { + this(process, ticks, factories, new ComponentRegistry(List.of()), methods); } SessionManager( ServerProcess process, TickExecutor ticks, Map factories, - @Nullable BiFunction backend, - ComponentRegistry components) { + ComponentRegistry components, + Map> methods) { this.process = process; this.ticks = ticks; this.factories = Map.copyOf(factories); - this.backend = backend; this.components = components; - } - - /** - * Completes after the tick thread runs the work queued before it, such as session creations. - */ - public CompletableFuture afterQueued() { - return ticks.submit(() -> null); + this.methods = Map.copyOf(methods); } public TickExecutor getTicks() { @@ -111,283 +84,138 @@ public ServerProcess getProcess() { return process; } - public void setWithdraw(Function> withdraw) { - this.withdraw = withdraw; - } - - void setMover(SessionScope.Mover mover) { - this.mover = mover; - } - - void setCreateDeadline(Duration createDeadline) { - this.createDeadline = createDeadline; - } - - /** - * Creates the session, or reports it failed if it cannot start. Beyond 256 live sessions it - * reports nothing, since control still counts the session and so never sees room for it. - */ - public CompletableFuture create(String id, JvmSession session) { - return ticks.submit( + @Override + public void create(SessionControl control) { + ticks.submit( () -> { - if (!sessions.containsKey(id) && activeCount() >= 256) - throw new IllegalStateException("Too many live sessions"); - try { - return start(id, session); - } catch (RuntimeException error) { - if (!sessions.containsKey(id)) - outcome(id, session, JvmSessionPhase.JVM_SESSION_PHASE_FAILED); - throw error; - } + var registration = factories.get(control.type()); + if (registration == null) + throw new IllegalArgumentException("Unknown session type"); + var managed = + new ManagedSession( + control, + registration.create( + control.capacity(), + control.configurationJson())); + sessions.put(control.id(), managed); + managed.start(); + return null; }) - .thenCompose(managed -> managed.ready.thenApply(ignored -> managed.status())); + .exceptionally( + error -> { + control.fail(error); + return null; + }); } - /** Reports a session this process will not create, such as one requested while draining. */ - public void reject(String id, JvmSession session) { + @Override + public void finish(SessionControl control) { ticks.submit( - () -> { - if (!sessions.containsKey(id)) - outcome(id, session, JvmSessionPhase.JVM_SESSION_PHASE_FAILED); - return null; - }); - } - - private ManagedSession start(String id, JvmSession session) { - if (!SESSION_ID.matcher(id).matches() - || session.getCapacity() < 1 - || session.getCapacity() > 128) { - throw new IllegalArgumentException("Invalid session"); - } - var configuration = configuration(session); - var previous = sessions.get(id); - if (previous != null) { - if (!previous.session.getSessionType().equals(session.getSessionType()) - || previous.session.getCapacity() != session.getCapacity() - || !configuration(previous.session).equals(configuration)) - throw new IllegalArgumentException("Session creation changed"); - return previous; - } - var factory = factories.get(session.getSessionType()); - if (factory == null) throw new IllegalArgumentException("Unknown session type"); - var managed = new ManagedSession(id, session, factory, configuration.toString()); - outcomes.remove(id); - sessions.put(id, managed); - managed.start(); - return managed; - } - - private static JsonNode configuration(JvmSession session) { - var bytes = session.getConfigurationJson(); - if (bytes.size() > 64 * 1024 || !bytes.isValidUtf8()) - throw new IllegalArgumentException("Invalid session configuration encoding or size"); - var config = BackendJson.mapper().readTree(bytes.isEmpty() ? "{}" : bytes.toStringUtf8()); - if (config == null || !config.isObject()) - throw new IllegalArgumentException("Session configuration must be an object"); - return config; - } - - /** - * Ends session {@code id}. One that never started ends at once, reported with {@code session}'s - * type and capacity. - */ - public CompletableFuture finish(String id, JvmSession session) { - var current = sessions.get(id); - if (current != null) current.methodsClosed = true; - return ticks.submit( () -> { - var managed = sessions.get(id); - if (managed == null) - return CompletableFuture.completedFuture( - outcome( - id, - session, - JvmSessionPhase.JVM_SESSION_PHASE_ENDED)); - return managed.finish().thenApply(ignored -> managed.status()); + var managed = sessions.get(control.id()); + if (managed == null) control.ended(); + else managed.finish(); + return null; }) - .thenCompose(Function.identity()); - } - - private JvmSessionStatus outcome(String id, JvmSession session, JvmSessionPhase phase) { - var status = - JvmSessionStatus.newBuilder() - .setId(id) - .setSessionType(session.getSessionType()) - .setCapacity(session.getCapacity()) - .setPhase(phase) - .build(); - outcomes.put(id, status); - return status; + .exceptionally( + error -> { + control.ended(error); + return null; + }); } - /** Drops the record of a session control no longer tracks, once it can hold nothing. */ - public void forget(String id) { - ticks.submit( + @Override + public CompletionStage method( + SessionControl control, String method, String argumentsJson) { + var binding = methods.get(control.type() + "/" + method); + if (binding == null) + return CompletableFuture.failedFuture( + new IllegalArgumentException("Undeclared session method")); + return ticks.submit( () -> { - var session = sessions.get(id); - if (session == null || terminal(session.phase)) { - sessions.remove(id); - outcomes.remove(id); - } - return null; + var managed = sessions.get(control.id()); + if (managed == null + || control.phase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) + throw new IllegalStateException("Session unavailable"); + return binding.invoke(managed.behavior, argumentsJson); }); } - public static boolean terminal(JvmSessionPhase phase) { - return phase == JvmSessionPhase.JVM_SESSION_PHASE_ENDED - || phase == JvmSessionPhase.JVM_SESSION_PHASE_FAILED; - } - - /** The phase of session {@code id}, or null while this manager has no record of it. */ - public @Nullable JvmSessionPhase phase(String id) { - var session = sessions.get(id); - if (session != null) return session.phase; - var outcome = outcomes.get(id); - return outcome == null ? null : outcome.getPhase(); - } - + /** The ready session {@code id}. */ public ManagedSession get(String id) { var session = sessions.get(id); if (session == null) throw new IllegalArgumentException("Unknown session"); - if (session.phase != JvmSessionPhase.JVM_SESSION_PHASE_READY) + if (session.getPhase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) throw new IllegalStateException("Session unavailable"); return session; } - public List inventory() { - var inventory = new ArrayList(); - sessions.values().forEach(session -> inventory.add(session.status())); - inventory.addAll(outcomes.values()); - return inventory; - } - - int activeCount() { - return (int) - sessions.values().stream() - .filter( - session -> - switch (session.phase) { - case JVM_SESSION_PHASE_STARTING, - JVM_SESSION_PHASE_READY, - JVM_SESSION_PHASE_ENDING -> - true; - default -> false; - }) - .count(); - } - @ApiStatus.Internal public final class ManagedSession { - private final String id; - private final JvmSession session; + private final SessionControl control; private final Session behavior; private final SessionScope scope; - private final CompletableFuture ready = new CompletableFuture<>(); - private final CompletableFuture ended = new CompletableFuture<>(); private final Set joined = Collections.newSetFromMap(new IdentityHashMap<>()); - private volatile JvmSessionPhase phase = JvmSessionPhase.JVM_SESSION_PHASE_STARTING; private boolean finishing; - private volatile boolean methodsClosed; - private @Nullable Throwable creationFailure; - ManagedSession( - String id, - JvmSession session, - SessionRegistration registration, - String configuration) { - this.id = id; - this.session = session; - behavior = registration.create(session.getCapacity(), configuration); + ManagedSession(SessionControl control, Session behavior) { + this.control = control; + this.behavior = behavior; scope = new SessionScope( process, - id, + control.id(), ticks, - this::finish, - registration.backend(id, backend), - components, - (delivery, generation, destination) -> - mover.move(delivery, generation, destination)); + control::finish, + control.backend(), + components); } public String getSessionType() { - return session.getSessionType(); + return control.type(); } public int getCapacity() { - return session.getCapacity(); + return control.capacity(); } public JvmSessionPhase getPhase() { - return phase; + return control.phase(); } public SessionScope getScope() { return scope; } - public String invokeMethod(SessionMethodBinding binding, String arguments) { - ticks.checkThread(); - requireMethodReady(); - return binding.invoke(behavior, arguments); - } - - public void requireMethodReady() { - if (methodsClosed || phase != JvmSessionPhase.JVM_SESSION_PHASE_READY) - throw new IllegalStateException("Session unavailable"); - } - void start() { - // The deadline settles a copy of the session's own future, so a late completion of the - // latter is ignored and its failures pass through unchanged. orTimeout fires on the - // JDK's - // own delay scheduler, so session work that fills the common pool can't hold it back. - var created = new CompletableFuture(); - var deadline = - new CompletableFuture() - .orTimeout(createDeadline.toMillis(), TimeUnit.MILLISECONDS); - deadline.whenComplete( - (ignored, error) -> { - if (error != null) - created.completeExceptionally( - new TimeoutException( - "onCreate did not finish within " + createDeadline)); - }); invoke(() -> behavior.onCreate(scope)) .whenComplete( - (ignored, error) -> { - deadline.complete(null); - if (error == null) created.complete(null); - else created.completeExceptionally(error); - }); - created.whenComplete( - (ignored, error) -> - ticks.submit( - () -> { - if (error == null && !scope.getInstances().isEmpty()) { - if (!finishing) - phase = JvmSessionPhase.JVM_SESSION_PHASE_READY; - process.eventHandler() - .call(new SessionCreateEvent(scope)); - ready.complete(null); - } else { - creationFailure = creationFailure(error); - ready.completeExceptionally(creationFailure); - finish(); - } - return null; - })); - } - - private Throwable creationFailure(@Nullable Throwable error) { - if (error == null) return new IllegalStateException("Session has no instances"); - return error; + (ignored, error) -> + ticks.submit( + () -> { + // A creation past its deadline already ended. + if (finishing) return null; + if (error == null + && !scope.getInstances().isEmpty()) { + control.ready(); + process.eventHandler() + .call(new SessionCreateEvent(scope)); + } else { + control.fail( + error == null + ? new IllegalStateException( + "Session has no" + + " instances") + : error); + } + return null; + })); } public CompletableFuture join(Player player) { return ticks.submit( () -> { - if (phase != JvmSessionPhase.JVM_SESSION_PHASE_READY) + if (getPhase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) throw new IllegalStateException("Session unavailable"); scope.getPlayers().add(player); return invoke(() -> behavior.onJoin(player)); @@ -444,65 +272,32 @@ private CompletableFuture notifyLeave(Player player, @Nullable Throwable f }); } - CompletableFuture finish() { - methodsClosed = true; - ticks.submit( - () -> { - if (!finishing) { - finishing = true; - phase = JvmSessionPhase.JVM_SESSION_PHASE_ENDING; - // Creation must settle before scoped resources can be disposed. - ready.handle((ignored, error) -> null) - .thenCompose(ignored -> withdraw.apply(id)) - .thenCompose( - ignored -> - ticks.submit(() -> invoke(behavior::onFinish)) - .thenCompose(Function.identity())) - .whenComplete( - (ignored, error) -> - ticks.submit( - () -> { - try { - scope.dispose(); - } catch (Exception failure) { - // An unclosed resource keeps - // the - // session ending and counted. - ended.completeExceptionally( - failure); - return null; - } - var failure = - error == null - ? creationFailure - : error; - phase = - failure == null - ? JvmSessionPhase - .JVM_SESSION_PHASE_ENDED - : JvmSessionPhase - .JVM_SESSION_PHASE_FAILED; - if (failure == null) - ended.complete(null); - else - ended.completeExceptionally( - failure); - return null; - })); - } - return null; - }); - return ended; - } - - JvmSessionStatus status() { - return JvmSessionStatus.newBuilder() - .setId(id) - .setSessionType(session.getSessionType()) - .setCapacity(session.getCapacity()) - .setPhase(phase) - .setAttached(scope.getPlayers().size()) - .build(); + /** Runs {@code onFinish} and disposes the scope, on the tick thread. */ + void finish() { + finishing = true; + invoke(behavior::onFinish) + .whenComplete( + (ignored, error) -> + ticks.submit( + () -> { + try { + scope.dispose(); + } catch (Exception failure) { + // An unclosed resource keeps the session + // ending and counted. + LOG.log( + System.Logger.Level.ERROR, + "Session " + + control.id() + + " failed to dispose", + failure); + return null; + } + sessions.remove(control.id(), this); + if (error == null) control.ended(); + else control.ended(error); + return null; + })); } } diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java index 1e697e03..b6271261 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java @@ -1,10 +1,8 @@ package com.chunkzero.chunk.runtime; import com.chunkzero.chunk.backend.api.BackendJson; -import com.chunkzero.chunk.backend.client.BackendSession; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.function.BiFunction; @@ -67,10 +65,4 @@ private static void requireEmptyConfiguration(String json) { throw new IllegalArgumentException( "Session provider does not declare creation configuration"); } - - @Nullable - BackendSession backend( - String sessionId, @Nullable BiFunction clients) { - return clients == null ? null : clients.apply(sessionId, appId); - } } diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java index dc0ad255..59d6a434 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java @@ -1,7 +1,5 @@ package com.chunkzero.chunk.runtime; -import chunk.sync.v1.CoreOuterClass.Position; - import com.chunkzero.chunk.backend.api.Destination; import com.chunkzero.chunk.backend.client.BackendSession; import com.chunkzero.chunk.backend.client.OperationId; @@ -45,7 +43,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; -import java.util.regex.Pattern; /** * The gameplay isolate of one session within the shared server process. It owns the session's @@ -53,7 +50,6 @@ * until disposal. It is not a memory or failure boundary. */ public final class SessionScope { - private static final Pattern ACTION = Pattern.compile("[A-Za-z0-9_-]{1,32}"); private final ServerProcess process; private final String id; @@ -61,7 +57,6 @@ public final class SessionScope { private final Supplier> requestFinish; private final @Nullable BackendSession backend; private final ComponentRegistry components; - private final Mover mover; private final List ownedInstances = new CopyOnWriteArrayList<>(); private final List resources = new ArrayList<>(); private final Map> playerResources = new IdentityHashMap<>(); @@ -88,24 +83,12 @@ public final class SessionScope { Supplier> requestFinish, @Nullable BackendSession backend, ComponentRegistry components) { - this(process, id, ticks, requestFinish, backend, components, Mover.UNAVAILABLE); - } - - SessionScope( - ServerProcess process, - String id, - TickExecutor ticks, - Supplier> requestFinish, - @Nullable BackendSession backend, - ComponentRegistry components, - Mover mover) { this.process = process; this.id = id; this.ticks = ticks; this.requestFinish = requestFinish; this.backend = backend; this.components = components; - this.mover = mover; events = EventNode.event("session-" + id, EventFilter.ALL, this::owns); try { var schedulers = process.schedulerManager(); @@ -271,20 +254,8 @@ public CompletableFuture onTick(Runnable action) { /** Stable mutation identity for one action on this exact player delivery. */ public OperationId operationId(Player player, String action) { - if (!ACTION.matcher(action).matches()) - throw new IllegalArgumentException("Invalid operation action"); if (!players.contains(player)) throw new IllegalArgumentException("Player is not admitted"); - var generation = ((ManagedPlayer) player).getBinding().getGeneration(); - return new OperationId( - id - + "/" - + player.getUuid() - + "/" - + generation.getEpoch() - + "." - + generation.getRevision() - + "/" - + action); + return ((ManagedPlayer) player).getDelivery().operationId(action); } /** @@ -296,9 +267,7 @@ public OperationId operationId(Player player, String action) { public CompletionStage move(Player player, Destination destination) { Objects.requireNonNull(destination); if (!players.contains(player)) throw new IllegalArgumentException("Player is not admitted"); - var managed = (ManagedPlayer) player; - return mover.move( - managed.getOperation(), managed.getBinding().getGeneration(), destination); + return ((ManagedPlayer) player).getDelivery().move(destination); } public CompletionStage finish() { @@ -369,17 +338,6 @@ private void checkActive() { if (disposed) throw new IllegalStateException("Session disposed"); } - /** Asks core to move the player of a delivery, as {@code ChunkProcess} does. */ - interface Mover { - Mover UNAVAILABLE = - (delivery, generation, destination) -> - CompletableFuture.failedFuture( - new IllegalStateException("Moves unavailable")); - - CompletionStage move( - String delivery, Position generation, Destination destination); - } - private static void reject(AutoCloseable resource, String message) { var failure = new IllegalStateException(message); try { diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java new file mode 100644 index 00000000..9adc3d8b --- /dev/null +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java @@ -0,0 +1,148 @@ +package com.chunkzero.chunk.runtime.minestom.internal; + +import com.chunkzero.chunk.runtime.Delivery; +import com.chunkzero.chunk.runtime.ManagedPlayer; +import com.chunkzero.chunk.runtime.SessionManager; + +import net.minestom.server.instance.Instance; +import net.minestom.server.network.player.PlayerConnection; + +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +/** + * A connection admitted under a delivery: it joins the delivery's session once spawned, arrives + * once its client confirmed its position, and is released once it closed and left the session. + */ +final class AdmittedPlayer { + private static final System.Logger LOG = System.getLogger(AdmittedPlayer.class.getName()); + + private final PlayerConnection connection; + private final SessionManager manager; + private final CompletableFuture delivery = new CompletableFuture<>(); + private final CompletableFuture released = new CompletableFuture<>(); + private @Nullable SessionManager.ManagedSession session; + private @Nullable ManagedPlayer player; + private CompletableFuture joining = CompletableFuture.completedFuture(null); + private boolean joinStarted; + private boolean closed; + + AdmittedPlayer(PlayerConnection connection, SessionManager manager) { + this.connection = connection; + this.manager = manager; + } + + void admitted(Delivery admitted) { + delivery.complete(admitted); + } + + boolean isReleased() { + return released.isDone(); + } + + synchronized Instance configure(ManagedPlayer current) { + var admitted = delivery.getNow(null); + if (closed || admitted == null || connection != current.getPlayerConnection()) + throw new IllegalStateException("Delivery closed"); + var managed = manager.get(admitted.session()); + current.setDelivery(admitted); + session = managed; + player = current; + return managed.getScope().getInstances().getFirst(); + } + + /** Joins the spawned player to the session, reports arrival, and closes once offline. */ + synchronized void check() { + if (closed) return; + if (!connection.isOnline()) { + close(); + return; + } + if (player == null || session == null) return; + var initialization = player.getInitialization(); + var spawned = + initialization != null + && initialization.isDone() + && !initialization.isCompletedExceptionally(); + if (!spawned) return; + if (!joinStarted) { + joinStarted = true; + joining = session.join(player); + joining.whenComplete( + (ignored, error) -> { + if (error != null) close(); + }); + } + if (joining.isDone() + && !joining.isCompletedExceptionally() + && player.getLastSentTeleportId() > 0 + && player.getLastReceivedTeleportId() == player.getLastSentTeleportId()) + delivery.join().arrived(); + } + + /** + * Disconnects the player, awaits their join and spawn, removes them, runs the session's leave, + * and then releases the delivery. + */ + synchronized CompletableFuture close() { + if (closed) return released; + closed = true; + connection.disconnect(); + var ticks = manager.getTicks(); + var current = player; + var joined = session; + joining.handle((ignored, error) -> null) + .thenCompose( + ignored -> + ticks.submit( + () -> { + var initialization = + current == null + ? null + : current.getInitialization(); + return initialization == null + ? CompletableFuture + .completedFuture(null) + : initialization; + }) + .thenCompose( + initialization -> + initialization.handle( + (result, error) -> null))) + .thenCompose( + ignored -> + ticks.submit( + () -> { + if (current != null) { + connection + .process() + .connectionManager() + .removePlayer(connection); + if (current.getInstance() != null + && !current.isRemoved()) current.remove(); + } + return null; + })) + .thenCompose( + ignored -> + current == null || joined == null + ? CompletableFuture.completedFuture(null) + : joined.leave(current)) + .whenComplete( + (ignored, error) -> { + if (error != null) + LOG.log( + System.Logger.Level.WARNING, + "Player leave did not complete cleanly", + error); + synchronized (this) { + player = null; + session = null; + } + delivery.thenAccept(Delivery::release); + released.complete(null); + }); + return released; + } +} diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java index e778175b..9affb0ce 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java @@ -1,12 +1,8 @@ package com.chunkzero.chunk.runtime.minestom.internal; -import chunk.sync.v1.Jvm.JvmDelivery; -import chunk.sync.v1.Jvm.JvmDeliveryPhase; -import chunk.sync.v1.Jvm.JvmDeliveryStatus; -import chunk.sync.v1.Jvm.JvmSessionPhase; -import chunk.sync.v1.Jvm.PlayerSetup; - +import com.chunkzero.chunk.runtime.ChunkSessions; import com.chunkzero.chunk.runtime.ManagedPlayer; +import com.chunkzero.chunk.runtime.PlayerProfile; import com.chunkzero.chunk.runtime.SessionManager; import net.kyori.adventure.text.Component; @@ -15,236 +11,87 @@ import net.minestom.server.event.EventNode; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.AsyncPlayerPreLoginEvent; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; import org.jetbrains.annotations.ApiStatus; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import java.util.function.BooleanSupplier; -import java.util.function.LongSupplier; -import java.util.regex.Pattern; /** - * Runs the deliveries core's topic lists. It prepares each once its session is ready, admits its - * player through the {@code chunk:delivery} login plugin, and closes it once it is withdrawn or its - * key is gone. A delivery it can't prepare, or that is withdrawn first, is closed at once. + * Admits the players core delivers through the {@code chunk:delivery} login plugin and spawns each + * in its session's first instance. A withdrawn delivery disconnects its player. */ @ApiStatus.Internal public final class GameplayService { - private static final Pattern USERNAME = Pattern.compile("[A-Za-z0-9_]{1,16}"); - private static final long WAIT_NANOS = TimeUnit.SECONDS.toNanos(30); - private final SessionManager manager; - private final LongSupplier now; - private final BooleanSupplier ready; - private final DeliveryFence owners = new DeliveryFence(); + private final ChunkSessions sessions; private final EventNode events = EventNode.all("gameplay-delivery"); - // Guarded by itself, like the maps after it: the topic's latest deliveries, and those held. - private final Map preparations = new LinkedHashMap<>(); - private Map wanted = Map.of(); - // Deliveries waiting for their session, since when, and those closed without preparing. - private final Map pending = new HashMap<>(); - private final Map refused = new HashMap<>(); + private final Map admitted = new ConcurrentHashMap<>(); - public GameplayService(SessionManager manager, LongSupplier now, BooleanSupplier ready) { + public GameplayService(SessionManager manager, ChunkSessions sessions) { this.manager = manager; - this.now = now; - this.ready = ready; - manager.setWithdraw( - id -> { - synchronized (preparations) { - var closing = - preparations.values().stream() - .filter( - prepared -> - prepared.getDelivery() - .getSession() - .equals(id)) - .map(PreparedDelivery::close) - .toArray(CompletableFuture[]::new); - return CompletableFuture.allOf(closing); - } - }); + this.sessions = sessions; events.addListener(AsyncPlayerPreLoginEvent.class, this::preLogin); events.addListener(AsyncPlayerConfigurationEvent.class, this::configure); manager.getProcess().eventHandler().addChild(events); } private void preLogin(AsyncPlayerPreLoginEvent event) { + var connection = event.getConnection(); + var player = new AdmittedPlayer(connection, manager); try { var payload = event.sendPluginRequest("chunk:delivery", new byte[0]) .get(5, TimeUnit.SECONDS) .payload(); - if (payload == null || payload.length > 4096) - throw new IllegalArgumentException("Invalid delivery setup"); - var setup = PlayerSetup.parseFrom(payload); - synchronized (preparations) { - var prepared = preparations.get(setup.getOperationId()); - if (prepared == null) throw new IllegalArgumentException("Unknown operation"); - event.setGameProfile( - prepared.consume(setup, event.getGameProfile(), event.getConnection())); - } + var presented = event.getGameProfile(); + var delivery = + sessions.admit(payload, presented.uuid(), presented.name(), player::close); + admitted.put(connection, player); + player.admitted(delivery); + event.setGameProfile(profile(delivery.player())); } catch (Exception ignored) { - event.getConnection().kick(Component.text("Delivery rejected")); + if (admitted.containsKey(connection)) player.close(); + connection.kick(Component.text("Delivery rejected")); } } private void configure(AsyncPlayerConfigurationEvent event) { try { - PreparedDelivery prepared; - synchronized (preparations) { - prepared = - preparations.values().stream() - .filter( - candidate -> - candidate.owns( - event.getPlayer().getPlayerConnection())) - .findFirst() - .orElseThrow( - () -> new IllegalArgumentException("Unknown delivery")); - } - event.setSpawningInstance(prepared.configure((ManagedPlayer) event.getPlayer())); + var player = admitted.get(event.getPlayer().getPlayerConnection()); + if (player == null) throw new IllegalArgumentException("Unknown delivery"); + event.setSpawningInstance(player.configure((ManagedPlayer) event.getPlayer())); event.getPlayer().setRespawnPoint(new Pos(0.5, 42, 0.5)); } catch (Exception ignored) { event.getPlayer().kick(Component.text("Session unavailable")); } } - /** Runs the topic's latest deliveries, keyed by operation ID. */ - public void apply(Map deliveries) { - synchronized (preparations) { - wanted = Map.copyOf(deliveries); - preparations.forEach( - (operation, prepared) -> { - var delivery = wanted.get(operation); - if (delivery == null || delivery.getWithdraw()) prepared.close(); - }); - refused.keySet().retainAll(wanted.keySet()); - pending.keySet().retainAll(wanted.keySet()); - wanted.forEach( - (operation, delivery) -> { - if (!preparations.containsKey(operation) && !refused.containsKey(operation)) - pending.putIfAbsent(operation, now.getAsLong()); - }); - settle(); - } - } - - /** Prepares waiting deliveries whose session became ready, and closes expired ones. */ + /** Advances admitted players, and forgets those released. Runs every tick. */ public void flush() { - synchronized (preparations) { - preparations.values().forEach(PreparedDelivery::checkDeadline); - settle(); - } - } - - /** Prepares or refuses each waiting delivery it can, and forgets those closed and gone. */ - private void settle() { - for (var operation : List.copyOf(pending.keySet())) { - var delivery = wanted.get(operation); - var phase = manager.phase(delivery.getSession()); - if (phase == JvmSessionPhase.JVM_SESSION_PHASE_READY && !delivery.getWithdraw()) { - try { - prepare(operation, delivery); - } catch (RuntimeException error) { - refused.put(operation, delivery); - } - } else if (delivery.getWithdraw() - || !ready.getAsBoolean() - || (phase != null && phase != JvmSessionPhase.JVM_SESSION_PHASE_STARTING) - || now.getAsLong() - pending.get(operation) >= WAIT_NANOS) { - refused.put(operation, delivery); - } else continue; - pending.remove(operation); - } - preparations - .entrySet() - .removeIf( - entry -> - !wanted.containsKey(entry.getKey()) - && entry.getValue().isReleased()); - } - - private void prepare(String operation, JvmDelivery delivery) { - if (!ready.getAsBoolean()) throw new IllegalStateException("Server not ready"); - var identity = delivery.getPlayer(); - if (!USERNAME.matcher(identity.getUsername()).matches() - || !UUID.fromString(identity.getUuid()).toString().equals(identity.getUuid())) { - throw new IllegalArgumentException("Invalid player identity"); - } - if (preparations.size() >= 4096) - throw new IllegalStateException("Process delivery capacity reached"); - var session = manager.get(delivery.getSession()); - var reserved = - preparations.values().stream() - .filter( - candidate -> - candidate - .getDelivery() - .getSession() - .equals(delivery.getSession()) - && !candidate.isReleased()) - .count(); - if (reserved >= session.getCapacity()) throw new IllegalStateException("Session full"); - preparations.put( - operation, - new PreparedDelivery( - operation, delivery, owners, now, session, manager.getTicks())); - } - - /** Whether the player of delivery {@code operation} is in session {@code session}. */ - public boolean arrived(String operation, String session) { - synchronized (preparations) { - var prepared = preparations.get(operation); - return prepared != null && prepared.arrivedIn(session); - } - } - - public List deliveries() { - return deliveries(new HashMap<>()); - } - - /** - * Every delivery it holds, including those closed whose key remains, counting those PREPARED in - * each session into {@code prepared}. - */ - public List deliveries(Map prepared) { - synchronized (preparations) { - var statuses = new ArrayList(); - preparations - .values() - .forEach( - delivery -> { - var status = delivery.status(); - statuses.add(status); - if (status.getPhase() - == JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED) - prepared.merge( - delivery.getDelivery().getSession(), 1, Integer::sum); - }); - refused.forEach( - (operation, delivery) -> - statuses.add( - JvmDeliveryStatus.newBuilder() - .setOperationId(operation) - .setGeneration(delivery.getGeneration()) - .setPhase(JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED) - .build())); - return statuses; - } + admitted.values().forEach(AdmittedPlayer::check); + admitted.values().removeIf(AdmittedPlayer::isReleased); } public void close() { manager.getProcess().eventHandler().removeChild(events); - synchronized (preparations) { - preparations.values().forEach(PreparedDelivery::close); - } + admitted.values().forEach(AdmittedPlayer::close); + } + + private static GameProfile profile(PlayerProfile player) { + return new GameProfile( + player.uuid(), + player.name(), + player.properties().stream() + .map( + property -> + new GameProfile.Property( + property.name(), + property.value(), + property.signature())) + .toList()); } } diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/PreparedDelivery.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/PreparedDelivery.java deleted file mode 100644 index 40a0df0c..00000000 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/PreparedDelivery.java +++ /dev/null @@ -1,259 +0,0 @@ -package com.chunkzero.chunk.runtime.minestom.internal; - -import chunk.sync.v1.Gateway.PlayerIdentity; -import chunk.sync.v1.Jvm.JvmDelivery; -import chunk.sync.v1.Jvm.JvmDeliveryPhase; -import chunk.sync.v1.Jvm.JvmDeliveryStatus; -import chunk.sync.v1.Jvm.JvmSessionPhase; -import chunk.sync.v1.Jvm.PlayerSetup; - -import com.chunkzero.chunk.runtime.ManagedPlayer; -import com.chunkzero.chunk.runtime.SessionManager; -import com.chunkzero.chunk.runtime.TickExecutor; -import com.google.protobuf.ByteString; - -import net.minestom.server.instance.Instance; -import net.minestom.server.network.ConnectionState; -import net.minestom.server.network.player.GameProfile; -import net.minestom.server.network.player.PlayerConnection; - -import org.jetbrains.annotations.Nullable; - -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.LongSupplier; - -/** - * A delivery the JVM prepared: a slot in its session and the capability it minted, which admits the - * player once. Once closed it holds no live player references. - */ -final class PreparedDelivery { - /** - * How long the gateway may take to connect the player: it loads their resource packs first. - * Control cancels the claim of a delivery not activated within 60 seconds of its preparation - * anyway. - */ - private static final long CONNECT_NANOS = TimeUnit.SECONDS.toNanos(60); - - private final String operation; - private final JvmDelivery delivery; - private final DeliveryFence owners; - private final LongSupplier now; - private final SessionManager.ManagedSession session; - private final TickExecutor ticks; - private final byte[] capability = new byte[32]; - private final long openedAt; - private boolean consumed; - private boolean closed; - private @Nullable PlayerConnection connection; - private @Nullable ManagedPlayer player; - private CompletableFuture joining = CompletableFuture.completedFuture(null); - private final CompletableFuture removed = new CompletableFuture<>(); - private boolean arrived; - private boolean joinStarted; - private @Nullable JvmDeliveryStatus status; - - PreparedDelivery( - String operation, - JvmDelivery delivery, - DeliveryFence owners, - LongSupplier now, - SessionManager.ManagedSession session, - TickExecutor ticks) { - this.operation = operation; - this.delivery = delivery; - this.owners = owners; - this.now = now; - this.session = session; - this.ticks = ticks; - new SecureRandom().nextBytes(capability); - openedAt = now.getAsLong(); - } - - JvmDelivery getDelivery() { - return delivery; - } - - /** Whether the player is in {@code id}, the delivery's session. */ - synchronized boolean arrivedIn(String id) { - return !closed - && arrived - && player != null - && player.isOnline() - && connection != null - && connection.isOnline() - && session.getPhase() == JvmSessionPhase.JVM_SESSION_PHASE_READY - && delivery.getSession().equals(id); - } - - synchronized boolean owns(PlayerConnection current) { - return !closed && connection == current; - } - - synchronized boolean isReleased() { - return removed.isDone() && !removed.isCompletedExceptionally(); - } - - synchronized Instance configure(ManagedPlayer current) { - if (closed || connection != current.getPlayerConnection()) - throw new IllegalStateException("Delivery closed"); - if (session.getPhase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) - throw new IllegalStateException("Session unavailable"); - current.setBinding(operation, delivery); - player = current; - return session.getScope().getInstances().getFirst(); - } - - synchronized JvmDeliveryStatus status() { - checkDeadline(); - JvmDeliveryPhase phase; - if (closed && !isReleased()) phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_WITHDRAWING; - else if (closed) phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED; - else if (arrived) phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_ARRIVED; - else if (connection != null && connection.getClientState() == ConnectionState.PLAY) { - phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_ATTACHED; - } else phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED; - if (status == null || status.getPhase() != phase) { - var next = - JvmDeliveryStatus.newBuilder() - .setOperationId(operation) - .setGeneration(delivery.getGeneration()) - .setPhase(phase); - if (phase == JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED) - next.setCapability(ByteString.copyFrom(capability)); - status = next.build(); - } - return status; - } - - synchronized GameProfile consume( - PlayerSetup setup, GameProfile presented, PlayerConnection accepted) { - checkDeadline(); - if (closed || consumed || session.getPhase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) { - throw new IllegalStateException("Delivery unavailable"); - } - if (!setup.getOperationId().equals(operation) - || !MessageDigest.isEqual(capability, setup.getCapability().toByteArray())) { - throw new IllegalArgumentException("Invalid delivery capability"); - } - var profile = profile(delivery.getPlayer()); - if (!presented.uuid().equals(profile.uuid()) || !presented.name().equals(profile.name())) { - throw new IllegalArgumentException("Player identity mismatch"); - } - owners.claim(delivery.getPlayer().getUuid(), delivery.getGeneration()); - consumed = true; - connection = accepted; - return profile; - } - - synchronized void checkDeadline() { - if (player != null) { - var initialization = player.getInitialization(); - var spawned = - initialization != null - && initialization.isDone() - && !initialization.isCompletedExceptionally(); - if (spawned && !closed && !joinStarted) { - joinStarted = true; - joining = session.join(player); - joining.whenComplete( - (ignored, error) -> { - if (error != null) close(); - }); - } - if (spawned - && joinStarted - && joining.isDone() - && !joining.isCompletedExceptionally() - && player.getLastSentTeleportId() > 0 - && player.getLastReceivedTeleportId() == player.getLastSentTeleportId()) { - arrived = true; - } - } - if ((!consumed && now.getAsLong() - openedAt >= CONNECT_NANOS) - || (connection != null && !connection.isOnline())) { - close(); - } - } - - synchronized CompletableFuture close() { - if (closed) return removed; - closed = true; - var current = connection; - if (current != null) current.disconnect(); - joining.handle((ignored, error) -> null) - .thenCompose( - ignored -> - ticks.submit( - () -> { - var initialization = - player == null - ? null - : player.getInitialization(); - return initialization == null - ? CompletableFuture - .completedFuture(null) - : initialization; - }) - .thenCompose( - initialization -> - initialization.handle( - (result, error) -> null))) - .thenCompose( - ignored -> - ticks.submit( - () -> { - if (player != null) { - Objects.requireNonNull(current) - .process() - .connectionManager() - .removePlayer(current); - if (player.getInstance() != null - && !player.isRemoved()) player.remove(); - } - return player; - })) - .thenCompose( - currentPlayer -> - currentPlayer == null - ? CompletableFuture.completedFuture(null) - : session.leave(currentPlayer)) - .whenComplete( - (ignored, error) -> { - synchronized (this) { - connection = null; - player = null; - if (error == null) { - if (consumed) - owners.release( - delivery.getPlayer().getUuid(), - delivery.getGeneration()); - removed.complete(null); - } else { - removed.completeExceptionally(error); - } - } - }); - return removed; - } - - private static GameProfile profile(PlayerIdentity identity) { - return new GameProfile( - UUID.fromString(identity.getUuid()), - identity.getUsername(), - identity.getPropertiesList().stream() - .map( - property -> - new GameProfile.Property( - property.getName(), - property.getValue(), - property.hasSignature() - ? property.getSignature() - : null)) - .toList()); - } -} diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ProcessService.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ProcessService.java deleted file mode 100644 index 034007b1..00000000 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ProcessService.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.chunkzero.chunk.runtime.minestom.internal; - -import chunk.sync.v1.Jvm.JvmDelivery; -import chunk.sync.v1.Jvm.JvmMethodCall; -import chunk.sync.v1.Jvm.JvmReport; -import chunk.sync.v1.Jvm.JvmSession; - -import com.chunkzero.chunk.runtime.SessionManager; -import com.chunkzero.chunk.runtime.control.ProcessState; -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Parser; - -import org.jetbrains.annotations.ApiStatus; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.BooleanSupplier; - -/** - * Runs the sessions, deliveries and session methods core's topic lists, and reports the engine's - * sessions and deliveries. - */ -@ApiStatus.Internal -public final class ProcessService implements ProcessState { - private final SessionManager sessions; - private final BooleanSupplier ready; - private final GameplayService gameplay; - private final SessionMethodService methods; - // The sessions the latest snapshot lists, those this service asked to create, and to finish. - private volatile Set desired = Set.of(); - private final Set created = new HashSet<>(); - private final Set finished = new HashSet<>(); - - public ProcessService( - SessionManager sessions, - BooleanSupplier ready, - GameplayService gameplay, - SessionMethodService methods) { - this.sessions = sessions; - this.ready = ready; - this.gameplay = gameplay; - this.methods = methods; - } - - @Override - public JvmReport inventory() { - var prepared = new HashMap(); - var report = JvmReport.newBuilder().addAllDeliveries(gameplay.deliveries(prepared)); - var desired = this.desired; - for (var session : sessions.inventory()) { - var id = session.getId(); - if (!desired.contains(id) && SessionManager.terminal(session.getPhase())) - sessions.forget(id); - else report.addSessions(session.toBuilder().setPrepared(prepared.getOrDefault(id, 0))); - } - return report.build(); - } - - /** Outcomes, including failures, surface in later reports. */ - @Override - public synchronized void apply(Map entries) { - var listed = entries(entries, "session/", JvmSession.parser()); - listed.forEach( - (id, session) -> { - if (created.add(id) && !session.getFinish()) { - if (ready.getAsBoolean()) sessions.create(id, session); - else sessions.reject(id, session); - } - if (session.getFinish() && finished.add(id)) sessions.finish(id, session); - }); - for (var id : Set.copyOf(created)) { - if (listed.containsKey(id)) continue; - created.remove(id); - if (finished.add(id)) sessions.finish(id, JvmSession.getDefaultInstance()); - } - finished.retainAll(created); - desired = Set.copyOf(listed.keySet()); - gameplay.apply(entries(entries, "delivery/", JvmDelivery.parser())); - methods.apply(entries(entries, "method/", JvmMethodCall.parser())); - } - - /** The entries under {@code prefix}, by the rest of their key, skipping undecodable ones. */ - private static Map entries( - Map entries, String prefix, Parser parser) { - var parsed = new HashMap(); - entries.forEach( - (key, value) -> { - if (!key.startsWith(prefix)) return; - try { - parsed.put(key.substring(prefix.length()), parser.parseFrom(value)); - } catch (InvalidProtocolBufferException ignored) { - // Not an entry this runtime can run. - } - }); - return parsed; - } -} diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodService.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodService.java deleted file mode 100644 index 302dce9e..00000000 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodService.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.chunkzero.chunk.runtime.minestom.internal; - -import chunk.sync.v1.Jvm.JvmMethodCall; -import chunk.sync.v1.Jvm.JvmMethodPhase; -import chunk.sync.v1.Jvm.JvmMethodResult; - -import com.chunkzero.chunk.runtime.SessionManager; -import com.chunkzero.chunk.runtime.SessionMethodBinding; -import com.google.protobuf.ByteString; - -import org.jetbrains.annotations.ApiStatus; - -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.BiPredicate; -import java.util.function.LongSupplier; - -/** - * Runs each session method core's topic lists once, on the tick thread, and hands its result to - * {@code results}. A method starts only before its deadline, unless cancelled, and while its player - * is arrived in its session; otherwise it is cancelled. - */ -@ApiStatus.Internal -public final class SessionMethodService { - private static final int MAX_JSON = 48 * 1024; - private final SessionManager manager; - private final Map> bindings; - private final BiPredicate arrived; - private final BiConsumer results; - private final LongSupplier now; - // Each method the topic lists, until its key is gone. - private final Map operations = new HashMap<>(); - private boolean closed; - - /** - * {@code arrived} tells whether a delivery's player is in a session, and {@code now} is Unix - * time in milliseconds. - */ - public SessionMethodService( - SessionManager manager, - Map> bindings, - BiPredicate arrived, - BiConsumer results, - LongSupplier now) { - this.manager = manager; - this.bindings = Map.copyOf(bindings); - this.arrived = arrived; - this.results = results; - this.now = now; - } - - /** Runs the topic's latest methods, keyed by operation ID. */ - public synchronized void apply(Map calls) { - operations.keySet().retainAll(calls.keySet()); - calls.forEach( - (id, call) -> { - var operation = operations.get(id); - if (operation == null) { - operation = new Operation(id, call); - operations.put(id, operation); - var queued = operation; - manager.getTicks() - .submit( - () -> { - execute(queued); - return null; - }); - } - if (call.getCancel() && !operation.started) cancel(operation); - }); - } - - public synchronized void close() { - closed = true; - } - - private void execute(Operation operation) { - var call = operation.call; - SessionMethodBinding binding; - SessionManager.ManagedSession session; - synchronized (this) { - if (operation.done || operations.get(operation.id) != operation) return; - try { - if (closed - || now.getAsLong() >= call.getDeadlineMs() - || !arrived.test(call.getDelivery(), call.getSession())) - throw new IllegalStateException("The method may not start"); - session = manager.get(call.getSession()); - session.requireMethodReady(); - } catch (RuntimeException error) { - cancel(operation); - return; - } - binding = bindings.get(session.getSessionType() + "/" + call.getMethod()); - operation.started = true; - } - JvmMethodResult result; - try { - if (binding == null) throw new IllegalArgumentException("Undeclared session method"); - var json = session.invokeMethod(binding, call.getArgumentsJson().toStringUtf8()); - var encoded = json.getBytes(StandardCharsets.UTF_8); - if (encoded.length > MAX_JSON) throw new IllegalArgumentException("Result size limit"); - result = - JvmMethodResult.newBuilder() - .setPhase(JvmMethodPhase.JVM_METHOD_PHASE_COMPLETED) - .setResultJson(ByteString.copyFrom(encoded)) - .build(); - } catch (RuntimeException error) { - result = - JvmMethodResult.newBuilder() - .setPhase(JvmMethodPhase.JVM_METHOD_PHASE_FAILED) - .build(); - } - synchronized (this) { - finish(operation, result); - } - } - - private void cancel(Operation operation) { - finish( - operation, - JvmMethodResult.newBuilder() - .setPhase(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED) - .build()); - } - - private void finish(Operation operation, JvmMethodResult result) { - if (operation.done) return; - operation.done = true; - if (operations.get(operation.id) == operation) results.accept(operation.id, result); - } - - private static final class Operation { - final String id; - final JvmMethodCall call; - boolean started; - boolean done; - - Operation(String id, JvmMethodCall call) { - this.id = id; - this.call = call; - } - } -} diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java index 32d2fff0..136f62da 100644 --- a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java @@ -28,7 +28,7 @@ class AppRegistryTest { @TempDir Path directory; @Test - void serviceRegistryCreatesIndependentSessionsWithAssignedCallerIdentity() throws Exception { + void serviceRegistryCreatesIndependentSessions() throws Exception { var lobby = compile("lobby"); var jar = jar("lobby.jar", lobby.classes(), lobby.provider()); try (var loader = loader(jar)) { @@ -39,13 +39,6 @@ void serviceRegistryCreatesIndependentSessionsWithAssignedCallerIdentity() throw assertEquals("lobby", first.toString()); assertNotSame(first, factory.create()); assertTrue(AppRegistry.load("renamed", loader).containsKey("renamed/default")); - factory.backend( - "instance", - (session, app) -> { - assertEquals("instance", session); - assertEquals("lobby", app); - return null; - }); } } diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java index 5ff148de..f2fe0b56 100644 --- a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java @@ -64,12 +64,13 @@ public Session create(SessionCreation creation) { SessionRegistration.provider("arena", () -> provider), "arena/legacy", new SessionRegistration("arena", FlatSession::new)), - null); + Map.of()); + var host = TestHosts.detached(manager); try { var first = session(16, "{\"map\":\"forest\",\"mode\":\"solo\"}"); var second = session(32, "{\"map\":\"desert\",\"mode\":\"solo\"}"); - var firstResult = manager.create("first", first); - var secondResult = manager.create("second", second); + var firstResult = host.create("first", first); + var secondResult = host.create("second", second); flush(ticks); assertEquals(16, firstResult.join().getCapacity()); assertEquals(32, secondResult.join().getCapacity()); @@ -79,7 +80,7 @@ public Session create(SessionCreation creation) { new SessionCreation<>(32, new Config("desert", "solo"))), settings); var replay = - manager.create( + host.create( "first", first.toBuilder() .setConfigurationJson( @@ -89,7 +90,7 @@ public Session create(SessionCreation creation) { flush(ticks); assertEquals(firstResult.join(), replay.join()); var changed = - manager.create( + host.create( "first", first.toBuilder() .setConfigurationJson(second.getConfigurationJson()) @@ -105,12 +106,12 @@ public Session create(SessionCreation creation) { "{\"map\":\"forest\",\"mode\":\"solo\",\"extra\":true}", "{\"map\":\"" + "x".repeat(65_536) + "\",\"mode\":\"solo\"}"); for (var json : invalidValues) { - var invalid = manager.create("invalid", session(16, json)); + var invalid = host.create("invalid", session(16, json)); flush(ticks); assertTrue(invalid.isCompletedExceptionally()); } var invalidEncoding = - manager.create( + host.create( "invalid", session(16, "{}").toBuilder() .setConfigurationJson( @@ -120,11 +121,11 @@ public Session create(SessionCreation creation) { assertTrue(invalidEncoding.isCompletedExceptionally()); assertEquals(2, settings.size()); var legacy = - manager.create( + host.create( "legacy", session(16, "{}").toBuilder().setSessionType("arena/legacy").build()); var invalidLegacy = - manager.create( + host.create( "invalidLegacy", session(16, "{\"map\":\"forest\"}").toBuilder() .setSessionType("arena/legacy") @@ -132,9 +133,9 @@ public Session create(SessionCreation creation) { flush(ticks); legacy.join(); assertTrue(invalidLegacy.isCompletedExceptionally()); - manager.finish("first", first); - manager.finish("second", second); - manager.finish("legacy", session(16, "{}")); + host.finish("first", first); + host.finish("second", second); + host.finish("legacy", session(16, "{}")); flush(ticks); } finally { process.stop(); @@ -156,10 +157,11 @@ void failedCreationAndUnknownFinishAreReported() { () -> { throw new IllegalStateException("provider failed"); })), - null); + Map.of()); + var host = TestHosts.detached(manager); try { - var failed = manager.create("failed", session(16, "{}")); - var unknown = manager.finish("unknown", session(16, "{}")); + var failed = host.create("failed", session(16, "{}")); + var unknown = host.finish("unknown", session(16, "{}")); flush(ticks); assertTrue(failed.isCompletedExceptionally()); assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDED, unknown.join().getPhase()); @@ -167,7 +169,7 @@ void failedCreationAndUnknownFinishAreReported() { Map.of( "failed", JvmSessionPhase.JVM_SESSION_PHASE_FAILED, "unknown", JvmSessionPhase.JVM_SESSION_PHASE_ENDED), - phases(manager)); + phases(host)); } finally { process.stop(); } @@ -217,22 +219,23 @@ public CompletionStage onCreate(SessionScope scope) { new SessionRegistration("arena", failing), "arena/leaky", new SessionRegistration("arena", leaking)), - null); + Map.of()); + var host = TestHosts.detached(manager); try { var leaky = session(16, "{}").toBuilder().setSessionType("arena/leaky").build(); - manager.create("failed", session(16, "{}")); - manager.create("leaky", leaky); - manager.finish("leaky", leaky); + host.create("failed", session(16, "{}")); + host.create("leaky", leaky); + host.finish("leaky", leaky); for (var tick = 0; tick < 20; tick++) { ticks.flush(); if (tick == 10) cleanup.complete(null); - var phases = phases(manager); - if (SessionManager.terminal(phases.get("failed"))) + var phases = phases(host); + if (ChunkSessions.terminal(phases.get("failed"))) assertFalse(instances.getFirst().isRegistered()); - assertFalse(SessionManager.terminal(phases.get("leaky"))); + assertFalse(ChunkSessions.terminal(phases.get("leaky"))); } - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, phases(manager).get("failed")); - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, phases(manager).get("leaky")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, phases(host).get("failed")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, phases(host).get("leaky")); } finally { process.stop(); } @@ -261,22 +264,22 @@ public CompletionStage onCreate( return creation; } })), - null); - manager.setCreateDeadline(Duration.ofMillis(50)); + Map.of()); + var host = TestHosts.withDeadline(manager, Duration.ofMillis(50)); try { - var created = manager.create("slow", session(16, "{}")); + var created = host.create("slow", session(16, "{}")); var deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); while (!created.isDone() && System.nanoTime() < deadline) ticks.flush(); assertTrue(created.isDone()); var failure = assertThrows(CompletionException.class, created::join); assertInstanceOf(TimeoutException.class, failure.getCause()); flush(ticks); - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, manager.phase("slow")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("slow")); assertFalse(instances.getFirst().isRegistered()); assertFalse(creation.isDone()); assertTrue(creation.complete(null)); flush(ticks); - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, manager.phase("slow")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("slow")); } finally { process.stop(); } @@ -291,27 +294,28 @@ void forgottenSessionsLeaveRoomForNewOnes() { process, ticks, Map.of("arena/default", new SessionRegistration("arena", FlatSession::new)), - null); + Map.of()); + var host = TestHosts.detached(manager); try { for (var index = 0; index < 300; index++) { var id = "session" + index; var session = session(16, "{}"); - var created = manager.create(id, session); + var created = host.create(id, session); flush(ticks); created.join(); - manager.finish(id, session); + host.finish(id, session); flush(ticks); - manager.forget(id); + host.forget(); flush(ticks); } - assertTrue(manager.inventory().isEmpty()); + assertTrue(host.inventory().getSessionsList().isEmpty()); } finally { process.stop(); } } - private static Map phases(SessionManager manager) { - return manager.inventory().stream() + private static Map phases(ChunkSessions host) { + return host.inventory().getSessionsList().stream() .collect(Collectors.toMap(JvmSessionStatus::getId, JvmSessionStatus::getPhase)); } diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodServiceTest.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodServiceTest.java deleted file mode 100644 index 27f9fbd7..00000000 --- a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodServiceTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.chunkzero.chunk.runtime; - -import static org.junit.jupiter.api.Assertions.*; - -import chunk.sync.v1.Jvm.JvmMethodCall; -import chunk.sync.v1.Jvm.JvmMethodPhase; -import chunk.sync.v1.Jvm.JvmSession; -import chunk.sync.v1.Jvm.JvmSessionPhase; - -import com.chunkzero.chunk.backend.api.BackendValues; -import com.chunkzero.chunk.backend.api.JsonType; -import com.chunkzero.chunk.backend.api.SessionMethodRef; -import com.chunkzero.chunk.runtime.minestom.internal.GameplayService; -import com.chunkzero.chunk.runtime.minestom.internal.ProcessService; -import com.chunkzero.chunk.runtime.minestom.internal.SessionMethodService; -import com.google.protobuf.ByteString; - -import net.minestom.server.ServerProcess; - -import org.junit.jupiter.api.Test; - -import tools.jackson.core.type.TypeReference; - -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; - -class SessionMethodServiceTest { - @Test - void topicMethodsRunOnceOnTheTickThreadUnlessCancelledOrTheirPlayerLeft() throws Exception { - var process = ServerProcess.create(); - var ticks = new TickExecutor(); - var game = new Game(); - var manager = - new SessionManager( - process, - ticks, - Map.of("lobby/default", new SessionRegistration("lobby", () -> game)), - null); - var args = JsonType.of(new TypeReference() {}, Objects::requireNonNull); - var number = JsonType.of(new TypeReference() {}, BackendValues::checkInteger); - var binding = - new SessionMethodBinding<>( - new SessionMethodRef<>("lobby", "default", "record", args, number), - (session, input) -> ((Game) session).record(input)); - var arrived = new AtomicBoolean(true); - var gameplay = new GameplayService(manager, System::nanoTime, () -> true); - try (var core = new FakeCore()) { - var methods = - new SessionMethodService( - manager, - Map.of("lobby/default/record", binding), - (delivery, session) -> - arrived.get() - && delivery.equals("delivery") - && session.equals("session"), - core::methodResult, - System::currentTimeMillis); - core.connect(new ProcessService(manager, () -> true, gameplay, methods)); - core.put( - "session/session", - JvmSession.newBuilder().setSessionType("lobby/default").setCapacity(2).build()); - while (manager.phase("session") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { - ticks.flush(); - Thread.sleep(10); - } - var call = - JvmMethodCall.newBuilder() - .setSession("session") - .setMethod("record") - .setArgumentsJson(ByteString.copyFromUtf8("{\"message\":\"first\"}")) - .setDelivery("delivery") - .setDeadlineMs(System.currentTimeMillis() + 30_000) - .build(); - - core.put("method/completes", call); - var completed = core.result("completes", ticks::flush); - assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_COMPLETED, completed.getPhase()); - assertEquals("1", completed.getResultJson().toStringUtf8()); - assertEquals(Thread.currentThread(), game.thread); - - core.put("method/cancelled", call.toBuilder().setCancel(true).build()); - assertEquals( - JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, - core.result("cancelled", ticks::flush).getPhase()); - - arrived.set(false); - core.put("method/departed", call); - assertEquals( - JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, - core.result("departed", ticks::flush).getPhase()); - assertEquals(1, game.calls); - } finally { - gameplay.close(); - process.stop(); - } - } - - public record Args(String message) { - public Args { - BackendValues.checkString(message); - } - } - - private static final class Game extends Session { - int calls; - Thread thread; - - @Override - public CompletableFuture onCreate(SessionScope scope) { - scope.createInstance(); - return CompletableFuture.completedFuture(null); - } - - long record(Args args) { - thread = Thread.currentThread(); - return ++calls; - } - } -} diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java new file mode 100644 index 00000000..f98e3c84 --- /dev/null +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java @@ -0,0 +1,103 @@ +package com.chunkzero.chunk.runtime; + +import static org.junit.jupiter.api.Assertions.*; + +import chunk.sync.v1.Jvm.JvmSession; + +import com.chunkzero.chunk.backend.api.BackendValues; +import com.chunkzero.chunk.backend.api.JsonType; +import com.chunkzero.chunk.backend.api.SessionMethodRef; + +import net.minestom.server.ServerProcess; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.type.TypeReference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +class SessionMethodTest { + @Test + void declaredMethodsRunOnTheTickThreadWhileTheSessionIsReady() { + var process = ServerProcess.create(); + var ticks = new TickExecutor(); + var game = new Game(); + var args = JsonType.of(new TypeReference() {}, Objects::requireNonNull); + var number = JsonType.of(new TypeReference() {}, BackendValues::checkInteger); + var binding = + new SessionMethodBinding<>( + new SessionMethodRef<>("lobby", "default", "record", args, number), + (session, input) -> ((Game) session).record(input)); + var manager = + new SessionManager( + process, + ticks, + Map.of("lobby/default", new SessionRegistration("lobby", () -> game)), + Map.of("lobby/default/record", binding)); + var controls = new ArrayList(); + var host = + TestHosts.detached( + new SessionHandler() { + @Override + public void create(SessionControl session) { + controls.add(session); + manager.create(session); + } + + @Override + public void finish(SessionControl session) { + manager.finish(session); + } + }); + var spec = JvmSession.newBuilder().setSessionType("lobby/default").setCapacity(2).build(); + try { + settle(ticks, host.create("session", spec)); + var control = controls.getFirst(); + var result = manager.method(control, "record", "{\"message\":\"first\"}"); + settle(ticks, result.toCompletableFuture()); + assertEquals("1", result.toCompletableFuture().join()); + assertEquals(List.of(Thread.currentThread()), game.threads); + assertTrue( + manager.method(control, "missing", "{}") + .toCompletableFuture() + .isCompletedExceptionally()); + settle(ticks, host.finish("session", spec)); + var late = manager.method(control, "record", "{\"message\":\"late\"}"); + ticks.flush(); + assertTrue(late.toCompletableFuture().isCompletedExceptionally()); + assertEquals(1, game.threads.size()); + } finally { + process.stop(); + } + } + + private static void settle(TickExecutor ticks, CompletableFuture future) { + for (var index = 0; index < 10 && !future.isDone(); index++) ticks.flush(); + assertTrue(future.isDone()); + } + + public record Args(String message) { + public Args { + BackendValues.checkString(message); + } + } + + private static final class Game extends Session { + final List threads = new ArrayList<>(); + + @Override + public CompletableFuture onCreate(SessionScope scope) { + scope.createInstance(); + return CompletableFuture.completedFuture(null); + } + + long record(Args args) { + threads.add(Thread.currentThread()); + return threads.size(); + } + } +} diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java new file mode 100644 index 00000000..c581fdc8 --- /dev/null +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java @@ -0,0 +1,68 @@ +package com.chunkzero.chunk.runtime; + +import chunk.sync.v1.CoreOuterClass.Position; +import chunk.sync.v1.Jvm.JvmMethodResult; + +import com.chunkzero.chunk.backend.api.Destination; +import com.chunkzero.chunk.backend.client.BackendSession; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.LongSupplier; + +/** Session hosts for tests, whose handler callbacks run on the calling thread. */ +final class TestHosts { + private TestHosts() {} + + static ChunkSessions detached(SessionHandler handler) { + return ChunkSessions.detached(handler, session -> null); + } + + static ChunkSessions withDeadline(SessionHandler handler, Duration deadline) { + return new ChunkSessions( + handler, link(null, System::nanoTime), Runnable::run, deadline, null); + } + + /** Reports to {@code core}, and expires deliveries by {@code nanos}. */ + static ChunkSessions linked(SessionHandler handler, FakeCore core, LongSupplier nanos) { + return new ChunkSessions( + handler, link(core, nanos), Runnable::run, ChunkSessions.CREATE_DEADLINE, null); + } + + private static ChunkSessions.Link link(FakeCore core, LongSupplier nanos) { + return new ChunkSessions.Link() { + @Override + public boolean acceptsWork() { + return true; + } + + @Override + public BackendSession backend(String session) { + return null; + } + + @Override + public CompletionStage move( + String delivery, Position generation, Destination destination) { + return CompletableFuture.failedFuture( + new IllegalStateException("Moves unavailable")); + } + + @Override + public void methodResult(String operation, JvmMethodResult result) { + if (core != null) core.methodResult(operation, result); + } + + @Override + public void flush() { + if (core != null) core.wake(); + } + + @Override + public long nanoTime() { + return nanos.getAsLong(); + } + }; + } +} diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt index 20b70758..27521947 100644 --- a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt +++ b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt @@ -14,6 +14,7 @@ import net.minestom.server.network.ConnectionState import net.minestom.server.network.packet.server.login.LoginSuccessPacket import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -68,13 +69,15 @@ class GameplayLifecycleTest { }, ), ) - val service = GameplayService(manager, System::nanoTime) { true } + val host = TestHosts.detached(manager) + val service = GameplayService(manager, host) val sockets = mutableListOf() minecraft .schedulerManager() .buildTask { ticks.flush() service.flush() + host.sweep() }.repeat( net.minestom.server.timer.TaskSchedule .tick(1), @@ -87,8 +90,8 @@ class GameplayLifecycleTest { .setSessionType(type) .setCapacity(2) .build() - manager.create("a", session()).get(3, TimeUnit.SECONDS) - manager.create("b", session()).get(3, TimeUnit.SECONDS) + host.create("a", session()).get(3, TimeUnit.SECONDS) + host.create("b", session()).get(3, TimeUnit.SECONDS) val uuid = UUID.randomUUID().toString() val wanted = mutableMapOf() @@ -102,7 +105,11 @@ class GameplayLifecycleTest { .setPlayer(PlayerIdentity.newBuilder().setUuid(uuid).setUsername("test")) .build() - fun phase(operation: String) = service.deliveries().single { it.operationId == operation }.phase + fun apply() = host.apply(wanted.mapKeys { "delivery/${it.key}" }.mapValues { it.value.toByteString() }) + + fun status(operation: String) = host.inventory().deliveriesList.single { it.operationId == operation } + + fun phase(operation: String) = status(operation).phase fun await( operation: String, @@ -120,8 +127,8 @@ class GameplayLifecycleTest { request: JvmDelivery, ): Socket { wanted[operation] = request - service.apply(wanted) - val prepared = service.deliveries().single { it.operationId == operation } + apply() + val prepared = status(operation) val setup = PlayerSetup .newBuilder() @@ -150,21 +157,21 @@ class GameplayLifecycleTest { .toBuilder() .setWithdraw(true) .build() - service.apply(wanted) + apply() } arrive(connect("first", delivery("a", 1)), "first") - assertTrue(service.arrived("first", "a")) - assertFalse(service.arrived("first", "b")) + assertTrue(host.delivery("first")!!.isArrived) + assertEquals("a", host.delivery("first")!!.session()) val oldPlayer = minecraft.connectionManager().onlinePlayers.single() assertThrows(IllegalStateException::class.java) { connect("destination", delivery("b", 2)) } withdraw("first") await("first", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED) - assertFalse(service.arrived("first", "a")) + assertNull(host.delivery("first")) assertTrue(oldPlayer.isRemoved) assertTrue(oldPlayer in closedPlayers) assertTrue(minecraft.connectionManager().onlinePlayers.isEmpty()) arrive(connect("next", delivery("b", 3)), "next") - manager.finish("a", session()).get(3, TimeUnit.SECONDS) + host.finish("a", session()).get(3, TimeUnit.SECONDS) val current = minecraft.connectionManager().onlinePlayers.single() assertEquals(uuid, current.uuid.toString()) assertTrue( @@ -177,7 +184,7 @@ class GameplayLifecycleTest { assertEquals(setOf(oldPlayer), closedPlayers) withdraw("next") await("next", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED) - manager.create("c", session("gated")).get(3, TimeUnit.SECONDS) + host.create("c", session("gated")).get(3, TimeUnit.SECONDS) connect("pending", delivery("c", 4)) joinStarted.get(3, TimeUnit.SECONDS) assertTrue(phase("pending") != JvmDeliveryPhase.JVM_DELIVERY_PHASE_ARRIVED) @@ -193,8 +200,8 @@ class GameplayLifecycleTest { arrive(connect("replacement", delivery("c", 6)), "replacement") withdraw("replacement") await("replacement", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED) - manager.finish("c", session("gated")).get(3, TimeUnit.SECONDS) - manager.finish("b", session()).get(3, TimeUnit.SECONDS) + host.finish("c", session("gated")).get(3, TimeUnit.SECONDS) + host.finish("b", session()).get(3, TimeUnit.SECONDS) } finally { sockets.forEach { it.close() } service.close() diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt index f246274a..0ccb7a28 100644 --- a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt +++ b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt @@ -10,8 +10,6 @@ import chunk.sync.v1.Jvm.JvmSessionPhase import chunk.sync.v1.Jvm.PlayerSetup import com.chunkzero.chunk.runtime.bootstrap.FlatSession import com.chunkzero.chunk.runtime.minestom.internal.GameplayService -import com.chunkzero.chunk.runtime.minestom.internal.ProcessService -import com.chunkzero.chunk.runtime.minestom.internal.SessionMethodService import com.google.protobuf.ByteString import net.minestom.server.ServerProcess import net.minestom.server.network.ConnectionState @@ -37,22 +35,22 @@ class GameplayServiceTest { val ticks = TickExecutor() val manager = SessionManager(minecraft, ticks, mapOf("bridge" to Supplier { FlatSession() })) val clock = AtomicLong(System.nanoTime()) - val gameplay = GameplayService(manager, clock::get) { true } - val methods = SessionMethodService(manager, mapOf(), gameplay::arrived, { _, _ -> }, System::currentTimeMillis) val core = FakeCore() + val host = TestHosts.linked(manager, core, clock::get) + val gameplay = GameplayService(manager, host) minecraft .schedulerManager() .buildTask { ticks.flush() gameplay.flush() - core.wake() + host.sweep() }.repeat(TaskSchedule.tick(1)) .schedule() minecraft.start(InetSocketAddress("127.0.0.1", 0)) val port = minecraft.server().port val sockets = mutableListOf() try { - core.connect(ProcessService(manager, { true }, gameplay, methods)) + core.connect(host.state()) core.put( "session/bridge", JvmSession @@ -62,7 +60,7 @@ class GameplayServiceTest { .build(), ) val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10) - while (manager.phase("bridge") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { + while (host.phase("bridge") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { check(System.nanoTime() < deadline) { "The session never became ready" } Thread.sleep(10) } diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt index 4da382c1..a4d95757 100644 --- a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt +++ b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt @@ -1,6 +1,7 @@ package com.chunkzero.chunk.runtime import chunk.sync.v1.Jvm.JvmSession +import chunk.sync.v1.Jvm.JvmSessionPhase import com.chunkzero.chunk.runtime.minestom.event.SessionCreateEvent import com.chunkzero.chunk.runtime.minestom.event.SessionDestroyEvent import com.chunkzero.chunk.runtime.minestom.event.SessionEvent @@ -96,7 +97,8 @@ class SessionEventsTest { override fun onFinish() = finishing }, ) - val create = manager.create("first", game) + val host = TestHosts.detached(manager) + val create = host.create("first", game) ticks.flush() assertTrue(global.isEmpty()) completeOffThread(creating) @@ -116,7 +118,8 @@ class SessionEventsTest { } }, ) - await(otherManager.create("second", game)) + val otherHost = TestHosts.detached(otherManager) + await(otherHost.create("second", game)) val player = player() val join = session.join(player) ticks.flush() @@ -144,7 +147,7 @@ class SessionEventsTest { assertEquals(1, playerTicks) await(session.leave(player)) - val finish = manager.finish("first", game) + val finish = host.finish("first", game) repeat(4) { ticks.flush() } assertFalse(disposed) completeOffThread(finishing) @@ -161,9 +164,9 @@ class SessionEventsTest { assertEquals(local, global.filter { it.session === scope }) assertEquals(1, other.size) assertFalse(process.eventHandler().children.contains(scope.events)) - await(manager.finish("first", game)) + await(host.finish("first", game)) assertEquals(4, local.size) - await(otherManager.finish("second", game)) + await(otherHost.finish("second", game)) } @Test @@ -180,13 +183,14 @@ class SessionEventsTest { CompletableFuture.failedFuture(IllegalStateException("Join failed")) }, ) - await(manager.create("first", game)) - await(manager.create("first", game)) + val host = TestHosts.detached(manager) + await(host.create("first", game)) + await(host.create("first", game)) val session = manager.get("first") val player = player() assertThrows(CompletionException::class.java) { await(session.join(player)) } await(session.leave(player)) - await(manager.finish("first", game)) + await(host.finish("first", game)) assertEquals( listOf(SessionCreateEvent::class.java, SessionDestroyEvent::class.java), global.map { it.javaClass }, @@ -209,8 +213,9 @@ class SessionEventsTest { } }, ) - assertThrows(CompletionException::class.java) { await(failed.create("failed", game)) } - assertThrows(CompletionException::class.java) { await(failed.finish("failed", game)) } + val failedHost = TestHosts.detached(failed) + assertThrows(CompletionException::class.java) { await(failedHost.create("failed", game)) } + assertThrows(CompletionException::class.java) { await(failedHost.finish("failed", game)) } assertEquals(1, global.count { it.session.id == "failed" && it is SessionDestroyEvent }) assertFalse(global.any { it.session.id == "failed" && it is SessionCreateEvent }) process.schedulerManager().processTick() @@ -234,8 +239,9 @@ class SessionEventsTest { } }, ) - await(manager.create("first", game)) - await(manager.create("second", game)) + val host = TestHosts.detached(manager) + await(host.create("first", game)) + await(host.create("second", game)) val (first, second) = scopes val firstEvents = mutableListOf() val secondEvents = mutableListOf() @@ -270,14 +276,14 @@ class SessionEventsTest { schedulers.processTickEnd() assertEquals(1 to 1, starts to ends) - await(manager.finish("first", game)) + await(host.finish("first", game)) schedulers.processTick() schedulers.processTickEnd() assertEquals(1 to 1, starts to ends) assertTrue(entity.isRemoved) assertFalse(outsider.isRemoved) assertEquals(setOf(second.instances.single(), outsider.instance), process.instanceManager().instances) - await(manager.finish("second", game)) + await(host.finish("second", game)) } @Test @@ -296,9 +302,10 @@ class SessionEventsTest { } }, ) - await(manager.create("owner", game)) + val host = TestHosts.detached(manager) + await(host.create("owner", game)) assertEquals(1, registered) - assertThrows(Throwable::class.java) { await(manager.finish("owner", game)) } + assertThrows(Throwable::class.java) { await(host.finish("owner", game)) } assertTrue(process.instanceManager().instances.isEmpty()) assertFalse(process.eventHandler().children.contains(scope.events)) assertEquals(1, global.count { it is SessionDestroyEvent }) @@ -317,7 +324,8 @@ class SessionEventsTest { } }, ) - await(manager.create("native", game)) + val host = TestHosts.detached(manager) + await(host.create("native", game)) val owned = CompletableFuture() scope.events.addListener(EntityTickEvent::class.java) { if (!owned.isDone) { @@ -327,7 +335,7 @@ class SessionEventsTest { } repeat(20) { if (!owned.isDone) process.ticker().tick(System.nanoTime()) } assertSame(process.dispatcher().threads().single(), owned.join()) - await(manager.finish("native", game)) + await(host.finish("native", game)) } @Test @@ -351,13 +359,14 @@ class SessionEventsTest { override fun onLeave(player: Player) = CompletableFuture.failedFuture(hookFailure) }, ) - await(manager.create("first", game)) + val host = TestHosts.detached(manager) + await(host.create("first", game)) val session = manager.get("first") val player = player() await(session.join(player)) val leave = assertThrows(CompletionException::class.java) { await(session.leave(player)) } assertSame(hookFailure, leave.cause) - assertThrows(CompletionException::class.java) { await(manager.finish("first", game)) } + assertStuckEnding(host, "first") assertEquals(3, reported.size) assertTrue(reported.all { it === listenerFailure }) assertTrue(global.last() is SessionDestroyEvent) @@ -379,6 +388,17 @@ class SessionEventsTest { node.addListener(SessionDestroyEvent::class.java, ::record) } + /** A scope that fails to dispose keeps its session ending, so it stays counted. */ + private fun assertStuckEnding( + host: ChunkSessions, + id: String, + ) { + val finish = host.finish(id, game) + repeat(20) { ticks.flush() } + assertFalse(finish.isDone) + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, host.phase(id)) + } + private fun manager(session: Session) = SessionManager(process, ticks, mapOf("game" to Supplier { session })) private val game = diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt index 05621a25..9a893e87 100644 --- a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt +++ b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt @@ -27,13 +27,15 @@ class SessionManagerTest { val ticks = TickExecutor() val firstManager = SessionManager(first, ticks, mapOf("flat" to Supplier { FlatSession() })) val secondManager = SessionManager(second, ticks, mapOf("flat" to Supplier { FlatSession() })) + val firstHost = TestHosts.detached(firstManager) + val secondHost = TestHosts.detached(secondManager) val firstEvents = mutableListOf() val secondEvents = mutableListOf() first.eventHandler().addListener(SessionCreateEvent::class.java) { firstEvents.add(it.session) } second.eventHandler().addListener(SessionCreateEvent::class.java) { secondEvents.add(it.session) } val session = session("flat") val creations = - listOf(firstManager.create("same-id", session), secondManager.create("same-id", session)) + listOf(firstHost.create("same-id", session), secondHost.create("same-id", session)) repeat(4) { ticks.flush() } creations.forEach { it.join() } val firstScope = firstManager.get("same-id").scope @@ -42,7 +44,7 @@ class SessionManagerTest { assertEquals(listOf(secondScope), secondEvents) assertSame(first, firstScope.process) assertSame(second, secondScope.instances.single().process()) - val ended = firstManager.finish("same-id", session) + val ended = firstHost.finish("same-id", session) repeat(8) { ticks.flush() } ended.join() first.close() @@ -56,24 +58,6 @@ class SessionManagerTest { } } - @Test - fun `work queued after a session creation sees the created session`() { - val process = ServerProcess.create() - val ticks = TickExecutor() - val manager = SessionManager(process, ticks, mapOf("flat" to Supplier { FlatSession() })) - try { - manager.create("queued", session("flat")) - val queued = manager.afterQueued() - assertTrue(manager.inventory().isEmpty()) - assertFalse(queued.isDone) - ticks.flush() - assertTrue(queued.isDone) - assertEquals(listOf("queued"), manager.inventory().map { it.id }) - } finally { - process.stop() - } - } - @Test fun `resource disposal failure still awaits the leave hook`() { val process = ServerProcess.create() @@ -99,8 +83,9 @@ class SessionManagerTest { ), ) val game = session("game") + val host = TestHosts.detached(manager) try { - val created = manager.create("game", game) + val created = host.create("game", game) repeat(4) { ticks.flush() } created.join() val session = manager.get("game") @@ -124,7 +109,7 @@ class SessionManagerTest { left.complete(null) ticks.flush() assertTrue(leavingResult.isCompletedExceptionally) - val ended = manager.finish("game", game) + val ended = host.finish("game", game) repeat(8) { ticks.flush() } ended.join() } finally { @@ -171,37 +156,38 @@ class SessionManagerTest { ), ) + val host = TestHosts.detached(manager) try { val first = session("delayed") val second = session("flat").toBuilder().setCapacity(32).build() - assertEquals(0, manager.activeCount()) - val pending = manager.create("first", first) - val independent = manager.create("second", second) + assertEquals(0, host.activeCount()) + val pending = host.create("first", first) + val independent = host.create("second", second) repeat(4) { ticks.flush() } assertFalse(pending.isDone) - assertEquals(2, manager.activeCount()) + assertEquals(2, host.activeCount()) assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, independent.join().phase) assertEquals(32, independent.join().capacity) assertEquals(3, process.instanceManager().instances.size) created.complete(null) repeat(4) { ticks.flush() } assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, pending.join().phase) - assertEquals(2, manager.activeCount()) - val duplicate = manager.create("first", first) + assertEquals(2, host.activeCount()) + val duplicate = host.create("first", first) repeat(2) { ticks.flush() } assertEquals(pending.join(), duplicate.join()) - val changed = manager.create("first", first.toBuilder().setCapacity(3).build()) + val changed = host.create("first", first.toBuilder().setCapacity(3).build()) ticks.flush() assertTrue(changed.isCompletedExceptionally) - val ending = manager.finish("first", first) + val ending = host.finish("first", first) repeat(5) { ticks.flush() } assertFalse(ending.isDone) - assertEquals(2, manager.activeCount()) + assertEquals(2, host.activeCount()) assertEquals(0, disposed) finished.complete(null) repeat(5) { ticks.flush() } assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDED, ending.join().phase) - assertEquals(1, manager.activeCount()) + assertEquals(1, host.activeCount()) assertEquals(1, disposed) assertEquals(1, process.instanceManager().instances.size) assertFalse(process.eventHandler().children.any { it.name == ownedScope.events.name }) @@ -209,19 +195,23 @@ class SessionManagerTest { val staleTask = ownedScope.onTick { error("Disposed task ran") } ticks.flush() assertTrue(staleTask.isCompletedExceptionally) - val stopSecond = manager.finish("second", second) + val stopSecond = host.finish("second", second) repeat(8) { ticks.flush() } assertTrue(stopSecond.isDone) - assertEquals(0, manager.activeCount()) - val failed = manager.create("failed", session("failed")) + assertEquals(0, host.activeCount()) + val failed = host.create("failed", session("failed")) repeat(8) { ticks.flush() } assertTrue(failed.isCompletedExceptionally) assertEquals( JvmSessionPhase.JVM_SESSION_PHASE_FAILED, - manager.inventory().single { it.id == "failed" }.phase, + host + .inventory() + .sessionsList + .single { it.id == "failed" } + .phase, ) - assertEquals(0, manager.activeCount()) - assertEquals(3, manager.inventory().size) + assertEquals(0, host.activeCount()) + assertEquals(3, host.inventory().sessionsList.size) } finally { process.stop() } diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/minestom/internal/DeliveryFenceTest.kt b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/minestom/internal/DeliveryFenceTest.kt deleted file mode 100644 index 5c561a12..00000000 --- a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/minestom/internal/DeliveryFenceTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.chunkzero.chunk.runtime.minestom.internal - -import chunk.sync.v1.CoreOuterClass.Position -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Test - -class DeliveryFenceTest { - private fun at( - revision: Long, - epoch: Long = 1, - ) = Position - .newBuilder() - .setEpoch(epoch) - .setRevision(revision) - .build() - - @Test - fun `active delivery cannot be duplicated and stale releases cannot withdraw a replacement`() { - val fence = DeliveryFence() - fence.claim("player", at(1)) - assertThrows(IllegalArgumentException::class.java) { fence.claim("player", at(1)) } - assertThrows(IllegalArgumentException::class.java) { fence.claim("player", at(2)) } - fence.release("player", at(1)) - assertThrows(IllegalArgumentException::class.java) { fence.claim("player", at(1)) } - fence.claim("player", at(2)) - fence.release("player", at(1)) - assertThrows(IllegalArgumentException::class.java) { fence.claim("player", at(3)) } - fence.release("player", at(2)) - // A restore's new epoch orders after every revision of the previous one. - fence.claim("player", at(1, epoch = 2)) - fence.claim("other", at(1)) - assertThrows(IllegalArgumentException::class.java) { fence.claim("unset", Position.getDefaultInstance()) } - } -} diff --git a/jvm/runtime/README.md b/jvm/runtime/README.md index 36e69eda..07fabbcb 100644 --- a/jvm/runtime/README.md +++ b/jvm/runtime/README.md @@ -7,11 +7,12 @@ Minestom or Kotlin dependency. Apps use it together with an engine adapter: -| Module | Artifact | Contents | -| ------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------- | -| `jvm/runtime` | `com.chunkzero.chunk:runtime` | `ChunkProcess`, `@SessionType`, `@Component` | -| [`jvm/runtime-minestom`](../runtime-minestom/README.md) | `com.chunkzero.chunk:runtime-minestom` | The Minestom adapter and the session API gameplay code uses | -| `jvm/runtime-minestom-kotlin` | `com.chunkzero.chunk:runtime-minestom-kotlin` | Coroutine adapters, covered in the Minestom README | +| Module | Artifact | Contents | +| ------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------- | +| `jvm/runtime` | `com.chunkzero.chunk:runtime` | `ChunkProcess`, `@SessionType`, `@Component` | +| [`jvm/runtime-minestom`](../runtime-minestom/README.md) | `com.chunkzero.chunk:runtime-minestom` | The Minestom adapter and the session API gameplay code uses | +| `jvm/runtime-minestom-kotlin` | `com.chunkzero.chunk:runtime-minestom-kotlin` | Coroutine adapters, covered in the Minestom README | +| [`jvm/minestom-primitives`](../minestom-primitives/README.md) | `com.chunkzero.chunk:minestom-primitives` | Login handling for apps that run their own sessions on Minestom | The [Gradle plugin](../gradle-plugin/README.md) adds the right one to each app. @@ -47,6 +48,58 @@ progress, heap, GC, CPU, sessions and players) at least every three seconds. A b again. Players do not pass through this link: the gateway connects to Minestom's own listener, and each login presents a single-use capability the JVM issued for that player's delivery. +## Running your own sessions + +`runtime-minestom` decides what a session is: a `Session` with its own scope in a shared Minestom process. An engine +adapter, or an app that wants other isolation, can instead implement `SessionHandler` and pass it to `chunk.host(...)`. +The returned `ChunkSessions` keeps the accounting core relies on, whatever a session is: + +| Chunk keeps | The handler decides | +| -------------------------------------------------------------------- | --------------------------------------------------------- | +| Session phases, the 10-second creation deadline, 256 live sessions | What a session is and how it is isolated from the others | +| Delivery capabilities, identity checks and fencing, session capacity | Where an admitted player spawns, and when they've arrived | +| Withdrawing a session's players before its handler finishes it | How it tears a session down | +| Which session methods may run, and for whom | What a session method does | + +```java +try (var chunk = ChunkProcess.connect()) { + var sessions = chunk.host(new SessionHandler() { + @Override public void create(SessionControl session) { + // session.id(), type(), capacity(), configurationJson(), backend() + games.open(session).whenComplete((ok, error) -> { + if (error == null) session.ready(); + else session.fail(error); + }); + } + + @Override public void finish(SessionControl session) { + games.close(session.id()).whenComplete((ok, error) -> { + if (error == null) session.ended(); + else session.ended(error); + }); + } + }); + // Start the engine's listener on chunk.playerAddress(), leaving compression and encryption to the gateway. + chunk.bind(port, protocolVersion); + chunk.ready(); + chunk.awaitShutdown(); +} +``` + +- Handler callbacks run one at a time on the host's own thread and must return promptly. Report through the + `SessionControl` from any thread. +- `finish` runs once every player of the session has been released, including after a failed creation. A session counts + against the JVM until it has `ended()`. +- Each login presents the payload of its `chunk:delivery` login plugin response. + `sessions.admit(setup, uuid, name, disconnect)` checks it and returns the player's `Delivery`; admit the player with + its `player()` profile, and call `arrived()` once they are in the session. If core withdraws the delivery, + `disconnect` runs; once the player's connection closed and any leave handling settled, call `release()`. +- Call `chunk.tick()` once per engine tick, so core can tell a stalled JVM. +- `Delivery.move(destination)` and `operationId(action)` work as `SessionScope`'s do. Session methods reach + `SessionHandler.method`, which no session declares by default. + +[`minestom-primitives`](../minestom-primitives/README.md) handles the login side for Minestom. + ## Launch configuration The host (control's local process host under `chunk dev`, or `chunk-jvm` on a JVM machine) sets these variables: diff --git a/jvm/runtime/build.gradle.kts b/jvm/runtime/build.gradle.kts index ee601c75..dc6dce1e 100644 --- a/jvm/runtime/build.gradle.kts +++ b/jvm/runtime/build.gradle.kts @@ -5,7 +5,7 @@ plugins { java { toolchain.languageVersion = JavaLanguageVersion.of(25) } tasks.withType().configureEach { options.release = 25 } dependencies { - implementation(project(":jvm:backend-client")) + api(project(":jvm:backend-client")) implementation(project(":jvm:proto")) implementation(libs.grpc.netty) compileOnly(libs.jetbrains.annotations) diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java index ff1ea2f7..51176609 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java @@ -15,6 +15,7 @@ import org.jetbrains.annotations.Nullable; +import java.net.Inet6Address; import java.net.InetAddress; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -30,6 +31,7 @@ public final class ChunkProcess implements AutoCloseable { private final ProcessHealth health = new ProcessHealth(ticks); private final CompletableFuture shutdown = new CompletableFuture<>(); private volatile @Nullable CoreLink link; + private volatile @Nullable ChunkSessions sessions; private @Nullable ProcessState state; private String playerEndpoint = ""; private int protocol; @@ -108,6 +110,71 @@ void progress(int sessions, int players) { health.tick(sessions, players); } + /** Records one engine tick; core watches tick progress to tell a stalled JVM. */ + public void tick() { + var current = sessions; + if (current == null) progress(0, 0); + else progress(current.activeCount(), current.players()); + } + + /** + * Runs the sessions, deliveries and session methods core assigns this JVM through {@code + * handler}. Call it once, before {@link #bind(int, int)}. + */ + public synchronized ChunkSessions host(SessionHandler handler) { + if (closed || sessions != null) + throw new IllegalStateException("Sessions already hosted or process closed"); + var hosted = + ChunkSessions.linked( + handler, + new ChunkSessions.Link() { + @Override + public boolean acceptsWork() { + return isReady(); + } + + @Override + public BackendSession backend(String session) { + return ChunkProcess.this.backend(session); + } + + @Override + public CompletionStage move( + String delivery, Position generation, Destination destination) { + return ChunkProcess.this.move(delivery, generation, destination); + } + + @Override + public void methodResult(String operation, JvmMethodResult result) { + ChunkProcess.this.methodResult(operation, result); + } + + @Override + public void flush() { + ChunkProcess.this.flush(); + } + }); + sessions = hosted; + return hosted; + } + + /** + * Names the engine's player listener: it accepts the gateway on {@link #playerAddress()} at + * {@code port}, speaking Minecraft protocol {@code protocol}. The engine must leave compression + * and encryption to the gateway. + */ + public synchronized void bind(int port, int protocol) { + var hosted = sessions; + if (hosted == null) throw new IllegalStateException("Host sessions before binding"); + if (port < 1 || port > 65_535) throw new IllegalArgumentException("Invalid port"); + var address = playerAddress(); + var host = + address instanceof Inet6Address + ? "[" + address.getHostAddress() + "]" + : address.getHostAddress(); + bind(host + ":" + port, protocol, hosted.state()); + } + /** Reports session and delivery changes to core. */ void flush() { var current = link; @@ -140,8 +207,8 @@ public boolean isReady() { return health.acceptsWork(); } - /** The address engine adapters serve players on. */ - InetAddress playerAddress() { + /** The address the engine's player listener binds; only the gateway connects to it. */ + public InetAddress playerAddress() { return environment.playerAddress(); } @@ -205,6 +272,8 @@ public synchronized void close() { shutdown.complete(null); var current = link; if (current != null) current.close(); + var hosted = sessions; + if (hosted != null) hosted.close(); backend.close(); } } diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java new file mode 100644 index 00000000..877a3621 --- /dev/null +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java @@ -0,0 +1,846 @@ +package com.chunkzero.chunk.runtime; + +import chunk.sync.v1.CoreOuterClass.Position; +import chunk.sync.v1.Jvm.JvmDelivery; +import chunk.sync.v1.Jvm.JvmDeliveryPhase; +import chunk.sync.v1.Jvm.JvmDeliveryStatus; +import chunk.sync.v1.Jvm.JvmMethodCall; +import chunk.sync.v1.Jvm.JvmMethodPhase; +import chunk.sync.v1.Jvm.JvmMethodResult; +import chunk.sync.v1.Jvm.JvmReport; +import chunk.sync.v1.Jvm.JvmSession; +import chunk.sync.v1.Jvm.JvmSessionPhase; +import chunk.sync.v1.Jvm.JvmSessionStatus; +import chunk.sync.v1.Jvm.PlayerSetup; + +import com.chunkzero.chunk.backend.api.BackendJson; +import com.chunkzero.chunk.backend.api.Destination; +import com.chunkzero.chunk.backend.client.BackendSession; +import com.chunkzero.chunk.runtime.control.ProcessState; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Parser; + +import org.jetbrains.annotations.Nullable; + +import tools.jackson.databind.JsonNode; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** + * Runs the sessions, player deliveries and session methods core assigns this JVM through a {@link + * SessionHandler}, and reports them to core. It keeps the accounting core relies on: session + * phases, capacity, delivery capabilities and fencing, and which methods may run. What a session + * is, and how it is isolated from the others, is up to the handler. + * + *

Get one from {@link ChunkProcess#host}. Its methods are thread-safe. + */ +public final class ChunkSessions implements AutoCloseable { + /** Matches how long control waits for a created session to become ready. */ + static final Duration CREATE_DEADLINE = Duration.ofSeconds(10); + + private static final Pattern SESSION_ID = Pattern.compile("[A-Za-z0-9_-]{1,128}"); + private static final Pattern USERNAME = Pattern.compile("[A-Za-z0-9_]{1,16}"); + private static final int MAX_SESSIONS = 256; + private static final int MAX_DELIVERIES = 4096; + private static final int MAX_SETUP = 4096; + private static final int MAX_JSON = 48 * 1024; + + /** How long a delivery waits for its session to become ready. */ + private static final long WAIT_NANOS = TimeUnit.SECONDS.toNanos(30); + + /** + * How long the gateway may take to connect the player: it loads their resource packs first. + * Control cancels the claim of a delivery not activated within 60 seconds of its preparation + * anyway. + */ + private static final long CONNECT_NANOS = TimeUnit.SECONDS.toNanos(60); + + private final SessionHandler handler; + private final Link link; + private final Executor callbacks; + private final Duration createDeadline; + private final @Nullable ScheduledExecutorService worker; + private final DeliveryFence fence = new DeliveryFence(); + + // Everything below is guarded by this. + private final Map sessions = new HashMap<>(); + // Sessions that failed before they were created, or ended without ever being created. + private final Map outcomes = new HashMap<>(); + // The sessions the latest snapshot lists, those it asked to create, and to finish. + private Set desired = Set.of(); + private final Set created = new HashSet<>(); + private final Set finished = new HashSet<>(); + // The deliveries prepared, the topic's latest, those waiting for their session since when, and + // those closed without preparing. + private final Map deliveries = new LinkedHashMap<>(); + private Map wanted = Map.of(); + private final Map pending = new HashMap<>(); + private final Map refused = new HashMap<>(); + // Each method the topic lists, until its key is gone. + private final Map methods = new HashMap<>(); + private boolean closed; + + /** + * Runs handler callbacks on {@code callbacks}, and fails sessions not ready within {@code + * createDeadline}. The owner calls {@link #sweep()} periodically. + */ + ChunkSessions( + SessionHandler handler, + Link link, + Executor callbacks, + Duration createDeadline, + @Nullable ScheduledExecutorService worker) { + this.handler = Objects.requireNonNull(handler); + this.link = link; + this.callbacks = callbacks; + this.createDeadline = createDeadline; + this.worker = worker; + } + + /** Runs callbacks and sweeps on a thread of its own. */ + static ChunkSessions linked(SessionHandler handler, Link link) { + var worker = + Executors.newSingleThreadScheduledExecutor( + task -> { + var thread = new Thread(task, "chunk-sessions"); + thread.setDaemon(true); + return thread; + }); + var sessions = new ChunkSessions(handler, link, worker, CREATE_DEADLINE, worker); + worker.scheduleWithFixedDelay(sessions::sweep, 50, 50, TimeUnit.MILLISECONDS); + return sessions; + } + + /** + * Sessions without core, for tests: callbacks run on the calling thread, moves fail and method + * results go nowhere. Call {@link #sweep()} to expire deliveries. + */ + static ChunkSessions detached( + SessionHandler handler, Function backends) { + return new ChunkSessions( + handler, + new Link() { + @Override + public boolean acceptsWork() { + return true; + } + + @Override + public @Nullable BackendSession backend(String session) { + return backends.apply(session); + } + + @Override + public CompletionStage move( + String delivery, Position generation, Destination destination) { + return CompletableFuture.failedFuture( + new IllegalStateException("Moves unavailable")); + } + + @Override + public void methodResult(String operation, JvmMethodResult result) {} + + @Override + public void flush() {} + }, + Runnable::run, + CREATE_DEADLINE, + null); + } + + /** + * Admits a player presenting {@code setup}, the payload of their {@code chunk:delivery} login + * plugin response, as {@code uuid} and {@code name}. Admit the player with the returned + * delivery's {@link Delivery#player() profile}. {@code disconnect} is called once if the + * delivery is withdrawn; the engine then disconnects the player and {@linkplain + * Delivery#release() releases} the delivery. + * + * @throws IllegalArgumentException if the setup is invalid or presents another player + * @throws IllegalStateException if the delivery can no longer be admitted + */ + public Delivery admit(byte[] setup, UUID uuid, String name, Runnable disconnect) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(name); + Objects.requireNonNull(disconnect); + if (setup == null || setup.length > MAX_SETUP) + throw new IllegalArgumentException("Invalid delivery setup"); + PlayerSetup parsed; + try { + parsed = PlayerSetup.parseFrom(setup); + } catch (InvalidProtocolBufferException error) { + throw new IllegalArgumentException("Invalid delivery setup", error); + } + var after = new ArrayList(); + try { + synchronized (this) { + var delivery = deliveries.get(parsed.getOperationId()); + if (delivery == null) throw new IllegalArgumentException("Unknown operation"); + expire(delivery, after); + if (delivery.closed + || delivery.consumed + || phase(delivery.session()) != JvmSessionPhase.JVM_SESSION_PHASE_READY) + throw new IllegalStateException("Delivery unavailable"); + if (!MessageDigest.isEqual( + delivery.capability, parsed.getCapability().toByteArray())) + throw new IllegalArgumentException("Invalid delivery capability"); + if (!delivery.player().uuid().equals(uuid) + || !delivery.player().name().equals(name)) + throw new IllegalArgumentException("Player identity mismatch"); + fence.claim(uuid.toString(), delivery.generation()); + delivery.consumed = true; + delivery.disconnect = disconnect; + return delivery; + } + } finally { + after.forEach(Runnable::run); + link.flush(); + } + } + + /** The open delivery {@code id}, or null. */ + public synchronized @Nullable Delivery delivery(String id) { + var delivery = deliveries.get(id); + return delivery == null || delivery.closed ? null : delivery; + } + + /** Sessions starting, ready or ending, which count against this JVM's capacity. */ + public synchronized int activeCount() { + return (int) + sessions.values().stream() + .filter( + session -> + switch (session.phase) { + case JVM_SESSION_PHASE_STARTING, + JVM_SESSION_PHASE_READY, + JVM_SESSION_PHASE_ENDING -> + true; + default -> false; + }) + .count(); + } + + /** Players admitted and not yet released. */ + public synchronized int players() { + return (int) + deliveries.values().stream() + .filter(delivery -> delivery.consumed && !delivery.isReleased) + .count(); + } + + /** Stops running the topic, and withdraws every delivery. */ + @Override + public void close() { + var after = new ArrayList(); + synchronized (this) { + if (closed) return; + closed = true; + deliveries.values().forEach(delivery -> close(delivery, after)); + } + after.forEach(Runnable::run); + if (worker != null) worker.shutdown(); + } + + ProcessState state() { + return new ProcessState() { + @Override + public JvmReport inventory() { + synchronized (ChunkSessions.this) { + forget(); + return ChunkSessions.this.inventory(); + } + } + + @Override + public void apply(Map entries) { + ChunkSessions.this.apply(entries); + } + }; + } + + synchronized JvmReport inventory() { + var prepared = new HashMap(); + var attached = new HashMap(); + var report = JvmReport.newBuilder(); + for (var delivery : deliveries.values()) { + var status = delivery.status(); + report.addDeliveries(status); + if (status.getPhase() == JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED) + prepared.merge(delivery.session(), 1, Integer::sum); + if (delivery.consumed && !delivery.isReleased) + attached.merge(delivery.session(), 1, Integer::sum); + } + refused.forEach( + (operation, delivery) -> + report.addDeliveries( + JvmDeliveryStatus.newBuilder() + .setOperationId(operation) + .setGeneration(delivery.getGeneration()) + .setPhase(JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED))); + sessions.forEach( + (id, session) -> + report.addSessions( + session.status( + prepared.getOrDefault(id, 0), + attached.getOrDefault(id, 0)))); + report.addAllSessions(outcomes.values()); + return report.build(); + } + + /** Drops the records of ended sessions the topic no longer lists. */ + synchronized void forget() { + sessions.entrySet() + .removeIf( + entry -> + !desired.contains(entry.getKey()) + && terminal(entry.getValue().phase)); + outcomes.keySet().removeIf(id -> !desired.contains(id)); + } + + /** Makes the JVM's work match the latest snapshot of its topic. */ + void apply(Map entries) { + var listed = entries(entries, "session/", JvmSession.parser()); + var latest = entries(entries, "delivery/", JvmDelivery.parser()); + var calls = entries(entries, "method/", JvmMethodCall.parser()); + var after = new ArrayList(); + synchronized (this) { + if (closed) return; + listed.forEach( + (id, session) -> { + if (created.add(id) && !session.getFinish()) { + if (link.acceptsWork()) create(id, session, after); + else outcome(id, session, JvmSessionPhase.JVM_SESSION_PHASE_FAILED); + } + if (session.getFinish() && finished.add(id)) finish(id, session, after); + }); + for (var id : Set.copyOf(created)) { + if (listed.containsKey(id)) continue; + created.remove(id); + if (finished.add(id)) finish(id, JvmSession.getDefaultInstance(), after); + } + finished.retainAll(created); + desired = Set.copyOf(listed.keySet()); + applyDeliveries(latest, after); + applyMethods(calls, after); + } + after.forEach(Runnable::run); + } + + /** Expires deliveries never connected, refuses those whose session won't come, and reports. */ + void sweep() { + var after = new ArrayList(); + synchronized (this) { + deliveries.values().forEach(delivery -> expire(delivery, after)); + settle(after); + } + after.forEach(Runnable::run); + link.flush(); + } + + /** + * Creates session {@code id}, completing once it is ready. A repeat with the same type, + * capacity and configuration finds the same session. + */ + CompletableFuture create(String id, JvmSession session) { + var after = new ArrayList(); + CompletableFuture result; + synchronized (this) { + result = create(id, session, after); + } + after.forEach(Runnable::run); + return result; + } + + /** Ends session {@code id}. One never created ends at once, reported with {@code session}. */ + CompletableFuture finish(String id, JvmSession session) { + var after = new ArrayList(); + CompletableFuture result; + synchronized (this) { + result = finish(id, session, after); + } + after.forEach(Runnable::run); + return result; + } + + /** The phase of session {@code id}, or null while there is no record of it. */ + synchronized @Nullable JvmSessionPhase phase(String id) { + var session = sessions.get(id); + if (session != null) return session.phase; + var outcome = outcomes.get(id); + return outcome == null ? null : outcome.getPhase(); + } + + static boolean terminal(@Nullable JvmSessionPhase phase) { + return phase == JvmSessionPhase.JVM_SESSION_PHASE_ENDED + || phase == JvmSessionPhase.JVM_SESSION_PHASE_FAILED; + } + + private CompletableFuture create( + String id, JvmSession session, List after) { + JsonNode configuration; + try { + if (!SESSION_ID.matcher(id).matches() + || session.getCapacity() < 1 + || session.getCapacity() > 128) + throw new IllegalArgumentException("Invalid session"); + configuration = configuration(session); + } catch (RuntimeException error) { + if (!sessions.containsKey(id)) + outcome(id, session, JvmSessionPhase.JVM_SESSION_PHASE_FAILED); + return CompletableFuture.failedFuture(error); + } + var previous = sessions.get(id); + if (previous != null) { + if (!previous.type().equals(session.getSessionType()) + || previous.capacity() != session.getCapacity() + || !previous.configuration().equals(configuration)) + return CompletableFuture.failedFuture( + new IllegalArgumentException("Session creation changed")); + return previous.created; + } + // Control still counts a session it asked for, so it never sees room for one beyond this. + if (activeCount() >= MAX_SESSIONS) + return CompletableFuture.failedFuture( + new IllegalStateException("Too many live sessions")); + var control = new SessionControl(this, id, session, configuration, link.backend(id)); + outcomes.remove(id); + sessions.put(id, control); + after.add( + () -> { + CompletableFuture.delayedExecutor( + createDeadline.toMillis(), TimeUnit.MILLISECONDS) + .execute( + () -> + fail( + control, + new TimeoutException( + "Session " + + id + + " was not ready within " + + createDeadline), + true)); + dispatch(control, () -> handler.create(control)); + }); + return control.created; + } + + private static JsonNode configuration(JvmSession session) { + var bytes = session.getConfigurationJson(); + if (bytes.size() > 64 * 1024 || !bytes.isValidUtf8()) + throw new IllegalArgumentException("Invalid session configuration encoding or size"); + var config = BackendJson.mapper().readTree(bytes.isEmpty() ? "{}" : bytes.toStringUtf8()); + if (config == null || !config.isObject()) + throw new IllegalArgumentException("Session configuration must be an object"); + return config; + } + + private CompletableFuture finish( + String id, JvmSession session, List after) { + var control = sessions.get(id); + if (control == null) + return CompletableFuture.completedFuture( + outcome(id, session, JvmSessionPhase.JVM_SESSION_PHASE_ENDED)); + finish(control, after); + return control.ended.thenApply(ignored -> control.status(0, 0)); + } + + private JvmSessionStatus outcome(String id, JvmSession session, JvmSessionPhase phase) { + var status = + JvmSessionStatus.newBuilder() + .setId(id) + .setSessionType(session.getSessionType()) + .setCapacity(session.getCapacity()) + .setPhase(phase) + .build(); + outcomes.put(id, status); + return status; + } + + void ready(SessionControl control) { + var after = new ArrayList(); + synchronized (this) { + if (control.settled) return; + control.settled = true; + if (!control.finishing) control.phase = JvmSessionPhase.JVM_SESSION_PHASE_READY; + var status = control.status(0, 0); + after.add(() -> control.created.complete(status)); + // A finish requested while it was starting waited for creation to settle. + if (control.finishing) withdraw(control, after); + } + after.forEach(Runnable::run); + link.flush(); + } + + void fail(SessionControl control, Throwable error) { + fail(control, error, false); + } + + private void fail(SessionControl control, Throwable error, boolean onlyStarting) { + Objects.requireNonNull(error); + var after = new ArrayList(); + synchronized (this) { + if (terminal(control.phase) || (onlyStarting && control.settled)) return; + if (control.failure == null) control.failure = error; + if (!control.settled) { + control.settled = true; + after.add(() -> control.created.completeExceptionally(error)); + if (control.finishing) withdraw(control, after); + } + finish(control, after); + } + after.forEach(Runnable::run); + link.flush(); + } + + CompletionStage finish(SessionControl control) { + var after = new ArrayList(); + synchronized (this) { + finish(control, after); + } + after.forEach(Runnable::run); + link.flush(); + return control.ended.minimalCompletionStage(); + } + + /** Ends a session once its creation settled: its deliveries close, then its handler ends it. */ + private void finish(SessionControl control, List after) { + if (control.finishing) return; + control.finishing = true; + control.phase = JvmSessionPhase.JVM_SESSION_PHASE_ENDING; + if (control.settled) withdraw(control, after); + } + + private void withdraw(SessionControl control, List after) { + var releases = + deliveries.values().stream() + .filter(delivery -> delivery.session().equals(control.id())) + .map(delivery -> close(delivery, after)) + .toArray(CompletableFuture[]::new); + after.add( + () -> + CompletableFuture.allOf(releases) + .whenComplete( + (ignored, error) -> { + synchronized (this) { + control.handlerFinishing = true; + } + dispatch( + () -> { + try { + handler.finish(control); + } catch (RuntimeException failure) { + ended(control, failure); + } + }); + })); + } + + void ended(SessionControl control, @Nullable Throwable error) { + Throwable failure; + synchronized (this) { + if (!control.handlerFinishing) + throw new IllegalStateException("Session is not finishing"); + if (terminal(control.phase)) return; + if (control.failure == null) control.failure = error; + failure = control.failure; + control.phase = + failure == null + ? JvmSessionPhase.JVM_SESSION_PHASE_ENDED + : JvmSessionPhase.JVM_SESSION_PHASE_FAILED; + } + var backend = control.backend(); + if (backend != null) backend.close(); + if (failure == null) control.ended.complete(null); + else control.ended.completeExceptionally(failure); + link.flush(); + } + + private void applyDeliveries(Map latest, List after) { + wanted = Map.copyOf(latest); + deliveries.forEach( + (operation, delivery) -> { + var current = wanted.get(operation); + if (current == null || current.getWithdraw()) close(delivery, after); + }); + refused.keySet().retainAll(wanted.keySet()); + pending.keySet().retainAll(wanted.keySet()); + wanted.forEach( + (operation, delivery) -> { + if (!deliveries.containsKey(operation) && !refused.containsKey(operation)) + pending.putIfAbsent(operation, link.nanoTime()); + }); + settle(after); + } + + /** Prepares or refuses each waiting delivery it can, and forgets those released and gone. */ + private void settle(List after) { + for (var operation : List.copyOf(pending.keySet())) { + var delivery = wanted.get(operation); + var phase = phase(delivery.getSession()); + if (phase == JvmSessionPhase.JVM_SESSION_PHASE_READY && !delivery.getWithdraw()) { + try { + prepare(operation, delivery); + } catch (RuntimeException error) { + refused.put(operation, delivery); + } + } else if (delivery.getWithdraw() + || closed + || !link.acceptsWork() + || (phase != null && phase != JvmSessionPhase.JVM_SESSION_PHASE_STARTING) + || link.nanoTime() - pending.get(operation) >= WAIT_NANOS) { + refused.put(operation, delivery); + } else continue; + pending.remove(operation); + } + deliveries + .entrySet() + .removeIf( + entry -> + !wanted.containsKey(entry.getKey()) && entry.getValue().isReleased); + } + + private void prepare(String operation, JvmDelivery delivery) { + if (closed || !link.acceptsWork()) throw new IllegalStateException("Not accepting work"); + var identity = delivery.getPlayer(); + if (!USERNAME.matcher(identity.getUsername()).matches() + || !UUID.fromString(identity.getUuid()).toString().equals(identity.getUuid())) + throw new IllegalArgumentException("Invalid player identity"); + if (deliveries.size() >= MAX_DELIVERIES) + throw new IllegalStateException("Process delivery capacity reached"); + var session = sessions.get(delivery.getSession()); + var reserved = + deliveries.values().stream() + .filter( + candidate -> + candidate.session().equals(delivery.getSession()) + && !candidate.isReleased) + .count(); + if (reserved >= session.capacity()) throw new IllegalStateException("Session full"); + deliveries.put(operation, new Delivery(this, operation, delivery, link.nanoTime())); + } + + private void expire(Delivery delivery, List after) { + if (!delivery.consumed + && !delivery.closed + && link.nanoTime() - delivery.openedAt >= CONNECT_NANOS) close(delivery, after); + } + + /** Closes a delivery: at once if never admitted, or else once the engine releases it. */ + private CompletableFuture close(Delivery delivery, List after) { + if (delivery.closed) return delivery.released; + delivery.closed = true; + if (!delivery.consumed) { + delivery.isReleased = true; + after.add(() -> delivery.released.complete(null)); + } else { + var disconnect = Objects.requireNonNull(delivery.disconnect); + dispatchLater(after, disconnect); + } + return delivery.released; + } + + void arrived(Delivery delivery) { + synchronized (this) { + if (!delivery.consumed || delivery.closed || delivery.arrived) return; + delivery.arrived = true; + } + link.flush(); + } + + void release(Delivery delivery) { + synchronized (this) { + if (delivery.isReleased) return; + delivery.closed = true; + delivery.isReleased = true; + if (delivery.consumed) + fence.release(delivery.player().uuid().toString(), delivery.generation()); + } + delivery.released.complete(null); + link.flush(); + } + + CompletionStage move(Delivery delivery, Destination destination) { + return link.move(delivery.id(), delivery.generation(), destination); + } + + private void applyMethods(Map calls, List after) { + methods.keySet().retainAll(calls.keySet()); + calls.forEach( + (id, call) -> { + var operation = methods.get(id); + if (operation == null) { + operation = new MethodCall(id, call); + methods.put(id, operation); + var queued = operation; + dispatchLater(after, () -> execute(queued)); + } + if (call.getCancel() && !operation.started) cancel(operation); + }); + } + + private void execute(MethodCall operation) { + var call = operation.call; + SessionControl session; + synchronized (this) { + if (operation.done || methods.get(operation.id) != operation) return; + session = sessions.get(call.getSession()); + if (closed + || session == null + || session.phase != JvmSessionPhase.JVM_SESSION_PHASE_READY + || link.currentTimeMillis() >= call.getDeadlineMs() + || !arrivedIn(call.getDelivery(), call.getSession())) { + cancel(operation); + return; + } + operation.started = true; + } + CompletionStage result; + try { + result = + handler.method( + session, call.getMethod(), call.getArgumentsJson().toStringUtf8()); + } catch (RuntimeException error) { + result = CompletableFuture.failedFuture(error); + } + result.whenComplete( + (json, error) -> { + var failed = + JvmMethodResult.newBuilder() + .setPhase(JvmMethodPhase.JVM_METHOD_PHASE_FAILED) + .build(); + var outcome = failed; + if (error == null && json != null) { + var encoded = json.getBytes(StandardCharsets.UTF_8); + if (encoded.length <= MAX_JSON) + outcome = + JvmMethodResult.newBuilder() + .setPhase(JvmMethodPhase.JVM_METHOD_PHASE_COMPLETED) + .setResultJson(ByteString.copyFrom(encoded)) + .build(); + } + synchronized (this) { + complete(operation, outcome); + } + }); + } + + private boolean arrivedIn(String operation, String session) { + var delivery = deliveries.get(operation); + return delivery != null + && delivery.arrived + && !delivery.closed + && delivery.session().equals(session); + } + + private void cancel(MethodCall operation) { + complete( + operation, + JvmMethodResult.newBuilder() + .setPhase(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED) + .build()); + } + + private void complete(MethodCall operation, JvmMethodResult result) { + if (operation.done) return; + operation.done = true; + if (methods.get(operation.id) == operation) link.methodResult(operation.id, result); + } + + /** Runs a handler callback for {@code control}, failing the session if it throws. */ + private void dispatch(SessionControl control, Runnable callback) { + dispatch( + () -> { + try { + callback.run(); + } catch (RuntimeException error) { + fail(control, error); + } + }); + } + + private void dispatchLater(List after, Runnable callback) { + after.add(() -> dispatch(callback)); + } + + private void dispatch(Runnable callback) { + try { + callbacks.execute(callback); + } catch (RejectedExecutionException ignored) { + // Closed: the process is stopping. + } + } + + /** The entries under {@code prefix}, by the rest of their key, skipping undecodable ones. */ + private static Map entries( + Map entries, String prefix, Parser parser) { + var parsed = new HashMap(); + entries.forEach( + (key, value) -> { + if (!key.startsWith(prefix)) return; + try { + parsed.put(key.substring(prefix.length()), parser.parseFrom(value)); + } catch (InvalidProtocolBufferException ignored) { + // Not an entry this runtime can run. + } + }); + return parsed; + } + + private static final class MethodCall { + final String id; + final JvmMethodCall call; + boolean started; + boolean done; + + MethodCall(String id, JvmMethodCall call) { + this.id = id; + this.call = call; + } + } + + /** What sessions need from the process and core. */ + interface Link { + /** Whether the process accepts new sessions and players. */ + boolean acceptsWork(); + + @Nullable + BackendSession backend(String session); + + CompletionStage move( + String delivery, Position generation, Destination destination); + + void methodResult(String operation, JvmMethodResult result); + + /** Reports what changed to core. */ + void flush(); + + default long nanoTime() { + return System.nanoTime(); + } + + default long currentTimeMillis() { + return System.currentTimeMillis(); + } + } +} diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java new file mode 100644 index 00000000..c60c794e --- /dev/null +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java @@ -0,0 +1,153 @@ +package com.chunkzero.chunk.runtime; + +import chunk.sync.v1.CoreOuterClass.Position; +import chunk.sync.v1.Jvm.JvmDelivery; +import chunk.sync.v1.Jvm.JvmDeliveryPhase; +import chunk.sync.v1.Jvm.JvmDeliveryStatus; + +import com.chunkzero.chunk.backend.api.Destination; +import com.chunkzero.chunk.backend.client.OperationId; +import com.google.protobuf.ByteString; + +import org.jetbrains.annotations.Nullable; + +import java.security.SecureRandom; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.regex.Pattern; + +/** + * A player core placed in one of this JVM's sessions, from the moment {@link ChunkSessions#admit} + * accepts their login until they are released. Its methods are thread-safe. + */ +public final class Delivery { + private static final Pattern ACTION = Pattern.compile("[A-Za-z0-9_-]{1,32}"); + + private final ChunkSessions owner; + private final String id; + private final JvmDelivery spec; + private final PlayerProfile player; + final byte[] capability = new byte[32]; + final long openedAt; + final CompletableFuture released = new CompletableFuture<>(); + + // Guarded by owner. + boolean consumed; + boolean arrived; + boolean closed; + boolean isReleased; + @Nullable Runnable disconnect; + + Delivery(ChunkSessions owner, String id, JvmDelivery spec, long openedAt) { + this.owner = owner; + this.id = id; + this.spec = spec; + this.openedAt = openedAt; + var identity = spec.getPlayer(); + player = + new PlayerProfile( + UUID.fromString(identity.getUuid()), + identity.getUsername(), + identity.getPropertiesList().stream() + .map( + property -> + new PlayerProfile.Property( + property.getName(), + property.getValue(), + property.hasSignature() + ? property.getSignature() + : null)) + .toList()); + new SecureRandom().nextBytes(capability); + } + + /** The claim's operation ID, as core names the delivery. */ + public String id() { + return id; + } + + /** The ID of the session the player joins. */ + public String session() { + return spec.getSession(); + } + + /** Who the player authenticated as. */ + public PlayerProfile player() { + return player; + } + + /** + * The player is in the session. Core then lets them run session methods and move. Ignored once + * the delivery closed. + */ + public void arrived() { + owner.arrived(this); + } + + public boolean isArrived() { + synchronized (owner) { + return arrived && !closed; + } + } + + /** + * The player is gone and the session no longer acts for them. Call it once their connection + * closed and any leave handling settled, whether they left or the delivery was withdrawn. + */ + public void release() { + owner.release(this); + } + + /** + * Asks core to move the player to {@code destination}, through its admission policy. Core + * accepts only an arrived player's current delivery. + */ + public CompletionStage move(Destination destination) { + return owner.move(this, Objects.requireNonNull(destination)); + } + + /** A mutation ID that stays the same for one action during this delivery. */ + public OperationId operationId(String action) { + if (!ACTION.matcher(action).matches()) + throw new IllegalArgumentException("Invalid operation action"); + var generation = spec.getGeneration(); + return new OperationId( + session() + + "/" + + player.uuid() + + "/" + + generation.getEpoch() + + "." + + generation.getRevision() + + "/" + + action); + } + + Position generation() { + return spec.getGeneration(); + } + + JvmDelivery spec() { + return spec; + } + + /** Called with the owner's lock held. */ + JvmDeliveryStatus status() { + JvmDeliveryPhase phase; + if (isReleased) phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED; + else if (closed) phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_WITHDRAWING; + else if (arrived) phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_ARRIVED; + else if (consumed) phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_ATTACHED; + else phase = JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED; + var status = + JvmDeliveryStatus.newBuilder() + .setOperationId(id) + .setGeneration(spec.getGeneration()) + .setPhase(phase); + if (phase == JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED) + status.setCapability(ByteString.copyFrom(capability)); + return status.build(); + } +} diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/DeliveryFence.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/DeliveryFence.java similarity index 97% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/DeliveryFence.java rename to jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/DeliveryFence.java index 204593f4..f271f728 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/DeliveryFence.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/DeliveryFence.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.runtime; import chunk.sync.v1.CoreOuterClass.Position; diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/PlayerProfile.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/PlayerProfile.java new file mode 100644 index 00000000..542f2e09 --- /dev/null +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/PlayerProfile.java @@ -0,0 +1,27 @@ +package com.chunkzero.chunk.runtime; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * Who a delivered player authenticated as at the gateway. Engines admit the player with this + * profile, including its signed properties. + */ +public record PlayerProfile(UUID uuid, String name, List properties) { + public PlayerProfile { + Objects.requireNonNull(uuid); + Objects.requireNonNull(name); + properties = List.copyOf(properties); + } + + /** A profile property, such as {@code textures}. */ + public record Property(String name, String value, @Nullable String signature) { + public Property { + Objects.requireNonNull(name); + Objects.requireNonNull(value); + } + } +} diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java new file mode 100644 index 00000000..152b2c18 --- /dev/null +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java @@ -0,0 +1,129 @@ +package com.chunkzero.chunk.runtime; + +import chunk.sync.v1.Jvm.JvmSession; +import chunk.sync.v1.Jvm.JvmSessionPhase; +import chunk.sync.v1.Jvm.JvmSessionStatus; + +import com.chunkzero.chunk.backend.client.BackendSession; + +import org.jetbrains.annotations.Nullable; + +import tools.jackson.databind.JsonNode; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +/** + * One session core asked this JVM to run, and how its {@link SessionHandler} reports on it. Its + * methods are thread-safe. + */ +public final class SessionControl { + private final ChunkSessions owner; + private final String id; + private final JvmSession spec; + private final JsonNode configuration; + private final @Nullable BackendSession backend; + + // Guarded by owner. + JvmSessionPhase phase = JvmSessionPhase.JVM_SESSION_PHASE_STARTING; + boolean settled; + boolean finishing; + boolean handlerFinishing; + @Nullable Throwable failure; + + final CompletableFuture created = new CompletableFuture<>(); + final CompletableFuture ended = new CompletableFuture<>(); + + SessionControl( + ChunkSessions owner, + String id, + JvmSession spec, + JsonNode configuration, + @Nullable BackendSession backend) { + this.owner = owner; + this.id = id; + this.spec = spec; + this.configuration = configuration; + this.backend = backend; + } + + /** The session's ID, unique within the deployment. */ + public String id() { + return id; + } + + /** The session type, as {@code /}. */ + public String type() { + return spec.getSessionType(); + } + + /** How many players the session admits at once, from 1 to 128. */ + public int capacity() { + return spec.getCapacity(); + } + + /** The session's creation configuration, a JSON object. */ + public String configurationJson() { + return configuration.toString(); + } + + /** Calls the backend as this session; closed once the session has ended. */ + public @Nullable BackendSession backend() { + return backend; + } + + /** The session admits players. Ignored unless it is still starting. */ + public void ready() { + owner.ready(this); + } + + /** The session can't continue: it ends, and is reported failed once its handler finished it. */ + public void fail(Throwable error) { + owner.fail(this, error); + } + + /** + * Ends the session: its players are released and then its handler finishes it. Completes once + * it ended, or exceptionally if it failed. + */ + public CompletionStage finish() { + return owner.finish(this); + } + + /** The handler finished the session. */ + public void ended() { + owner.ended(this, null); + } + + /** The handler finished the session, which failed with {@code error}. */ + public void ended(Throwable error) { + owner.ended(this, error); + } + + JvmSessionPhase phase() { + synchronized (owner) { + return phase; + } + } + + JsonNode configuration() { + return configuration; + } + + JvmSession spec() { + return spec; + } + + JvmSessionStatus status(int prepared, int attached) { + synchronized (owner) { + return JvmSessionStatus.newBuilder() + .setId(id) + .setSessionType(spec.getSessionType()) + .setCapacity(spec.getCapacity()) + .setPhase(phase) + .setPrepared(prepared) + .setAttached(attached) + .build(); + } + } +} diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java new file mode 100644 index 00000000..a633532b --- /dev/null +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java @@ -0,0 +1,36 @@ +package com.chunkzero.chunk.runtime; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +/** + * Decides what a session is and how it is isolated, for {@link ChunkSessions}. Callbacks run one at + * a time on the host's own thread and must return promptly; report outcomes through the {@link + * SessionControl}, from any thread. + */ +public interface SessionHandler { + /** + * Builds the session. Call {@link SessionControl#ready()} once it admits players, or {@link + * SessionControl#fail(Throwable)}. A session not ready within 10 seconds fails. Throwing fails + * the session. + */ + void create(SessionControl session); + + /** + * Tears the session down once its players have been released, and then calls {@link + * SessionControl#ended()}. Runs after a failed creation too. Throwing ends the session failed. + * Until it ends, the session keeps counting against this JVM's capacity. + */ + void finish(SessionControl session); + + /** + * Runs session method {@code method} of a ready session for one of its arrived players, and + * completes with its result as JSON, in at most 48 KiB. By default, sessions declare no + * methods. + */ + default CompletionStage method( + SessionControl session, String method, String argumentsJson) { + return CompletableFuture.failedFuture( + new IllegalArgumentException("Undeclared session method: " + method)); + } +} diff --git a/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java new file mode 100644 index 00000000..52cb4794 --- /dev/null +++ b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java @@ -0,0 +1,276 @@ +package com.chunkzero.chunk.runtime; + +import static org.junit.jupiter.api.Assertions.*; + +import chunk.sync.v1.CoreOuterClass.Position; +import chunk.sync.v1.Gateway.PlayerIdentity; +import chunk.sync.v1.Jvm.JvmDelivery; +import chunk.sync.v1.Jvm.JvmDeliveryPhase; +import chunk.sync.v1.Jvm.JvmDeliveryStatus; +import chunk.sync.v1.Jvm.JvmMethodCall; +import chunk.sync.v1.Jvm.JvmMethodPhase; +import chunk.sync.v1.Jvm.JvmMethodResult; +import chunk.sync.v1.Jvm.JvmSession; +import chunk.sync.v1.Jvm.JvmSessionPhase; +import chunk.sync.v1.Jvm.PlayerSetup; + +import com.chunkzero.chunk.backend.api.Destination; +import com.chunkzero.chunk.backend.client.BackendSession; +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +class ChunkSessionsTest { + private final UUID uuid = UUID.randomUUID(); + private final AtomicLong clock = new AtomicLong(); + private final Map results = new ConcurrentHashMap<>(); + private final Map topic = new TreeMap<>(); + + @Test + void sessionsAdmitDeliveredPlayersAndEndOnlyOnceTheyAreReleased() { + var handler = new Handler(); + var host = host(handler, Duration.ofSeconds(10)); + put("session/a", session(1, false)); + host.apply(topic); + var control = handler.created.getFirst(); + assertEquals("app/default", control.type()); + assertEquals(1, control.capacity()); + assertEquals("{}", control.configurationJson()); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_STARTING, host.phase("a")); + // A delivery waits for its session to become ready. + put("delivery/first", delivery("a", 1)); + host.apply(topic); + assertTrue(host.inventory().getDeliveriesList().isEmpty()); + control.ready(); + host.sweep(); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, host.phase("a")); + var prepared = status(host, "first"); + assertEquals(JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED, prepared.getPhase()); + // The session has room for one player. + put("delivery/second", delivery("a", 2)); + host.apply(topic); + assertEquals(JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED, status(host, "second").getPhase()); + + var setup = setup("first", prepared.getCapability()); + var disconnects = new AtomicInteger(); + assertThrows( + IllegalArgumentException.class, + () -> host.admit(setup("first", ByteString.EMPTY), uuid, "player", () -> {})); + assertThrows( + IllegalArgumentException.class, () -> host.admit(setup, uuid, "other", () -> {})); + var delivery = host.admit(setup, uuid, "player", disconnects::incrementAndGet); + assertEquals("a", delivery.session()); + assertEquals("player", delivery.player().name()); + assertEquals("a/" + uuid + "/1.1/coin", delivery.operationId("coin").value()); + assertThrows( + IllegalStateException.class, () -> host.admit(setup, uuid, "player", () -> {})); + assertEquals( + JvmDeliveryPhase.JVM_DELIVERY_PHASE_ATTACHED, status(host, "first").getPhase()); + assertEquals(1, host.players()); + + // Methods run only for an arrived player. + put("method/early", call("first")); + host.apply(topic); + assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, results.get("early").getPhase()); + delivery.arrived(); + put("method/late", call("first")); + host.apply(topic); + assertEquals("\"ok\"", results.get("late").getResultJson().toStringUtf8()); + + // Ending withdraws the player, and the handler finishes only after the release. + put("session/a", session(1, true)); + host.apply(topic); + assertEquals(1, disconnects.get()); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, host.phase("a")); + assertEquals( + JvmDeliveryPhase.JVM_DELIVERY_PHASE_WITHDRAWING, status(host, "first").getPhase()); + assertTrue(handler.finished.isEmpty()); + delivery.release(); + assertEquals(List.of(control), handler.finished); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, host.phase("a")); + control.ended(); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDED, host.phase("a")); + assertEquals(JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED, status(host, "first").getPhase()); + assertEquals(0, host.activeCount()); + + topic.clear(); + host.apply(topic); + host.state().inventory(); + assertTrue(host.inventory().getSessionsList().isEmpty()); + assertTrue(host.inventory().getDeliveriesList().isEmpty()); + } + + @Test + void unconnectedDeliveriesExpireAndLateCreationsFail() throws Exception { + var handler = new Handler(); + var host = host(handler, Duration.ofMillis(50)); + put("session/slow", session(4, false)); + put("session/fast", session(4, false)); + host.apply(topic); + handler.created.stream() + .filter(session -> session.id().equals("fast")) + .forEach(SessionControl::ready); + put("delivery/expired", delivery("fast", 1)); + host.apply(topic); + var capability = status(host, "expired").getCapability(); + clock.addAndGet(TimeUnit.SECONDS.toNanos(61)); + host.sweep(); + assertEquals( + JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED, status(host, "expired").getPhase()); + assertThrows( + IllegalStateException.class, + () -> host.admit(setup("expired", capability), uuid, "player", () -> {})); + + var deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (handler.finished.isEmpty() && System.nanoTime() < deadline) Thread.sleep(5); + var slow = handler.finished.getFirst(); + assertEquals("slow", slow.id()); + slow.ended(); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("slow")); + slow.ready(); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("slow")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, host.phase("fast")); + } + + @Test + void activeDeliveriesFenceTheirPlayerAndOnlyNewerOnesReplaceThem() { + var fence = new DeliveryFence(); + fence.claim("player", at(1, 1)); + assertThrows(IllegalArgumentException.class, () -> fence.claim("player", at(1, 1))); + assertThrows(IllegalArgumentException.class, () -> fence.claim("player", at(1, 2))); + fence.release("player", at(1, 1)); + assertThrows(IllegalArgumentException.class, () -> fence.claim("player", at(1, 1))); + fence.claim("player", at(1, 2)); + fence.release("player", at(1, 1)); + assertThrows(IllegalArgumentException.class, () -> fence.claim("player", at(1, 3))); + fence.release("player", at(1, 2)); + // A restore's new epoch orders after every revision of the previous one. + fence.claim("player", at(2, 1)); + assertThrows( + IllegalArgumentException.class, + () -> fence.claim("unset", Position.getDefaultInstance())); + } + + private ChunkSessions host(SessionHandler handler, Duration deadline) { + return new ChunkSessions( + handler, + new ChunkSessions.Link() { + @Override + public boolean acceptsWork() { + return true; + } + + @Override + public BackendSession backend(String session) { + return null; + } + + @Override + public CompletionStage move( + String delivery, Position generation, Destination destination) { + return CompletableFuture.completedFuture(MoveResult.ACCEPTED); + } + + @Override + public void methodResult(String operation, JvmMethodResult result) { + results.put(operation, result); + } + + @Override + public void flush() {} + + @Override + public long nanoTime() { + return clock.get(); + } + }, + Runnable::run, + deadline, + null); + } + + private void put(String key, Message value) { + topic.put(key, value.toByteString()); + } + + private static JvmSession session(int capacity, boolean finish) { + return JvmSession.newBuilder() + .setSessionType("app/default") + .setCapacity(capacity) + .setFinish(finish) + .build(); + } + + private JvmDelivery delivery(String session, long revision) { + return JvmDelivery.newBuilder() + .setSession(session) + .setGeneration(at(1, revision)) + .setPlayer( + PlayerIdentity.newBuilder().setUuid(uuid.toString()).setUsername("player")) + .build(); + } + + private static JvmMethodCall call(String delivery) { + return JvmMethodCall.newBuilder() + .setSession("a") + .setMethod("greet") + .setArgumentsJson(ByteString.copyFromUtf8("{}")) + .setDelivery(delivery) + .setDeadlineMs(System.currentTimeMillis() + 30_000) + .build(); + } + + private static byte[] setup(String operation, ByteString capability) { + return PlayerSetup.newBuilder() + .setOperationId(operation) + .setCapability(capability) + .build() + .toByteArray(); + } + + private static JvmDeliveryStatus status(ChunkSessions host, String operation) { + return host.inventory().getDeliveriesList().stream() + .filter(status -> status.getOperationId().equals(operation)) + .findFirst() + .orElseThrow(); + } + + private static Position at(long epoch, long revision) { + return Position.newBuilder().setEpoch(epoch).setRevision(revision).build(); + } + + private static final class Handler implements SessionHandler { + final List created = new CopyOnWriteArrayList<>(); + final List finished = new CopyOnWriteArrayList<>(); + + @Override + public void create(SessionControl session) { + created.add(session); + } + + @Override + public void finish(SessionControl session) { + finished.add(session); + } + + @Override + public CompletionStage method( + SessionControl session, String method, String argumentsJson) { + return CompletableFuture.completedFuture("\"ok\""); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dde6b863..282b8c7e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,4 @@ include(":jvm:backend-client-kotlin") include(":jvm:runtime") include(":jvm:runtime-minestom") include(":jvm:runtime-minestom-kotlin") +include(":jvm:minestom-primitives") From 38b2b619e08cb51c5afd8cd6de5a3eecb4687042 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:57:05 -0400 Subject: [PATCH 02/11] fix(jvm): keep delivery fences on failed cleanup and enforce creation deadlines in sweep Release a delivery only when player cleanup or leave succeeds, enforce the session creation deadline from sweep() instead of the common pool, and publish SessionCreateEvent only when ready() took effect. --- jvm/minestom-primitives/README.md | 3 +- .../chunkzero/chunk/minestom/ChunkLogin.java | 12 ++- .../chunk/runtime/SessionManager.java | 8 +- .../minestom/internal/AdmittedPlayer.java | 21 ++++-- .../chunk/runtime/SessionCreationTest.java | 42 ++++++++++- .../chunkzero/chunk/runtime/TestHosts.java | 6 +- .../chunk/runtime/GameplayLifecycleTest.kt | 32 ++++++++ .../chunk/runtime/ChunkSessions.java | 75 ++++++++++--------- .../chunk/runtime/SessionControl.java | 15 +++- 9 files changed, 154 insertions(+), 60 deletions(-) diff --git a/jvm/minestom-primitives/README.md b/jvm/minestom-primitives/README.md index f9c616a4..0463975f 100644 --- a/jvm/minestom-primitives/README.md +++ b/jvm/minestom-primitives/README.md @@ -37,7 +37,8 @@ try (var chunk = ChunkProcess.connect()) { - Keep Minestom in offline mode: the gateway authenticates players and owns encryption and compression. - The leave handler runs once a player's connection closed, whether they left or core withdrew their delivery, and the - delivery is released when it settles. A session ends only after its players are released. + delivery is released once it completes successfully; a failed leave is logged and leaves the delivery unreleased. A + session ends only after its players are released. - `login.delivery(player)` also gives `move(destination)` and `operationId(action)` for that player. - Isolation is the app's choice: several sessions can share instances, own several each, or the app can run one session per JVM through its machine profile's `max_sessions = 1`. diff --git a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java index 78c99965..3b111eaa 100644 --- a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java +++ b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java @@ -31,7 +31,8 @@ * up to the app. * *

Once a player's connection closes, whether they left or their delivery was withdrawn, it runs - * the app's leave handler and then releases the delivery. + * the app's leave handler, and releases the delivery once that completes successfully. A leave that + * fails is logged and its delivery stays unreleased. */ public final class ChunkLogin implements AutoCloseable { private static final System.Logger LOG = System.getLogger(ChunkLogin.class.getName()); @@ -62,7 +63,8 @@ public static ChunkLogin create(ChunkSessions sessions) { /** * Admits delivered players. Once a player's connection closes, {@code leave} runs, and their - * delivery is released once it settles, so the session can still act for them meanwhile. + * delivery is released once it completes successfully, so the session can still act for them + * meanwhile. */ public static ChunkLogin create( ChunkSessions sessions, BiFunction> leave) { @@ -126,11 +128,13 @@ private void sweep() { } left.whenComplete( (ignored, error) -> { - if (error != null) + if (error != null) { LOG.log( System.Logger.Level.WARNING, - "Player leave did not complete cleanly", + "Player leave failed; the delivery stays unreleased", error); + return; + } admissions.remove(connection, admission); admission.delivery.release(); }); diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java index 2db11f5f..fbd46d29 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java @@ -197,9 +197,11 @@ void start() { if (finishing) return null; if (error == null && !scope.getInstances().isEmpty()) { - control.ready(); - process.eventHandler() - .call(new SessionCreateEvent(scope)); + if (control.ready()) + process.eventHandler() + .call( + new SessionCreateEvent( + scope)); } else { control.fail( error == null diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java index 9adc3d8b..05bc5aa0 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java @@ -13,7 +13,8 @@ /** * A connection admitted under a delivery: it joins the delivery's session once spawned, arrives - * once its client confirmed its position, and is released once it closed and left the session. + * once its client confirmed its position, and is released once it closed and left the session. A + * failed cleanup keeps it unreleased, so the session keeps ending and its player stays fenced. */ final class AdmittedPlayer { private static final System.Logger LOG = System.getLogger(AdmittedPlayer.class.getName()); @@ -38,7 +39,7 @@ void admitted(Delivery admitted) { } boolean isReleased() { - return released.isDone(); + return released.isDone() && !released.isCompletedExceptionally(); } synchronized Instance configure(ManagedPlayer current) { @@ -83,7 +84,7 @@ synchronized void check() { /** * Disconnects the player, awaits their join and spawn, removes them, runs the session's leave, - * and then releases the delivery. + * and then releases the delivery unless that cleanup failed. */ synchronized CompletableFuture close() { if (closed) return released; @@ -131,15 +132,19 @@ synchronized CompletableFuture close() { : joined.leave(current)) .whenComplete( (ignored, error) -> { - if (error != null) - LOG.log( - System.Logger.Level.WARNING, - "Player leave did not complete cleanly", - error); synchronized (this) { player = null; session = null; } + if (error != null) { + LOG.log( + System.Logger.Level.WARNING, + "Player leave did not complete cleanly; the delivery" + + " stays unreleased", + error); + released.completeExceptionally(error); + return; + } delivery.thenAccept(Delivery::release); released.complete(null); }); diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java index f2fe0b56..b7411230 100644 --- a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java @@ -8,6 +8,7 @@ import com.chunkzero.chunk.backend.api.JsonType; import com.chunkzero.chunk.runtime.bootstrap.FlatSession; +import com.chunkzero.chunk.runtime.minestom.event.SessionCreateEvent; import com.google.protobuf.ByteString; import net.minestom.server.ServerProcess; @@ -26,6 +27,7 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -265,11 +267,15 @@ public CompletionStage onCreate( } })), Map.of()); - var host = TestHosts.withDeadline(manager, Duration.ofMillis(50)); + var clock = new AtomicLong(); + var host = TestHosts.withDeadline(manager, Duration.ofMillis(50), clock::get); try { var created = host.create("slow", session(16, "{}")); - var deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (!created.isDone() && System.nanoTime() < deadline) ticks.flush(); + ticks.flush(); + host.sweep(); + assertFalse(created.isDone()); + clock.addAndGet(Duration.ofMillis(50).toNanos()); + host.sweep(); assertTrue(created.isDone()); var failure = assertThrows(CompletionException.class, created::join); assertInstanceOf(TimeoutException.class, failure.getCause()); @@ -285,6 +291,36 @@ public CompletionStage onCreate( } } + @Test + void creationCompletingAfterTheDeadlinePublishesNoCreateEvent() { + var process = ServerProcess.create(); + var ticks = new TickExecutor(); + var manager = + new SessionManager( + process, + ticks, + Map.of("arena/default", new SessionRegistration("arena", FlatSession::new)), + Map.of()); + var clock = new AtomicLong(); + var host = TestHosts.withDeadline(manager, Duration.ofMillis(50), clock::get); + var events = new ArrayList(); + process.eventHandler() + .addListener(SessionCreateEvent.class, event -> events.add(event.getSession())); + try { + var created = host.create("late", session(16, "{}")); + // The creation has completed, but its tick task has not run yet. + ticks.flush(); + clock.addAndGet(Duration.ofMillis(50).toNanos()); + host.sweep(); + flush(ticks); + assertThrows(CompletionException.class, created::join); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("late")); + assertTrue(events.isEmpty()); + } finally { + process.stop(); + } + } + @Test void forgottenSessionsLeaveRoomForNewOnes() { var process = ServerProcess.create(); diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java index c581fdc8..12c63306 100644 --- a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java @@ -19,9 +19,9 @@ static ChunkSessions detached(SessionHandler handler) { return ChunkSessions.detached(handler, session -> null); } - static ChunkSessions withDeadline(SessionHandler handler, Duration deadline) { - return new ChunkSessions( - handler, link(null, System::nanoTime), Runnable::run, deadline, null); + static ChunkSessions withDeadline( + SessionHandler handler, Duration deadline, LongSupplier nanos) { + return new ChunkSessions(handler, link(null, nanos), Runnable::run, deadline, null); } /** Reports to {@code core}, and expires deliveries by {@code nanos}. */ diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt index 27521947..f3549868 100644 --- a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt +++ b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt @@ -36,6 +36,7 @@ class GameplayLifecycleTest { val closedPlayers = ConcurrentHashMap.newKeySet() val joinStarted = CompletableFuture() val joinFinished = CompletableFuture() + val closeFailed = CompletableFuture() val manager = SessionManager( minecraft, @@ -55,6 +56,26 @@ class GameplayLifecycleTest { } } }, + "failing" to + Supplier { + object : Session() { + lateinit var scope: SessionScope + + override fun onCreate(scope: SessionScope) = + FlatSession().onCreate(scope).also { this.scope = scope } + + override fun onJoin(player: Player): CompletableFuture { + scope.own( + player, + AutoCloseable { + closeFailed.complete(Unit) + error("Close failed") + }, + ) + return CompletableFuture.completedFuture(null) + } + } + }, "gated" to Supplier { object : Session() { @@ -201,6 +222,17 @@ class GameplayLifecycleTest { withdraw("replacement") await("replacement", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED) host.finish("c", session("gated")).get(3, TimeUnit.SECONDS) + host.create("d", session("failing")).get(3, TimeUnit.SECONDS) + arrive(connect("broken", delivery("d", 7)), "broken") + withdraw("broken") + closeFailed.get(3, TimeUnit.SECONDS) + Thread.sleep(200) + assertEquals( + JvmDeliveryPhase.JVM_DELIVERY_PHASE_WITHDRAWING, + phase("broken"), + "A failed player cleanup keeps the delivery unreleased", + ) + assertThrows(IllegalStateException::class.java) { connect("blocked", delivery("d", 8)) } host.finish("b", session()).get(3, TimeUnit.SECONDS) } finally { sockets.forEach { it.close() } diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java index 877a3621..1bb03654 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java @@ -347,10 +347,26 @@ void apply(Map entries) { after.forEach(Runnable::run); } - /** Expires deliveries never connected, refuses those whose session won't come, and reports. */ + /** + * Fails sessions not ready within the creation deadline, expires deliveries never connected, + * refuses those whose session won't come, and reports. + */ void sweep() { var after = new ArrayList(); synchronized (this) { + var now = link.nanoTime(); + for (var control : List.copyOf(sessions.values())) { + if (!control.settled && now - control.startedAt >= createDeadline.toNanos()) + fail( + control, + new TimeoutException( + "Session " + + control.id() + + " was not ready within " + + createDeadline), + true, + after); + } deliveries.values().forEach(delivery -> expire(delivery, after)); settle(after); } @@ -423,25 +439,12 @@ private CompletableFuture create( if (activeCount() >= MAX_SESSIONS) return CompletableFuture.failedFuture( new IllegalStateException("Too many live sessions")); - var control = new SessionControl(this, id, session, configuration, link.backend(id)); + var control = + new SessionControl( + this, id, session, configuration, link.backend(id), link.nanoTime()); outcomes.remove(id); sessions.put(id, control); - after.add( - () -> { - CompletableFuture.delayedExecutor( - createDeadline.toMillis(), TimeUnit.MILLISECONDS) - .execute( - () -> - fail( - control, - new TimeoutException( - "Session " - + id - + " was not ready within " - + createDeadline), - true)); - dispatch(control, () -> handler.create(control)); - }); + after.add(() -> dispatch(control, () -> handler.create(control))); return control.created; } @@ -477,12 +480,14 @@ private JvmSessionStatus outcome(String id, JvmSession session, JvmSessionPhase return status; } - void ready(SessionControl control) { + boolean ready(SessionControl control) { var after = new ArrayList(); + boolean ready; synchronized (this) { - if (control.settled) return; + if (control.settled) return false; control.settled = true; - if (!control.finishing) control.phase = JvmSessionPhase.JVM_SESSION_PHASE_READY; + ready = !control.finishing; + if (ready) control.phase = JvmSessionPhase.JVM_SESSION_PHASE_READY; var status = control.status(0, 0); after.add(() -> control.created.complete(status)); // A finish requested while it was starting waited for creation to settle. @@ -490,29 +495,31 @@ void ready(SessionControl control) { } after.forEach(Runnable::run); link.flush(); + return ready; } void fail(SessionControl control, Throwable error) { - fail(control, error, false); - } - - private void fail(SessionControl control, Throwable error, boolean onlyStarting) { - Objects.requireNonNull(error); var after = new ArrayList(); synchronized (this) { - if (terminal(control.phase) || (onlyStarting && control.settled)) return; - if (control.failure == null) control.failure = error; - if (!control.settled) { - control.settled = true; - after.add(() -> control.created.completeExceptionally(error)); - if (control.finishing) withdraw(control, after); - } - finish(control, after); + fail(control, error, false, after); } after.forEach(Runnable::run); link.flush(); } + private void fail( + SessionControl control, Throwable error, boolean onlyStarting, List after) { + Objects.requireNonNull(error); + if (terminal(control.phase) || (onlyStarting && control.settled)) return; + if (control.failure == null) control.failure = error; + if (!control.settled) { + control.settled = true; + after.add(() -> control.created.completeExceptionally(error)); + if (control.finishing) withdraw(control, after); + } + finish(control, after); + } + CompletionStage finish(SessionControl control) { var after = new ArrayList(); synchronized (this) { diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java index 152b2c18..36eaafea 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java @@ -23,6 +23,7 @@ public final class SessionControl { private final JvmSession spec; private final JsonNode configuration; private final @Nullable BackendSession backend; + final long startedAt; // Guarded by owner. JvmSessionPhase phase = JvmSessionPhase.JVM_SESSION_PHASE_STARTING; @@ -39,12 +40,14 @@ public final class SessionControl { String id, JvmSession spec, JsonNode configuration, - @Nullable BackendSession backend) { + @Nullable BackendSession backend, + long startedAt) { this.owner = owner; this.id = id; this.spec = spec; this.configuration = configuration; this.backend = backend; + this.startedAt = startedAt; } /** The session's ID, unique within the deployment. */ @@ -72,9 +75,13 @@ public String configurationJson() { return backend; } - /** The session admits players. Ignored unless it is still starting. */ - public void ready() { - owner.ready(this); + /** + * The session admits players. Ignored unless it is still starting. + * + * @return whether this call made the session ready + */ + public boolean ready() { + return owner.ready(this); } /** The session can't continue: it ends, and is reported failed once its handler finished it. */ From b89479069df5931e6bb70456f7eac4ee66579ac5 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:03:19 -0400 Subject: [PATCH 03/11] fix(jvm): authorize session methods when they run --- jvm/minestom-primitives/README.md | 6 +- .../chunkzero/chunk/minestom/ChunkLogin.java | 3 +- .../chunk/runtime/SessionManager.java | 8 +-- .../chunk/runtime/SessionMethodTest.java | 10 ++- jvm/runtime/README.md | 3 +- .../chunk/runtime/ChunkSessions.java | 39 +++++++++-- .../chunk/runtime/SessionHandler.java | 10 +-- .../chunk/runtime/SessionMethod.java | 35 ++++++++++ .../chunk/runtime/ChunkSessionsTest.java | 67 +++++++++++++++++-- 9 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java diff --git a/jvm/minestom-primitives/README.md b/jvm/minestom-primitives/README.md index 0463975f..0cd7d6b5 100644 --- a/jvm/minestom-primitives/README.md +++ b/jvm/minestom-primitives/README.md @@ -36,9 +36,9 @@ try (var chunk = ChunkProcess.connect()) { ``` - Keep Minestom in offline mode: the gateway authenticates players and owns encryption and compression. -- The leave handler runs once a player's connection closed, whether they left or core withdrew their delivery, and the - delivery is released once it completes successfully; a failed leave is logged and leaves the delivery unreleased. A - session ends only after its players are released. +- The leave handler runs once a player's connection closed and Minestom removed them, whether they left or core withdrew + their delivery, and the delivery is released once it completes successfully; a failed leave is logged and leaves the + delivery unreleased. A session ends only after its players are released. - `login.delivery(player)` also gives `move(destination)` and `operationId(action)` for that player. - Isolation is the app's choice: several sessions can share instances, own several each, or the app can run one session per JVM through its machine profile's `max_sessions = 1`. diff --git a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java index 3b111eaa..9487f4b8 100644 --- a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java +++ b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java @@ -115,8 +115,9 @@ private void sweep() { admissions.forEach( (connection, admission) -> { if (connection.isOnline() || admission.leaving) return; - admission.leaving = true; var player = connection.getPlayer(); + if (player != null && !player.isRemoved()) return; + admission.leaving = true; CompletionStage left; try { left = diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java index fbd46d29..2d7b1710 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java @@ -125,19 +125,19 @@ public void finish(SessionControl control) { } @Override - public CompletionStage method( - SessionControl control, String method, String argumentsJson) { - var binding = methods.get(control.type() + "/" + method); + public CompletionStage method(SessionControl control, SessionMethod call) { + var binding = methods.get(control.type() + "/" + call.name()); if (binding == null) return CompletableFuture.failedFuture( new IllegalArgumentException("Undeclared session method")); return ticks.submit( () -> { + if (!call.start()) return null; var managed = sessions.get(control.id()); if (managed == null || control.phase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) throw new IllegalStateException("Session unavailable"); - return binding.invoke(managed.behavior, argumentsJson); + return binding.invoke(managed.behavior, call.argumentsJson()); }); } diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java index f98e3c84..bfe0b4a6 100644 --- a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java @@ -57,16 +57,16 @@ public void finish(SessionControl session) { try { settle(ticks, host.create("session", spec)); var control = controls.getFirst(); - var result = manager.method(control, "record", "{\"message\":\"first\"}"); + var result = manager.method(control, call("record", "{\"message\":\"first\"}")); settle(ticks, result.toCompletableFuture()); assertEquals("1", result.toCompletableFuture().join()); assertEquals(List.of(Thread.currentThread()), game.threads); assertTrue( - manager.method(control, "missing", "{}") + manager.method(control, call("missing", "{}")) .toCompletableFuture() .isCompletedExceptionally()); settle(ticks, host.finish("session", spec)); - var late = manager.method(control, "record", "{\"message\":\"late\"}"); + var late = manager.method(control, call("record", "{\"message\":\"late\"}")); ticks.flush(); assertTrue(late.toCompletableFuture().isCompletedExceptionally()); assertEquals(1, game.threads.size()); @@ -75,6 +75,10 @@ public void finish(SessionControl session) { } } + private static SessionMethod call(String name, String argumentsJson) { + return new SessionMethod(name, argumentsJson, () -> true); + } + private static void settle(TickExecutor ticks, CompletableFuture future) { for (var index = 0; index < 10 && !future.isDone(); index++) ticks.flush(); assertTrue(future.isDone()); diff --git a/jvm/runtime/README.md b/jvm/runtime/README.md index 07fabbcb..d99937b4 100644 --- a/jvm/runtime/README.md +++ b/jvm/runtime/README.md @@ -96,7 +96,8 @@ try (var chunk = ChunkProcess.connect()) { `disconnect` runs; once the player's connection closed and any leave handling settled, call `release()`. - Call `chunk.tick()` once per engine tick, so core can tell a stalled JVM. - `Delivery.move(destination)` and `operationId(action)` work as `SessionScope`'s do. Session methods reach - `SessionHandler.method`, which no session declares by default. + `SessionHandler.method`, which no session declares by default; a handler calls `SessionMethod.start()` right before + running the method's effects and skips them if it returns false. [`minestom-primitives`](../minestom-primitives/README.md) handles the login side for Minestom. diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java index 1bb03654..3cf4ae63 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java @@ -444,7 +444,16 @@ private CompletableFuture create( this, id, session, configuration, link.backend(id), link.nanoTime()); outcomes.remove(id); sessions.put(id, control); - after.add(() -> dispatch(control, () -> handler.create(control))); + after.add( + () -> + dispatch( + control, + () -> { + synchronized (this) { + if (control.handlerFinishing) return; + } + handler.create(control); + })); return control.created; } @@ -720,13 +729,16 @@ private void execute(MethodCall operation) { cancel(operation); return; } - operation.started = true; } CompletionStage result; try { result = handler.method( - session, call.getMethod(), call.getArgumentsJson().toStringUtf8()); + session, + new SessionMethod( + call.getMethod(), + call.getArgumentsJson().toStringUtf8(), + () -> start(operation))); } catch (RuntimeException error) { result = CompletableFuture.failedFuture(error); } @@ -747,11 +759,30 @@ private void execute(MethodCall operation) { .build(); } synchronized (this) { - complete(operation, outcome); + if (operation.started) complete(operation, outcome); + else cancel(operation); } }); } + /** Marks {@code operation} started if it may still run, and cancels it otherwise. */ + private synchronized boolean start(MethodCall operation) { + if (operation.done || methods.get(operation.id) != operation) return false; + if (operation.started) return true; + var call = operation.call; + var session = sessions.get(call.getSession()); + if (closed + || session == null + || session.phase != JvmSessionPhase.JVM_SESSION_PHASE_READY + || link.currentTimeMillis() >= call.getDeadlineMs() + || !arrivedIn(call.getDelivery(), call.getSession())) { + cancel(operation); + return false; + } + operation.started = true; + return true; + } + private boolean arrivedIn(String operation, String session) { var delivery = deliveries.get(operation); return delivery != null diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java index a633532b..c60f894e 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java @@ -25,12 +25,12 @@ public interface SessionHandler { /** * Runs session method {@code method} of a ready session for one of its arrived players, and - * completes with its result as JSON, in at most 48 KiB. By default, sessions declare no - * methods. + * completes with its result as JSON, in at most 48 KiB. Call {@link SessionMethod#start()} + * right before running the method's effects, and do not run them if it returns false. A call + * that completes without having started is cancelled. By default, sessions declare no methods. */ - default CompletionStage method( - SessionControl session, String method, String argumentsJson) { + default CompletionStage method(SessionControl session, SessionMethod call) { return CompletableFuture.failedFuture( - new IllegalArgumentException("Undeclared session method: " + method)); + new IllegalArgumentException("Undeclared session method: " + call.name())); } } diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java new file mode 100644 index 00000000..03a6d7f0 --- /dev/null +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java @@ -0,0 +1,35 @@ +package com.chunkzero.chunk.runtime; + +import java.util.function.BooleanSupplier; + +/** One call of a session method, handed to {@link SessionHandler#method}. */ +public final class SessionMethod { + private final String name; + private final String argumentsJson; + private final BooleanSupplier start; + + SessionMethod(String name, String argumentsJson, BooleanSupplier start) { + this.name = name; + this.argumentsJson = argumentsJson; + this.start = start; + } + + /** The method's name. */ + public String name() { + return name; + } + + /** The method's arguments as a JSON document. */ + public String argumentsJson() { + return argumentsJson; + } + + /** + * Authorizes the call right before its effects run. Returns true if the call may run: it is not + * cancelled or past its deadline, and its session and delivery are still open. Otherwise the + * call is cancelled and its effects must not run. + */ + public boolean start() { + return start.getAsBoolean(); + } +} diff --git a/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java index 52cb4794..e890c220 100644 --- a/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java +++ b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import java.time.Duration; +import java.util.ArrayDeque; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -30,6 +31,7 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -165,7 +167,57 @@ void activeDeliveriesFenceTheirPlayerAndOnlyNewerOnesReplaceThem() { () -> fence.claim("unset", Position.getDefaultInstance())); } + @Test + void methodsCancelledBeforeTheyStartDoNotRun() { + var handler = new Handler(); + var host = host(handler, Duration.ofSeconds(10)); + put("session/a", session(1, false)); + host.apply(topic); + handler.created.getFirst().ready(); + put("delivery/first", delivery("a", 1)); + host.apply(topic); + host.admit(setup("first", status(host, "first").getCapability()), uuid, "player", () -> {}) + .arrived(); + + handler.gate = new CompletableFuture<>(); + put("method/slow", call("first")); + host.apply(topic); + put("method/slow", call("first").toBuilder().setCancel(true).build()); + host.apply(topic); + handler.gate.complete(null); + assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, results.get("slow").getPhase()); + assertTrue(handler.effects.isEmpty()); + + handler.gate = new CompletableFuture<>(); + put("method/late", call("first")); + host.apply(topic); + clock.addAndGet(TimeUnit.SECONDS.toNanos(61)); + put("delivery/first", delivery("a", 1).toBuilder().setWithdraw(true).build()); + host.apply(topic); + handler.gate.complete(null); + assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, results.get("late").getPhase()); + assertTrue(handler.effects.isEmpty()); + } + + @Test + void creationDispatchedAfterTeardownDoesNotRun() { + var handler = new Handler(); + var queue = new ArrayDeque(); + var host = host(handler, Duration.ofMillis(50), queue::add); + put("session/a", session(1, false)); + host.apply(topic); + clock.addAndGet(TimeUnit.SECONDS.toNanos(1)); + host.sweep(); + while (!queue.isEmpty()) queue.poll().run(); + assertTrue(handler.created.isEmpty()); + assertEquals(1, handler.finished.size()); + } + private ChunkSessions host(SessionHandler handler, Duration deadline) { + return host(handler, deadline, Runnable::run); + } + + private ChunkSessions host(SessionHandler handler, Duration deadline, Executor callbacks) { return new ChunkSessions( handler, new ChunkSessions.Link() { @@ -198,7 +250,7 @@ public long nanoTime() { return clock.get(); } }, - Runnable::run, + callbacks, deadline, null); } @@ -256,6 +308,8 @@ private static Position at(long epoch, long revision) { private static final class Handler implements SessionHandler { final List created = new CopyOnWriteArrayList<>(); final List finished = new CopyOnWriteArrayList<>(); + final List effects = new CopyOnWriteArrayList<>(); + volatile CompletableFuture gate; @Override public void create(SessionControl session) { @@ -268,9 +322,14 @@ public void finish(SessionControl session) { } @Override - public CompletionStage method( - SessionControl session, String method, String argumentsJson) { - return CompletableFuture.completedFuture("\"ok\""); + public CompletionStage method(SessionControl session, SessionMethod call) { + var run = gate == null ? CompletableFuture.completedFuture(null) : gate; + return run.thenApply( + ignored -> { + if (!call.start()) return null; + effects.add(call.name()); + return "\"ok\""; + }); } } } From c3ae103c282b7cc6eba289ea6b92dccb81dbabc8 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:04:01 -0400 Subject: [PATCH 04/11] fix(jvm): report session methods that never started as failed --- .../main/java/com/chunkzero/chunk/runtime/ChunkSessions.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java index 3cf4ae63..c3039f49 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java @@ -759,8 +759,7 @@ private void execute(MethodCall operation) { .build(); } synchronized (this) { - if (operation.started) complete(operation, outcome); - else cancel(operation); + complete(operation, operation.started ? outcome : failed); } }); } From d3dd6988ca2f8febde07fcf0b99ac33d0403c73e Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:09:16 -0400 Subject: [PATCH 05/11] fix(minestom-primitives): release deliveries of players who left during configuration --- .../chunkzero/chunk/minestom/ChunkLogin.java | 7 +- .../chunk/runtime/ChunkLoginTest.java | 82 +++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java index 9487f4b8..d614b180 100644 --- a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java +++ b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java @@ -10,6 +10,7 @@ import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import net.minestom.server.event.player.AsyncPlayerPreLoginEvent; +import net.minestom.server.network.ConnectionState; import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.timer.Task; @@ -116,7 +117,11 @@ private void sweep() { (connection, admission) -> { if (connection.isOnline() || admission.leaving) return; var player = connection.getPlayer(); - if (player != null && !player.isRemoved()) return; + // Minestom removes a PLAY player's entity on a later tick; a player that left + // during configuration is never removed. + if (player != null + && connection.getServerState() == ConnectionState.PLAY + && !player.isRemoved()) return; admission.leaving = true; CompletionStage left; try { diff --git a/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java b/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java index 5fc663c6..d59c445e 100644 --- a/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java +++ b/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java @@ -14,10 +14,12 @@ import com.google.protobuf.ByteString; import net.minestom.server.MinecraftServer; +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.network.ConnectionState; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.PacketVanilla; import net.minestom.server.network.packet.client.handshake.ClientHandshakePacket; +import net.minestom.server.network.packet.client.login.ClientLoginAcknowledgedPacket; import net.minestom.server.network.packet.client.login.ClientLoginPluginResponsePacket; import net.minestom.server.network.packet.client.login.ClientLoginStartPacket; import net.minestom.server.network.packet.server.ServerPacket; @@ -34,6 +36,7 @@ import java.util.Map; import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; class ChunkLoginTest { @@ -45,20 +48,7 @@ void upstreamMinestomAdmitsOnlyDeliveredPlayersAndReleasesThemOnceDisconnected() throws Exception { MinecraftServer.setCompressionThreshold(0); var server = MinecraftServer.init(); - var host = - ChunkSessions.detached( - new SessionHandler() { - @Override - public void create(SessionControl session) { - session.ready(); - } - - @Override - public void finish(SessionControl session) { - session.ended(); - } - }, - session -> null); + var host = host(); var sockets = new ArrayList(); try (var login = ChunkLogin.create(host)) { MinecraftServer.getGlobalEventHandler().addChild(login.events()); @@ -108,6 +98,70 @@ public void finish(SessionControl session) { } } + @Test + void releasesPlayersWhoDisconnectDuringConfiguration() throws Exception { + MinecraftServer.setCompressionThreshold(0); + var server = MinecraftServer.init(); + var host = host(); + try (var login = ChunkLogin.create(host)) { + MinecraftServer.getGlobalEventHandler().addChild(login.events()); + server.start(new InetSocketAddress("127.0.0.1", 0)); + var port = MinecraftServer.getServer().getPort(); + topic.put( + "session/game", + JvmSession.newBuilder() + .setSessionType("app/default") + .setCapacity(4) + .build() + .toByteString()); + var setup = prepare(host, "first", 1); + var configuring = new CountDownLatch(1); + var proceed = new CountDownLatch(1); + // Holds the player in configuration until the test is done with them. + MinecraftServer.getGlobalEventHandler() + .addListener( + AsyncPlayerConfigurationEvent.class, + event -> { + configuring.countDown(); + try { + proceed.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + try (var socket = login(port, "player", setup)) { + assertInstanceOf(LoginSuccessPacket.class, packet(socket)); + send( + socket, + 3, + ClientLoginAcknowledgedPacket.SERIALIZER, + new ClientLoginAcknowledgedPacket()); + assertTrue(configuring.await(10, TimeUnit.SECONDS)); + } + await(host, "first", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED); + proceed.countDown(); + } finally { + host.close(); + MinecraftServer.stopCleanly(); + } + } + + private static ChunkSessions host() { + return ChunkSessions.detached( + new SessionHandler() { + @Override + public void create(SessionControl session) { + session.ready(); + } + + @Override + public void finish(SessionControl session) { + session.ended(); + } + }, + session -> null); + } + private PlayerSetup prepare(ChunkSessions host, String operation, long revision) { topic.put("delivery/" + operation, delivery(revision).toByteString()); host.apply(topic); From 951dc80984b03044fbe8aa7a7f4381a066217a06 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:14:50 -0400 Subject: [PATCH 06/11] fix(jvm): stop session methods once a delivered player leaves --- .../chunkzero/chunk/minestom/ChunkLogin.java | 1 + .../minestom/internal/AdmittedPlayer.java | 1 + .../chunk/runtime/ChunkSessions.java | 7 +++++ .../com/chunkzero/chunk/runtime/Delivery.java | 11 ++++++- .../chunk/runtime/ChunkSessionsTest.java | 30 +++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java index d614b180..6ac0a6c7 100644 --- a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java +++ b/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java @@ -116,6 +116,7 @@ private void sweep() { admissions.forEach( (connection, admission) -> { if (connection.isOnline() || admission.leaving) return; + admission.delivery.left(); var player = connection.getPlayer(); // Minestom removes a PLAY player's entity on a later tick; a player that left // during configuration is never removed. diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java index 05bc5aa0..112c8694 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java @@ -89,6 +89,7 @@ synchronized void check() { synchronized CompletableFuture close() { if (closed) return released; closed = true; + delivery.thenAccept(Delivery::left); connection.disconnect(); var ticks = manager.getTicks(); var current = player; diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java index c3039f49..14d8e11f 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java @@ -684,6 +684,12 @@ void arrived(Delivery delivery) { link.flush(); } + void left(Delivery delivery) { + synchronized (this) { + delivery.left = true; + } + } + void release(Delivery delivery) { synchronized (this) { if (delivery.isReleased) return; @@ -787,6 +793,7 @@ private boolean arrivedIn(String operation, String session) { return delivery != null && delivery.arrived && !delivery.closed + && !delivery.left && delivery.session().equals(session); } diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java index c60c794e..ac681e34 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/Delivery.java @@ -37,6 +37,7 @@ public final class Delivery { boolean consumed; boolean arrived; boolean closed; + boolean left; boolean isReleased; @Nullable Runnable disconnect; @@ -88,10 +89,18 @@ public void arrived() { public boolean isArrived() { synchronized (owner) { - return arrived && !closed; + return arrived && !closed && !left; } } + /** + * The player's connection closed: no further session methods run for them. The delivery stays + * fenced until {@link #release()}. Calling it again has no effect. + */ + public void left() { + owner.left(this); + } + /** * The player is gone and the session no longer acts for them. Call it once their connection * closed and any leave handling settled, whether they left or the delivery was withdrawn. diff --git a/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java index e890c220..e821b5b0 100644 --- a/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java +++ b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java @@ -167,6 +167,36 @@ void activeDeliveriesFenceTheirPlayerAndOnlyNewerOnesReplaceThem() { () -> fence.claim("unset", Position.getDefaultInstance())); } + @Test + void methodsQueuedBeforeAPlayerLeavesDoNotStartAndTheDeliveryStaysFenced() { + var handler = new Handler(); + var host = host(handler, Duration.ofSeconds(10)); + put("session/a", session(1, false)); + host.apply(topic); + handler.created.getFirst().ready(); + put("delivery/first", delivery("a", 1)); + host.apply(topic); + var delivery = + host.admit( + setup("first", status(host, "first").getCapability()), + uuid, + "player", + () -> {}); + delivery.arrived(); + + handler.gate = new CompletableFuture<>(); + put("method/queued", call("first")); + host.apply(topic); + delivery.left(); + delivery.left(); + handler.gate.complete(null); + assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, results.get("queued").getPhase()); + assertTrue(handler.effects.isEmpty()); + assertEquals(JvmDeliveryPhase.JVM_DELIVERY_PHASE_ARRIVED, status(host, "first").getPhase()); + delivery.release(); + assertEquals(JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED, status(host, "first").getPhase()); + } + @Test void methodsCancelledBeforeTheyStartDoNotRun() { var handler = new Handler(); From 3e68ee74c87db5f51bfdb780401dc6f85818cb7f Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:19:15 -0400 Subject: [PATCH 07/11] fix(jvm): cancel session methods their handler never started --- .../chunk/runtime/ChunkSessions.java | 12 ++++++- .../chunk/runtime/SessionHandler.java | 3 +- .../chunk/runtime/SessionMethod.java | 3 +- .../chunk/runtime/ChunkSessionsTest.java | 31 ++++++++++++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java index 14d8e11f..a62f3569 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java @@ -765,7 +765,17 @@ private void execute(MethodCall operation) { .build(); } synchronized (this) { - complete(operation, operation.started ? outcome : failed); + if (operation.started) complete(operation, outcome); + else + complete( + operation, + error == null + ? JvmMethodResult.newBuilder() + .setPhase( + JvmMethodPhase + .JVM_METHOD_PHASE_CANCELLED) + .build() + : failed); } }); } diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java index c60f894e..be276740 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionHandler.java @@ -27,7 +27,8 @@ public interface SessionHandler { * Runs session method {@code method} of a ready session for one of its arrived players, and * completes with its result as JSON, in at most 48 KiB. Call {@link SessionMethod#start()} * right before running the method's effects, and do not run them if it returns false. A call - * that completes without having started is cancelled. By default, sessions declare no methods. + * that never started is cancelled if the stage completes normally, and fails if it completes + * exceptionally. By default, sessions declare no methods. */ default CompletionStage method(SessionControl session, SessionMethod call) { return CompletableFuture.failedFuture( diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java index 03a6d7f0..f17417b8 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java @@ -27,7 +27,8 @@ public String argumentsJson() { /** * Authorizes the call right before its effects run. Returns true if the call may run: it is not * cancelled or past its deadline, and its session and delivery are still open. Otherwise the - * call is cancelled and its effects must not run. + * call is cancelled and its effects must not run. A call whose handler completes normally + * without ever starting it is cancelled; one whose handler fails without starting it fails. */ public boolean start() { return start.getAsBoolean(); diff --git a/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java index e821b5b0..f2f86ef6 100644 --- a/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java +++ b/jvm/runtime/src/test/java/com/chunkzero/chunk/runtime/ChunkSessionsTest.java @@ -243,6 +243,35 @@ void creationDispatchedAfterTeardownDoesNotRun() { assertEquals(1, handler.finished.size()); } + @Test + void methodsTheirHandlerNeverStartedAreCancelledOrFailed() { + var handler = + new Handler() { + @Override + public CompletionStage method( + SessionControl session, SessionMethod call) { + return call.name().equals("skip") + ? CompletableFuture.completedFuture(null) + : CompletableFuture.failedFuture( + new IllegalStateException("undeclared")); + } + }; + var host = host(handler, Duration.ofSeconds(10)); + put("session/a", session(1, false)); + host.apply(topic); + handler.created.getFirst().ready(); + put("delivery/first", delivery("a", 1)); + host.apply(topic); + host.admit(setup("first", status(host, "first").getCapability()), uuid, "player", () -> {}) + .arrived(); + + put("method/skip", call("first").toBuilder().setMethod("skip").build()); + put("method/undeclared", call("first").toBuilder().setMethod("undeclared").build()); + host.apply(topic); + assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, results.get("skip").getPhase()); + assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_FAILED, results.get("undeclared").getPhase()); + } + private ChunkSessions host(SessionHandler handler, Duration deadline) { return host(handler, deadline, Runnable::run); } @@ -335,7 +364,7 @@ private static Position at(long epoch, long revision) { return Position.newBuilder().setEpoch(epoch).setRevision(revision).build(); } - private static final class Handler implements SessionHandler { + private static class Handler implements SessionHandler { final List created = new CopyOnWriteArrayList<>(); final List finished = new CopyOnWriteArrayList<>(); final List effects = new CopyOnWriteArrayList<>(); From 1737920316f5cdab347dc8aa3af3995e8f407e8c Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:25:50 -0400 Subject: [PATCH 08/11] fix(jvm): check a method's player is still connected before it runs --- .../chunk/runtime/ChunkMinestom.java | 2 +- .../chunk/runtime/SessionManager.java | 12 +- .../chunk/runtime/SessionMethodTest.java | 2 +- .../runtime/SessionMethodDisconnectTest.kt | 143 ++++++++++++++++++ .../chunk/runtime/ChunkSessions.java | 3 + .../chunk/runtime/SessionMethod.java | 13 +- 6 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java index 7f632092..97f8a0dc 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java @@ -71,8 +71,8 @@ public synchronized void start() { server.schedulerManager() .buildTask( () -> { - ticks.flush(); gameplay.flush(); + ticks.flush(); chunk.tick(); }) .repeat(TaskSchedule.tick(1)) diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java index 2d7b1710..edcd6e39 100644 --- a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java +++ b/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java @@ -132,8 +132,10 @@ public CompletionStage method(SessionControl control, SessionMethod call new IllegalArgumentException("Undeclared session method")); return ticks.submit( () -> { - if (!call.start()) return null; var managed = sessions.get(control.id()); + if (managed != null && managed.isDisconnected(call.delivery())) + call.delivery().left(); + if (!call.start()) return null; if (managed == null || control.phase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) throw new IllegalStateException("Session unavailable"); @@ -187,6 +189,14 @@ public SessionScope getScope() { return scope; } + boolean isDisconnected(Delivery delivery) { + return scope.getPlayers().stream() + .anyMatch( + player -> + ((ManagedPlayer) player).getDelivery() == delivery + && !player.getPlayerConnection().isOnline()); + } + void start() { invoke(() -> behavior.onCreate(scope)) .whenComplete( diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java index bfe0b4a6..5a09d872 100644 --- a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java +++ b/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java @@ -76,7 +76,7 @@ public void finish(SessionControl session) { } private static SessionMethod call(String name, String argumentsJson) { - return new SessionMethod(name, argumentsJson, () -> true); + return new SessionMethod(name, argumentsJson, null, () -> true); } private static void settle(TickExecutor ticks, CompletableFuture future) { diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt new file mode 100644 index 00000000..d19328d4 --- /dev/null +++ b/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt @@ -0,0 +1,143 @@ +package com.chunkzero.chunk.runtime + +import chunk.sync.v1.CoreOuterClass.Position +import chunk.sync.v1.Gateway.PlayerIdentity +import chunk.sync.v1.Jvm.JvmDelivery +import chunk.sync.v1.Jvm.JvmDeliveryPhase +import chunk.sync.v1.Jvm.JvmMethodCall +import chunk.sync.v1.Jvm.JvmMethodPhase +import chunk.sync.v1.Jvm.JvmSession +import chunk.sync.v1.Jvm.JvmSessionPhase +import chunk.sync.v1.Jvm.PlayerSetup +import com.chunkzero.chunk.backend.api.BackendValues +import com.chunkzero.chunk.backend.api.JsonType +import com.chunkzero.chunk.backend.api.SessionMethodRef +import com.chunkzero.chunk.runtime.bootstrap.FlatSession +import com.chunkzero.chunk.runtime.minestom.internal.GameplayService +import com.google.protobuf.ByteString +import net.minestom.server.ServerProcess +import net.minestom.server.network.ConnectionState +import net.minestom.server.network.packet.server.login.LoginSuccessPacket +import net.minestom.server.timer.TaskSchedule +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import tools.jackson.core.type.TypeReference +import java.net.InetSocketAddress +import java.util.UUID +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Supplier + +class SessionMethodDisconnectTest { + @Test + fun `a method queued for a player who disconnected before its tick is cancelled`() { + val minecraft = ServerProcess.create() + minecraft.setCompressionThreshold(0) + minecraft.connectionManager().setPlayerProvider(::ManagedPlayer) + val ticks = TickExecutor() + val runs = AtomicInteger() + val number = JsonType.of(object : TypeReference() {}, BackendValues::checkInteger) + val binding = + SessionMethodBinding( + SessionMethodRef("lobby", "default", "record", number, number), + ) { _, input -> runs.incrementAndGet().toLong() + input } + val manager = + SessionManager( + minecraft, + ticks, + mapOf("lobby/default" to SessionRegistration("lobby", Supplier { FlatSession() })), + mapOf("lobby/default/record" to binding), + ) + val core = FakeCore() + val host = TestHosts.linked(manager, core, System::nanoTime) + val gameplay = GameplayService(manager, host) + + // Pausing gameplay.flush keeps the disconnect unnoticed, as it is until a tick's flush. + val paused = AtomicBoolean() + minecraft + .schedulerManager() + .buildTask { + ticks.flush() + if (!paused.get()) gameplay.flush() + host.sweep() + }.repeat(TaskSchedule.tick(1)) + .schedule() + minecraft.start(InetSocketAddress("127.0.0.1", 0)) + val socket = + try { + core.connect(host.state()) + core.put( + "session/lobby", + JvmSession + .newBuilder() + .setSessionType("lobby/default") + .setCapacity(2) + .build(), + ) + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10) + while (host.phase("lobby") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { + check(System.nanoTime() < deadline) { "The session never became ready" } + Thread.sleep(10) + } + val uuid = UUID.randomUUID() + core.put( + "delivery/first", + JvmDelivery + .newBuilder() + .setSession("lobby") + .setGeneration(Position.newBuilder().setEpoch(1).setRevision(1)) + .setPlayer(PlayerIdentity.newBuilder().setUuid(uuid.toString()).setUsername("player")) + .build(), + ) + val prepared = core.await("first", JvmDeliveryPhase.JVM_DELIVERY_PHASE_PREPARED) + val setup = + PlayerSetup + .newBuilder() + .setOperationId("first") + .setCapability(prepared.capability) + .build() + login(minecraft.server().port, "player", uuid, setup).also { + check(it.packet(ConnectionState.LOGIN) is LoginSuccessPacket) + it.configure() + it.confirmTeleports() + core.await("first", JvmDeliveryPhase.JVM_DELIVERY_PHASE_ARRIVED) + } + } catch (error: Throwable) { + core.close() + gameplay.close() + minecraft.stop() + throw error + } + try { + val player = minecraft.connectionManager().onlinePlayers.single() + paused.set(true) + socket.close() + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) + while (player.playerConnection.isOnline) { + check(System.nanoTime() < deadline) { "The disconnect was never noticed" } + Thread.sleep(10) + } + core.put( + "method/call", + JvmMethodCall + .newBuilder() + .setSession("lobby") + .setMethod("record") + .setArgumentsJson(ByteString.copyFromUtf8("1")) + .setDelivery("first") + .setDeadlineMs(System.currentTimeMillis() + 30_000) + .build(), + ) + assertEquals(JvmMethodPhase.JVM_METHOD_PHASE_CANCELLED, core.result("call") {}.phase) + assertEquals(0, runs.get()) + assertTrue(!host.delivery("first")!!.isArrived) + } finally { + socket.close() + core.close() + gameplay.close() + minecraft.stop() + } + } +} diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java index a62f3569..c4b3ceb5 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkSessions.java @@ -724,6 +724,7 @@ private void applyMethods(Map calls, List after private void execute(MethodCall operation) { var call = operation.call; SessionControl session; + Delivery delivery; synchronized (this) { if (operation.done || methods.get(operation.id) != operation) return; session = sessions.get(call.getSession()); @@ -735,6 +736,7 @@ private void execute(MethodCall operation) { cancel(operation); return; } + delivery = deliveries.get(call.getDelivery()); } CompletionStage result; try { @@ -744,6 +746,7 @@ private void execute(MethodCall operation) { new SessionMethod( call.getMethod(), call.getArgumentsJson().toStringUtf8(), + delivery, () -> start(operation))); } catch (RuntimeException error) { result = CompletableFuture.failedFuture(error); diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java index f17417b8..352bdc80 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionMethod.java @@ -6,11 +6,13 @@ public final class SessionMethod { private final String name; private final String argumentsJson; + private final Delivery delivery; private final BooleanSupplier start; - SessionMethod(String name, String argumentsJson, BooleanSupplier start) { + SessionMethod(String name, String argumentsJson, Delivery delivery, BooleanSupplier start) { this.name = name; this.argumentsJson = argumentsJson; + this.delivery = delivery; this.start = start; } @@ -24,11 +26,18 @@ public String argumentsJson() { return argumentsJson; } + /** The delivery of the player the call acts for. */ + public Delivery delivery() { + return delivery; + } + /** * Authorizes the call right before its effects run. Returns true if the call may run: it is not * cancelled or past its deadline, and its session and delivery are still open. Otherwise the * call is cancelled and its effects must not run. A call whose handler completes normally - * without ever starting it is cancelled; one whose handler fails without starting it fails. + * without ever starting it is cancelled; one whose handler fails without starting it fails. A + * handler that queues the call before running it should first check, through {@link + * #delivery()}, that the player is still connected, and call {@link Delivery#left()} if not. */ public boolean start() { return start.getAsBoolean(); From a0e1c35a574f61b7280992eca8516025872d9ecb Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:42:44 -0400 Subject: [PATCH 09/11] refactor(jvm)!: split Minestom modules into minestom and multistom Rename runtime-minestom to multistom, runtime-minestom-kotlin to multistom-kotlin and minestom-primitives to minestom, so each published module names the Minestom it compiles against. The version catalog now calls the fork `multistom` and upstream Minestom `minestom`. --- README.md | 2 +- crates/chunk-cli/src/creation/tests.rs | 2 +- crates/chunk-cli/src/creation/toolchain.rs | 2 +- crates/chunk-cli/templates/common/README.md | 2 +- docs/distribution.md | 6 +++--- gradle/libs.versions.toml | 6 +++--- jvm/backend-client/README.md | 2 +- jvm/gradle-plugin/README.md | 4 ++-- .../chunkzero/chunk/gradle/ChunkKotlinPlugin.kt | 2 +- .../com/chunkzero/chunk/gradle/ChunkPlugin.kt | 2 +- .../chunkzero/chunk/gradle/ChunkPluginTest.kt | 10 +++++----- jvm/{minestom-primitives => minestom}/README.md | 9 +++++---- .../build.gradle.kts | 4 ++-- .../com/chunkzero/chunk/minestom/ChunkLogin.java | 0 .../chunkzero/chunk/runtime/ChunkLoginTest.java | 0 .../build.gradle.kts | 2 +- .../chunkzero/chunk/runtime/SessionCoroutines.kt | 0 .../chunkzero/chunk/runtime/SessionExtensions.kt | 0 .../chunk/runtime/SessionCoroutinesTest.kt | 0 .../chunk/runtime/SessionExtensionsTest.kt | 0 .../chunk/runtime/SessionOwnershipTest.kt | 0 jvm/{runtime-minestom => multistom}/README.md | 12 ++++++------ .../build.gradle.kts | 2 +- .../chunkzero/chunk/runtime/ChunkMinestom.java | 0 .../chunk/runtime/ComponentBinding.java | 0 .../chunk/runtime/ComponentProvider.java | 0 .../chunk/runtime/ConfiguredSessionProvider.java | 0 .../chunkzero/chunk/runtime/ManagedPlayer.java | 0 .../com/chunkzero/chunk/runtime/Session.java | 0 .../chunkzero/chunk/runtime/SessionCreation.java | 0 .../chunkzero/chunk/runtime/SessionManager.java | 0 .../chunk/runtime/SessionMethodBinding.java | 0 .../chunk/runtime/SessionMethodProvider.java | 0 .../chunkzero/chunk/runtime/SessionProvider.java | 0 .../chunk/runtime/SessionRegistration.java | 0 .../chunkzero/chunk/runtime/SessionScope.java | 0 .../chunkzero/chunk/runtime/TickExecutor.java | 0 .../chunk/runtime/assets/AssetDirectory.java | 0 .../chunkzero/chunk/runtime/assets/Assets.java | 0 .../chunk/runtime/assets/FrozenWorld.java | 0 .../chunk/runtime/assets/PolarLoader.java | 0 .../chunkzero/chunk/runtime/assets/World.java | 0 .../minestom/event/SessionCreateEvent.java | 0 .../minestom/event/SessionDestroyEvent.java | 0 .../runtime/minestom/event/SessionEvent.java | 0 .../runtime/minestom/event/SessionJoinEvent.java | 0 .../minestom/event/SessionLeaveEvent.java | 0 .../minestom/internal/AdmittedPlayer.java | 0 .../minestom/internal/AnvilConverter.java | 0 .../runtime/minestom/internal/AppRegistry.java | 0 .../minestom/internal/ComponentRegistry.java | 0 .../minestom/internal/GameplayService.java | 0 .../minestom/internal/SessionMethodRegistry.java | 0 .../src/main/resources/META-INF/POLAR-LICENSE | 0 .../chunkzero/chunk/runtime/AppRegistryTest.java | 0 .../chunk/runtime/ComponentRegistryTest.java | 0 .../com/chunkzero/chunk/runtime/FakeCore.java | 0 .../chunk/runtime/SessionCreationTest.java | 0 .../chunk/runtime/SessionMethodBindingTest.java | 0 .../chunk/runtime/SessionMethodTest.java | 0 .../chunk/runtime/SharedInstanceTest.java | 0 .../com/chunkzero/chunk/runtime/TestHosts.java | 0 .../com/chunkzero/chunk/runtime/TestScopes.java | 0 .../chunk/runtime/assets/AssetDirectoryTest.java | 0 .../chunk/runtime/assets/WorldTest.java | 0 .../chunk/runtime/bootstrap/FlatSession.java | 0 .../minestom/internal/AnvilConverterTest.java | 0 .../chunk/runtime/GameplayLifecycleTest.kt | 0 .../chunk/runtime/GameplayServiceTest.kt | 0 .../chunkzero/chunk/runtime/SessionEventsTest.kt | 0 .../chunk/runtime/SessionManagerTest.kt | 0 .../chunk/runtime/SessionMethodDisconnectTest.kt | 0 .../com/chunkzero/chunk/runtime/TestClient.kt | 0 jvm/runtime/README.md | 16 ++++++++-------- settings.gradle.kts | 6 +++--- 75 files changed, 46 insertions(+), 45 deletions(-) rename jvm/{minestom-primitives => minestom}/README.md (88%) rename jvm/{minestom-primitives => minestom}/build.gradle.kts (83%) rename jvm/{minestom-primitives => minestom}/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java (100%) rename jvm/{minestom-primitives => minestom}/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java (100%) rename jvm/{runtime-minestom-kotlin => multistom-kotlin}/build.gradle.kts (89%) rename jvm/{runtime-minestom-kotlin => multistom-kotlin}/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt (100%) rename jvm/{runtime-minestom-kotlin => multistom-kotlin}/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt (100%) rename jvm/{runtime-minestom-kotlin => multistom-kotlin}/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt (100%) rename jvm/{runtime-minestom-kotlin => multistom-kotlin}/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt (100%) rename jvm/{runtime-minestom-kotlin => multistom-kotlin}/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt (100%) rename jvm/{runtime-minestom => multistom}/README.md (96%) rename jvm/{runtime-minestom => multistom}/build.gradle.kts (97%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/Session.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/assets/World.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java (100%) rename jvm/{runtime-minestom => multistom}/src/main/resources/META-INF/POLAR-LICENSE (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java (100%) rename jvm/{runtime-minestom => multistom}/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt (100%) rename jvm/{runtime-minestom => multistom}/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt (100%) rename jvm/{runtime-minestom => multistom}/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt (100%) rename jvm/{runtime-minestom => multistom}/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt (100%) rename jvm/{runtime-minestom => multistom}/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt (100%) rename jvm/{runtime-minestom => multistom}/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt (100%) diff --git a/README.md b/README.md index b353b35c..ed078975 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ target/debug/chunk create ../my-server --chunk-source . ``` It creates a Kotlin project (`--language java` for Java) and prints the commands to build and run it. Gameplay code -starts at [`jvm/runtime-minestom`](jvm/runtime-minestom/README.md); [`examples/arena`](examples/arena/README.md) shows +starts at [`jvm/multistom`](jvm/multistom/README.md); [`examples/arena`](examples/arena/README.md) shows the Java side of the API. The [CLI README](crates/chunk-cli/README.md) covers every command: `dev` runs a project locally and rebuilds it on change, `build` packages a release, `players` and `nodes` operate the local environment, and `auth login` and `deploy` deploy to a self-hosted install. [`docs/distribution.md`](docs/distribution.md) covers diff --git a/crates/chunk-cli/src/creation/tests.rs b/crates/chunk-cli/src/creation/tests.rs index 9d7aff57..198f7b02 100644 --- a/crates/chunk-cli/src/creation/tests.rs +++ b/crates/chunk-cli/src/creation/tests.rs @@ -6,7 +6,7 @@ fn source(root: &Path) -> PathBuf { for name in WRAPPER.iter().map(|(name, _)| *name).chain([ "settings.gradle.kts", "jvm/gradle-plugin/settings.gradle.kts", - "jvm/runtime-minestom/build.gradle.kts", + "jvm/multistom/build.gradle.kts", ]) { let path = source.join(name); fs::create_dir_all(path.parent().unwrap()).unwrap(); diff --git a/crates/chunk-cli/src/creation/toolchain.rs b/crates/chunk-cli/src/creation/toolchain.rs index b456507e..b3c80493 100644 --- a/crates/chunk-cli/src/creation/toolchain.rs +++ b/crates/chunk-cli/src/creation/toolchain.rs @@ -38,7 +38,7 @@ impl Toolchain { for name in super::WRAPPER .iter() .map(|(name, _)| *name) - .chain(["jvm/gradle-plugin/settings.gradle.kts", "jvm/runtime-minestom/build.gradle.kts"]) + .chain(["jvm/gradle-plugin/settings.gradle.kts", "jvm/multistom/build.gradle.kts"]) { if !source.join(name).is_file() { return Err(io::Error::new(io::ErrorKind::NotFound, format!("Chunk checkout is missing {name}"))); diff --git a/crates/chunk-cli/templates/common/README.md b/crates/chunk-cli/templates/common/README.md index e039b81f..43f16647 100644 --- a/crates/chunk-cli/templates/common/README.md +++ b/crates/chunk-cli/templates/common/README.md @@ -47,7 +47,7 @@ hook, imported from `#chunk/apps`. A `scope.ts` in any directory under `apps/` a below it. See the [TypeScript SDK](https://github.com/chunkzero/chunk/blob/main/crates/chunk-build/sdk/README.md), the -[session API](https://github.com/chunkzero/chunk/blob/main/jvm/runtime-minestom/README.md) and the +[session API](https://github.com/chunkzero/chunk/blob/main/jvm/multistom/README.md) and the [Gradle plugin](https://github.com/chunkzero/chunk/blob/main/jvm/gradle-plugin/README.md) for the details. ## Toolchain diff --git a/docs/distribution.md b/docs/distribution.md index f687dfbe..c181d6df 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -8,9 +8,9 @@ How the chunk SDK is packaged, installed and published. A release of the SDK has TypeScript SDK, the project templates, the Gradle wrapper and the version pins that `chunk create` writes. - **The JVM artifacts**, published to `https://maven.chunkzero.com` (nightlies to `https://maven.chunkzero.com/nightlies`) under `com.chunkzero.chunk`: `proto`, `backend-api`, `backend-client`, - `backend-client-kotlin`, `runtime`, `runtime-minestom`, `runtime-minestom-kotlin`, `minestom-primitives`, the Gradle - plugin and its three plugin markers. Each library has `-sources.jar` and `-javadoc.jar` (Dokka HTML) artifacts. - Projects resolve these through Gradle; the CLI archives contain no JARs. + `backend-client-kotlin`, `runtime`, `multistom`, `multistom-kotlin`, `minestom`, the Gradle plugin and its three + plugin markers. Each library has `-sources.jar` and `-javadoc.jar` (Dokka HTML) artifacts. Projects resolve these + through Gradle; the CLI archives contain no JARs. Linux archives are built on Ubuntu 24.04 and need its glibc and OpenSSL 3; macOS and Windows archives link OpenSSL statically. Users need Java to run Gradle; the Minestom runtime needs Java 25, which the generated projects download diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7668e813..5e16c241 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ dokka = "2.2.0" protobuf = "4.36.1" grpc = "1.84.0" +minestom = "2026.10.05-26.2" multistom = "0.1.0-nightly.20261005021334.g24a253a8ad29" [libraries] @@ -22,9 +23,8 @@ grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } grpc-api = { module = "io.grpc:grpc-api", version.ref = "grpc" } grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } grpc-netty = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } -minestom = { module = "com.chunkzero.multistom:multistom", version.ref = "multistom" } -# Upstream Minestom, which apps that manage their own sessions may use instead of multistom. -upstream-minestom = { module = "net.minestom:minestom", version = "2026.10.05-26.2" } +minestom = { module = "net.minestom:minestom", version.ref = "minestom" } +multistom = { module = "com.chunkzero.multistom:multistom", version.ref = "multistom" } polar = { module = "dev.hollowcube:polar", version = "1.16.0" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } diff --git a/jvm/backend-client/README.md b/jvm/backend-client/README.md index 7e1f375b..16028e2a 100644 --- a/jvm/backend-client/README.md +++ b/jvm/backend-client/README.md @@ -7,7 +7,7 @@ and derives the `caller` the TypeScript function sees, so arguments can never ch library; the Kotlin adapter lives in `jvm/backend-client-kotlin`. Sessions get their `BackendSession` from `scope.getBackend()` (see the -[Minestom runtime](../runtime-minestom/README.md)) and wrap it in the generated `BackendClient`, which follows the +[Minestom runtime](../multistom/README.md)) and wrap it in the generated `BackendClient`, which follows the backend's function paths. Functions that read `caller.player` need a child session that names the player, owned by the scope for as long as the player stays: diff --git a/jvm/gradle-plugin/README.md b/jvm/gradle-plugin/README.md index 3e30100b..636a87b0 100644 --- a/jvm/gradle-plugin/README.md +++ b/jvm/gradle-plugin/README.md @@ -65,7 +65,7 @@ application { mainClass = "example.Lobby" } `@Component` factories, so its factories are indexed. The project plugins add the Chunk libraries at the plugin's own version: the root exports -`com.chunkzero.chunk:backend-client`, Java apps get `runtime-minestom` and Kotlin apps `runtime-minestom-kotlin`. +`com.chunkzero.chunk:backend-client`, Java apps get `multistom` and Kotlin apps `multistom-kotlin`. ### Settings @@ -163,7 +163,7 @@ Every module applying a Chunk plugin indexes its public static `@Component` fact (`generateChunkComponentIndex`). Each app then reads its own index and those in its dependencies' JARs, checks the graph for missing or duplicate providers, cycles and process components that depend on session ones (`generateChunkComponentBindings`), and compiles one provider that calls the factories directly -(`compileChunkComponents`). See the [Minestom runtime](../runtime-minestom/README.md#components) for writing components. +(`compileChunkComponents`). See the [Minestom runtime](../multistom/README.md#components) for writing components. ## Testing diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkKotlinPlugin.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkKotlinPlugin.kt index ed85682d..64a80f61 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkKotlinPlugin.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkKotlinPlugin.kt @@ -21,7 +21,7 @@ class ChunkKotlinPlugin : Plugin { } applyKotlin(project) project.dependencies.add("implementation", project.dependencies.project(mapOf("path" to KOTLIN_BINDINGS))) - project.dependencies.add("implementation", framework("runtime-minestom-kotlin")) + project.dependencies.add("implementation", framework("multistom-kotlin")) } } } diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkPlugin.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkPlugin.kt index 82bc10ab..d19968e0 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkPlugin.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ChunkPlugin.kt @@ -20,7 +20,7 @@ class ChunkPlugin : Plugin { "Apply com.chunkzero.chunk to the root project before its consumers" } project.dependencies.add("implementation", project.dependencies.project(mapOf("path" to ":"))) - project.dependencies.add("implementation", framework("runtime-minestom")) + project.dependencies.add("implementation", framework("multistom")) } val app = configuration.apps.find { it.projectPath == project.path } configureModule(project, app?.id.orEmpty()) diff --git a/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt b/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt index bc7e85a0..d39089e4 100644 --- a/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt +++ b/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt @@ -297,8 +297,8 @@ class ChunkPluginTest { val repository = File(System.getProperty("chunk.test.repository")) for ((module, name) in listOf( "runtime" to "Component", - "runtime-minestom" to "ComponentBinding", - "runtime-minestom" to "ComponentProvider", + "multistom" to "ComponentBinding", + "multistom" to "ComponentProvider", )) { write( "src/main/java/com/chunkzero/chunk/runtime/$name.java", @@ -403,7 +403,7 @@ class ChunkPluginTest { "apps/lobby/src/main/java/com/chunkzero/chunk/runtime/$name.java", repository .resolve( - "jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/$name.java", + "jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/$name.java", ).readText(), ) } @@ -596,9 +596,9 @@ class ChunkPluginTest { write("chunk.toml", "") listOf( "backend-client", - "runtime-minestom", + "multistom", "backend-client-kotlin", - "runtime-minestom-kotlin", + "multistom-kotlin", ).forEach(::module) write( "chunk-fixture", diff --git a/jvm/minestom-primitives/README.md b/jvm/minestom/README.md similarity index 88% rename from jvm/minestom-primitives/README.md rename to jvm/minestom/README.md index 0cd7d6b5..535782bc 100644 --- a/jvm/minestom-primitives/README.md +++ b/jvm/minestom/README.md @@ -1,9 +1,10 @@ -# Minestom primitives +# Minestom Login handling for Minestom apps that run their own sessions through -[`ChunkSessions`](../runtime/README.md#running-your-own-sessions) instead of `runtime-minestom`. It compiles against -upstream [Minestom](https://minestom.net) (`net.minestom:minestom`) and leaves the Minestom dependency to the app, so -the app picks its Minestom build; it must speak the gateway's Minecraft version, Java Edition 26.2. +[`ChunkSessions`](../runtime/README.md#running-your-own-sessions) instead of the [`multistom`](../multistom/README.md) +runtime. It compiles against upstream [Minestom](https://minestom.net) (`net.minestom:minestom`) and leaves the Minestom +dependency to the app, so the app picks its Minestom build; it must speak the gateway's Minecraft version, Java Edition +26.2. `ChunkLogin` answers the gateway's `chunk:delivery` login request, admits only players this JVM was asked to admit, with the profile the gateway authenticated, and releases each delivery once its player's connection closed. Everything else, diff --git a/jvm/minestom-primitives/build.gradle.kts b/jvm/minestom/build.gradle.kts similarity index 83% rename from jvm/minestom-primitives/build.gradle.kts rename to jvm/minestom/build.gradle.kts index 230d73b7..d12a96f2 100644 --- a/jvm/minestom-primitives/build.gradle.kts +++ b/jvm/minestom/build.gradle.kts @@ -7,8 +7,8 @@ tasks.withType().configureEach { options.release = 25 } dependencies { api(project(":jvm:runtime")) // Apps bring their own Minestom; this module only uses API upstream Minestom shares. - compileOnly(libs.upstream.minestom) + compileOnly(libs.minestom) compileOnly(libs.jetbrains.annotations) - testImplementation(libs.upstream.minestom) + testImplementation(libs.minestom) testImplementation(project(":jvm:proto")) } diff --git a/jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java b/jvm/minestom/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java similarity index 100% rename from jvm/minestom-primitives/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java rename to jvm/minestom/src/main/java/com/chunkzero/chunk/minestom/ChunkLogin.java diff --git a/jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java b/jvm/minestom/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java similarity index 100% rename from jvm/minestom-primitives/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java rename to jvm/minestom/src/test/java/com/chunkzero/chunk/runtime/ChunkLoginTest.java diff --git a/jvm/runtime-minestom-kotlin/build.gradle.kts b/jvm/multistom-kotlin/build.gradle.kts similarity index 89% rename from jvm/runtime-minestom-kotlin/build.gradle.kts rename to jvm/multistom-kotlin/build.gradle.kts index 3758c8b5..57b76360 100644 --- a/jvm/runtime-minestom-kotlin/build.gradle.kts +++ b/jvm/multistom-kotlin/build.gradle.kts @@ -9,7 +9,7 @@ kotlin { } dependencies { - api(project(":jvm:runtime-minestom")) + api(project(":jvm:multistom")) api(project(":jvm:backend-client-kotlin")) testImplementation(project(":jvm:proto")) testImplementation(libs.grpc.netty) diff --git a/jvm/runtime-minestom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt b/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt similarity index 100% rename from jvm/runtime-minestom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt rename to jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt diff --git a/jvm/runtime-minestom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt b/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt similarity index 100% rename from jvm/runtime-minestom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt rename to jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt diff --git a/jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt similarity index 100% rename from jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt rename to jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt diff --git a/jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt similarity index 100% rename from jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt rename to jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt diff --git a/jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt similarity index 100% rename from jvm/runtime-minestom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt rename to jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt diff --git a/jvm/runtime-minestom/README.md b/jvm/multistom/README.md similarity index 96% rename from jvm/runtime-minestom/README.md rename to jvm/multistom/README.md index f4d3b3f9..6516994b 100644 --- a/jvm/runtime-minestom/README.md +++ b/jvm/multistom/README.md @@ -1,8 +1,8 @@ -# Minestom runtime +# Multistom runtime The session API gameplay code is written against, and the adapter that runs it on [Minestom](https://minestom.net). `ChunkMinestom` attaches an app-owned Minestom `ServerProcess` to a [`ChunkProcess`](../runtime/README.md), creates -sessions when core asks, admits the players core delivers, and runs session methods. `jvm/runtime-minestom-kotlin` adds +sessions when core asks, admits the players core delivers, and runs session methods. `jvm/multistom-kotlin` adds coroutine adapters, described [below](#kotlin). Minestom comes from [chunkzero/multistom](https://github.com/chunkzero/multistom), pinned to a nightly of @@ -206,10 +206,10 @@ deliver them to a listener on the `SessionEvent` interface. ## Kotlin -`jvm/runtime-minestom-kotlin` lets sessions use coroutines. Extend `CoroutineSession` and override its suspending -`create`, `join`, `leave` and `finish`. `scope.coroutines` (import `com.chunkzero.chunk.runtime.coroutines`) is a -`CoroutineScope` owned by the session and dispatched on the tick thread, so `launch`, `async` and `Flow.launchIn` resume -there and are cancelled when the session ends. From the Kotlin project template: +`jvm/multistom-kotlin` lets sessions use coroutines. Extend `CoroutineSession` and override its suspending `create`, +`join`, `leave` and `finish`. `scope.coroutines` (import `com.chunkzero.chunk.runtime.coroutines`) is a `CoroutineScope` +owned by the session and dispatched on the tick thread, so `launch`, `async` and `Flow.launchIn` resume there and are +cancelled when the session ends. From the Kotlin project template: ```kotlin private class GreetingSession : CoroutineSession() { diff --git a/jvm/runtime-minestom/build.gradle.kts b/jvm/multistom/build.gradle.kts similarity index 97% rename from jvm/runtime-minestom/build.gradle.kts rename to jvm/multistom/build.gradle.kts index 8d0b2576..f8a06528 100644 --- a/jvm/runtime-minestom/build.gradle.kts +++ b/jvm/multistom/build.gradle.kts @@ -15,7 +15,7 @@ tasks.withType().configureEach { options.release = 25 } dependencies { api(project(":jvm:runtime")) api(project(":jvm:backend-client")) - api(libs.minestom) + api(libs.multistom) api(libs.polar) implementation(project(":jvm:proto")) testImplementation(libs.grpc.netty) diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/Session.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/Session.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/Session.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/Session.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/World.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/World.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/assets/World.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/World.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java diff --git a/jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java similarity index 100% rename from jvm/runtime-minestom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java diff --git a/jvm/runtime-minestom/src/main/resources/META-INF/POLAR-LICENSE b/jvm/multistom/src/main/resources/META-INF/POLAR-LICENSE similarity index 100% rename from jvm/runtime-minestom/src/main/resources/META-INF/POLAR-LICENSE rename to jvm/multistom/src/main/resources/META-INF/POLAR-LICENSE diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java diff --git a/jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java similarity index 100% rename from jvm/runtime-minestom/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt similarity index 100% rename from jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt similarity index 100% rename from jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt similarity index 100% rename from jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt similarity index 100% rename from jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt similarity index 100% rename from jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt diff --git a/jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt similarity index 100% rename from jvm/runtime-minestom/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt diff --git a/jvm/runtime/README.md b/jvm/runtime/README.md index d99937b4..f5aa5240 100644 --- a/jvm/runtime/README.md +++ b/jvm/runtime/README.md @@ -7,12 +7,12 @@ Minestom or Kotlin dependency. Apps use it together with an engine adapter: -| Module | Artifact | Contents | -| ------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------- | -| `jvm/runtime` | `com.chunkzero.chunk:runtime` | `ChunkProcess`, `@SessionType`, `@Component` | -| [`jvm/runtime-minestom`](../runtime-minestom/README.md) | `com.chunkzero.chunk:runtime-minestom` | The Minestom adapter and the session API gameplay code uses | -| `jvm/runtime-minestom-kotlin` | `com.chunkzero.chunk:runtime-minestom-kotlin` | Coroutine adapters, covered in the Minestom README | -| [`jvm/minestom-primitives`](../minestom-primitives/README.md) | `com.chunkzero.chunk:minestom-primitives` | Login handling for apps that run their own sessions on Minestom | +| Module | Artifact | Contents | +| ----------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------- | +| `jvm/runtime` | `com.chunkzero.chunk:runtime` | `ChunkProcess`, `@SessionType`, `@Component` | +| [`jvm/multistom`](../multistom/README.md) | `com.chunkzero.chunk:multistom` | The session API and its runtime on the multistom Minestom fork | +| `jvm/multistom-kotlin` | `com.chunkzero.chunk:multistom-kotlin` | Coroutine adapters, covered in the multistom README | +| [`jvm/minestom`](../minestom/README.md) | `com.chunkzero.chunk:minestom` | Login handling for apps running their own sessions on upstream Minestom | The [Gradle plugin](../gradle-plugin/README.md) adds the right one to each app. @@ -50,7 +50,7 @@ single-use capability the JVM issued for that player's delivery. ## Running your own sessions -`runtime-minestom` decides what a session is: a `Session` with its own scope in a shared Minestom process. An engine +The `multistom` module decides what a session is: a `Session` with its own scope in a shared Minestom process. An engine adapter, or an app that wants other isolation, can instead implement `SessionHandler` and pass it to `chunk.host(...)`. The returned `ChunkSessions` keeps the accounting core relies on, whatever a session is: @@ -99,7 +99,7 @@ try (var chunk = ChunkProcess.connect()) { `SessionHandler.method`, which no session declares by default; a handler calls `SessionMethod.start()` right before running the method's effects and skips them if it returns false. -[`minestom-primitives`](../minestom-primitives/README.md) handles the login side for Minestom. +[`minestom`](../minestom/README.md) handles the login side for upstream Minestom. ## Launch configuration diff --git a/settings.gradle.kts b/settings.gradle.kts index 282b8c7e..8680b6a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,6 +27,6 @@ include(":jvm:backend-api") include(":jvm:backend-client") include(":jvm:backend-client-kotlin") include(":jvm:runtime") -include(":jvm:runtime-minestom") -include(":jvm:runtime-minestom-kotlin") -include(":jvm:minestom-primitives") +include(":jvm:multistom") +include(":jvm:multistom-kotlin") +include(":jvm:minestom") From f631c9455a31fcd494999c98b205fa736cf537c4 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:44:47 -0400 Subject: [PATCH 10/11] style: reflow READMEs after the module rename --- README.md | 10 +++++----- jvm/backend-client/README.md | 7 +++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ed078975..702fda7f 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ target/debug/chunk create ../my-server --chunk-source . ``` It creates a Kotlin project (`--language java` for Java) and prints the commands to build and run it. Gameplay code -starts at [`jvm/multistom`](jvm/multistom/README.md); [`examples/arena`](examples/arena/README.md) shows -the Java side of the API. The [CLI README](crates/chunk-cli/README.md) covers every command: `dev` runs a project -locally and rebuilds it on change, `build` packages a release, `players` and `nodes` operate the local environment, and -`auth login` and `deploy` deploy to a self-hosted install. [`docs/distribution.md`](docs/distribution.md) covers -packaging and publishing the SDK. +starts at [`jvm/multistom`](jvm/multistom/README.md); [`examples/arena`](examples/arena/README.md) shows the Java side +of the API. The [CLI README](crates/chunk-cli/README.md) covers every command: `dev` runs a project locally and rebuilds +it on change, `build` packages a release, `players` and `nodes` operate the local environment, and `auth login` and +`deploy` deploy to a self-hosted install. [`docs/distribution.md`](docs/distribution.md) covers packaging and publishing +the SDK. ## Self-hosting diff --git a/jvm/backend-client/README.md b/jvm/backend-client/README.md index 16028e2a..da604056 100644 --- a/jvm/backend-client/README.md +++ b/jvm/backend-client/README.md @@ -6,10 +6,9 @@ The Java client gameplay code uses to call the project's queries, mutations and and derives the `caller` the TypeScript function sees, so arguments can never change who is calling. It is a Java 21 library; the Kotlin adapter lives in `jvm/backend-client-kotlin`. -Sessions get their `BackendSession` from `scope.getBackend()` (see the -[Minestom runtime](../multistom/README.md)) and wrap it in the generated `BackendClient`, which follows the -backend's function paths. Functions that read `caller.player` need a child session that names the player, owned by the -scope for as long as the player stays: +Sessions get their `BackendSession` from `scope.getBackend()` (see the [Minestom runtime](../multistom/README.md)) and +wrap it in the generated `BackendClient`, which follows the backend's function paths. Functions that read +`caller.player` need a child session that names the player, owned by the scope for as long as the player stays: ```java var backend = From cbe1c6aab27c1da20649452727dbc9340a68e3c9 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:53:02 -0400 Subject: [PATCH 11/11] refactor(jvm)!: move the multistom runtime to com.chunkzero.chunk.multistom --- .../src/codegen/java/configurations.rs | 2 +- .../src/release/session_configurations.rs | 2 +- .../src/release/session_methods.rs | 2 +- crates/chunk-build/src/release/tests.rs | 4 +- crates/chunk-cli/templates/java/Lobby.java | 8 +- crates/chunk-cli/templates/kotlin/Lobby.kt | 12 +-- .../src/main/java/example/arena/Arena.java | 4 +- .../main/java/example/arena/ArenaSession.java | 6 +- .../src/main/java/example/arena/Combat.java | 2 +- .../main/java/example/lobby/Leaderboard.java | 2 +- .../src/main/java/example/lobby/Lobby.java | 4 +- .../main/java/example/lobby/LobbySession.java | 6 +- .../src/main/java/example/lobby/Portal.java | 2 +- .../com/chunkzero/chunk/example/load/Lobby.kt | 12 +-- .../chunk/example/arena/ArenaSessions.kt | 4 +- .../chunk/example/lobby/LobbySessions.kt | 4 +- .../chunk/example/ExampleSessions.kt | 8 +- .../CoinCommandTest.java | 19 +++-- .../chunkzero/chunk/runtime/TestHosts.java | 34 +++++++++ jvm/gradle-plugin/README.md | 2 +- .../chunkzero/chunk/gradle/ComponentIndex.kt | 2 +- .../chunk/gradle/ConvertAnvilWorld.kt | 2 +- .../chunk/gradle/SessionConfigurationIndex.kt | 2 +- .../chunk/gradle/SessionMethodIndex.kt | 12 +-- .../chunk/gradle/WriteComponentBindings.kt | 8 +- .../chunk/gradle/WriteSessionRegistry.kt | 13 +++- .../chunkzero/chunk/gradle/ChunkPluginTest.kt | 73 +++++++++--------- .../chunk/gradle/ComponentIndexTest.kt | 4 +- .../SessionCoroutines.kt | 2 +- .../SessionExtensions.kt | 2 +- .../SessionCoroutinesTest.kt | 15 ++-- .../SessionExtensionsTest.kt | 2 +- .../SessionOwnershipTest.kt | 2 +- .../com/chunkzero/chunk/runtime/TestHosts.kt | 20 +++++ jvm/multistom/README.md | 12 +-- jvm/multistom/build.gradle.kts | 2 +- .../{runtime => multistom}/ChunkMinestom.java | 12 +-- .../ComponentBinding.java | 4 +- .../ComponentProvider.java | 2 +- .../ConfiguredSessionProvider.java | 2 +- .../{runtime => multistom}/ManagedPlayer.java | 4 +- .../chunk/{runtime => multistom}/Session.java | 2 +- .../SessionCreation.java | 2 +- .../SessionManager.java | 28 +++---- .../SessionMethodBinding.java | 2 +- .../SessionMethodProvider.java | 2 +- .../SessionProvider.java | 2 +- .../SessionRegistration.java | 2 +- .../{runtime => multistom}/SessionScope.java | 9 ++- .../{runtime => multistom}/TickExecutor.java | 2 +- .../assets/AssetDirectory.java | 2 +- .../{runtime => multistom}/assets/Assets.java | 2 +- .../assets/FrozenWorld.java | 2 +- .../assets/PolarLoader.java | 2 +- .../{runtime => multistom}/assets/World.java | 4 +- .../event/SessionCreateEvent.java | 4 +- .../event/SessionDestroyEvent.java | 4 +- .../event/SessionEvent.java | 4 +- .../event/SessionJoinEvent.java | 4 +- .../event/SessionLeaveEvent.java | 4 +- .../internal/AdmittedPlayer.java | 6 +- .../internal/AnvilConverter.java | 2 +- .../internal/AppRegistry.java | 6 +- .../internal/ComponentRegistry.java | 8 +- .../internal/GameplayService.java | 6 +- .../internal/SessionMethodRegistry.java | 6 +- .../AppRegistryTest.java | 12 +-- .../ComponentRegistryTest.java | 5 +- .../{runtime => multistom}/FakeCore.java | 8 +- .../SessionCreationTest.java | 75 ++++++++++--------- .../SessionMethodBindingTest.java | 2 +- .../SessionMethodTest.java | 12 ++- .../SharedInstanceTest.java | 2 +- .../{runtime => multistom}/TestScopes.java | 2 +- .../assets/AssetDirectoryTest.java | 2 +- .../assets/WorldTest.java | 4 +- .../bootstrap/FlatSession.java | 6 +- .../internal/AnvilConverterTest.java | 2 +- .../chunkzero/chunk/runtime/TestHosts.java | 67 +++++++++++++++-- .../GameplayLifecycleTest.kt | 32 ++++---- .../GameplayServiceTest.kt | 13 ++-- .../SessionEventsTest.kt | 56 +++++++------- .../SessionManagerTest.kt | 42 ++++++----- .../SessionMethodDisconnectTest.kt | 13 ++-- .../{runtime => multistom}/TestClient.kt | 2 +- .../chunkzero/chunk/runtime/ChunkProcess.java | 3 +- .../chunk/runtime/SessionControl.java | 7 +- scripts/check-consumers.py | 4 +- 88 files changed, 489 insertions(+), 335 deletions(-) rename examples/local/shared/src/test/java/com/chunkzero/chunk/{runtime => multistom}/CoinCommandTest.java (95%) create mode 100644 examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java rename jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionCoroutines.kt (98%) rename jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionExtensions.kt (96%) rename jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionCoroutinesTest.kt (95%) rename jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionExtensionsTest.kt (99%) rename jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionOwnershipTest.kt (98%) create mode 100644 jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/TestHosts.kt rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/ChunkMinestom.java (92%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/ComponentBinding.java (88%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/ComponentProvider.java (87%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/ConfiguredSessionProvider.java (92%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/ManagedPlayer.java (94%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/Session.java (95%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/SessionCreation.java (90%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/SessionManager.java (94%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/SessionMethodBinding.java (96%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/SessionMethodProvider.java (84%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/SessionProvider.java (82%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/SessionRegistration.java (98%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/SessionScope.java (98%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/TickExecutor.java (97%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/assets/AssetDirectory.java (98%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/assets/Assets.java (97%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/assets/FrozenWorld.java (98%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/assets/PolarLoader.java (99%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime => multistom}/assets/World.java (97%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/event/SessionCreateEvent.java (82%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/event/SessionDestroyEvent.java (85%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/event/SessionEvent.java (81%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/event/SessionJoinEvent.java (88%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/event/SessionLeaveEvent.java (90%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/internal/AdmittedPlayer.java (97%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/internal/AnvilConverter.java (99%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/internal/AppRegistry.java (90%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/internal/ComponentRegistry.java (97%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/internal/GameplayService.java (96%) rename jvm/multistom/src/main/java/com/chunkzero/chunk/{runtime/minestom => multistom}/internal/SessionMethodRegistry.java (89%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/AppRegistryTest.java (94%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/ComponentRegistryTest.java (99%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/FakeCore.java (97%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/SessionCreationTest.java (86%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/SessionMethodBindingTest.java (97%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/SessionMethodTest.java (89%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/SharedInstanceTest.java (97%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/TestScopes.java (92%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/assets/AssetDirectoryTest.java (98%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/assets/WorldTest.java (96%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime => multistom}/bootstrap/FlatSession.java (81%) rename jvm/multistom/src/test/java/com/chunkzero/chunk/{runtime/minestom => multistom}/internal/AnvilConverterTest.java (98%) rename jvm/multistom/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/GameplayLifecycleTest.kt (89%) rename jvm/multistom/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/GameplayServiceTest.kt (94%) rename jvm/multistom/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionEventsTest.kt (90%) rename jvm/multistom/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionManagerTest.kt (86%) rename jvm/multistom/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/SessionMethodDisconnectTest.kt (93%) rename jvm/multistom/src/test/kotlin/com/chunkzero/chunk/{runtime => multistom}/TestClient.kt (99%) diff --git a/crates/chunk-build/src/codegen/java/configurations.rs b/crates/chunk-build/src/codegen/java/configurations.rs index b9e4df22..32dc9c7f 100644 --- a/crates/chunk-build/src/codegen/java/configurations.rs +++ b/crates/chunk-build/src/codegen/java/configurations.rs @@ -37,7 +37,7 @@ pub(super) fn sources( let provider = names::type_name(session, &provider_scope.path); provider_scope.declare(&provider, session)?; provider_scope.declarations.push(format!( - "public interface {provider} extends com.chunkzero.chunk.runtime.ConfiguredSessionProvider<{}> {{\n@Override default JsonType<{}> configurationType() {{ return {}.TYPE; }}\n}}", + "public interface {provider} extends com.chunkzero.chunk.multistom.ConfiguredSessionProvider<{}> {{\n@Override default JsonType<{}> configurationType() {{ return {}.TYPE; }}\n}}", config.ty, config.ty, scope.path.join(".") )); metadata.push(json!({ diff --git a/crates/chunk-build/src/release/session_configurations.rs b/crates/chunk-build/src/release/session_configurations.rs index 656a3dde..974be6f5 100644 --- a/crates/chunk-build/src/release/session_configurations.rs +++ b/crates/chunk-build/src/release/session_configurations.rs @@ -64,7 +64,7 @@ pub(super) fn validate( { return Ok(None); } - let registration = read_registration(archive, "com.chunkzero.chunk.runtime.SessionProvider")?; + let registration = read_registration(archive, "com.chunkzero.chunk.multistom.SessionProvider")?; if registration.len() > 65_536 || !registration.lines().any(|line| line.trim() == configuration.provider) { return Err(io::Error::other("unregistered configured session provider")); } diff --git a/crates/chunk-build/src/release/session_methods.rs b/crates/chunk-build/src/release/session_methods.rs index cdf45375..ae990227 100644 --- a/crates/chunk-build/src/release/session_methods.rs +++ b/crates/chunk-build/src/release/session_methods.rs @@ -79,7 +79,7 @@ pub(super) fn validate( if actual.is_empty() { return Ok(()); } - let registration = read_registration(archive, "com.chunkzero.chunk.runtime.SessionMethodProvider")?; + let registration = read_registration(archive, "com.chunkzero.chunk.multistom.SessionMethodProvider")?; let providers: Vec<_> = registration.lines().filter(|line| !line.trim().is_empty()).collect(); if registration.len() > 65_536 || providers.len() != 1 diff --git a/crates/chunk-build/src/release/tests.rs b/crates/chunk-build/src/release/tests.rs index 9d59cd2b..030d1a16 100644 --- a/crates/chunk-build/src/release/tests.rs +++ b/crates/chunk-build/src/release/tests.rs @@ -397,7 +397,7 @@ fn session_methods_publish_with_the_runtime_contract_and_reject_stale_jars() { ("sample/lobby/Provider.class", class(25, 1)), ("sample/Method.class", class(25, 1)), ( - "META-INF/services/com.chunkzero.chunk.runtime.SessionMethodProvider", + "META-INF/services/com.chunkzero.chunk.multistom.SessionMethodProvider", b"sample.lobby.Provider\n".to_vec(), ), ( @@ -460,7 +460,7 @@ fn session_configurations_require_matching_packaged_schemas_and_registered_provi ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\r\nMain-Class: sample.lobby.Provider\r\n\r\n".to_vec()), ("sample/lobby/Provider.class", class(25, 1)), ("sample/ConfigProvider.class", class(25, 1)), - ("META-INF/services/com.chunkzero.chunk.runtime.SessionProvider", provider.as_bytes().to_vec()), + ("META-INF/services/com.chunkzero.chunk.multistom.SessionProvider", provider.as_bytes().to_vec()), ( "META-INF/chunk/session-configurations.json", serde_json::to_vec(&json!({"version":1,"app":"lobby","configurations":[configuration]})).unwrap(), diff --git a/crates/chunk-cli/templates/java/Lobby.java b/crates/chunk-cli/templates/java/Lobby.java index 256578e8..d1f4a68e 100644 --- a/crates/chunk-cli/templates/java/Lobby.java +++ b/crates/chunk-cli/templates/java/Lobby.java @@ -3,11 +3,11 @@ import com.chunkzero.chunk.generated.BackendClient; import com.chunkzero.chunk.generated.BackendTypes.Shared.Greetings.MessageArgs; import com.chunkzero.chunk.generated.BackendTypes.Shared.Greetings.MessageResult; -import com.chunkzero.chunk.runtime.ChunkMinestom; +import com.chunkzero.chunk.multistom.ChunkMinestom; +import com.chunkzero.chunk.multistom.Session; +import com.chunkzero.chunk.multistom.SessionProvider; +import com.chunkzero.chunk.multistom.SessionScope; import com.chunkzero.chunk.runtime.ChunkProcess; -import com.chunkzero.chunk.runtime.Session; -import com.chunkzero.chunk.runtime.SessionProvider; -import com.chunkzero.chunk.runtime.SessionScope; import com.chunkzero.chunk.runtime.SessionType; import net.kyori.adventure.text.Component; diff --git a/crates/chunk-cli/templates/kotlin/Lobby.kt b/crates/chunk-cli/templates/kotlin/Lobby.kt index c90c6bf8..a7224dd4 100644 --- a/crates/chunk-cli/templates/kotlin/Lobby.kt +++ b/crates/chunk-cli/templates/kotlin/Lobby.kt @@ -2,14 +2,14 @@ package example import com.chunkzero.chunk.generated.BackendTypes.Shared.Greetings.MessageArgs import com.chunkzero.chunk.generated.CoroutineBackendClient -import com.chunkzero.chunk.runtime.ChunkMinestom +import com.chunkzero.chunk.multistom.ChunkMinestom +import com.chunkzero.chunk.multistom.CoroutineSession +import com.chunkzero.chunk.multistom.Session +import com.chunkzero.chunk.multistom.SessionProvider +import com.chunkzero.chunk.multistom.SessionScope +import com.chunkzero.chunk.multistom.coroutines import com.chunkzero.chunk.runtime.ChunkProcess -import com.chunkzero.chunk.runtime.CoroutineSession -import com.chunkzero.chunk.runtime.Session -import com.chunkzero.chunk.runtime.SessionProvider -import com.chunkzero.chunk.runtime.SessionScope import com.chunkzero.chunk.runtime.SessionType -import com.chunkzero.chunk.runtime.coroutines import net.kyori.adventure.text.Component import net.minestom.server.ServerProcess import net.minestom.server.entity.Player diff --git a/examples/arena/apps/arena/src/main/java/example/arena/Arena.java b/examples/arena/apps/arena/src/main/java/example/arena/Arena.java index 3b36576f..dcde0aaf 100644 --- a/examples/arena/apps/arena/src/main/java/example/arena/Arena.java +++ b/examples/arena/apps/arena/src/main/java/example/arena/Arena.java @@ -2,9 +2,9 @@ import com.chunkzero.chunk.generated.ArenaSessionProviders; import com.chunkzero.chunk.generated.SessionConfigs; -import com.chunkzero.chunk.runtime.ChunkMinestom; +import com.chunkzero.chunk.multistom.ChunkMinestom; +import com.chunkzero.chunk.multistom.SessionCreation; import com.chunkzero.chunk.runtime.ChunkProcess; -import com.chunkzero.chunk.runtime.SessionCreation; import com.chunkzero.chunk.runtime.SessionType; import net.minestom.server.ServerProcess; diff --git a/examples/arena/apps/arena/src/main/java/example/arena/ArenaSession.java b/examples/arena/apps/arena/src/main/java/example/arena/ArenaSession.java index 114a4a0b..9df3b09a 100644 --- a/examples/arena/apps/arena/src/main/java/example/arena/ArenaSession.java +++ b/examples/arena/apps/arena/src/main/java/example/arena/ArenaSession.java @@ -5,10 +5,10 @@ import com.chunkzero.chunk.generated.Destinations; import com.chunkzero.chunk.generated.SessionMethods; import com.chunkzero.chunk.generated.Worlds; +import com.chunkzero.chunk.multistom.Session; +import com.chunkzero.chunk.multistom.SessionScope; +import com.chunkzero.chunk.multistom.assets.Assets; import com.chunkzero.chunk.runtime.MoveResult; -import com.chunkzero.chunk.runtime.Session; -import com.chunkzero.chunk.runtime.SessionScope; -import com.chunkzero.chunk.runtime.assets.Assets; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; diff --git a/examples/arena/apps/arena/src/main/java/example/arena/Combat.java b/examples/arena/apps/arena/src/main/java/example/arena/Combat.java index 2264b7c3..bcf65b5a 100644 --- a/examples/arena/apps/arena/src/main/java/example/arena/Combat.java +++ b/examples/arena/apps/arena/src/main/java/example/arena/Combat.java @@ -1,6 +1,6 @@ package example.arena; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; diff --git a/examples/arena/apps/lobby/src/main/java/example/lobby/Leaderboard.java b/examples/arena/apps/lobby/src/main/java/example/lobby/Leaderboard.java index 60950e9c..f8e4f3ec 100644 --- a/examples/arena/apps/lobby/src/main/java/example/lobby/Leaderboard.java +++ b/examples/arena/apps/lobby/src/main/java/example/lobby/Leaderboard.java @@ -4,7 +4,7 @@ import com.chunkzero.chunk.backend.client.WatchState; import com.chunkzero.chunk.generated.BackendClient; import com.chunkzero.chunk.generated.BackendTypes.Shared.Stats.LeaderboardResultItem; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; diff --git a/examples/arena/apps/lobby/src/main/java/example/lobby/Lobby.java b/examples/arena/apps/lobby/src/main/java/example/lobby/Lobby.java index f363f6bb..5eb2f312 100644 --- a/examples/arena/apps/lobby/src/main/java/example/lobby/Lobby.java +++ b/examples/arena/apps/lobby/src/main/java/example/lobby/Lobby.java @@ -1,8 +1,8 @@ package example.lobby; -import com.chunkzero.chunk.runtime.ChunkMinestom; +import com.chunkzero.chunk.multistom.ChunkMinestom; +import com.chunkzero.chunk.multistom.SessionProvider; import com.chunkzero.chunk.runtime.ChunkProcess; -import com.chunkzero.chunk.runtime.SessionProvider; import com.chunkzero.chunk.runtime.SessionType; import net.minestom.server.ServerProcess; diff --git a/examples/arena/apps/lobby/src/main/java/example/lobby/LobbySession.java b/examples/arena/apps/lobby/src/main/java/example/lobby/LobbySession.java index ea9dd3e8..af659bd3 100644 --- a/examples/arena/apps/lobby/src/main/java/example/lobby/LobbySession.java +++ b/examples/arena/apps/lobby/src/main/java/example/lobby/LobbySession.java @@ -4,9 +4,9 @@ import com.chunkzero.chunk.generated.BackendClient; import com.chunkzero.chunk.generated.BackendTypes.Shared.Stats.MineResult; import com.chunkzero.chunk.generated.Worlds; -import com.chunkzero.chunk.runtime.Session; -import com.chunkzero.chunk.runtime.SessionScope; -import com.chunkzero.chunk.runtime.assets.Assets; +import com.chunkzero.chunk.multistom.Session; +import com.chunkzero.chunk.multistom.SessionScope; +import com.chunkzero.chunk.multistom.assets.Assets; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; diff --git a/examples/arena/apps/lobby/src/main/java/example/lobby/Portal.java b/examples/arena/apps/lobby/src/main/java/example/lobby/Portal.java index 97c76f1f..f218cd32 100644 --- a/examples/arena/apps/lobby/src/main/java/example/lobby/Portal.java +++ b/examples/arena/apps/lobby/src/main/java/example/lobby/Portal.java @@ -1,8 +1,8 @@ package example.lobby; import com.chunkzero.chunk.generated.Destinations; +import com.chunkzero.chunk.multistom.SessionScope; import com.chunkzero.chunk.runtime.MoveResult; -import com.chunkzero.chunk.runtime.SessionScope; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; diff --git a/examples/load/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/load/Lobby.kt b/examples/load/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/load/Lobby.kt index 376fb284..1f736fb2 100644 --- a/examples/load/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/load/Lobby.kt +++ b/examples/load/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/load/Lobby.kt @@ -2,14 +2,14 @@ package com.chunkzero.chunk.example.load import com.chunkzero.chunk.example.load.generated.BackendTypes import com.chunkzero.chunk.example.load.generated.CoroutineBackendClient -import com.chunkzero.chunk.runtime.ChunkMinestom +import com.chunkzero.chunk.multistom.ChunkMinestom +import com.chunkzero.chunk.multistom.CoroutineSession +import com.chunkzero.chunk.multistom.SessionProvider +import com.chunkzero.chunk.multistom.SessionScope +import com.chunkzero.chunk.multistom.coroutines +import com.chunkzero.chunk.multistom.own import com.chunkzero.chunk.runtime.ChunkProcess -import com.chunkzero.chunk.runtime.CoroutineSession -import com.chunkzero.chunk.runtime.SessionProvider -import com.chunkzero.chunk.runtime.SessionScope import com.chunkzero.chunk.runtime.SessionType -import com.chunkzero.chunk.runtime.coroutines -import com.chunkzero.chunk.runtime.own import kotlinx.coroutines.launch import net.minestom.server.ServerProcess import net.minestom.server.entity.Player diff --git a/examples/local/apps/games/arena/src/main/kotlin/com/chunkzero/chunk/example/arena/ArenaSessions.kt b/examples/local/apps/games/arena/src/main/kotlin/com/chunkzero/chunk/example/arena/ArenaSessions.kt index 50ee9479..9f05a411 100644 --- a/examples/local/apps/games/arena/src/main/kotlin/com/chunkzero/chunk/example/arena/ArenaSessions.kt +++ b/examples/local/apps/games/arena/src/main/kotlin/com/chunkzero/chunk/example/arena/ArenaSessions.kt @@ -3,9 +3,9 @@ package com.chunkzero.chunk.example.arena import com.chunkzero.chunk.example.ExampleSessions import com.chunkzero.chunk.example.generated.ArenaSessionProviders import com.chunkzero.chunk.example.generated.SessionConfigs -import com.chunkzero.chunk.runtime.ChunkMinestom +import com.chunkzero.chunk.multistom.ChunkMinestom +import com.chunkzero.chunk.multistom.SessionCreation import com.chunkzero.chunk.runtime.ChunkProcess -import com.chunkzero.chunk.runtime.SessionCreation import com.chunkzero.chunk.runtime.SessionType import net.minestom.server.ServerProcess diff --git a/examples/local/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/lobby/LobbySessions.kt b/examples/local/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/lobby/LobbySessions.kt index 74b35097..f9b06232 100644 --- a/examples/local/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/lobby/LobbySessions.kt +++ b/examples/local/apps/lobby/src/main/kotlin/com/chunkzero/chunk/example/lobby/LobbySessions.kt @@ -1,9 +1,9 @@ package com.chunkzero.chunk.example.lobby import com.chunkzero.chunk.example.ExampleSessions -import com.chunkzero.chunk.runtime.ChunkMinestom +import com.chunkzero.chunk.multistom.ChunkMinestom +import com.chunkzero.chunk.multistom.SessionProvider import com.chunkzero.chunk.runtime.ChunkProcess -import com.chunkzero.chunk.runtime.SessionProvider import com.chunkzero.chunk.runtime.SessionType import net.minestom.server.ServerProcess diff --git a/examples/local/shared/src/main/kotlin/com/chunkzero/chunk/example/ExampleSessions.kt b/examples/local/shared/src/main/kotlin/com/chunkzero/chunk/example/ExampleSessions.kt index 52e97009..5ed7ac1b 100644 --- a/examples/local/shared/src/main/kotlin/com/chunkzero/chunk/example/ExampleSessions.kt +++ b/examples/local/shared/src/main/kotlin/com/chunkzero/chunk/example/ExampleSessions.kt @@ -4,11 +4,11 @@ import com.chunkzero.chunk.backend.client.QueryResult import com.chunkzero.chunk.example.generated.CoroutineBackendClient import com.chunkzero.chunk.example.generated.Destinations import com.chunkzero.chunk.example.generated.SessionMethods -import com.chunkzero.chunk.runtime.CoroutineSession +import com.chunkzero.chunk.multistom.CoroutineSession +import com.chunkzero.chunk.multistom.Session +import com.chunkzero.chunk.multistom.SessionScope +import com.chunkzero.chunk.multistom.coroutines import com.chunkzero.chunk.runtime.MoveResult -import com.chunkzero.chunk.runtime.Session -import com.chunkzero.chunk.runtime.SessionScope -import com.chunkzero.chunk.runtime.coroutines import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.launch import net.kyori.adventure.text.Component diff --git a/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/CoinCommandTest.java b/examples/local/shared/src/test/java/com/chunkzero/chunk/multistom/CoinCommandTest.java similarity index 95% rename from examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/CoinCommandTest.java rename to examples/local/shared/src/test/java/com/chunkzero/chunk/multistom/CoinCommandTest.java index 69933a06..dc4bc198 100644 --- a/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/CoinCommandTest.java +++ b/examples/local/shared/src/test/java/com/chunkzero/chunk/multistom/CoinCommandTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static org.junit.jupiter.api.Assertions.*; @@ -17,6 +17,7 @@ import com.chunkzero.chunk.backend.client.BackendSession; import com.chunkzero.chunk.backend.client.SessionIdentity; import com.chunkzero.chunk.example.ExampleSessions; +import com.chunkzero.chunk.runtime.TestHosts; import com.google.protobuf.ByteString; import io.grpc.ManagedChannelBuilder; @@ -115,7 +116,7 @@ public void subscribe( () -> ExampleSessions.INSTANCE.arena("Arena"))), Map.of()); var host = - ChunkSessions.detached( + TestHosts.detached( manager, session -> BackendSession.overCore( @@ -129,8 +130,8 @@ public void subscribe( var lobby = session("lobby"); var arena = session("arena"); try { - await(ticks, host.create("lobby", lobby)); - await(ticks, host.create("arena", arena)); + await(ticks, TestHosts.create(host, "lobby", lobby)); + await(ticks, TestHosts.create(host, "arena", arena)); var uuid = UUID.randomUUID(); long generation = 0; for (var destination : List.of("lobby", "arena", "arena")) { @@ -148,8 +149,7 @@ public SocketAddress getRemoteAddress() { connection.setClientState(ConnectionState.PLAY); var player = new ManagedPlayer(connection, new GameProfile(uuid, "player")); player.setDelivery( - new Delivery( - null, + TestHosts.delivery( "delivery", JvmDelivery.newBuilder() .setSession(destination) @@ -158,8 +158,7 @@ public SocketAddress getRemoteAddress() { PlayerIdentity.newBuilder() .setUuid(uuid.toString()) .setUsername("player")) - .build(), - 0)); + .build())); var managed = manager.get(destination); try { await(ticks, managed.join(player)); @@ -181,8 +180,8 @@ public SocketAddress getRemoteAddress() { } } finally { try { - await(ticks, host.finish("lobby", lobby)); - await(ticks, host.finish("arena", arena)); + await(ticks, TestHosts.finish(host, "lobby", lobby)); + await(ticks, TestHosts.finish(host, "arena", arena)); } finally { channel.shutdownNow().awaitTermination(3, TimeUnit.SECONDS); server.shutdownNow().awaitTermination(3, TimeUnit.SECONDS); diff --git a/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java b/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java new file mode 100644 index 00000000..7471a86e --- /dev/null +++ b/examples/local/shared/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java @@ -0,0 +1,34 @@ +package com.chunkzero.chunk.runtime; + +import chunk.sync.v1.Jvm.JvmDelivery; +import chunk.sync.v1.Jvm.JvmSession; +import chunk.sync.v1.Jvm.JvmSessionStatus; + +import com.chunkzero.chunk.backend.client.BackendSession; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +/** Test bridge to the package-private parts of {@link ChunkSessions} and {@link Delivery}. */ +public final class TestHosts { + private TestHosts() {} + + public static ChunkSessions detached( + SessionHandler handler, Function backends) { + return ChunkSessions.detached(handler, backends); + } + + public static CompletableFuture create( + ChunkSessions host, String id, JvmSession session) { + return host.create(id, session); + } + + public static CompletableFuture finish( + ChunkSessions host, String id, JvmSession session) { + return host.finish(id, session); + } + + public static Delivery delivery(String id, JvmDelivery spec) { + return new Delivery(null, id, spec, 0); + } +} diff --git a/jvm/gradle-plugin/README.md b/jvm/gradle-plugin/README.md index 636a87b0..137e3477 100644 --- a/jvm/gradle-plugin/README.md +++ b/jvm/gradle-plugin/README.md @@ -115,7 +115,7 @@ run as. The plugin runs that CLI; it never builds Rust tools or installs Node pa The app's compiled classes are scanned for `@SessionType("id")`. Each annotated class must be public, concrete, have a public no-argument constructor and implement `SessionProvider`; an app has 1 to 128 of them, with distinct IDs, and its main class needs a `public static void main(String[])`. `generateChunkSessionRegistry` writes -`META-INF/services/com.chunkzero.chunk.runtime.SessionProvider`, from which the runtime loads the providers, and a +`META-INF/services/com.chunkzero.chunk.multistom.SessionProvider`, from which the runtime loads the providers, and a catalog of the session type IDs for release assembly. Packaging fails unless those IDs exactly match the app's `implementations` in `app.ts` (by default just `default`). diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ComponentIndex.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ComponentIndex.kt index d2d9a8d3..29010f9e 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ComponentIndex.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ComponentIndex.kt @@ -120,7 +120,7 @@ internal fun validateComponents(factories: List): List() val builtins = - setOf("com/chunkzero/chunk/runtime/SessionScope", "com/chunkzero/chunk/backend/client/BackendSession") + setOf("com/chunkzero/chunk/multistom/SessionScope", "com/chunkzero/chunk/backend/client/BackendSession") for (factory in factories) { require(factory.type !in builtins) { "Component cannot replace session builtin: ${factory.type}" } require(byType.put(factory.type, factory) == null) { "Duplicate component identity: ${factory.type}" } diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ConvertAnvilWorld.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ConvertAnvilWorld.kt index 99d5a3e2..9e724332 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ConvertAnvilWorld.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/ConvertAnvilWorld.kt @@ -55,7 +55,7 @@ abstract class ConvertAnvilWorld : DefaultTask() { .get() .executablePath.asFile.absolutePath classpath = this@ConvertAnvilWorld.classpath - mainClass.set("com.chunkzero.chunk.runtime.minestom.internal.AnvilConverter") + mainClass.set("com.chunkzero.chunk.multistom.internal.AnvilConverter") jvmArgs("--enable-native-access=ALL-UNNAMED") args(source.get().asFile.absolutePath, output.get().asFile.absolutePath) args(chunks.get()) diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionConfigurationIndex.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionConfigurationIndex.kt index 5784ac96..67aa27d1 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionConfigurationIndex.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionConfigurationIndex.kt @@ -28,7 +28,7 @@ internal fun writeSessionConfigurations( } for ((session, provider) in providers) { val type = provider.replace('.', '/') - val configured = lookup.inherits(type, "com/chunkzero/chunk/runtime/ConfiguredSessionProvider") + val configured = lookup.inherits(type, "com/chunkzero/chunk/multistom/ConfiguredSessionProvider") val declared = declarations.any { it["session"].asString == session } require( configured == declared, diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionMethodIndex.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionMethodIndex.kt index 798aa430..f2e2aeaa 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionMethodIndex.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/SessionMethodIndex.kt @@ -33,7 +33,7 @@ internal fun writeSessionMethods( val implementations = providers.mapValues { (_, provider) -> val type = provider.replace('.', '/') - creates(type, lookup.inherits(type, "com/chunkzero/chunk/runtime/ConfiguredSessionProvider")) + creates(type, lookup.inherits(type, "com/chunkzero/chunk/multistom/ConfiguredSessionProvider")) } val bindings = declarations.map { method -> @@ -42,7 +42,7 @@ internal fun writeSessionMethods( val implementation = implementations[session] require( implementation != null && lookup(implementation)?.publicConcrete == true && - lookup.inherits(implementation, "com/chunkzero/chunk/runtime/Session"), + lookup.inherits(implementation, "com/chunkzero/chunk/multistom/Session"), ) { "Session $app/$session method $name requires a provider creation method with a public concrete Session return type" } @@ -51,7 +51,7 @@ internal fun writeSessionMethods( "Session $app/$session must implement ${method["interface"].asString} for method $name" } val type = method["interface"].asString - "new com.chunkzero.chunk.runtime.SessionMethodBinding<>($type.REF, " + + "new com.chunkzero.chunk.multistom.SessionMethodBinding<>($type.REF, " + "(session, args) -> (($type) session).${method["function"].asString}(args))" } for ((session, implementation) in implementations) { @@ -62,7 +62,7 @@ internal fun writeSessionMethods( } sources.deleteRecursively() sources.mkdirs() - val service = resources.resolve("META-INF/services/com.chunkzero.chunk.runtime.SessionMethodProvider") + val service = resources.resolve("META-INF/services/com.chunkzero.chunk.multistom.SessionMethodProvider") service.delete() if (bindings.isNotEmpty()) { val type = "com.chunkzero.chunk.generated.sessions.a${app.toByteArray().joinToString( @@ -73,8 +73,8 @@ internal fun writeSessionMethods( file.writeText( """ package ${type.substringBeforeLast('.')}; - public final class ChunkSessionMethods implements com.chunkzero.chunk.runtime.SessionMethodProvider { - public java.util.Collection> methods() { + public final class ChunkSessionMethods implements com.chunkzero.chunk.multistom.SessionMethodProvider { + public java.util.Collection> methods() { return java.util.List.of(${bindings.joinToString(",\n")}); } } diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteComponentBindings.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteComponentBindings.kt index 31b7ef95..395dafe7 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteComponentBindings.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteComponentBindings.kt @@ -84,7 +84,7 @@ abstract class WriteComponentBindings : DefaultTask() { factories.map { factory -> val dependencies = factory.dependencies.joinToString(", ") { "${sourceType(it)}.class" } val arguments = factory.dependencies.mapIndexed { index, type -> "(${sourceType(type)}) args[$index]" } - "new com.chunkzero.chunk.runtime.ComponentBinding<>(${sourceType(factory.type)}.class, " + + "new com.chunkzero.chunk.multistom.ComponentBinding<>(${sourceType(factory.type)}.class, " + "com.chunkzero.chunk.runtime.Component.Scope.${factory.scope}, java.util.List.of($dependencies), " + "args -> ${sourceType(factory.owner)}.${factory.name}(${arguments.joinToString(", ")}))" } @@ -93,14 +93,14 @@ abstract class WriteComponentBindings : DefaultTask() { source.writeText( """ package $namespace; - public final class ChunkComponents implements com.chunkzero.chunk.runtime.ComponentProvider { - public java.util.Collection> components() { + public final class ChunkComponents implements com.chunkzero.chunk.multistom.ComponentProvider { + public java.util.Collection> components() { return java.util.List.of(${bindings.joinToString(",\n")}); } } """.trimIndent() + "\n", ) - val service = resources.resolve("META-INF/services/com.chunkzero.chunk.runtime.ComponentProvider") + val service = resources.resolve("META-INF/services/com.chunkzero.chunk.multistom.ComponentProvider") service.parentFile.mkdirs() service.writeText("$namespace.ChunkComponents\n") } diff --git a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteSessionRegistry.kt b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteSessionRegistry.kt index 7d0bd4f1..7ac402cb 100644 --- a/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteSessionRegistry.kt +++ b/jvm/gradle-plugin/src/main/kotlin/com/chunkzero/chunk/gradle/WriteSessionRegistry.kt @@ -88,7 +88,9 @@ abstract class WriteSessionRegistry : DefaultTask() { for (type in found.values.filter { it.session != null }) { val id = requireNotNull(type.session) require(id.matches(Regex("[A-Za-z_][A-Za-z0-9_]{0,127}"))) { "Invalid session type ID: $id" } - require(type.constructible && ::lookup.inherits(type.name, "com/chunkzero/chunk/runtime/SessionProvider")) { + require( + type.constructible && ::lookup.inherits(type.name, "com/chunkzero/chunk/multistom/SessionProvider"), + ) { "Session $id requires a public concrete SessionProvider with a public no-argument constructor" } require(sessions.put(id, type.name.replace('/', '.')) == null) { "Duplicate session type: $id" } @@ -109,7 +111,12 @@ abstract class WriteSessionRegistry : DefaultTask() { outputDirectory.get().asFile, bindingSourceDirectory.get().asFile, ) - val output = outputDirectory.file("META-INF/services/com.chunkzero.chunk.runtime.SessionProvider").get().asFile + val output = + outputDirectory + .file( + "META-INF/services/com.chunkzero.chunk.multistom.SessionProvider", + ).get() + .asFile output.parentFile.mkdirs() output.writeText(sessions.values.joinToString("\n", postfix = "\n")) val catalog = catalogFile.get().asFile @@ -174,7 +181,7 @@ private fun inspect(bytes: ByteArray): CompiledClass { ): MethodVisitor? { val arguments = Type.getArgumentTypes(descriptor) val creation = - arguments.size == 1 && arguments[0].descriptor == "Lcom/chunkzero/chunk/runtime/SessionCreation;" + arguments.size == 1 && arguments[0].descriptor == "Lcom/chunkzero/chunk/multistom/SessionCreation;" if (name == "create" && (arguments.isEmpty() || creation) && access and Opcodes.ACC_PUBLIC != 0 && access and (Opcodes.ACC_STATIC or Opcodes.ACC_BRIDGE) == 0 && diff --git a/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt b/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt index d39089e4..52532171 100644 --- a/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt +++ b/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ChunkPluginTest.kt @@ -164,7 +164,7 @@ class ChunkPluginTest { """ package fixture.lobby; @com.chunkzero.chunk.runtime.SessionType("default") - public final class Duplicate implements com.chunkzero.chunk.runtime.SessionProvider {} + public final class Duplicate implements com.chunkzero.chunk.multistom.SessionProvider {} """, ) assertTrue(runFailure(":apps:lobby:generateChunkSessionRegistry").output.contains("Duplicate session type")) @@ -179,7 +179,7 @@ class ChunkPluginTest { "apps/lobby/src/main/java/Game.java", """ package fixture.lobby; - public final class Game extends com.chunkzero.chunk.runtime.Session {} + public final class Game extends com.chunkzero.chunk.multistom.Session {} """, ) assertTrue(runFailure("chunkArtifacts").output.contains("must implement fixture.generated.Ping")) @@ -220,7 +220,7 @@ class ChunkPluginTest { .readText() .replace( "fixture.generated.LobbySessionProviders.Default", - "com.chunkzero.chunk.runtime.ConfiguredSessionProvider", + "com.chunkzero.chunk.multistom.ConfiguredSessionProvider", ).replace( "public Game create", "public com.chunkzero.chunk.backend.api.JsonType configurationType() { return null; } public Game create", @@ -300,9 +300,10 @@ class ChunkPluginTest { "multistom" to "ComponentBinding", "multistom" to "ComponentProvider", )) { + val packagePath = "com/chunkzero/chunk/" + if (module == "runtime") "runtime" else "multistom" write( - "src/main/java/com/chunkzero/chunk/runtime/$name.java", - repository.resolve("jvm/$module/src/main/java/com/chunkzero/chunk/runtime/$name.java").readText(), + "src/main/java/$packagePath/$name.java", + repository.resolve("jvm/$module/src/main/java/$packagePath/$name.java").readText(), ) } write( @@ -324,10 +325,10 @@ class ChunkPluginTest { """ package fixture.lobby; public final class Verify { - private static java.util.Map, com.chunkzero.chunk.runtime.ComponentBinding> bindings; + private static java.util.Map, com.chunkzero.chunk.multistom.ComponentBinding> bindings; public static void run() throws Exception { bindings = new java.util.HashMap<>(); - var providers = java.util.ServiceLoader.load(com.chunkzero.chunk.runtime.ComponentProvider.class).stream().toList(); + var providers = java.util.ServiceLoader.load(com.chunkzero.chunk.multistom.ComponentProvider.class).stream().toList(); if (providers.size() != 1) throw new AssertionError("one generated app registry"); for (var binding : providers.getFirst().get().components()) bindings.put(binding.type(),binding); if (bindings.size() != 3) throw new AssertionError("expected shared and app factories"); @@ -351,7 +352,7 @@ class ChunkPluginTest { package fixture.lobby import com.chunkzero.chunk.runtime.Component @com.chunkzero.chunk.runtime.SessionType("default") - class Factory : com.chunkzero.chunk.runtime.SessionProvider + class Factory : com.chunkzero.chunk.multistom.SessionProvider class Services { companion object { @JvmStatic @@ -371,7 +372,7 @@ class ChunkPluginTest { package fixture.lobby; import com.chunkzero.chunk.runtime.Component; @com.chunkzero.chunk.runtime.SessionType("default") - public final class App implements com.chunkzero.chunk.runtime.SessionProvider { + public final class App implements com.chunkzero.chunk.multistom.SessionProvider { @Component(Component.Scope.PROCESS) public static Extra extra() { return new Extra("app"); } @Component(Component.Scope.SESSION) @@ -400,10 +401,10 @@ class ChunkPluginTest { val repository = File(System.getProperty("chunk.test.repository")) for (name in listOf("SessionCreation", "ConfiguredSessionProvider", "SessionProvider")) { write( - "apps/lobby/src/main/java/com/chunkzero/chunk/runtime/$name.java", + "apps/lobby/src/main/java/com/chunkzero/chunk/multistom/$name.java", repository .resolve( - "jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/$name.java", + "jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/$name.java", ).readText(), ) } @@ -416,14 +417,14 @@ class ChunkPluginTest { "apps/lobby/src/main/kotlin/App.kt", """ package fixture.lobby - import com.chunkzero.chunk.runtime.SessionCreation + import com.chunkzero.chunk.multistom.SessionCreation import fixture.generated.Bindings @com.chunkzero.chunk.runtime.SessionType("default") class Factory : fixture.generated.LobbySessionProviders.Default { override fun create(creation: SessionCreation) = Game() - override fun create(): com.chunkzero.chunk.runtime.Session = Game() + override fun create(): com.chunkzero.chunk.multistom.Session = Game() } - class Game : com.chunkzero.chunk.runtime.Session(), fixture.generated.Ping { + class Game : com.chunkzero.chunk.multistom.Session(), fixture.generated.Ping { override fun ping(args: Bindings): String = "echo:" + args.value() } fun main() { Verify.run(Factory().create(SessionCreation(32, Bindings("ok")))) } @@ -436,10 +437,10 @@ class ChunkPluginTest { package fixture.lobby; @com.chunkzero.chunk.runtime.SessionType("default") public final class App implements fixture.generated.LobbySessionProviders.Default { - public Game create(com.chunkzero.chunk.runtime.SessionCreation creation) { return new Game(); } - public com.chunkzero.chunk.runtime.Session create() { return new Game(); } + public Game create(com.chunkzero.chunk.multistom.SessionCreation creation) { return new Game(); } + public com.chunkzero.chunk.multistom.Session create() { return new Game(); } public static void main(String[] args) { - Verify.run(new App().create(new com.chunkzero.chunk.runtime.SessionCreation<>(32, new fixture.generated.Bindings("ok")))); + Verify.run(new App().create(new com.chunkzero.chunk.multistom.SessionCreation<>(32, new fixture.generated.Bindings("ok")))); } } """, @@ -452,16 +453,16 @@ class ChunkPluginTest { app("lobby", kotlin) write("method-fixture", "") write( - "apps/lobby/src/main/java/com/chunkzero/chunk/runtime/Session.java", + "apps/lobby/src/main/java/com/chunkzero/chunk/multistom/Session.java", """ - package com.chunkzero.chunk.runtime; + package com.chunkzero.chunk.multistom; public abstract class Session {} """, ) write( - "apps/lobby/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java", + "apps/lobby/src/main/java/com/chunkzero/chunk/multistom/SessionMethodBinding.java", """ - package com.chunkzero.chunk.runtime; + package com.chunkzero.chunk.multistom; public final class SessionMethodBinding { private final java.util.function.BiFunction body; public SessionMethodBinding(fixture.generated.Ping.Ref ref, java.util.function.BiFunction body) { this.body=body; } @@ -470,9 +471,9 @@ class ChunkPluginTest { """, ) write( - "apps/lobby/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java", + "apps/lobby/src/main/java/com/chunkzero/chunk/multistom/SessionMethodProvider.java", """ - package com.chunkzero.chunk.runtime; + package com.chunkzero.chunk.multistom; public interface SessionMethodProvider { java.util.Collection> methods(); } @@ -484,9 +485,9 @@ class ChunkPluginTest { package fixture.lobby; public final class Verify { @SuppressWarnings("unchecked") - public static void run(com.chunkzero.chunk.runtime.Session session) { - var provider=java.util.ServiceLoader.load(com.chunkzero.chunk.runtime.SessionMethodProvider.class).findFirst().orElseThrow(); - var method=(com.chunkzero.chunk.runtime.SessionMethodBinding) provider.methods().iterator().next(); + public static void run(com.chunkzero.chunk.multistom.Session session) { + var provider=java.util.ServiceLoader.load(com.chunkzero.chunk.multistom.SessionMethodProvider.class).findFirst().orElseThrow(); + var method=(com.chunkzero.chunk.multistom.SessionMethodBinding) provider.methods().iterator().next(); System.out.println(method.invoke(session,new fixture.generated.Bindings("ok"))); } } @@ -498,10 +499,10 @@ class ChunkPluginTest { """ package fixture.lobby @com.chunkzero.chunk.runtime.SessionType("default") - class Factory : com.chunkzero.chunk.runtime.SessionProvider { + class Factory : com.chunkzero.chunk.multistom.SessionProvider { fun create() = Game() } - class Game : com.chunkzero.chunk.runtime.Session(), fixture.generated.Ping { + class Game : com.chunkzero.chunk.multistom.Session(), fixture.generated.Ping { override fun ping(args: fixture.generated.Bindings): String = "echo:" + args.value() } fun main() { Verify.run(Factory().create()) } @@ -513,7 +514,7 @@ class ChunkPluginTest { """ package fixture.lobby; @com.chunkzero.chunk.runtime.SessionType("default") - public final class App implements com.chunkzero.chunk.runtime.SessionProvider { + public final class App implements com.chunkzero.chunk.multistom.SessionProvider { public Game create() { return new Game(); } public static void main(String[] args) { Verify.run(new App().create()); } } @@ -523,7 +524,7 @@ class ChunkPluginTest { "apps/lobby/src/main/java/Game.java", """ package fixture.lobby; - public final class Game extends com.chunkzero.chunk.runtime.Session implements fixture.generated.Ping { + public final class Game extends com.chunkzero.chunk.multistom.Session implements fixture.generated.Ping { public String ping(fixture.generated.Bindings args) { return "echo:" + args.value(); } } """, @@ -632,7 +633,7 @@ class ChunkPluginTest { files['java/fixture/generated/Ping.java'] = 'package fixture.generated; public interface Ping { String ping(Bindings args); record Ref() {} Ref REF = new Ref<>(); }' methods = [{'app':'lobby','session':'default','name':'ping','interface':'fixture.generated.Ping','binary_interface':'fixture.generated.Ping','function':'ping','arguments':{'type':'object','fields':{}},'result':{'type':'string'}}] if (root / 'configuration-fixture').is_file(): - files['java-session/lobby/fixture/generated/LobbySessionProviders.java'] = 'package fixture.generated; public final class LobbySessionProviders { public interface Default extends com.chunkzero.chunk.runtime.ConfiguredSessionProvider { default com.chunkzero.chunk.backend.api.JsonType configurationType() { return null; } } }' + files['java-session/lobby/fixture/generated/LobbySessionProviders.java'] = 'package fixture.generated; public final class LobbySessionProviders { public interface Default extends com.chunkzero.chunk.multistom.ConfiguredSessionProvider { default com.chunkzero.chunk.backend.api.JsonType configurationType() { return null; } } }' configurations = [{'app':'lobby','session':'default','interface':'fixture.generated.LobbySessionProviders.Default','binary_interface':'fixture.generated.LobbySessionProviders${'$'}Default','configuration':{'type':'object','fields':{'value':{'schema':{'type':'string'}}}}}] for name, content in files.items(): path = output / name @@ -676,9 +677,9 @@ class ChunkPluginTest { """, ) write( - "apps/$id/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java", + "apps/$id/src/main/java/com/chunkzero/chunk/multistom/SessionProvider.java", """ - package com.chunkzero.chunk.runtime; + package com.chunkzero.chunk.multistom; public interface SessionProvider {} """, ) @@ -688,7 +689,7 @@ class ChunkPluginTest { """ package fixture.$id @com.chunkzero.chunk.runtime.SessionType("default") - class Factory : com.chunkzero.chunk.runtime.SessionProvider + class Factory : com.chunkzero.chunk.multistom.SessionProvider fun main(args: Array) {} suspend fun result(): fixture.generated.Bindings = fixture.generated.value() """, @@ -699,7 +700,7 @@ class ChunkPluginTest { """ package fixture.$id; @com.chunkzero.chunk.runtime.SessionType("default") - public final class App implements com.chunkzero.chunk.runtime.SessionProvider { + public final class App implements com.chunkzero.chunk.multistom.SessionProvider { public static void main(String[] args) {} public fixture.generated.Bindings value() { return fixture.generated.Client.value(); } } @@ -763,7 +764,7 @@ class ChunkPluginTest { JarFile(app.asJsonObject["jar"].asString).use { jar -> assertTrue(jar.getJarEntry("META-INF/chunk/app.json") == null) val entry = - requireNotNull(jar.getJarEntry("META-INF/services/com.chunkzero.chunk.runtime.SessionProvider")) + requireNotNull(jar.getJarEntry("META-INF/services/com.chunkzero.chunk.multistom.SessionProvider")) val providers = jar.getInputStream(entry).bufferedReader().use { it.readLines() } assertEquals(1, providers.size) assertTrue(jar.getJarEntry(providers.single().replace('.', '/') + ".class") != null) diff --git a/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ComponentIndexTest.kt b/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ComponentIndexTest.kt index b3418947..4664d99a 100644 --- a/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ComponentIndexTest.kt +++ b/jvm/gradle-plugin/src/test/kotlin/com/chunkzero/chunk/gradle/ComponentIndexTest.kt @@ -16,7 +16,7 @@ class ComponentIndexTest { listOf(factory("A", "SESSION", "B"), factory("B", "SESSION", "A")) to "Component dependency cycle", listOf(factory("A", "PROCESS", "B"), factory("B", "PROCESS", "C"), factory("C")) to "captures session dependency", - listOf(factory("A", "PROCESS", "com/chunkzero/chunk/runtime/SessionScope")) to + listOf(factory("A", "PROCESS", "com/chunkzero/chunk/multistom/SessionScope")) to "captures session dependency", listOf( factory("com/chunkzero/chunk/backend/client/BackendSession"), @@ -32,7 +32,7 @@ class ComponentIndexTest { fun `sessions can use process providers and session builtins without duplicate shared construction`() { val graph = listOf( - factory("View", "SESSION", "Clock", "Backend", "com/chunkzero/chunk/runtime/SessionScope"), + factory("View", "SESSION", "Clock", "Backend", "com/chunkzero/chunk/multistom/SessionScope"), factory("Clock", "PROCESS"), factory("Backend", "SESSION", "com/chunkzero/chunk/backend/client/BackendSession"), ) diff --git a/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt b/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/multistom/SessionCoroutines.kt similarity index 98% rename from jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt rename to jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/multistom/SessionCoroutines.kt index 06e37be8..4a5eccd4 100644 --- a/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionCoroutines.kt +++ b/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/multistom/SessionCoroutines.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import com.chunkzero.chunk.backend.CoroutineBackend import com.chunkzero.chunk.backend.api.PlayerId diff --git a/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt b/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/multistom/SessionExtensions.kt similarity index 96% rename from jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt rename to jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/multistom/SessionExtensions.kt index 74ecacf9..eab8b203 100644 --- a/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/runtime/SessionExtensions.kt +++ b/jvm/multistom-kotlin/src/main/kotlin/com/chunkzero/chunk/multistom/SessionExtensions.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import net.minestom.server.entity.Player import net.minestom.server.timer.Task diff --git a/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionCoroutinesTest.kt similarity index 95% rename from jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt rename to jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionCoroutinesTest.kt index 303f9193..1a2ca22b 100644 --- a/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionCoroutinesTest.kt +++ b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionCoroutinesTest.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import chunk.sync.v1.CoreGrpc import chunk.sync.v1.CoreOuterClass.CallRequest @@ -18,6 +18,7 @@ import com.chunkzero.chunk.backend.api.SessionId import com.chunkzero.chunk.backend.client.BackendSession import com.chunkzero.chunk.backend.client.OperationId import com.chunkzero.chunk.backend.client.SessionIdentity +import com.chunkzero.chunk.runtime.TestHosts import com.google.protobuf.ByteString import io.grpc.ManagedChannelBuilder import io.grpc.ServerBuilder @@ -167,15 +168,15 @@ class SessionCoroutinesTest { } assertTrue(until()) } - val host = ChunkSessions.detached(manager) { null } + val host = TestHosts.detached(manager) try { val game = session("game") val other = session("flat") - val created = host.create("game", game) - val independent = host.create("other", other) + val created = TestHosts.create(host, "game", game) + val independent = TestHosts.create(host, "other", other) pump { created.isDone && independent.isDone && observed } assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, created.join().phase) - val ending = host.finish("game", game) + val ending = TestHosts.finish(host, "game", game) pump { mutationStarted.isDone } assertFalse(ending.isDone) assertFalse(watchClosed.isDone) @@ -183,8 +184,8 @@ class SessionCoroutinesTest { pump { ending.isDone && watchClosed.isDone } assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDED, ending.join().phase) assertTrue(committed) - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, manager.get("other").phase) - val stopOther = host.finish("other", other) + manager.get("other") + val stopOther = TestHosts.finish(host, "other", other) pump { stopOther.isDone } } finally { result.complete(Unit) diff --git a/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionExtensionsTest.kt similarity index 99% rename from jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt rename to jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionExtensionsTest.kt index c521dc60..896b10fe 100644 --- a/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionExtensionsTest.kt +++ b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionExtensionsTest.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import net.minestom.server.ServerProcess import net.minestom.server.entity.Player diff --git a/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionOwnershipTest.kt similarity index 98% rename from jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt rename to jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionOwnershipTest.kt index fcf2e16c..269707df 100644 --- a/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/SessionOwnershipTest.kt +++ b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/multistom/SessionOwnershipTest.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch diff --git a/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/TestHosts.kt b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/TestHosts.kt new file mode 100644 index 00000000..a05db04f --- /dev/null +++ b/jvm/multistom-kotlin/src/test/kotlin/com/chunkzero/chunk/runtime/TestHosts.kt @@ -0,0 +1,20 @@ +package com.chunkzero.chunk.runtime + +import chunk.sync.v1.Jvm.JvmSession + +/** Test bridge to the package-private parts of [ChunkSessions]. */ +object TestHosts { + fun detached(handler: SessionHandler): ChunkSessions = ChunkSessions.detached(handler) { null } + + fun create( + host: ChunkSessions, + id: String, + session: JvmSession, + ) = host.create(id, session) + + fun finish( + host: ChunkSessions, + id: String, + session: JvmSession, + ) = host.finish(id, session) +} diff --git a/jvm/multistom/README.md b/jvm/multistom/README.md index 6516994b..8504d037 100644 --- a/jvm/multistom/README.md +++ b/jvm/multistom/README.md @@ -168,7 +168,7 @@ values they own. In Kotlin, use top-level functions or `@JvmStatic` functions in ## Assets -`com.chunkzero.chunk.runtime.assets.Assets` reads the deployment's assets from the read-only directory the platform +`com.chunkzero.chunk.multistom.assets.Assets` reads the deployment's assets from the read-only directory the platform names in `CHUNK_ASSETS`. `Assets.file("config/rules.json")` returns the app's `assets/config/rules.json`, or else the project's shared one. `Assets.world(Worlds.Arena.KOTH)` resolves a world the app declares in `app.ts`, by its generated handle; another app's world is an error. A `World` is resolved and parsed once per JVM: @@ -184,8 +184,8 @@ Call both on the tick thread. A shared world's blocks can't be changed, since ev ## Events -`com.chunkzero.chunk.runtime.minestom.event` has one event per lifecycle step. Listen on the process to see every -session, or on `scope.getEvents()` for one: +`com.chunkzero.chunk.multistom.event` has one event per lifecycle step. Listen on the process to see every session, or +on `scope.getEvents()` for one: ```java server.eventHandler().addListener(SessionJoinEvent.class, event -> { @@ -207,9 +207,9 @@ deliver them to a listener on the `SessionEvent` interface. ## Kotlin `jvm/multistom-kotlin` lets sessions use coroutines. Extend `CoroutineSession` and override its suspending `create`, -`join`, `leave` and `finish`. `scope.coroutines` (import `com.chunkzero.chunk.runtime.coroutines`) is a `CoroutineScope` -owned by the session and dispatched on the tick thread, so `launch`, `async` and `Flow.launchIn` resume there and are -cancelled when the session ends. From the Kotlin project template: +`join`, `leave` and `finish`. `scope.coroutines` (import `com.chunkzero.chunk.multistom.coroutines`) is a +`CoroutineScope` owned by the session and dispatched on the tick thread, so `launch`, `async` and `Flow.launchIn` resume +there and are cancelled when the session ends. From the Kotlin project template: ```kotlin private class GreetingSession : CoroutineSession() { diff --git a/jvm/multistom/build.gradle.kts b/jvm/multistom/build.gradle.kts index f8a06528..b0d9bbf7 100644 --- a/jvm/multistom/build.gradle.kts +++ b/jvm/multistom/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { dokka { dokkaSourceSets.configureEach { suppressedFiles.from( - fileTree("src/main/java/com/chunkzero/chunk/runtime") { + fileTree("src/main/java/com/chunkzero/chunk/multistom") { include("ManagedPlayer.java", "SessionManager.java", "SessionRegistration.java", "TickExecutor.java") }, ) diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ChunkMinestom.java similarity index 92% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ChunkMinestom.java index 97f8a0dc..bb197f89 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ChunkMinestom.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ChunkMinestom.java @@ -1,9 +1,11 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; -import com.chunkzero.chunk.runtime.minestom.internal.AppRegistry; -import com.chunkzero.chunk.runtime.minestom.internal.ComponentRegistry; -import com.chunkzero.chunk.runtime.minestom.internal.GameplayService; -import com.chunkzero.chunk.runtime.minestom.internal.SessionMethodRegistry; +import com.chunkzero.chunk.multistom.internal.AppRegistry; +import com.chunkzero.chunk.multistom.internal.ComponentRegistry; +import com.chunkzero.chunk.multistom.internal.GameplayService; +import com.chunkzero.chunk.multistom.internal.SessionMethodRegistry; +import com.chunkzero.chunk.runtime.ChunkProcess; +import com.chunkzero.chunk.runtime.ChunkSessions; import net.minestom.server.MinecraftConstants; import net.minestom.server.ServerProcess; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ComponentBinding.java similarity index 88% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ComponentBinding.java index ab8f2c84..d1b7f6b0 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentBinding.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ComponentBinding.java @@ -1,4 +1,6 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; + +import com.chunkzero.chunk.runtime.Component; import org.jetbrains.annotations.ApiStatus; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ComponentProvider.java similarity index 87% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ComponentProvider.java index 8dd41ed6..8f73cf8d 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ComponentProvider.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ComponentProvider.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import org.jetbrains.annotations.ApiStatus; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ConfiguredSessionProvider.java similarity index 92% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ConfiguredSessionProvider.java index 7a418fce..1264b218 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ConfiguredSessionProvider.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ConfiguredSessionProvider.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import com.chunkzero.chunk.backend.api.JsonType; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ManagedPlayer.java similarity index 94% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ManagedPlayer.java index 50ed7709..6285160f 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/ManagedPlayer.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/ManagedPlayer.java @@ -1,4 +1,6 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; + +import com.chunkzero.chunk.runtime.Delivery; import net.minestom.server.entity.Player; import net.minestom.server.network.player.GameProfile; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/Session.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/Session.java similarity index 95% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/Session.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/Session.java index 7be68424..43ee029e 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/Session.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/Session.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import net.minestom.server.entity.Player; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionCreation.java similarity index 90% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionCreation.java index fe5424b2..dcac0066 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionCreation.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionCreation.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import java.util.Objects; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionManager.java similarity index 94% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionManager.java index edcd6e39..ca7016df 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionManager.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionManager.java @@ -1,11 +1,13 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; -import chunk.sync.v1.Jvm.JvmSessionPhase; - -import com.chunkzero.chunk.runtime.minestom.event.SessionCreateEvent; -import com.chunkzero.chunk.runtime.minestom.event.SessionJoinEvent; -import com.chunkzero.chunk.runtime.minestom.event.SessionLeaveEvent; -import com.chunkzero.chunk.runtime.minestom.internal.ComponentRegistry; +import com.chunkzero.chunk.multistom.event.SessionCreateEvent; +import com.chunkzero.chunk.multistom.event.SessionJoinEvent; +import com.chunkzero.chunk.multistom.event.SessionLeaveEvent; +import com.chunkzero.chunk.multistom.internal.ComponentRegistry; +import com.chunkzero.chunk.runtime.Delivery; +import com.chunkzero.chunk.runtime.SessionControl; +import com.chunkzero.chunk.runtime.SessionHandler; +import com.chunkzero.chunk.runtime.SessionMethod; import net.minestom.server.ServerProcess; import net.minestom.server.entity.Player; @@ -136,8 +138,7 @@ public CompletionStage method(SessionControl control, SessionMethod call if (managed != null && managed.isDisconnected(call.delivery())) call.delivery().left(); if (!call.start()) return null; - if (managed == null - || control.phase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) + if (managed == null || !control.isReady()) throw new IllegalStateException("Session unavailable"); return binding.invoke(managed.behavior, call.argumentsJson()); }); @@ -147,8 +148,7 @@ public CompletionStage method(SessionControl control, SessionMethod call public ManagedSession get(String id) { var session = sessions.get(id); if (session == null) throw new IllegalArgumentException("Unknown session"); - if (session.getPhase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) - throw new IllegalStateException("Session unavailable"); + if (!session.control.isReady()) throw new IllegalStateException("Session unavailable"); return session; } @@ -181,10 +181,6 @@ public int getCapacity() { return control.capacity(); } - public JvmSessionPhase getPhase() { - return control.phase(); - } - public SessionScope getScope() { return scope; } @@ -227,7 +223,7 @@ void start() { public CompletableFuture join(Player player) { return ticks.submit( () -> { - if (getPhase() != JvmSessionPhase.JVM_SESSION_PHASE_READY) + if (!control.isReady()) throw new IllegalStateException("Session unavailable"); scope.getPlayers().add(player); return invoke(() -> behavior.onJoin(player)); diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionMethodBinding.java similarity index 96% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionMethodBinding.java index e7352948..911ff8e9 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodBinding.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionMethodBinding.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import com.chunkzero.chunk.backend.api.SessionMethodRef; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionMethodProvider.java similarity index 84% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionMethodProvider.java index d950aa5e..afcc72f2 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionMethodProvider.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionMethodProvider.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import java.util.Collection; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionProvider.java similarity index 82% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionProvider.java index 3ea92b3c..65dbf763 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionProvider.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionProvider.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; /** One app-owned service provider creates fresh session state for each instance of that app. */ @FunctionalInterface diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionRegistration.java similarity index 98% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionRegistration.java index b6271261..c7441ce4 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionRegistration.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionRegistration.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import com.chunkzero.chunk.backend.api.BackendJson; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionScope.java similarity index 98% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionScope.java index 59d6a434..cfe21b37 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/SessionScope.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/SessionScope.java @@ -1,11 +1,12 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import com.chunkzero.chunk.backend.api.Destination; import com.chunkzero.chunk.backend.client.BackendSession; import com.chunkzero.chunk.backend.client.OperationId; -import com.chunkzero.chunk.runtime.minestom.event.SessionDestroyEvent; -import com.chunkzero.chunk.runtime.minestom.event.SessionEvent; -import com.chunkzero.chunk.runtime.minestom.internal.ComponentRegistry; +import com.chunkzero.chunk.multistom.event.SessionDestroyEvent; +import com.chunkzero.chunk.multistom.event.SessionEvent; +import com.chunkzero.chunk.multistom.internal.ComponentRegistry; +import com.chunkzero.chunk.runtime.MoveResult; import net.minestom.server.ServerProcess; import net.minestom.server.entity.Player; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/TickExecutor.java similarity index 97% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/TickExecutor.java index 9885c510..dc8cf2fe 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/TickExecutor.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/TickExecutor.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/AssetDirectory.java similarity index 98% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/AssetDirectory.java index c29f997a..f262059f 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/AssetDirectory.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/AssetDirectory.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime.assets; +package com.chunkzero.chunk.multistom.assets; import com.chunkzero.chunk.backend.api.BackendJson; import com.chunkzero.chunk.backend.api.WorldAsset; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/Assets.java similarity index 97% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/Assets.java index 3d762846..14568225 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/Assets.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/Assets.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime.assets; +package com.chunkzero.chunk.multistom.assets; import com.chunkzero.chunk.backend.api.WorldAsset; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/FrozenWorld.java similarity index 98% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/FrozenWorld.java index d9ccad70..48d0cf48 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/FrozenWorld.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/FrozenWorld.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime.assets; +package com.chunkzero.chunk.multistom.assets; import net.minestom.server.ServerProcess; import net.minestom.server.coordinate.Point; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/PolarLoader.java similarity index 99% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/PolarLoader.java index f555b01f..d91b60e6 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/PolarLoader.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/PolarLoader.java @@ -1,6 +1,6 @@ // Adapted from Hollow Cube Polar 1.16.0 (MIT); see META-INF/POLAR-LICENSE. // Loading only, bound to a process's registries; codec and file layout unchanged. -package com.chunkzero.chunk.runtime.assets; +package com.chunkzero.chunk.multistom.assets; import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/World.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/World.java similarity index 97% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/World.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/World.java index 523e8e2b..a5f68982 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/assets/World.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/assets/World.java @@ -1,7 +1,7 @@ -package com.chunkzero.chunk.runtime.assets; +package com.chunkzero.chunk.multistom.assets; import com.chunkzero.chunk.backend.api.WorldAsset; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import net.hollowcube.polar.PolarReader; import net.hollowcube.polar.PolarWorld; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionCreateEvent.java similarity index 82% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionCreateEvent.java index 1d6944cb..d99f0562 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionCreateEvent.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionCreateEvent.java @@ -1,6 +1,6 @@ -package com.chunkzero.chunk.runtime.minestom.event; +package com.chunkzero.chunk.multistom.event; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import org.jetbrains.annotations.NotNull; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionDestroyEvent.java similarity index 85% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionDestroyEvent.java index 91840d96..0c3e771b 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionDestroyEvent.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionDestroyEvent.java @@ -1,6 +1,6 @@ -package com.chunkzero.chunk.runtime.minestom.event; +package com.chunkzero.chunk.multistom.event; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import org.jetbrains.annotations.NotNull; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionEvent.java similarity index 81% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionEvent.java index 4c759d80..7fcf5392 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionEvent.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionEvent.java @@ -1,6 +1,6 @@ -package com.chunkzero.chunk.runtime.minestom.event; +package com.chunkzero.chunk.multistom.event; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import net.minestom.server.event.Event; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionJoinEvent.java similarity index 88% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionJoinEvent.java index c62b4ec9..39e67752 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionJoinEvent.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionJoinEvent.java @@ -1,6 +1,6 @@ -package com.chunkzero.chunk.runtime.minestom.event; +package com.chunkzero.chunk.multistom.event; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import net.minestom.server.entity.Player; import net.minestom.server.event.trait.PlayerEvent; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionLeaveEvent.java similarity index 90% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionLeaveEvent.java index f4e37f2e..6411180a 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/event/SessionLeaveEvent.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/event/SessionLeaveEvent.java @@ -1,6 +1,6 @@ -package com.chunkzero.chunk.runtime.minestom.event; +package com.chunkzero.chunk.multistom.event; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.SessionScope; import net.minestom.server.entity.Player; import net.minestom.server.event.trait.PlayerEvent; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AdmittedPlayer.java similarity index 97% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AdmittedPlayer.java index 112c8694..8672f753 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AdmittedPlayer.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AdmittedPlayer.java @@ -1,8 +1,8 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.multistom.internal; +import com.chunkzero.chunk.multistom.ManagedPlayer; +import com.chunkzero.chunk.multistom.SessionManager; import com.chunkzero.chunk.runtime.Delivery; -import com.chunkzero.chunk.runtime.ManagedPlayer; -import com.chunkzero.chunk.runtime.SessionManager; import net.minestom.server.instance.Instance; import net.minestom.server.network.player.PlayerConnection; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AnvilConverter.java similarity index 99% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AnvilConverter.java index b42d1e3d..0745409b 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverter.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AnvilConverter.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.multistom.internal; import net.hollowcube.polar.PolarChunk; import net.hollowcube.polar.PolarDataConverter; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AppRegistry.java similarity index 90% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AppRegistry.java index ea9dbbfc..de1d9564 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/AppRegistry.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/AppRegistry.java @@ -1,7 +1,7 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.multistom.internal; -import com.chunkzero.chunk.runtime.SessionProvider; -import com.chunkzero.chunk.runtime.SessionRegistration; +import com.chunkzero.chunk.multistom.SessionProvider; +import com.chunkzero.chunk.multistom.SessionRegistration; import com.chunkzero.chunk.runtime.SessionType; import org.jetbrains.annotations.ApiStatus; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/ComponentRegistry.java similarity index 97% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/ComponentRegistry.java index 7652f015..61749758 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/ComponentRegistry.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/ComponentRegistry.java @@ -1,10 +1,10 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.multistom.internal; import com.chunkzero.chunk.backend.client.BackendSession; +import com.chunkzero.chunk.multistom.ComponentBinding; +import com.chunkzero.chunk.multistom.ComponentProvider; +import com.chunkzero.chunk.multistom.SessionScope; import com.chunkzero.chunk.runtime.Component; -import com.chunkzero.chunk.runtime.ComponentBinding; -import com.chunkzero.chunk.runtime.ComponentProvider; -import com.chunkzero.chunk.runtime.SessionScope; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/GameplayService.java similarity index 96% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/GameplayService.java index 9affb0ce..633d93c3 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/GameplayService.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/GameplayService.java @@ -1,9 +1,9 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.multistom.internal; +import com.chunkzero.chunk.multistom.ManagedPlayer; +import com.chunkzero.chunk.multistom.SessionManager; import com.chunkzero.chunk.runtime.ChunkSessions; -import com.chunkzero.chunk.runtime.ManagedPlayer; import com.chunkzero.chunk.runtime.PlayerProfile; -import com.chunkzero.chunk.runtime.SessionManager; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Pos; diff --git a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/SessionMethodRegistry.java similarity index 89% rename from jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java rename to jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/SessionMethodRegistry.java index 1be740be..2ce6e7bb 100644 --- a/jvm/multistom/src/main/java/com/chunkzero/chunk/runtime/minestom/internal/SessionMethodRegistry.java +++ b/jvm/multistom/src/main/java/com/chunkzero/chunk/multistom/internal/SessionMethodRegistry.java @@ -1,7 +1,7 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.multistom.internal; -import com.chunkzero.chunk.runtime.SessionMethodBinding; -import com.chunkzero.chunk.runtime.SessionMethodProvider; +import com.chunkzero.chunk.multistom.SessionMethodBinding; +import com.chunkzero.chunk.multistom.SessionMethodProvider; import org.jetbrains.annotations.ApiStatus; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/AppRegistryTest.java similarity index 94% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/AppRegistryTest.java index 136f62da..533fcad0 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/AppRegistryTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/AppRegistryTest.java @@ -1,8 +1,9 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static org.junit.jupiter.api.Assertions.*; -import com.chunkzero.chunk.runtime.minestom.internal.AppRegistry; +import com.chunkzero.chunk.multistom.internal.AppRegistry; +import com.chunkzero.chunk.runtime.SessionType; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -84,8 +85,8 @@ private Fixture compile(String app) throws Exception { source, """ package fixtures.%s; - import com.chunkzero.chunk.runtime.Session; - import com.chunkzero.chunk.runtime.SessionProvider; + import com.chunkzero.chunk.multistom.Session; + import com.chunkzero.chunk.multistom.SessionProvider; @com.chunkzero.chunk.runtime.SessionType("default") public final class Provider implements SessionProvider { public Session create() { @@ -146,7 +147,8 @@ private Path jar(String name, Map classes, String providers) var path = directory.resolve(name); try (var output = new JarOutputStream(Files.newOutputStream(path))) { output.putNextEntry( - new JarEntry("META-INF/services/com.chunkzero.chunk.runtime.SessionProvider")); + new JarEntry( + "META-INF/services/com.chunkzero.chunk.multistom.SessionProvider")); output.write(providers.getBytes(StandardCharsets.UTF_8)); output.closeEntry(); for (var entry : classes.entrySet()) { diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/ComponentRegistryTest.java similarity index 99% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/ComponentRegistryTest.java index e9190621..0fcbe79d 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/ComponentRegistryTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/ComponentRegistryTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static com.chunkzero.chunk.runtime.Component.Scope.PROCESS; import static com.chunkzero.chunk.runtime.Component.Scope.SESSION; @@ -6,7 +6,8 @@ import static org.junit.jupiter.api.Assertions.*; import com.chunkzero.chunk.backend.client.BackendSession; -import com.chunkzero.chunk.runtime.minestom.internal.ComponentRegistry; +import com.chunkzero.chunk.multistom.internal.ComponentRegistry; +import com.chunkzero.chunk.runtime.Component; import net.minestom.server.ServerProcess; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/FakeCore.java similarity index 97% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/FakeCore.java index b39783a8..86773a69 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/FakeCore.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/FakeCore.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static org.junit.jupiter.api.Assertions.fail; @@ -41,7 +41,7 @@ * the deliveries the JVM reports and removes each once it is CLOSED, and records each method's * first result and then removes the method. */ -final class FakeCore extends CoreGrpc.CoreImplBase implements AutoCloseable { +public final class FakeCore extends CoreGrpc.CoreImplBase implements AutoCloseable { private final Server server; private final AtomicReference link = new AtomicReference<>(); private final Map entries = new TreeMap<>(); @@ -71,12 +71,12 @@ void connect(ProcessState state) { } /** Wakes the link to report, as the engine's tick does. */ - void wake() { + public void wake() { var current = link.get(); if (current != null) current.wake(); } - void methodResult(String operation, JvmMethodResult result) { + public void methodResult(String operation, JvmMethodResult result) { link.get().methodResult(operation, result); } diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionCreationTest.java similarity index 86% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionCreationTest.java index b7411230..d5c040e7 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionCreationTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionCreationTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static org.junit.jupiter.api.Assertions.*; @@ -7,8 +7,10 @@ import chunk.sync.v1.Jvm.JvmSessionStatus; import com.chunkzero.chunk.backend.api.JsonType; -import com.chunkzero.chunk.runtime.bootstrap.FlatSession; -import com.chunkzero.chunk.runtime.minestom.event.SessionCreateEvent; +import com.chunkzero.chunk.multistom.bootstrap.FlatSession; +import com.chunkzero.chunk.multistom.event.SessionCreateEvent; +import com.chunkzero.chunk.runtime.ChunkSessions; +import com.chunkzero.chunk.runtime.TestHosts; import com.google.protobuf.ByteString; import net.minestom.server.ServerProcess; @@ -71,8 +73,8 @@ public Session create(SessionCreation creation) { try { var first = session(16, "{\"map\":\"forest\",\"mode\":\"solo\"}"); var second = session(32, "{\"map\":\"desert\",\"mode\":\"solo\"}"); - var firstResult = host.create("first", first); - var secondResult = host.create("second", second); + var firstResult = TestHosts.create(host, "first", first); + var secondResult = TestHosts.create(host, "second", second); flush(ticks); assertEquals(16, firstResult.join().getCapacity()); assertEquals(32, secondResult.join().getCapacity()); @@ -82,7 +84,8 @@ public Session create(SessionCreation creation) { new SessionCreation<>(32, new Config("desert", "solo"))), settings); var replay = - host.create( + TestHosts.create( + host, "first", first.toBuilder() .setConfigurationJson( @@ -92,7 +95,8 @@ public Session create(SessionCreation creation) { flush(ticks); assertEquals(firstResult.join(), replay.join()); var changed = - host.create( + TestHosts.create( + host, "first", first.toBuilder() .setConfigurationJson(second.getConfigurationJson()) @@ -108,12 +112,13 @@ public Session create(SessionCreation creation) { "{\"map\":\"forest\",\"mode\":\"solo\",\"extra\":true}", "{\"map\":\"" + "x".repeat(65_536) + "\",\"mode\":\"solo\"}"); for (var json : invalidValues) { - var invalid = host.create("invalid", session(16, json)); + var invalid = TestHosts.create(host, "invalid", session(16, json)); flush(ticks); assertTrue(invalid.isCompletedExceptionally()); } var invalidEncoding = - host.create( + TestHosts.create( + host, "invalid", session(16, "{}").toBuilder() .setConfigurationJson( @@ -123,11 +128,13 @@ public Session create(SessionCreation creation) { assertTrue(invalidEncoding.isCompletedExceptionally()); assertEquals(2, settings.size()); var legacy = - host.create( + TestHosts.create( + host, "legacy", session(16, "{}").toBuilder().setSessionType("arena/legacy").build()); var invalidLegacy = - host.create( + TestHosts.create( + host, "invalidLegacy", session(16, "{\"map\":\"forest\"}").toBuilder() .setSessionType("arena/legacy") @@ -135,9 +142,9 @@ public Session create(SessionCreation creation) { flush(ticks); legacy.join(); assertTrue(invalidLegacy.isCompletedExceptionally()); - host.finish("first", first); - host.finish("second", second); - host.finish("legacy", session(16, "{}")); + TestHosts.finish(host, "first", first); + TestHosts.finish(host, "second", second); + TestHosts.finish(host, "legacy", session(16, "{}")); flush(ticks); } finally { process.stop(); @@ -162,8 +169,8 @@ void failedCreationAndUnknownFinishAreReported() { Map.of()); var host = TestHosts.detached(manager); try { - var failed = host.create("failed", session(16, "{}")); - var unknown = host.finish("unknown", session(16, "{}")); + var failed = TestHosts.create(host, "failed", session(16, "{}")); + var unknown = TestHosts.finish(host, "unknown", session(16, "{}")); flush(ticks); assertTrue(failed.isCompletedExceptionally()); assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDED, unknown.join().getPhase()); @@ -225,16 +232,16 @@ public CompletionStage onCreate(SessionScope scope) { var host = TestHosts.detached(manager); try { var leaky = session(16, "{}").toBuilder().setSessionType("arena/leaky").build(); - host.create("failed", session(16, "{}")); - host.create("leaky", leaky); - host.finish("leaky", leaky); + TestHosts.create(host, "failed", session(16, "{}")); + TestHosts.create(host, "leaky", leaky); + TestHosts.finish(host, "leaky", leaky); for (var tick = 0; tick < 20; tick++) { ticks.flush(); if (tick == 10) cleanup.complete(null); var phases = phases(host); - if (ChunkSessions.terminal(phases.get("failed"))) + if (TestHosts.terminal(phases.get("failed"))) assertFalse(instances.getFirst().isRegistered()); - assertFalse(ChunkSessions.terminal(phases.get("leaky"))); + assertFalse(TestHosts.terminal(phases.get("leaky"))); } assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, phases(host).get("failed")); assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, phases(host).get("leaky")); @@ -270,22 +277,22 @@ public CompletionStage onCreate( var clock = new AtomicLong(); var host = TestHosts.withDeadline(manager, Duration.ofMillis(50), clock::get); try { - var created = host.create("slow", session(16, "{}")); + var created = TestHosts.create(host, "slow", session(16, "{}")); ticks.flush(); - host.sweep(); + TestHosts.sweep(host); assertFalse(created.isDone()); clock.addAndGet(Duration.ofMillis(50).toNanos()); - host.sweep(); + TestHosts.sweep(host); assertTrue(created.isDone()); var failure = assertThrows(CompletionException.class, created::join); assertInstanceOf(TimeoutException.class, failure.getCause()); flush(ticks); - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("slow")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, TestHosts.phase(host, "slow")); assertFalse(instances.getFirst().isRegistered()); assertFalse(creation.isDone()); assertTrue(creation.complete(null)); flush(ticks); - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("slow")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, TestHosts.phase(host, "slow")); } finally { process.stop(); } @@ -307,14 +314,14 @@ void creationCompletingAfterTheDeadlinePublishesNoCreateEvent() { process.eventHandler() .addListener(SessionCreateEvent.class, event -> events.add(event.getSession())); try { - var created = host.create("late", session(16, "{}")); + var created = TestHosts.create(host, "late", session(16, "{}")); // The creation has completed, but its tick task has not run yet. ticks.flush(); clock.addAndGet(Duration.ofMillis(50).toNanos()); - host.sweep(); + TestHosts.sweep(host); flush(ticks); assertThrows(CompletionException.class, created::join); - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, host.phase("late")); + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_FAILED, TestHosts.phase(host, "late")); assertTrue(events.isEmpty()); } finally { process.stop(); @@ -336,22 +343,22 @@ void forgottenSessionsLeaveRoomForNewOnes() { for (var index = 0; index < 300; index++) { var id = "session" + index; var session = session(16, "{}"); - var created = host.create(id, session); + var created = TestHosts.create(host, id, session); flush(ticks); created.join(); - host.finish(id, session); + TestHosts.finish(host, id, session); flush(ticks); - host.forget(); + TestHosts.forget(host); flush(ticks); } - assertTrue(host.inventory().getSessionsList().isEmpty()); + assertTrue(TestHosts.inventory(host).getSessionsList().isEmpty()); } finally { process.stop(); } } private static Map phases(ChunkSessions host) { - return host.inventory().getSessionsList().stream() + return TestHosts.inventory(host).getSessionsList().stream() .collect(Collectors.toMap(JvmSessionStatus::getId, JvmSessionStatus::getPhase)); } diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionMethodBindingTest.java similarity index 97% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionMethodBindingTest.java index 0dec8e4f..46c6e112 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodBindingTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionMethodBindingTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static org.junit.jupiter.api.Assertions.*; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionMethodTest.java similarity index 89% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionMethodTest.java index 5a09d872..a622022d 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SessionMethodTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SessionMethodTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static org.junit.jupiter.api.Assertions.*; @@ -7,6 +7,10 @@ import com.chunkzero.chunk.backend.api.BackendValues; import com.chunkzero.chunk.backend.api.JsonType; import com.chunkzero.chunk.backend.api.SessionMethodRef; +import com.chunkzero.chunk.runtime.SessionControl; +import com.chunkzero.chunk.runtime.SessionHandler; +import com.chunkzero.chunk.runtime.SessionMethod; +import com.chunkzero.chunk.runtime.TestHosts; import net.minestom.server.ServerProcess; @@ -55,7 +59,7 @@ public void finish(SessionControl session) { }); var spec = JvmSession.newBuilder().setSessionType("lobby/default").setCapacity(2).build(); try { - settle(ticks, host.create("session", spec)); + settle(ticks, TestHosts.create(host, "session", spec)); var control = controls.getFirst(); var result = manager.method(control, call("record", "{\"message\":\"first\"}")); settle(ticks, result.toCompletableFuture()); @@ -65,7 +69,7 @@ public void finish(SessionControl session) { manager.method(control, call("missing", "{}")) .toCompletableFuture() .isCompletedExceptionally()); - settle(ticks, host.finish("session", spec)); + settle(ticks, TestHosts.finish(host, "session", spec)); var late = manager.method(control, call("record", "{\"message\":\"late\"}")); ticks.flush(); assertTrue(late.toCompletableFuture().isCompletedExceptionally()); @@ -76,7 +80,7 @@ public void finish(SessionControl session) { } private static SessionMethod call(String name, String argumentsJson) { - return new SessionMethod(name, argumentsJson, null, () -> true); + return TestHosts.method(name, argumentsJson, null, () -> true); } private static void settle(TickExecutor ticks, CompletableFuture future) { diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SharedInstanceTest.java similarity index 97% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SharedInstanceTest.java index 9760f659..872ded70 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/SharedInstanceTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/SharedInstanceTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import static org.junit.jupiter.api.Assertions.*; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/TestScopes.java similarity index 92% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/TestScopes.java index 7d982b85..76a9a10b 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestScopes.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/TestScopes.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime; +package com.chunkzero.chunk.multistom; import net.minestom.server.ServerProcess; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/assets/AssetDirectoryTest.java similarity index 98% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/assets/AssetDirectoryTest.java index 9b1c42e4..9612a57a 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/AssetDirectoryTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/assets/AssetDirectoryTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime.assets; +package com.chunkzero.chunk.multistom.assets; import static org.junit.jupiter.api.Assertions.*; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/assets/WorldTest.java similarity index 96% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/assets/WorldTest.java index 4614c99d..0e8221d3 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/assets/WorldTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/assets/WorldTest.java @@ -1,9 +1,9 @@ -package com.chunkzero.chunk.runtime.assets; +package com.chunkzero.chunk.multistom.assets; import static org.junit.jupiter.api.Assertions.*; import com.chunkzero.chunk.backend.api.WorldAsset; -import com.chunkzero.chunk.runtime.TestScopes; +import com.chunkzero.chunk.multistom.TestScopes; import net.hollowcube.polar.PolarWorld; import net.hollowcube.polar.PolarWriter; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/bootstrap/FlatSession.java similarity index 81% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/bootstrap/FlatSession.java index 2598dd8d..6190d961 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/bootstrap/FlatSession.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/bootstrap/FlatSession.java @@ -1,7 +1,7 @@ -package com.chunkzero.chunk.runtime.bootstrap; +package com.chunkzero.chunk.multistom.bootstrap; -import com.chunkzero.chunk.runtime.Session; -import com.chunkzero.chunk.runtime.SessionScope; +import com.chunkzero.chunk.multistom.Session; +import com.chunkzero.chunk.multistom.SessionScope; import net.minestom.server.instance.LightingChunk; import net.minestom.server.instance.block.Block; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/internal/AnvilConverterTest.java similarity index 98% rename from jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java rename to jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/internal/AnvilConverterTest.java index eea088da..5f08aa3d 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/minestom/internal/AnvilConverterTest.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/multistom/internal/AnvilConverterTest.java @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime.minestom.internal; +package com.chunkzero.chunk.multistom.internal; import static org.junit.jupiter.api.Assertions.*; diff --git a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java index 12c63306..c1504597 100644 --- a/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java +++ b/jvm/multistom/src/test/java/com/chunkzero/chunk/runtime/TestHosts.java @@ -2,34 +2,91 @@ import chunk.sync.v1.CoreOuterClass.Position; import chunk.sync.v1.Jvm.JvmMethodResult; +import chunk.sync.v1.Jvm.JvmReport; +import chunk.sync.v1.Jvm.JvmSession; +import chunk.sync.v1.Jvm.JvmSessionPhase; +import chunk.sync.v1.Jvm.JvmSessionStatus; import com.chunkzero.chunk.backend.api.Destination; import com.chunkzero.chunk.backend.client.BackendSession; +import com.chunkzero.chunk.multistom.FakeCore; +import com.chunkzero.chunk.runtime.control.ProcessState; +import com.google.protobuf.ByteString; + +import org.jetbrains.annotations.Nullable; import java.time.Duration; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BooleanSupplier; import java.util.function.LongSupplier; -/** Session hosts for tests, whose handler callbacks run on the calling thread. */ -final class TestHosts { +/** + * Test bridge to the package-private parts of {@link ChunkSessions}. Its hosts run handler + * callbacks on the calling thread. + */ +public final class TestHosts { private TestHosts() {} - static ChunkSessions detached(SessionHandler handler) { + public static ChunkSessions detached(SessionHandler handler) { return ChunkSessions.detached(handler, session -> null); } - static ChunkSessions withDeadline( + public static ChunkSessions withDeadline( SessionHandler handler, Duration deadline, LongSupplier nanos) { return new ChunkSessions(handler, link(null, nanos), Runnable::run, deadline, null); } /** Reports to {@code core}, and expires deliveries by {@code nanos}. */ - static ChunkSessions linked(SessionHandler handler, FakeCore core, LongSupplier nanos) { + public static ChunkSessions linked(SessionHandler handler, FakeCore core, LongSupplier nanos) { return new ChunkSessions( handler, link(core, nanos), Runnable::run, ChunkSessions.CREATE_DEADLINE, null); } + public static void sweep(ChunkSessions host) { + host.sweep(); + } + + public static void apply(ChunkSessions host, Map entries) { + host.apply(entries); + } + + public static JvmReport inventory(ChunkSessions host) { + return host.inventory(); + } + + public static ProcessState state(ChunkSessions host) { + return host.state(); + } + + public static void forget(ChunkSessions host) { + host.forget(); + } + + public static CompletableFuture create( + ChunkSessions host, String id, JvmSession session) { + return host.create(id, session); + } + + public static CompletableFuture finish( + ChunkSessions host, String id, JvmSession session) { + return host.finish(id, session); + } + + public static @Nullable JvmSessionPhase phase(ChunkSessions host, String id) { + return host.phase(id); + } + + public static boolean terminal(JvmSessionPhase phase) { + return ChunkSessions.terminal(phase); + } + + public static SessionMethod method( + String name, String argumentsJson, Delivery delivery, BooleanSupplier start) { + return new SessionMethod(name, argumentsJson, delivery, start); + } + private static ChunkSessions.Link link(FakeCore core, LongSupplier nanos) { return new ChunkSessions.Link() { @Override diff --git a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/GameplayLifecycleTest.kt similarity index 89% rename from jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/GameplayLifecycleTest.kt index f3549868..f3491e89 100644 --- a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayLifecycleTest.kt +++ b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/GameplayLifecycleTest.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import chunk.sync.v1.CoreOuterClass.Position import chunk.sync.v1.Gateway.PlayerIdentity @@ -6,8 +6,9 @@ import chunk.sync.v1.Jvm.JvmDelivery import chunk.sync.v1.Jvm.JvmDeliveryPhase import chunk.sync.v1.Jvm.JvmSession import chunk.sync.v1.Jvm.PlayerSetup -import com.chunkzero.chunk.runtime.bootstrap.FlatSession -import com.chunkzero.chunk.runtime.minestom.internal.GameplayService +import com.chunkzero.chunk.multistom.bootstrap.FlatSession +import com.chunkzero.chunk.multistom.internal.GameplayService +import com.chunkzero.chunk.runtime.TestHosts import net.minestom.server.ServerProcess import net.minestom.server.entity.Player import net.minestom.server.network.ConnectionState @@ -98,7 +99,7 @@ class GameplayLifecycleTest { .buildTask { ticks.flush() service.flush() - host.sweep() + TestHosts.sweep(host) }.repeat( net.minestom.server.timer.TaskSchedule .tick(1), @@ -111,8 +112,8 @@ class GameplayLifecycleTest { .setSessionType(type) .setCapacity(2) .build() - host.create("a", session()).get(3, TimeUnit.SECONDS) - host.create("b", session()).get(3, TimeUnit.SECONDS) + TestHosts.create(host, "a", session()).get(3, TimeUnit.SECONDS) + TestHosts.create(host, "b", session()).get(3, TimeUnit.SECONDS) val uuid = UUID.randomUUID().toString() val wanted = mutableMapOf() @@ -126,9 +127,14 @@ class GameplayLifecycleTest { .setPlayer(PlayerIdentity.newBuilder().setUuid(uuid).setUsername("test")) .build() - fun apply() = host.apply(wanted.mapKeys { "delivery/${it.key}" }.mapValues { it.value.toByteString() }) + fun apply() = + TestHosts.apply(host, wanted.mapKeys { "delivery/${it.key}" }.mapValues { it.value.toByteString() }) - fun status(operation: String) = host.inventory().deliveriesList.single { it.operationId == operation } + fun status(operation: String) = + TestHosts.inventory(host).deliveriesList.single { + it.operationId == + operation + } fun phase(operation: String) = status(operation).phase @@ -192,7 +198,7 @@ class GameplayLifecycleTest { assertTrue(oldPlayer in closedPlayers) assertTrue(minecraft.connectionManager().onlinePlayers.isEmpty()) arrive(connect("next", delivery("b", 3)), "next") - host.finish("a", session()).get(3, TimeUnit.SECONDS) + TestHosts.finish(host, "a", session()).get(3, TimeUnit.SECONDS) val current = minecraft.connectionManager().onlinePlayers.single() assertEquals(uuid, current.uuid.toString()) assertTrue( @@ -205,7 +211,7 @@ class GameplayLifecycleTest { assertEquals(setOf(oldPlayer), closedPlayers) withdraw("next") await("next", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED) - host.create("c", session("gated")).get(3, TimeUnit.SECONDS) + TestHosts.create(host, "c", session("gated")).get(3, TimeUnit.SECONDS) connect("pending", delivery("c", 4)) joinStarted.get(3, TimeUnit.SECONDS) assertTrue(phase("pending") != JvmDeliveryPhase.JVM_DELIVERY_PHASE_ARRIVED) @@ -221,8 +227,8 @@ class GameplayLifecycleTest { arrive(connect("replacement", delivery("c", 6)), "replacement") withdraw("replacement") await("replacement", JvmDeliveryPhase.JVM_DELIVERY_PHASE_CLOSED) - host.finish("c", session("gated")).get(3, TimeUnit.SECONDS) - host.create("d", session("failing")).get(3, TimeUnit.SECONDS) + TestHosts.finish(host, "c", session("gated")).get(3, TimeUnit.SECONDS) + TestHosts.create(host, "d", session("failing")).get(3, TimeUnit.SECONDS) arrive(connect("broken", delivery("d", 7)), "broken") withdraw("broken") closeFailed.get(3, TimeUnit.SECONDS) @@ -233,7 +239,7 @@ class GameplayLifecycleTest { "A failed player cleanup keeps the delivery unreleased", ) assertThrows(IllegalStateException::class.java) { connect("blocked", delivery("d", 8)) } - host.finish("b", session()).get(3, TimeUnit.SECONDS) + TestHosts.finish(host, "b", session()).get(3, TimeUnit.SECONDS) } finally { sockets.forEach { it.close() } service.close() diff --git a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/GameplayServiceTest.kt similarity index 94% rename from jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/GameplayServiceTest.kt index 0ccb7a28..f683d9a8 100644 --- a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/GameplayServiceTest.kt +++ b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/GameplayServiceTest.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import chunk.sync.v1.CoreOuterClass.Position import chunk.sync.v1.Gateway.PlayerIdentity @@ -8,8 +8,9 @@ import chunk.sync.v1.Jvm.JvmDeliveryPhase import chunk.sync.v1.Jvm.JvmSession import chunk.sync.v1.Jvm.JvmSessionPhase import chunk.sync.v1.Jvm.PlayerSetup -import com.chunkzero.chunk.runtime.bootstrap.FlatSession -import com.chunkzero.chunk.runtime.minestom.internal.GameplayService +import com.chunkzero.chunk.multistom.bootstrap.FlatSession +import com.chunkzero.chunk.multistom.internal.GameplayService +import com.chunkzero.chunk.runtime.TestHosts import com.google.protobuf.ByteString import net.minestom.server.ServerProcess import net.minestom.server.network.ConnectionState @@ -43,14 +44,14 @@ class GameplayServiceTest { .buildTask { ticks.flush() gameplay.flush() - host.sweep() + TestHosts.sweep(host) }.repeat(TaskSchedule.tick(1)) .schedule() minecraft.start(InetSocketAddress("127.0.0.1", 0)) val port = minecraft.server().port val sockets = mutableListOf() try { - core.connect(host.state()) + core.connect(TestHosts.state(host)) core.put( "session/bridge", JvmSession @@ -60,7 +61,7 @@ class GameplayServiceTest { .build(), ) val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10) - while (host.phase("bridge") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { + while (TestHosts.phase(host, "bridge") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { check(System.nanoTime() < deadline) { "The session never became ready" } Thread.sleep(10) } diff --git a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionEventsTest.kt similarity index 90% rename from jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionEventsTest.kt index a4d95757..b79792b2 100644 --- a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionEventsTest.kt +++ b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionEventsTest.kt @@ -1,12 +1,14 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import chunk.sync.v1.Jvm.JvmSession import chunk.sync.v1.Jvm.JvmSessionPhase -import com.chunkzero.chunk.runtime.minestom.event.SessionCreateEvent -import com.chunkzero.chunk.runtime.minestom.event.SessionDestroyEvent -import com.chunkzero.chunk.runtime.minestom.event.SessionEvent -import com.chunkzero.chunk.runtime.minestom.event.SessionJoinEvent -import com.chunkzero.chunk.runtime.minestom.event.SessionLeaveEvent +import com.chunkzero.chunk.multistom.event.SessionCreateEvent +import com.chunkzero.chunk.multistom.event.SessionDestroyEvent +import com.chunkzero.chunk.multistom.event.SessionEvent +import com.chunkzero.chunk.multistom.event.SessionJoinEvent +import com.chunkzero.chunk.multistom.event.SessionLeaveEvent +import com.chunkzero.chunk.runtime.ChunkSessions +import com.chunkzero.chunk.runtime.TestHosts import net.minestom.server.ServerProcess import net.minestom.server.coordinate.Pos import net.minestom.server.entity.Entity @@ -98,7 +100,7 @@ class SessionEventsTest { }, ) val host = TestHosts.detached(manager) - val create = host.create("first", game) + val create = TestHosts.create(host, "first", game) ticks.flush() assertTrue(global.isEmpty()) completeOffThread(creating) @@ -119,7 +121,7 @@ class SessionEventsTest { }, ) val otherHost = TestHosts.detached(otherManager) - await(otherHost.create("second", game)) + await(TestHosts.create(otherHost, "second", game)) val player = player() val join = session.join(player) ticks.flush() @@ -147,7 +149,7 @@ class SessionEventsTest { assertEquals(1, playerTicks) await(session.leave(player)) - val finish = host.finish("first", game) + val finish = TestHosts.finish(host, "first", game) repeat(4) { ticks.flush() } assertFalse(disposed) completeOffThread(finishing) @@ -164,9 +166,9 @@ class SessionEventsTest { assertEquals(local, global.filter { it.session === scope }) assertEquals(1, other.size) assertFalse(process.eventHandler().children.contains(scope.events)) - await(host.finish("first", game)) + await(TestHosts.finish(host, "first", game)) assertEquals(4, local.size) - await(otherHost.finish("second", game)) + await(TestHosts.finish(otherHost, "second", game)) } @Test @@ -184,13 +186,13 @@ class SessionEventsTest { }, ) val host = TestHosts.detached(manager) - await(host.create("first", game)) - await(host.create("first", game)) + await(TestHosts.create(host, "first", game)) + await(TestHosts.create(host, "first", game)) val session = manager.get("first") val player = player() assertThrows(CompletionException::class.java) { await(session.join(player)) } await(session.leave(player)) - await(host.finish("first", game)) + await(TestHosts.finish(host, "first", game)) assertEquals( listOf(SessionCreateEvent::class.java, SessionDestroyEvent::class.java), global.map { it.javaClass }, @@ -214,8 +216,8 @@ class SessionEventsTest { }, ) val failedHost = TestHosts.detached(failed) - assertThrows(CompletionException::class.java) { await(failedHost.create("failed", game)) } - assertThrows(CompletionException::class.java) { await(failedHost.finish("failed", game)) } + assertThrows(CompletionException::class.java) { await(TestHosts.create(failedHost, "failed", game)) } + assertThrows(CompletionException::class.java) { await(TestHosts.finish(failedHost, "failed", game)) } assertEquals(1, global.count { it.session.id == "failed" && it is SessionDestroyEvent }) assertFalse(global.any { it.session.id == "failed" && it is SessionCreateEvent }) process.schedulerManager().processTick() @@ -240,8 +242,8 @@ class SessionEventsTest { }, ) val host = TestHosts.detached(manager) - await(host.create("first", game)) - await(host.create("second", game)) + await(TestHosts.create(host, "first", game)) + await(TestHosts.create(host, "second", game)) val (first, second) = scopes val firstEvents = mutableListOf() val secondEvents = mutableListOf() @@ -276,14 +278,14 @@ class SessionEventsTest { schedulers.processTickEnd() assertEquals(1 to 1, starts to ends) - await(host.finish("first", game)) + await(TestHosts.finish(host, "first", game)) schedulers.processTick() schedulers.processTickEnd() assertEquals(1 to 1, starts to ends) assertTrue(entity.isRemoved) assertFalse(outsider.isRemoved) assertEquals(setOf(second.instances.single(), outsider.instance), process.instanceManager().instances) - await(host.finish("second", game)) + await(TestHosts.finish(host, "second", game)) } @Test @@ -303,9 +305,9 @@ class SessionEventsTest { }, ) val host = TestHosts.detached(manager) - await(host.create("owner", game)) + await(TestHosts.create(host, "owner", game)) assertEquals(1, registered) - assertThrows(Throwable::class.java) { await(host.finish("owner", game)) } + assertThrows(Throwable::class.java) { await(TestHosts.finish(host, "owner", game)) } assertTrue(process.instanceManager().instances.isEmpty()) assertFalse(process.eventHandler().children.contains(scope.events)) assertEquals(1, global.count { it is SessionDestroyEvent }) @@ -325,7 +327,7 @@ class SessionEventsTest { }, ) val host = TestHosts.detached(manager) - await(host.create("native", game)) + await(TestHosts.create(host, "native", game)) val owned = CompletableFuture() scope.events.addListener(EntityTickEvent::class.java) { if (!owned.isDone) { @@ -335,7 +337,7 @@ class SessionEventsTest { } repeat(20) { if (!owned.isDone) process.ticker().tick(System.nanoTime()) } assertSame(process.dispatcher().threads().single(), owned.join()) - await(host.finish("native", game)) + await(TestHosts.finish(host, "native", game)) } @Test @@ -360,7 +362,7 @@ class SessionEventsTest { }, ) val host = TestHosts.detached(manager) - await(host.create("first", game)) + await(TestHosts.create(host, "first", game)) val session = manager.get("first") val player = player() await(session.join(player)) @@ -393,10 +395,10 @@ class SessionEventsTest { host: ChunkSessions, id: String, ) { - val finish = host.finish(id, game) + val finish = TestHosts.finish(host, id, game) repeat(20) { ticks.flush() } assertFalse(finish.isDone) - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, host.phase(id)) + assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_ENDING, TestHosts.phase(host, id)) } private fun manager(session: Session) = SessionManager(process, ticks, mapOf("game" to Supplier { session })) diff --git a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionManagerTest.kt similarity index 86% rename from jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionManagerTest.kt index 9a893e87..ea74c544 100644 --- a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionManagerTest.kt +++ b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionManagerTest.kt @@ -1,9 +1,10 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import chunk.sync.v1.Jvm.JvmSession import chunk.sync.v1.Jvm.JvmSessionPhase -import com.chunkzero.chunk.runtime.bootstrap.FlatSession -import com.chunkzero.chunk.runtime.minestom.event.SessionCreateEvent +import com.chunkzero.chunk.multistom.bootstrap.FlatSession +import com.chunkzero.chunk.multistom.event.SessionCreateEvent +import com.chunkzero.chunk.runtime.TestHosts import net.minestom.server.ServerProcess import net.minestom.server.entity.Player import net.minestom.server.network.packet.server.SendablePacket @@ -35,7 +36,10 @@ class SessionManagerTest { second.eventHandler().addListener(SessionCreateEvent::class.java) { secondEvents.add(it.session) } val session = session("flat") val creations = - listOf(firstHost.create("same-id", session), secondHost.create("same-id", session)) + listOf( + TestHosts.create(firstHost, "same-id", session), + TestHosts.create(secondHost, "same-id", session), + ) repeat(4) { ticks.flush() } creations.forEach { it.join() } val firstScope = firstManager.get("same-id").scope @@ -44,7 +48,7 @@ class SessionManagerTest { assertEquals(listOf(secondScope), secondEvents) assertSame(first, firstScope.process) assertSame(second, secondScope.instances.single().process()) - val ended = firstHost.finish("same-id", session) + val ended = TestHosts.finish(firstHost, "same-id", session) repeat(8) { ticks.flush() } ended.join() first.close() @@ -53,7 +57,7 @@ class SessionManagerTest { second.schedulerManager().process() assertTrue(ran) assertEquals(secondScope.instances.toSet(), second.instanceManager().instances) - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, secondManager.get("same-id").phase) + secondManager.get("same-id") } } } @@ -85,7 +89,7 @@ class SessionManagerTest { val game = session("game") val host = TestHosts.detached(manager) try { - val created = host.create("game", game) + val created = TestHosts.create(host, "game", game) repeat(4) { ticks.flush() } created.join() val session = manager.get("game") @@ -109,7 +113,7 @@ class SessionManagerTest { left.complete(null) ticks.flush() assertTrue(leavingResult.isCompletedExceptionally) - val ended = host.finish("game", game) + val ended = TestHosts.finish(host, "game", game) repeat(8) { ticks.flush() } ended.join() } finally { @@ -161,8 +165,8 @@ class SessionManagerTest { val first = session("delayed") val second = session("flat").toBuilder().setCapacity(32).build() assertEquals(0, host.activeCount()) - val pending = host.create("first", first) - val independent = host.create("second", second) + val pending = TestHosts.create(host, "first", first) + val independent = TestHosts.create(host, "second", second) repeat(4) { ticks.flush() } assertFalse(pending.isDone) assertEquals(2, host.activeCount()) @@ -173,13 +177,13 @@ class SessionManagerTest { repeat(4) { ticks.flush() } assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, pending.join().phase) assertEquals(2, host.activeCount()) - val duplicate = host.create("first", first) + val duplicate = TestHosts.create(host, "first", first) repeat(2) { ticks.flush() } assertEquals(pending.join(), duplicate.join()) - val changed = host.create("first", first.toBuilder().setCapacity(3).build()) + val changed = TestHosts.create(host, "first", first.toBuilder().setCapacity(3).build()) ticks.flush() assertTrue(changed.isCompletedExceptionally) - val ending = host.finish("first", first) + val ending = TestHosts.finish(host, "first", first) repeat(5) { ticks.flush() } assertFalse(ending.isDone) assertEquals(2, host.activeCount()) @@ -191,27 +195,27 @@ class SessionManagerTest { assertEquals(1, disposed) assertEquals(1, process.instanceManager().instances.size) assertFalse(process.eventHandler().children.any { it.name == ownedScope.events.name }) - assertEquals(JvmSessionPhase.JVM_SESSION_PHASE_READY, manager.get("second").phase) + manager.get("second") val staleTask = ownedScope.onTick { error("Disposed task ran") } ticks.flush() assertTrue(staleTask.isCompletedExceptionally) - val stopSecond = host.finish("second", second) + val stopSecond = TestHosts.finish(host, "second", second) repeat(8) { ticks.flush() } assertTrue(stopSecond.isDone) assertEquals(0, host.activeCount()) - val failed = host.create("failed", session("failed")) + val failed = TestHosts.create(host, "failed", session("failed")) repeat(8) { ticks.flush() } assertTrue(failed.isCompletedExceptionally) assertEquals( JvmSessionPhase.JVM_SESSION_PHASE_FAILED, - host - .inventory() + TestHosts + .inventory(host) .sessionsList .single { it.id == "failed" } .phase, ) assertEquals(0, host.activeCount()) - assertEquals(3, host.inventory().sessionsList.size) + assertEquals(3, TestHosts.inventory(host).sessionsList.size) } finally { process.stop() } diff --git a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionMethodDisconnectTest.kt similarity index 93% rename from jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionMethodDisconnectTest.kt index d19328d4..6db19da7 100644 --- a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/SessionMethodDisconnectTest.kt +++ b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/SessionMethodDisconnectTest.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import chunk.sync.v1.CoreOuterClass.Position import chunk.sync.v1.Gateway.PlayerIdentity @@ -12,8 +12,9 @@ import chunk.sync.v1.Jvm.PlayerSetup import com.chunkzero.chunk.backend.api.BackendValues import com.chunkzero.chunk.backend.api.JsonType import com.chunkzero.chunk.backend.api.SessionMethodRef -import com.chunkzero.chunk.runtime.bootstrap.FlatSession -import com.chunkzero.chunk.runtime.minestom.internal.GameplayService +import com.chunkzero.chunk.multistom.bootstrap.FlatSession +import com.chunkzero.chunk.multistom.internal.GameplayService +import com.chunkzero.chunk.runtime.TestHosts import com.google.protobuf.ByteString import net.minestom.server.ServerProcess import net.minestom.server.network.ConnectionState @@ -61,13 +62,13 @@ class SessionMethodDisconnectTest { .buildTask { ticks.flush() if (!paused.get()) gameplay.flush() - host.sweep() + TestHosts.sweep(host) }.repeat(TaskSchedule.tick(1)) .schedule() minecraft.start(InetSocketAddress("127.0.0.1", 0)) val socket = try { - core.connect(host.state()) + core.connect(TestHosts.state(host)) core.put( "session/lobby", JvmSession @@ -77,7 +78,7 @@ class SessionMethodDisconnectTest { .build(), ) val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10) - while (host.phase("lobby") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { + while (TestHosts.phase(host, "lobby") != JvmSessionPhase.JVM_SESSION_PHASE_READY) { check(System.nanoTime() < deadline) { "The session never became ready" } Thread.sleep(10) } diff --git a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/TestClient.kt similarity index 99% rename from jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt rename to jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/TestClient.kt index e03efcc2..18862142 100644 --- a/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/runtime/TestClient.kt +++ b/jvm/multistom/src/test/kotlin/com/chunkzero/chunk/multistom/TestClient.kt @@ -1,4 +1,4 @@ -package com.chunkzero.chunk.runtime +package com.chunkzero.chunk.multistom import chunk.sync.v1.Jvm.PlayerSetup import net.minestom.server.MinecraftConstants diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java index 51176609..e740ab9c 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/ChunkProcess.java @@ -68,7 +68,8 @@ public void awaitShutdown() throws InterruptedException { } } - String app() { + /** The ID of the app this process runs. */ + public String app() { return environment.appId(); } diff --git a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java index 36eaafea..e9fd4c37 100644 --- a/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java +++ b/jvm/runtime/src/main/java/com/chunkzero/chunk/runtime/SessionControl.java @@ -107,9 +107,12 @@ public void ended(Throwable error) { owner.ended(this, error); } - JvmSessionPhase phase() { + /** + * Whether the session is ready: {@link #ready()} took effect and it has not started finishing. + */ + public boolean isReady() { synchronized (owner) { - return phase; + return phase == JvmSessionPhase.JVM_SESSION_PHASE_READY; } } diff --git a/scripts/check-consumers.py b/scripts/check-consumers.py index 8e7f5974..688ea767 100755 --- a/scripts/check-consumers.py +++ b/scripts/check-consumers.py @@ -17,7 +17,7 @@ "gradlew", "gradlew.bat", "gradle", "gradle.properties", "settings.gradle.kts", "build.gradle.kts", "buildSrc", "jvm", "proto", "examples/arena", "examples/local", ) -PROVIDER = "META-INF/services/com.chunkzero.chunk.runtime.SessionProvider" +PROVIDER = "META-INF/services/com.chunkzero.chunk.multistom.SessionProvider" def require(condition, message): @@ -90,7 +90,7 @@ def verify_release(project, package, app_ids, kotlin): require(len(providers) == len(app["sessions"]), "Session registry differs from deployment capabilities") for provider in providers: require(provider.replace(".", "/") + ".class" in names, "Session factory missing") - for entry in ("com/chunkzero/chunk/runtime/ChunkProcess.class", "com/chunkzero/chunk/runtime/ChunkMinestom.class", + for entry in ("com/chunkzero/chunk/runtime/ChunkProcess.class", "com/chunkzero/chunk/multistom/ChunkMinestom.class", "net/minestom/server/ServerProcess.class", package.replace(".", "/") + "/BackendTypes.class", package.replace(".", "/") + "/BackendClient.class"): require(entry in names, f"App executable missing {entry}")