Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 79 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repositories {
}

dependencies {
implementation("com.github.YOUR_USERNAME:msmp-lib:VERSION")
implementation("com.github.MinecraftPlayground:msmp-lib-mod:VERSION")
}
```

Expand All @@ -34,20 +34,20 @@ Payloads are simple records that describe the data sent and received over MSMP.
Each payload needs a `Codec` for serialization and a `Schema` for MSMP discovery.

```java
public record PingPayload(String message) {
public record EchoPayload(String message) {

public static final Codec<PingPayload> CODEC = RecordCodecBuilder.create(i -> i.group(
Codec.STRING.fieldOf("message").forGetter(PingPayload::message)
).apply(i, PingPayload::new));
public static final Codec<EchoPayload> CODEC = RecordCodecBuilder.create(i -> i.group(
Codec.STRING.fieldOf("message").forGetter(EchoPayload::message)
).apply(i, EchoPayload::new));

public static final Schema<PingPayload> SCHEMA = Schema.record(CODEC)
public static final Schema<EchoPayload> SCHEMA = Schema.record(CODEC)
.withField("message", Schema.STRING_SCHEMA);
}
```

### 2. Register methods and notifications

Create a `MsmpNamespace` and register methods and notifications in `onInitialize()`
Create a `MSMPNamespace` and register methods and notifications in `onInitialize()` -
before the server starts and the registry is frozen.

Use `attach()` and `detach()` in the server lifecycle events to bind and release
Expand All @@ -56,26 +56,34 @@ the server instance for use in method handlers.
```java
public class MyMod implements ModInitializer {

private static final MsmpNamespace NS = new MsmpNamespace("my_mod");
private static final MSMPNamespace NS = new MSMPNamespace("my_mod");

// Registered immediately as static fields — before the server starts
private static final MsmpNotification<PingPayload> PING =
NS.notification("ping", PingPayload.SCHEMA, "A ping notification");
// Registered as a field - before the server starts
private static final MSMPNotification<EchoPayload> PING =
NS.notification("ping")
.responseSchema(EchoPayload.SCHEMA)
.description("A ping notification")
.register();

private static MsmpServer msmp;
private static MSMPServer msmp;

@Override
public void onInitialize() {
// Methods are registered here — registry is still open
NS.method("echo",
EchoPayload.SCHEMA,
EchoPayload.SCHEMA,
"Echoes a message back to the client",
(server, params, client) -> {
// Register a method that receives a parameter
NS.method("echo")
.requestSchema(EchoPayload.SCHEMA)
.responseSchema(EchoPayload.SCHEMA)
.description("Echoes a message back to the client")
.register((server, client, params) -> {
System.out.println("Called by connection: " + client.connectionId());
return params;
}
);
});

// Register a method without parameters
NS.method("get_time")
.responseSchema(TimePayload.SCHEMA)
.description("Returns the current game time")
.register((server, client) -> new TimePayload(server.getGameTime()));

ServerLifecycleEvents.SERVER_STARTED.register(server -> {
NS.attach(server); // bind server for use in method handlers
Expand All @@ -90,52 +98,84 @@ public class MyMod implements ModInitializer {

// Broadcast a notification to all connected clients
public static void sendPing() {
if (msmp != null) msmp.send(PING, new PingPayload("hello"));
if (msmp != null) msmp.send(PING, new EchoPayload("hello"));
}
}
```

### Method types

**Methods with request and response payload:**

```java
NS.method("echo")
.requestSchema(EchoPayload.SCHEMA)
.responseSchema(EchoPayload.SCHEMA)
.description("Echoes a message back to the client")
.register((server, client, params) -> {
System.out.println("Received: " + params.message());
return params;
});
```

**Parameterless methods (no request payload):**

```java
NS.method("get_time")
.responseSchema(TimePayload.SCHEMA)
.description("Returns the current game time")
.register((server, client) -> new TimePayload(server.getGameTime()));
```

### Handler parameters

The method handler receives three parameters:
The method handler signature depends on whether the method expects parameters:

**With parameters:** `(server, client, params) -> result`

| Parameter | Type | Description |
|---|---|---|
| `server` | `MinecraftServer` | The running Minecraft server instance |
| `params` | `Param` | The payload received from the client |
| `client` | `ClientInfo` | Info about the calling client, including `connectionId()` |
| Parameter | Type | Description |
|-----------|-------------------|:--------------------------------------|
| `server` | `MinecraftServer` | The running Minecraft server instance |
| `client` | `ClientInfo` | Info about the calling client |
| `params` | `Param` | The payload received from the client |

**Without parameters:** `(server, client) -> result`

| Parameter | Type | Description |
|-----------|-------------------|:--------------------------------------|
| `server` | `MinecraftServer` | The running Minecraft server instance |
| `client` | `ClientInfo` | Info about the calling client |

### Sending notifications

**Broadcast to all connected clients:**

```java
msmp.send(PING, new PingPayload("hello"));
msmp.send(PING, new EchoPayload("hello"));
```

**Send to a specific client** using the `connectionId` from a method handler:

```java
NS.method("echo",
EchoPayload.SCHEMA,
EchoPayload.SCHEMA,
"Echoes a message back to the calling client only",
(server, params, client) -> {
msmp.sendTo(client.connectionId(), PING, new PingPayload("only for you"));
return params;
}
);
NS.method("notify_client")
.requestSchema(RequestPayload.SCHEMA)
.responseSchema(ResponsePayload.SCHEMA)
.description("Sends a notification only to the calling client")
.register((server, client, params) -> {
msmp.sendTo(client.connectionId(), PING, new EchoPayload("only for you"));
return new ResponsePayload("sent");
});
```

### JSON-RPC examples

**Calling a method** (`my_mod:method/echo`):
**Calling a method** (`my_mod:echo`):

```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "my_mod:method/echo",
"method": "my_mod:echo",
"params": [{
"message": "hello"
}]
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ loom_version=1.16-SNAPSHOT
# check this on https://fabricmc.net/develop/

# Mod Properties
mod_version=1.4.0
mod_version=2.0.0
maven_group=dev.loat
archives_base_name=msmp-lib-mod

Expand Down
79 changes: 51 additions & 28 deletions src/main/java/dev/loat/msmp/MSMPMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,83 @@
import net.minecraft.server.jsonrpc.IncomingRpcMethod;
import net.minecraft.server.jsonrpc.api.Schema;


/**
* Represents an incoming MSMP method that can be called by connected clients.
*
* <p>Instances are created via
* {@link MSMPNamespace#method(String, Schema, Schema, java.util.function.BiFunction)} or
* {@link MSMPNamespace#method(String, Schema, Schema, String, java.util.function.BiFunction)}
* <p>Instances are created via {@link dev.loat.msmp.builder.MethodBuilderWithSchemas#register(MSMPMethodHandler)}
* or {@link dev.loat.msmp.builder.MethodBuilderWithoutParameters#register(MSMPMethodHandlerWithoutParameters)}
* and are registered in the MSMP registry immediately upon creation.</p>
*
* <pre>{@code
* msmp.namespace("my_mod")
* .method("get_time",
* VoidPayload.SCHEMA,
* TimePayload.SCHEMA,
* "Returns the current game time",
* (server, params) -> new TimePayload(server.getGameTime())
* );
* // With request payload:
* NS.method("echo")
* .requestSchema(EchoPayload.SCHEMA)
* .responseSchema(EchoPayload.SCHEMA)
* .description("Echoes a message back to the client")
* .register((server, client, params) -> params);
*
* // Without request payload:
* NS.method("get_time")
* .responseSchema(TimePayload.SCHEMA)
* .description("Returns the current game time")
* .register((server, client) -> new TimePayload(server.getGameTime()));
* }</pre>
*
* @param <Param> The type of the payload received from the client
* @param <Param> The type of the payload received from the client, or {@link Void} for methods without parameters
* @param <Result> The type of the payload returned to the client
*/
public final class MSMPMethod<Param, Result> {

/**
* Creates and registers a new incoming method under the given namespace.
* Package-private — use
* {@link MSMPNamespace#method(String, Schema, Schema, java.util.function.BiFunction)} or
* {@link MSMPNamespace#method(String, Schema, Schema, String, java.util.function.BiFunction)}.
* Creates and registers a new incoming method.
*
* @param namespace The namespace to register this method under (e.g. {@code "my_mod"})
* @param name The name of this method (e.g. {@code "get_time"}),
* resulting in the identifier {@code namespace:name}
* @param namespace The namespace to register this method under
* @param name The name of this method
* @param paramSchema The schema describing the payload received from the client
* @param resultSchema The schema describing the payload returned to the client
* @param description A human-readable description of this method
* @param handler The function to invoke when this method is called by a client
* @param rpcFunction The raw RPC function to invoke when this method is called
*/
@SuppressWarnings("unchecked")
MSMPMethod(
public MSMPMethod(
String namespace,
String name,
Schema<Param> paramSchema,
Schema<Result> resultSchema,
String description,
IncomingRpcMethod.RpcMethodFunction<Param, Result> handler
IncomingRpcMethod.RpcMethodFunction<Param, Result> rpcFunction
) {
Identifier id = Identifier.fromNamespaceAndPath(namespace, name);
((IncomingRpcMethodBuilderAccessor<Param, Result>)
IncomingRpcMethod.<Param, Result>method(rpcFunction)
.description(description)
.param(name, paramSchema)
.response(name, resultSchema)
).invokeRegister(BuiltInRegistries.INCOMING_RPC_METHOD, id);
}

/**
* Creates and registers a new incoming method without parameters.
*
* @param namespace The namespace to register this method under
* @param name The name of this method
* @param resultSchema The schema describing the payload returned to the client
* @param description A human-readable description of this method
* @param rpcFunction The raw RPC function without parameters to invoke when this method is called
*/
@SuppressWarnings("unchecked")
public MSMPMethod(
String namespace,
String name,
Schema<Result> resultSchema,
String description,
IncomingRpcMethod.ParameterlessRpcMethodFunction<Result> rpcFunction
) {
Identifier id = Identifier.fromNamespaceAndPath(namespace, name);

((IncomingRpcMethodBuilderAccessor<Param, Result>) IncomingRpcMethod
.<Param, Result>method(handler)
.description(description)
.param(name, paramSchema)
.response(name, resultSchema)
((IncomingRpcMethodBuilderAccessor<Void, Result>)
IncomingRpcMethod.<Result>method(rpcFunction)
.description(description)
.response(name, resultSchema)
).invokeRegister(BuiltInRegistries.INCOMING_RPC_METHOD, id);
}
}
4 changes: 2 additions & 2 deletions src/main/java/dev/loat/msmp/MSMPMethodHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public interface MSMPMethodHandler<Param, Result> {
* Handles an incoming method call from a connected client.
*
* @param server The running {@link MinecraftServer} instance
* @param params The payload received from the client
* @param client The {@link ClientInfo} of the calling client,
* providing access to the client's {@link ClientInfo#connectionId()}
* @param params The payload received from the client
* @return The payload to return to the client
*/
Result apply(MinecraftServer server, Param params, ClientInfo client);
Result apply(MinecraftServer server, ClientInfo client, Param params);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.loat.msmp;

import net.minecraft.server.MinecraftServer;
import net.minecraft.server.jsonrpc.methods.ClientInfo;

/**
* Functional interface for handling incoming MSMP method calls without parameters.
*
* <p>Use this when no request payload is expected from the client.</p>
*
* @param <Result> The type of the payload returned to the client
*/
@FunctionalInterface
public interface MSMPMethodHandlerWithoutParameters<Result> {

/**
* Handles an incoming method call without parameters from a connected client.
*
* @param server The running {@link MinecraftServer} instance
* @param client The {@link ClientInfo} of the calling client
* @return the payload to return to the client
*/
Result apply(MinecraftServer server, ClientInfo client);
}
Loading