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