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} that tracks + * which clients want to receive notifications for which events on which entities. + * Thread-safe via {@link ConcurrentHashMap}.

+ * + *

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 -> Set */ + private final Map>> subscriptions = + new ConcurrentHashMap<>(); + + /** + * Subscribes a connection to the given event for the given entity UUIDs. + * Pass {@link #WILDCARD} to subscribe to all entities. + * + * @param connectionId The client connection ID + * @param event The event to subscribe to + * @param entityIds The entity UUIDs to track (or {@link #WILDCARD}) + */ + public void subscribe(String connectionId, SubscriptionEvent event, Set entityIds) { + subscriptions + .computeIfAbsent(connectionId, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(event, k -> ConcurrentHashMap.newKeySet()) + .addAll(entityIds); + } + + /** + * Unsubscribes a connection from the given event for the given entity UUIDs. + * If no entity IDs are provided, removes the subscription entirely for that event. + * + * @param connectionId The client connection ID + * @param event The event to unsubscribe from + * @param entityIds The entity UUIDs to stop tracking, or empty to remove all + */ + public void unsubscribe(String connectionId, SubscriptionEvent event, Set entityIds) { + Map> events = subscriptions.get(connectionId); + if (events == null) return; + + if (entityIds.isEmpty()) { + events.remove(event); + } else { + Set tracked = events.get(event); + if (tracked != null) { + tracked.removeAll(entityIds); + if (tracked.isEmpty()) events.remove(event); + } + } + + if (events.isEmpty()) subscriptions.remove(connectionId); + } + + /** + * Removes all subscriptions for a connection. + * Should be called when a client disconnects. + * + * @param connectionId The client connection ID + */ + public void removeAll(String connectionId) { + subscriptions.remove(connectionId); + } + + /** + * Returns all connection IDs that are subscribed to the given event + * for the given entity UUID (including wildcard subscribers). + * + * @param event The event to check + * @param entityId The UUID of the entity that triggered the event + * @return A set of connection IDs that should receive the notification + */ + public Set getSubscribers(SubscriptionEvent event, UUID entityId) { + Set result = ConcurrentHashMap.newKeySet(); + for (Map.Entry>> entry : subscriptions.entrySet()) { + Set tracked = entry.getValue().get(event); + if (tracked != null && (tracked.contains(WILDCARD) || tracked.contains(entityId))) { + result.add(entry.getKey()); + } + } + return Collections.unmodifiableSet(result); + } + + /** + * Returns whether any connection is subscribed to the given event + * for the given entity UUID. + * + * @param event The event to check + * @param entityId The UUID of the entity + * @return {@code true} if at least one connection is subscribed + */ + public boolean hasSubscribers(SubscriptionEvent event, UUID entityId) { + return !getSubscribers(event, entityId).isEmpty(); + } +} From 1ca26c4e636648f4ee411d63f617bba69f7755da Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Tue, 26 May 2026 15:16:36 +0200 Subject: [PATCH 3/7] feat: dimension changed --- .../loat/msmp_entity_data/MSMPEntityData.java | 24 +++++- .../msmp/notifications/Notifications.java | 37 +++++++++ .../dimension/changed/DimensionChanged.java | 83 +++++++++++++++++++ .../changed/DimensionChangedPayload.java | 42 ++++++++++ .../changed/DimensionChangedSubscribe.java | 67 +++++++++++++++ .../changed/DimensionChangedUnsubscribe.java | 66 +++++++++++++++ .../msmp/subscription/SubscribeRequest.java | 53 ++++++++++++ .../msmp/subscription/SubscribeResponse.java | 41 +++++++++ .../subscription/SubscriptionManager.java | 78 +++++++---------- 9 files changed, 444 insertions(+), 47 deletions(-) create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/notifications/Notifications.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedPayload.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedSubscribe.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeResponse.java diff --git a/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java b/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java index ae34a7b..a3048e6 100644 --- a/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java +++ b/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java @@ -4,20 +4,42 @@ import dev.loat.msmp.MSMPServer; import dev.loat.msmp_entity_data.logging.Logger; import dev.loat.msmp_entity_data.msmp.methods.Methods; +import dev.loat.msmp_entity_data.msmp.notifications.Notifications; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; - +/** + * Main entrypoint for the MSMP Entity Data mod. + * + *

Initializes 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:

+ *
    + *
  • {@code entity_data:notification/dimension/changed} — Fired when an entity changes dimension
  • + *
+ */ +public class Notifications { + + private Notifications() {} + + /** + * Registers all {@code entity_data} notifications and their subscribe/unsubscribe + * methods on the given {@link MSMPNamespace}. + * + * @param namespace The namespace to register all notifications under + * @param msmpServer Supplier of the current {@link MSMPServer} instance + */ + public static void register( + MSMPNamespace namespace, + DimensionChanged.MSMPServerSupplier msmpServer + ) { + DimensionChanged.register(namespace, msmpServer, new SubscriptionManager()); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java new file mode 100644 index 0000000..ba88cd6 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java @@ -0,0 +1,83 @@ +package dev.loat.msmp_entity_data.msmp.notifications.dimension.changed; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp.MSMPNotification; +import dev.loat.msmp.MSMPServer; +import dev.loat.msmp_entity_data.msmp.components.EntityResolver; +import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; +import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; + +/** + * Handles the {@code entity_data:notification/dimension/changed} notification. + * + *

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 + ) { + MSMPNotification notification = + 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) -> { + MSMPServer server = msmpServer.get(); + if (server == null) return; + + if (!subscriptionManager.hasSubscribers(entity.getUUID())) return; + + String from = origin.dimension().identifier().toString(); + String to = destination.dimension().identifier().toString(); + DimensionChangedPayload payload = new DimensionChangedPayload( + EntityResolver.toEntityRef(entity), + from, + to + ); + + for (String connectionId : subscriptionManager.getSubscribers(entity.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/DimensionChangedPayload.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedPayload.java new file mode 100644 index 0000000..a99c3e7 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedPayload.java @@ -0,0 +1,42 @@ +package dev.loat.msmp_entity_data.msmp.notifications.dimension.changed; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.loat.msmp_entity_data.msmp.components.EntityRef; +import net.minecraft.server.jsonrpc.api.Schema; + +/** + * Payload for the {@code entity_data:notification/dimension/changed} notification. + * + *

Example 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 Codec CODEC = RecordCodecBuilder.create(i -> i.group( + EntityRef.CODEC.fieldOf("entity").forGetter(DimensionChangedPayload::entity), + Codec.STRING.fieldOf("from").forGetter(DimensionChangedPayload::from), + Codec.STRING.fieldOf("to").forGetter(DimensionChangedPayload::to) + ).apply(i, DimensionChangedPayload::new)); + + /** + * MSMP schema for {@link DimensionChangedPayload}, used for protocol discovery. + */ + public static final Schema SCHEMA = Schema.record(CODEC) + .withField("entity", EntityRef.SCHEMA) + .withField("from", Schema.STRING_SCHEMA) + .withField("to", Schema.STRING_SCHEMA); +} 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 new file mode 100644 index 0000000..45f70ae --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedSubscribe.java @@ -0,0 +1,67 @@ +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 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(Optional id, Optional name) implements EntityLookup { + + /** + * Codec for serializing and deserializing {@link SubscribeRequest} instances. + */ + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + Codec.STRING.optionalFieldOf("id").forGetter(SubscribeRequest::id), + Codec.STRING.optionalFieldOf("name").forGetter(SubscribeRequest::name) + ).apply(i, SubscribeRequest::new)); + + /** + * MSMP schema for {@link SubscribeRequest}, used for protocol discovery. + */ + public static final Schema SCHEMA = Schema.record(CODEC) + .withField("id", Schema.STRING_SCHEMA) + .withField("name", Schema.STRING_SCHEMA); + + /** + * Returns {@code true} if this request is a wildcard subscription + * (neither {@code id} nor {@code name} provided). + * + * @return {@code true} if wildcard + */ + public boolean isWildcard() { + return id().isEmpty() && name().isEmpty(); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeResponse.java b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeResponse.java new file mode 100644 index 0000000..3629be1 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeResponse.java @@ -0,0 +1,41 @@ +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.EntityRef; +import net.minecraft.server.jsonrpc.api.Schema; + +import java.util.List; + +/** + * Common response payload for notification subscribe and unsubscribe methods. + * + *

Returns 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(List subscribed) { + + /** + * Codec for serializing and deserializing {@link SubscribeResponse} instances. + */ + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + EntityRef.CODEC.listOf().fieldOf("subscribed").forGetter(SubscribeResponse::subscribed) + ).apply(i, SubscribeResponse::new)); + + /** + * MSMP schema for {@link SubscribeResponse}, used for protocol discovery. + */ + public static final Schema SCHEMA = Schema.record(CODEC) + .withField("subscribed", Schema.ofType("array", EntityRef.CODEC.listOf())); +} 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 index aa819da..7708a42 100644 --- 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 @@ -6,67 +6,56 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; - /** - * Manages per-connection entity event subscriptions. + * Manages per-connection entity subscriptions for a single notification type. * - *

Holds a mapping of {@code connectionId -> event -> Set} that tracks - * which clients want to receive notifications for which events on which entities. - * Thread-safe via {@link ConcurrentHashMap}.

+ *

Each notification (e.g. {@code dimension/changed}) owns its own + * {@link SubscriptionManager} instance. Holds a mapping of + * {@code connectionId -> Set} where {@link #WILDCARD} means all entities.

* - *

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 -> Set */ - private final Map>> subscriptions = - new ConcurrentHashMap<>(); + /** connectionId -> Set */ + private final Map> subscriptions = new ConcurrentHashMap<>(); /** - * Subscribes a connection to the given event for the given entity UUIDs. - * Pass {@link #WILDCARD} to subscribe to all entities. + * Subscribes a connection to this notification for the given entity UUIDs. + * Pass {@link #WILDCARD} to receive notifications for all entities. * * @param connectionId The client connection ID - * @param event The event to subscribe to - * @param entityIds The entity UUIDs to track (or {@link #WILDCARD}) + * @param entityIds The entity UUIDs to track, or a set containing {@link #WILDCARD} */ - public void subscribe(String connectionId, SubscriptionEvent event, Set entityIds) { + public void subscribe(String connectionId, Set entityIds) { subscriptions - .computeIfAbsent(connectionId, k -> new ConcurrentHashMap<>()) - .computeIfAbsent(event, k -> ConcurrentHashMap.newKeySet()) + .computeIfAbsent(connectionId, k -> ConcurrentHashMap.newKeySet()) .addAll(entityIds); } /** - * Unsubscribes a connection from the given event for the given entity UUIDs. - * If no entity IDs are provided, removes the subscription entirely for that event. + * Unsubscribes a connection from this notification for the given entity UUIDs. + * If {@code entityIds} is empty, removes the connection entirely. * * @param connectionId The client connection ID - * @param event The event to unsubscribe from * @param entityIds The entity UUIDs to stop tracking, or empty to remove all */ - public void unsubscribe(String connectionId, SubscriptionEvent event, Set entityIds) { - Map> events = subscriptions.get(connectionId); - if (events == null) return; - + public void unsubscribe(String connectionId, Set entityIds) { if (entityIds.isEmpty()) { - events.remove(event); - } else { - Set tracked = events.get(event); - if (tracked != null) { - tracked.removeAll(entityIds); - if (tracked.isEmpty()) events.remove(event); - } + subscriptions.remove(connectionId); + return; + } + Set tracked = subscriptions.get(connectionId); + if (tracked != null) { + tracked.removeAll(entityIds); + if (tracked.isEmpty()) subscriptions.remove(connectionId); } - - if (events.isEmpty()) subscriptions.remove(connectionId); } /** @@ -80,18 +69,17 @@ public void removeAll(String connectionId) { } /** - * Returns all connection IDs that are subscribed to the given event + * Returns all connection IDs subscribed to this notification * for the given entity UUID (including wildcard subscribers). * - * @param event The event to check * @param entityId The UUID of the entity that triggered the event - * @return A set of connection IDs that should receive the notification + * @return An unmodifiable set of connection IDs that should receive the notification */ - public Set getSubscribers(SubscriptionEvent event, UUID entityId) { + public Set getSubscribers(UUID entityId) { Set result = ConcurrentHashMap.newKeySet(); - for (Map.Entry>> entry : subscriptions.entrySet()) { - Set tracked = entry.getValue().get(event); - if (tracked != null && (tracked.contains(WILDCARD) || tracked.contains(entityId))) { + for (Map.Entry> entry : subscriptions.entrySet()) { + Set tracked = entry.getValue(); + if (tracked.contains(WILDCARD) || tracked.contains(entityId)) { result.add(entry.getKey()); } } @@ -99,14 +87,12 @@ public Set getSubscribers(SubscriptionEvent event, UUID entityId) { } /** - * Returns whether any connection is subscribed to the given event - * for the given entity UUID. + * Returns whether any connection is subscribed for the given entity UUID. * - * @param event The event to check * @param entityId The UUID of the entity * @return {@code true} if at least one connection is subscribed */ - public boolean hasSubscribers(SubscriptionEvent event, UUID entityId) { - return !getSubscribers(event, entityId).isEmpty(); + public boolean hasSubscribers(UUID entityId) { + return !getSubscribers(entityId).isEmpty(); } } From 80fbcd33c836dad82b2d7ba377ac9338d8815c0e Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Wed, 27 May 2026 22:07:47 +0200 Subject: [PATCH 4/7] fix: refactor subscription --- .../logging/RPCConnectionLogger.java | 20 ++++++ .../msmp/methods/Methods.java | 7 ++ .../msmp/methods/dimension/DimensionSet.java | 6 +- .../subscribe/DimensionSubscribe.java | 52 ++++++++++++++ .../subscribe/DimensionUnsubscribe.java | 52 ++++++++++++++ .../msmp/methods/inventory/InventorySet.java | 1 - .../dimension/changed/DimensionChanged.java | 48 ++----------- .../changed/DimensionChangedSubscribe.java | 67 ----------------- .../changed/DimensionChangedUnsubscribe.java | 66 ----------------- .../msmp/subscription/SubscribeRequest.java | 43 +++-------- .../subscription/SubscriptionManager.java | 71 +++---------------- 11 files changed, 162 insertions(+), 271 deletions(-) create mode 100644 src/main/java/dev/loat/msmp_entity_data/logging/RPCConnectionLogger.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java create mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java delete mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedSubscribe.java delete mode 100644 src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChangedUnsubscribe.java diff --git a/src/main/java/dev/loat/msmp_entity_data/logging/RPCConnectionLogger.java b/src/main/java/dev/loat/msmp_entity_data/logging/RPCConnectionLogger.java new file mode 100644 index 0000000..bc2d2ac --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/logging/RPCConnectionLogger.java @@ -0,0 +1,20 @@ +package dev.loat.msmp_entity_data.logging; + +public class RPCConnectionLogger { + + public static void debug(Integer connectionId, String message) { + Logger.debug("RPC Connection #%s: %s".formatted(connectionId, message)); + } + + public static void info(Integer connectionId, String message) { + Logger.info("RPC Connection #%s: %s".formatted(connectionId, message)); + } + + public static void warning(Integer connectionId, String message) { + Logger.warning("RPC Connection #%s: %s".formatted(connectionId, message)); + } + + public static void error(Integer connectionId, String message) { + Logger.error("RPC Connection #%s: %s".formatted(connectionId, message)); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java index 4f72437..ee4903e 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java @@ -3,6 +3,8 @@ import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity_data.msmp.methods.dimension.Dimension; import dev.loat.msmp_entity_data.msmp.methods.dimension.DimensionSet; +import dev.loat.msmp_entity_data.msmp.methods.dimension.subscribe.DimensionSubscribe; +import dev.loat.msmp_entity_data.msmp.methods.dimension.subscribe.DimensionUnsubscribe; import dev.loat.msmp_entity_data.msmp.methods.health.Health; import dev.loat.msmp_entity_data.msmp.methods.health.HealthSet; import dev.loat.msmp_entity_data.msmp.methods.inventory.Inventory; @@ -14,6 +16,7 @@ import dev.loat.msmp_entity_data.msmp.methods.saturation.Saturation; import dev.loat.msmp_entity_data.msmp.methods.saturation.SaturationSet; import dev.loat.msmp_entity_data.msmp.methods.uuid.UUID; +import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; /** @@ -33,8 +36,12 @@ private Methods() {} * @param namespace The namespace to register all methods under */ public static void register(MSMPNamespace namespace) { + SubscriptionManager dimensionSubscriptionManager = new SubscriptionManager(); + Dimension.register(namespace); DimensionSet.register(namespace); + DimensionSubscribe.register(namespace, dimensionSubscriptionManager); + DimensionUnsubscribe.register(namespace, dimensionSubscriptionManager); Health.register(namespace); HealthSet.register(namespace); Inventory.register(namespace); diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/DimensionSet.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/DimensionSet.java index 34253ca..9c1b4b4 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/DimensionSet.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/DimensionSet.java @@ -1,7 +1,7 @@ package dev.loat.msmp_entity_data.msmp.methods.dimension; import dev.loat.msmp.MSMPNamespace; -import dev.loat.msmp_entity_data.logging.Logger; +import dev.loat.msmp_entity_data.logging.RPCConnectionLogger; import dev.loat.msmp_entity_data.msmp.components.EntityResolver; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; @@ -64,12 +64,14 @@ public static void register(MSMPNamespace namespace) { true ); + RPCConnectionLogger.info(client.connectionId(), "entity_data:dimension/set - teleported %s to dimension %s".formatted(entity.getUUID(), params.dimension())); + return new DimensionResponse( EntityResolver.toEntityRef(entity), entity.level().dimension().identifier().toString() ); } catch (IllegalArgumentException e) { - Logger.warning("entity_data:dimension/set - " + e.getMessage()); + RPCConnectionLogger.warning(client.connectionId(), "entity_data:dimension/set - " + e.getMessage()); throw e; } } diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java new file mode 100644 index 0000000..56cae88 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java @@ -0,0 +1,52 @@ +package dev.loat.msmp_entity_data.msmp.methods.dimension.subscribe; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp_entity_data.logging.RPCConnectionLogger; +import dev.loat.msmp_entity_data.msmp.components.EntityRef; +import dev.loat.msmp_entity_data.msmp.components.EntityRequest; +import dev.loat.msmp_entity_data.msmp.components.EntityResolver; +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.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class DimensionSubscribe { + + public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) { + namespace.method("dimension/subscribe", + SubscribeRequest.SCHEMA, + SubscribeResponse.SCHEMA, + "Subscribe to dimension change notifications for the given entities", + (server, params, client) -> { + if (params.entities().isEmpty()) { + return new SubscribeResponse(List.of()); + } + + int connectionId = client.connectionId(); + Set uuids = new HashSet<>(); + List resolved = new ArrayList<>(); + + for (EntityRequest entry : params.entities()) { + try { + Entity entity = EntityResolver.resolveEntity(server, entry); + uuids.add(entity.getUUID()); + resolved.add(EntityResolver.toEntityRef(entity)); + } catch (IllegalArgumentException e) { + RPCConnectionLogger.warning(client.connectionId(), "entity_data:dimension/subscribe - %s".formatted(e.getMessage())); + throw e; + } + } + + subscriptionManager.subscribe(connectionId, uuids); + RPCConnectionLogger.info(client.connectionId(), "entity_data:dimension/subscribe - subscribed to %s".formatted(uuids)); + return new SubscribeResponse(resolved); + } + ); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java new file mode 100644 index 0000000..683b6c1 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java @@ -0,0 +1,52 @@ +package dev.loat.msmp_entity_data.msmp.methods.dimension.subscribe; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp_entity_data.logging.RPCConnectionLogger; +import dev.loat.msmp_entity_data.msmp.components.EntityRef; +import dev.loat.msmp_entity_data.msmp.components.EntityRequest; +import dev.loat.msmp_entity_data.msmp.components.EntityResolver; +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.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class DimensionUnsubscribe { + + public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) { + namespace.method("dimension/unsubscribe", + SubscribeRequest.SCHEMA, + SubscribeResponse.SCHEMA, + "Unsubscribe from dimension change notifications for the given entities", + (server, params, client) -> { + if (params.entities().isEmpty()) { + return new SubscribeResponse(List.of()); + } + + int connectionId = client.connectionId(); + Set uuids = new HashSet<>(); + List resolved = new ArrayList<>(); + + for (EntityRequest entry : params.entities()) { + try { + Entity entity = EntityResolver.resolveEntity(server, entry); + uuids.add(entity.getUUID()); + resolved.add(EntityResolver.toEntityRef(entity)); + } catch (IllegalArgumentException e) { + RPCConnectionLogger.warning(client.connectionId(), "entity_data:dimension/unsubscribe - ".formatted(e.getMessage())); + throw e; + } + } + + subscriptionManager.unsubscribe(connectionId, uuids); + RPCConnectionLogger.info(client.connectionId(), "entity_data:dimension/unsubscribe - unsubscribed from %s".formatted(uuids)); + return new SubscribeResponse(resolved); + } + ); + } +} diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/inventory/InventorySet.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/inventory/InventorySet.java index 5f70f45..93712ab 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/inventory/InventorySet.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/inventory/InventorySet.java @@ -7,7 +7,6 @@ import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity_data.logging.Logger; import dev.loat.msmp_entity_data.msmp.components.EntityResolver; -import dev.loat.msmp_entity_data.msmp.methods.inventory.InventoryResponse; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java index ba88cd6..310c11f 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java @@ -5,39 +5,10 @@ import dev.loat.msmp.MSMPServer; import dev.loat.msmp_entity_data.msmp.components.EntityResolver; import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; -import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.entity.Entity; +import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents; -/** - * Handles the {@code entity_data:notification/dimension/changed} notification. - * - *

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, @@ -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(Optional id, Optional name) implements EntityLookup { +public record SubscribeRequest(List entities) { - /** - * Codec for serializing and deserializing {@link SubscribeRequest} instances. - */ public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - Codec.STRING.optionalFieldOf("id").forGetter(SubscribeRequest::id), - Codec.STRING.optionalFieldOf("name").forGetter(SubscribeRequest::name) + EntityRequest.CODEC.listOf().fieldOf("entities").forGetter(SubscribeRequest::entities) ).apply(i, SubscribeRequest::new)); - /** - * MSMP schema for {@link SubscribeRequest}, used for protocol discovery. - */ public static final Schema SCHEMA = Schema.record(CODEC) - .withField("id", Schema.STRING_SCHEMA) - .withField("name", Schema.STRING_SCHEMA); - - /** - * Returns {@code true} if this request is a wildcard subscription - * (neither {@code id} nor {@code name} provided). - * - * @return {@code true} if wildcard - */ - public boolean isWildcard() { - return id().isEmpty() && name().isEmpty(); - } + .withField("entities", Schema.ofType("array", EntityRequest.CODEC.listOf())); } 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 index 7708a42..f16546b 100644 --- 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 @@ -6,51 +6,20 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -/** - * Manages per-connection entity subscriptions for a single notification type. - * - *

Each notification (e.g. {@code dimension/changed}) owns its own - * {@link SubscriptionManager} instance. Holds a mapping of - * {@code connectionId -> Set} where {@link #WILDCARD} means all entities.

- * - *

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 */ - private final Map> subscriptions = new ConcurrentHashMap<>(); + private final Map> subscriptions = new ConcurrentHashMap<>(); - /** - * Subscribes a connection to this notification for the given entity UUIDs. - * Pass {@link #WILDCARD} to receive notifications for all entities. - * - * @param connectionId The client connection ID - * @param entityIds The entity UUIDs to track, or a set containing {@link #WILDCARD} - */ - public void subscribe(String connectionId, Set entityIds) { + public void subscribe(int connectionId, Set entityIds) { + if (entityIds.isEmpty()) return; subscriptions .computeIfAbsent(connectionId, k -> ConcurrentHashMap.newKeySet()) .addAll(entityIds); } - /** - * Unsubscribes a connection from this notification for the given entity UUIDs. - * If {@code entityIds} is empty, removes the connection entirely. - * - * @param connectionId The client connection ID - * @param entityIds The entity UUIDs to stop tracking, or empty to remove all - */ - public void unsubscribe(String connectionId, Set entityIds) { - if (entityIds.isEmpty()) { - subscriptions.remove(connectionId); - return; - } + public void unsubscribe(int connectionId, Set entityIds) { + if (entityIds.isEmpty()) return; Set tracked = subscriptions.get(connectionId); if (tracked != null) { tracked.removeAll(entityIds); @@ -58,40 +27,20 @@ public void unsubscribe(String connectionId, Set entityIds) { } } - /** - * Removes all subscriptions for a connection. - * Should be called when a client disconnects. - * - * @param connectionId The client connection ID - */ - public void removeAll(String connectionId) { + public void removeAll(int connectionId) { subscriptions.remove(connectionId); } - /** - * Returns all connection IDs subscribed to this notification - * for the given entity UUID (including wildcard subscribers). - * - * @param entityId The UUID of the entity that triggered the event - * @return An unmodifiable set of connection IDs that should receive the notification - */ - public Set getSubscribers(UUID entityId) { - Set result = ConcurrentHashMap.newKeySet(); - for (Map.Entry> entry : subscriptions.entrySet()) { - Set tracked = entry.getValue(); - if (tracked.contains(WILDCARD) || tracked.contains(entityId)) { + public Set getSubscribers(UUID entityId) { + Set result = ConcurrentHashMap.newKeySet(); + for (Map.Entry> entry : subscriptions.entrySet()) { + if (entry.getValue().contains(entityId)) { result.add(entry.getKey()); } } return Collections.unmodifiableSet(result); } - /** - * Returns whether any connection is subscribed for the given entity UUID. - * - * @param entityId The UUID of the entity - * @return {@code true} if at least one connection is subscribed - */ public boolean hasSubscribers(UUID entityId) { return !getSubscribers(entityId).isEmpty(); } From 38f1ef0711565bbe0a97bf30d7a94ce3a961902b Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Thu, 28 May 2026 15:19:11 +0200 Subject: [PATCH 5/7] fix: only one SubscriptionManager --- .../java/dev/loat/msmp_entity_data/MSMPEntityData.java | 7 +++++-- .../dev/loat/msmp_entity_data/msmp/methods/Methods.java | 4 +--- .../msmp_entity_data/msmp/notifications/Notifications.java | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java b/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java index a3048e6..74c6f38 100644 --- a/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java +++ b/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java @@ -5,6 +5,7 @@ import dev.loat.msmp_entity_data.logging.Logger; import dev.loat.msmp_entity_data.msmp.methods.Methods; import dev.loat.msmp_entity_data.msmp.notifications.Notifications; +import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; @@ -38,8 +39,10 @@ public class MSMPEntityData implements ModInitializer { public void onInitialize() { Logger.setLoggerClass(MSMPEntityData.class); - Methods.register(NS); - Notifications.register(NS, () -> msmp); + SubscriptionManager dimensionSubscriptionManager = new SubscriptionManager(); + + Methods.register(NS, dimensionSubscriptionManager); + Notifications.register(NS, () -> msmp, dimensionSubscriptionManager); ServerLifecycleEvents.SERVER_STARTED.register(server -> { NS.attach(server); diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java index ee4903e..3014a28 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java @@ -35,9 +35,7 @@ private Methods() {} * * @param namespace The namespace to register all methods under */ - public static void register(MSMPNamespace namespace) { - SubscriptionManager dimensionSubscriptionManager = new SubscriptionManager(); - + public static void register(MSMPNamespace namespace, SubscriptionManager dimensionSubscriptionManager) { Dimension.register(namespace); DimensionSet.register(namespace); DimensionSubscribe.register(namespace, dimensionSubscriptionManager); 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 index 7cf7a31..23ade57 100644 --- 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 @@ -30,8 +30,9 @@ private Notifications() {} */ public static void register( MSMPNamespace namespace, - DimensionChanged.MSMPServerSupplier msmpServer + DimensionChanged.MSMPServerSupplier msmpServer, + SubscriptionManager dimensionSubscriptionManager ) { - DimensionChanged.register(namespace, msmpServer, new SubscriptionManager()); + DimensionChanged.register(namespace, msmpServer, dimensionSubscriptionManager); } } From f8f7296819f0cf962494b13b2ac25fd596badf57 Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Thu, 28 May 2026 15:49:42 +0200 Subject: [PATCH 6/7] Update DimensionChanged.java --- .../dimension/changed/DimensionChanged.java | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java index 310c11f..2a309cb 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java @@ -6,6 +6,8 @@ import dev.loat.msmp_entity_data.msmp.components.EntityResolver; import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; public class DimensionChanged { @@ -15,29 +17,42 @@ public static void register( SubscriptionManager subscriptionManager ) { MSMPNotification notification = - namespace.notification("notification/dimension/changed", DimensionChangedPayload.SCHEMA, + namespace.notification("dimension/changed", DimensionChangedPayload.SCHEMA, "Fired when an entity changes dimension"); ServerEntityLevelChangeEvents.AFTER_ENTITY_CHANGE_LEVEL.register( - (originalEntity, newEntity, origin, destination) -> { - MSMPServer server = msmpServer.get(); - if (server == null) 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(newEntity), - from, - to - ); - - for (Integer connectionId : subscriptionManager.getSubscribers(newEntity.getUUID())) { - server.sendTo(connectionId, notification, payload); - } - } + (originalEntity, newEntity, origin, destination) -> + dispatch(msmpServer, subscriptionManager, notification, newEntity, origin, destination) ); + + ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register( + (player, origin, destination) -> + dispatch(msmpServer, subscriptionManager, notification, player, origin, destination) + ); + } + + private static void dispatch( + MSMPServerSupplier msmpServer, + SubscriptionManager subscriptionManager, + MSMPNotification notification, + Entity entity, + ServerLevel origin, + ServerLevel destination + ) { + MSMPServer server = msmpServer.get(); + if (server == null) return; + + if (!subscriptionManager.hasSubscribers(entity.getUUID())) return; + + DimensionChangedPayload payload = new DimensionChangedPayload( + EntityResolver.toEntityRef(entity), + origin.dimension().identifier().toString(), + destination.dimension().identifier().toString() + ); + + for (Integer connectionId : subscriptionManager.getSubscribers(entity.getUUID())) { + server.sendTo(connectionId, notification, payload); + } } @FunctionalInterface From 44bdac2bb8b4694f07e35528a6bafc35bfb97c94 Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Fri, 29 May 2026 13:23:47 +0200 Subject: [PATCH 7/7] fix: dimension/changed notification --- .../loat/msmp_entity_data/MSMPEntityData.java | 7 +-- .../msmp/methods/Methods.java | 7 ++- .../subscribe/DimensionSubscribe.java | 8 ++-- .../subscribe/DimensionUnsubscribe.java | 8 ++-- .../msmp/notifications/Notifications.java | 7 ++- .../dimension/changed/DimensionChanged.java | 15 +++---- .../subscription/SubscriptionManager.java | 43 +++++++------------ 7 files changed, 38 insertions(+), 57 deletions(-) diff --git a/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java b/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java index 74c6f38..a3048e6 100644 --- a/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java +++ b/src/main/java/dev/loat/msmp_entity_data/MSMPEntityData.java @@ -5,7 +5,6 @@ import dev.loat.msmp_entity_data.logging.Logger; import dev.loat.msmp_entity_data.msmp.methods.Methods; import dev.loat.msmp_entity_data.msmp.notifications.Notifications; -import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; @@ -39,10 +38,8 @@ public class MSMPEntityData implements ModInitializer { public void onInitialize() { Logger.setLoggerClass(MSMPEntityData.class); - SubscriptionManager dimensionSubscriptionManager = new SubscriptionManager(); - - Methods.register(NS, dimensionSubscriptionManager); - Notifications.register(NS, () -> msmp, dimensionSubscriptionManager); + 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/methods/Methods.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java index 3014a28..36167a8 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/Methods.java @@ -16,7 +16,6 @@ import dev.loat.msmp_entity_data.msmp.methods.saturation.Saturation; import dev.loat.msmp_entity_data.msmp.methods.saturation.SaturationSet; import dev.loat.msmp_entity_data.msmp.methods.uuid.UUID; -import dev.loat.msmp_entity_data.msmp.subscription.SubscriptionManager; /** @@ -35,11 +34,11 @@ private Methods() {} * * @param namespace The namespace to register all methods under */ - public static void register(MSMPNamespace namespace, SubscriptionManager dimensionSubscriptionManager) { + public static void register(MSMPNamespace namespace) { Dimension.register(namespace); DimensionSet.register(namespace); - DimensionSubscribe.register(namespace, dimensionSubscriptionManager); - DimensionUnsubscribe.register(namespace, dimensionSubscriptionManager); + DimensionSubscribe.register(namespace); + DimensionUnsubscribe.register(namespace); Health.register(namespace); HealthSet.register(namespace); Inventory.register(namespace); diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java index 56cae88..8c07734 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionSubscribe.java @@ -18,7 +18,7 @@ public class DimensionSubscribe { - public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) { + public static void register(MSMPNamespace namespace) { namespace.method("dimension/subscribe", SubscribeRequest.SCHEMA, SubscribeResponse.SCHEMA, @@ -28,7 +28,7 @@ public static void register(MSMPNamespace namespace, SubscriptionManager subscri return new SubscribeResponse(List.of()); } - int connectionId = client.connectionId(); + SubscriptionManager manager = SubscriptionManager.get("entity_data:dimension/subscribe"); Set uuids = new HashSet<>(); List resolved = new ArrayList<>(); @@ -38,12 +38,12 @@ public static void register(MSMPNamespace namespace, SubscriptionManager subscri uuids.add(entity.getUUID()); resolved.add(EntityResolver.toEntityRef(entity)); } catch (IllegalArgumentException e) { - RPCConnectionLogger.warning(client.connectionId(), "entity_data:dimension/subscribe - %s".formatted(e.getMessage())); + RPCConnectionLogger.warning(client.connectionId(), "entity_data:dimension/subscribe - " + e.getMessage()); throw e; } } - subscriptionManager.subscribe(connectionId, uuids); + manager.subscribe(uuids); RPCConnectionLogger.info(client.connectionId(), "entity_data:dimension/subscribe - subscribed to %s".formatted(uuids)); return new SubscribeResponse(resolved); } diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java index 683b6c1..8a6b73c 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java @@ -18,7 +18,7 @@ public class DimensionUnsubscribe { - public static void register(MSMPNamespace namespace, SubscriptionManager subscriptionManager) { + public static void register(MSMPNamespace namespace) { namespace.method("dimension/unsubscribe", SubscribeRequest.SCHEMA, SubscribeResponse.SCHEMA, @@ -28,7 +28,7 @@ public static void register(MSMPNamespace namespace, SubscriptionManager subscri return new SubscribeResponse(List.of()); } - int connectionId = client.connectionId(); + SubscriptionManager manager = SubscriptionManager.get("entity_data:dimension/subscribe"); Set uuids = new HashSet<>(); List resolved = new ArrayList<>(); @@ -38,12 +38,12 @@ public static void register(MSMPNamespace namespace, SubscriptionManager subscri uuids.add(entity.getUUID()); resolved.add(EntityResolver.toEntityRef(entity)); } catch (IllegalArgumentException e) { - RPCConnectionLogger.warning(client.connectionId(), "entity_data:dimension/unsubscribe - ".formatted(e.getMessage())); + RPCConnectionLogger.warning(client.connectionId(), "entity_data:dimension/unsubscribe - " + e.getMessage()); throw e; } } - subscriptionManager.unsubscribe(connectionId, uuids); + manager.unsubscribe(uuids); RPCConnectionLogger.info(client.connectionId(), "entity_data:dimension/unsubscribe - unsubscribed from %s".formatted(uuids)); return new SubscribeResponse(resolved); } 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 index 23ade57..3e0c0fe 100644 --- 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 @@ -3,7 +3,7 @@ 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. @@ -30,9 +30,8 @@ private Notifications() {} */ public static void register( MSMPNamespace namespace, - DimensionChanged.MSMPServerSupplier msmpServer, - SubscriptionManager dimensionSubscriptionManager + DimensionChanged.MSMPServerSupplier msmpServer ) { - DimensionChanged.register(namespace, msmpServer, dimensionSubscriptionManager); + DimensionChanged.register(namespace, msmpServer); } } diff --git a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java index 2a309cb..54022c6 100644 --- a/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java @@ -13,8 +13,7 @@ public class DimensionChanged { public static void register( MSMPNamespace namespace, - MSMPServerSupplier msmpServer, - SubscriptionManager subscriptionManager + MSMPServerSupplier msmpServer ) { MSMPNotification notification = namespace.notification("dimension/changed", DimensionChangedPayload.SCHEMA, @@ -22,18 +21,17 @@ public static void register( ServerEntityLevelChangeEvents.AFTER_ENTITY_CHANGE_LEVEL.register( (originalEntity, newEntity, origin, destination) -> - dispatch(msmpServer, subscriptionManager, notification, newEntity, origin, destination) + dispatch(msmpServer, notification, newEntity, origin, destination) ); ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register( (player, origin, destination) -> - dispatch(msmpServer, subscriptionManager, notification, player, origin, destination) + dispatch(msmpServer, notification, player, origin, destination) ); } private static void dispatch( MSMPServerSupplier msmpServer, - SubscriptionManager subscriptionManager, MSMPNotification notification, Entity entity, ServerLevel origin, @@ -42,7 +40,8 @@ private static void dispatch( MSMPServer server = msmpServer.get(); if (server == null) return; - if (!subscriptionManager.hasSubscribers(entity.getUUID())) return; + SubscriptionManager manager = SubscriptionManager.get("entity_data:dimension/subscribe"); + if (!manager.isSubscribed(entity.getUUID())) return; DimensionChangedPayload payload = new DimensionChangedPayload( EntityResolver.toEntityRef(entity), @@ -50,9 +49,7 @@ private static void dispatch( destination.dimension().identifier().toString() ); - for (Integer connectionId : subscriptionManager.getSubscribers(entity.getUUID())) { - server.sendTo(connectionId, notification, payload); - } + server.send(notification, payload); } @FunctionalInterface 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 index f16546b..686c26d 100644 --- 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 @@ -1,47 +1,36 @@ 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; +import java.util.Map; public class SubscriptionManager { - /** connectionId -> Set */ - private final Map> subscriptions = new ConcurrentHashMap<>(); + private static final Map REGISTRY = new ConcurrentHashMap<>(); - public void subscribe(int connectionId, Set entityIds) { - if (entityIds.isEmpty()) return; - subscriptions - .computeIfAbsent(connectionId, k -> ConcurrentHashMap.newKeySet()) - .addAll(entityIds); + public static SubscriptionManager get(String key) { + return REGISTRY.computeIfAbsent(key, k -> new SubscriptionManager()); } - public void unsubscribe(int connectionId, Set entityIds) { - if (entityIds.isEmpty()) return; - Set tracked = subscriptions.get(connectionId); - if (tracked != null) { - tracked.removeAll(entityIds); - if (tracked.isEmpty()) subscriptions.remove(connectionId); - } + private final Set subscriptions = ConcurrentHashMap.newKeySet(); + + private SubscriptionManager() {} + + public void subscribe(Set entityIds) { + subscriptions.addAll(entityIds); } - public void removeAll(int connectionId) { - subscriptions.remove(connectionId); + public void unsubscribe(Set entityIds) { + subscriptions.removeAll(entityIds); } - public Set getSubscribers(UUID entityId) { - Set result = ConcurrentHashMap.newKeySet(); - for (Map.Entry> entry : subscriptions.entrySet()) { - if (entry.getValue().contains(entityId)) { - result.add(entry.getKey()); - } - } - return Collections.unmodifiableSet(result); + public boolean isSubscribed(UUID entityId) { + return subscriptions.contains(entityId); } - public boolean hasSubscribers(UUID entityId) { - return !getSubscribers(entityId).isEmpty(); + public Set getSubscriptions() { + return Collections.unmodifiableSet(subscriptions); } }