1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-05-23 01:34:11 +00:00

Store command computers in a separate folder

- Remove the /computercraft-computer-folder client command, and replace
   it with OPEN_FILE on NeoForge, or a /computercraft-open-folder
   command on Fabric (which now accepts a path, rather than an id).

 - Store command computer files in "computercraft/command_computer/<id>".

Fixes #1581.
This commit is contained in:
Jonathan Coates 2025-03-03 20:53:43 +00:00
parent 9277aa33e9
commit 05163a4911
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
15 changed files with 87 additions and 110 deletions

View File

@ -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<ResourceLocation, MapCodec<? extends ConditionalItemModelProperty>> 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 <T> The type of the client-side command context.
*/
public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
.then(RequiredArgumentBuilder.<T, Integer>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 <T> The type of the client-side command context.
* @return {@code 1} if a folder was opened, {@code 0} otherwise.
*/
private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> 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;
}
}

View File

@ -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")
);
}

View File

@ -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();

View File

@ -29,6 +29,21 @@ public enum ComputerFamily {
};
}
/**
* Get the save folder for this computer type.
* <p>
* 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()) {

View File

@ -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) {

View File

@ -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);
}

View File

@ -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");

View File

@ -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.<FabricClientCommandSource>literal(ComputerCraft.CLIENT_OPEN_FOLDER)
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
.then(RequiredArgumentBuilder.<FabricClientCommandSource, String>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"));
}

View File

@ -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);

View File

@ -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<T> implements RegistrationHelper<T> {
private final Registry<T> registry;
private final List<RegistryEntryImpl<? extends T>> entries = new ArrayList<>();

View File

@ -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);
}
}

View File

@ -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.<ItemFrame, ItemFrameRenderState>registerEntityModifier(new TypeToken<>() {
event.registerEntityModifier(new TypeToken<ItemFrameRenderer<?>>() {
}, (e, s) -> {
var data = s.getRenderData(ITEM_FRAME_STATE);
if (data == null) s.setRenderData(ITEM_FRAME_STATE, data = new ExtendedItemFrameRenderState());

View File

@ -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<Boolean> ci) {
if (command.startsWith(CommandComputerCraft.CLIENT_OPEN_FOLDER) && ClientCommandHandler.runCommand(command)) {
ci.setReturnValue(true);
}
}
}

View File

@ -7,7 +7,6 @@
"defaultRequire": 1
},
"client": [
"BlockRenderDispatcherMixin",
"ClientPacketListenerMixin"
"BlockRenderDispatcherMixin"
]
}

View File

@ -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<R>(DeferredRegister<R> registry) implements RegistrationHelper<R> {
@Override
public <T extends R> RegistryEntry<T> register(String name, Supplier<T> create) {