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 ccd292fa8..f9ad68044 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -4,10 +4,6 @@ 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 com.mojang.serialization.MapCodec; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller; @@ -24,11 +20,8 @@ import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer; import dan200.computercraft.client.turtle.TurtleModemModeller; import dan200.computercraft.client.turtle.TurtleUpgradeModellers; import dan200.computercraft.shared.ModRegistry; -import dan200.computercraft.shared.command.CommandComputerCraft; -import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.turtle.TurtleOverlay; -import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.color.item.ItemTintSource; import net.minecraft.client.gui.screens.MenuScreens; @@ -39,13 +32,11 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; import net.minecraft.client.renderer.item.ItemModel; import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty; import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty; -import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.MenuType; -import java.io.File; import java.util.Collection; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -133,45 +124,4 @@ public final class ClientRegistry { public static void registerConditionalItemProperties(BiConsumer> register) { register.accept(TurtleShowElfOverlay.ID, TurtleShowElfOverlay.CODEC); } - - /** - * 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/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index d86306720..b18944c18 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 @@ -36,7 +36,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; -import java.io.File; +import java.nio.file.Files; import java.util.*; import static dan200.computercraft.shared.command.CommandUtils.isPlayer; @@ -52,12 +52,6 @@ import static net.minecraft.commands.Commands.literal; public final class CommandComputerCraft { public static final UUID SYSTEM_UUID = new UUID(0, 0); - /** - * 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() { } @@ -351,8 +345,8 @@ public final class CommandComputerCraft { } } - if (isPlayer(source) && UserLevel.isOwner(source)) { - var linkPath = linkStorage(source, computerId); + if (computer != null && isPlayer(source) && UserLevel.isOwner(source)) { + var linkPath = linkStorage(source, computer); if (linkPath != null) out.append(" ").append(linkPath); } @@ -371,13 +365,13 @@ public final class CommandComputerCraft { } } - private static @Nullable Component linkStorage(CommandSourceStack source, int id) { - var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id); - if (!file.isDirectory()) return null; + private static @Nullable Component linkStorage(CommandSourceStack source, ServerComputer computer) { + var file = ServerContext.get(source.getServer()).storageDir().resolve(computer.getFamily().getSaveFolder()).resolve(Integer.toString(computer.getID())); + if (!Files.isDirectory(file)) return null; - return clientLink( + return link( text("\u270E"), - "/" + CLIENT_OPEN_FOLDER + " " + id, + PlatformHelper.get().createOpenFolderAction(file), Component.translatable("commands.computercraft.dump.open_path") ); } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java b/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java index 44549eefa..07eebe762 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java @@ -52,10 +52,6 @@ public final class ChatHelpers { return link(component, new ClickEvent(ClickEvent.Action.RUN_COMMAND, command), toolTip); } - public static Component clientLink(MutableComponent component, String command, Component toolTip) { - return link(component, new ClickEvent(ClickEvent.Action.RUN_COMMAND, command), toolTip); - } - public static Component link(Component component, ClickEvent click, Component toolTip) { var style = component.getStyle(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ComputerFamily.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ComputerFamily.java index 099c74e72..4dd64ba9d 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ComputerFamily.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ComputerFamily.java @@ -29,6 +29,21 @@ public enum ComputerFamily { }; } + /** + * Get the save folder for this computer type. + *

+ * Command computers are saved under a different namespace, to prevent people who have obtained a normal computer + * with the same ID as a command computer (e.g. via creative mode, or a bug), having access to its files. + * + * @return The save folder for a + */ + public String getSaveFolder() { + return switch (this) { + case NORMAL, ADVANCED -> "computer"; + case COMMAND -> "command_computer"; + }; + } + private static boolean checkCommandUsable(Player player) { var server = player.getServer(); if (server == null || !server.isCommandBlockEnabled()) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index 54fea5803..ae3f84588 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -268,7 +268,7 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei @Override public final WritableMount createRootMount() { var capacity = storageCapacity <= 0 ? ConfigSpec.computerSpaceLimit.get() : storageCapacity; - return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), capacity); + return ComputerCraftAPI.createSaveDirMount(level.getServer(), family.getSaveFolder() + "/" + computer.getID(), capacity); } public static Properties properties(int computerID, ComputerFamily family) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java index f0bf6ec68..f3dfaf952 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java @@ -18,6 +18,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Registry; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; @@ -48,6 +49,7 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; +import java.nio.file.Path; import java.util.List; import java.util.function.Consumer; @@ -353,4 +355,12 @@ public interface PlatformHelper { */ @Nullable IMedia getMedia(ItemStack stack); + + /** + * Construct a {@link ClickEvent} that opens a folder. + * + * @param path The folder to open. + * @return The click event. + */ + ClickEvent createOpenFolderAction(Path path); } diff --git a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java index 8129434be..58bd1c4a7 100644 --- a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java +++ b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java @@ -20,6 +20,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Registry; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; @@ -44,6 +45,7 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; +import java.nio.file.Path; import java.util.List; import java.util.function.Consumer; @@ -154,6 +156,11 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat return null; } + @Override + public ClickEvent createOpenFolderAction(Path path) { + throw new UnsupportedOperationException("Cannot create open folder ClickEvent"); + } + @Override public ContainerTransfer.Slotted wrapContainer(Container container) { throw new UnsupportedOperationException("Cannot wrap container"); 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 bc6f5712a..9cb4261b2 100644 --- a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java +++ b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java @@ -4,11 +4,15 @@ package dan200.computercraft.client; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.client.FabricComputerCraftAPIClient; import dan200.computercraft.client.model.ExtraModels; import dan200.computercraft.core.util.Nullability; import dan200.computercraft.impl.Services; +import dan200.computercraft.shared.ComputerCraft; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.config.ConfigSpec; import dan200.computercraft.shared.network.NetworkMessages; @@ -22,6 +26,7 @@ import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlu import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.color.item.ItemTintSources; import net.minecraft.client.gui.screens.MenuScreens; @@ -31,6 +36,8 @@ import net.minecraft.client.renderer.item.properties.conditional.ConditionalItem import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperties; import net.minecraft.world.phys.BlockHitResult; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import static dan200.computercraft.core.util.Nullability.assertNonNull; @@ -75,9 +82,17 @@ public class ComputerCraftClient { } }); - ClientCommandRegistrationCallback.EVENT.register( - (dispatcher, registryAccess) -> ClientRegistry.registerClientCommands(dispatcher, FabricClientCommandSource::sendError) - ); + // Register our open folder command + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(LiteralArgumentBuilder.literal(ComputerCraft.CLIENT_OPEN_FOLDER) + .requires(x -> Minecraft.getInstance().getSingleplayerServer() != null) + .then(RequiredArgumentBuilder.argument("path", StringArgumentType.string()) + .executes(c -> { + var file = Path.of(c.getArgument("path", String.class)); + if (Files.isDirectory(file)) Util.getPlatform().openFile(file.toFile()); + return 0; + }) + ))); ((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml")); } diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java index 5746221b5..c5e663d97 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java @@ -64,6 +64,12 @@ import java.util.function.BiFunction; public class ComputerCraft { private static final LevelResource SERVERCONFIG = new LevelResource("serverconfig"); + /** + * The client-side command to open a 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-open-folder"; + public static void init() { for (var type : NetworkMessages.getServerbound()) registerPayloadType(PayloadTypeRegistry.playC2S(), type); for (var type : NetworkMessages.getClientbound()) registerPayloadType(PayloadTypeRegistry.playS2C(), type); diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 3e96617e5..dc2685cca 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -7,6 +7,7 @@ package dan200.computercraft.shared.platform; import com.google.auto.service.AutoService; import com.mojang.authlib.GameProfile; import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.media.MediaLookup; @@ -16,6 +17,7 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.PeripheralLookup; import dan200.computercraft.impl.Peripherals; import dan200.computercraft.mixin.ArgumentTypeInfosAccessor; +import dan200.computercraft.shared.ComputerCraft; import dan200.computercraft.shared.config.ConfigFile; import dan200.computercraft.shared.network.container.ContainerData; import dan200.computercraft.shared.util.InventoryUtil; @@ -37,6 +39,7 @@ import net.minecraft.core.Direction; import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; @@ -64,6 +67,7 @@ import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -235,6 +239,14 @@ public class PlatformHelperImpl implements PlatformHelper { return MediaLookup.get().find(stack, null); } + @Override + public ClickEvent createOpenFolderAction(Path path) { + return new ClickEvent( + ClickEvent.Action.RUN_COMMAND, + "/" + ComputerCraft.CLIENT_OPEN_FOLDER + " " + StringArgumentType.escapeIfRequired(path.toAbsolutePath().toString()) + ); + } + private static final class RegistrationHelperImpl implements RegistrationHelper { private final Registry registry; private final List> entries = new ArrayList<>(); 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 6417c30da..fbba4bab2 100644 --- a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientHooks.java +++ b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientHooks.java @@ -6,7 +6,6 @@ package dan200.computercraft.client; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.client.sound.SpeakerSound; -import net.minecraft.commands.CommandSourceStack; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; @@ -67,7 +66,6 @@ public final class ForgeClientHooks { } - @SubscribeEvent public static void onRenderInFrame(RenderItemInFrameEvent event) { var state = event.getItemFrameRenderState().getRenderData(ITEM_FRAME_STATE); @@ -83,9 +81,4 @@ 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/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java index 7b10a91c4..523d879ff 100644 --- a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java +++ b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java @@ -11,10 +11,9 @@ import dan200.computercraft.client.model.ExtraModels; import dan200.computercraft.client.render.ExtendedItemFrameRenderState; import dan200.computercraft.client.turtle.TurtleUpgradeModellers; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.entity.state.ItemFrameRenderState; +import net.minecraft.client.renderer.entity.ItemFrameRenderer; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.context.ContextKey; -import net.minecraft.world.entity.decoration.ItemFrame; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.ModLoader; @@ -98,7 +97,7 @@ public final class ForgeClientRegistry { @SubscribeEvent public static void registerRenderStateModifiers(RegisterRenderStateModifiersEvent event) { - event.registerEntityModifier(new TypeToken<>() { + event.registerEntityModifier(new TypeToken>() { }, (e, s) -> { var data = s.getRenderData(ITEM_FRAME_STATE); if (data == null) s.setRenderData(ITEM_FRAME_STATE, data = new ExtendedItemFrameRenderState()); diff --git a/projects/forge/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java b/projects/forge/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java deleted file mode 100644 index e98b88df5..000000000 --- a/projects/forge/src/client/java/dan200/computercraft/mixin/client/ClientPacketListenerMixin.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.mixin.client; - -import dan200.computercraft.shared.command.CommandComputerCraft; -import net.minecraft.client.multiplayer.ClientPacketListener; -import net.neoforged.neoforge.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 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 b1976873d..6c325d339 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,6 @@ "defaultRequire": 1 }, "client": [ - "BlockRenderDispatcherMixin", - "ClientPacketListenerMixin" + "BlockRenderDispatcherMixin" ] } diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 6d1426d8d..fe74da372 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -25,6 +25,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Registry; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; @@ -65,6 +66,7 @@ import net.neoforged.neoforge.registries.DeferredHolder; import net.neoforged.neoforge.registries.DeferredRegister; import org.jspecify.annotations.Nullable; +import java.nio.file.Path; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -244,6 +246,11 @@ public class PlatformHelperImpl implements PlatformHelper { return stack.getCapability(MediaCapability.get()); } + @Override + public ClickEvent createOpenFolderAction(Path path) { + return new ClickEvent(ClickEvent.Action.OPEN_FILE, path.toAbsolutePath().toString()); + } + private record RegistrationHelperImpl(DeferredRegister registry) implements RegistrationHelper { @Override public RegistryEntry register(String name, Supplier create) {