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"); + SetEach 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:
+ *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 CodecProvide 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(ListReturns 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(ListUsed 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