From 2c5ad100b7752ee1da8c2eacc195d54296373536 Mon Sep 17 00:00:00 2001 From: Mqxx <62719703+Mqxx@users.noreply.github.com> Date: Thu, 21 May 2026 10:35:09 +0200 Subject: [PATCH 1/7] feat: generic exceptions --- .../exceptions/EntityNotFoundException.java | 14 ++++++++++++++ .../exceptions/EntityNotLivingException.java | 15 +++++++++++++++ .../exceptions/EntityNotPlayerException.java | 15 +++++++++++++++ .../exceptions/InvalidParamsException.java | 17 +++++++++++++++++ .../msmp/exceptions/InvalidUUIDException.java | 14 ++++++++++++++ .../msmp/exceptions/MSMPException.java | 19 +++++++++++++++++++ .../exceptions/MissingAttributeException.java | 16 ++++++++++++++++ .../MissingIdentifierException.java | 12 ++++++++++++ .../exceptions/UnknownDimensionException.java | 14 ++++++++++++++ 9 files changed, 136 insertions(+) create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotFoundException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotLivingException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotPlayerException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidParamsException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidUUIDException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MSMPException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingAttributeException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingIdentifierException.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/UnknownDimensionException.java diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotFoundException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotFoundException.java new file mode 100644 index 0000000..d59fab5 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotFoundException.java @@ -0,0 +1,14 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when an entity cannot be found by the given UUID or player name. + */ +public class EntityNotFoundException extends MSMPException { + + /** + * @param identifier The UUID or name that was used for the lookup + */ + public EntityNotFoundException(String identifier) { + super("Entity not found: " + identifier); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotLivingException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotLivingException.java new file mode 100644 index 0000000..b7819cd --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotLivingException.java @@ -0,0 +1,15 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when a method requires a {@link net.minecraft.world.entity.LivingEntity} + * but the resolved entity is not one. + */ +public class EntityNotLivingException extends MSMPException { + + /** + * @param uuid The UUID of the entity that was found but is not a LivingEntity + */ + public EntityNotLivingException(String uuid) { + super("Entity is not a LivingEntity: " + uuid); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotPlayerException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotPlayerException.java new file mode 100644 index 0000000..ecdf301 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/EntityNotPlayerException.java @@ -0,0 +1,15 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when a method requires a {@link net.minecraft.world.entity.player.Player} + * but the resolved entity is not one. + */ +public class EntityNotPlayerException extends MSMPException { + + /** + * @param uuid The UUID of the entity that was found but is not a Player + */ + public EntityNotPlayerException(String uuid) { + super("Entity is not a Player: " + uuid); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidParamsException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidParamsException.java new file mode 100644 index 0000000..31e3026 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidParamsException.java @@ -0,0 +1,17 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when a method receives invalid or incomplete parameters. + * + *
Used for method-specific validation errors, such as missing required + * fields or out-of-range values.
+ */ +public class InvalidParamsException extends MSMPException { + + /** + * @param message A description of which parameter is invalid and why + */ + public InvalidParamsException(String message) { + super(message); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidUUIDException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidUUIDException.java new file mode 100644 index 0000000..480b75d --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/InvalidUUIDException.java @@ -0,0 +1,14 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when a provided UUID string cannot be parsed. + */ +public class InvalidUUIDException extends MSMPException { + + /** + * @param raw The malformed UUID string + */ + public InvalidUUIDException(String raw) { + super("Invalid UUID: " + raw); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MSMPException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MSMPException.java new file mode 100644 index 0000000..03e2b0c --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MSMPException.java @@ -0,0 +1,19 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Base exception for all MSMP entity data errors. + * + *All method-specific exceptions extend this class, allowing handlers + * to catch all MSMP errors with a single {@code catch (MSMPException e)} block.
+ */ +public class MSMPException extends RuntimeException { + + /** + * Creates a new {@link MSMPException} with the given message. + * + * @param message A human-readable description of the error + */ + public MSMPException(String message) { + super(message); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingAttributeException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingAttributeException.java new file mode 100644 index 0000000..3bd1223 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingAttributeException.java @@ -0,0 +1,16 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when an entity is missing an expected attribute, + * such as {@link net.minecraft.world.entity.ai.attributes.Attributes#MAX_HEALTH}. + */ +public class MissingAttributeException extends MSMPException { + + /** + * @param uuid The UUID of the entity + * @param attribute The name of the missing attribute + */ + public MissingAttributeException(String uuid, String attribute) { + super("Entity %s has no %s attribute".formatted(uuid, attribute)); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingIdentifierException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingIdentifierException.java new file mode 100644 index 0000000..9ce66b3 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/MissingIdentifierException.java @@ -0,0 +1,12 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when a request provides neither {@code id} nor {@code name} + * for entity lookup. + */ +public class MissingIdentifierException extends MSMPException { + + public MissingIdentifierException() { + super("Either 'id' or 'name' must be provided"); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/UnknownDimensionException.java b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/UnknownDimensionException.java new file mode 100644 index 0000000..5ff4032 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/exceptions/UnknownDimensionException.java @@ -0,0 +1,14 @@ +package dev.loat.msmp_entity_data.msmp.exceptions; + +/** + * Thrown when a requested dimension identifier is not registered on the server. + */ +public class UnknownDimensionException extends MSMPException { + + /** + * @param dimension The dimension identifier that could not be resolved + */ + public UnknownDimensionException(String dimension) { + super("Unknown dimension: " + dimension); + } +} From 655162e1973cbb0b209b9de239a609310d272a58 Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Sat, 23 May 2026 17:14:24 +0200 Subject: [PATCH 2/7] feat: subscription system --- build.gradle | 2 +- .../msmp/subscription/SubscriptionEvent.java | 45 +++++++ .../subscription/SubscriptionManager.java | 112 ++++++++++++++++++ 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionEvent.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionManager.java diff --git a/build.gradle b/build.gradle index 712caba..82bcdcc 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ dependencies { implementation(include("org.yaml:snakeyaml:2.2")) - implementation("com.github.MinecraftPlayground:msmp-lib-mod:v1.3.0") + implementation("com.github.MinecraftPlayground:msmp-lib-mod:v1.4.1") } processResources { diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionEvent.java b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionEvent.java new file mode 100644 index 0000000..cb97b0d --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionEvent.java @@ -0,0 +1,45 @@ +package dev.loat.msmp_entity_data.msmp.subscription; + + +/** + * Enum of all subscribable entity data events. + * + *Used by clients to specify which events they want to receive + * notifications for via {@code entity_data:subscribe}.
+ */ +public enum SubscriptionEvent { + + /** Fired when a tracked entity changes dimension. */ + DIMENSION_CHANGED, + + /** Fired when a tracked entity's health changes. */ + HEALTH_CHANGED, + + /** Fired when a tracked player dies. */ + DEATH, + + /** Fired when a tracked player respawns. */ + RESPAWN; + + /** + * Parses a {@link SubscriptionEvent} from its lowercase string representation. + * + * @param value The string value (e.g. {@code "dimension_changed"}) + * @return The matching {@link SubscriptionEvent} + * @throws IllegalArgumentException if the value does not match any event + */ + public static SubscriptionEvent fromString(String value) { + return valueOf(value.toUpperCase()); + } + + /** + * Returns the lowercase string representation of this event, + * as used in the MSMP protocol. + * + * @return The lowercase event name (e.g. {@code "dimension_changed"}) + */ + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionManager.java b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionManager.java new file mode 100644 index 0000000..aa819da --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionManager.java @@ -0,0 +1,112 @@ +package dev.loat.msmp_entity_data.msmp.subscription; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Manages per-connection entity event subscriptions. + * + *Holds a mapping of {@code connectionId -> event -> Set
A wildcard UUID ({@link #WILDCARD}) can be used to subscribe to an event + * for all entities (e.g. all players).
+ */ +public class SubscriptionManager { + + /** + * Special UUID sentinel meaning "all entities". + * Used when a client subscribes to an event without specifying entity IDs. + */ + public static final UUID WILDCARD = new UUID(0, 0); + + /** connectionId -> event -> SetInitializes the {@code entity_data} MSMP namespace, registers all methods + * and notifications, and manages the server lifecycle binding.
+ */ public class MSMPEntityData implements ModInitializer { + /** + * The shared {@code entity_data} namespace used for all MSMP registrations. + * Attached to the running server in {@code SERVER_STARTED} and detached in {@code SERVER_STOPPED}. + */ private static final MSMPNamespace NS = new MSMPNamespace("entity_data"); + + /** + * Provides access to the {@link net.minecraft.server.jsonrpc.ManagementServer} + * for broadcasting notifications. {@code null} when no server is running. + */ private static MSMPServer msmp; + /** + * Called by Fabric during mod initialization, before the server starts. + * + *Registers all MSMP methods and notifications and sets up server + * lifecycle hooks for attaching and detaching the namespace.
+ */ @Override public void onInitialize() { Logger.setLoggerClass(MSMPEntityData.class); Methods.register(NS); + Notifications.register(NS, () -> msmp); ServerLifecycleEvents.SERVER_STARTED.register(server -> { NS.attach(server); diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/Notifications.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/Notifications.java new file mode 100644 index 0000000..7cf7a31 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/Notifications.java @@ -0,0 +1,37 @@ +package dev.loat.msmp_entity_data.msmp.notifications; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp.MSMPServer; +import dev.loat.msmp_entity_data.msmp.notifications.dimension.changed.DimensionChanged; +import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; + +/** + * Central registration point for all {@code entity_data} MSMP notifications. + * + *Each notification is implemented in its own sub-package and registered here. + * Call {@link #register(MSMPNamespace, DimensionChanged.MSMPServerSupplier)} once + * during mod initialization, before the server starts.
+ * + *Registered notifications:
+ *Fires when any entity changes dimension. Only sent to connections that + * have subscribed via {@code entity_data:notification/dimension/changed/subscribe}.
+ * + *Example notification payload:
+ *{@code
+ * {
+ * "jsonrpc": "2.0",
+ * "method": "entity_data:notification/dimension/changed",
+ * "params": [{
+ * "entity": { "id": "069a...", "name": "Steve" },
+ * "from": "minecraft:overworld",
+ * "to": "minecraft:the_nether"
+ * }]
+ * }
+ * }
+ */
+public class DimensionChanged {
+
+ /**
+ * Registers the notification, subscribe and unsubscribe methods, and
+ * hooks into the Fabric {@link ServerEntityWorldChangeEvents#AFTER_ENTITY_CHANGE_WORLD} event.
+ *
+ * @param namespace The namespace to register under
+ * @param msmpServer Supplier of the current {@link MSMPServer} instance
+ * @param subscriptionManager The {@link SubscriptionManager} for this notification
+ */
+ public static void register(
+ MSMPNamespace namespace,
+ MSMPServerSupplier msmpServer,
+ SubscriptionManager subscriptionManager
+ ) {
+ MSMPNotificationExample JSON representation:
+ *{@code
+ * {
+ * "entity": { "id": "069a...", "name": "Steve" },
+ * "from": "minecraft:overworld",
+ * "to": "minecraft:the_nether"
+ * }
+ * }
+ *
+ * @param entity The entity that changed dimension
+ * @param from The dimension the entity came from
+ * @param to The dimension the entity entered
+ */
+public record DimensionChangedPayload(EntityRef entity, String from, String to) {
+
+ /**
+ * Codec for serializing and deserializing {@link DimensionChangedPayload} instances.
+ */
+ public static final CodecExample requests:
+ *{@code
+ * // Wildcard — all entities:
+ * { "jsonrpc": "2.0", "id": 1,
+ * "method": "entity_data:notification/dimension/changed/subscribe",
+ * "params": [{}] }
+ *
+ * // Specific player by name:
+ * { "jsonrpc": "2.0", "id": 1,
+ * "method": "entity_data:notification/dimension/changed/subscribe",
+ * "params": [{ "name": "Steve" }] }
+ * }
+ */
+public class DimensionChangedSubscribe {
+
+ /**
+ * Registers the {@code entity_data:notification/dimension/changed/subscribe} method.
+ *
+ * @param namespace The namespace to register this method under
+ * @param subscriptionManager The {@link SubscriptionManager} for this notification
+ */
+ public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) {
+ namespace.method("notification/dimension/changed/subscribe",
+ SubscribeRequest.SCHEMA,
+ SubscribeResponse.SCHEMA,
+ "Subscribe to dimension change notifications for a specific entity or all entities",
+ (server, params, client) -> {
+ if (params.isWildcard()) {
+ subscriptionManager.subscribe(client.connectionId().toString(), Set.of(SubscriptionManager.WILDCARD));
+ Logger.info("entity_data:notification/dimension/changed/subscribe - connection %s subscribed to all entities".formatted(client.connectionId()));
+ return new SubscribeResponse(List.of());
+ }
+
+ try {
+ Entity entity = EntityResolver.resolveEntity(server, params);
+ UUID uuid = entity.getUUID();
+ subscriptionManager.subscribe(client.connectionId().toString(), Set.of(uuid));
+ EntityRef ref = EntityResolver.toEntityRef(entity);
+ Logger.info("entity_data:notification/dimension/changed/subscribe - connection %s subscribed to %s".formatted(client.connectionId(), uuid));
+ return new SubscribeResponse(List.of(ref));
+ } catch (MSMPException e) {
+ Logger.warning("entity_data:notification/dimension/changed/subscribe - " + e.getMessage());
+ throw e;
+ }
+ }
+ );
+ }
+}
diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java
new file mode 100644
index 0000000..0038150
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java
@@ -0,0 +1,66 @@
+package dev.loat.msmp_entity_data.msmp.notifications.dimension.changed;
+
+import dev.loat.msmp.MSMPNamespace;
+import dev.loat.msmp_entity_data.logging.Logger;
+import dev.loat.msmp_entity_data.msmp.components.EntityRef;
+import dev.loat.msmp_entity_data.msmp.components.EntityResolver;
+import dev.loat.msmp_entity_data.msmp.exceptions.MSMPException;
+import dev.loat.msmp_entity_data.msmp.subscription.SubscribeRequest;
+import dev.loat.msmp_entity_data.msmp.subscription.SubscribeResponse;
+import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager;
+import net.minecraft.world.entity.Entity;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Registers the {@code entity_data:notification/dimension/changed/unsubscribe} MSMP method.
+ *
+ * Example requests:
+ *{@code
+ * // Unsubscribe from all:
+ * { "jsonrpc": "2.0", "id": 1,
+ * "method": "entity_data:notification/dimension/changed/unsubscribe",
+ * "params": [{}] }
+ *
+ * // Unsubscribe from specific entity:
+ * { "jsonrpc": "2.0", "id": 1,
+ * "method": "entity_data:notification/dimension/changed/unsubscribe",
+ * "params": [{ "name": "Steve" }] }
+ * }
+ */
+public class DimensionChangedUnsubscribe {
+
+ /**
+ * Registers the {@code entity_data:notification/dimension/changed/unsubscribe} method.
+ *
+ * @param namespace The namespace to register this method under
+ * @param subscriptionManager The {@link SubscriptionManager} for this notification
+ */
+ public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) {
+ namespace.method("notification/dimension/changed/unsubscribe",
+ SubscribeRequest.SCHEMA,
+ SubscribeResponse.SCHEMA,
+ "Unsubscribe from dimension change notifications",
+ (server, params, client) -> {
+ if (params.isWildcard()) {
+ subscriptionManager.removeAll(client.connectionId().toString());
+ Logger.info("entity_data:notification/dimension/changed/unsubscribe - connection %s unsubscribed from all".formatted(client.connectionId()));
+ return new SubscribeResponse(List.of());
+ }
+
+ try {
+ Entity entity = EntityResolver.resolveEntity(server, params);
+ EntityRef ref = EntityResolver.toEntityRef(entity);
+ subscriptionManager.unsubscribe(client.connectionId().toString(), Set.of(entity.getUUID()));
+ Logger.info("entity_data:notification/dimension/changed/unsubscribe - connection %s unsubscribed from %s".formatted(client.connectionId(), entity.getUUID()));
+ return new SubscribeResponse(List.of(ref));
+ } catch (MSMPException e) {
+ Logger.warning("entity_data:notification/dimension/changed/unsubscribe - " + e.getMessage());
+ throw e;
+ }
+ }
+ );
+ }
+}
diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java
new file mode 100644
index 0000000..8dcb373
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java
@@ -0,0 +1,53 @@
+package dev.loat.msmp_entity_data.msmp.subscription;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import dev.loat.msmp_entity_data.msmp.components.EntityLookup;
+import net.minecraft.server.jsonrpc.api.Schema;
+
+import java.util.Optional;
+
+/**
+ * Common request payload for notification subscribe methods.
+ *
+ * At least one of {@code id} or {@code name} must be present to subscribe + * to a specific entity. If both are omitted, a wildcard subscription is created + * (all entities).
+ * + *Example JSON representations:
+ *{@code
+ * {} // wildcard — all entities
+ * { "name": "Steve" } // specific player by name
+ * { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5" } // specific entity by UUID
+ * }
+ *
+ * @param id The entity's UUID as a string, if provided
+ * @param name The player's in-game name, if provided
+ */
+public record SubscribeRequest(OptionalReturns the list of entities now being tracked. An empty list means + * the connection is subscribed with a wildcard (all entities).
+ * + *Example JSON representations:
+ *{@code
+ * // Wildcard subscription:
+ * { "subscribed": [] }
+ *
+ * // Specific entity subscription:
+ * { "subscribed": [{ "id": "069a...", "name": "Steve" }] }
+ * }
+ *
+ * @param subscribed The entities now tracked; empty means wildcard
+ */
+public record SubscribeResponse(ListHolds a mapping of {@code connectionId -> event -> Set
Each notification (e.g. {@code dimension/changed}) owns its own
+ * {@link SubscriptionManager} instance. Holds a mapping of
+ * {@code connectionId -> Set
A wildcard UUID ({@link #WILDCARD}) can be used to subscribe to an event - * for all entities (e.g. all players).
+ *Thread-safe via {@link ConcurrentHashMap}.
*/ public class SubscriptionManager { /** * Special UUID sentinel meaning "all entities". - * Used when a client subscribes to an event without specifying entity IDs. + * Used when a client subscribes without specifying entity IDs. */ public static final UUID WILDCARD = new UUID(0, 0); - /** connectionId -> event -> SetFires when any entity changes dimension. Only sent to connections that - * have subscribed via {@code entity_data:notification/dimension/changed/subscribe}.
- * - *Example notification payload:
- *{@code
- * {
- * "jsonrpc": "2.0",
- * "method": "entity_data:notification/dimension/changed",
- * "params": [{
- * "entity": { "id": "069a...", "name": "Steve" },
- * "from": "minecraft:overworld",
- * "to": "minecraft:the_nether"
- * }]
- * }
- * }
- */
public class DimensionChanged {
- /**
- * Registers the notification, subscribe and unsubscribe methods, and
- * hooks into the Fabric {@link ServerEntityWorldChangeEvents#AFTER_ENTITY_CHANGE_WORLD} event.
- *
- * @param namespace The namespace to register under
- * @param msmpServer Supplier of the current {@link MSMPServer} instance
- * @param subscriptionManager The {@link SubscriptionManager} for this notification
- */
public static void register(
MSMPNamespace namespace,
MSMPServerSupplier msmpServer,
@@ -47,35 +18,28 @@ public static void register(
namespace.notification("notification/dimension/changed", DimensionChangedPayload.SCHEMA,
"Fired when an entity changes dimension");
- DimensionChangedSubscribe.register(namespace, subscriptionManager);
- DimensionChangedUnsubscribe.register(namespace, subscriptionManager);
-
- ServerEntityWorldChangeEvents.AFTER_ENTITY_CHANGE_WORLD.register(
- (entity, origin, destination) -> {
+ ServerEntityLevelChangeEvents.AFTER_ENTITY_CHANGE_LEVEL.register(
+ (originalEntity, newEntity, origin, destination) -> {
MSMPServer server = msmpServer.get();
if (server == null) return;
- if (!subscriptionManager.hasSubscribers(entity.getUUID())) return;
+ if (!subscriptionManager.hasSubscribers(newEntity.getUUID())) return;
String from = origin.dimension().identifier().toString();
String to = destination.dimension().identifier().toString();
DimensionChangedPayload payload = new DimensionChangedPayload(
- EntityResolver.toEntityRef(entity),
+ EntityResolver.toEntityRef(newEntity),
from,
to
);
- for (String connectionId : subscriptionManager.getSubscribers(entity.getUUID())) {
+ for (Integer connectionId : subscriptionManager.getSubscribers(newEntity.getUUID())) {
server.sendTo(connectionId, notification, payload);
}
}
);
}
- /**
- * Functional interface for lazily resolving the current {@link MSMPServer} instance.
- * Returns {@code null} when no server is running.
- */
@FunctionalInterface
public interface MSMPServerSupplier {
MSMPServer get();
diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedSubscribe.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedSubscribe.java
deleted file mode 100644
index 45f70ae..0000000
--- a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedSubscribe.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package dev.loat.msmp_entity_data.msmp.notifications.dimension.changed;
-
-import dev.loat.msmp.MSMPNamespace;
-import dev.loat.msmp_entity_data.logging.Logger;
-import dev.loat.msmp_entity_data.msmp.components.EntityRef;
-import dev.loat.msmp_entity_data.msmp.components.EntityResolver;
-import dev.loat.msmp_entity_data.msmp.exceptions.MSMPException;
-import dev.loat.msmp_entity_data.msmp.subscription.SubscribeRequest;
-import dev.loat.msmp_entity_data.msmp.subscription.SubscribeResponse;
-import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager;
-import net.minecraft.world.entity.Entity;
-
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
-
-/**
- * Registers the {@code entity_data:notification/dimension/changed/subscribe} MSMP method.
- *
- * Example requests:
- *{@code
- * // Wildcard — all entities:
- * { "jsonrpc": "2.0", "id": 1,
- * "method": "entity_data:notification/dimension/changed/subscribe",
- * "params": [{}] }
- *
- * // Specific player by name:
- * { "jsonrpc": "2.0", "id": 1,
- * "method": "entity_data:notification/dimension/changed/subscribe",
- * "params": [{ "name": "Steve" }] }
- * }
- */
-public class DimensionChangedSubscribe {
-
- /**
- * Registers the {@code entity_data:notification/dimension/changed/subscribe} method.
- *
- * @param namespace The namespace to register this method under
- * @param subscriptionManager The {@link SubscriptionManager} for this notification
- */
- public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) {
- namespace.method("notification/dimension/changed/subscribe",
- SubscribeRequest.SCHEMA,
- SubscribeResponse.SCHEMA,
- "Subscribe to dimension change notifications for a specific entity or all entities",
- (server, params, client) -> {
- if (params.isWildcard()) {
- subscriptionManager.subscribe(client.connectionId().toString(), Set.of(SubscriptionManager.WILDCARD));
- Logger.info("entity_data:notification/dimension/changed/subscribe - connection %s subscribed to all entities".formatted(client.connectionId()));
- return new SubscribeResponse(List.of());
- }
-
- try {
- Entity entity = EntityResolver.resolveEntity(server, params);
- UUID uuid = entity.getUUID();
- subscriptionManager.subscribe(client.connectionId().toString(), Set.of(uuid));
- EntityRef ref = EntityResolver.toEntityRef(entity);
- Logger.info("entity_data:notification/dimension/changed/subscribe - connection %s subscribed to %s".formatted(client.connectionId(), uuid));
- return new SubscribeResponse(List.of(ref));
- } catch (MSMPException e) {
- Logger.warning("entity_data:notification/dimension/changed/subscribe - " + e.getMessage());
- throw e;
- }
- }
- );
- }
-}
diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java
deleted file mode 100644
index 0038150..0000000
--- a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package dev.loat.msmp_entity_data.msmp.notifications.dimension.changed;
-
-import dev.loat.msmp.MSMPNamespace;
-import dev.loat.msmp_entity_data.logging.Logger;
-import dev.loat.msmp_entity_data.msmp.components.EntityRef;
-import dev.loat.msmp_entity_data.msmp.components.EntityResolver;
-import dev.loat.msmp_entity_data.msmp.exceptions.MSMPException;
-import dev.loat.msmp_entity_data.msmp.subscription.SubscribeRequest;
-import dev.loat.msmp_entity_data.msmp.subscription.SubscribeResponse;
-import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager;
-import net.minecraft.world.entity.Entity;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Registers the {@code entity_data:notification/dimension/changed/unsubscribe} MSMP method.
- *
- * Example requests:
- *{@code
- * // Unsubscribe from all:
- * { "jsonrpc": "2.0", "id": 1,
- * "method": "entity_data:notification/dimension/changed/unsubscribe",
- * "params": [{}] }
- *
- * // Unsubscribe from specific entity:
- * { "jsonrpc": "2.0", "id": 1,
- * "method": "entity_data:notification/dimension/changed/unsubscribe",
- * "params": [{ "name": "Steve" }] }
- * }
- */
-public class DimensionChangedUnsubscribe {
-
- /**
- * Registers the {@code entity_data:notification/dimension/changed/unsubscribe} method.
- *
- * @param namespace The namespace to register this method under
- * @param subscriptionManager The {@link SubscriptionManager} for this notification
- */
- public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) {
- namespace.method("notification/dimension/changed/unsubscribe",
- SubscribeRequest.SCHEMA,
- SubscribeResponse.SCHEMA,
- "Unsubscribe from dimension change notifications",
- (server, params, client) -> {
- if (params.isWildcard()) {
- subscriptionManager.removeAll(client.connectionId().toString());
- Logger.info("entity_data:notification/dimension/changed/unsubscribe - connection %s unsubscribed from all".formatted(client.connectionId()));
- return new SubscribeResponse(List.of());
- }
-
- try {
- Entity entity = EntityResolver.resolveEntity(server, params);
- EntityRef ref = EntityResolver.toEntityRef(entity);
- subscriptionManager.unsubscribe(client.connectionId().toString(), Set.of(entity.getUUID()));
- Logger.info("entity_data:notification/dimension/changed/unsubscribe - connection %s unsubscribed from %s".formatted(client.connectionId(), entity.getUUID()));
- return new SubscribeResponse(List.of(ref));
- } catch (MSMPException e) {
- Logger.warning("entity_data:notification/dimension/changed/unsubscribe - " + e.getMessage());
- throw e;
- }
- }
- );
- }
-}
diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java
index 8dcb373..679a280 100644
--- a/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java
+++ b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java
@@ -2,52 +2,31 @@
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
-import dev.loat.msmp_entity_data.msmp.components.EntityLookup;
+import dev.loat.msmp_entity_data.msmp.components.EntityRequest;
import net.minecraft.server.jsonrpc.api.Schema;
-import java.util.Optional;
+import java.util.List;
/**
- * Common request payload for notification subscribe methods.
+ * Request payload for notification subscribe/unsubscribe methods.
*
- * At least one of {@code id} or {@code name} must be present to subscribe - * to a specific entity. If both are omitted, a wildcard subscription is created - * (all entities).
+ *Provide a list of entity objects to subscribe to. + * An empty list is a no-op.
* *Example JSON representations:
*{@code
- * {} // wildcard — all entities
- * { "name": "Steve" } // specific player by name
- * { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5" } // specific entity by UUID
+ * { "entities": [{ "name": "Steve" }, { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5" }] }
+ * { "entities": [] } // no-op
* }
*
- * @param id The entity's UUID as a string, if provided
- * @param name The player's in-game name, if provided
+ * @param entities List of entity lookups to subscribe to
*/
-public record SubscribeRequest(OptionalEach notification (e.g. {@code dimension/changed}) owns its own
- * {@link SubscriptionManager} instance. Holds a mapping of
- * {@code connectionId -> Set
Thread-safe via {@link ConcurrentHashMap}.
- */ public class SubscriptionManager { - /** - * Special UUID sentinel meaning "all entities". - * Used when a client subscribes without specifying entity IDs. - */ - public static final UUID WILDCARD = new UUID(0, 0); - /** connectionId -> Set