From 424e7aa550a2d5dcd4d41c1ea0c24383b95a795d Mon Sep 17 00:00:00 2001
From: Mqx <62719703+Mqxx@users.noreply.github.com>
Date: Sun, 12 Jul 2026 21:11:32 +0200
Subject: [PATCH 1/7] feat: big refactor
---
...new_method_or_notification_suggesetion.yml | 142 ++++++++++++++++++
.github/workflows/build.yml | 77 +++++++---
build.gradle | 21 +--
.../client/MSMPConsoleDataGenerator.java | 12 --
.../resources/msmp-console.client.mixins.json | 14 --
.../dev/loat/msmp_console/MSMPConsole.java | 131 +++-------------
.../dev/loat/msmp_console/config/Config.java | 108 ++-----------
.../msmp_console/config/ConfigManager.java | 82 ----------
.../config/annotation/Comment.java | 16 --
.../config/files/MSMPConsoleConfigFile.java | 23 ---
.../config/files/MSMPEntityConfigFile.java | 14 ++
.../config/parser/YamlConfig.java | 132 ----------------
.../constructor/ComponentConstructor.java | 105 -------------
.../representer/ComponentRepresenter.java | 52 -------
.../logging/ConsoleNotificationAppender.java | 56 +++----
.../msmp/endpoints/Endpoints.java | 29 ++++
.../event/NotificationLogEvent.java | 47 ++++++
.../event/NotificationLogEventPayload.java | 58 +++++++
.../msmp/endpoints/send/Send.java | 23 +++
.../msmp/endpoints/send/SendRequest.java | 35 +++++
.../msmp/endpoints/send/SendResponse.java | 41 +++++
src/main/resources/fabric.mod.json | 13 +-
22 files changed, 508 insertions(+), 723 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE/new_method_or_notification_suggesetion.yml
delete mode 100644 src/client/java/dev/loat/msmp_console/client/MSMPConsoleDataGenerator.java
delete mode 100644 src/client/resources/msmp-console.client.mixins.json
delete mode 100644 src/main/java/dev/loat/msmp_console/config/ConfigManager.java
delete mode 100644 src/main/java/dev/loat/msmp_console/config/annotation/Comment.java
delete mode 100644 src/main/java/dev/loat/msmp_console/config/files/MSMPConsoleConfigFile.java
create mode 100644 src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java
delete mode 100644 src/main/java/dev/loat/msmp_console/config/parser/YamlConfig.java
delete mode 100644 src/main/java/dev/loat/msmp_console/config/parser/constructor/ComponentConstructor.java
delete mode 100644 src/main/java/dev/loat/msmp_console/config/parser/representer/ComponentRepresenter.java
create mode 100644 src/main/java/dev/loat/msmp_console/msmp/endpoints/Endpoints.java
create mode 100644 src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java
create mode 100644 src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEventPayload.java
create mode 100644 src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java
create mode 100644 src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendRequest.java
create mode 100644 src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendResponse.java
diff --git a/.github/ISSUE_TEMPLATE/new_method_or_notification_suggesetion.yml b/.github/ISSUE_TEMPLATE/new_method_or_notification_suggesetion.yml
new file mode 100644
index 0000000..38e1d50
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new_method_or_notification_suggesetion.yml
@@ -0,0 +1,142 @@
+name: 'New Method/Notification Suggestion'
+description: Suggest a new MSMP RPC method or notification, along with the functionality it should provide.
+title: '[Suggestion] '
+labels: ['enhancement']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to suggest a new method/notification!
+
+ Please follow the existing naming and structure conventions where possible, ex.:
+ - `console:foo` - get current state
+ - `console:foo/set` - partially update state
+ - `console:foo/changed`, `console:foo/changed/add`, `console:foo/changed/remove` - manage the notification tracker
+ - `console:notification/foo/changed` - the actual pushed event
+
+ You don't need to fill out every field perfectly - just give us as much detail as you can.
+
+ - type: dropdown
+ id: kind
+ attributes:
+ label: 'What are you suggesting?'
+ description: |
+ Methods are request/response (called by the client). Notifications are pushed by the server when something happens.
+
+ options:
+ - 'New method (request/response)'
+ - 'New notification (server-pushed event)'
+ - 'Both a method and a matching notification'
+ - 'Not sure/something else'
+
+ validations:
+ required: true
+
+ - type: textarea
+ id: kind-other
+ attributes:
+ label: 'If "Not sure/something else" - please elaborate'
+ description: |
+ Leave this empty if it doesn't apply to you.
+
+ placeholder: ex. a config option, a change to an existing method, something else entirely...
+
+ validations:
+ required: false
+
+ - type: textarea
+ id: use-case
+ attributes:
+ label: 'Use Case'
+ description: |
+ Why do you want this? What benefit would that have for you? What tooling, dashboard, or automation would this enable?
+
+ validations:
+ required: true
+
+ - type: input
+ id: name
+ attributes:
+ label: 'Proposed Name'
+ description: |
+ Follow the existing naming convention if this fits an existing family (ex. foo, bar, bazz).
+
+ placeholder: 'console:foo, console:bar, console:bazz, ...'
+
+ validations:
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: 'What should it do?'
+ description: |
+ Describe the functionality in detail. What data does it return or accept? Or, for a notification, what event does it represent and when should it fire?
+
+ placeholder: |
+ - `console:foo` - Get current state of foo
+ - `console:foo/set` - Set current state of foo
+ - `console:foo/changed` - Returns a list of all tracked entities for the foo changed event
+ - `console:foo/changed/add` - Add entities to the foo change notification tracker
+ - `console:foo/changed/remove` - Remove entities from the foo change notification tracker
+
+ validations:
+ required: true
+
+ - type: textarea
+ id: example-payload
+ attributes:
+ label: 'Example request/response/notification payload'
+ render: json
+ description: |
+ If you have an idea of the JSON shape, share it here. Not required, but it helps a lot.
+
+ placeholder: |
+ {
+ "jsonrpc": "2.0",
+ "method": "console:foo/set",
+ "params": [{
+ "name": "Steve",
+ "foo": 15
+ }],
+ "id": 1
+ }
+
+ validations:
+ required: false
+
+ - type: textarea
+ id: related
+ attributes:
+ label: 'Related existing methods or notifications'
+ description: |
+ Is there a similar existing method/notification this should be modeled after (ex. `console:health/changed`, `console:position/set`)?
+
+ validations:
+ required: false
+
+ - type: dropdown
+ id: needs-config
+ attributes:
+ label: 'Would this need configurable settings?'
+ description: |
+ Some change-notifications use polling with a configurable interval/threshold (ex. position's `block-delta`), while others are purely event-driven with no settings.
+
+ options:
+ - 'No, it should just work without configuration'
+ - 'Yes, it would need some configurable settings'
+ - 'Not sure'
+
+ validations:
+ required: true
+
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: 'Checks'
+ options:
+ - label: "I searched existing issues and this hasn't been suggested yet"
+ required: true
+
+ - label: 'This is a request for this mod specifically (not Minecraft itself or another mod)'
+ required: false
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 979cb8c..a3f53b9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,17 +6,18 @@ env:
on:
workflow_call: {}
workflow_dispatch: {}
+
push:
paths:
- - 'build.gradle'
- - 'gradle.properties'
- - 'gradlew'
- - 'gradlew.bat'
- - 'settings.gradle'
- - '.github/workflows/build.yml'
- - 'gradle/**'
- - 'src/**'
-
+ - build.gradle
+ - gradle.properties
+ - gradlew
+ - gradlew.bat
+ - settings.gradle
+ - .github/workflows/build.yml
+ - gradle/**
+ - src/**
+
pull_request:
branches:
- main
@@ -26,32 +27,62 @@ permissions:
jobs:
build:
- runs-on: ubuntu-latest
name: 'Build'
+ runs-on: ubuntu-latest
+
steps:
- - uses: actions/checkout@v4
-
+ - name: 'Checkout Repository'
+ uses: actions/checkout@v6
+
- name: 'Setup JDK'
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
- java-version: '25'
- distribution: 'temurin'
+ distribution: temurin
+ java-version: "25"
- name: 'Setup Gradle'
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v6
with:
build-scan-publish: true
- build-scan-terms-of-use-agree: 'yes'
+ build-scan-terms-of-use-agree: "yes"
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
- name: 'Build with Gradle Wrapper'
- run: |-
+ run: |
./gradlew build --scan
+
+ echo "Files:"
ls -la build/libs/
- - name: 'Upload Artifact'
- uses: actions/upload-artifact@v4
- id: save-artifact
+ - name: 'Upload Workflow Artifact'
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ env.ARTIFACT_NAME }}
+ path: build/libs/*.jar
+
+ - name: 'Read mod version'
+ id: version
+ run: |
+ VERSION=$(grep '^mod_version=' gradle.properties | cut -d= -f2)
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+
+ - name: 'Check if tag exists'
+ id: tag
+ run: |
+ if git ls-remote --exit-code --tags origin "refs/tags/v${{ steps.version.outputs.version }}" > /dev/null 2>&1; then
+ echo "exists=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "exists=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: 'Create GitHub Release'
+ if: >
+ github.event_name == 'push' &&
+ github.ref == 'refs/heads/main' &&
+ steps.tag.outputs.exists == 'false'
+ uses: softprops/action-gh-release@v3
with:
- name: '${{ env.ARTIFACT_NAME }}'
- path: '${{ github.workspace }}/build/libs/*.jar'
+ tag_name: v${{ steps.version.outputs.version }}
+ name: v${{ steps.version.outputs.version }}
+ generate_release_notes: true
+ files: build/libs/*.jar
diff --git a/build.gradle b/build.gradle
index 5f2a683..76b26cd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,29 +6,13 @@ plugins {
version = project.mod_version
group = project.maven_group
-loom {
- splitEnvironmentSourceSets()
-
- mods {
- "msmp-console" {
- sourceSet sourceSets.main
- sourceSet sourceSets.client
- }
- }
-}
-
-fabricApi {
- configureDataGeneration {
- client = true
- }
-}
-
repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
+ maven { url = uri("https://jitpack.io") }
}
dependencies {
@@ -39,7 +23,8 @@ dependencies {
implementation("net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}")
- implementation(include('org.yaml:snakeyaml:2.2'))
+ implementation(include("com.github.MinecraftPlayground:msmp-lib-mod:v2.1.1"))
+ implementation(include("com.github.MinecraftPlayground:config-lib-mod:v1.3.5"))
}
processResources {
diff --git a/src/client/java/dev/loat/msmp_console/client/MSMPConsoleDataGenerator.java b/src/client/java/dev/loat/msmp_console/client/MSMPConsoleDataGenerator.java
deleted file mode 100644
index 54db6cc..0000000
--- a/src/client/java/dev/loat/msmp_console/client/MSMPConsoleDataGenerator.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package dev.loat.msmp_console.client;
-
-import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
-import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
-
-public class MSMPConsoleDataGenerator implements DataGeneratorEntrypoint {
-
- @Override
- public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
- FabricDataGenerator.Pack pack = fabricDataGenerator.createPack();
- }
-}
diff --git a/src/client/resources/msmp-console.client.mixins.json b/src/client/resources/msmp-console.client.mixins.json
deleted file mode 100644
index dcb2e06..0000000
--- a/src/client/resources/msmp-console.client.mixins.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "required": true,
- "minVersion": "0.8",
- "package": "dev.loat.msmp_console.mixin.client",
- "compatibilityLevel": "JAVA_21",
- "client": [
- ],
- "injectors": {
- "defaultRequire": 1
- },
- "overwrites": {
- "requireAnnotations": true
- }
-}
diff --git a/src/main/java/dev/loat/msmp_console/MSMPConsole.java b/src/main/java/dev/loat/msmp_console/MSMPConsole.java
index 73e1ebb..0f180ef 100644
--- a/src/main/java/dev/loat/msmp_console/MSMPConsole.java
+++ b/src/main/java/dev/loat/msmp_console/MSMPConsole.java
@@ -1,24 +1,17 @@
package dev.loat.msmp_console;
+import dev.loat.msmp.MSMPNamespace;
+import dev.loat.msmp.MSMPServer;
import dev.loat.msmp_console.config.Config;
-import dev.loat.msmp_console.config.ConfigManager;
-import dev.loat.msmp_console.config.files.MSMPConsoleConfigFile;
import dev.loat.msmp_console.logging.ConsoleNotificationAppender;
import dev.loat.msmp_console.logging.Logger;
-import dev.loat.msmp_console.mixin.ManagementServerAccessor;
import dev.loat.msmp_console.mixin.OutgoingRpcMethodBuilderAccessor;
+import dev.loat.msmp_console.msmp.endpoints.Endpoints;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
-import net.minecraft.core.Holder;
-import net.minecraft.resources.Identifier;
-import net.minecraft.server.MinecraftServer;
import net.minecraft.server.jsonrpc.ManagementServer;
import net.minecraft.server.jsonrpc.OutgoingRpcMethod;
-import java.lang.reflect.Field;
-
-import org.apache.logging.log4j.LogManager;
-
/**
* Main entrypoint of the MSMP Console mod.
@@ -41,31 +34,16 @@
public class MSMPConsole implements ModInitializer {
/**
- * The registered JSON-RPC notification method for console log messages.
- *
- *
Registered under {@code console:notification/message} using
- * {@link OutgoingRpcMethodBuilderAccessor#invokeRegister(Identifier)} to bypass
- * the default {@code minecraft:notification/} namespace that the public
- * {@code register(String)} overload would apply.
- *
- * The {@code @SuppressWarnings("unchecked")} is required because the cast from
- * {@link OutgoingRpcMethod.OutgoingRpcMethodBuilder} to
- * {@link OutgoingRpcMethodBuilderAccessor} is a generic unchecked cast that is
- * safe at runtime due to Mixin's bytecode transformation.
+ * The shared {@code entity} namespace used for all MSMP registrations.
+ * Attached to the running server in {@code SERVER_STARTED} and detached in {@code SERVER_STOPPED}.
*/
- @SuppressWarnings("unchecked")
- public static final Holder.Reference> CONSOLE_MESSAGE =
- ((OutgoingRpcMethodBuilderAccessor)
- OutgoingRpcMethod.notificationWithParams()
- .description("A server console log message")
- .param("message", ConsoleLogPayload.SCHEMA)
- ).invokeRegister(Identifier.fromNamespaceAndPath("console", "notification/log_event"));
+ private static final MSMPNamespace NS = new MSMPNamespace("console");
/**
- * The cached {@link ManagementServer} instance, set on {@code SERVER_STARTED}
- * and cleared on {@code SERVER_STOPPED}. {@code null} if the server is not running.
+ * Provides access to the {@link net.minecraft.server.jsonrpc.ManagementServer}
+ * for broadcasting notifications. {@code null} when no server is running.
*/
- private static ManagementServer managementServer;
+ private static MSMPServer msmp;
/**
* Called by Fabric when the mod is initialized.
@@ -76,94 +54,19 @@ public class MSMPConsole implements ModInitializer {
public void onInitialize() {
Logger.setLoggerClass(MSMPConsole.class);
- ConfigManager.addConfig(new Config<>(
- ConfigManager.resolve("config.yml"),
- MSMPConsoleConfigFile.class
- ));
-
- registerLogAppender();
+ Config.register();
+
+ Endpoints.register(NS, () -> msmp);
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
- managementServer = getManagementServer(server);
+ NS.attach(server);
+ msmp = new MSMPServer(server);
});
ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
- managementServer = null;
+ NS.detach();
+ msmp = null;
});
- Logger.info("MSMP Console initialized.");
- }
-
- /**
- * Attaches a {@link ConsoleNotificationAppender} to the root Log4j2 logger.
- *
- * The appender is created programmatically so no {@code log4j2.xml}
- * configuration is required.
- */
- private void registerLogAppender() {
- org.apache.logging.log4j.core.LoggerContext ctx = (org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false);
- org.apache.logging.log4j.core.Logger root = ctx.getRootLogger();
- ConsoleNotificationAppender appender =
- ConsoleNotificationAppender.createAppender("ConsoleNotificationAppender");
- appender.start();
- root.addAppender(appender);
- }
-
- /**
- * Finds the {@link ManagementServer} instance held by the given {@link MinecraftServer}
- * by traversing its class hierarchy via reflection.
- *
- * The field ({@code jsonRpcServer} in {@code DedicatedServer}) is not publicly
- * accessible, so reflection is used to locate the first field of type
- * {@link ManagementServer} in the class hierarchy.
- *
- * @param server the running {@link MinecraftServer} instance
- * @return the {@link ManagementServer} instance, or {@code null} if not found
- */
- private static ManagementServer getManagementServer(MinecraftServer server) {
- Class> clazz = server.getClass();
- while (clazz != null) {
- for (Field field : clazz.getDeclaredFields()) {
- if (field.getType() == ManagementServer.class) {
- try {
- field.setAccessible(true);
- ManagementServer ms = (ManagementServer) field.get(server);
- if (ms != null) return ms;
- } catch (Exception ignored) {}
- }
- }
- clazz = clazz.getSuperclass();
- }
- return null;
- }
-
- /**
- * Sends a console log notification to all connected MSMP clients.
- *
- * Called by {@link ConsoleNotificationAppender} for every intercepted log event.
- * Does nothing if the {@link ManagementServer} is not yet available (i.e. before
- * {@code SERVER_STARTED} or after {@code SERVER_STOPPED}).
- *
- * The {@link ConsoleNotificationAppender.LogPayload} is mapped to a
- * {@link ConsoleLogPayload} with {@code null} throwables replaced by empty strings
- * to satisfy the non-null codec contract.
- *
- * @param payload the log payload captured by {@link ConsoleNotificationAppender}
- */
- public static void sendConsoleNotification(ConsoleNotificationAppender.LogPayload payload) {
- if (managementServer == null) return;
-
- ConsoleLogPayload rpcPayload = new ConsoleLogPayload(
- payload.timestamp(),
- payload.level(),
- payload.thread(),
- payload.logger(),
- payload.message(),
- payload.throwable() != null ? payload.throwable() : ""
- );
-
- ((ManagementServerAccessor) managementServer)
- .invokeForEachConnection(conn ->
- conn.sendNotification(CONSOLE_MESSAGE, rpcPayload)
- );
+ Logger.info("Mod initialized.");
}
}
diff --git a/src/main/java/dev/loat/msmp_console/config/Config.java b/src/main/java/dev/loat/msmp_console/config/Config.java
index 61efcb9..871771c 100644
--- a/src/main/java/dev/loat/msmp_console/config/Config.java
+++ b/src/main/java/dev/loat/msmp_console/config/Config.java
@@ -1,105 +1,23 @@
package dev.loat.msmp_console.config;
-import dev.loat.msmp_console.config.parser.YamlConfig;
-import dev.loat.msmp_console.logging.Logger;
+import dev.loat.config_lib.ConfigManager;
+import dev.loat.msmp_console.config.files.MSMPEntityConfigFile;
-import java.io.File;
-import java.nio.file.Path;
+public class Config {
+
+ private Config() {}
+
+ private static final String ROOT_DIRECTORY = "msmp/console";
+ private static final ConfigManager CONFIG_MANAGER = new ConfigManager(ROOT_DIRECTORY);
-/**
- * Represents a configuration file.
- *
- * @param The config file type
- */
-public class Config {
- private Path path;
- private final Class configFileClass;
- private final YamlConfig yamlConfig;
- @SuppressWarnings("null")
- private ConfigFile config = null;
-
- /**
- * Creates a new config instance.
- *
- * @param path The path to the config file
- * @param configFileClass The config file class
- */
- public Config(
- Path path,
- Class configFileClass
- ) {
- this.path = path;
- this.configFileClass = configFileClass;
- this.yamlConfig = new YamlConfig<>(
- path.toString(),
- configFileClass
- );
-
- this.createIfNotExist();
- this.load();
- }
-
- /**
- * Creates the config file if it does not exist and loads the config.
- * If an error occurs while serializing the config file, it will be logged.
- */
- private void createIfNotExist() {
- try {
- File file = path.toFile();
-
- if(!file.exists()) {
- this.yamlConfig.serialize(configFileClass.getDeclaredConstructor().newInstance());
- }
- } catch (Exception serializeException) {
- Logger.error("Error while serializing the config file (%s):\n%s".formatted(this.path.toString(), serializeException));
- }
+ public static void register() {
+
+ CONFIG_MANAGER.add("config.yml", MSMPEntityConfigFile.class);
}
- /**
- * Returns the config file class associated with this config.
- *
- * @return The config file class
- */
- Class getConfigFileClass() {
- return this.configFileClass;
- }
-
- /**
- * Loads the configuration from the file.
- *
- * If the file does not exist, it will be created with default values.
- *
- * If an error occurs while parsing the file, it will be caught and logged.
- * In this case, a new instance of the config class will be created using its default constructor.
- * If an error occurs while creating a new instance of the config class, it will be caught and logged.
- * In this case, the config will be set to null.
- */
- @SuppressWarnings("null")
- public void load() {
- this.createIfNotExist();
-
- try {
- this.config = this.yamlConfig.parse();
- } catch (Exception parseException) {
- Logger.error("Error while parsing the config file (%s):\n%s".formatted(this.path.toString(), parseException));
-
- try {
- this.config = this.configFileClass.getDeclaredConstructor().newInstance();
- } catch (Exception newInstanceException) {
- Logger.error("Error while creating a new instance of the config class:\n%s".formatted(newInstanceException));
-
- this.config = null;
- }
- }
- }
+ public static MSMPEntityConfigFile getConfig() {
- /**
- * Returns the configuration associated with this config.
- *
- * @return The configuration associated with this config, or null if an error occurred while parsing or creating the config.
- */
- public ConfigFile get() {
- return this.config;
+ return CONFIG_MANAGER.get(MSMPEntityConfigFile.class);
}
}
diff --git a/src/main/java/dev/loat/msmp_console/config/ConfigManager.java b/src/main/java/dev/loat/msmp_console/config/ConfigManager.java
deleted file mode 100644
index d4a2fd6..0000000
--- a/src/main/java/dev/loat/msmp_console/config/ConfigManager.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package dev.loat.msmp_console.config;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.Map;
-
-import dev.loat.msmp_console.logging.Logger;
-import net.fabricmc.loader.api.FabricLoader;
-
-
-/**
- * Manages configuration files.
- */
-public class ConfigManager {
- private static final Map, Config>> configs = new HashMap<>();
-
- public static final String rootDirectory = "msmp/console";
-
- /**
- * Adds a config to the manager.
- *
- * @param config The config type
- * @param config The config class to add
- */
- public static void addConfig(Config config) {
- configs.put(config.getConfigFileClass(), config);
- }
-
- /**
- * Gets a config by its type.
- *
- * @param type The type of the config
- * @param config The config class to get
- * @return The config, or null if not found
- */
- @SuppressWarnings("unchecked")
- public static Config getConfig(Class config) {
- return (Config) configs.get(config);
- }
-
- /**
- * Resolves a config file path relative to the config directory.
- * Creates the config directory if it does not exist.
- *
- * @param configFile The config file name
- * @return The resolved path
- */
- public static Path resolve(String configFile) {
- return ConfigManager.resolve(Path.of(configFile));
- }
-
- /**
- * Resolves a config file path relative to the config directory.
- * Creates the config directory if it does not exist.
- *
- * @param configFile The config file name
- * @return The resolved path
- */
- public static Path resolve(Path configFile) {
- Path configFilePath = FabricLoader
- .getInstance()
- .getConfigDir()
- .resolve(ConfigManager.rootDirectory);
-
- try {
- Files.createDirectories(configFilePath);
- } catch (IOException e) {
- Logger.error("Could not create config directory:\n%s".formatted(e));
- }
-
- return configFilePath.resolve(configFile);
- }
-
- /**
- * Loads all registered config files.
- */
- public static void loadAll() {
- ConfigManager.configs.values().forEach(Config::load);
- }
-}
diff --git a/src/main/java/dev/loat/msmp_console/config/annotation/Comment.java b/src/main/java/dev/loat/msmp_console/config/annotation/Comment.java
deleted file mode 100644
index 4ba4026..0000000
--- a/src/main/java/dev/loat/msmp_console/config/annotation/Comment.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package dev.loat.msmp_console.config.annotation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-
-/**
- * Indicates a comment for a configuration field.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
-public @interface Comment {
- String value();
-}
diff --git a/src/main/java/dev/loat/msmp_console/config/files/MSMPConsoleConfigFile.java b/src/main/java/dev/loat/msmp_console/config/files/MSMPConsoleConfigFile.java
deleted file mode 100644
index eaf0071..0000000
--- a/src/main/java/dev/loat/msmp_console/config/files/MSMPConsoleConfigFile.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package dev.loat.msmp_console.config.files;
-
-import dev.loat.msmp_console.config.annotation.Comment;
-
-
-/**
- * Represents the structure of the console configuration file.
- */
-public final class MSMPConsoleConfigFile {
- @Comment("""
- The default log level to use for the console logger.
- Log levels are used to determine the severity of log messages, and can be used to filter out less important messages.
-
- This can be one of the following values:
- - TRACE
- - DEBUG
- - INFO
- - WARN
- - ERROR
- - FATAL
- """)
- public String defaultLogLevel = "INFO";
-}
diff --git a/src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java b/src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java
new file mode 100644
index 0000000..1e1e033
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java
@@ -0,0 +1,14 @@
+package dev.loat.msmp_console.config.files;
+
+import dev.loat.config_lib.annotation.Annotation;
+
+
+@Annotation.Comment("""
+ Main configuration file for MSMP Entity.
+""")
+public class MSMPEntityConfigFile {
+ private MSMPEntityConfigFile() {}
+
+ @Annotation.Comment("Example")
+ public String example = "42";
+}
diff --git a/src/main/java/dev/loat/msmp_console/config/parser/YamlConfig.java b/src/main/java/dev/loat/msmp_console/config/parser/YamlConfig.java
deleted file mode 100644
index 57670fe..0000000
--- a/src/main/java/dev/loat/msmp_console/config/parser/YamlConfig.java
+++ /dev/null
@@ -1,132 +0,0 @@
-package dev.loat.msmp_console.config.parser;
-
-import org.yaml.snakeyaml.DumperOptions;
-import org.yaml.snakeyaml.LoaderOptions;
-import org.yaml.snakeyaml.Yaml;
-import org.yaml.snakeyaml.constructor.Constructor;
-import org.yaml.snakeyaml.representer.Representer;
-
-import dev.loat.msmp_console.config.annotation.Comment;
-import dev.loat.msmp_console.config.parser.constructor.ComponentConstructor;
-import dev.loat.msmp_console.config.parser.representer.ComponentRepresenter;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Field;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-
-/**
- * Utility class for parsing and serializing YAML configuration files with support for comments.
- *
- * @param The type of the configuration class to parse/serialize.
- */
-public class YamlConfig {
- private final String filePath;
- private final Class configClass;
-
- public YamlConfig(
- String filePath,
- Class configClass
- ) {
- this.filePath = filePath;
- this.configClass = configClass;
- }
-
- /**
- * Serializes a configuration object to a YAML file with comments.
- *
- * @param config The configuration object to serialize.
- * @throws IOException If an error occurs while writing the file.
- */
- public void serialize(ConfigClass config) throws IOException {
- Path path = Paths.get(filePath);
- if (!Files.exists(path)) {
- Files.createDirectories(path.getParent());
- Files.createFile(path);
- }
-
- DumperOptions options = new DumperOptions();
- options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
- options.setPrettyFlow(true);
- options.setIndent(2);
-
- Representer componentRepresenter = new ComponentRepresenter(this.configClass, options);
-
- Yaml yaml = new Yaml(componentRepresenter, options);
-
- String yamlString = yaml.dump(config);
- String[] lines = yamlString.split("\n");
-
- Field[] fields = this.configClass.getDeclaredFields();
- Map> commentsMap = new HashMap<>();
- for (Field field : fields) {
- @SuppressWarnings("null")
- Comment comment = field.getAnnotation(Comment.class);
- if (comment != null) {
- String[] commentLines = comment.value().split("\n");
- List formattedComments = new ArrayList<>();
- for (String commentLine : commentLines) {
- String trimmed = commentLine.trim();
- if (!trimmed.isEmpty()) {
- formattedComments.add("# " + trimmed);
- } else {
- formattedComments.add("#");
- }
- }
- commentsMap.put(field.getName(), formattedComments);
- }
- }
-
- List modifiedLines = new ArrayList<>();
- for (String line : lines) {
- for (String fieldName : commentsMap.keySet()) {
- if (line.startsWith(fieldName + ":")) {
- modifiedLines.addAll(commentsMap.get(fieldName));
- break;
- }
- }
- modifiedLines.add(line);
- }
-
- Files.write(path, modifiedLines);
- }
-
- /**
- * Parses a YAML file and returns a configuration object.
- *
- * @return The configuration object reflecting the YAML file structure.
- * @throws IOException If an error occurs while reading the file or casting.
- */
- public ConfigClass parse() throws IOException {
- Path path = Paths.get(filePath);
- if (!Files.exists(path)) {
- Files.createDirectories(path.getParent());
- Files.createFile(path);
- throw new IllegalStateException("YAML file was newly created and is empty: " + filePath);
- }
-
- try (InputStream inputStream = Files.newInputStream(path)) {
- LoaderOptions options = new LoaderOptions();
- options.setAllowDuplicateKeys(false);
-
- Constructor componentConstructor = new ComponentConstructor(this.configClass, options);
-
- Yaml yaml = new Yaml(componentConstructor);
-
- Object result = yaml.load(inputStream);
-
- if (result == null) {
- throw new IllegalStateException("No data found in YAML file: " + filePath);
- }
-
- return this.configClass.cast(result);
- }
- }
-}
diff --git a/src/main/java/dev/loat/msmp_console/config/parser/constructor/ComponentConstructor.java b/src/main/java/dev/loat/msmp_console/config/parser/constructor/ComponentConstructor.java
deleted file mode 100644
index 2b432fd..0000000
--- a/src/main/java/dev/loat/msmp_console/config/parser/constructor/ComponentConstructor.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package dev.loat.msmp_console.config.parser.constructor;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonNull;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonPrimitive;
-import com.mojang.serialization.JsonOps;
-
-import net.minecraft.network.chat.Component;
-import net.minecraft.network.chat.ComponentSerialization;
-
-import org.yaml.snakeyaml.LoaderOptions;
-import org.yaml.snakeyaml.constructor.Constructor;
-import org.yaml.snakeyaml.nodes.MappingNode;
-import org.yaml.snakeyaml.nodes.Node;
-import org.yaml.snakeyaml.nodes.NodeTuple;
-import org.yaml.snakeyaml.nodes.ScalarNode;
-import org.yaml.snakeyaml.nodes.SequenceNode;
-import org.yaml.snakeyaml.nodes.Tag;
-
-
-/**
- * Custom SnakeYAML constructor for parsing Minecraft chat components from YAML.
- *
- * This constructor extends the default SnakeYAML constructor and adds support for parsing YAML nodes into Minecraft's Component objects.
- * It converts YAML nodes into JSON elements and then uses Minecraft's ComponentSerialization to decode them into Component instances.
- */
-public class ComponentConstructor extends Constructor {
-
- /**
- * Constructs a new component constructor for parsing components (YAML -> components).
- */
- public ComponentConstructor(
- Class> configClass,
- LoaderOptions options
- ) {
- super(configClass, options);
- }
-
- @Override
- protected Object constructObject(Node node) {
- if (Component.class.isAssignableFrom(node.getType())) {
- return this.constructComponent(node);
- }
- return super.constructObject(node);
- }
-
- /**
- * Constructs a component from a YAML node.
- *
- * @param node The YAML node to construct the component from.
- * @return The constructed component.
- */
- private Component constructComponent(Node node) {
- JsonElement json = nodeToJson(node);
-
- return ComponentSerialization.CODEC
- .decode(JsonOps.INSTANCE, json)
- .getOrThrow(error -> new IllegalStateException("Failed to decode Component:\n%s".formatted(error)))
- .getFirst();
- }
-
- /**
- * Converts a YAML node into a JSON element.
- *
- * This method supports scalar nodes (with tags bool, int, float, and null), sequence nodes, and mapping nodes.
- *
- * @param node The YAML node to convert.
- * @return The converted JSON element.
- * @throws IllegalArgumentException If the node type is not supported.
- */
- private JsonElement nodeToJson(Node node) {
- if (node instanceof ScalarNode scalar) {
- Object value = constructScalar(scalar);
- if (value == null) {
- return JsonNull.INSTANCE;
- } else if (node.getTag().equals(Tag.BOOL)) {
- return new JsonPrimitive(Boolean.parseBoolean(value.toString()));
- } else if (node.getTag().equals(Tag.INT)) {
- return new JsonPrimitive(Integer.parseInt(value.toString()));
- } else if (node.getTag().equals(Tag.FLOAT)) {
- return new JsonPrimitive(Double.parseDouble(value.toString()));
- } else {
- return new JsonPrimitive(String.valueOf(value).translateEscapes());
- }
- } else if (node instanceof SequenceNode sequence) {
- JsonArray array = new JsonArray();
- for (Node child : sequence.getValue()) {
- array.add(nodeToJson(child));
- }
- return array;
- } else if (node instanceof MappingNode mapping) {
- JsonObject object = new JsonObject();
- for (NodeTuple tuple : mapping.getValue()) {
- ScalarNode keyNode = (ScalarNode) tuple.getKeyNode();
- String key = constructScalar(keyNode);
- JsonElement value = nodeToJson(tuple.getValueNode());
- object.add(key, value);
- }
- return object;
- }
- throw new IllegalArgumentException("Unexpected node type: " + node.getClass().getName());
- }
-}
diff --git a/src/main/java/dev/loat/msmp_console/config/parser/representer/ComponentRepresenter.java b/src/main/java/dev/loat/msmp_console/config/parser/representer/ComponentRepresenter.java
deleted file mode 100644
index dcad306..0000000
--- a/src/main/java/dev/loat/msmp_console/config/parser/representer/ComponentRepresenter.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package dev.loat.msmp_console.config.parser.representer;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonElement;
-import org.yaml.snakeyaml.DumperOptions;
-import org.yaml.snakeyaml.nodes.Node;
-import org.yaml.snakeyaml.nodes.Tag;
-import org.yaml.snakeyaml.representer.Representer;
-
-import com.mojang.serialization.JsonOps;
-
-import net.minecraft.network.chat.Component;
-import net.minecraft.network.chat.ComponentSerialization;
-import net.minecraft.network.chat.MutableComponent;
-
-
-/**
- * Custom SnakeYAML representer for serializing Minecraft chat components to YAML.
- */
-public class ComponentRepresenter extends Representer {
- /**
- * Constructs a new component representer for serializing components (components -> YAML).
- *
- * @param configClass The configuration class to associate with this representer.
- * @param options The DumperOptions to use.
- */
- public ComponentRepresenter(
- Class> configClass,
- DumperOptions options
- ) {
- super(options);
-
- this.addClassTag(configClass, Tag.MAP);
-
- this.representers.put(Component.class, (data) -> this.representComponent((Component) data));
- this.representers.put(MutableComponent.class, (data) -> this.representComponent((Component) data));
- }
-
- /**
- * Represents a Component as a Node.
- *
- * @param component The Component to represent.
- * @return The Node representation of the Component.
- */
- private Node representComponent(Component component) {
- JsonElement json = ComponentSerialization.CODEC
- .encodeStart(JsonOps.INSTANCE, component)
- .getOrThrow(err -> new IllegalStateException("Failed to encode Component: " + err));
-
- return this.represent(new Gson().fromJson(json, Object.class));
- }
-}
diff --git a/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java b/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
index f443dc5..6f018a1 100644
--- a/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
+++ b/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
@@ -1,6 +1,7 @@
package dev.loat.msmp_console.logging;
import dev.loat.msmp_console.MSMPConsole;
+
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
@@ -9,19 +10,10 @@
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import java.time.Instant;
-
+import java.util.function.Consumer;
/**
- * A Log4j2 {@link Appender} that intercepts every log event from the root logger
- * and forwards it as a JSON-RPC notification to all connected MSMP clients via
- * {@link MSMPConsole#sendConsoleNotification(LogPayload)}.
- *
- * The appender is registered programmatically in {@link MSMPConsole#onInitialize()}
- * and does not require any Log4j2 XML configuration.
- *
- * A per-thread reentrancy guard ({@link #IS_APPENDING}) prevents infinite recursion
- * in case {@link MSMPConsole#sendConsoleNotification(LogPayload)} itself produces
- * a log event.
+ * A Log4j2 {@link Appender} that intercepts log events and forwards them to a consumer.
*/
@Plugin(
name = "ConsoleNotificationAppender",
@@ -38,25 +30,29 @@ public class ConsoleNotificationAppender extends AbstractAppender {
private static final ThreadLocal IS_APPENDING =
ThreadLocal.withInitial(() -> false);
+ private final Consumer listener;
+
/**
* Creates a new {@code ConsoleNotificationAppender} with the given name and filter.
*
* @param name the name of this appender
* @param filter an optional Log4j2 filter, or {@code null} for no filtering
*/
- protected ConsoleNotificationAppender(String name, Filter filter) {
+ protected ConsoleNotificationAppender(String name, Filter filter, Consumer listener) {
super(name, filter, null, true, null);
+ this.listener = listener;
}
/**
* Factory method used by Log4j2's plugin system to instantiate this appender.
*
- * @param name the name of the appender instance
- * @return a new {@code ConsoleNotificationAppender}
+ * @param name The name of the appender instance
+ * @param listener Callback for the appender
+ * @return A new {@code ConsoleNotificationAppender}
*/
@PluginFactory
- public static ConsoleNotificationAppender createAppender(String name) {
- return new ConsoleNotificationAppender(name, null);
+ public static ConsoleNotificationAppender createAppender(String name, Consumer listener) {
+ return new ConsoleNotificationAppender(name, null, listener);
}
/**
@@ -72,7 +68,9 @@ public void append(LogEvent event) {
if (IS_APPENDING.get()) return;
IS_APPENDING.set(true);
try {
- MSMPConsole.sendConsoleNotification(buildPayload(event));
+ if (listener != null) {
+ listener.accept(buildPayload(event));
+ }
} finally {
IS_APPENDING.set(false);
}
@@ -88,24 +86,28 @@ public void append(LogEvent event) {
* @return a {@link LogPayload} containing all relevant fields of the event
*/
private static LogPayload buildPayload(LogEvent event) {
- String throwable = null;
+ String throwable = "";
if (event.getThrown() != null) {
- Throwable t = event.getThrown();
- StringBuilder sb = new StringBuilder();
- sb.append(t.getClass().getName());
- if (t.getMessage() != null)
- sb.append(": ").append(t.getMessage());
- for (StackTraceElement el : t.getStackTrace())
- sb.append("\n\tat ").append(el);
- throwable = sb.toString();
+ Throwable thrown = event.getThrown();
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append(thrown.getClass().getName());
+ if (thrown.getMessage() != null) {
+ stringBuilder.append(": ").append(thrown.getMessage());
+ }
+ for (StackTraceElement el : thrown.getStackTrace()) {
+ stringBuilder.append("\n\tat ").append(el);
+ }
+ throwable = stringBuilder.toString();
}
+ String message = event.getMessage() == null ? "" : event.getMessage().getFormattedMessage();
+
return new LogPayload(
Instant.ofEpochMilli(event.getTimeMillis()).toString(),
event.getLevel().name(),
event.getThreadName(),
event.getLoggerName(),
- event.getMessage().getFormattedMessage(),
+ message,
throwable
);
}
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/Endpoints.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/Endpoints.java
new file mode 100644
index 0000000..1de7050
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/Endpoints.java
@@ -0,0 +1,29 @@
+package dev.loat.msmp_console.msmp.endpoints;
+
+import dev.loat.msmp.MSMPNamespace;
+import dev.loat.msmp.MSMPServer;
+import dev.loat.msmp_console.msmp.endpoints.log.notification.event.NotificationLogEvent;
+
+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) {
+
+ NotificationLogEvent.register(namespace, msmpServer);
+ }
+}
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java
new file mode 100644
index 0000000..d671583
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java
@@ -0,0 +1,47 @@
+package dev.loat.msmp_console.msmp.endpoints.log.notification.event;
+
+import java.util.function.Supplier;
+
+import org.apache.logging.log4j.LogManager;
+
+import dev.loat.msmp.MSMPNamespace;
+import dev.loat.msmp.MSMPNotification;
+import dev.loat.msmp.MSMPServer;
+import dev.loat.msmp_console.logging.ConsoleNotificationAppender;
+
+
+public class NotificationLogEvent {
+
+ private NotificationLogEvent() {}
+
+ public static void register(MSMPNamespace namespace, Supplier msmpServer) {
+ MSMPNotification notification = namespace.notification("log/event")
+ .description("A server console log message")
+ .responseSchema(NotificationLogEventPayload.SCHEMA)
+ .register();
+
+ org.apache.logging.log4j.core.Logger root = ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)).getRootLogger();
+
+ ConsoleNotificationAppender appender = ConsoleNotificationAppender.createAppender(
+ "ConsoleNotificationAppender",
+ (payload) -> {
+ MSMPServer server = msmpServer.get();
+ if (server == null) return;
+
+ server.send(notification, new NotificationLogEventPayload(
+ payload.timestamp(),
+ payload.level(),
+ payload.thread(),
+ payload.logger(),
+ payload.message(),
+ payload.throwable()
+ ));
+ }
+ );
+
+ appender.start();
+ root.addAppender(appender);
+ }
+}
+
+
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEventPayload.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEventPayload.java
new file mode 100644
index 0000000..45b298b
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEventPayload.java
@@ -0,0 +1,58 @@
+package dev.loat.msmp_console.msmp.endpoints.log.notification.event;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import net.minecraft.server.jsonrpc.api.Schema;
+
+
+/**
+ * Payload for the {@code console:notification/log/event} notification.
+ *
+ * Fired when a new log event occurs.
+ *
+ * Example JSON representation:
+ *
+ * {
+ * "timestamp": ""
+ * "level": ""
+ * "thread": ""
+ * "logger": ""
+ * "message": ""
+ * "throwable": ""
+ * }
+ *
+ *
+ * @param timestamp ISO-8601 timestamp of the log event
+ * @param level log level (e.g. {@code INFO}, {@code WARN}, {@code ERROR})
+ * @param thread name of the thread that produced the event
+ * @param logger fully qualified name of the originating logger
+ * @param message fully interpolated log message
+ * @param throwable serialized stacktrace, or {@code null} if no exception was thrown
+ */
+public record NotificationLogEventPayload(
+ String timestamp,
+ String level,
+ String thread,
+ String logger,
+ String message,
+ String throwable
+) {
+
+ public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group(
+ Codec.STRING.fieldOf("timestamp").forGetter(NotificationLogEventPayload::timestamp),
+ Codec.STRING.fieldOf("level").forGetter(NotificationLogEventPayload::level),
+ Codec.STRING.fieldOf("thread").forGetter(NotificationLogEventPayload::thread),
+ Codec.STRING.fieldOf("logger").forGetter(NotificationLogEventPayload::logger),
+ Codec.STRING.fieldOf("message").forGetter(NotificationLogEventPayload::message),
+ Codec.STRING.fieldOf("throwable").forGetter(NotificationLogEventPayload::throwable)
+ ).apply(i, NotificationLogEventPayload::new));
+
+ public static final Schema SCHEMA = Schema.record(CODEC)
+ .withField("timestamp", Schema.STRING_SCHEMA)
+ .withField("level", Schema.STRING_SCHEMA)
+ .withField("thread", Schema.STRING_SCHEMA)
+ .withField("logger", Schema.STRING_SCHEMA)
+ .withField("message", Schema.STRING_SCHEMA)
+ .withField("throwable", Schema.STRING_SCHEMA);
+}
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java
new file mode 100644
index 0000000..16c322c
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java
@@ -0,0 +1,23 @@
+package dev.loat.msmp_console.msmp.endpoints.send;
+
+import dev.loat.msmp.MSMPNamespace;
+
+
+public class Send {
+
+ private Send() {}
+
+ public static void register(MSMPNamespace namespace) {
+ namespace.method("send")
+ .description("Send a command to the server console")
+ .requestSchema(SendRequest.SCHEMA)
+ .responseSchema(SendResponse.SCHEMA)
+ .register((server, client, params) -> {
+ String command = params.command();
+
+ // execute command on server, evaluate if successful and return response
+
+ return new SendResponse(command, "", false);
+ });
+ }
+}
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendRequest.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendRequest.java
new file mode 100644
index 0000000..fa4ae23
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendRequest.java
@@ -0,0 +1,35 @@
+package dev.loat.msmp_console.msmp.endpoints.send;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import net.minecraft.server.jsonrpc.api.Schema;
+
+
+/**
+ * Response payload for the {@code console:send} method.
+ *
+ * Example JSON representation:
+ *
+ * { "command": "say Hello" }
+ *
+ *
+ * @param command The command
+ * @param success If the command was ran successful
+ */
+public record SendRequest(String command) {
+
+ /**
+ * Codec for serializing and deserializing {@link SendResponse} instances.
+ */
+ public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group(
+ Codec.STRING.fieldOf("command").forGetter(SendRequest::command)
+ ).apply(i, SendRequest::new));
+
+ /**
+ * MSMP schema for {@link SendRequest}, used for protocol discovery.
+ */
+ public static final Schema SCHEMA = Schema.record(CODEC)
+ .withField("command", Schema.STRING_SCHEMA);
+
+}
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendResponse.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendResponse.java
new file mode 100644
index 0000000..7fbc9d8
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/SendResponse.java
@@ -0,0 +1,41 @@
+package dev.loat.msmp_console.msmp.endpoints.send;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import net.minecraft.server.jsonrpc.api.Schema;
+
+
+/**
+ * Response payload for the {@code console:send} method.
+ *
+ * Example response:
+ *
+ * {
+ * "command": "say Hello",
+ * "success": true
+ * }
+ *
+ *
+ * @param command The command
+ * @param success If the command was ran successful
+ */
+public record SendResponse(String command, String result, boolean success) {
+
+ /**
+ * Codec for serializing and deserializing {@link SendResponse} instances.
+ */
+ public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group(
+ Codec.STRING.fieldOf("command").forGetter(SendResponse::command),
+ Codec.STRING.fieldOf("result").forGetter(SendResponse::result),
+ Codec.BOOL.fieldOf("success").forGetter(SendResponse::success)
+ ).apply(i, SendResponse::new));
+
+ /**
+ * MSMP schema for {@link SendResponse}, used for protocol discovery.
+ */
+ public static final Schema SCHEMA = Schema.record(CODEC)
+ .withField("command", Schema.STRING_SCHEMA)
+ .withField("result", Schema.STRING_SCHEMA)
+ .withField("success", Schema.BOOL_SCHEMA);
+}
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index 67b9ff3..a1e2b2f 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -3,31 +3,24 @@
"id": "msmp-console",
"version": "${version}",
"name": "MSMP Console",
- "description": "This Minecraft Fabric Mod Extends the Minecraft Server Management Protocol (MSMP) forwarding every server console log event to all connected clients as a JSON-RPC 2.0 notification.",
+ "description": "Extends the Minecraft Server Management Protocol (MSMP) by providing additional functions for interacting with the console",
"authors": [],
"contact": {},
"license": "LGPL-3.0-only",
"icon": "assets/icon.png",
"environment": "server",
"entrypoints": {
- "fabric-datagen": [
- "dev.loat.msmp_console.client.MSMPConsoleDataGenerator"
- ],
"main": [
"dev.loat.msmp_console.MSMPConsole"
]
},
"mixins": [
- "msmp-console.mixins.json",
- {
- "config": "msmp-console.client.mixins.json",
- "environment": "client"
- }
+ "msmp-console.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric-api": "*",
- "minecraft": "${minecraft_version}"
+ "minecraft": "^${minecraft_version}"
},
"custom": {
"mc-publish": {
From 777c613a9f14fcb844346427eb62767e3bd6f63e Mon Sep 17 00:00:00 2001
From: Mqx <62719703+Mqxx@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:45:56 +0200
Subject: [PATCH 2/7] feat: console:send endpoint and better configuration
---
.../config/files/MSMPEntityConfigFile.java | 9 +++-
.../config/files/log/LogConfig.java | 22 ++++++++
.../config/files/send/SendConfig.java | 13 +++++
.../logging/ConsoleNotificationAppender.java | 24 +++++++--
.../msmp/endpoints/Endpoints.java | 3 ++
.../send/CapturingCommandSource.java | 50 +++++++++++++++++++
.../msmp/endpoints/send/Send.java | 42 ++++++++++++++--
7 files changed, 155 insertions(+), 8 deletions(-)
create mode 100644 src/main/java/dev/loat/msmp_console/config/files/log/LogConfig.java
create mode 100644 src/main/java/dev/loat/msmp_console/config/files/send/SendConfig.java
create mode 100644 src/main/java/dev/loat/msmp_console/msmp/endpoints/send/CapturingCommandSource.java
diff --git a/src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java b/src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java
index 1e1e033..92a4ed9 100644
--- a/src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java
+++ b/src/main/java/dev/loat/msmp_console/config/files/MSMPEntityConfigFile.java
@@ -1,6 +1,8 @@
package dev.loat.msmp_console.config.files;
import dev.loat.config_lib.annotation.Annotation;
+import dev.loat.msmp_console.config.files.log.LogConfig;
+import dev.loat.msmp_console.config.files.send.SendConfig;
@Annotation.Comment("""
@@ -9,6 +11,9 @@
public class MSMPEntityConfigFile {
private MSMPEntityConfigFile() {}
- @Annotation.Comment("Example")
- public String example = "42";
+ @Annotation.Comment("Configuration for log related settings.")
+ public LogConfig log = new LogConfig();
+
+ @Annotation.Comment("Configuration for send related settings.")
+ public SendConfig send = new SendConfig();
}
diff --git a/src/main/java/dev/loat/msmp_console/config/files/log/LogConfig.java b/src/main/java/dev/loat/msmp_console/config/files/log/LogConfig.java
new file mode 100644
index 0000000..a949f83
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/config/files/log/LogConfig.java
@@ -0,0 +1,22 @@
+package dev.loat.msmp_console.config.files.log;
+
+import dev.loat.config_lib.annotation.Annotation;
+
+
+public class LogConfig {
+
+ public static enum LogLevel {
+ TRACE,
+ DEBUG,
+ INFO,
+ WARN,
+ ERROR,
+ FATAL
+ }
+
+ @Annotation.Comment("""
+ The minimum log level that gets forwarded as a console:notification/log_event.
+ Events below this level are ignored entirely and never sent to connected clients.
+ """)
+ public LogLevel level = LogLevel.INFO;
+}
diff --git a/src/main/java/dev/loat/msmp_console/config/files/send/SendConfig.java b/src/main/java/dev/loat/msmp_console/config/files/send/SendConfig.java
new file mode 100644
index 0000000..a495f44
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/config/files/send/SendConfig.java
@@ -0,0 +1,13 @@
+package dev.loat.msmp_console.config.files.send;
+
+import dev.loat.config_lib.annotation.Annotation;
+
+
+public class SendConfig {
+
+ @Annotation.Comment("""
+ Enable logging for the execution of a command in the console, this prevents echoing the send command.
+ """)
+ @Annotation.Key("log-command-execution")
+ public boolean logCommandExecution = true;
+}
diff --git a/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java b/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
index 6f018a1..4597695 100644
--- a/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
+++ b/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
@@ -1,7 +1,8 @@
package dev.loat.msmp_console.logging;
-import dev.loat.msmp_console.MSMPConsole;
+import dev.loat.msmp_console.config.Config;
+import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
@@ -56,7 +57,22 @@ public static ConsoleNotificationAppender createAppender(String name, ConsumerFalls back to {@link Level#INFO} if the configured value is missing or cannot be
+ * parsed into a valid Log4j2 level.
+ *
+ * @param eventLevel The level of the incoming log event
+ * @return {@code true} if the event should be forwarded, {@code false} if it should be dropped
+ */
+ private static boolean isAtOrAboveMinLevel(Level eventLevel) {
+ String configuredLevel = Config.getConfig().log.level.toString();
+ Level minLevel = Level.toLevel(configuredLevel, Level.INFO);
+ return eventLevel.isMoreSpecificThan(minLevel);
+ }
+
+ /**
+ * Intercepts a log event and.
*
* Guarded by {@link #IS_APPENDING} to prevent infinite recursion. If the flag
* is already set on the current thread, the event is silently dropped.
@@ -66,6 +82,8 @@ public static ConsoleNotificationAppender createAppender(String name, Consumer msmpServer) {
NotificationLogEvent.register(namespace, msmpServer);
+
+ Send.register(namespace);
}
}
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/CapturingCommandSource.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/CapturingCommandSource.java
new file mode 100644
index 0000000..953b7b7
--- /dev/null
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/CapturingCommandSource.java
@@ -0,0 +1,50 @@
+package dev.loat.msmp_console.msmp.endpoints.send;
+
+import net.minecraft.commands.CommandSource;
+import net.minecraft.network.chat.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * A {@link CommandSource} that captures all feedback and error messages produced by a
+ * command into an in-memory list instead of forwarding them anywhere (e.g. the real
+ * server console), so they can be returned as the {@code result} of {@code console:send}.
+ *
+ * Both success and failure feedback are captured ({@link #acceptsSuccess()} and
+ * {@link #acceptsFailure()} both return {@code true}), and nothing is broadcast to
+ * operators ({@link #shouldInformAdmins()} returns {@code false}), since the caller
+ * already receives the full output directly in the RPC response.
+ */
+final class CapturingCommandSource implements CommandSource {
+
+ private final List messages = new ArrayList<>();
+
+ @Override
+ public void sendSystemMessage(Component message) {
+ messages.add(message.getString());
+ }
+
+ @Override
+ public boolean acceptsSuccess() {
+ return true;
+ }
+
+ @Override
+ public boolean acceptsFailure() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldInformAdmins() {
+ return false;
+ }
+
+ /**
+ * @return All captured feedback/error lines, joined by newlines, in the order they were produced
+ */
+ String getOutput() {
+ return String.join("\n", messages);
+ }
+}
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java
index 16c322c..019d4a5 100644
--- a/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/send/Send.java
@@ -1,8 +1,23 @@
package dev.loat.msmp_console.msmp.endpoints.send;
+
import dev.loat.msmp.MSMPNamespace;
+import dev.loat.msmp_console.config.Config;
+import dev.loat.msmp_console.logging.Logger;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.permissions.PermissionSet;
+/**
+ * Registers the {@code console:send} MSMP method.
+ *
+ * Executes an arbitrary command as if typed by an operator (permission level 4) and
+ * returns its textual feedback/error output together with a success indicator.
+ *
+ * The command is executed via {@link net.minecraft.commands.Commands#performPrefixedCommand},
+ * which strips a leading {@code /} if present - so {@code command} may be sent either with
+ * or without the prefix.
+ */
public class Send {
private Send() {}
@@ -15,9 +30,30 @@ public static void register(MSMPNamespace namespace) {
.register((server, client, params) -> {
String command = params.command();
- // execute command on server, evaluate if successful and return response
-
- return new SendResponse(command, "", false);
+ CapturingCommandSource capturingSource = new CapturingCommandSource();
+
+ boolean[] successHolder = {false};
+
+ CommandSourceStack source = server.createCommandSourceStack()
+ .withSource(capturingSource)
+ .withPermission(PermissionSet.ALL_PERMISSIONS)
+ .withCallback((success, result) -> successHolder[0] = success);
+
+ try {
+ server.getCommands().performPrefixedCommand(source, command);
+ } catch (Exception e) {
+ Logger.warning("console:send - failed to execute '%s': %s".formatted(command, e.getMessage()));
+ return new SendResponse(command, e.getMessage() != null ? e.getMessage() : "", false);
+ }
+
+ String result = capturingSource.getOutput();
+ boolean success = successHolder[0];
+
+ if (Config.getConfig().send.logCommandExecution) {
+ Logger.info("console:send - executed '%s'".formatted(command));
+ }
+
+ return new SendResponse(command, result, success);
});
}
}
From dee5f73cb594306b5021ad0561ffe08f6af393de Mon Sep 17 00:00:00 2001
From: Mqx <62719703+Mqxx@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:46:59 +0200
Subject: [PATCH 3/7] Delete ConsoleLogPayload.java
---
.../loat/msmp_console/ConsoleLogPayload.java | 74 -------------------
1 file changed, 74 deletions(-)
delete mode 100644 src/main/java/dev/loat/msmp_console/ConsoleLogPayload.java
diff --git a/src/main/java/dev/loat/msmp_console/ConsoleLogPayload.java b/src/main/java/dev/loat/msmp_console/ConsoleLogPayload.java
deleted file mode 100644
index d966477..0000000
--- a/src/main/java/dev/loat/msmp_console/ConsoleLogPayload.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package dev.loat.msmp_console;
-
-import com.mojang.serialization.Codec;
-import com.mojang.serialization.codecs.RecordCodecBuilder;
-import net.minecraft.server.jsonrpc.api.Schema;
-
-
-/**
- * Represents the payload of a console log notification sent over the
- * Minecraft Server Management Protocol (MSMP).
- *
- * Each instance corresponds to a single log event captured by the
- * {@link dev.loat.msmp_console.logging.ConsoleNotificationAppender} and
- * transmitted as a JSON-RPC notification to all connected management clients
- * via {@code console:notification/message}.
- *
- * Example JSON representation:
- * {@code
- * {
- * "timestamp": "2026-03-21T15:06:06.146Z",
- * "level": "INFO",
- * "thread": "Server thread",
- * "logger": "net.minecraft.server.MinecraftServer",
- * "message": "Done (1.019s)! For help, type \"help\"",
- * "throwable": ""
- * }
- * }
- *
- * @param timestamp ISO-8601 timestamp of when the log event occurred
- * @param level log level of the event (e.g. {@code INFO}, {@code WARN}, {@code ERROR})
- * @param thread name of the thread that produced the log event
- * @param logger fully qualified name of the logger (usually the class name)
- * @param message the fully interpolated log message
- * @param throwable serialized stacktrace if an exception was attached, empty string otherwise
- */
-public record ConsoleLogPayload(
- String timestamp,
- String level,
- String thread,
- String logger,
- String message,
- String throwable
-) {
- /**
- * Mojang Serialization {@link Codec} for encoding and decoding
- * {@link ConsoleLogPayload} instances to and from JSON.
- *
- * {@code throwable} is optional in deserialization and defaults
- * to an empty string if absent.
- */
- public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group(
- Codec.STRING.fieldOf("timestamp").forGetter(ConsoleLogPayload::timestamp),
- Codec.STRING.fieldOf("level").forGetter(ConsoleLogPayload::level),
- Codec.STRING.fieldOf("thread").forGetter(ConsoleLogPayload::thread),
- Codec.STRING.fieldOf("logger").forGetter(ConsoleLogPayload::logger),
- Codec.STRING.fieldOf("message").forGetter(ConsoleLogPayload::message),
- Codec.STRING.optionalFieldOf("throwable", "").forGetter(ConsoleLogPayload::throwable)
- ).apply(i, ConsoleLogPayload::new));
-
- /**
- * JSON-RPC API {@link Schema} describing the structure of {@link ConsoleLogPayload}.
- *
- * Used by the MSMP discovery endpoint ({@code rpc.discover}) to expose
- * the shape of the {@code console:notification/message} notification to
- * connected clients.
- */
- public static final Schema SCHEMA = Schema.record(CODEC)
- .withField("timestamp", Schema.STRING_SCHEMA)
- .withField("level", Schema.STRING_SCHEMA)
- .withField("thread", Schema.STRING_SCHEMA)
- .withField("logger", Schema.STRING_SCHEMA)
- .withField("message", Schema.STRING_SCHEMA)
- .withField("throwable", Schema.STRING_SCHEMA);
-}
From 3bef4ed7101da271eb05a02ca7bd2b302f3a8262 Mon Sep 17 00:00:00 2001
From: Mqx <62719703+Mqxx@users.noreply.github.com>
Date: Sun, 12 Jul 2026 23:00:28 +0200
Subject: [PATCH 4/7] fix: log appender cleanup
---
.../logging/ConsoleNotificationAppender.java | 76 +++++++------------
.../event/NotificationLogEvent.java | 36 ++++-----
2 files changed, 41 insertions(+), 71 deletions(-)
diff --git a/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java b/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
index 4597695..e636855 100644
--- a/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
+++ b/src/main/java/dev/loat/msmp_console/logging/ConsoleNotificationAppender.java
@@ -3,72 +3,50 @@
import dev.loat.msmp_console.config.Config;
import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.core.Appender;
-import org.apache.logging.log4j.core.Core;
-import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
-import org.apache.logging.log4j.core.config.plugins.Plugin;
-import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import java.time.Instant;
import java.util.function.Consumer;
+
/**
- * A Log4j2 {@link Appender} that intercepts log events and forwards them to a consumer.
+ * A Log4j2 appender that forwards every log event at or above the configured minimum
+ * level to a listener as a {@link LogPayload}.
+ *
+ * Attached directly to the root logger via {@link #register(Consumer)} - there is no
+ * Log4j2 config/plugin discovery involved, so no {@code @Plugin} metadata is needed.
*/
-@Plugin(
- name = "ConsoleNotificationAppender",
- category = Core.CATEGORY_NAME,
- elementType = Appender.ELEMENT_TYPE
-)
public class ConsoleNotificationAppender extends AbstractAppender {
/**
- * Per-thread flag that prevents recursive invocation of {@link #append(LogEvent)}.
- * Set to {@code true} while a notification is being dispatched, and reset to
- * {@code false} in the {@code finally} block.
+ * Prevents feedback loops: if forwarding an event causes something to log again
+ * (e.g. an MSMP send failure), that new event must not be captured too.
*/
- private static final ThreadLocal IS_APPENDING =
- ThreadLocal.withInitial(() -> false);
+ private static final ThreadLocal IS_APPENDING = ThreadLocal.withInitial(() -> false);
private final Consumer listener;
- /**
- * Creates a new {@code ConsoleNotificationAppender} with the given name and filter.
- *
- * @param name the name of this appender
- * @param filter an optional Log4j2 filter, or {@code null} for no filtering
- */
- protected ConsoleNotificationAppender(String name, Filter filter, Consumer listener) {
- super(name, filter, null, true, null);
+ private ConsoleNotificationAppender(Consumer listener) {
+ super("ConsoleNotificationAppender", null, null, true, null);
this.listener = listener;
}
/**
- * Factory method used by Log4j2's plugin system to instantiate this appender.
+ * Creates, starts, and attaches a new {@link ConsoleNotificationAppender} to the root
+ * logger, forwarding every captured event to {@code listener}.
*
- * @param name The name of the appender instance
- * @param listener Callback for the appender
- * @return A new {@code ConsoleNotificationAppender}
+ * @param listener Called for every log event at or above the configured minimum level
+ * @return The attached appender
*/
- @PluginFactory
- public static ConsoleNotificationAppender createAppender(String name, Consumer listener) {
- return new ConsoleNotificationAppender(name, null, listener);
- }
+ public static ConsoleNotificationAppender register(Consumer listener) {
+ ConsoleNotificationAppender appender = new ConsoleNotificationAppender(listener);
+ appender.start();
- /**
- * Checks whether the given event level is at or above the configured minimum level.
- *
- * Falls back to {@link Level#INFO} if the configured value is missing or cannot be
- * parsed into a valid Log4j2 level.
- *
- * @param eventLevel The level of the incoming log event
- * @return {@code true} if the event should be forwarded, {@code false} if it should be dropped
- */
- private static boolean isAtOrAboveMinLevel(Level eventLevel) {
- String configuredLevel = Config.getConfig().log.level.toString();
- Level minLevel = Level.toLevel(configuredLevel, Level.INFO);
- return eventLevel.isMoreSpecificThan(minLevel);
+ ((LoggerContext) LogManager.getContext(false)).getRootLogger().addAppender(appender);
+
+ return appender;
}
/**
@@ -82,12 +60,14 @@ private static boolean isAtOrAboveMinLevel(Level eventLevel) {
@Override
public void append(LogEvent event) {
if (IS_APPENDING.get()) return;
- if (!isAtOrAboveMinLevel(event.getLevel())) return;
+
+ Level minLevel = Level.toLevel(Config.getConfig().log.level.toString(), Level.INFO);
+ if (!event.getLevel().isMoreSpecificThan(minLevel)) return;
IS_APPENDING.set(true);
try {
if (listener != null) {
- listener.accept(buildPayload(event));
+ listener.accept(toPayload(event));
}
} finally {
IS_APPENDING.set(false);
@@ -103,7 +83,7 @@ public void append(LogEvent event) {
* @param event the log event to extract data from
* @return a {@link LogPayload} containing all relevant fields of the event
*/
- private static LogPayload buildPayload(LogEvent event) {
+ private static LogPayload toPayload(LogEvent event) {
String throwable = "";
if (event.getThrown() != null) {
Throwable thrown = event.getThrown();
diff --git a/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java b/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java
index d671583..a6491e1 100644
--- a/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java
+++ b/src/main/java/dev/loat/msmp_console/msmp/endpoints/log/notification/event/NotificationLogEvent.java
@@ -2,8 +2,6 @@
import java.util.function.Supplier;
-import org.apache.logging.log4j.LogManager;
-
import dev.loat.msmp.MSMPNamespace;
import dev.loat.msmp.MSMPNotification;
import dev.loat.msmp.MSMPServer;
@@ -20,27 +18,19 @@ public static void register(MSMPNamespace namespace, Supplier msmpSe
.responseSchema(NotificationLogEventPayload.SCHEMA)
.register();
- org.apache.logging.log4j.core.Logger root = ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)).getRootLogger();
-
- ConsoleNotificationAppender appender = ConsoleNotificationAppender.createAppender(
- "ConsoleNotificationAppender",
- (payload) -> {
- MSMPServer server = msmpServer.get();
- if (server == null) return;
-
- server.send(notification, new NotificationLogEventPayload(
- payload.timestamp(),
- payload.level(),
- payload.thread(),
- payload.logger(),
- payload.message(),
- payload.throwable()
- ));
- }
- );
-
- appender.start();
- root.addAppender(appender);
+ ConsoleNotificationAppender.register(payload -> {
+ MSMPServer server = msmpServer.get();
+ if (server == null) return;
+
+ server.send(notification, new NotificationLogEventPayload(
+ payload.timestamp(),
+ payload.level(),
+ payload.thread(),
+ payload.logger(),
+ payload.message(),
+ payload.throwable()
+ ));
+ });
}
}
From 04513be8d255b05987bdb8937874f9bdee87142b Mon Sep 17 00:00:00 2001
From: Mqx <62719703+Mqxx@users.noreply.github.com>
Date: Sun, 12 Jul 2026 23:07:37 +0200
Subject: [PATCH 5/7] Update README.md
---
README.md | 93 ++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 72 insertions(+), 21 deletions(-)
diff --git a/README.md b/README.md
index 5397776..22622dd 100644
--- a/README.md
+++ b/README.md
@@ -2,54 +2,105 @@
# MSMP Console
-A server-side Fabric mod that extends the [Minecraft Server Management Protocol](https://minecraft.wiki/w/Minecraft_Server_Management_Protocol) (MSMP) by forwarding every server console log event.
+A server-side Fabric mod that extends the [Minecraft Server Management Protocol](https://minecraft.wiki/w/Minecraft_Server_Management_Protocol) (MSMP) by providing additional functions for interacting with the console.
+
+This mod is designed for tooling, dashboards, automation systems, external monitoring tools, and integrations that need structured access to the server console without relying on RCON or log-file tailing.
+
## Installation
1. Download the mod `.jar` and place it in your server's `mods/` folder.
2. Enable the Management Server in `server.properties`:
- ```properties
+```properties
management-server-enabled=true
- ```
+```
3. Start the server. The Management Server will listen on `localhost:25576` by default.
-## Notification
-Once a client connects to the WebSocket endpoint, it will receive a notification for every log event produced by the server.
+## Configuration
+
+On first start, the mod generates a configuration file at `/config/msmp/console/config.yml`:
+
+```yaml
+# Main configuration file for MSMP Entity.
+
+# Configuration for log related settings.
+log:
+ # The minimum log level that gets forwarded as a console:notification/log_event.
+ # Events below this level are ignored entirely and never sent to connected clients.
+ # @possible: TRACE | DEBUG | INFO | WARN | ERROR | FATAL
+ # @default: 'INFO'
+ level: INFO
+# Configuration for send related settings.
+send:
+ # Enable logging for the execution of a command in the console, this prevents echoing the send command.
+ # @default: true
+ log-command-execution: true
+```
+
+## RPC Methods
+
+The mod currently provides the following MSMP RPC methods. All of these methods are also automatically discoverable through the standard `rpc.discover` MSMP endpoint.
+
+| Method | Description |
+|:---------------|:--------------------------------------------------------------------------------------|
+| `console:send` | Executes a command on the server console with full permissions and returns its output |
+
+> If you want more methods or notifications for other purposes, please [open an issue](https://github.com/MinecraftPlayground/msmp-console-mod/issues/new?template=new_method_or_notification_suggesetion.yml)
+
-**Method:** `console:notification/log_event`
+## RPC Notifications
+
+The mod also provides the following MSMP RPC notification that clients can subscribe to:
+
+| Method | Description |
+|:---------------------------------|:------------------------------------------------------------------------------------|
+| `console:notification/log/event` | Fired for every server console log event at or above the configured `log.min-level` |
+
+
+## Method Reference
+
+### `console:send`
+
+Executes an arbitrary command as if typed by an operator (full permissions) and returns its textual feedback/error output together with a success indicator. The command may be sent with or without a leading `/`.
+
+```jsonc
+// Request
+{ "command": "say Hello" }
+
+// Response
+{
+ "command": "say Hello",
+ "result": "",
+ "success": true
+}
+```
-### Payload
+---
-| Field | Type | Description |
-|-------------|--------|---------------------------------------------------------------------------|
-| `timestamp` | string | ISO-8601 timestamp of when the log event occurred |
-| `level` | string | Log level: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` or `FATAL` |
-| `thread` | string | Name of the thread that produced the log event |
-| `logger` | string | Fully qualified name of the originating logger (e.g. the class name) |
-| `message` | string | The fully interpolated log message |
-| `throwable` | string | Serialized stacktrace if an exception was attached, omitted otherwise |
+### `console:notification/log/event`
-### Example
+Fired for every server console log event whose level is at or above the configured `log.level`. Events below that level are dropped before ever reaching connected clients.
-```json
+```jsonc
{
"jsonrpc": "2.0",
- "method": "console:notification/log_event",
+ "method": "console:notification/log/event",
"params": [{
"timestamp": "2026-03-21T15:06:06.146Z",
"level": "INFO",
"thread": "Server thread",
"logger": "net.minecraft.server.MinecraftServer",
- "message": "Done (1.019s)! For help, type \"help\""
+ "message": "Done (1.019s)! For help, type \"help\"",
+ "throwable": ""
}]
}
```
-```json
+```jsonc
{
"jsonrpc": "2.0",
- "method": "console:notification/log_event",
+ "method": "console:notification/log/event",
"params": [{
"timestamp": "2026-03-21T15:06:07.212Z",
"level": "ERROR",
From a965c7f1d342ee489ccbad3635b8f59cb9b10c2c Mon Sep 17 00:00:00 2001
From: Mqx <62719703+Mqxx@users.noreply.github.com>
Date: Sun, 12 Jul 2026 23:07:52 +0200
Subject: [PATCH 6/7] Update gradle.properties
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 65ccec3..32d6d3f 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=0.1.0
+mod_version=1.0.0
maven_group=dev.loat
archives_base_name=msmp-console-mod
From d837f320e5f24d16cfc79d70334618c4648e5290 Mon Sep 17 00:00:00 2001
From: Mqx <62719703+Mqxx@users.noreply.github.com>
Date: Sun, 12 Jul 2026 23:08:36 +0200
Subject: [PATCH 7/7] Update README.md
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 22622dd..6856b87 100644
--- a/README.md
+++ b/README.md
@@ -53,9 +53,9 @@ The mod currently provides the following MSMP RPC methods. All of these methods
The mod also provides the following MSMP RPC notification that clients can subscribe to:
-| Method | Description |
-|:---------------------------------|:------------------------------------------------------------------------------------|
-| `console:notification/log/event` | Fired for every server console log event at or above the configured `log.min-level` |
+| Method | Description |
+|:---------------------------------|:--------------------------------------------------------------------------------|
+| `console:notification/log/event` | Fired for every server console log event at or above the configured `log.level` |
## Method Reference