diff --git a/projects/common/build.gradle.kts b/projects/common/build.gradle.kts index 28f505a6e..2b4810975 100644 --- a/projects/common/build.gradle.kts +++ b/projects/common/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { compileOnly(libs.bundles.externalMods.common) clientCompileOnly(variantOf(libs.emi) { classifier("api") }) - compileOnly(libs.mixin) annotationProcessorEverywhere(libs.autoService) testFixturesAnnotationProcessor(libs.autoService) @@ -47,6 +46,7 @@ dependencies { testImplementation(libs.bundles.test) testRuntimeOnly(libs.bundles.testRuntime) + testModCompileOnly(libs.mixin) testModImplementation(testFixtures(project(":core"))) testModImplementation(testFixtures(project(":common"))) testModImplementation(libs.bundles.kotlin) diff --git a/projects/common/src/client/java/dan200/computercraft/client/ClientHooks.java b/projects/common/src/client/java/dan200/computercraft/client/ClientHooks.java index 637bdb7b3..36b331939 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientHooks.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientHooks.java @@ -17,8 +17,6 @@ import dan200.computercraft.client.render.monitor.MonitorRenderState; import dan200.computercraft.client.sound.SpeakerManager; import dan200.computercraft.shared.CommonHooks; import dan200.computercraft.shared.ModRegistry; -import dan200.computercraft.shared.command.CommandComputerCraft; -import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.media.items.PrintoutItem; import dan200.computercraft.shared.peripheral.modem.wired.CableBlock; import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant; @@ -28,7 +26,6 @@ import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity; import dan200.computercraft.shared.util.PauseAwareTimer; import dan200.computercraft.shared.util.WorldUtil; -import net.minecraft.Util; import net.minecraft.client.Camera; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.MultiBufferSource; @@ -43,7 +40,6 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; import javax.annotation.Nullable; -import java.io.File; import java.util.function.Consumer; /** @@ -71,10 +67,6 @@ public final class ClientHooks { ClientPocketComputers.reset(); } - public static boolean onChatMessage(String message) { - return handleOpenComputerCommand(message); - } - public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) { return CableHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit) || MonitorHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit); @@ -109,34 +101,6 @@ public final class ClientHooks { SpeakerManager.onPlayStreaming(engine, channel, stream); } - /** - * Handle the {@link CommandComputerCraft#OPEN_COMPUTER} "clientside command". This isn't a true command, as we - * don't want it to actually be visible to the user. - * - * @param message The current chat message. - * @return Whether to cancel sending this message. - */ - private static boolean handleOpenComputerCommand(String message) { - if (!message.startsWith(CommandComputerCraft.OPEN_COMPUTER)) return false; - - var server = Minecraft.getInstance().getSingleplayerServer(); - if (server == null) return false; - - var idStr = message.substring(CommandComputerCraft.OPEN_COMPUTER.length()).trim(); - int id; - try { - id = Integer.parseInt(idStr); - } catch (NumberFormatException ignore) { - return false; - } - - var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id); - if (!file.isDirectory()) return false; - - Util.getPlatform().openFile(file); - return true; - } - /** * Add additional information about the currently targeted block to the debug screen. * diff --git a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java index e4d0f9d9e..d319f2e90 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -4,9 +4,13 @@ package dan200.computercraft.client; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; import dan200.computercraft.api.ComputerCraftAPI; -import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller; import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller; +import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller; import dan200.computercraft.client.gui.*; import dan200.computercraft.client.pocket.ClientPocketComputers; import dan200.computercraft.client.render.RenderTypes; @@ -16,11 +20,14 @@ import dan200.computercraft.client.turtle.TurtleModemModeller; import dan200.computercraft.client.turtle.TurtleUpgradeModellers; import dan200.computercraft.core.util.Colour; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.command.CommandComputerCraft; import dan200.computercraft.shared.common.IColouredItem; +import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; import dan200.computercraft.shared.media.items.DiskItem; import dan200.computercraft.shared.media.items.TreasureDiskItem; +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.color.item.ItemColor; import net.minecraft.client.gui.screens.MenuScreens; @@ -30,6 +37,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; import net.minecraft.client.renderer.item.ClampedItemPropertyFunction; import net.minecraft.client.renderer.item.ItemProperties; +import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.ResourceProvider; @@ -39,6 +47,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ItemLike; import javax.annotation.Nullable; +import java.io.File; import java.io.IOException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -181,4 +190,45 @@ public final class ClientRegistry { return function.unclampedCall(stack, level, entity, layer); } } + + /** + * Register client-side commands. + * + * @param dispatcher The dispatcher to register the commands to. + * @param sendError A function to send an error message. + * @param The type of the client-side command context. + */ + public static void registerClientCommands(CommandDispatcher dispatcher, BiConsumer sendError) { + dispatcher.register(LiteralArgumentBuilder.literal(CommandComputerCraft.CLIENT_OPEN_FOLDER) + .requires(x -> Minecraft.getInstance().getSingleplayerServer() != null) + .then(RequiredArgumentBuilder.argument("computer_id", IntegerArgumentType.integer(0)) + .executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class))) + )); + } + + /** + * Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command. + * + * @param context The command context. + * @param sendError A function to send an error message. + * @param id The computer's id. + * @param The type of the client-side command context. + * @return {@code 1} if a folder was opened, {@code 0} otherwise. + */ + private static int handleOpenComputerCommand(T context, BiConsumer sendError, int id) { + var server = Minecraft.getInstance().getSingleplayerServer(); + if (server == null) { + sendError.accept(context, Component.literal("Not on a single-player server")); + return 0; + } + + var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id); + if (!file.isDirectory()) { + sendError.accept(context, Component.literal("Computer's folder does not exist")); + return 0; + } + + Util.getPlatform().openFile(file); + return 1; + } } diff --git a/projects/common/src/client/resources/computercraft-client.mixins.json b/projects/common/src/client/resources/computercraft-client.mixins.json deleted file mode 100644 index 0f9d50ff5..000000000 --- a/projects/common/src/client/resources/computercraft-client.mixins.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "required": true, - "package": "dan200.computercraft.mixin.client", - "minVersion": "0.8", - "compatibilityLevel": "JAVA_17", - "injectors": { - "defaultRequire": 1 - }, - "client": [ - "ClientPacketListenerMixin" - ], - "refmap": "client-computercraft.refmap.json" -} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index 834ce2191..f67d07ced 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -54,7 +54,12 @@ import static net.minecraft.commands.Commands.literal; public final class CommandComputerCraft { public static final UUID SYSTEM_UUID = new UUID(0, 0); - public static final String OPEN_COMPUTER = "computercraft open-computer "; + + /** + * The client-side command to open the folder. Ideally this would live under the main {@code computercraft} + * namespace, but unfortunately that overrides commands, rather than merging them. + */ + public static final String CLIENT_OPEN_FOLDER = "computercraft-computer-folder"; private CommandComputerCraft() { } @@ -389,7 +394,7 @@ public final class CommandComputerCraft { return link( text("\u270E"), - "/" + OPEN_COMPUTER + id, + "/" + CLIENT_OPEN_FOLDER + " " + id, Component.translatable("commands.computercraft.dump.open_path") ); } diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java index 530c596c8..ec16c4ecb 100644 --- a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java +++ b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java @@ -16,6 +16,8 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableBlock; import dan200.computercraft.shared.platform.FabricConfigFile; import dan200.computercraft.shared.platform.FabricMessageType; import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; @@ -81,6 +83,10 @@ public class ComputerCraftClient { return cable.getCloneItemStack(state, hit, level, pos, player); }); + ClientCommandRegistrationCallback.EVENT.register( + (dispatcher, registryAccess) -> ClientRegistry.registerClientCommands(dispatcher, FabricClientCommandSource::sendError) + ); + ((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml")); } } diff --git a/projects/fabric/src/main/resources/fabric.mod.json b/projects/fabric/src/main/resources/fabric.mod.json index 9a651c3e2..578385401 100644 --- a/projects/fabric/src/main/resources/fabric.mod.json +++ b/projects/fabric/src/main/resources/fabric.mod.json @@ -38,10 +38,6 @@ }, "mixins": [ "computercraft.fabric.mixins.json", - { - "config": "computercraft-client.mixins.json", - "environment": "client" - }, { "config": "computercraft-client.fabric.mixins.json", "environment": "client" diff --git a/projects/forge/build.gradle.kts b/projects/forge/build.gradle.kts index 35bc30dd8..bb0b71666 100644 --- a/projects/forge/build.gradle.kts +++ b/projects/forge/build.gradle.kts @@ -106,10 +106,8 @@ minecraft { } mixin { - add(sourceSets.main.get(), "computercraft.refmap.json") add(sourceSets.client.get(), "client-computercraft.refmap.json") - config("computercraft-client.mixins.json") config("computercraft-client.forge.mixins.json") } diff --git a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientHooks.java b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientHooks.java index 69fdb7502..ea97bf120 100644 --- a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientHooks.java +++ b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientHooks.java @@ -6,11 +6,9 @@ package dan200.computercraft.client; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.client.sound.SpeakerSound; +import net.minecraft.commands.CommandSourceStack; import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.event.CustomizeGuiOverlayEvent; -import net.minecraftforge.client.event.RenderHandEvent; -import net.minecraftforge.client.event.RenderHighlightEvent; -import net.minecraftforge.client.event.RenderItemInFrameEvent; +import net.minecraftforge.client.event.*; import net.minecraftforge.client.event.sound.PlayStreamingSourceEvent; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.level.LevelEvent; @@ -78,4 +76,9 @@ public final class ForgeClientHooks { if (!(event.getSound() instanceof SpeakerSound sound) || sound.getStream() == null) return; ClientHooks.onPlayStreaming(event.getEngine(), event.getChannel(), sound.getStream()); } + + @SubscribeEvent + public static void registerClientCommands(RegisterClientCommandsEvent event) { + ClientRegistry.registerClientCommands(event.getDispatcher(), CommandSourceStack::sendFailure); + } } diff --git a/projects/common/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java b/projects/forge/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java similarity index 57% rename from projects/common/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java rename to projects/forge/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java index 08380209d..dd5ecb11b 100644 --- a/projects/common/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java +++ b/projects/forge/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java @@ -4,17 +4,23 @@ package dan200.computercraft.mixin.client; -import dan200.computercraft.client.ClientHooks; +import dan200.computercraft.shared.command.CommandComputerCraft; import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraftforge.client.ClientCommandHandler; 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.CallbackInfoReturnable; +/** + * Allows triggering ComputerCraft's client commands from chat components events. + */ @Mixin(ClientPacketListener.class) class ClientPacketListenerMixin { @Inject(method = "sendUnsignedCommand", at = @At("HEAD"), cancellable = true) - void commandUnsigned(String message, CallbackInfoReturnable ci) { - if (ClientHooks.onChatMessage(message)) ci.setReturnValue(true); + void commandUnsigned(String command, CallbackInfoReturnable ci) { + if (command.startsWith(CommandComputerCraft.CLIENT_OPEN_FOLDER) && ClientCommandHandler.runCommand(command)) { + ci.setReturnValue(true); + } } } diff --git a/projects/forge/src/client/resources/computercraft-client.forge.mixins.json b/projects/forge/src/client/resources/computercraft-client.forge.mixins.json index fa8e7abed..c076594c8 100644 --- a/projects/forge/src/client/resources/computercraft-client.forge.mixins.json +++ b/projects/forge/src/client/resources/computercraft-client.forge.mixins.json @@ -7,7 +7,8 @@ "defaultRequire": 1 }, "client": [ - "BlockRenderDispatcherMixin" + "BlockRenderDispatcherMixin", + "ClientPacketListenerMixin" ], "refmap": "client-computercraft.refmap.json" }