diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 1aff60a8..edc442be 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -84,5 +84,6 @@ public static void registerCommands(CommandDispatcher PosCommand.register(dispatcher); CrackRNGCommand.register(dispatcher); WeatherCommand.register(dispatcher); + WaypointCommand.register(dispatcher); } } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/WaypointCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/WaypointCommand.java new file mode 100644 index 00000000..559f8534 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/WaypointCommand.java @@ -0,0 +1,230 @@ +package net.earthcomputer.clientcommands.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Dynamic; +import net.earthcomputer.clientcommands.ClientCommands; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.command.CommandSource; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtHelper; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtOps; +import net.minecraft.registry.RegistryKey; +import net.minecraft.text.Text; +import net.minecraft.util.Pair; +import net.minecraft.util.Util; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.mojang.brigadier.arguments.BoolArgumentType.*; +import static com.mojang.brigadier.arguments.StringArgumentType.*; +import static dev.xpple.clientarguments.arguments.CBlockPosArgumentType.*; +import static dev.xpple.clientarguments.arguments.CDimensionArgumentType.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class WaypointCommand { + + public static final Map>>> waypoints = new HashMap<>(); + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final SimpleCommandExceptionType SAVE_FAILED_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.cwaypoint.saveFailed")); + private static final DynamicCommandExceptionType ALREADY_EXISTS_EXCEPTION = new DynamicCommandExceptionType(name -> Text.translatable("commands.cwaypoint.alreadyExists", name)); + private static final DynamicCommandExceptionType NOT_FOUND_EXCEPTION = new DynamicCommandExceptionType(name -> Text.translatable("commands.cwaypoint.notFound", name)); + + static { + try { + loadFile(); + } catch (IOException e) { + LOGGER.error("Could not load waypoints file, hence /cwaypoint will not work!", e); + } + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(literal("cwaypoint") + .then(literal("add") + .then(argument("name", word()) + .then(argument("pos", blockPos()) + .executes(ctx -> add(ctx.getSource(), getString(ctx, "name"), getCBlockPos(ctx, "pos"))) + .then(argument("dimension", dimension()) + .executes(ctx -> add(ctx.getSource(), getString(ctx, "name"), getCBlockPos(ctx, "pos"), getCDimensionArgument(ctx, "dimension").getRegistryKey())))))) + .then(literal("remove") + .then(argument("name", word()) + .suggests((ctx, builder) -> { + Map>> worldWaypoints = waypoints.get(getWorldIdentifier(ctx.getSource())); + return CommandSource.suggestMatching(worldWaypoints != null ? worldWaypoints.keySet() : Collections.emptySet(), builder); + }) + .executes(ctx -> remove(ctx.getSource(), getString(ctx, "name"))))) + .then(literal("edit") + .then(argument("name", word()) + .suggests((ctx, builder) -> { + Map>> worldWaypoints = waypoints.get(getWorldIdentifier(ctx.getSource())); + return CommandSource.suggestMatching(worldWaypoints != null ? worldWaypoints.keySet() : Collections.emptySet(), builder); + }) + .then(argument("pos", blockPos()) + .executes(ctx -> edit(ctx.getSource(), getString(ctx, "name"), getCBlockPos(ctx, "pos"))) + .then(argument("dimension", dimension()) + .executes(ctx -> edit(ctx.getSource(), getString(ctx, "name"), getCBlockPos(ctx, "pos"), getCDimensionArgument(ctx, "dimension").getRegistryKey())))))) + .then(literal("list") + .executes(ctx -> list(ctx.getSource())) + .then(argument("current", bool()) + .executes(ctx -> list(ctx.getSource(), getBool(ctx, "current")))))); + } + + private static String getWorldIdentifier(FabricClientCommandSource source) { + String worldIdentifier; + if (source.getClient().isIntegratedServerRunning()) { + worldIdentifier = source.getClient().getServer().getSaveProperties().getLevelName(); + } else { + worldIdentifier = source.getClient().getNetworkHandler().getConnection().getAddress().toString(); + } + return worldIdentifier; + } + + private static int add(FabricClientCommandSource source, String name, BlockPos pos) throws CommandSyntaxException { + return add(source, name, pos, source.getWorld().getRegistryKey()); + } + + private static int add(FabricClientCommandSource source, String name, BlockPos pos, RegistryKey dimension) throws CommandSyntaxException { + String worldIdentifier = getWorldIdentifier(source); + + Map>> worldWaypoints = waypoints.computeIfAbsent(worldIdentifier, key -> new HashMap<>()); + + if (worldWaypoints.putIfAbsent(name, new Pair<>(pos, dimension)) != null) { + throw ALREADY_EXISTS_EXCEPTION.create(name); + } + + saveFile(); + source.sendFeedback(Text.translatable("commands.cwaypoint.add.success", name, pos.toShortString(), dimension.getValue())); + return Command.SINGLE_SUCCESS; + } + + private static int remove(FabricClientCommandSource source, String name) throws CommandSyntaxException { + String worldIdentifier = getWorldIdentifier(source); + + Map>> worldWaypoints = waypoints.get(worldIdentifier); + + if (worldWaypoints == null) { + throw NOT_FOUND_EXCEPTION.create(name); + } + + if (worldWaypoints.remove(name) == null) { + throw NOT_FOUND_EXCEPTION.create(name); + } + + saveFile(); + source.sendFeedback(Text.translatable("commands.cwaypoint.remove.success", name)); + return Command.SINGLE_SUCCESS; + } + + private static int edit(FabricClientCommandSource source, String name, BlockPos pos) throws CommandSyntaxException { + return edit(source, name, pos, source.getWorld().getRegistryKey()); + } + + private static int edit(FabricClientCommandSource source, String name, BlockPos pos, RegistryKey dimension) throws CommandSyntaxException { + String worldIdentifier = getWorldIdentifier(source); + + Map>> worldWaypoints = waypoints.get(worldIdentifier); + + if (worldWaypoints == null) { + throw NOT_FOUND_EXCEPTION.create(name); + } + + if (worldWaypoints.computeIfPresent(name, (key, value) -> new Pair<>(pos, dimension)) == null) { + throw NOT_FOUND_EXCEPTION.create(name); + } + + saveFile(); + source.sendFeedback(Text.translatable("commands.cwaypoint.edit.success", name, pos.toShortString(), dimension.getValue())); + return Command.SINGLE_SUCCESS; + } + + private static int list(FabricClientCommandSource source) { + return list(source, false); + } + + private static int list(FabricClientCommandSource source, boolean current) { + if (current) { + String worldIdentifier = getWorldIdentifier(source); + + Map>> worldWaypoints = waypoints.get(worldIdentifier); + + if (worldWaypoints.isEmpty()) { + source.sendFeedback(Text.translatable("commands.cwaypoint.list.empty")); + return Command.SINGLE_SUCCESS; + } + + worldWaypoints.forEach((name, waypoint) -> source.sendFeedback(Text.translatable("commands.cwaypoint.list", name, waypoint.getLeft().toShortString(), waypoint.getRight().getValue()))); + return Command.SINGLE_SUCCESS; + } + + if (waypoints.isEmpty()) { + source.sendFeedback(Text.translatable("commands.cwaypoint.list.empty")); + return Command.SINGLE_SUCCESS; + } + + waypoints.forEach((worldIdentifier, worldWaypoints) -> { + if (worldWaypoints.isEmpty()) { + return; + } + + source.sendFeedback(Text.literal(worldIdentifier).append(":")); + worldWaypoints.forEach((name, waypoint) -> source.sendFeedback(Text.translatable("commands.cwaypoint.list", name, waypoint.getLeft().toShortString(), waypoint.getRight().getValue()))); + }); + return Command.SINGLE_SUCCESS; + } + + private static void saveFile() throws CommandSyntaxException { + try { + NbtCompound rootTag = new NbtCompound(); + waypoints.forEach((worldIdentifier, worldWaypoints) -> rootTag.put(worldIdentifier, worldWaypoints.entrySet().stream() + .collect(NbtCompound::new, (result, entry) -> { + NbtCompound waypoint = new NbtCompound(); + NbtCompound pos = NbtHelper.fromBlockPos(entry.getValue().getLeft()); + String dimension = entry.getValue().getRight().getValue().toString(); + waypoint.put("Pos", pos); + waypoint.putString("Dimension", dimension); + result.put(entry.getKey(), waypoint); + }, NbtCompound::copyFrom))); + File newFile = File.createTempFile("waypoints", ".dat", ClientCommands.configDir.toFile()); + NbtIo.write(rootTag, newFile); + File backupFile = new File(ClientCommands.configDir.toFile(), "waypoints.dat_old"); + File currentFile = new File(ClientCommands.configDir.toFile(), "waypoints.dat"); + Util.backupAndReplace(currentFile, newFile, backupFile); + } catch (IOException e) { + throw SAVE_FAILED_EXCEPTION.create(); + } + } + + private static void loadFile() throws IOException { + waypoints.clear(); + NbtCompound rootTag = NbtIo.read(new File(ClientCommands.configDir.toFile(), "waypoints.dat")); + if (rootTag == null) { + return; + } + rootTag.getKeys().forEach(worldIdentifier -> { + NbtCompound worldWaypoints = rootTag.getCompound(worldIdentifier); + waypoints.put(worldIdentifier, worldWaypoints.getKeys().stream() + .collect(Collectors.toMap(Function.identity(), name -> { + NbtCompound waypoint = worldWaypoints.getCompound(name); + BlockPos pos = NbtHelper.toBlockPos(waypoint.getCompound("Pos")); + RegistryKey dimension = World.CODEC.parse(new Dynamic<>(NbtOps.INSTANCE, waypoint.get("Dimension"))).resultOrPartial(LOGGER::error).orElseThrow(); + return new Pair<>(pos, dimension); + }))); + }); + } +} diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index 8ccf4dad..3372a3c5 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -207,6 +207,15 @@ "commands.cvar.list.empty": "No available variables", "commands.cvar.list": "Available variables: %s", + "commands.cwaypoint.add.success": "Waypoint \"%s\" at %s in %s successfully added", + "commands.cwaypoint.remove.success": "Waypoint \"%s\" successfully removed", + "commands.cwaypoint.edit.success": "Waypoint \"%s\" has successfully been changed to %s in %s", + "commands.cwaypoint.list": " - \"%s\" at %s in %s", + "commands.cwaypoint.list.empty": "No available waypoints", + "commands.cwaypoint.alreadyExists": "A waypoint with the name \"%s\" already exists", + "commands.cwaypoint.notFound": "No waypoint with the name \"%s\" could be found", + "commands.cwaypoint.saveFailed": "Could not save waypoints file", + "commands.cwe.playerNotFound": "Player not found", "commands.cweather.reset": "Stopped overriding weather",