diff --git a/gradle.properties b/gradle.properties index 91a322c..9ab218a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ loom_version=1.16-SNAPSHOT fabric_api_version=0.145.1+26.1 # Mod Properties -mod_version=1.0.0 +mod_version=1.1.0 maven_group=dev.loat archives_base_name=msmp-entity-mod diff --git a/src/main/java/dev/loat/msmp_entity/MSMPEntity.java b/src/main/java/dev/loat/msmp_entity/MSMPEntity.java index d264ffe..35188c2 100644 --- a/src/main/java/dev/loat/msmp_entity/MSMPEntity.java +++ b/src/main/java/dev/loat/msmp_entity/MSMPEntity.java @@ -3,8 +3,7 @@ import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp.MSMPServer; import dev.loat.msmp_entity.logging.Logger; -import dev.loat.msmp_entity.msmp.methods.Methods; -import dev.loat.msmp_entity.msmp.notifications.Notifications; +import dev.loat.msmp_entity.msmp.endpoints.Endpoints; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; @@ -38,8 +37,7 @@ public class MSMPEntity implements ModInitializer { public void onInitialize() { Logger.setLoggerClass(MSMPEntity.class); - Methods.register(NS); - Notifications.register(NS, () -> msmp); + Endpoints.register(NS, () -> msmp); ServerLifecycleEvents.SERVER_STARTED.register(server -> { NS.attach(server); diff --git a/src/main/java/dev/loat/msmp_entity/msmp/endpoints/Endpoints.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/Endpoints.java new file mode 100644 index 0000000..d60fc0a --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/Endpoints.java @@ -0,0 +1,58 @@ +package dev.loat.msmp_entity.msmp.endpoints; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp.MSMPServer; +import dev.loat.msmp_entity.msmp.endpoints.dimension.Dimension; +import dev.loat.msmp_entity.msmp.endpoints.dimension.DimensionSet; +import dev.loat.msmp_entity.msmp.endpoints.dimension.changed.DimensionChangedAdd; +import dev.loat.msmp_entity.msmp.endpoints.dimension.changed.DimensionChangedRemove; +import dev.loat.msmp_entity.msmp.endpoints.dimension.notification.changed.DimensionChanged; +import dev.loat.msmp_entity.msmp.endpoints.health.Health; +import dev.loat.msmp_entity.msmp.endpoints.health.HealthSet; +import dev.loat.msmp_entity.msmp.endpoints.items.Items; +import dev.loat.msmp_entity.msmp.endpoints.items.ItemsSet; +import dev.loat.msmp_entity.msmp.endpoints.position.Position; +import dev.loat.msmp_entity.msmp.endpoints.position.PositionSet; +import dev.loat.msmp_entity.msmp.endpoints.rotation.Rotation; +import dev.loat.msmp_entity.msmp.endpoints.rotation.RotationSet; +import dev.loat.msmp_entity.msmp.endpoints.uuid.UUID; + +import java.util.function.Supplier; + + +/** + * Central registration point for all {@code entity} MSMP endpoints. + * + *

Each endpoint is implemented in its own sub-package and registered here.

