diff --git a/gradle.properties b/gradle.properties index c247dd425..e7c339f0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx2G # also check this on https://fabricmc.net/develop/ fabric_version=0.110.5+1.21.4 clientarguments_version=1.10.1 - betterconfig_version=2.1.2 + betterconfig_version=2.3.0 seedfinding_core_version=1.200.1 seedfinding_biome_version=1.171.1 seedfinding_feature_version=1.171.9 diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index fba306937..db9a9b4e4 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -21,7 +21,6 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.commands.CommandBuildContext; import org.slf4j.Logger; @@ -37,25 +36,11 @@ public class ClientCommands implements ClientModInitializer { private static final Logger LOGGER = LogUtils.getLogger(); - public static Path configDir; + public static final Path CONFIG_DIR = FabricLoader.getInstance().getConfigDir().resolve("clientcommands"); private static final Set clientcommandsCommands = new HashSet<>(); private static final Set COMMANDS_TO_NOT_SEND_TO_SERVER = Set.of("cwe", "cnote"); // could contain private information - public static final boolean SCRAMBLE_WINDOW_TITLE = Util.make(() -> { - String playerUUID = String.valueOf(Minecraft.getInstance().getUser().getProfileId()); - - Set victims = Set.of( - "fa68270b-1071-46c6-ac5c-6c4a0b777a96", // Earthcomputer - "d4557649-e553-413e-a019-56d14548df96", // Azteched - "8dc3d945-cf90-47c1-a122-a576319d05a7", // samnrad - "c5d72740-cabc-42d1-b789-27859041d553", // allocator - "e4093360-a200-4f99-aa13-be420b8d9a79", // Rybot666 - "083fb87e-c9e4-4489-8fb7-a45b06bfca90", // Kerbaras - "973e8f6e-2f51-4307-97dc-56fdc71d194f" // KatieTheQt - ); - - return victims.contains(playerUUID) || Boolean.getBoolean("clientcommands.scrambleWindowTitle"); - }); + public static boolean scrambleWindowTitle = false; private static final Set CHAT_COMMAND_USERS = Set.of( "b793c3b9-425f-4dd8-a056-9dec4d835e24", // wsb @@ -65,10 +50,11 @@ public class ClientCommands implements ClientModInitializer { @Override public void onInitializeClient() { + setupScrambleWindowTitle(); + // Config - configDir = FabricLoader.getInstance().getConfigDir().resolve("clientcommands"); try { - Files.createDirectories(configDir); + Files.createDirectories(CONFIG_DIR); } catch (IOException e) { LOGGER.error("Failed to create config dir", e); } @@ -91,6 +77,24 @@ public void onInitializeClient() { FishingCracker.registerEvents(); PlayerRandCracker.registerEvents(); ServerBrandManager.registerEvents(); + WaypointCommand.registerEvents(); + } + + private static void setupScrambleWindowTitle() { + // can't set this up during class initializer, because Minecraft.getInstance() is null during automated tests + String playerUUID = String.valueOf(Minecraft.getInstance().getUser().getProfileId()); + + Set victims = Set.of( + "fa68270b-1071-46c6-ac5c-6c4a0b777a96", // Earthcomputer + "d4557649-e553-413e-a019-56d14548df96", // Azteched + "8dc3d945-cf90-47c1-a122-a576319d05a7", // samnrad + "c5d72740-cabc-42d1-b789-27859041d553", // allocator + "e4093360-a200-4f99-aa13-be420b8d9a79", // Rybot666 + "083fb87e-c9e4-4489-8fb7-a45b06bfca90", // Kerbaras + "973e8f6e-2f51-4307-97dc-56fdc71d194f" // KatieTheQt + ); + + scrambleWindowTitle = victims.contains(playerUUID) || Boolean.getBoolean("clientcommands.scrambleWindowTitle"); } private static Set getCommands(CommandDispatcher dispatcher) { @@ -173,6 +177,7 @@ public static void registerCommands(CommandDispatcher UsageTreeCommand.register(dispatcher); UuidCommand.register(dispatcher); VarCommand.register(dispatcher); + WaypointCommand.register(dispatcher); WeatherCommand.register(dispatcher); WhisperEncryptedCommand.register(dispatcher); WikiCommand.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 000000000..e44b249c4 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/WaypointCommand.java @@ -0,0 +1,429 @@ +package net.earthcomputer.clientcommands.command; + +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.vertex.PoseStack; +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.earthcomputer.clientcommands.render.RenderQueue; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.ChatFormatting; +import net.minecraft.SharedConstants; +import net.minecraft.Util; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.multiplayer.ClientChunkCache; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.ShapeRenderer; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentUtils; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.VisibleForTesting; +import org.joml.Vector2d; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +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.CBlockPosArgument.*; +import static dev.xpple.clientarguments.arguments.CDimensionArgument.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class WaypointCommand { + + private static final Map> waypoints = new HashMap<>(); + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final SimpleCommandExceptionType SAVE_FAILED_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cwaypoint.saveFailed")); + private static final DynamicCommandExceptionType ALREADY_EXISTS_EXCEPTION = new DynamicCommandExceptionType(name -> Component.translatable("commands.cwaypoint.alreadyExists", name)); + private static final DynamicCommandExceptionType NOT_FOUND_EXCEPTION = new DynamicCommandExceptionType(name -> Component.translatable("commands.cwaypoint.notFound", name)); + + static { + try { + loadFile(); + } catch (Exception 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"), getBlockPos(ctx, "pos"))) + .then(argument("dimension", dimension()) + .executes(ctx -> add(ctx.getSource(), getString(ctx, "name"), getBlockPos(ctx, "pos"), getDimension(ctx, "dimension"))))))) + .then(literal("remove") + .then(argument("name", word()) + .suggests((ctx, builder) -> { + Map worldWaypoints = waypoints.get(getWorldIdentifier(ctx.getSource().getClient())); + return SharedSuggestionProvider.suggest(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().getClient())); + return SharedSuggestionProvider.suggest(worldWaypoints != null ? worldWaypoints.keySet() : Collections.emptySet(), builder); + }) + .then(argument("pos", blockPos()) + .executes(ctx -> edit(ctx.getSource(), getString(ctx, "name"), getBlockPos(ctx, "pos"))) + .then(argument("dimension", dimension()) + .executes(ctx -> edit(ctx.getSource(), getString(ctx, "name"), getBlockPos(ctx, "pos"), getDimension(ctx, "dimension"))))))) + .then(literal("list") + .executes(ctx -> list(ctx.getSource())) + .then(argument("current", bool()) + .executes(ctx -> list(ctx.getSource(), getBool(ctx, "current")))))); + } + + private static String getWorldIdentifier(Minecraft minecraft) { + String worldIdentifier; + if (minecraft.hasSingleplayerServer()) { + // the level id remains the same even after the level is renamed + worldIdentifier = minecraft.getSingleplayerServer().storageSource.getLevelId(); + } else { + worldIdentifier = minecraft.getConnection().getConnection().getRemoteAddress().toString(); + } + return worldIdentifier; + } + + private static int add(FabricClientCommandSource source, String name, BlockPos pos) throws CommandSyntaxException { + return add(source, name, pos, source.getWorld().dimension()); + } + + private static int add(FabricClientCommandSource source, String name, BlockPos pos, ResourceKey dimension) throws CommandSyntaxException { + String worldIdentifier = getWorldIdentifier(source.getClient()); + + Map worldWaypoints = waypoints.computeIfAbsent(worldIdentifier, key -> new HashMap<>()); + + if (worldWaypoints.putIfAbsent(name, new WaypointLocation(dimension, pos)) != null) { + throw ALREADY_EXISTS_EXCEPTION.create(name); + } + + saveFile(); + source.sendFeedback(Component.translatable("commands.cwaypoint.add.success", name, formatCoordinates(pos), dimension.location())); + return Command.SINGLE_SUCCESS; + } + + private static int remove(FabricClientCommandSource source, String name) throws CommandSyntaxException { + String worldIdentifier = getWorldIdentifier(source.getClient()); + + 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(Component.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().dimension()); + } + + private static int edit(FabricClientCommandSource source, String name, BlockPos pos, ResourceKey dimension) throws CommandSyntaxException { + String worldIdentifier = getWorldIdentifier(source.getClient()); + + Map worldWaypoints = waypoints.get(worldIdentifier); + + if (worldWaypoints == null) { + throw NOT_FOUND_EXCEPTION.create(name); + } + + if (worldWaypoints.computeIfPresent(name, (key, value) -> new WaypointLocation(dimension, pos)) == null) { + throw NOT_FOUND_EXCEPTION.create(name); + } + + saveFile(); + source.sendFeedback(Component.translatable("commands.cwaypoint.edit.success", name, formatCoordinates(pos), dimension.location())); + 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.getClient()); + + Map worldWaypoints = waypoints.get(worldIdentifier); + + if (worldWaypoints == null || worldWaypoints.isEmpty()) { + source.sendFeedback(Component.translatable("commands.cwaypoint.list.empty")); + return 0; + } + + worldWaypoints.forEach((name, waypoint) -> source.sendFeedback(Component.translatable("commands.cwaypoint.list", name, formatCoordinates(waypoint.location()), waypoint.dimension().location()))); + return worldWaypoints.size(); + } + + if (waypoints.isEmpty()) { + source.sendFeedback(Component.translatable("commands.cwaypoint.list.empty")); + return 0; + } + + int[] count = {0}; + waypoints.forEach((worldIdentifier, worldWaypoints) -> { + if (worldWaypoints.isEmpty()) { + return; + } + + count[0] += worldWaypoints.size(); + + source.sendFeedback(Component.literal(worldIdentifier).append(":")); + worldWaypoints.forEach((name, waypoint) -> source.sendFeedback(Component.translatable("commands.cwaypoint.list", name, formatCoordinates(waypoint.location()), waypoint.dimension().location()))); + }); + return count[0]; + } + + private static void saveFile() throws CommandSyntaxException { + try { + CompoundTag rootTag = new CompoundTag(); + rootTag.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + CompoundTag compoundTag = new CompoundTag(); + waypoints.forEach((worldIdentifier, worldWaypoints) -> compoundTag.put(worldIdentifier, worldWaypoints.entrySet().stream() + .collect(CompoundTag::new, (result, entry) -> { + CompoundTag waypoint = new CompoundTag(); + Tag pos = NbtUtils.writeBlockPos(entry.getValue().location()); + waypoint.put("pos", pos); + String dimension = entry.getValue().dimension().location().toString(); + waypoint.putString("Dimension", dimension); + result.put(entry.getKey(), waypoint); + }, CompoundTag::merge))); + rootTag.put("Waypoints", compoundTag); + Path newFile = Files.createTempFile(ClientCommands.CONFIG_DIR, "waypoints", ".dat"); + NbtIo.write(rootTag, newFile); + Path backupFile = ClientCommands.CONFIG_DIR.resolve("waypoints.dat_old"); + Path currentFile = ClientCommands.CONFIG_DIR.resolve("waypoints.dat"); + Util.safeReplaceFile(currentFile, newFile, backupFile); + } catch (IOException e) { + throw SAVE_FAILED_EXCEPTION.create(); + } + } + + private static void loadFile() throws Exception { + waypoints.clear(); + CompoundTag rootTag = NbtIo.read(ClientCommands.CONFIG_DIR.resolve("waypoints.dat")); + if (rootTag == null) { + return; + } + waypoints.putAll(deserializeWaypoints(rootTag)); + } + + @VisibleForTesting + public static Map> deserializeWaypoints(CompoundTag rootTag) { + Map> waypoints = new HashMap<>(); + + CompoundTag compoundTag = rootTag.getCompound("Waypoints"); + compoundTag.getAllKeys().forEach(worldIdentifier -> { + CompoundTag worldWaypoints = compoundTag.getCompound(worldIdentifier); + waypoints.put(worldIdentifier, worldWaypoints.getAllKeys().stream() + .collect(Collectors.toMap(Function.identity(), name -> { + CompoundTag waypoint = worldWaypoints.getCompound(name); + BlockPos pos = NbtUtils.readBlockPos(waypoint, "pos").orElseThrow(); + ResourceKey dimension = Level.RESOURCE_KEY_CODEC.parse(new Dynamic<>(NbtOps.INSTANCE, waypoint.get("Dimension"))).resultOrPartial(LOGGER::error).orElseThrow(); + return new WaypointLocation(dimension, pos); + }))); + }); + + return waypoints; + } + + private static Component formatCoordinates(BlockPos waypoint) { + return ComponentUtils.wrapInSquareBrackets(Component.literal(waypoint.toShortString())).withStyle(style -> style + .withColor(ChatFormatting.GREEN) + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, waypoint.getX() + " " + waypoint.getY() + " " + waypoint.getZ())) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("chat.copy.click"))) + ); + } + + public static void registerEvents() { + HudRenderCallback.EVENT.register(WaypointCommand::renderWaypointLabels); + WorldRenderEvents.AFTER_ENTITIES.register(WaypointCommand::renderWaypointBoxes); + } + + private static void renderWaypointLabels(GuiGraphics guiGraphics, DeltaTracker deltaTracker) { + String worldIdentifier = getWorldIdentifier(Minecraft.getInstance()); + Map waypoints = WaypointCommand.waypoints.get(worldIdentifier); + if (waypoints == null) { + return; + } + + Minecraft minecraft = Minecraft.getInstance(); + GameRenderer gameRenderer = minecraft.gameRenderer; + Camera camera = gameRenderer.getMainCamera(); + Entity cameraEntity = camera.getEntity(); + float partialTicks = deltaTracker.getGameTimeDeltaPartialTick(true); + double verticalFovRad = Math.toRadians(gameRenderer.getFov(camera, partialTicks, false)); + Window window = minecraft.getWindow(); + double aspectRatio = (double) window.getGuiScaledWidth() / window.getGuiScaledHeight(); + double horizontalFovRad = 2 * Math.atan(Math.tan(verticalFovRad / 2) * aspectRatio); + + Vec3 viewVector3 = cameraEntity.getViewVector(1.0f); + Vector2d viewVector = new Vector2d(viewVector3.x, viewVector3.z); + Vector2d position = new Vector2d(cameraEntity.getEyePosition().x, cameraEntity.getEyePosition().z); + + List xPositions = new ArrayList<>(); + waypoints.forEach((waypointName, waypoint) -> { + if (!waypoint.dimension().location().equals(minecraft.level.dimension().location())) { + return; + } + + double distanceSquared = waypoint.location().distToCenterSqr(cameraEntity.position()); + long distance = Math.round(Math.sqrt(distanceSquared)); + Component label = ComponentUtils.wrapInSquareBrackets(Component.literal(waypointName + ' ' + distance).withStyle(ChatFormatting.YELLOW)); + + Vector2d waypointLocation = new Vector2d(waypoint.location().getX(), waypoint.location().getZ()); + double angleRad = viewVector.angle(waypointLocation.sub(position, new Vector2d())); + boolean right = angleRad > 0; + angleRad = Math.abs(angleRad); + + int x; + if (angleRad > horizontalFovRad / 2) { + int width = minecraft.font.width(label); + x = right ? guiGraphics.guiWidth() - width / 2 : width / 2; + } else { + // V is the view vector + // A is the leftmost visible direction + // B is the rightmost visible direction + // M is the intersection of the position -> waypoint line with AB + double mv = Math.tan(angleRad) * GameRenderer.PROJECTION_Z_NEAR; + double av = Math.tan(horizontalFovRad / 2) * GameRenderer.PROJECTION_Z_NEAR; + double ab = 2 * av; + double am = right ? mv + av : ab - (mv + av); + double perc = am / ab; + x = (int) (perc * guiGraphics.guiWidth()); + } + xPositions.add(new WaypointLabelLocation(label, x)); + }); + + xPositions.sort(Comparator.comparingInt(WaypointLabelLocation::location)); + + List> positions = new ArrayList<>(); + positions.add(xPositions); + + for (int line = 0; line < positions.size(); line++) { + List waypointLabelLocations = positions.get(line); + int i = 0; + while (i < waypointLabelLocations.size() - 1) { + WaypointLabelLocation left = waypointLabelLocations.get(i); + WaypointLabelLocation right = waypointLabelLocations.get(i + 1); + int leftX = left.location(); + int rightX = right.location(); + int leftWidth = minecraft.font.width(left.label()); + int rightWidth = minecraft.font.width(right.label()); + if (leftWidth / 2 + rightWidth / 2 > rightX - leftX) { + if (line + 1 == positions.size()) { + positions.add(new ArrayList<>()); + } + List nextLevel = positions.get(line + 1); + WaypointLabelLocation removed = waypointLabelLocations.remove(i + 1); + nextLevel.add(removed); + } else { + i++; + } + } + } + + for (int line = 0; line < positions.size(); line++) { + List w = positions.get(line); + for (WaypointLabelLocation waypoint : w) { + guiGraphics.drawCenteredString(minecraft.font, waypoint.label(), waypoint.location(), 1 + line * minecraft.font.lineHeight, 0xFFFFFF); + } + } + } + + private static void renderWaypointBoxes(WorldRenderContext context) { + String worldIdentifier = getWorldIdentifier(Minecraft.getInstance()); + Map waypoints = WaypointCommand.waypoints.get(worldIdentifier); + if (waypoints == null) { + return; + } + + ClientChunkCache chunkSource = context.world().getChunkSource(); + waypoints.forEach((waypointName, waypoint) -> { + if (!waypoint.dimension().location().equals(context.world().dimension().location())) { + return; + } + + BlockPos waypointLocation = waypoint.location(); + if (!chunkSource.hasChunk(waypointLocation.getX() >> 4, waypointLocation.getZ() >> 4)) { + return; + } + + Vec3 cameraPosition = context.camera().getPosition(); + float distance = (float) waypointLocation.distToCenterSqr(cameraPosition); + distance = (float) Math.sqrt(distance) / 6; + + PoseStack stack = context.matrixStack(); + stack.pushPose(); + stack.translate(cameraPosition.scale(-1)); + + AABB box = new AABB(waypointLocation); + ShapeRenderer.renderLineBox(stack, context.consumers().getBuffer(RenderQueue.NO_DEPTH_LAYER), box, 1, 1, 1, 1); + + stack.translate(waypointLocation.getCenter().add(new Vec3(0, 1, 0))); + stack.mulPose(context.camera().rotation()); + stack.scale(0.025f * distance, -0.025f * distance, 0.025f * distance); + + Font font = Minecraft.getInstance().font; + int width = font.width(waypointName) / 2; + int backgroundColour = (int) (Minecraft.getInstance().options.getBackgroundOpacity(0.25f) * 255.0f) << 24; + font.drawInBatch(waypointName, -width, 0, 0xFFFFFF, false, stack.last().pose(), context.consumers(), Font.DisplayMode.SEE_THROUGH, backgroundColour, LightTexture.FULL_SKY); + + stack.popPose(); + }); + } + + @VisibleForTesting + public record WaypointLocation(ResourceKey dimension, BlockPos location) { + } + + record WaypointLabelLocation(Component label, int location) { + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/ClientCommandFunctions.java b/src/main/java/net/earthcomputer/clientcommands/features/ClientCommandFunctions.java index 796730a62..4a18ff968 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/ClientCommandFunctions.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/ClientCommandFunctions.java @@ -40,7 +40,7 @@ public class ClientCommandFunctions { private static final Logger LOGGER = LogUtils.getLogger(); - private static final Path FUNCTION_DIR = ClientCommands.configDir.resolve("functions"); + private static final Path FUNCTION_DIR = ClientCommands.CONFIG_DIR.resolve("functions"); private static final DynamicCommandExceptionType NO_SUCH_FUNCTION_EXCEPTION = new DynamicCommandExceptionType(id -> Component.translatable("arguments.function.unknown", id)); private static final DynamicCommandExceptionType COMMAND_LIMIT_REACHED_EXCEPTION = new DynamicCommandExceptionType(limit -> Component.translatable("commands.cfunction.limitReached", limit)); diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/scrambletitle/MinecraftMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/scrambletitle/MinecraftMixin.java index 525dd2263..6ccd88d13 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/scrambletitle/MinecraftMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/scrambletitle/MinecraftMixin.java @@ -17,7 +17,7 @@ public class MinecraftMixin { // Earth annoying his friends <3 nothing to see here @Inject(method = "createTitle", at = @At("RETURN"), cancellable = true) private void modifyWindowTitle(CallbackInfoReturnable ci) { - if (ClientCommands.SCRAMBLE_WINDOW_TITLE) { + if (ClientCommands.scrambleWindowTitle) { List chars = ci.getReturnValue().chars().mapToObj(c -> (char) c).collect(Collectors.toCollection(ArrayList::new)); Collections.shuffle(chars); ci.setReturnValue(chars.stream().map(String::valueOf).collect(Collectors.joining())); diff --git a/src/main/java/net/earthcomputer/clientcommands/render/RenderQueue.java b/src/main/java/net/earthcomputer/clientcommands/render/RenderQueue.java index 489b3accf..f03a3e3ac 100644 --- a/src/main/java/net/earthcomputer/clientcommands/render/RenderQueue.java +++ b/src/main/java/net/earthcomputer/clientcommands/render/RenderQueue.java @@ -113,7 +113,7 @@ private record AddQueueEntry(Layer layer, Object key, Shape shape, int life) {} private record RemoveQueueEntry(Layer layer, Object key) {} - private static final RenderType NO_DEPTH_LAYER = RenderType.create("clientcommands_no_depth", DefaultVertexFormat.POSITION_COLOR_NORMAL, VertexFormat.Mode.LINES, 256, true, true, RenderType.CompositeState.builder() + public static final RenderType NO_DEPTH_LAYER = RenderType.create("clientcommands_no_depth", DefaultVertexFormat.POSITION_COLOR_NORMAL, VertexFormat.Mode.LINES, 256, true, true, RenderType.CompositeState.builder() .setShaderState(RenderType.RENDERTYPE_LINES_SHADER) .setWriteMaskState(RenderType.COLOR_WRITE) .setCullState(RenderType.NO_CULL) diff --git a/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java b/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java index bf14fd9ff..67a74f69c 100644 --- a/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java +++ b/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java @@ -116,7 +116,7 @@ private void handleStackTrace(int stackTrace) { public void writeToFile() { try { this.nbtStream.close(); - Path debugDir = ClientCommands.configDir.resolve("debug"); + Path debugDir = ClientCommands.CONFIG_DIR.resolve("debug"); Files.createDirectories(debugDir); try (DataOutputStream dataOutput = new DataOutputStream(new GZIPOutputStream(Files.newOutputStream(debugDir.resolve(this.entity.getStringUUID() + ".dat"))))) { dataOutput.writeInt(stackTraceById.size()); diff --git a/src/main/java/net/earthcomputer/clientcommands/util/MappingsHelper.java b/src/main/java/net/earthcomputer/clientcommands/util/MappingsHelper.java index 018a10be6..b676472c6 100644 --- a/src/main/java/net/earthcomputer/clientcommands/util/MappingsHelper.java +++ b/src/main/java/net/earthcomputer/clientcommands/util/MappingsHelper.java @@ -46,7 +46,7 @@ public static void load() { private static final Logger LOGGER = LogUtils.getLogger(); - private static final Path MAPPINGS_DIR = ClientCommands.configDir.resolve("mappings"); + private static final Path MAPPINGS_DIR = ClientCommands.CONFIG_DIR.resolve("mappings"); private static final boolean IS_DEV_ENV = FabricLoader.getInstance().isDevelopmentEnvironment(); diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index a0f6884ba..6e1d6af7f 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -284,6 +284,15 @@ "commands.cvar.remove.success": "Successfully removed variable \"%s\"", "commands.cvar.saveFile.failed": "Could not save variables file", + "commands.cwaypoint.add.success": "Waypoint \"%s\" at %s in %s successfully added", + "commands.cwaypoint.alreadyExists": "A waypoint with the name \"%s\" already exists", + "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.notFound": "No waypoint with the name \"%s\" could be found", + "commands.cwaypoint.remove.success": "Waypoint \"%s\" successfully removed", + "commands.cwaypoint.saveFailed": "Could not save waypoints file", + "commands.cwe.playerNotFound": "Player not found", "commands.cweather.reset": "Stopped overriding weather", diff --git a/src/main/resources/clientcommands.aw b/src/main/resources/clientcommands.aw index 7e78137d2..1abc9abbb 100644 --- a/src/main/resources/clientcommands.aw +++ b/src/main/resources/clientcommands.aw @@ -36,6 +36,10 @@ accessible field net/minecraft/network/codec/IdDispatchCodec toId Lit/unimi/dsi/ # cpermissionlevel accessible method net/minecraft/client/player/LocalPlayer getPermissionLevel ()I +# cwaypoint +accessible field net/minecraft/server/MinecraftServer storageSource Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess; +accessible method net/minecraft/client/renderer/GameRenderer getFov (Lnet/minecraft/client/Camera;FZ)F + # Game Options accessible field net/minecraft/client/OptionInstance value Ljava/lang/Object; diff --git a/src/test/java/net/earthcomputer/clientcommands/test/WaypointLoadingTest.java b/src/test/java/net/earthcomputer/clientcommands/test/WaypointLoadingTest.java new file mode 100644 index 000000000..b2ce5f056 --- /dev/null +++ b/src/test/java/net/earthcomputer/clientcommands/test/WaypointLoadingTest.java @@ -0,0 +1,58 @@ +package net.earthcomputer.clientcommands.test; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.earthcomputer.clientcommands.command.WaypointCommand; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.Level; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public final class WaypointLoadingTest { + @BeforeAll + public static void setup() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + private static CompoundTag parseSnbt(String snbt) { + try { + return new TagParser(new StringReader(snbt)).readStruct(); + } catch (CommandSyntaxException e) { + throw new AssertionError(e); + } + } + + @Test + public void testWaypointLoading() { + CompoundTag waypointTag = parseSnbt(""" + { + DataVersion: 4189, + Waypoints: { + foo: { + testWaypoint: { + pos: [I; 1, 2, 3], + Dimension: "minecraft:overworld" + } + } + } + } + """); + + var waypoints = WaypointCommand.deserializeWaypoints(waypointTag); + assertEquals(1, waypoints.size()); + assertTrue(waypoints.containsKey("foo")); + var worldWaypoints = waypoints.get("foo"); + assertEquals(1, worldWaypoints.size()); + assertTrue(worldWaypoints.containsKey("testWaypoint")); + var waypoint = worldWaypoints.get("testWaypoint"); + assertEquals(new BlockPos(1, 2, 3), waypoint.location()); + assertEquals(Level.OVERWORLD, waypoint.dimension()); + } +}