diff --git a/README.md b/README.md index b28a860..bb44973 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,95 @@ -# Sniffer +> [!NOTE] +> This project is now being developed by Bookshelf team and has been renamed to Sniffer. Bookshelf has added more features to Sniffer and developed a VSCode plugin! So head over to their repository to stay updated~ +> +> It's worth mentioning that the original author is still actively contributing to Sniffer's development~ +> +> Sniffer: -## Overview -Sniffer is a debug adapter for Minecraft datapacks that allows you to debug your `.mcfunction` files directly from Visual Studio Code. It provides features like breakpoints, step execution, and variable inspection to make datapack development easier and more efficient. +# Datapack Breakpoint -## Features +English | [简体中文](README_zh.md) -- Set breakpoints in `.mcfunction` files -- Connect to a running Minecraft instance -- Inspect game state during debugging -- Step through command execution -- Path mapping between Minecraft and local files +## Introduce -## Requirements +This is a fabric mod for Minecraft 1.21, which allows you to set breakpoints in the game and "freeze" the game when +the breakpoint is reached. -- Minecraft with Fabric Loader -- Visual Studio Code +## Usage +* Set a breakpoint - - -## Mod Configuration -The mod can be configured through the in-game configuration screen, accessible via Mod Menu. -You can also configure the mod in the `config/sniffer.json` file. -The following options are available: - -### Debug Server Settings -- **Server Port**: The port number for the debug server (default: 25599) -- **Server path**: The path to the debug server (default: `/dap`) - -## Connecting to Minecraft - -1. Open your datapack project in VSCode -2. Create a `.vscode/launch.json` file with the following configuration: - -```json -{ - "version": "0.2.0", - "configurations": [ - { - "type": "sniffer", - "request": "attach", - "name": "Connect to Minecraft", - "address": "ws://localhost:25599/dap" - } - ] -} +say 1 +say 2 +#breakpoint +say 3 +say 4 ``` -3. Start Minecraft with the Sniffer mod installed -4. In VSCode, press F5 or click the "Run and Debug" button -5. Select "Connect to Minecraft" from the dropdown menu +In this case, after the game executes `say 2`, the game will be "frozen" because it meets the breakpoint. -You can now place breakpoints in your `.mcfunction` files and execute it from the game to step through the code. +When the game is "frozen", you can still move around, do whatever you want, just like execute the command `tick freeze`. +So you can check the game state, or do some debugging. -## Usage in Minecraft +* Step -The debugger can be controlled directly from Minecraft using the following commands: +When the game is "frozen", you can use the command `/breakpoint step` to execute the next command. In above example, +after the game meets the breakpoint, you can use `/breakpoint step` to execute `say 3`, and then use `/breakpoint step` +to execute `say 4`. When all commands are executed, the game will be unfrozen and continue running. -- `/breakpoint continue`: Resume execution after hitting a breakpoint -- `/breakpoint step`: Execute the next command and pause -- `/breakpoint step_over`: Skip to the next command in the current function -- `/breakpoint step_out`: Continue execution until the current function returns +* Continue -All commands require operator permissions (level 2) to use. +When the game is "frozen", you can use the command `/breakpoint move` to unfreeze the game and continue running. -When execution is paused at a breakpoint, the gametick will be freezed. +* Get Macro Arguments +By using `/breakpoint get `, you can get the value of the macro argument if the game is executing a macro function. +For example: +```mcfunction +#test:test_macro -## Development - -### Project Structure +say start +#breakpoint +$say $(msg) +say end +``` -- `src/main`: Main mod code for Minecraft -- `src/client`: Client-side mod code -- `vscode`: VSCode extension source code +After executing `function test:test_macro {"msg":"test"}`, we passed the value `test` to the macro argument `msg` and +then the game will pause before `$say $(msg)`. At this time, you can use `/breakpoint get msg` to get the value `test`. -### Building the Project +* Get Function Stack -To build the Minecraft mod: +By using `/breakpoint stack`, you can get the function stack of the current game. For example, if we have following two +functions: -```bash -./gradlew build -``` +```mcfunction +#test:test1 -To build the VSCode extension: +say 1 +function test:test2 +say 2 -```bash -cd vscode -npm install -npm run build +#test: test2 +say A +#breakpoint +say B ``` -## License +When the game pauses at the breakpoint, you can use `/breakpoint stack` and the function stack will be printed in the +chat screen: -This project is licensed under the MPL-2.0 License - see the [LICENSE](LICENSE) file for details. - -## Contributing +``` +test:test2 +test:test -Contributions are welcome! Please feel free to submit a Pull Request. +``` -## Acknowledgements +* Run command in current context -- [Fabric](https://fabricmc.net/) - Mod loader for Minecraft -- [VSCode Debug Adapter](https://code.visualstudio.com/api/extension-guides/debugger-extension) - VSCode debugging API -- [Datapack Debugger](https://github.com/Alumopper/Datapack-Debugger/) by [Alumopper](https://github.com/Alumopper) - Original implementation of the debugger, without the DAP layer +By using `/breakpoint run `, you can run any command in the current context, just like `execute ... run ...`. diff --git a/build.gradle b/build.gradle index 02d1549..f37ecbd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'fabric-loom' version '1.7-SNAPSHOT' + id 'fabric-loom' version '1.11-SNAPSHOT' id 'maven-publish' } @@ -32,6 +32,8 @@ loom { } } +// log4jConfigs.from(file("log4j.xml")) + } dependencies { @@ -51,10 +53,10 @@ dependencies { modImplementation "net.fabricmc.fabric-api:fabric-key-binding-api-v1:${project.fabric_version}" // Added Cloth Config and ModMenu - modApi("me.shedaniel.cloth:cloth-config-fabric:17.0.144") { + modApi("me.shedaniel.cloth:cloth-config-fabric:20.0.148") { exclude(group: "net.fabricmc.fabric-api") } - modImplementation("com.terraformersmc:modmenu:13.0.2") + modImplementation("com.terraformersmc:modmenu:16.0.0-rc.1") } processResources { diff --git a/gradle.properties b/gradle.properties index 6169699..c94579a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,9 +4,10 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=1.21.4 -yarn_mappings=1.21.4+build.8 -loader_version=0.16.10 +minecraft_version=1.21.10 +yarn_mappings=1.21.10+build.2 +loader_version=0.17.3 +loom_version=1.11-SNAPSHOT # Mod Properties mod_version=0.1.0 @@ -14,4 +15,4 @@ maven_group=net.gunivers archives_base_name=sniffer # Dependencies -fabric_version=0.118.0+1.21.4 \ No newline at end of file +fabric_version=0.136.0+1.21.10 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cf2b9a4..7ab7614 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://maven.fastmirror.net/repositories/gradle-dist/gradle-8.8-bin.zip +distributionUrl=https\://maven.fastmirror.net/repositories/gradle-dist/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/client/java/net/gunivers/sniffer/DatapackBreakpointClient.java b/src/client/java/net/gunivers/sniffer/DatapackBreakpointClient.java index e968e85..4fc30e3 100644 --- a/src/client/java/net/gunivers/sniffer/DatapackBreakpointClient.java +++ b/src/client/java/net/gunivers/sniffer/DatapackBreakpointClient.java @@ -5,6 +5,7 @@ import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.minecraft.client.option.KeyBinding; import net.minecraft.client.util.InputUtil; +import net.minecraft.util.Identifier; import org.lwjgl.glfw.GLFW; import net.gunivers.sniffer.command.BreakPointCommand;; @@ -22,13 +23,13 @@ public void onInitializeClient() { "sniffer.step", // The translation key of the keybinding's name InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse. GLFW.GLFW_KEY_F7, // The keycode of the key - "sniffer.name" // The translation key of the keybinding's category. + KeyBinding.Category.create(Identifier.of("sniffer.name")) // The translation key of the keybinding's category. )); ClientTickEvents.END_CLIENT_TICK.register(client -> { while(stepInto.wasPressed()) { if(BreakPointCommand.debugMode) { - BreakPointCommand.step(1, client.player.getCommandSource(client.getServer().getWorld(client.player.getWorld().getRegistryKey()))); + BreakPointCommand.step(1, client.player.getCommandSource(client.getServer().getWorld(client.player.getEntityWorld().getRegistryKey()))); } } }); diff --git a/src/main/java/net/gunivers/sniffer/command/BreakPointCommand.java b/src/main/java/net/gunivers/sniffer/command/BreakPointCommand.java index 358f1f6..44978c6 100644 --- a/src/main/java/net/gunivers/sniffer/command/BreakPointCommand.java +++ b/src/main/java/net/gunivers/sniffer/command/BreakPointCommand.java @@ -1,29 +1,31 @@ package net.gunivers.sniffer.command; import com.google.common.collect.Queues; -import com.mojang.brigadier.arguments.*; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.gunivers.sniffer.DatapackDebugger; +import net.gunivers.sniffer.dap.DebuggerState; +import net.gunivers.sniffer.dap.ScopeManager; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.command.CommandExecutionContext; - -import static net.gunivers.sniffer.Utils.addSnifferPrefix; -import static net.minecraft.server.command.CommandManager.literal; -import static net.minecraft.server.command.CommandManager.argument; - import net.minecraft.nbt.NbtElement; import net.minecraft.nbt.NbtHelper; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.MutableText; import net.minecraft.text.Text; +import net.minecraft.text.TextColor; import net.minecraft.util.Formatting; import net.minecraft.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import net.gunivers.sniffer.DatapackDebugger; -import net.gunivers.sniffer.dap.DebuggerState; -import net.gunivers.sniffer.dap.ScopeManager; import java.util.Deque; +import static net.gunivers.sniffer.util.Utils.addSnifferPrefix; +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + /** * Main command handler for the datapack debugging system. * Provides commands for setting breakpoints, stepping through code, and inspecting variables. @@ -173,7 +175,7 @@ public static void onInitialize() { var t = Text.literal(stack.getFunction()); var style = t.getStyle(); if(stacks.indexOf(stack) == 0){ - style = style.withBold(true); + style = style.withBold(true).withColor(TextColor.parse("aqua").getOrThrow()); }else { style = style.withBold(false); } @@ -240,40 +242,30 @@ public static void step(int steps, ServerCommandSource source) { isDebugCommand = true; moveSteps = steps; CommandExecutionContext context = null; - try { - while (moveSteps > 0) { - context = storedCommandExecutionContext.peekFirst(); - if (context != null) { - var cls = context.getClass(); - var method = cls.getDeclaredMethod("onStep"); - method.setAccessible(true); - method.invoke(context); - if (moveSteps != 0) { + while (moveSteps > 0) { + context = storedCommandExecutionContext.peekFirst(); + if (context != null) { + ReflectUtil.invoke(context, "onStep").onFailure(LOGGER::error); + if (moveSteps != 0) { + storedCommandExecutionContext.pollFirst().close(); + }else { + var result = (boolean) ReflectUtil.invoke(context, "ifContainsCommandAction").onFailure(LOGGER::error).getData(); + if(!result){ storedCommandExecutionContext.pollFirst().close(); - }else { - var method1 = cls.getDeclaredMethod("ifContainsCommandAction"); - method1.setAccessible(true); - boolean result = (boolean) method1.invoke(context); - if(!result){ - storedCommandExecutionContext.pollFirst().close(); - } - break; } - } else { - source.sendFeedback(() -> addSnifferPrefix(Text.translatable("sniffer.commands.breakpoint.step.over").formatted(Formatting.WHITE)), false); - continueExec(source); + break; } + } else { + source.sendFeedback(() -> addSnifferPrefix(Text.translatable("sniffer.commands.breakpoint.step.over").formatted(Formatting.WHITE)), false); + continueExec(source); } - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - } finally { - isDebugCommand = false; - if (context != null) { - try { - context.close(); - } catch (Exception e) { - LOGGER.error(e.getMessage()); - } + } + isDebugCommand = false; + if (context != null) { + try { + context.close(); + } catch (Exception e) { + LOGGER.error(e.getMessage()); } } } @@ -313,10 +305,10 @@ public static void continueExec(@NotNull ServerCommandSource source) { return null; } try { - var cls = context.getClass(); - var method = cls.getDeclaredMethod("getKey", String.class); - method.setAccessible(true); - return (Pair) method.invoke(context, key); + //noinspection unchecked + return (Pair) ReflectUtil.invoke(context, "getKey", key) + .onFailure(LOGGER::error) + .getDataOrElse(null); }catch (Exception e){ LOGGER.error(e.toString()); source.sendError(Text.translatable("sniffer.commands.breakpoint.get.fail.error", e.toString())); diff --git a/src/main/java/net/gunivers/sniffer/command/FunctionInAction.java b/src/main/java/net/gunivers/sniffer/command/FunctionInAction.java index 9ba8d6b..f84d1cf 100644 --- a/src/main/java/net/gunivers/sniffer/command/FunctionInAction.java +++ b/src/main/java/net/gunivers/sniffer/command/FunctionInAction.java @@ -1,17 +1,21 @@ package net.gunivers.sniffer.command; -import net.gunivers.sniffer.EncapsulationBreaker; +import com.mojang.logging.LogUtils; +import net.gunivers.sniffer.dap.ScopeManager; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.command.CommandExecutionContext; import net.minecraft.command.Frame; import net.minecraft.command.SourcedCommandAction; import net.minecraft.nbt.NbtCompound; import net.minecraft.server.command.AbstractServerCommandSource; import net.minecraft.server.function.CommandFunction; -import net.gunivers.sniffer.dap.ScopeManager; import net.minecraft.server.function.ExpandedMacro; +import net.minecraft.server.function.Macro; +import net.minecraft.server.function.Procedure; +import org.slf4j.Logger; -import static net.gunivers.sniffer.Utils.getId; import static net.gunivers.sniffer.command.StepType.isStepOut; +import static net.gunivers.sniffer.util.Utils.getId; /** * Action handler for when a function is entered during debugging. @@ -25,6 +29,8 @@ */ public class FunctionInAction> implements SourcedCommandAction { + private final static Logger LOGGER = LogUtils.getLogger(); + /** * The function being entered. * This reference stores the Minecraft function that is about to be executed @@ -52,13 +58,23 @@ public FunctionInAction(CommandFunction function){ public void execute(T source, CommandExecutionContext context, Frame frame){ // Each time we are going into a deeper scope, we want to decrement of one to not skip the mustStop evaluation at the first command // We must do it here since the decrementation in FixCommandActionMixin is not called when a mcfunction is called - if(BreakPointCommand.moveSteps > 0 && !isStepOut()) BreakPointCommand.moveSteps --; + if(BreakPointCommand.isDebugging && BreakPointCommand.moveSteps > 0 && !isStepOut()){ + BreakPointCommand.moveSteps --; + } var id = function.id(); if(function instanceof ExpandedMacro macro) { id = getId(macro); } + if(ReflectUtil.getT(function, "originalMacro", Macro.class).onFailure(LOGGER::error).getDataOrElse(null) != null){ + //if originalMacro is not null, we are in a macro call, so we need to get the macro arguments and the original macro. + var function = ReflectUtil.getT(frame, "function", Procedure.class).onFailure(LOGGER::error).getDataOrElse(null); + if(function == null) return; + var macroVariables = ReflectUtil.getT(function, "arguments", NbtCompound.class).onFailure(LOGGER::error).getDataOrElse(null); + ScopeManager.get().newScope(id.toString(), source, macroVariables); + }else{ + //otherwise it is a regular function call, so we just create a new scope with the function id without getting the macro arguments. + ScopeManager.get().newScope(id.toString(), source); + } - var macroVariables = EncapsulationBreaker.getAttribute(frame, "function").flatMap(fun -> EncapsulationBreaker.getAttribute(fun, "arguments")); - ScopeManager.get().newScope(id.toString(), source, (NbtCompound) macroVariables.orElse(null)); } } diff --git a/src/main/java/net/gunivers/sniffer/command/FunctionOutAction.java b/src/main/java/net/gunivers/sniffer/command/FunctionOutAction.java index be00dcb..1d62b9d 100644 --- a/src/main/java/net/gunivers/sniffer/command/FunctionOutAction.java +++ b/src/main/java/net/gunivers/sniffer/command/FunctionOutAction.java @@ -48,6 +48,6 @@ public FunctionOutAction(CommandFunction function){ public void execute(T source, CommandExecutionContext context, Frame frame) { ScopeManager.get().unscope(); // BreakPointCommand.stepDepth - 1 because we only want to decrement if we go higher than the stepDepth - if(BreakPointCommand.moveSteps > 0 && isStepOut() && frame.depth() - 1 <= BreakPointCommand.stepDepth - 1) BreakPointCommand.moveSteps --; + if(BreakPointCommand.isDebugging && BreakPointCommand.moveSteps > 0 && isStepOut() && frame.depth() - 1 <= BreakPointCommand.stepDepth - 1) BreakPointCommand.moveSteps --; } } diff --git a/src/main/java/net/gunivers/sniffer/dap/DapServer.java b/src/main/java/net/gunivers/sniffer/dap/DapServer.java index 691ccfb..7e7561c 100644 --- a/src/main/java/net/gunivers/sniffer/dap/DapServer.java +++ b/src/main/java/net/gunivers/sniffer/dap/DapServer.java @@ -15,7 +15,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; -import static net.gunivers.sniffer.Utils.addSnifferPrefix; +import static net.gunivers.sniffer.util.Utils.addSnifferPrefix; import static net.gunivers.sniffer.command.BreakPointCommand.continueExec; /** @@ -70,10 +70,6 @@ public CompletableFuture initialize(InitializeRequestArguments arg capabilities.setSupportsConfigurationDoneRequest(true); // capabilities.setSupportsBreakpointLocationsRequest(true); - // Register event handlers - debuggerState.onStop(this::onStop); - debuggerState.onContinue(this::onContinue); - LOGGER.debug("Sending capabilities response: {}", capabilities); return CompletableFuture.completedFuture(capabilities).thenApply(capabilities1 -> { LOGGER.debug("Sending initialized event"); diff --git a/src/main/java/net/gunivers/sniffer/dap/NbtElementVariableVisitor.java b/src/main/java/net/gunivers/sniffer/dap/NbtElementVariableVisitor.java index 056ac4a..916b6b2 100644 --- a/src/main/java/net/gunivers/sniffer/dap/NbtElementVariableVisitor.java +++ b/src/main/java/net/gunivers/sniffer/dap/NbtElementVariableVisitor.java @@ -162,7 +162,7 @@ public void visitList(NbtList element) { public void visitCompound(NbtCompound compound) { var children = new LinkedList(); var compoundIndex = this.index++; - var compoundVar = new DebuggerVariable(compoundIndex, this.currentName, compound.asString(), children, isRoot); + var compoundVar = new DebuggerVariable(compoundIndex, this.currentName, compound.asString().orElse(null), children, isRoot); isRoot = false; variables.put(compoundIndex, compoundVar); for(var key: compound.getKeys()) { @@ -187,15 +187,16 @@ public void visitEnd(NbtEnd element) {} * * @param list The NBT list or array to convert */ - private void convertList(AbstractNbtList list) { + private void convertList(AbstractNbtList list) { var arrayIndex = index++; var array = new LinkedList(); var name = currentName; - var result = new DebuggerVariable(arrayIndex, name, list.asString(), array, false); + var result = new DebuggerVariable(arrayIndex, name, list.asString().orElse(null), array, false); variables.put(arrayIndex, result); for(int i = 0; i < list.size(); i++) { currentName = Integer.toString(index); - list.get(i).accept(this); + //method_10534(int i) = get(int i) + list.method_10534(i).accept(this); array.add(returnVariable); } index++; @@ -210,7 +211,7 @@ private void convertList(AbstractNbtList list) { */ private void convertPrimitive(NbtElement element) { var i = index++; - returnVariable = new DebuggerVariable(i, currentName, element.asString(), List.of(), isRoot); + returnVariable = new DebuggerVariable(i, currentName, element.asString().orElse(null), List.of(), isRoot); variables.put(i, returnVariable); } } diff --git a/src/main/java/net/gunivers/sniffer/dap/ScopeManager.java b/src/main/java/net/gunivers/sniffer/dap/ScopeManager.java index cc4aec1..3278305 100644 --- a/src/main/java/net/gunivers/sniffer/dap/ScopeManager.java +++ b/src/main/java/net/gunivers/sniffer/dap/ScopeManager.java @@ -1,5 +1,6 @@ package net.gunivers.sniffer.dap; +import com.google.common.base.Suppliers; import net.minecraft.nbt.NbtCompound; import net.minecraft.server.command.AbstractServerCommandSource; import org.jetbrains.annotations.Nullable; @@ -7,6 +8,7 @@ import java.nio.file.Path; import java.util.*; +import java.util.function.Supplier; /** * Manager for debug scopes in the debugger. @@ -54,26 +56,49 @@ public static class DebugScope { private final RealPath path; private final AbstractServerCommandSource executor; private int line = -2; - private final Map variables; + private final Supplier> variables; private final int id = ID++; private final DebugScope parent; /** - * Creates a new debug scope. + * Creates a new debug scope for a macro * * @param parent The parent scope, or null if this is the root scope * @param function The function mcpath being executed * @param executor The command source executing the function + * @param macroVariables The NBT compound containing macro variables */ - protected DebugScope(@Nullable DebugScope parent, String function, AbstractServerCommandSource executor, NbtCompound macroVariables) { + protected DebugScope(@Nullable DebugScope parent, String function, AbstractServerCommandSource executor,@Nullable NbtCompound macroVariables) { this.parent = parent; this.function = function; this.path = PATHS.get(function); this.executor = executor; - this.variables = VariableManager.convertCommandSource(executor, ID); - var macro = VariableManager.convertNbtCompound("macro", macroVariables, ID + this.variables.size(), true); - this.variables.putAll(macro); - ID += variables.size(); + this.variables = Suppliers.memoize(() -> { + var qwq = VariableManager.convertCommandSource(executor, ID); + var macro = VariableManager.convertNbtCompound("macro", macroVariables, ID + qwq.size(), true); + qwq.putAll(macro); + ID += qwq.size(); + return qwq; + }); + } + + /** + * Creates a new debug scope for a normal function + * + * @param parent The parent scope, or null if this is the root scope + * @param function The function mcpath being executed + * @param executor The command source executing the function + */ + protected DebugScope(@Nullable DebugScope parent, String function, AbstractServerCommandSource executor) { + this.parent = parent; + this.function = function; + this.path = PATHS.get(function); + this.executor = executor; + this.variables = Suppliers.memoize(() -> { + var qwq = VariableManager.convertCommandSource(executor, ID); + ID += qwq.size(); + return qwq; + }); } /** @@ -118,7 +143,7 @@ public int getId() { * @return A list of all variables in this scope */ public List getVariables() { - return List.copyOf(variables.values()); + return List.copyOf(variables.get().values()); } /** @@ -145,7 +170,7 @@ public Optional getCallerLine() { * @return A list of root variables */ public List getRootVariables() { - return this.variables.values().stream().filter(DebuggerVariable::isRoot).toList(); + return this.variables.get().values().stream().filter(DebuggerVariable::isRoot).toList(); } /** @@ -204,6 +229,13 @@ public void newScope(String function, AbstractServerCommandSource executor, N this.currentScope = scope; } + public void newScope(String function, AbstractServerCommandSource executor) { + var scope = new DebugScope(this.currentScope, function, executor); + this.scopeIds.add(scope.id); + this.debugScopeStack.push(scope); + this.currentScope = scope; + } + /** * Removes the current scope from the stack and sets the parent as current. * Call this when exiting a function. @@ -226,6 +258,13 @@ public int count() { return this.debugScopeStack.size(); } + /** + * If the scope stack is empty. + */ + public boolean isEmpty(){ + return this.debugScopeStack.isEmpty(); + } + /** * Clears all scopes and resets the state. */ @@ -263,7 +302,7 @@ public Optional getScope(int id) { * @return A list of root-level variables in this scope */ private List getRootVariables(DebugScope scope) { - return scope.variables.values().stream().filter(DebuggerVariable::isRoot).toList(); + return scope.variables.get().values().stream().filter(DebuggerVariable::isRoot).toList(); } /** diff --git a/src/main/java/net/gunivers/sniffer/dap/VariableManager.java b/src/main/java/net/gunivers/sniffer/dap/VariableManager.java index 94c5edf..a19723e 100644 --- a/src/main/java/net/gunivers/sniffer/dap/VariableManager.java +++ b/src/main/java/net/gunivers/sniffer/dap/VariableManager.java @@ -26,7 +26,6 @@ */ public class VariableManager { - /** * Converts a command source into a map of debugger variables. * This method extracts relevant information from the command source such as @@ -38,6 +37,7 @@ public class VariableManager { */ public static Map convertCommandSource(AbstractServerCommandSource source, int startIndex) { if(source instanceof ServerCommandSource commandSource) { + //if executor is an entity var executorVariable = convertEntityVariables(commandSource.getEntity(), startIndex, true); var currentIndex = executorVariable.getRight(); @@ -74,7 +74,10 @@ public static Map convertCommandSource(AbstractServer */ private static Pair convertEntityVariables(@Nullable Entity entity, int startIndex, boolean isRoot) { if(entity == null) { - return new Pair<>(new DebuggerVariable(startIndex, "executor", "server", List.of(), isRoot), ++startIndex); + return new Pair<>( + new DebuggerVariable(startIndex, "executor", "server", List.of(), isRoot), + ++startIndex + ); } var id = startIndex + 1; @@ -84,7 +87,7 @@ private static Pair convertEntityVariables(@Nullable var objectUuid = new DebuggerVariable(id++, "uuid", entity.getUuidAsString(), List.of(), false); - var pos = convertPos(entity.getPos(), id, false); + var pos = convertPos(entity.getEntityPos(), id, false); id = pos.getRight(); var rot = convertRotation(entity.getRotationClient(), id, false); @@ -98,7 +101,6 @@ private static Pair convertEntityVariables(@Nullable return new Pair<>(new DebuggerVariable(startIndex, "executor", displayName, children, isRoot), id); } - /** * Converts an entity type to a readable string representation. * @@ -197,7 +199,7 @@ private static void flattenToMap(DebuggerVariable variable, Map convertNbtCompound(String name, NbtCompound compound, int startIndex, boolean isRoot) { + public static Map convertNbtCompound(String name, @Nullable NbtCompound compound, int startIndex, boolean isRoot) { if(compound != null) { var visitor = new NbtElementVariableVisitor(startIndex, name, isRoot); compound.accept(visitor); diff --git a/src/main/java/net/gunivers/sniffer/mixin/CommandExecutionContextMixin.java b/src/main/java/net/gunivers/sniffer/mixin/CommandExecutionContextMixin.java index 50ee180..f2fb98c 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/CommandExecutionContextMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/CommandExecutionContextMixin.java @@ -1,6 +1,7 @@ package net.gunivers.sniffer.mixin; import net.gunivers.sniffer.command.FunctionInAction; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.command.*; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; @@ -18,7 +19,6 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import net.gunivers.sniffer.EncapsulationBreaker; import net.gunivers.sniffer.command.FunctionOutAction; import net.gunivers.sniffer.dap.DebuggerState; import net.gunivers.sniffer.dap.ScopeManager; @@ -58,7 +58,8 @@ abstract public class CommandExecutionContextMixin { @Shadow private int currentDepth; @Shadow protected abstract void queuePendingCommands(); - @Shadow private static > Frame frame(CommandExecutionContext context, ReturnValueConsumer returnValueConsumer){return null;} + @SuppressWarnings("DataFlowIssue") + @Shadow @NotNull private static > Frame frame(CommandExecutionContext context, ReturnValueConsumer returnValueConsumer){return null;} /** * Holds the next command that will be executed in the current context. @@ -88,13 +89,9 @@ private static > void enqueueProcedureC ) { // Create a new frame for the procedure call Frame frame = frame(context, returnValueConsumer); - try { - // Use EncapsulationBreaker instead of reflection - EncapsulationBreaker.getAttribute(frame, "function") - .ifPresent(ignored -> EncapsulationBreaker.callFunction(frame, "setFunction", procedure)); - } catch (Exception e) { - throw new RuntimeException(e); - } + ReflectUtil.set(frame, "function", Procedure.class, procedure) + .onFailure(e -> LOGGER.error("Failed to set function in frame for procedure call: {}", e) + ); // Add the command to the queue with the modified frame context.enqueueCommand( new CommandQueueEntry<>(frame, new CommandFunctionAction<>(procedure, source.getReturnValueConsumer(), false).bind(source)) @@ -110,6 +107,7 @@ private static > void enqueueProcedureC */ @Inject(method = "run()V", at = @At("HEAD"), cancellable = true) private void onRun(CallbackInfo ci){ + //noinspection unchecked final var THIS = (CommandExecutionContext) (Object) this; // Process pending commands before starting the main loop @@ -169,6 +167,7 @@ private void onRun(CallbackInfo ci){ */ @Unique private void onStep() { + //noinspection unchecked final var THIS = (CommandExecutionContext) (Object) this; // Process pending commands before starting the step @@ -314,7 +313,13 @@ private boolean processBreakpointForEntry(CommandQueueEntry commandQueueEntry var function = this.getNextCommand(commandQueueEntry); - var lineOpt = function.flatMap(fun -> EncapsulationBreaker.getAttribute(fun, "sourceLine")); + var lineOpt = function.flatMap(fun -> { + if(fun instanceof SingleCommandAction.Sourced){ + return ReflectUtil.getT(fun, "sourceLine", int.class).onFailure(LOGGER::error).toOptional(); + }else{ + return Optional.empty(); + } + }); if (lineOpt.isEmpty()) { return false; } @@ -353,11 +358,9 @@ private boolean mustPause(CommandQueueEntry commandQueueEntry) { if(nextCommand instanceof FunctionOutAction || nextCommand instanceof FunctionInAction) { shouldPause = false; } else { - var lineOpt = EncapsulationBreaker.getAttribute(nextCommand, "sourceLine"); - if(lineOpt.isPresent()) { - var line = (int) lineOpt.get(); - ScopeManager.get().getCurrentScope().ifPresent(scope -> scope.setLine(line)); - } + ReflectUtil.getT(nextCommand, "sourceLine", int.class).onFailure(LOGGER::error).onSuccess(line -> + ScopeManager.get().getCurrentScope().ifPresent(scope -> scope.setLine(line)) + ); } } @@ -391,7 +394,7 @@ private Optional> getNextCommand(CommandQueueEntry co return Optional.empty(); } - var index = (int) EncapsulationBreaker.getAttribute(steppedAction, "nextActionIndex").get(); + int index = ReflectUtil.getT(steppedAction, "nextActionIndex", int.class).getData(); if(index < 0) { return Optional.empty(); } @@ -403,29 +406,23 @@ private Optional> getNextCommand(CommandQueueEntry co /** * Gets the expanded macro from a frame using reflection. - * This method uses EncapsulationBreaker to access private fields in the Frame object. * * @param frame The frame to get the macro from * @return The expanded macro, or null if an error occurred */ @Unique private ExpandedMacro getExpandedMacroFromFrame(Frame frame) { - try { - return (ExpandedMacro) EncapsulationBreaker.getAttribute(frame, "function").get(); - } catch (Exception e) { - LOGGER.error("Failed to get expanded macro from frame: {}", e.getMessage()); - return null; - } + return ReflectUtil.getT(frame, "function", ExpandedMacro.class).onFailure(LOGGER::error).getDataOrElse(null); } /** * Retrieves the expanded macro and its arguments from a command queue entry. * This method extracts function information and arguments from a command entry. - * It uses EncapsulationBreaker to access private fields. - * + * * @param commandQueueEntry The command queue entry * @return A pair containing the expanded macro and its arguments, or null if not available */ + @SuppressWarnings({"unchecked"}) @Unique private Pair, NbtCompound> getMacroAndArgsFromEntry(CommandQueueEntry commandQueueEntry) { if (commandQueueEntry == null) { @@ -434,17 +431,15 @@ private Pair, NbtCompound> getMacroAndArgsFromEntry(CommandQueu var frame = commandQueueEntry.frame(); try { - // Use EncapsulationBreaker instead of reflection - var function = (ExpandedMacro) EncapsulationBreaker.getAttribute(frame, "function").orElse(null); + var function = ReflectUtil.getT(frame, "function", ExpandedMacro.class).onFailure(LOGGER::error).getDataOrElse(null); if (function == null) { return null; } - // Get the arguments using EncapsulationBreaker - var args = (NbtCompound) EncapsulationBreaker.getAttribute(function, "arguments").orElse(null); + var args = ReflectUtil.getT(function, "arguments", NbtCompound.class).onFailure(LOGGER::error).getDataOrElse(null); - return new Pair<>(function, args); + return new Pair, NbtCompound>(function, args); } catch (Exception e) { LOGGER.error("Failed to get macro and args: {}", e.getMessage()); return null; @@ -543,7 +538,7 @@ private boolean isFixCommandAction(@NotNull CommandAction action) { } } } catch (IllegalAccessException e) { - e.printStackTrace(); + LOGGER.error("Failed to check if action is fix command action: {}", e.getMessage()); } return false; } diff --git a/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionActionMixin.java b/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionActionMixin.java index 443e132..7c141c0 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionActionMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionActionMixin.java @@ -1,11 +1,14 @@ package net.gunivers.sniffer.mixin; import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.logging.LogUtils; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.command.CommandExecutionContext; import net.minecraft.command.CommandFunctionAction; import net.minecraft.command.Frame; import net.minecraft.server.command.AbstractServerCommandSource; import net.minecraft.server.function.Procedure; +import org.slf4j.Logger; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -13,8 +16,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.lang.reflect.Field; - /** * Mixin for the CommandFunctionAction class to enhance function execution tracking. * This mixin associates the function being executed with its execution frame, @@ -27,6 +28,8 @@ @Mixin(CommandFunctionAction.class) public class CommandFunctionActionMixin> { + private static final Logger LOGGER = LogUtils.getLogger(); + /** The function being executed by this action */ @Shadow @Final private Procedure function; @@ -42,15 +45,14 @@ public class CommandFunctionActionMixin * @param ci The callback info * @param frame2 The newly created frame for the function execution */ - @Inject(method = "execute(Lnet/minecraft/server/command/AbstractServerCommandSource;Lnet/minecraft/command/CommandExecutionContext;Lnet/minecraft/command/Frame;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/command/SteppedCommandAction;enqueueCommands(Lnet/minecraft/command/CommandExecutionContext;Lnet/minecraft/command/Frame;Ljava/util/List;Lnet/minecraft/command/SteppedCommandAction$ActionWrapper;)V")) + @Inject(method = "execute(Lnet/minecraft/server/command/AbstractServerCommandSource;Lnet/minecraft/command/CommandExecutionContext;Lnet/minecraft/command/Frame;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/command/SteppedCommandAction;enqueueCommands(Lnet/minecraft/command/CommandExecutionContext;Lnet/minecraft/command/Frame;Ljava/util/List;Lnet/minecraft/command/SteppedCommandAction$ActionWrapper;)V" + ) + ) public void onExecute(T abstractServerCommandSource, CommandExecutionContext commandExecutionContext, Frame frame, CallbackInfo ci, @Local(ordinal = 1) Frame frame2){ - try { - Field field = frame2.getClass().getDeclaredField("function"); - field.setAccessible(true); - field.set(frame2, function); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } + ReflectUtil.set(frame2, "function", Procedure.class, function).onFailure(LOGGER::error); } } diff --git a/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionManagerMixin.java b/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionManagerMixin.java new file mode 100644 index 0000000..a17f3bc --- /dev/null +++ b/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionManagerMixin.java @@ -0,0 +1,42 @@ +package net.gunivers.sniffer.mixin; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.function.CommandFunctionManager; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(CommandFunctionManager.class) +public class CommandFunctionManagerMixin { + + @Shadow @Final private static Logger LOGGER; + + @Shadow @Final private MinecraftServer server; + + @Inject(method = "tick", at = @At("HEAD")) + public void beforeTick(CallbackInfo ci){ + //TODO at every start of a tick, the stack should be empty +// if(this.server.getTickManager().shouldTick()){ +// ScopeManager.get().clear(); +// } + } + + @Inject(method = "tick", at = @At("TAIL")) + public void afterTick(CallbackInfo ci) { + //TODO at the end of a tick, the stack should be empty too, or a leak has occurred +// if(this.server.getTickManager().shouldTick()){ +// if(!ScopeManager.get().isEmpty()){ +// var scopes = new StringBuilder(); +// for (var scope: ScopeManager.get().getDebugScopes()){ +// scopes.append(scope.getFunction()).append('\n'); +// } +// LOGGER.warn("A leak occurred! \n Current scopes: \n {}", scopes); +// ScopeManager.get().clear(); +// } +// } + } +} diff --git a/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionMixin.java b/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionMixin.java index 712b659..a07b043 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/CommandFunctionMixin.java @@ -3,6 +3,8 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.gunivers.sniffer.command.FunctionTextLoader; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.command.SourcedCommandAction; import net.minecraft.server.command.AbstractServerCommandSource; import net.minecraft.server.function.CommandFunction; @@ -13,15 +15,13 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyVariable; -import net.gunivers.sniffer.EncapsulationBreaker; -import net.gunivers.sniffer.command.FunctionTextLoader; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; -import static net.minecraft.server.function.CommandFunction.validateCommandLength; import static net.minecraft.server.function.CommandFunction.parse; +import static net.minecraft.server.function.CommandFunction.validateCommandLength; /** * @author theogiraudet @@ -66,7 +66,7 @@ static > CommandFunction create(Iden for (int i = 0; i < lines.size(); ++i) { int j = i + 1; - String string = ((String) lines.get(i)).trim(); + String string = lines.get(i).trim(); String string3; if (continuesToNextLine(string)) { StringBuilder stringBuilder = new StringBuilder(string); @@ -78,7 +78,7 @@ static > CommandFunction create(Iden } stringBuilder.deleteCharAt(stringBuilder.length() - 1); - String string2 = ((String) lines.get(i)).trim(); + String string2 = lines.get(i).trim(); stringBuilder.append(string2); validateCommandLength(stringBuilder); } while (continuesToNextLine(stringBuilder)); @@ -106,8 +106,14 @@ static > CommandFunction create(Iden } else { try { SourcedCommandAction action = parse(dispatcher, source, stringReader); - EncapsulationBreaker.callFunction(action, "setSourceFunction", id.toString()); - EncapsulationBreaker.callFunction(action, "setSourceLine", j - 1); + ReflectUtil.invoke(action, "setSourceFunction", id.toString()) + .onFailure(msg -> { + throw new IllegalArgumentException(msg); + }); + ReflectUtil.invoke(action, "setSourceLine", j - 1) + .onFailure(msg -> { + throw new IllegalArgumentException(msg); + }); functionBuilder.addAction(action); } catch (CommandSyntaxException commandSyntaxException) { throw new IllegalArgumentException("Whilst parsing command on line " + j + ": " + commandSyntaxException.getMessage()); diff --git a/src/main/java/net/gunivers/sniffer/mixin/DirectoryResourcePackMixin.java b/src/main/java/net/gunivers/sniffer/mixin/DirectoryResourcePackMixin.java index 646772a..2b8a4ba 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/DirectoryResourcePackMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/DirectoryResourcePackMixin.java @@ -4,8 +4,8 @@ import com.mojang.logging.LogUtils; import net.minecraft.resource.*; import net.minecraft.util.Identifier; -import net.minecraft.util.PathUtil; import net.minecraft.util.Util; +import net.minecraft.util.path.PathUtil; import org.slf4j.Logger; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; diff --git a/src/main/java/net/gunivers/sniffer/mixin/ExpandedMacroMixin.java b/src/main/java/net/gunivers/sniffer/mixin/ExpandedMacroMixin.java index bdeba6b..3b97be1 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/ExpandedMacroMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/ExpandedMacroMixin.java @@ -4,6 +4,7 @@ import net.minecraft.nbt.NbtCompound; import net.minecraft.server.command.AbstractServerCommandSource; import net.minecraft.server.function.ExpandedMacro; +import net.minecraft.server.function.Macro; import net.minecraft.util.Identifier; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -40,6 +41,10 @@ public class ExpandedMacroMixin> { @Unique private Identifier functionIdentifier; + /** The original macro this function is resolved from */ + @Unique + private Macro originalMacro; + /** * Injects function entry and exit actions into the macro's command list. * This allows the debugger to track when a macro is entered and exited, @@ -49,6 +54,7 @@ public class ExpandedMacroMixin> { */ @Inject(method = "", at = @At("TAIL")) private void onInit(CallbackInfo ci) { + //noinspection unchecked final var THIS = (ExpandedMacro) (Object) this; entries.addFirst(new FunctionInAction<>(THIS)); entries.add(new FunctionOutAction<>(THIS)); diff --git a/src/main/java/net/gunivers/sniffer/mixin/FallthroughCommandActionMixin.java b/src/main/java/net/gunivers/sniffer/mixin/FallthroughCommandActionMixin.java new file mode 100644 index 0000000..0cbb2d9 --- /dev/null +++ b/src/main/java/net/gunivers/sniffer/mixin/FallthroughCommandActionMixin.java @@ -0,0 +1,22 @@ +package net.gunivers.sniffer.mixin; + +import net.gunivers.sniffer.command.BreakPointCommand; +import net.minecraft.command.CommandExecutionContext; +import net.minecraft.command.FallthroughCommandAction; +import net.minecraft.command.Frame; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static net.gunivers.sniffer.command.StepType.isStepOut; + +@Mixin(FallthroughCommandAction.class) +public class FallthroughCommandActionMixin { + @Inject(method = "execute", at = @At("HEAD")) + private void onExecute(CommandExecutionContext commandExecutionContext, Frame frame, CallbackInfo ci){ + if(BreakPointCommand.isDebugging && BreakPointCommand.moveSteps > 0 && !isStepOut()) { + BreakPointCommand.moveSteps --; + } + } +} diff --git a/src/main/java/net/gunivers/sniffer/mixin/FixCommandActionMixin.java b/src/main/java/net/gunivers/sniffer/mixin/FixCommandActionMixin.java index 92ccd5f..50357f6 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/FixCommandActionMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/FixCommandActionMixin.java @@ -15,7 +15,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import net.gunivers.sniffer.command.BreakPointCommand; -import static net.gunivers.sniffer.Utils.addSnifferPrefix; +import static net.gunivers.sniffer.util.Utils.addSnifferPrefix; import static net.gunivers.sniffer.command.StepType.isStepOut; /** @@ -42,7 +42,8 @@ public class FixCommandActionMixin> { * @param frame The execution frame * @param ci The callback info */ - @Inject(method = "execute(Lnet/minecraft/server/command/AbstractServerCommandSource;Lnet/minecraft/command/CommandExecutionContext;Lnet/minecraft/command/Frame;)V", at = @At("HEAD")) + @Inject(method = "execute(Lnet/minecraft/server/command/AbstractServerCommandSource;Lnet/minecraft/command/CommandExecutionContext;Lnet/minecraft/command/Frame;)V", + at = @At("HEAD")) private void execute(T abstractServerCommandSource, CommandExecutionContext commandExecutionContext, Frame frame, CallbackInfo ci) { if(frame.depth() == 0) return; diff --git a/src/main/java/net/gunivers/sniffer/mixin/FrameMixin.java b/src/main/java/net/gunivers/sniffer/mixin/FrameMixin.java index 2c0f16e..57c8ad9 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/FrameMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/FrameMixin.java @@ -1,9 +1,17 @@ package net.gunivers.sniffer.mixin; +import net.gunivers.sniffer.command.BreakPointCommand; +import net.gunivers.sniffer.dap.ScopeManager; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.command.Frame; import net.minecraft.server.function.Procedure; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static net.gunivers.sniffer.command.StepType.isStepOut; /** * Mixin for the Frame class to add additional functionality for debugging. @@ -20,4 +28,17 @@ public class FrameMixin { */ @Unique private Procedure function; + + @Unique + private int getDepth(){ + return (int)ReflectUtil.invoke(this, "depth").getData(); + } + + @Inject(method = "doReturn", at = @At("HEAD")) + private void beforeReturn(CallbackInfo ci) { + // when a function is returned by a return command, the FunctionOutAction will not execute, so we need to execute it here manually + ScopeManager.get().unscope(); + // BreakPointCommand.stepDepth - 1 because we only want to decrement if we go higher than the stepDepth + if(BreakPointCommand.moveSteps > 0 && isStepOut() && getDepth() - 1 <= BreakPointCommand.stepDepth - 1) BreakPointCommand.moveSteps --; + } } diff --git a/src/main/java/net/gunivers/sniffer/mixin/MacroMixin.java b/src/main/java/net/gunivers/sniffer/mixin/MacroMixin.java index 0d1c4ec..7038c13 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/MacroMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/MacroMixin.java @@ -1,6 +1,7 @@ package net.gunivers.sniffer.mixin; import com.mojang.brigadier.CommandDispatcher; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.nbt.NbtCompound; import net.minecraft.server.command.AbstractServerCommandSource; import net.minecraft.server.function.ExpandedMacro; @@ -14,7 +15,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.lang.reflect.Field; import java.util.List; /** @@ -43,7 +43,8 @@ public abstract class MacroMixin> { * @param dispatcher The command dispatcher * @param cir The callback info returnable */ - @Inject(method = "withMacroReplaced(Lnet/minecraft/nbt/NbtCompound;Lcom/mojang/brigadier/CommandDispatcher;)Lnet/minecraft/server/function/Procedure;", at = @At("HEAD")) + @Inject(method = "withMacroReplaced(Lnet/minecraft/nbt/NbtCompound;Lcom/mojang/brigadier/CommandDispatcher;)Lnet/minecraft/server/function/Procedure;", + at = @At("HEAD")) private void OnWithMacroReplaced(NbtCompound arguments, CommandDispatcher dispatcher, CallbackInfoReturnable> cir){ this.arguments = arguments; } @@ -58,19 +59,13 @@ private void OnWithMacroReplaced(NbtCompound arguments, CommandDispatcher dis * @param dispatcher The command dispatcher * @param cir The callback info returnable */ - @Inject(method = "withMacroReplaced(Ljava/util/List;Ljava/util/List;Lcom/mojang/brigadier/CommandDispatcher;)Lnet/minecraft/server/function/Procedure;", at = @At("RETURN"), cancellable = true) + @Inject(method = "withMacroReplaced(Ljava/util/List;Ljava/util/List;Lcom/mojang/brigadier/CommandDispatcher;)Lnet/minecraft/server/function/Procedure;", + at = @At("RETURN"), cancellable = true) private void OnWithMacroReplaced(List varNames, List arguments, CommandDispatcher dispatcher, CallbackInfoReturnable> cir){ ExpandedMacro function = (ExpandedMacro) cir.getReturnValue(); - try { - Field field = function.getClass().getDeclaredField("arguments"); - field.setAccessible(true); - field.set(function, this.arguments); - field = function.getClass().getDeclaredField("functionIdentifier"); - field.setAccessible(true); - field.set(function, this.id()); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } + ReflectUtil.set(function, "arguments", NbtCompound.class, this.arguments); + ReflectUtil.set(function, "functionIdentifier", Identifier.class, this.id()); + ReflectUtil.set(function, "originalMacro", Macro.class, this); cir.setReturnValue(function); } diff --git a/src/main/java/net/gunivers/sniffer/mixin/SingleCommandActionMixin.java b/src/main/java/net/gunivers/sniffer/mixin/SingleCommandActionMixin.java new file mode 100644 index 0000000..22c4d8f --- /dev/null +++ b/src/main/java/net/gunivers/sniffer/mixin/SingleCommandActionMixin.java @@ -0,0 +1,36 @@ +package net.gunivers.sniffer.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import net.gunivers.sniffer.command.BreakPointCommand; +import net.minecraft.command.*; +import net.minecraft.server.command.AbstractServerCommandSource; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +import static net.gunivers.sniffer.command.StepType.isStepOut; + +@Mixin(SingleCommandAction.class) +public class SingleCommandActionMixin> { + + @Inject(method = "execute", at = @At(value = "JUMP", opcode = Opcodes.IFEQ, ordinal = 4)) + private void onExecute( + T baseSource, + List sources, + CommandExecutionContext context, + Frame frame, + ExecutionFlags flags, + CallbackInfo ci, + @Local(ordinal = 1) ExecutionFlags executionFlags + ){ + if (!executionFlags.isInsideReturnRun()) { + if(BreakPointCommand.isDebugging && BreakPointCommand.moveSteps > 0 && !isStepOut()) { + BreakPointCommand.moveSteps --; + } + } + } +} diff --git a/src/main/java/net/gunivers/sniffer/mixin/VariableLineMixin.java b/src/main/java/net/gunivers/sniffer/mixin/VariableLineMixin.java index f19b416..42605a7 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/VariableLineMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/VariableLineMixin.java @@ -3,7 +3,7 @@ import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.mojang.brigadier.CommandDispatcher; -import net.gunivers.sniffer.EncapsulationBreaker; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.command.SourcedCommandAction; import net.minecraft.server.command.AbstractServerCommandSource; import net.minecraft.server.function.MacroException; @@ -63,13 +63,13 @@ int getLine() { * @param id The function identifier * @param original The original operation being wrapped * @return The modified command action with source information attached - * @throws MacroException If there's an error instantiating the command */ @WrapMethod(method = "instantiate") SourcedCommandAction instantiate(List args, CommandDispatcher dispatcher, Identifier id, Operation> original) throws MacroException { var result = original.call(args, dispatcher, id); - EncapsulationBreaker.callFunction(result, "setSourceFunction", id.toString()); - EncapsulationBreaker.getAttribute(this, "line").ifPresent(line -> EncapsulationBreaker.callFunction(result, "setSourceLine", line)); + ReflectUtil.invoke(result, "setSourceFunction", id.toString()); + int line = ReflectUtil.getT(this, "line", int.class).getData(); + ReflectUtil.invoke(result, "setSourceLine", line); return result; } } diff --git a/src/main/java/net/gunivers/sniffer/mixin/ZipResourcePackMixin.java b/src/main/java/net/gunivers/sniffer/mixin/ZipResourcePackMixin.java index cd8fbe5..9ddf610 100644 --- a/src/main/java/net/gunivers/sniffer/mixin/ZipResourcePackMixin.java +++ b/src/main/java/net/gunivers/sniffer/mixin/ZipResourcePackMixin.java @@ -1,6 +1,9 @@ package net.gunivers.sniffer.mixin; import com.mojang.logging.LogUtils; +import net.gunivers.sniffer.dap.RealPath; +import net.gunivers.sniffer.dap.ScopeManager; +import net.gunivers.sniffer.util.ReflectUtil; import net.minecraft.resource.InputSupplier; import net.minecraft.resource.ResourcePack; import net.minecraft.resource.ResourceType; @@ -10,17 +13,12 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; -import net.gunivers.sniffer.dap.RealPath; -import net.gunivers.sniffer.dap.ScopeManager; import java.nio.file.Path; import java.util.Enumeration; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import static net.gunivers.sniffer.EncapsulationBreaker.callFunction; -import static net.gunivers.sniffer.EncapsulationBreaker.getAttribute; - /** * Mixin for the ZipResourcePack class to provide debugging capabilities. * This mixin allows the debugger to access and track resources within ZIP-based datapacks, @@ -50,7 +48,7 @@ public class ZipResourcePackMixin { */ @Overwrite public void findResources(ResourceType type, String namespace, String prefix, ResourcePack.ResultConsumer consumer) { - var zipFileOpt = getAttribute(this, "zipFile").flatMap(obj -> callFunction(obj, "open")); + var zipFileOpt = ReflectUtil.getT(this, "zipFile").flatMap(obj -> ReflectUtil.invoke(obj, "open")).onFailure(LOGGER::error).toOptional(); if (zipFileOpt.isPresent()) { var zipFile = (ZipFile) zipFileOpt.get(); Enumeration enumeration = zipFile.entries(); @@ -59,7 +57,7 @@ public void findResources(ResourceType type, String namespace, String prefix, Re String string2 = string + prefix + "/"; while(enumeration.hasMoreElements()) { - ZipEntry zipEntry = (ZipEntry)enumeration.nextElement(); + ZipEntry zipEntry = enumeration.nextElement(); if (!zipEntry.isDirectory()) { String string3 = zipEntry.getName(); if (string3.startsWith(string2)) { diff --git a/src/main/java/net/gunivers/sniffer/util/ExceptionCode.java b/src/main/java/net/gunivers/sniffer/util/ExceptionCode.java new file mode 100644 index 0000000..7612343 --- /dev/null +++ b/src/main/java/net/gunivers/sniffer/util/ExceptionCode.java @@ -0,0 +1,16 @@ +package net.gunivers.sniffer.util; + +public enum ExceptionCode { + REFLECT_INVOKE_EXCEPTION(0), + NULL_POINTER_EXCEPTION(1); + + private final int code; + + ExceptionCode(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/src/main/java/net/gunivers/sniffer/util/ReflectUtil.java b/src/main/java/net/gunivers/sniffer/util/ReflectUtil.java new file mode 100644 index 0000000..20c86e6 --- /dev/null +++ b/src/main/java/net/gunivers/sniffer/util/ReflectUtil.java @@ -0,0 +1,385 @@ +package net.gunivers.sniffer.util; + +import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.lang.invoke.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lightweight utility for fast reflective access using VarHandle, MethodHandle and LambdaMetafactory. + * + *