+ */ +public class Endpoints { + private Endpoints() {} + + /** + * Registers all endpoints on the given {@link MSMPNamespace}. + * + * @param namespace The namespace to register all endpoints under + * @param msmpServer A supplier for the MSMPServer instance, used by some endpoints to subscribe to server events + */ + public static void register(MSMPNamespace namespace, Supplier msmpServer) { + Dimension.register(namespace); + DimensionSet.register(namespace); + DimensionChanged.register(namespace, msmpServer); + DimensionChangedAdd.register(namespace); + DimensionChangedRemove.register(namespace); + + Health.register(namespace); + HealthSet.register(namespace); + + Items.register(namespace); + ItemsSet.register(namespace); + + Position.register(namespace); + PositionSet.register(namespace); + + Rotation.register(namespace); + RotationSet.register(namespace); + + UUID.register(namespace); + } +} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/Dimension.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/Dimension.java similarity index 55% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/Dimension.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/Dimension.java index 9b5fda2..e433d74 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/Dimension.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/Dimension.java @@ -1,44 +1,51 @@ -package dev.loat.msmp_entity.msmp.methods.dimension; +package dev.loat.msmp_entity.msmp.endpoints.dimension; import dev.loat.msmp.MSMPNamespace; -import dev.loat.msmp_entity.logging.Logger; +import dev.loat.msmp_entity.logging.RPCConnectionLogger; import dev.loat.msmp_entity.msmp.components.EntityRequest; import dev.loat.msmp_entity.msmp.components.EntityResolver; + import net.minecraft.world.entity.Entity; /** * Registers the {@code entity:dimension} MSMP method. - * - *

Returns the dimension of any loaded entity. - * Players can be looked up by UUID or name; all other entities require a UUID.

- * + * + *

Returns the current dimension of any loaded entity.

+ * *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:dimension",
- *   "params": [{ "name": "Steve" }] }
- * }
+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:dimension",
+ *   "params": [{ "name": "Steve" }]
+ * }
+ * 
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "dimension": "minecraft:overworld" }
- * }
+ *

+ * { 
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "dimension": "minecraft:overworld"
+ * }
+ * 
*/ public class Dimension { private Dimension() {} /** - * Registers the {@code entity:dimension} method on the given {@link MSMPNamespace}. + * Registers the {@code entity:dimension} method. * - *

Entity lookup is delegated to {@link EntityResolver#resolveEntity}. - * The dimension is returned as a resource key string via {@code identifier().toString()}, - * e.g. {@code minecraft:overworld}, {@code minecraft:the_nether}, {@code minecraft:the_end}.

+ *

The dimension is returned as a resource key string, ex. {@code minecraft:overworld}, + * {@code minecraft:the_nether}, {@code minecraft:the_end}.

* * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("dimension", + namespace.method( + "dimension", EntityRequest.SCHEMA, DimensionResponse.SCHEMA, "Returns the current dimension of any loaded entity by UUID, or a player by name", @@ -50,7 +57,7 @@ public static void register(MSMPNamespace namespace) { entity.level().dimension().identifier().toString() ); } catch (IllegalArgumentException e) { - Logger.warning("entity:dimension - " + e.getMessage()); + RPCConnectionLogger.warning(client.connectionId(), "entity:dimension - " + e.getMessage()); throw e; } } diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionResponse.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionResponse.java similarity index 74% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionResponse.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionResponse.java index f5191d5..bc2ceb7 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionResponse.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionResponse.java @@ -1,23 +1,24 @@ -package dev.loat.msmp_entity.msmp.methods.dimension; +package dev.loat.msmp_entity.msmp.endpoints.dimension; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.loat.msmp_entity.msmp.components.EntityRef; + import net.minecraft.server.jsonrpc.api.Schema; /** - * Response payload shared between {@code entity:dimension} and {@code entity:dimension/set}. + * Response payload for the {@code entity:dimension} and {@code entity:dimension/set} method. * - *

Example JSON representation:

- *
{@code
+ * 

Example response:

+ *

  * {
- *   "entity":    { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" },
+ *   "entity": { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" },
  *   "dimension": "minecraft:overworld"
  * }
- * }
+ *
* - * @param entity The entity reference; always includes UUID, name only for players + * @param entity The entity reference; always includes UUID, name only for players * @param dimension The resource key of the dimension the entity is currently in */ public record DimensionResponse(EntityRef entity, String dimension) { @@ -37,3 +38,4 @@ public record DimensionResponse(EntityRef entity, String dimension) { .withField("entity", EntityRef.SCHEMA) .withField("dimension", Schema.STRING_SCHEMA); } + diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionSet.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionSet.java similarity index 80% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionSet.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionSet.java index 1786c01..c258395 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionSet.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionSet.java @@ -1,14 +1,16 @@ -package dev.loat.msmp_entity.msmp.methods.dimension; +package dev.loat.msmp_entity.msmp.endpoints.dimension; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.RPCConnectionLogger; import dev.loat.msmp_entity.msmp.components.EntityResolver; +import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; + /** * Registers the {@code entity:dimension/set} MSMP method. * @@ -16,13 +18,20 @@ * *

Example request:

*
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:dimension/set",
- *   "params": [{ "name": "Steve", "dimension": "minecraft:the_nether" }] }
+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:dimension/set",
+ *   "params": [{ "name": "Steve", "dimension": "minecraft:the_nether" }]
+ * }
  * }
* *

Example response:

*
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "dimension": "minecraft:the_nether" }
+ * { 
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "dimension": "minecraft:the_nether"
+ * }
  * }
*/ public class DimensionSet { @@ -30,10 +39,9 @@ public class DimensionSet { private DimensionSet() {} /** - * Registers the {@code entity:dimension/set} method on the given {@link MSMPNamespace}. + * Registers the {@code entity:dimension/set} method. * - *

The entity is teleported to the target dimension at its current position and rotation. - * Throws {@link IllegalArgumentException} if the dimension identifier is unknown.

+ *

The entity is teleported to the target dimension at its current position and rotation.

* * @param namespace The namespace to register this method under */ @@ -46,10 +54,10 @@ public static void register(MSMPNamespace namespace) { try { Entity entity = EntityResolver.resolveEntity(server, params); - Identifier dimId = Identifier.parse(params.dimension()); + Identifier dimensionId = Identifier.parse(params.dimension()); ResourceKey dimKey = ResourceKey.create( - net.minecraft.core.registries.Registries.DIMENSION, - dimId + Registries.DIMENSION, + dimensionId ); ServerLevel targetLevel = server.getLevel(dimKey); if (targetLevel == null) { @@ -78,3 +86,4 @@ public static void register(MSMPNamespace namespace) { ); } } + diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionSetRequest.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionSetRequest.java similarity index 86% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionSetRequest.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionSetRequest.java index 50c81a9..8f32175 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/DimensionSetRequest.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/DimensionSetRequest.java @@ -1,12 +1,13 @@ -package dev.loat.msmp_entity.msmp.methods.dimension; +package dev.loat.msmp_entity.msmp.endpoints.dimension; + +import java.util.Optional; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.loat.msmp_entity.msmp.components.EntityLookup; -import net.minecraft.server.jsonrpc.api.Schema; -import java.util.Optional; +import net.minecraft.server.jsonrpc.api.Schema; /** @@ -15,12 +16,12 @@ *

Transfers the entity to the given dimension at its current position.

* *

Example JSON representation:

- *
{@code
+ * 

  * { "name": "Steve", "dimension": "minecraft:the_nether" }
- * }
+ *
* - * @param id The entity's UUID as a string, if provided - * @param name The player's in-game name, if provided (only works for online players) + * @param id The entity's UUID as a string, if provided + * @param name The player's in-game name, if provided (only works for online players) * @param dimension The target dimension resource key (e.g. {@code minecraft:the_nether}) */ public record DimensionSetRequest( @@ -46,3 +47,4 @@ public record DimensionSetRequest( .withField("name", Schema.STRING_SCHEMA) .withField("dimension", Schema.STRING_SCHEMA); } + diff --git a/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/changed/DimensionChangedAdd.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/changed/DimensionChangedAdd.java new file mode 100644 index 0000000..fda0ae4 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/changed/DimensionChangedAdd.java @@ -0,0 +1,89 @@ +package dev.loat.msmp_entity.msmp.endpoints.dimension.changed; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp_entity.logging.RPCConnectionLogger; +import dev.loat.msmp_entity.msmp.components.EntityRef; +import dev.loat.msmp_entity.msmp.components.EntityRequest; +import dev.loat.msmp_entity.msmp.components.EntityResolver; +import dev.loat.msmp_entity.msmp.subscription.SubscribeRequest; +import dev.loat.msmp_entity.msmp.subscription.SubscribeResponse; +import dev.loat.msmp_entity.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; + + +/** + * Registers the {@code entity:dimension/changed/add} MSMP subscription method. + * + *

Adds specified entities to the dimension change notification subscription list. + * When a subscribed entity changes dimensions, clients receive notifications. This method + * allows clients to subscribe to receiving notifications for the given entities.

+ * + *

Example request:

+ *

+ * {
+ *   "jsonrpc": "2.0", "id": 1, "method": "entity:dimension/changed/add",
+ *   "params": [{ "entities": [{ "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5" }] }]
+ * }
+ * 
+ * + *

Example response:

+ *

+ * { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" }
+ * 
+ */ +public class DimensionChangedAdd { + + private DimensionChangedAdd() {} + + /** + * Registers the {@code entity:dimension/changed/add} method on the given {@link MSMPNamespace}. + * + *

Resolves the provided entities and adds them to the dimension change notification + * subscription list. If the entity list is empty, returns an empty response immediately. + * Throws {@link IllegalArgumentException} if any entity cannot be resolved.

+ * + * @param namespace The namespace to register this method under + */ + public static void register(MSMPNamespace namespace) { + namespace.method( + "dimension/changed/add", + SubscribeRequest.SCHEMA, + SubscribeResponse.SCHEMA, + "Add entities to the dimension change notification list", + (server, params, client) -> { + if (params.entities().isEmpty()) { + return new SubscribeResponse(List.of()); + } + + SubscriptionManager manager = SubscriptionManager.get("entity:notification/dimension/changed"); + 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:dimension/changed/add - " + e.getMessage()); + throw e; + } + } + + manager.subscribe(uuids); + RPCConnectionLogger.info( + client.connectionId(), + "entity:dimension/changed/add - added %s to the dimension change notification list".formatted(uuids) + ); + return new SubscribeResponse(resolved); + } + ); + } +} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/changed/DimensionChangedRemove.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/changed/DimensionChangedRemove.java new file mode 100644 index 0000000..f0b41a6 --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/changed/DimensionChangedRemove.java @@ -0,0 +1,90 @@ +package dev.loat.msmp_entity.msmp.endpoints.dimension.changed; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp_entity.logging.RPCConnectionLogger; +import dev.loat.msmp_entity.msmp.components.EntityRef; +import dev.loat.msmp_entity.msmp.components.EntityRequest; +import dev.loat.msmp_entity.msmp.components.EntityResolver; +import dev.loat.msmp_entity.msmp.subscription.SubscribeRequest; +import dev.loat.msmp_entity.msmp.subscription.SubscribeResponse; +import dev.loat.msmp_entity.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; + +/** + * Registers the {@code entity:dimension/changed/remove} MSMP subscription method. + * + *

Removes specified entities from the dimension change notification subscription list. + * When a subscribed entity changes dimensions, clients receive notifications. This method + * allows clients to unsubscribe from receiving further notifications for the given entities.

+ * + *

Example request:

+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:dimension/changed/remove",
+ *   "params": [{ "entities": [{ "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5" }] }]
+ * }
+ * 
+ * + *

Example response:

+ *

+ * { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" }
+ * 
+ */ +public class DimensionChangedRemove { + + private DimensionChangedRemove() {} + + /** + * Registers the {@code entity:dimension/changed/remove} method on the given {@link MSMPNamespace}. + * + *

Resolves the provided entities and removes them from the dimension change notification + * subscription list. If the entity list is empty, returns an empty response immediately. + * Throws {@link IllegalArgumentException} if any entity cannot be resolved.

+ * + * @param namespace The namespace to register this method under + */ + public static void register(MSMPNamespace namespace) { + namespace.method( + "dimension/changed/remove", + SubscribeRequest.SCHEMA, + SubscribeResponse.SCHEMA, + "Remove entities from the dimension change notification list", + (server, params, client) -> { + if (params.entities().isEmpty()) { + return new SubscribeResponse(List.of()); + } + + SubscriptionManager manager = SubscriptionManager.get("entity:notification/dimension/changed"); + 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:dimension/changed/remove - " + e.getMessage()); + throw e; + } + } + + manager.unsubscribe(uuids); + RPCConnectionLogger.info( + client.connectionId(), + "entity:dimension/changed/remove - Removed %s from the dimension change notification list".formatted(uuids) + ); + return new SubscribeResponse(resolved); + } + ); + } +} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/notification/changed/DimensionChanged.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/notification/changed/DimensionChanged.java new file mode 100644 index 0000000..1c50aad --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/notification/changed/DimensionChanged.java @@ -0,0 +1,98 @@ +package dev.loat.msmp_entity.msmp.endpoints.dimension.notification.changed; + +import java.util.function.Supplier; + +import dev.loat.msmp.MSMPNamespace; +import dev.loat.msmp.MSMPNotification; +import dev.loat.msmp.MSMPServer; +import dev.loat.msmp_entity.msmp.components.EntityResolver; +import dev.loat.msmp_entity.msmp.subscription.SubscriptionManager; + +import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; + + +/** + * Registers the {@code entity:dimension/changed} MSMP notification method. + * + *

Fires when an entity changes dimension. Clients can subscribe to this notification to receive + * updates whenever a specified entity changes dimensions.

+ * + *

Example notification payload:

+ *

+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "origin": "minecraft:overworld",
+ *   "destination": "minecraft:the_nether"
+ * }
+ * 
+ */ +public class DimensionChanged { + + private DimensionChanged() {} + + /** + * Registers the {@code entity:dimension/changed} notification method and its associated event listeners. + * + *

Listens for entity dimension change events and dispatches notifications to subscribed clients. + * The notification is only sent if the entity that changed dimensions is currently subscribed to + * receive dimension change notifications.

+ * + * @param namespace The namespace to register this notification under + * @param msmpServer A supplier that provides access to the MSMPServer instance for sending notifications + */ + public static void register( + MSMPNamespace namespace, + Supplier msmpServer + ) { + MSMPNotification notification = + namespace.notification( + "dimension/changed", + DimensionChangedPayload.SCHEMA, + "Fires 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) + ); + } + + /** + * Dispatches the dimension change notification to subscribed clients. + * + * @param msmpServer A supplier that provides access to the MSMPServer instance for sending notifications + * @param notification The notification to dispatch + * @param entity The entity that changed dimensions + * @param origin The origin dimension + * @param destination The destination dimension + */ + private static void dispatch( + Supplier msmpServer, + MSMPNotification notification, + Entity entity, + ServerLevel origin, + ServerLevel destination + ) { + MSMPServer server = msmpServer.get(); + if (server == null) return; + + SubscriptionManager manager = SubscriptionManager.get("entity:notification/dimension/changed"); + 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); + } +} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/notifications/dimension/changed/DimensionChangedPayload.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/notification/changed/DimensionChangedPayload.java similarity index 93% rename from src/main/java/dev/loat/msmp_entity/msmp/notifications/dimension/changed/DimensionChangedPayload.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/notification/changed/DimensionChangedPayload.java index 2c2e885..55b89a5 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/notifications/dimension/changed/DimensionChangedPayload.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/dimension/notification/changed/DimensionChangedPayload.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.notifications.dimension.changed; +package dev.loat.msmp_entity.msmp.endpoints.dimension.notification.changed; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -10,13 +10,13 @@ * Payload for the {@code entity: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 diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/Health.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/Health.java similarity index 89% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/health/Health.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/Health.java index cab83a2..1b496d9 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/Health.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/Health.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.health; +package dev.loat.msmp_entity.msmp.endpoints.health; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; @@ -15,21 +15,23 @@ * Players can be looked up by UUID or name; all other entities require a UUID.

* *

Example request:

- *
{@code
+ * 

  * {
- *   "jsonrpc": "2.0", "id": 1, "method": "entity:health",
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:health",
  *   "params": [{ "name": "Steve" }]
  * }
- * }
+ *
* *

Example response:

- *
{@code
+ * 

  * {
  *   "entity": { "id": "069a...", "name": "Steve" },
  *   "health": 20.0,
  *   "max_health": 20.0
  * }
- * }
+ *
*/ public class Health { @@ -45,10 +47,11 @@ private Health() {} * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("health", + namespace.method( + "health", EntityRequest.SCHEMA, HealthResponse.SCHEMA, - "Returns the current and maximum health of any LivingEntity by UUID, or a player by name", + "Returns the current and maximum health of any LivingEntity", (server, params, client) -> { try { LivingEntity living = EntityResolver.resolveLivingEntity(server, params); diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthResponse.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthResponse.java similarity index 70% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthResponse.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthResponse.java index ef7ee40..fc4f61d 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthResponse.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthResponse.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.health; +package dev.loat.msmp_entity.msmp.endpoints.health; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -10,25 +10,17 @@ /** * Response payload shared between {@code entity:health} and {@code entity:health/set}. * - *

Example JSON representations:

- *
{@code
- * // Player:
+ * 

Example response:

+ *

  * {
- *   "entity":     { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" },
- *   "health":     20.0,
+ *   "entity": { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" },
+ *   "health": 20.0,
  *   "max_health": 20.0
  * }
+ * 
* - * // Non-player entity: - * { - * "entity": { "id": "1b3e9f2a-12cd-4b56-a832-ff1234567890" }, - * "health": 14.0, - * "max_health": 20.0 - * } - * }
- * - * @param entity Reference to the entity; always includes UUID, name only for players - * @param health The entity's current health points + * @param entity Reference to the entity; always includes UUID, name only for players + * @param health The entity's current health points * @param maxHealth The entity's maximum health points */ public record HealthResponse(EntityRef entity, double health, double maxHealth) { diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthSet.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthSet.java similarity index 79% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthSet.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthSet.java index 9cf825d..7782ac8 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthSet.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthSet.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.health; +package dev.loat.msmp_entity.msmp.endpoints.health; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; @@ -16,24 +16,38 @@ * Returns the actual values after the update.

* *

Example requests:

- *
{@code
- * // Set only health:
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:health/set",
- *   "params": [{ "name": "Steve", "health": 15.0 }] }
- *
- * // Set only max_health:
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:health/set",
- *   "params": [{ "name": "Steve", "max_health": 40.0 }] }
- *
- * // Set both:
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:health/set",
- *   "params": [{ "name": "Steve", "health": 15.0, "max_health": 40.0 }] }
- * }
+ *

+ * // Only set health
+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:health/set",
+ *   "params": [{ "name": "Steve", "health": 15.0 }]
+ * }
+ * // Only set max health
+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:health/set",
+ *   "params": [{ "name": "Steve", "max_health": 40.0 }]
+ * }
+ * // Set both
+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:health/set",
+ *   "params": [{ "name": "Steve", "health": 15.0, "max_health": 40.0 }]
+ * }
+ * 
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "health": 15.0, "max_health": 40.0 }
- * }
+ *

+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "health": 15.0,
+ *   "max_health": 40.0
+ * }
+ * 
*/ public class HealthSet { @@ -47,13 +61,11 @@ private HealthSet() {} * by Minecraft. If {@code health} is set, it is applied after {@code max_health} to ensure * the value is within the valid range.

* - *

Throws {@link IllegalArgumentException} if neither {@code health} nor - * {@code max_health} is provided, or if the entity is not a {@link LivingEntity}.

- * * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("health/set", + namespace.method( + "health/set", HealthSetRequest.SCHEMA, HealthResponse.SCHEMA, "Partially updates the health and/or maximum health of any LivingEntity", diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthSetRequest.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthSetRequest.java similarity index 82% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthSetRequest.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthSetRequest.java index 376c614..9c0af59 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/health/HealthSetRequest.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/health/HealthSetRequest.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.health; +package dev.loat.msmp_entity.msmp.endpoints.health; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -14,16 +14,19 @@ *

At least one of {@code health} or {@code max_health} must be present. * Only the provided fields are updated; omitted fields remain unchanged.

* - *

Example JSON representations:

- *
{@code
+ * 

Example request:

+ *

+ * // Only health is set
  * { "id": "069a...", "health": 15.0 }
+ * // Only max health is set
  * { "id": "069a...", "max_health": 40.0 }
+ * // Both are set
  * { "name": "Steve", "health": 15.0, "max_health": 40.0 }
- * }
- * - * @param id The entity's UUID as a string, if provided - * @param name The player's in-game name, if provided (only works for online players) - * @param health The new health value to set, if provided + *
+ * + * @param id The entity's UUID as a string, if provided + * @param name The player's in-game name, if provided (only works for online players) + * @param health The new health value to set, if provided * @param maxHealth The new maximum health value to set, if provided */ public record HealthSetRequest( diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/Inventory.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/Items.java similarity index 53% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/Inventory.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/Items.java index b6c9509..a3ebae1 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/Inventory.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/Items.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.inventory; +package dev.loat.msmp_entity.msmp.endpoints.items; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -8,12 +8,13 @@ import dev.loat.msmp_entity.logging.Logger; import dev.loat.msmp_entity.msmp.components.EntityRequest; import dev.loat.msmp_entity.msmp.components.EntityResolver; +import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; /** - * Registers the {@code entity:inventory} MSMP method. + * Registers the {@code entity:items} MSMP method. * *

Returns all occupied inventory slots of an online player in the Vanilla NBT * {@code Inventory} format, equivalent to {@code /data get entity @s Inventory}.

@@ -27,28 +28,36 @@ * * *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:inventory",
- *   "params": [{ "name": "Steve" }] }
- * }
+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:items",
+ *   "params": [{ "name": "Steve" }]
+ * }
+ * 
* *

Example response:

- *
{@code
+ * 

  * {
  *   "entity": { "id": "069a...", "name": "Steve" },
  *   "inventory": [
  *     { "Slot": 0, "id": "minecraft:diamond_sword", "count": 1, "components": { ... } },
- *     { "Slot": 36, "id": "minecraft:iron_boots", "count": 1 }
- *   ]
+ *     { "Slot": 20, "id": "minecraft:iron_boots", "count": 1 }
+ *   ],
+ *   "equipment": {
+ *     "head": { "id": "minecraft:diamond_helmet", "count": 1 },
+ *     "feet": { "id": "minecraft:diamond_boots", "count": 1 }
+ *   }
  * }
- * }
+ *
*/ -public class Inventory { +public class Items { - private Inventory() {} + private Items() {} /** - * Registers the {@code entity:inventory} method on the given {@link MSMPNamespace}. + * Registers the {@code entity:items} method on the given {@link MSMPNamespace}. * *

Entity lookup is delegated to {@link EntityResolver#resolvePlayer} since only * players have an inventory. Each occupied slot is serialized via {@link ItemStack#CODEC} @@ -58,19 +67,21 @@ private Inventory() {} * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("inventory", + namespace.method( + "items", EntityRequest.SCHEMA, - InventoryResponse.SCHEMA, + ItemsResponse.SCHEMA, "Returns all occupied inventory slots of an online player in Vanilla NBT format", (server, params, client) -> { try { Player player = EntityResolver.resolvePlayer(server, params); - net.minecraft.world.entity.player.Inventory inv = player.getInventory(); + Inventory items = player.getInventory(); - JsonArray inventory = new JsonArray(); + JsonArray inventoryItems = new JsonArray(); + JsonObject equipmentItems = new JsonObject(); - for (int slot = 0; slot < inv.getContainerSize(); slot++) { - ItemStack stack = inv.getItem(slot); + for (int slot = 0; slot < items.getContainerSize(); slot++) { + ItemStack stack = items.getItem(slot); if (stack.isEmpty()) continue; final int finalSlot = slot; @@ -81,13 +92,29 @@ public static void register(MSMPNamespace namespace) { )); JsonObject entry = itemJson.getAsJsonObject().deepCopy(); - entry.addProperty("Slot", slot); - inventory.add(entry); + entry.addProperty("count", entry.has("count") ? entry.get("count").getAsInt() : 1); + + if (slot >= 36 && slot <= 40) { + String equipmentKey = switch (slot) { + case 36 -> "feet"; + case 37 -> "legs"; + case 38 -> "chest"; + case 39 -> "head"; + case 40 -> "offhand"; + default -> null; + }; + if (equipmentKey != null) { + equipmentItems.add(equipmentKey, entry); + } + } else { + entry.addProperty("Slot", slot); + inventoryItems.add(entry); + } } - return new InventoryResponse(EntityResolver.toEntityRef(player), inventory); + return new ItemsResponse(EntityResolver.toEntityRef(player), inventoryItems, equipmentItems); } catch (IllegalArgumentException e) { - Logger.warning("entity:inventory - " + e.getMessage()); + Logger.warning("entity:items - " + e.getMessage()); throw e; } } diff --git a/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsResponse.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsResponse.java new file mode 100644 index 0000000..49a907b --- /dev/null +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsResponse.java @@ -0,0 +1,73 @@ +package dev.loat.msmp_entity.msmp.endpoints.items; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import dev.loat.msmp_entity.msmp.components.EntityRef; +import net.minecraft.server.jsonrpc.api.Schema; + + +/** + * Response payload shared between {@code entity:items} and {@code entity:items/set}. + * + *

The {@code inventory} field mirrors the Vanilla NBT {@code Inventory} format exactly — + * each entry contains {@code Slot}, {@code id}, {@code count}, and optionally {@code components}.

+ * + *

The {@code equipment} field is an object with keys for each equipped item slot: + * {@code head}, {@code chest}, {@code legs}, {@code feet}, {@code offhand}.

+ * + *

Example JSON representation:

+ *
{@code
+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "inventory": [
+ *     { "Slot": 0, "id": "minecraft:diamond_sword", "count": 1, "components": { ... } },
+ *     { "Slot": 20, "id": "minecraft:iron_boots", "count": 1 }
+ *   ],
+ *   "equipment": {
+ *     "head": { "id": "minecraft:diamond_helmet", "count": 1 },
+ *     "feet": { "id": "minecraft:diamond_boots", "count": 1 }
+ *   }
+ * }
+ * }
+ * + * @param entity The entity reference; always includes UUID, name only for players + * @param inventory The occupied inventory slots in Vanilla NBT format + * @param equipment The equipped items keyed by slot name (head, chest, legs, feet, offhand) + */ +public record ItemsResponse(EntityRef entity, JsonElement inventory, JsonElement equipment) { + + /** + * Codec for passing {@link JsonElement} through the serialization pipeline without modification. + */ + public static final Codec JSON_ELEMENT_CODEC = Codec.PASSTHROUGH.xmap( + dynamic -> dynamic.convert(JsonOps.INSTANCE).getValue(), + json -> new Dynamic<>(JsonOps.INSTANCE, json) + ); + + private static final Schema INVENTORY_SCHEMA = + Schema.ofType("array", JSON_ELEMENT_CODEC); + + private static final Schema EQUIPMENT_SCHEMA = + Schema.ofType("object", JSON_ELEMENT_CODEC); + + /** + * Codec for serializing and deserializing {@link ItemsResponse} instances. + */ + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + EntityRef.CODEC.fieldOf("entity").forGetter(ItemsResponse::entity), + JSON_ELEMENT_CODEC.fieldOf("inventory").forGetter(ItemsResponse::inventory), + JSON_ELEMENT_CODEC.fieldOf("equipment").forGetter(ItemsResponse::equipment) + ).apply(i, ItemsResponse::new)); + + /** + * MSMP schema for {@link ItemsResponse}, used for protocol discovery. + */ + public static final Schema SCHEMA = Schema.record(CODEC) + .withField("entity", EntityRef.SCHEMA) + .withField("inventory", INVENTORY_SCHEMA) + .withField("equipment", EQUIPMENT_SCHEMA); +} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventorySet.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsSet.java similarity index 54% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventorySet.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsSet.java index 46da9d9..7ae1557 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventorySet.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsSet.java @@ -1,4 +1,6 @@ -package dev.loat.msmp_entity.msmp.methods.inventory; +package dev.loat.msmp_entity.msmp.endpoints.items; + +import java.util.Map; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -12,7 +14,7 @@ /** - * Registers the {@code entity:inventory/set} MSMP method. + * Registers the {@code entity:items/set} MSMP method. * *

Partially updates an online player's inventory using a diff approach — * only the provided slots are modified, all others remain unchanged. @@ -21,31 +23,36 @@ *

To clear a slot, provide {@code "id": "minecraft:air"}.

* *

Example request:

- *
{@code
+ * 

  * {
- *   "jsonrpc": "2.0", "id": 1, "method": "entity:inventory/set",
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:items/set",
  *   "params": [{
  *     "name": "Steve",
  *     "inventory": [
  *       { "Slot": 0, "id": "minecraft:diamond_sword", "count": 1 },
  *       { "Slot": 9, "id": "minecraft:air", "count": 0 }
- *     ]
+ *     ],
+ *     "equipment": {
+ *       "head": { "id": "minecraft:diamond_helmet", "count": 1 }
+ *     }
  *   }]
  * }
- * }
+ *
* *

Example response:

- *
{@code
+ * 

  * {
  *   "entity": { "id": "069a...", "name": "Steve" },
  *   "inventory": [ ... ]
  * }
- * }
+ *
*/ -public class InventorySet { +public class ItemsSet { /** - * Registers the {@code entity:inventory/set} method on the given {@link MSMPNamespace}. + * Registers the {@code entity:items/set} method on the given {@link MSMPNamespace}. * *

Each entry in the {@code inventory} array must contain a {@code Slot} key (integer). * The {@code Slot} key is stripped before passing the entry to {@link ItemStack#CODEC} for @@ -57,12 +64,13 @@ public class InventorySet { * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("inventory/set", - InventorySetRequest.SCHEMA, - InventoryResponse.SCHEMA, + namespace.method( + "items/set", + ItemsSetRequest.SCHEMA, + ItemsResponse.SCHEMA, "Partially updates an online player's inventory using a diff approach", (server, params, client) -> { - InventorySetRequest req = params; + ItemsSetRequest req = params; try { Player player = EntityResolver.resolvePlayer(server, req); net.minecraft.world.entity.player.Inventory inv = player.getInventory(); @@ -103,8 +111,50 @@ public static void register(MSMPNamespace namespace) { inv.setItem(slot, stack); } - // Re-serialize full inventory + if (req.equipment().isJsonObject()) { + var equipmentObj = req.equipment().getAsJsonObject(); + var equipmentSlots = Map.ofEntries( + Map.entry("feet", 36), + Map.entry("legs", 37), + Map.entry("chest", 38), + Map.entry("head", 39), + Map.entry("offhand", 40) + ); + + for (var key : equipmentObj.keySet()) { + if (!equipmentSlots.containsKey(key)) { + throw new IllegalArgumentException( + "Invalid equipment slot '%s' — must be one of: head, chest, legs, feet, offhand".formatted(key) + ); + } + + int slot = equipmentSlots.get(key); + JsonElement itemElement = equipmentObj.get(key); + + if (!itemElement.isJsonObject()) { + throw new IllegalArgumentException("Equipment entry for '%s' must be a JSON object".formatted(key)); + } + + JsonObject entry = itemElement.getAsJsonObject().deepCopy(); + + // Clear the slot if no id given or id is minecraft:air + boolean isEmpty = !entry.has("id") + || entry.get("id").getAsString().equals("minecraft:air"); + + ItemStack stack = isEmpty ? ItemStack.EMPTY : ItemStack.CODEC + .decode(ctx, entry) + .getOrThrow(err -> new IllegalArgumentException( + "Failed to deserialize equipment for '%s': %s".formatted(key, err) + )) + .getFirst(); + + inv.setItem(slot, stack); + } + } + JsonArray inventory = new JsonArray(); + JsonObject equipment = new JsonObject(); + for (int slot = 0; slot < containerSize; slot++) { ItemStack stack = inv.getItem(slot); if (stack.isEmpty()) continue; @@ -117,13 +167,28 @@ public static void register(MSMPNamespace namespace) { )); JsonObject responseEntry = itemJson.getAsJsonObject().deepCopy(); - responseEntry.addProperty("Slot", slot); - inventory.add(responseEntry); + + if (slot >= 36 && slot <= 40) { + String equipmentKey = switch (slot) { + case 36 -> "feet"; + case 37 -> "legs"; + case 38 -> "chest"; + case 39 -> "head"; + case 40 -> "offhand"; + default -> null; + }; + if (equipmentKey != null) { + equipment.add(equipmentKey, responseEntry); + } + } else { + responseEntry.addProperty("Slot", slot); + inventory.add(responseEntry); + } } - return new InventoryResponse(EntityResolver.toEntityRef(player), inventory); + return new ItemsResponse(EntityResolver.toEntityRef(player), inventory, equipment); } catch (IllegalArgumentException e) { - Logger.warning("entity:inventory/set - " + e.getMessage()); + Logger.warning("entity:items/set - " + e.getMessage()); throw e; } } diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventorySetRequest.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsSetRequest.java similarity index 53% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventorySetRequest.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsSetRequest.java index f24a9bc..7167551 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventorySetRequest.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/items/ItemsSetRequest.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.inventory; +package dev.loat.msmp_entity.msmp.endpoints.items; import com.google.gson.JsonElement; import com.mojang.serialization.Codec; @@ -19,24 +19,29 @@ * To clear a slot, provide {@code "id": "minecraft:air"}.

* *

Example JSON representation:

- *
{@code
+ * 

  * {
  *   "name": "Steve",
  *   "inventory": [
  *     { "Slot": 0, "id": "minecraft:diamond_sword", "count": 1 },
  *     { "Slot": 9, "id": "minecraft:air", "count": 0 }
- *   ]
+ *   ],
+ *   "equipment": {
+ *     "head": { "id": "minecraft:diamond_helmet", "count": 1 }
+ *   }
  * }
- * }
+ *
* - * @param id The entity's UUID as a string, if provided - * @param name The player's in-game name, if provided (only works for online players) + * @param id The entity's UUID as a string, if provided + * @param name The player's in-game name, if provided (only works for online players) * @param inventory The list of slot updates in Vanilla NBT format + * @param equipment The equipment updates keyed by slot name (head, chest, legs, feet, offhand) */ -public record InventorySetRequest( +public record ItemsSetRequest( Optional id, Optional name, - JsonElement inventory + JsonElement inventory, + JsonElement equipment ) implements EntityLookup { /** @@ -50,20 +55,25 @@ public record InventorySetRequest( private static final Schema INVENTORY_SCHEMA = Schema.ofType("array", JSON_ELEMENT_CODEC); + private static final Schema EQUIPMENT_SCHEMA = + Schema.ofType("object", JSON_ELEMENT_CODEC); + /** - * Codec for serializing and deserializing {@link InventorySetRequest} instances. + * Codec for serializing and deserializing {@link ItemsSetRequest} instances. */ - public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - Codec.STRING.optionalFieldOf("id").forGetter(InventorySetRequest::id), - Codec.STRING.optionalFieldOf("name").forGetter(InventorySetRequest::name), - JSON_ELEMENT_CODEC.fieldOf("inventory").forGetter(InventorySetRequest::inventory) - ).apply(i, InventorySetRequest::new)); + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + Codec.STRING.optionalFieldOf("id").forGetter(ItemsSetRequest::id), + Codec.STRING.optionalFieldOf("name").forGetter(ItemsSetRequest::name), + JSON_ELEMENT_CODEC.fieldOf("inventory").forGetter(ItemsSetRequest::inventory), + JSON_ELEMENT_CODEC.fieldOf("equipment").forGetter(ItemsSetRequest::equipment) + ).apply(i, ItemsSetRequest::new)); /** - * MSMP schema for {@link InventorySetRequest}, used for protocol discovery. + * MSMP schema for {@link ItemsSetRequest}, used for protocol discovery. */ - public static final Schema SCHEMA = Schema.record(CODEC) + public static final Schema SCHEMA = Schema.record(CODEC) .withField("id", Schema.STRING_SCHEMA) .withField("name", Schema.STRING_SCHEMA) - .withField("inventory", INVENTORY_SCHEMA); + .withField("inventory", INVENTORY_SCHEMA) + .withField("equipment", EQUIPMENT_SCHEMA); } diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/Position.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/Position.java similarity index 77% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/position/Position.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/Position.java index 24db664..c42899e 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/Position.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/Position.java @@ -1,13 +1,15 @@ -package dev.loat.msmp_entity.msmp.methods.position; +package dev.loat.msmp_entity.msmp.endpoints.position; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; import dev.loat.msmp_entity.msmp.components.EntityRequest; import dev.loat.msmp_entity.msmp.components.EntityResolver; + import net.minecraft.world.entity.Entity; import java.util.List; + /** * Registers the {@code entity:position} MSMP method. * @@ -15,15 +17,22 @@ * Players can be looked up by UUID or name; all other entities require a UUID.

* *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:position",
- *   "params": [{ "name": "Steve" }] }
- * }
+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:position",
+ *   "params": [{ "name": "Steve" }]
+ * }
+ * 
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "position": [128.5, 64.0, -32.3] }
- * }
+ *

+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "position": [128.5, 64.0, -32.3]
+ * }
+ * 
*/ public class Position { @@ -38,10 +47,11 @@ private Position() {} * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("position", + namespace.method( + "position", EntityRequest.SCHEMA, PositionResponse.SCHEMA, - "Returns the current position of any loaded entity by UUID, or a player by name", + "Returns the current position of any loaded entity", (server, params, client) -> { try { Entity entity = EntityResolver.resolveEntity(server, params); diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionResponse.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionResponse.java similarity index 89% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionResponse.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionResponse.java index 578968e..be18b01 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionResponse.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionResponse.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.position; +package dev.loat.msmp_entity.msmp.endpoints.position; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -10,15 +10,15 @@ /** - * Response payload shared between {@code entity:position} and {@code entity:position/set}. + * Response payload for {@code entity:position} and {@code entity:position/set}. * *

Example JSON representation:

- *
{@code
+ * 

  * {
  *   "entity":   { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" },
  *   "position": [128.5, 64.0, -32.3]
  * }
- * }
+ *
* * @param entity The entity reference; always includes UUID, name only for players * @param position The entity's position as {@code [x, y, z]} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionSet.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionSet.java similarity index 86% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionSet.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionSet.java index 1ce3ec6..48e8af0 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionSet.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionSet.java @@ -1,8 +1,9 @@ -package dev.loat.msmp_entity.msmp.methods.position; +package dev.loat.msmp_entity.msmp.endpoints.position; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; import dev.loat.msmp_entity.msmp.components.EntityResolver; + import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; @@ -14,15 +15,22 @@ *

Teleports the entity to the given position within its current dimension.

* *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:position/set",
- *   "params": [{ "name": "Steve", "position": [100.0, 64.0, 200.0] }] }
- * }
+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:position/set",
+ *   "params": [{ "name": "Steve", "position": [100.0, 64.0, 200.0] }]
+ * }
+ * 
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "position": [100.0, 64.0, 200.0] }
- * }
+ *

+ * { 
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "position": [100.0, 64.0, 200.0]
+ * }
+ * 
*/ public class PositionSet { @@ -37,7 +45,8 @@ private PositionSet() {} * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("position/set", + namespace.method( + "position/set", PositionSetRequest.SCHEMA, PositionResponse.SCHEMA, "Teleports any loaded entity to the given position within its current dimension", diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionSetRequest.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionSetRequest.java similarity index 83% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionSetRequest.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionSetRequest.java index 43c9309..9b5076d 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/position/PositionSetRequest.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/position/PositionSetRequest.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.position; +package dev.loat.msmp_entity.msmp.endpoints.position; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -14,12 +14,15 @@ * Request payload for the {@code entity:position/set} method. * *

Example JSON representation:

- *
{@code
- * { "name": "Steve", "position": [100.0, 64.0, 200.0] }
- * }
+ *

+ * {
+ *   "name": "Steve",
+ *   "position": [100.0, 64.0, 200.0]
+ * }
+ * 
* - * @param id The entity's UUID as a string, if provided - * @param name The player's in-game name, if provided (only works for online players) + * @param id The entity's UUID as a string, if provided + * @param name The player's in-game name, if provided (only works for online players) * @param position The target position as {@code [x, y, z]} */ public record PositionSetRequest( diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/Rotation.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/Rotation.java similarity index 81% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/Rotation.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/Rotation.java index 0bddaf8..48bce37 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/Rotation.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/Rotation.java @@ -1,9 +1,10 @@ -package dev.loat.msmp_entity.msmp.methods.rotation; +package dev.loat.msmp_entity.msmp.endpoints.rotation; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; import dev.loat.msmp_entity.msmp.components.EntityRequest; import dev.loat.msmp_entity.msmp.components.EntityResolver; + import net.minecraft.world.entity.Entity; import java.util.List; @@ -16,15 +17,22 @@ * Players can be looked up by UUID or name; all other entities require a UUID.

* *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:rotation",
- *   "params": [{ "name": "Steve" }] }
- * }
+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:rotation",
+ *   "params": [{ "name": "Steve" }]
+ * }
+ * 
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "rotation": [90.0, -15.0] }
- * }
+ *

+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "rotation": [90.0, -15.0]
+ * }
+ * 
*/ public class Rotation { @@ -39,7 +47,8 @@ private Rotation() {} * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("rotation", + namespace.method( + "rotation", EntityRequest.SCHEMA, RotationResponse.SCHEMA, "Returns the current rotation of any loaded entity by UUID, or a player by name", diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationResponse.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationResponse.java similarity index 84% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationResponse.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationResponse.java index e9622e6..a0f05fd 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationResponse.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationResponse.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.rotation; +package dev.loat.msmp_entity.msmp.endpoints.rotation; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -10,17 +10,17 @@ /** - * Response payload shared between {@code entity:rotation} and {@code entity:rotation/set}. + * Response payload for {@code entity:rotation} and {@code entity:rotation/set}. * *

Example JSON representation:

- *
{@code
+ * 

  * {
  *   "entity":   { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" },
  *   "rotation": [90.0, -15.0]
  * }
- * }
+ *
* - * @param entity The entity reference; always includes UUID, name only for players + * @param entity The entity reference; always includes UUID, name only for players * @param rotation The entity's rotation as {@code [yaw, pitch]} */ public record RotationResponse(EntityRef entity, List rotation) { diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationSet.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationSet.java similarity index 85% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationSet.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationSet.java index 9512a4c..e197723 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationSet.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationSet.java @@ -1,8 +1,9 @@ -package dev.loat.msmp_entity.msmp.methods.rotation; +package dev.loat.msmp_entity.msmp.endpoints.rotation; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; import dev.loat.msmp_entity.msmp.components.EntityResolver; + import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; @@ -15,15 +16,22 @@ *

Sets the rotation of any loaded entity. Position and dimension are preserved.

* *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:rotation/set",
- *   "params": [{ "name": "Steve", "rotation": [90.0, -15.0] }] }
- * }
+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:rotation/set",
+ *   "params": [{ "name": "Steve", "rotation": [90.0, -15.0] }]
+ * }
+ * 
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "rotation": [90.0, -15.0] }
- * }
+ *

+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "rotation": [90.0, -15.0]
+ * }
+ * 
*/ public class RotationSet { @@ -38,7 +46,8 @@ private RotationSet() {} * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("rotation/set", + namespace.method( + "rotation/set", RotationSetRequest.SCHEMA, RotationResponse.SCHEMA, "Sets the rotation of any loaded entity, preserving its position and dimension", diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationSetRequest.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationSetRequest.java similarity index 83% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationSetRequest.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationSetRequest.java index 21d11d9..67203e3 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/rotation/RotationSetRequest.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/rotation/RotationSetRequest.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.rotation; +package dev.loat.msmp_entity.msmp.endpoints.rotation; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -14,12 +14,15 @@ * Request payload for the {@code entity:rotation/set} method. * *

Example JSON representation:

- *
{@code
- * { "name": "Steve", "rotation": [90.0, -15.0] }
- * }
+ *

+ * {
+ *   "name": "Steve",
+ *   "rotation": [90.0, -15.0]
+ * }
+ * 
* - * @param id The entity's UUID as a string, if provided - * @param name The player's in-game name, if provided (only works for online players) + * @param id The entity's UUID as a string, if provided + * @param name The player's in-game name, if provided (only works for online players) * @param rotation The target rotation as {@code [yaw, pitch]} */ public record RotationSetRequest( diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/Saturation.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/Saturation.java similarity index 81% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/Saturation.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/Saturation.java index 2e3af0c..a9e62ae 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/Saturation.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/Saturation.java @@ -1,9 +1,10 @@ -package dev.loat.msmp_entity.msmp.methods.saturation; +package dev.loat.msmp_entity.msmp.endpoints.saturation; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; import dev.loat.msmp_entity.msmp.components.EntityRequest; import dev.loat.msmp_entity.msmp.components.EntityResolver; + import net.minecraft.world.entity.player.Player; @@ -13,15 +14,23 @@ *

Returns the current food level and saturation of an online player.

* *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:saturation",
- *   "params": [{ "name": "Steve" }] }
- * }
+ *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:saturation",
+ *   "params": [{ "name": "Steve" }]
+ * }
+ * 
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "food": 18, "saturation": 5.0 }
- * }
+ *

+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "food": 18,
+ *   "saturation": 5.0
+ * }
+ * 
*/ public class Saturation { @@ -37,7 +46,8 @@ private Saturation() {} * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("saturation", + namespace.method( + "saturation", EntityRequest.SCHEMA, SaturationResponse.SCHEMA, "Returns the current food level and saturation of an online player", diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationResponse.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationResponse.java similarity index 81% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationResponse.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationResponse.java index 580a111..b9ab86b 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationResponse.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationResponse.java @@ -1,9 +1,10 @@ -package dev.loat.msmp_entity.msmp.methods.saturation; +package dev.loat.msmp_entity.msmp.endpoints.saturation; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.loat.msmp_entity.msmp.components.EntityRef; + import net.minecraft.server.jsonrpc.api.Schema; @@ -11,16 +12,16 @@ * Response payload shared between {@code entity:saturation} and {@code entity:saturation/set}. * *

Example JSON representation:

- *
{@code
+ * 

  * {
- *   "entity":     { "id": "069a...", "name": "Steve" },
- *   "food":       18,
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "food": 18,
  *   "saturation": 5.0
  * }
  * }
* - * @param entity The entity reference; always includes UUID and name (players only) - * @param food The player's current food level (0–20) + * @param entity The entity reference; always includes UUID and name (players only) + * @param food The player's current food level (0–20) * @param saturation The player's current saturation level (0.0–20.0) */ public record SaturationResponse(EntityRef entity, int food, double saturation) { diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationSet.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationSet.java similarity index 79% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationSet.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationSet.java index 562429e..7871b4c 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationSet.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationSet.java @@ -1,8 +1,9 @@ -package dev.loat.msmp_entity.msmp.methods.saturation; +package dev.loat.msmp_entity.msmp.endpoints.saturation; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; import dev.loat.msmp_entity.msmp.components.EntityResolver; + import net.minecraft.world.entity.player.Player; import net.minecraft.world.food.FoodData; @@ -15,24 +16,40 @@ * Returns the actual values after the update.

* *

Example requests:

- *
{@code
+ * 

  * // Set only food:
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:saturation/set",
- *   "params": [{ "name": "Steve", "food": 20 }] }
+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:saturation/set",
+ *   "params": [{ "name": "Steve", "food": 20 }]
+ * }
  *
  * // Set only saturation:
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:saturation/set",
- *   "params": [{ "name": "Steve", "saturation": 10.0 }] }
+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": 1,
+ *   "method": "entity:saturation/set",
+ *   "params": [{ "name": "Steve", "saturation": 10.0 }]
+ * }
  *
  * // Set both:
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:saturation/set",
- *   "params": [{ "name": "Steve", "food": 20, "saturation": 10.0 }] }
- * }
+ * { + * "jsonrpc": "2.0", + * "id": 1, + * "method": "entity:saturation/set", + * "params": [{ "name": "Steve", "food": 20, "saturation": 10.0 }] + * } + *
* *

Example response:

- *
{@code
- * { "entity": { "id": "069a...", "name": "Steve" }, "food": 20, "saturation": 10.0 }
- * }
+ *

+ * {
+ *   "entity": { "id": "069a...", "name": "Steve" },
+ *   "food": 20,
+ *   "saturation": 10.0
+ * }
+ * 
*/ public class SaturationSet { @@ -44,13 +61,11 @@ private SaturationSet() {} *

Food level is clamped to 0–20 by Minecraft internally. * Saturation is clamped to {@code 0.0–foodLevel} by Minecraft internally.

* - *

Throws {@link IllegalArgumentException} if neither {@code food} nor - * {@code saturation} is provided.

- * * @param namespace The namespace to register this method under */ public static void register(MSMPNamespace namespace) { - namespace.method("saturation/set", + namespace.method( + "saturation/set", SaturationSetRequest.SCHEMA, SaturationResponse.SCHEMA, "Partially updates the food level and/or saturation of an online player", diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationSetRequest.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationSetRequest.java similarity index 85% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationSetRequest.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationSetRequest.java index a89086c..4ccc19b 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/saturation/SaturationSetRequest.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/saturation/SaturationSetRequest.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.saturation; +package dev.loat.msmp_entity.msmp.endpoints.saturation; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -16,15 +16,15 @@ * Only the provided fields are updated; omitted fields remain unchanged.

* *

Example JSON representations:

- *
{@code
+ * 

  * { "name": "Steve", "food": 20 }
  * { "name": "Steve", "saturation": 10.0 }
  * { "name": "Steve", "food": 20, "saturation": 10.0 }
- * }
+ *
* - * @param id The entity's UUID as a string, if provided - * @param name The player's in-game name, if provided (only works for online players) - * @param food The new food level to set (0–20), if provided + * @param id The entity's UUID as a string, if provided + * @param name The player's in-game name, if provided (only works for online players) + * @param food The new food level to set (0–20), if provided * @param saturation The new saturation level to set (0.0–20.0), if provided */ public record SaturationSetRequest( diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/uuid/UUID.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/uuid/UUID.java similarity index 89% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/uuid/UUID.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/uuid/UUID.java index 5b7e69f..924bfb9 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/uuid/UUID.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/uuid/UUID.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.uuid; +package dev.loat.msmp_entity.msmp.endpoints.uuid; import dev.loat.msmp.MSMPNamespace; import dev.loat.msmp_entity.logging.Logger; @@ -13,18 +13,23 @@ * which always includes the UUID and the confirmed in-game name.

* *

Example request:

- *
{@code
- * { "jsonrpc": "2.0", "id": 1, "method": "entity:uuid",
- *   "params": [{ "name": "Steve" }] }
+ * 
+ * {@code {
+ *   "jsonrpc": "2.0", "id": 1, "method": "entity:uuid",
+ *   "params": [{ "name": "Steve" }]
+ * }
  * }
* *

Example response:

- *
{@code
+ * 
+ * {@code
  * { "id": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Steve" }
  * }
*/ public class UUID { + private UUID() {} + /** * Registers the {@code entity:uuid} method on the given {@link MSMPNamespace}. * diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/uuid/UUIDRequest.java b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/uuid/UUIDRequest.java similarity index 94% rename from src/main/java/dev/loat/msmp_entity/msmp/methods/uuid/UUIDRequest.java rename to src/main/java/dev/loat/msmp_entity/msmp/endpoints/uuid/UUIDRequest.java index c050968..76d094e 100644 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/uuid/UUIDRequest.java +++ b/src/main/java/dev/loat/msmp_entity/msmp/endpoints/uuid/UUIDRequest.java @@ -1,4 +1,4 @@ -package dev.loat.msmp_entity.msmp.methods.uuid; +package dev.loat.msmp_entity.msmp.endpoints.uuid; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/Methods.java b/src/main/java/dev/loat/msmp_entity/msmp/methods/Methods.java deleted file mode 100644 index 7915977..0000000 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/Methods.java +++ /dev/null @@ -1,54 +0,0 @@ -package dev.loat.msmp_entity.msmp.methods; - -import dev.loat.msmp.MSMPNamespace; -import dev.loat.msmp_entity.msmp.methods.dimension.Dimension; -import dev.loat.msmp_entity.msmp.methods.dimension.DimensionSet; -import dev.loat.msmp_entity.msmp.methods.dimension.subscribe.DimensionSubscribe; -import dev.loat.msmp_entity.msmp.methods.dimension.subscribe.DimensionUnsubscribe; -import dev.loat.msmp_entity.msmp.methods.health.Health; -import dev.loat.msmp_entity.msmp.methods.health.HealthSet; -import dev.loat.msmp_entity.msmp.methods.inventory.Inventory; -import dev.loat.msmp_entity.msmp.methods.inventory.InventorySet; -import dev.loat.msmp_entity.msmp.methods.position.Position; -import dev.loat.msmp_entity.msmp.methods.position.PositionSet; -import dev.loat.msmp_entity.msmp.methods.rotation.Rotation; -import dev.loat.msmp_entity.msmp.methods.rotation.RotationSet; -import dev.loat.msmp_entity.msmp.methods.saturation.Saturation; -import dev.loat.msmp_entity.msmp.methods.saturation.SaturationSet; -import dev.loat.msmp_entity.msmp.methods.uuid.UUID; - - -/** - * Central registration point for all {@code entity} MSMP methods. - * - *

Each method is implemented in its own sub-package and registered here. - * Call {@link #register(MSMPNamespace)} once during mod initialization, before - * the server starts.

- */ -public class Methods { - - private Methods() {} - - /** - * Registers all {@code entity} methods on the given {@link MSMPNamespace}. - * - * @param namespace The namespace to register all methods under - */ - 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); - InventorySet.register(namespace); - Position.register(namespace); - PositionSet.register(namespace); - Rotation.register(namespace); - RotationSet.register(namespace); - Saturation.register(namespace); - SaturationSet.register(namespace); - UUID.register(namespace); - } -} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/subscribe/DimensionSubscribe.java b/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/subscribe/DimensionSubscribe.java deleted file mode 100644 index 24a3023..0000000 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/subscribe/DimensionSubscribe.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.loat.msmp_entity.msmp.methods.dimension.subscribe; - -import dev.loat.msmp.MSMPNamespace; -import dev.loat.msmp_entity.logging.RPCConnectionLogger; -import dev.loat.msmp_entity.msmp.components.EntityRef; -import dev.loat.msmp_entity.msmp.components.EntityRequest; -import dev.loat.msmp_entity.msmp.components.EntityResolver; -import dev.loat.msmp_entity.msmp.subscription.SubscribeRequest; -import dev.loat.msmp_entity.msmp.subscription.SubscribeResponse; -import dev.loat.msmp_entity.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: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:dimension/subscribe - " + e.getMessage()); - throw e; - } - } - - manager.subscribe(uuids); - RPCConnectionLogger.info(client.connectionId(), "entity:dimension/subscribe - subscribed to %s".formatted(uuids)); - return new SubscribeResponse(resolved); - } - ); - } -} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java b/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java deleted file mode 100644 index 3d5c4cd..0000000 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/dimension/subscribe/DimensionUnsubscribe.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.loat.msmp_entity.msmp.methods.dimension.subscribe; - -import dev.loat.msmp.MSMPNamespace; -import dev.loat.msmp_entity.logging.RPCConnectionLogger; -import dev.loat.msmp_entity.msmp.components.EntityRef; -import dev.loat.msmp_entity.msmp.components.EntityRequest; -import dev.loat.msmp_entity.msmp.components.EntityResolver; -import dev.loat.msmp_entity.msmp.subscription.SubscribeRequest; -import dev.loat.msmp_entity.msmp.subscription.SubscribeResponse; -import dev.loat.msmp_entity.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: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:dimension/unsubscribe - " + e.getMessage()); - throw e; - } - } - - manager.unsubscribe(uuids); - RPCConnectionLogger.info(client.connectionId(), "entity:dimension/unsubscribe - unsubscribed from %s".formatted(uuids)); - return new SubscribeResponse(resolved); - } - ); - } -} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventoryResponse.java b/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventoryResponse.java deleted file mode 100644 index 9c564e2..0000000 --- a/src/main/java/dev/loat/msmp_entity/msmp/methods/inventory/InventoryResponse.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.loat.msmp_entity.msmp.methods.inventory; - -import com.google.gson.JsonElement; -import com.mojang.serialization.Codec; -import com.mojang.serialization.Dynamic; -import com.mojang.serialization.JsonOps; -import com.mojang.serialization.codecs.RecordCodecBuilder; - -import dev.loat.msmp_entity.msmp.components.EntityRef; -import net.minecraft.server.jsonrpc.api.Schema; - - -/** - * Response payload shared between {@code entity:inventory} and {@code entity:inventory/set}. - * - *

The {@code inventory} field mirrors the Vanilla NBT {@code Inventory} format exactly — - * each entry contains {@code Slot}, {@code id}, {@code count}, and optionally {@code components}.

- * - *

Example JSON representation:

- *
{@code
- * {
- *   "entity": { "id": "069a...", "name": "Steve" },
- *   "inventory": [
- *     { "Slot": 0, "id": "minecraft:diamond_sword", "count": 1, "components": { ... } },
- *     { "Slot": 36, "id": "minecraft:iron_boots", "count": 1 }
- *   ]
- * }
- * }
- * - * @param entity The entity reference; always includes UUID, name only for players - * @param inventory The occupied inventory slots in Vanilla NBT format - */ -public record InventoryResponse(EntityRef entity, JsonElement inventory) { - - /** - * Codec for passing {@link JsonElement} through the serialization pipeline without modification. - */ - public static final Codec JSON_ELEMENT_CODEC = Codec.PASSTHROUGH.xmap( - dynamic -> dynamic.convert(JsonOps.INSTANCE).getValue(), - json -> new Dynamic<>(JsonOps.INSTANCE, json) - ); - - private static final Schema INVENTORY_SCHEMA = - Schema.ofType("array", JSON_ELEMENT_CODEC); - - /** - * Codec for serializing and deserializing {@link InventoryResponse} instances. - */ - public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( - EntityRef.CODEC.fieldOf("entity").forGetter(InventoryResponse::entity), - JSON_ELEMENT_CODEC.fieldOf("inventory").forGetter(InventoryResponse::inventory) - ).apply(i, InventoryResponse::new)); - - /** - * MSMP schema for {@link InventoryResponse}, used for protocol discovery. - */ - public static final Schema SCHEMA = Schema.record(CODEC) - .withField("entity", EntityRef.SCHEMA) - .withField("inventory", INVENTORY_SCHEMA); -} diff --git a/src/main/java/dev/loat/msmp_entity/msmp/notifications/Notifications.java b/src/main/java/dev/loat/msmp_entity/msmp/notifications/Notifications.java deleted file mode 100644 index 4196fb3..0000000 --- a/src/main/java/dev/loat/msmp_entity/msmp/notifications/Notifications.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.loat.msmp_entity.msmp.notifications; - -import dev.loat.msmp.MSMPNamespace; -import dev.loat.msmp.MSMPServer; -import dev.loat.msmp_entity.msmp.notifications.dimension.changed.DimensionChanged; - - -/** - * Central registration point for all {@code entity} 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:notification/dimension/changed} — Fired when an entity changes dimension
  • - *
- */ -public class Notifications { - - private Notifications() {} - - /** - * Registers all {@code entity} 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/msmp/notifications/dimension/changed/DimensionChanged.java b/src/main/java/dev/loat/msmp_entity/msmp/notifications/dimension/changed/DimensionChanged.java deleted file mode 100644 index 8ba1e38..0000000 --- a/src/main/java/dev/loat/msmp_entity/msmp/notifications/dimension/changed/DimensionChanged.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.loat.msmp_entity.msmp.notifications.dimension.changed; - -import dev.loat.msmp.MSMPNamespace; -import dev.loat.msmp.MSMPNotification; -import dev.loat.msmp.MSMPServer; -import dev.loat.msmp_entity.msmp.components.EntityResolver; -import dev.loat.msmp_entity.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: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/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 104f0f7..da3dd08 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "msmp-data", "version": "${version}", "name": "MSMP Entity", - "description": "Extends the Minecraft Server Management Protocol (MSMP) by providing additional functions for querying and setting entity data", + "description": "Extends the Minecraft Server Management Protocol (MSMP) by providing additional functions for getting and setting entity data", "authors": [], "contact": {}, "license": "LGPL-3.0-only", @@ -24,7 +24,7 @@ "custom": { "mc-publish": { "modrinth": { - "project_id": "---", + "project_id": "KoGT0iGr", "loaders": [ "fabric" ]