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/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/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/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); + } +} 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..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 @@ -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; @@ -35,6 +37,8 @@ private Methods() {} public static void register(MSMPNamespace namespace) { Dimension.register(namespace); DimensionSet.register(namespace); + 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/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..8c07734 --- /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) { + 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()); + } + + SubscriptionManager manager = SubscriptionManager.get("entity_data:dimension/subscribe"); + 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 - " + e.getMessage()); + throw e; + } + } + + 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 new file mode 100644 index 0000000..8a6b73c --- /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) { + 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()); + } + + SubscriptionManager manager = SubscriptionManager.get("entity_data:dimension/subscribe"); + 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 - " + e.getMessage()); + throw e; + } + } + + 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/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/Notifications.java b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/Notifications.java new file mode 100644 index 0000000..3e0c0fe --- /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; + + +/** + * 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); + } +} 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..54022c6 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/notifications/dimension/changed/DimensionChanged.java @@ -0,0 +1,59 @@ +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.ServerEntityLevelChangeEvents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; + +public class DimensionChanged { + + public static void register( + MSMPNamespace namespace, + MSMPServerSupplier msmpServer + ) { + MSMPNotification notification = + namespace.notification("dimension/changed", DimensionChangedPayload.SCHEMA, + "Fired when an entity changes dimension"); + + ServerEntityLevelChangeEvents.AFTER_ENTITY_CHANGE_LEVEL.register( + (originalEntity, newEntity, origin, destination) -> + dispatch(msmpServer, notification, newEntity, origin, destination) + ); + + ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register( + (player, origin, destination) -> + dispatch(msmpServer, notification, player, origin, destination) + ); + } + + private static void dispatch( + MSMPServerSupplier msmpServer, + MSMPNotification notification, + Entity entity, + ServerLevel origin, + ServerLevel destination + ) { + MSMPServer server = msmpServer.get(); + if (server == null) return; + + SubscriptionManager manager = SubscriptionManager.get("entity_data:dimension/subscribe"); + if (!manager.isSubscribed(entity.getUUID())) return; + + DimensionChangedPayload payload = new DimensionChangedPayload( + EntityResolver.toEntityRef(entity), + origin.dimension().identifier().toString(), + destination.dimension().identifier().toString() + ); + + server.send(notification, payload); + } + + @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/subscription/SubscribeRequest.java b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java new file mode 100644 index 0000000..679a280 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscribeRequest.java @@ -0,0 +1,32 @@ +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.EntityRequest; +import net.minecraft.server.jsonrpc.api.Schema; + +import java.util.List; + +/** + * Request payload for notification subscribe/unsubscribe methods. + * + *

Provide a list of entity objects to subscribe to. + * An empty list is a no-op.

+ * + *

Example JSON representations:

+ *
{@code
+ * { "entities": [{ "name": "Steve" }, { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5" }] }
+ * { "entities": [] }  // no-op
+ * }
+ * + * @param entities List of entity lookups to subscribe to + */ +public record SubscribeRequest(List entities) { + + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + EntityRequest.CODEC.listOf().fieldOf("entities").forGetter(SubscribeRequest::entities) + ).apply(i, SubscribeRequest::new)); + + public static final Schema SCHEMA = Schema.record(CODEC) + .withField("entities", Schema.ofType("array", EntityRequest.CODEC.listOf())); +} 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/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..686c26d --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity_data/msmp/subscription/SubscriptionManager.java @@ -0,0 +1,36 @@ +package dev.loat.msmp_entity_data.msmp.subscription; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +public class SubscriptionManager { + + private static final Map REGISTRY = new ConcurrentHashMap<>(); + + public static SubscriptionManager get(String key) { + return REGISTRY.computeIfAbsent(key, k -> new SubscriptionManager()); + } + + private final Set subscriptions = ConcurrentHashMap.newKeySet(); + + private SubscriptionManager() {} + + public void subscribe(Set entityIds) { + subscriptions.addAll(entityIds); + } + + public void unsubscribe(Set entityIds) { + subscriptions.removeAll(entityIds); + } + + public boolean isSubscribed(UUID entityId) { + return subscriptions.contains(entityId); + } + + public Set getSubscriptions() { + return Collections.unmodifiableSet(subscriptions); + } +}