Usage examples: + *

+ * // VarHandle read/write
+ * VarHandle vh = ReflectUtil.findVarHandle(MyClass.class, "value", int.class);
+ * int v = (int) ReflectUtil.vhGet(vh, myObj);
+ * ReflectUtil.vhSet(vh, myObj, 123);
+ *
+ * // Method handle invoke
+ * MethodHandle mh = ReflectUtil.findMethodHandle(MyClass.class, "compute", int.class);
+ * Object result = ReflectUtil.invoke(mh, myObj, 5);
+ * 
+ * + */ +@SuppressWarnings("unused") +public final class ReflectUtil { + private ReflectUtil() {} + private static final Logger LOGGER = LogUtils.getLogger(); + private static final MethodHandles.Lookup PUBLIC_LOOKUP = MethodHandles.lookup(); + private static final Map VAR_HANDLE_CACHE = new ConcurrentHashMap<>(); + private static final Map MH_HANDLE_CACHE = new ConcurrentHashMap<>(); + private static final Map LAMBDA_CACHE = new ConcurrentHashMap<>(); + + private static String key(Class c, String name, Class... types) { + StringBuilder sb = new StringBuilder(c.getName()).append('#').append(name); + for (Class t : types) sb.append(':').append(t == null ? "null" : t.getName()); + return sb.toString(); + } + + //region variable + + /** + * Find and cache varhandle in a class. + * @param target the class to look up the varhandle + * @param fieldName the name of the field to look up + * @param fieldType the type of the field to look up + * @return the varhandle if found, or null if not found + */ + @Nullable + private static VarHandle findVarHandle(Class target, String fieldName, Class fieldType) { + String k = key(target, fieldName); + return VAR_HANDLE_CACHE.computeIfAbsent(k, kk -> { + try { + MethodHandles.Lookup lookup = + MethodHandles.privateLookupIn(target, PUBLIC_LOOKUP); + return lookup.findVarHandle(target, fieldName, fieldType); + } catch (ReflectiveOperationException e) { + LOGGER.error("Failed to find varhandle for field {} in class {}", fieldName, target, e); + return null; + } + }); + } + + /** + * Find and cache varhandle in a class without specifying the field type. + * @param target the class to look up the varhandle + * @param fieldName the name of the field to look up + * @return the varhandle if found, or null if not found + */ + @Nullable + private static VarHandle findVarHandle(Class target, String fieldName) { + String k = key(target, fieldName); + return VAR_HANDLE_CACHE.computeIfAbsent(k, kk -> { + try { + // 先通过反射获取字段类型 + Field field = target.getDeclaredField("fieldName"); + Class fieldType = field.getType(); + MethodHandles.Lookup lookup = + MethodHandles.privateLookupIn(target, PUBLIC_LOOKUP); + return lookup.findVarHandle(target, fieldName, fieldType); + } catch (ReflectiveOperationException e) { + LOGGER.error("Failed to find varhandle for field {} in class {}", fieldName, target, e); + return null; + } + }); + } + + /** + * Get a field in a class using a varhandle + * @param vh a varhandle, which should obtained from {@link ReflectUtil#findVarHandle(Class, String, Class)} + * @param receiver the object to get the field value from + * @return the value of the field + */ + private static Object vhGet(VarHandle vh, Object receiver) { + return vh.get(receiver); + } + + /** + * Check if a field exists in a class + * @param object the object to check the field existence from + * @param fieldName the name of the field to check + * @param fieldType the type of the field to check + * @return whether the field exists or not + */ + public static boolean exist(@NotNull Object object, String fieldName, Class fieldType) { + return findVarHandle(object.getClass(), fieldName, fieldType) != null; + } + + + /** + * Check if a field exists in a class + * @param object the object to check the field existence from + * @param fieldName the name of the field to check + * @return whether the field exists or not + */ + public static boolean exist(@NotNull Object object, String fieldName) { + return findVarHandle(object.getClass(), fieldName) != null; + } + + /** + * Get a field in a class + * @param object the object to get the field value from + * @param fieldName the name of the field to get + * @param fieldType the type of the field to get + * @return the value of the field + */ + public static Result get(Object object, String fieldName, Class fieldType){ + var vh = findVarHandle(object.getClass(), fieldName, fieldType); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + return Result.success(vhGet(vh, object)); + } + } + + /** + * Get a field in a class + * @param object the object to get the field value from + * @param fieldName the name of the field to get + * @return the value of the field + */ + public static Result get(Object object, String fieldName){ + var vh = findVarHandle(object.getClass(), fieldName); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + return Result.success(vhGet(vh, object)); + } + } + + @SuppressWarnings("unchecked") + public static Result getT(Object object, String fieldName, Class fieldType) { + var vh = findVarHandle(object.getClass(), fieldName, fieldType); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + return Result.success((T) vhGet(vh, object)); + } + } + + @SuppressWarnings("unchecked") + public static Result getT(Object object, String fieldName) { + var vh = findVarHandle(object.getClass(), fieldName); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + return Result.success((T) vhGet(vh, object)); + } + } + + /** + * Set a field in a class using a varhandle + * @param vh a varhandle, which should obtained from {@link ReflectUtil#findVarHandle(Class, String, Class)} + * @param receiver the object to set the field value to + * @param value the value to set + */ + private static void vhSet(VarHandle vh, Object receiver, Object value) { + vh.set(receiver, value); + } + + /** + * Set a field in a class + * @param object the object to set the field value to + * @param fieldName the name of the field to set + * @param fieldType the type of the field to set + * @param value the value to set + * @return success if the field was set, failed if not + */ + public static Result set(Object object, String fieldName, Class fieldType, Object value) { + var vh = findVarHandle(object.getClass(), fieldName, fieldType); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + vhSet(vh, object, value); + return Result.success(); + } + } + + public static Result set(Object object, String fieldName, Object value) { + var vh = findVarHandle(object.getClass(), fieldName); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + vhSet(vh, object, value); + return Result.success(); + } + } + + public static Result setT(Object object, String fieldName, Class fieldType, T value) { + var vh = findVarHandle(object.getClass(), fieldName, fieldType); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + vhSet(vh, object, value); + return Result.success(); + } + } + + public static Result setT(Object object, String fieldName, T value) { + var vh = findVarHandle(object.getClass(), fieldName, value.getClass()); + if(vh == null){ + return Result.failure("Field not found" + fieldName + " in " + object.getClass()); + }else{ + vhSet(vh, object, value); + return Result.success(); + } + } + + //endregion + + //region method + + /** + * Find and cache methodhandle in a class. + * @param target the class to look up the methodhandle + * @param name the name of the method to look up + * @param paramTypes the types of the parameters of the method to look up + * @return the methodhandle if found, or null if not found + */ + @Nullable + private static MethodHandle findMethodHandle(Class target, String name, Class... paramTypes) { + String k = key(target, name, paramTypes); + return MH_HANDLE_CACHE.computeIfAbsent(k, kk -> { + try { + Method m = findMethodReflective(target, name, paramTypes); + if (m == null) { + LOGGER.error("Method not found: {}({})", name, Arrays.toString(paramTypes)); + return null; + } + m.setAccessible(true); + return PUBLIC_LOOKUP.unreflect(m); + } catch (ReflectiveOperationException e) { + LOGGER.error("Error while looking up method: {}({})", name, Arrays.toString(paramTypes), e); + return null; + } + }); + } + + @Nullable + private static Method findMethodReflective(Class target, String name, Class... paramTypes) { + Class c = target; + while (c != null) { + try { + return c.getDeclaredMethod(name, paramTypes); + } catch (NoSuchMethodException ignored) { + c = c.getSuperclass(); + } + } + return null; + } + + /** + * A convenient method to invoke a methodhandle. + * @param mh the methodhandle to invoke + * @param args the arguments to pass to the methodhandle + * @return the result of the methodhandle invocation + */ + private static Result invoke(MethodHandle mh, Object... args) { + try { + return Result.success(mh.invoke(args)); + } catch (Throwable t) { + LOGGER.error("Error while invoking method: {}({})", mh, Arrays.toString(args), t); + return Result.failure(t.getMessage()); + } + } + + private static Result invoke(MethodHandle mh, List args) { + try { + return Result.success(mh.invokeWithArguments(args)); + } catch (Throwable t) { + LOGGER.error("Error while invoking method: {}({})", mh, Arrays.toString(args.toArray()), t); + return Result.failure(t.getMessage()); + } + } + + public static Result invokeWithParamType(Object caller, String name, List> paramTypes, Object... args){ + var handle = findMethodHandle(caller.getClass(), name, paramTypes.toArray(new Class[0])); + if(handle != null){ + try { + var qwq = new ArrayList<>(args.length + 1); + qwq.add(caller); + qwq.addAll(Arrays.asList(args)); + return Result.success(handle.invokeWithArguments(qwq)); + } catch (Throwable t) { + LOGGER.error("Error while invoking method: {}({})", name, Arrays.toString(args), t); + return Result.failure(t.getMessage()); + } + }else{ + return Result.failure("Method not found: " + name); + } + } + + public static Result invoke(Object caller, String name, Object... args){ + var handle = findMethodHandle(caller.getClass(), name, Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)); + if(handle != null){ + try { + var qwq = new ArrayList<>(args.length + 1); + qwq.add(caller); + qwq.addAll(Arrays.asList(args)); + return Result.success(handle.invokeWithArguments(qwq)); + } catch (Throwable t) { + LOGGER.error("Error while invoking method: {}({})", name, Arrays.toString(args), t); + return Result.failure(t.getMessage()); + } + }else{ + return Result.failure("Method not found: " + name); + } + } + + public static Result invoke(Object caller, String name){ + var handle = findMethodHandle(caller.getClass(), name); + if(handle != null){ + try { + return Result.success(handle.invoke(caller)); + } catch (Throwable t) { + LOGGER.error("Error while invoking method: {}()", name, t); + return Result.failure(t.getMessage()); + } + }else{ + return Result.failure("Method not found: " + name); + } + } + + + @SuppressWarnings("unchecked") + public static Result methodAsFunctional(Class funcInterface, Class target, String methodName, Class... paramTypes) { + String k = "MH:LAMBDA:" + funcInterface.getName() + ":" + key(target, methodName, paramTypes); + return Result.success((T) LAMBDA_CACHE.computeIfAbsent(k, kk -> { + try { + Method m = findMethodReflective(target, methodName, paramTypes); + if (m == null) throw new NoSuchMethodException(methodName); + m.setAccessible(true); + MethodHandle mh = PUBLIC_LOOKUP.unreflect(m); + MethodType invokedType = MethodType.methodType(funcInterface); + MethodType sam = MethodType.methodType(mh.type().returnType(), mh.type().parameterArray()); + CallSite cs = LambdaMetafactory.metafactory( + PUBLIC_LOOKUP, + getSingleAbstractMethodName(funcInterface), + invokedType, + sam.erase(), // erased form + mh, + mh.type() + ); + return cs.getTarget().invoke(); + } catch (Throwable t) { + return Result.failure(t.getMessage()); + } + })); + } + + private static String getSingleAbstractMethodName(Class iface) { + for (Method m : iface.getMethods()) { + if (Modifier.isAbstract(m.getModifiers())) return m.getName(); + } + throw new IllegalArgumentException("Not a functional interface: " + iface); + } + + //endregion +} diff --git a/src/main/java/net/gunivers/sniffer/util/Result.java b/src/main/java/net/gunivers/sniffer/util/Result.java new file mode 100644 index 0000000..0b92500 --- /dev/null +++ b/src/main/java/net/gunivers/sniffer/util/Result.java @@ -0,0 +1,107 @@ +package net.gunivers.sniffer.util; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public class Result { + private final boolean success; + private final T data; + private final String error; + private final ExceptionCode code; + + // 私有构造器 + private Result(boolean success, T data, String error, ExceptionCode errorCode) { + this.success = success; + this.data = data; + this.error = error; + this.code = errorCode; + } + + public static Result success(T data) { + return new Result<>(true, data, null, null); + } + + public static Result success() { + return new Result<>(true, null, null, null); + } + + public static Result failure(String error) { + return new Result<>(false, null, error, null); + } + + public static Result failure(String error, ExceptionCode errorCode) { + return new Result<>(false, null, error, errorCode); + } + + public boolean isSuccess() { + return success; + } + + public boolean isFailure() { + return !success; + } + + // 数据获取 + public T getData() { + if (!success) { + throw new IllegalStateException("Cannot get data from failed result: " + error); + } + return data; + } + + public T getDataOrElse(T defaultValue) { + return success ? data : defaultValue; + } + + // 错误信息 + public String getError() { + return error; + } + + public ExceptionCode getErrorCode() { + return code; + } + + // 函数式支持 + public Result map(Function mapper) { + if (isFailure()) { + return Result.failure(error, code); + } + try { + return Result.success(mapper.apply(data)); + } catch (Exception e) { + return Result.failure("Mapping failed: " + e.getMessage()); + } + } + + public Result flatMap(Function> mapper) { + if (isFailure()) { + return Result.failure(error, code); + } + try { + return mapper.apply(data); + } catch (Exception e) { + return Result.failure("Flat mapping failed: " + e.getMessage()); + } + } + + public Result onSuccess(Consumer action) { + if (isSuccess()) { + action.accept(data); + } + return this; + } + + public Result onFailure(Consumer action) { + if (isFailure()) { + action.accept(error); + } + return this; + } + + public Optional toOptional() { + return success ? Optional.of(data) : Optional.empty(); + } +} diff --git a/src/main/java/net/gunivers/sniffer/util/Utils.java b/src/main/java/net/gunivers/sniffer/util/Utils.java new file mode 100644 index 0000000..19ac800 --- /dev/null +++ b/src/main/java/net/gunivers/sniffer/util/Utils.java @@ -0,0 +1,42 @@ +package net.gunivers.sniffer.util; + +import com.mojang.logging.LogUtils; +import net.minecraft.server.function.ExpandedMacro; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import org.slf4j.Logger; + +/** + * Utility class providing helper methods for the Datapack Debugger. + * Contains various utility functions to work with Minecraft's internal classes + * and handle encapsulation breaking where necessary. + */ +public class Utils { + + final static Logger LOGGER = LogUtils.getLogger(); + + /** + * Retrieves the identifier from an ExpandedMacro function. + * This method uses EncapsulationBreaker to access the private field + * that contains the function's identifier. + * + * @param function The ExpandedMacro function to get the ID from + * @return The Identifier of the function, or a fallback identifier if not found + */ + public static Identifier getId(ExpandedMacro function) { + return ReflectUtil.getT(function, "functionIdentifier", Identifier.class).onFailure(LOGGER::error).getDataOrElse(Identifier.of("foo:bar")); + } + + private static final String MESSAGE_PREFIX = "[Sniffer] "; + + public static Text addSnifferPrefix(Text text) { + var header = Text.literal(MESSAGE_PREFIX).formatted(Formatting.AQUA); + return header.append(text); + } + + public static Text addSnifferPrefix(String text) { + return addSnifferPrefix(Text.literal(text).formatted(Formatting.WHITE)); + } + +} diff --git a/src/main/resources/assets/sniffer/lang/zh_cn.json b/src/main/resources/assets/sniffer/lang/zh_cn.json new file mode 100644 index 0000000..d201819 --- /dev/null +++ b/src/main/resources/assets/sniffer/lang/zh_cn.json @@ -0,0 +1,24 @@ +{ + "sniffer.commands.breakpoint.set": "断点已触发", + "sniffer.commands.breakpoint.get": "参数 %s 的值为: %s", + "sniffer.commands.breakpoint.get.fail": "无法获取参数 %s 的值", + "sniffer.commands.breakpoint.get.fail.error": "获取参数的值: %s 时发生了意外错误", + "sniffer.commands.breakpoint.get.fail.not_macro": "此函数不是宏", + "sniffer.commands.breakpoint.move": "游戏已继续", + "sniffer.commands.breakpoint.move.not_debugging": "`move` 命令只能在断点时使用", + "sniffer.commands.breakpoint.step.fail": "`step` 命令只能在断点时使用", + "sniffer.commands.breakpoint.step.over": "当前tick已完成, 退出断点模式", + "sniffer.commands.breakpoint.run": "执行: %s", + "sniffer.commands.breakpoint.on": "断点已激活", + "sniffer.commands.breakpoint.off": "断点已停用", + "sniffer.step": "步入", + "sniffer.name": "Sniffer", + "sniffer.config.title": "Sniffer 配置", + "sniffer.config.category.main": "主要设置", + "sniffer.config.port": "WebSocket 端口", + "sniffer.config.port.tooltip": "调试器 WebSocket 服务器将运行的端口 (1024-65535)", + "sniffer.config.path": "WebSocket 路径", + "sniffer.config.path.tooltip": "WebSocket 连接的端点路径", + "sniffer.config.server_address": "服务器地址: %s", + "sniffer.config.server_address.tooltip": "连接调试器客户端的完整地址" +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 01ace07..1a4ee65 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -37,7 +37,7 @@ ], "depends": { "fabricloader": ">=0.15.11", - "minecraft": "~1.21", + "minecraft": "1.21.10", "java": ">=21", "fabric-api": "*", "cloth-config": ">=17.0.144" diff --git a/src/main/resources/sniffer.mixins.json b/src/main/resources/sniffer.mixins.json index addcd1c..c199506 100644 --- a/src/main/resources/sniffer.mixins.json +++ b/src/main/resources/sniffer.mixins.json @@ -5,13 +5,16 @@ "mixins": [ "CommandExecutionContextMixin", "CommandFunctionActionMixin", + "CommandFunctionManagerMixin", "CommandFunctionMixin", "DirectoryResourcePackMixin", "ExpandedMacroMixin", + "FallthroughCommandActionMixin", "FixCommandActionMixin", "FrameMixin", "FunctionBuilderMixin", "MacroMixin", + "SingleCommandActionMixin", "SingleCommandActionSourcedMixin", "VariableLineMixin", "ZipResourcePackMixin" diff --git a/vscode/package-lock.json b/vscode/package-lock.json index 46d4957..901b55c 100644 --- a/vscode/package-lock.json +++ b/vscode/package-lock.json @@ -1,13 +1,13 @@ { - "name": "mock-debug", - "version": "0.52.0", + "name": "sniffer", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "mock-debug", - "version": "0.52.0", - "license": "MIT", + "name": "sniffer", + "version": "0.1.0", + "license": "MPL-2.0", "dependencies": { "websocket-stream": "^5.5.2" },