diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java index 270fcfc78..92f389ae8 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java @@ -1,5 +1,6 @@ package net.earthcomputer.clientcommands.c2c; +import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; @@ -9,6 +10,7 @@ import net.earthcomputer.clientcommands.c2c.packets.PutTicTacToeMarkC2CPacket; import net.earthcomputer.clientcommands.c2c.packets.StartTicTacToeGameC2CPacket; import net.earthcomputer.clientcommands.command.ListenCommand; +import net.earthcomputer.clientcommands.command.arguments.FormattedComponentArgument; import net.earthcomputer.clientcommands.interfaces.IClientPacketListener_C2C; import net.earthcomputer.clientcommands.command.TicTacToeCommand; import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; @@ -188,12 +190,18 @@ public static boolean handleC2CPacket(String content, String sender) { public void onMessageC2CPacket(MessageC2CPacket packet) { String sender = packet.sender(); String message = packet.message(); + Component formattedComponent; + try { + formattedComponent = FormattedComponentArgument.formattedComponent().parse(new StringReader(message)); + } catch (CommandSyntaxException e) { + formattedComponent = Component.nullToEmpty(message); + } MutableComponent prefix = Component.empty(); prefix.append(Component.literal("[").withStyle(ChatFormatting.DARK_GRAY)); prefix.append(Component.literal("/cwe").withStyle(ChatFormatting.AQUA)); prefix.append(Component.literal("]").withStyle(ChatFormatting.DARK_GRAY)); prefix.append(Component.literal(" ")); - Component component = prefix.append(Component.translatable("c2cpacket.messageC2CPacket.incoming", sender, message).withStyle(ChatFormatting.GRAY)); + Component component = prefix.append(Component.translatable("c2cpacket.messageC2CPacket.incoming", sender, formattedComponent)); Minecraft.getInstance().gui.getChat().addMessage(component); } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/NoteCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/NoteCommand.java index e16475533..3a587c8d7 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/NoteCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/NoteCommand.java @@ -5,15 +5,15 @@ import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.minecraft.network.chat.MutableComponent; -import static net.earthcomputer.clientcommands.command.arguments.FormattedComponentArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.StyledComponentArgument.*; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; public class NoteCommand { public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("cnote") - .then(argument("message", formattedComponent()) - .executes(ctx -> note(ctx.getSource(), getFormattedComponent(ctx, "message"))))); + .then(argument("message", styledComponent()) + .executes(ctx -> note(ctx.getSource(), getStyledComponent(ctx, "message"))))); } private static int note(FabricClientCommandSource source, MutableComponent message) { diff --git a/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java index 0bb5ede87..06ce2eecb 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java @@ -13,8 +13,9 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; -import static com.mojang.brigadier.arguments.StringArgumentType.*; import static dev.xpple.clientarguments.arguments.CGameProfileArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.FormattedComponentArgument.*; +import static net.earthcomputer.clientcommands.command.arguments.WithStringArgument.*; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; public class WhisperEncryptedCommand { @@ -24,24 +25,24 @@ public class WhisperEncryptedCommand { public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("cwe") .then(argument("player", gameProfile(true)) - .then(argument("message", greedyString()) - .executes((ctx) -> whisper(ctx.getSource(), getSingleProfileArgument(ctx, "player"), getString(ctx, "message")))))); + .then(argument("message", withString(formattedComponent())) + .executes((ctx) -> whisper(ctx.getSource(), getSingleProfileArgument(ctx, "player"), getWithString(ctx, "message", MutableComponent.class)))))); } - private static int whisper(FabricClientCommandSource source, GameProfile player, String message) throws CommandSyntaxException { + private static int whisper(FabricClientCommandSource source, GameProfile player, Result result) throws CommandSyntaxException { PlayerInfo recipient = source.getClient().getConnection().getPlayerInfo(player.getId()); if (recipient == null) { throw PLAYER_NOT_FOUND_EXCEPTION.create(); } - MessageC2CPacket packet = new MessageC2CPacket(source.getClient().getConnection().getLocalGameProfile().getName(), message); + MessageC2CPacket packet = new MessageC2CPacket(source.getClient().getConnection().getLocalGameProfile().getName(), result.string()); C2CPacketHandler.getInstance().sendPacket(packet, recipient); MutableComponent prefix = Component.empty(); prefix.append(Component.literal("[").withStyle(ChatFormatting.DARK_GRAY)); prefix.append(Component.literal("/cwe").withStyle(ChatFormatting.AQUA)); prefix.append(Component.literal("]").withStyle(ChatFormatting.DARK_GRAY)); prefix.append(Component.literal(" ")); - Component component = prefix.append(Component.translatable("c2cpacket.messageC2CPacket.outgoing", recipient.getProfile().getName(), message).withStyle(ChatFormatting.GRAY)); + Component component = prefix.append(Component.translatable("c2cpacket.messageC2CPacket.outgoing", recipient.getProfile().getName(), result.value())); source.sendFeedback(component); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/FormattedComponentArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/FormattedComponentArgument.java index 280fa8fc9..3f819ba09 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/arguments/FormattedComponentArgument.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/FormattedComponentArgument.java @@ -1,47 +1,33 @@ package net.earthcomputer.clientcommands.command.arguments; -import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import com.mojang.serialization.JsonOps; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.minecraft.ChatFormatting; import net.minecraft.commands.SharedSuggestionProvider; -import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.ComponentSerialization; -import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.TextColor; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.StringRepresentable; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import java.util.function.Function; public class FormattedComponentArgument implements ArgumentType { - private static final Collection EXAMPLES = Arrays.asList("Earth", "bold{xpple}", "bold{italic{red{nwex}}}"); - private static final DynamicCommandExceptionType INVALID_CLICK_ACTION = new DynamicCommandExceptionType(action -> Component.translatable("commands.client.invalidClickAction", action)); - private static final DynamicCommandExceptionType INVALID_HOVER_ACTION = new DynamicCommandExceptionType(action -> Component.translatable("commands.client.invalidHoverAction", action)); - private static final DynamicCommandExceptionType INVALID_HOVER_EVENT = new DynamicCommandExceptionType(event -> Component.translatable("commands.client.invalidHoverEvent", event)); - private FormattedComponentArgument() { - } + private static final Collection EXAMPLES = Arrays.asList("Earth", "&lxpple", "&l&o#fb8919nwex"); + + private static final SimpleCommandExceptionType EXPECTED_FORMATTING_CODE_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.client.expectedFormattingCode")); + private static final SimpleCommandExceptionType UNKNOWN_FORMATTING_CODE_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.client.unknownFormattingCode")); + private static final SimpleCommandExceptionType EXPECTED_HEX_VALUE_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.client.expectedHexValue")); + private static final SimpleCommandExceptionType INVALID_HEX_VALUE_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.client.invalidHexValue")); public static FormattedComponentArgument formattedComponent() { return new FormattedComponentArgument(); @@ -62,10 +48,9 @@ public CompletableFuture listSuggestions(CommandContext cont reader.setCursor(builder.getStart()); Parser parser = new Parser(reader); - try { parser.parse(); - } catch (CommandSyntaxException ignored) { + } catch (CommandSyntaxException ignore) { } if (parser.suggestor != null) { @@ -89,160 +74,50 @@ public Parser(StringReader reader) { } public MutableComponent parse() throws CommandSyntaxException { - int cursor = reader.getCursor(); - suggestor = builder -> { - SuggestionsBuilder newBuilder = builder.createOffset(cursor); - SharedSuggestionProvider.suggest(FormattedText.FORMATTING.keySet(), newBuilder); - builder.add(newBuilder); - }; - - String word = reader.readUnquotedString(); - - if (FormattedText.FORMATTING.containsKey(word.toLowerCase(Locale.ROOT))) { - FormattedText.Styler styler = FormattedText.FORMATTING.get(word.toLowerCase(Locale.ROOT)); - suggestor = null; - reader.skipWhitespace(); - - if (!reader.canRead() || reader.peek() != '{') { - throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader, "{"); - } - reader.skip(); - reader.skipWhitespace(); - MutableComponent literalText; - List arguments = new ArrayList<>(); - if (reader.canRead()) { - if (reader.peek() != '}') { - if (StringReader.isQuotedStringStart(reader.peek())) { - literalText = Component.literal(reader.readQuotedString()); - } else { - literalText = parse(); - } - reader.skipWhitespace(); - while (reader.canRead() && reader.peek() != '}') { - if (arguments.isEmpty()) { - suggestor = builder -> { - SuggestionsBuilder newBuilder = builder.createOffset(cursor); - SharedSuggestionProvider.suggest(styler.suggestions, newBuilder); - builder.add(newBuilder); - }; - } - if (reader.peek() != ',') { - throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader, ","); - } - reader.skip(); - reader.skipWhitespace(); - arguments.add(readArgument()); - reader.skipWhitespace(); + MutableComponent text = Component.empty(); + Style style = Style.EMPTY; + while (reader.canRead()) { + char c = reader.read(); + if (c == '&') { // ChatFormatting.PREFIX_CODE is not writable in chat + suggestor = suggestions -> { + SuggestionsBuilder builder = suggestions.createOffset(reader.getCursor()); + SharedSuggestionProvider.suggest(Arrays.stream(ChatFormatting.values()).map(f -> String.valueOf(f.getChar())), builder); + suggestions.add(builder); + }; + if (!reader.canRead()) { + throw EXPECTED_FORMATTING_CODE_EXCEPTION.create(); + } + char code = reader.read(); + ChatFormatting formatting = ChatFormatting.getByCode(code); + if (formatting == null) { + throw UNKNOWN_FORMATTING_CODE_EXCEPTION.create(); + } + style = style.applyFormat(formatting); + } else if (c == TextColor.CUSTOM_COLOR_PREFIX.charAt(0)) { + if (!reader.canRead()) { + throw EXPECTED_HEX_VALUE_EXCEPTION.create(); + } + StringBuilder builder = new StringBuilder(); + int hex = -1; + for (int i = 0; i < 6 && reader.canRead(); i++) { + builder.append(reader.peek()); + try { + hex = Integer.parseInt(builder.toString(), 16); + } catch (NumberFormatException e) { + break; } - } else { - literalText = Component.literal(""); + reader.skip(); + } + if (hex == -1) { + throw INVALID_HEX_VALUE_EXCEPTION.create(); } + style = style.withColor(hex); } else { - throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader, "}"); - } - reader.skip(); - - if (styler.argumentCount != arguments.size()) { - reader.setCursor(cursor); - reader.readUnquotedString(); - throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader); + text.append(Component.literal(String.valueOf(c)).setStyle(style)); } - return new FormattedText(styler.operator, literalText, arguments).style(); - } else { - return Component.literal(word + readArgument()); - } - } - - private String readArgument() { - final int start = reader.getCursor(); - while (reader.canRead() && isAllowedInArgument(reader.peek())) { - reader.skip(); - } - return reader.getString().substring(start, reader.getCursor()); - } - - private static boolean isAllowedInArgument(final char c) { - return c != ',' && c != '{' && c != '}'; - } - } - - static class FormattedText { - private static final Map FORMATTING = ImmutableMap.builder() - .put("aqua", new Styler((s, o) -> s.applyFormat(ChatFormatting.AQUA), 0)) - .put("black", new Styler((s, o) -> s.applyFormat(ChatFormatting.BLACK), 0)) - .put("blue", new Styler((s, o) -> s.applyFormat(ChatFormatting.BLUE), 0)) - .put("bold", new Styler((s, o) -> s.applyFormat(ChatFormatting.BOLD), 0)) - .put("dark_aqua", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_AQUA), 0)) - .put("dark_blue", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_BLUE), 0)) - .put("dark_gray", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_GRAY), 0)) - .put("dark_green", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_GREEN), 0)) - .put("dark_purple", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_PURPLE), 0)) - .put("dark_red", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_RED), 0)) - .put("gold", new Styler((s, o) -> s.applyFormat(ChatFormatting.GOLD), 0)) - .put("gray", new Styler((s, o) -> s.applyFormat(ChatFormatting.GRAY), 0)) - .put("green", new Styler((s, o) -> s.applyFormat(ChatFormatting.GREEN), 0)) - .put("italic", new Styler((s, o) -> s.applyFormat(ChatFormatting.ITALIC), 0)) - .put("light_purple", new Styler((s, o) -> s.applyFormat(ChatFormatting.LIGHT_PURPLE), 0)) - .put("obfuscated", new Styler((s, o) -> s.applyFormat(ChatFormatting.OBFUSCATED), 0)) - .put("red", new Styler((s, o) -> s.applyFormat(ChatFormatting.RED), 0)) - .put("reset", new Styler((s, o) -> s.applyFormat(ChatFormatting.RESET), 0)) - .put("strikethrough", new Styler((s, o) -> s.applyFormat(ChatFormatting.STRIKETHROUGH), 0)) - .put("underline", new Styler((s, o) -> s.applyFormat(ChatFormatting.UNDERLINE), 0)) - .put("white", new Styler((s, o) -> s.applyFormat(ChatFormatting.WHITE), 0)) - .put("yellow", new Styler((s, o) -> s.applyFormat(ChatFormatting.YELLOW), 0)) - - .put("font", new Styler((s, o) -> s.withFont(ResourceLocation.tryParse(o.getFirst())), 1, "alt", "default")) - .put("hex", new Styler((s, o) -> s.withColor(TextColor.fromRgb(Integer.parseInt(o.getFirst(), 16))), 1)) - .put("insert", new Styler((s, o) -> s.withInsertion(o.getFirst()), 1)) - - .put("click", new Styler((s, o) -> s.withClickEvent(parseClickEvent(o.getFirst(), o.get(1))), 2, "change_page", "copy_to_clipboard", "open_file", "open_url", "run_command", "suggest_command")) - .put("hover", new Styler((s, o) -> s.withHoverEvent(parseHoverEvent(o.getFirst(), o.get(1))), 2, "show_entity", "show_item", "show_text")) - - // aliases - .put("strike", new Styler((s, o) -> s.applyFormat(ChatFormatting.STRIKETHROUGH), 0)) - .put("magic", new Styler((s, o) -> s.applyFormat(ChatFormatting.OBFUSCATED), 0)) - .build(); - - private final StylerFunc styler; - private final MutableComponent argument; - private final List args; - - public FormattedText(StylerFunc styler, MutableComponent argument, List args) { - this.styler = styler; - this.argument = argument; - this.args = args; - } - - public MutableComponent style() throws CommandSyntaxException { - return this.argument.setStyle(this.styler.apply(this.argument.getStyle(), this.args)); - } - - private record Styler(StylerFunc operator, int argumentCount, String... suggestions) {} - - @FunctionalInterface - interface StylerFunc { - Style apply(Style style, List args) throws CommandSyntaxException; - } - - private static final Function CLICK_EVENT_ACTION_BY_NAME = StringRepresentable.createNameLookup(ClickEvent.Action.values(), Function.identity()); - - private static ClickEvent parseClickEvent(String name, String value) throws CommandSyntaxException { - ClickEvent.Action action = CLICK_EVENT_ACTION_BY_NAME.apply(name); - if (action == null) { - throw INVALID_CLICK_ACTION.create(name); - } - return new ClickEvent(action, value); - } - - private static HoverEvent parseHoverEvent(String name, String value) throws CommandSyntaxException { - HoverEvent.Action action = HoverEvent.Action.UNSAFE_CODEC.parse(JsonOps.INSTANCE, new JsonPrimitive(name)).result().orElse(null); - if (action == null) { - throw INVALID_HOVER_ACTION.create(name); + suggestor = null; } - - JsonElement component = ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, Component.nullToEmpty(value)).getOrThrow(); - HoverEvent.TypedHoverEvent eventData = action.legacyCodec.codec().parse(JsonOps.INSTANCE, component).getOrThrow(error -> INVALID_HOVER_EVENT.create(value)); - return new HoverEvent(eventData); + return text; } } } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/StyledComponentArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/StyledComponentArgument.java new file mode 100644 index 000000000..1d43347f8 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/StyledComponentArgument.java @@ -0,0 +1,248 @@ +package net.earthcomputer.clientcommands.command.arguments; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.serialization.JsonOps; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.StringRepresentable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; + +public class StyledComponentArgument implements ArgumentType { + private static final Collection EXAMPLES = Arrays.asList("Earth", "bold{xpple}", "bold{italic{red{nwex}}}"); + private static final DynamicCommandExceptionType INVALID_CLICK_ACTION = new DynamicCommandExceptionType(action -> Component.translatable("commands.client.invalidClickAction", action)); + private static final DynamicCommandExceptionType INVALID_HOVER_ACTION = new DynamicCommandExceptionType(action -> Component.translatable("commands.client.invalidHoverAction", action)); + private static final DynamicCommandExceptionType INVALID_HOVER_EVENT = new DynamicCommandExceptionType(event -> Component.translatable("commands.client.invalidHoverEvent", event)); + + private StyledComponentArgument() { + } + + public static StyledComponentArgument styledComponent() { + return new StyledComponentArgument(); + } + + public static MutableComponent getStyledComponent(CommandContext context, String arg) { + return context.getArgument(arg, MutableComponent.class); + } + + @Override + public MutableComponent parse(StringReader reader) throws CommandSyntaxException { + return new Parser(reader).parse(); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + StringReader reader = new StringReader(builder.getInput()); + reader.setCursor(builder.getStart()); + + Parser parser = new Parser(reader); + + try { + parser.parse(); + } catch (CommandSyntaxException ignored) { + } + + if (parser.suggestor != null) { + parser.suggestor.accept(builder); + } + + return builder.buildFuture(); + } + + @Override + public Collection getExamples() { + return EXAMPLES; + } + + private static class Parser { + private final StringReader reader; + private Consumer suggestor; + + public Parser(StringReader reader) { + this.reader = reader; + } + + public MutableComponent parse() throws CommandSyntaxException { + int cursor = reader.getCursor(); + suggestor = builder -> { + SuggestionsBuilder newBuilder = builder.createOffset(cursor); + SharedSuggestionProvider.suggest(StyledComponent.FORMATTING.keySet(), newBuilder); + builder.add(newBuilder); + }; + + String word = reader.readUnquotedString(); + + if (StyledComponent.FORMATTING.containsKey(word.toLowerCase(Locale.ROOT))) { + StyledComponent.Styler styler = StyledComponent.FORMATTING.get(word.toLowerCase(Locale.ROOT)); + suggestor = null; + reader.skipWhitespace(); + + if (!reader.canRead() || reader.peek() != '{') { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader, "{"); + } + reader.skip(); + reader.skipWhitespace(); + MutableComponent literalText; + List arguments = new ArrayList<>(); + if (reader.canRead()) { + if (reader.peek() != '}') { + if (StringReader.isQuotedStringStart(reader.peek())) { + literalText = Component.literal(reader.readQuotedString()); + } else { + literalText = parse(); + } + reader.skipWhitespace(); + while (reader.canRead() && reader.peek() != '}') { + if (arguments.isEmpty()) { + suggestor = builder -> { + SuggestionsBuilder newBuilder = builder.createOffset(cursor); + SharedSuggestionProvider.suggest(styler.suggestions, newBuilder); + builder.add(newBuilder); + }; + } + if (reader.peek() != ',') { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader, ","); + } + reader.skip(); + reader.skipWhitespace(); + arguments.add(readArgument()); + reader.skipWhitespace(); + } + } else { + literalText = Component.literal(""); + } + } else { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader, "}"); + } + reader.skip(); + + if (styler.argumentCount != arguments.size()) { + reader.setCursor(cursor); + reader.readUnquotedString(); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader); + } + return new StyledComponent(styler.operator, literalText, arguments).style(); + } else { + return Component.literal(word + readArgument()); + } + } + + private String readArgument() { + final int start = reader.getCursor(); + while (reader.canRead() && isAllowedInArgument(reader.peek())) { + reader.skip(); + } + return reader.getString().substring(start, reader.getCursor()); + } + + private static boolean isAllowedInArgument(final char c) { + return c != ',' && c != '{' && c != '}'; + } + } + + static class StyledComponent { + private static final Map FORMATTING = ImmutableMap.builder() + .put("aqua", new Styler((s, o) -> s.applyFormat(ChatFormatting.AQUA), 0)) + .put("black", new Styler((s, o) -> s.applyFormat(ChatFormatting.BLACK), 0)) + .put("blue", new Styler((s, o) -> s.applyFormat(ChatFormatting.BLUE), 0)) + .put("bold", new Styler((s, o) -> s.applyFormat(ChatFormatting.BOLD), 0)) + .put("dark_aqua", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_AQUA), 0)) + .put("dark_blue", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_BLUE), 0)) + .put("dark_gray", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_GRAY), 0)) + .put("dark_green", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_GREEN), 0)) + .put("dark_purple", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_PURPLE), 0)) + .put("dark_red", new Styler((s, o) -> s.applyFormat(ChatFormatting.DARK_RED), 0)) + .put("gold", new Styler((s, o) -> s.applyFormat(ChatFormatting.GOLD), 0)) + .put("gray", new Styler((s, o) -> s.applyFormat(ChatFormatting.GRAY), 0)) + .put("green", new Styler((s, o) -> s.applyFormat(ChatFormatting.GREEN), 0)) + .put("italic", new Styler((s, o) -> s.applyFormat(ChatFormatting.ITALIC), 0)) + .put("light_purple", new Styler((s, o) -> s.applyFormat(ChatFormatting.LIGHT_PURPLE), 0)) + .put("obfuscated", new Styler((s, o) -> s.applyFormat(ChatFormatting.OBFUSCATED), 0)) + .put("red", new Styler((s, o) -> s.applyFormat(ChatFormatting.RED), 0)) + .put("reset", new Styler((s, o) -> s.applyFormat(ChatFormatting.RESET), 0)) + .put("strikethrough", new Styler((s, o) -> s.applyFormat(ChatFormatting.STRIKETHROUGH), 0)) + .put("underline", new Styler((s, o) -> s.applyFormat(ChatFormatting.UNDERLINE), 0)) + .put("white", new Styler((s, o) -> s.applyFormat(ChatFormatting.WHITE), 0)) + .put("yellow", new Styler((s, o) -> s.applyFormat(ChatFormatting.YELLOW), 0)) + + .put("font", new Styler((s, o) -> s.withFont(ResourceLocation.tryParse(o.getFirst())), 1, "alt", "default")) + .put("hex", new Styler((s, o) -> s.withColor(TextColor.fromRgb(Integer.parseInt(o.getFirst(), 16))), 1)) + .put("insert", new Styler((s, o) -> s.withInsertion(o.getFirst()), 1)) + + .put("click", new Styler((s, o) -> s.withClickEvent(parseClickEvent(o.getFirst(), o.get(1))), 2, "change_page", "copy_to_clipboard", "open_file", "open_url", "run_command", "suggest_command")) + .put("hover", new Styler((s, o) -> s.withHoverEvent(parseHoverEvent(o.getFirst(), o.get(1))), 2, "show_entity", "show_item", "show_text")) + + // aliases + .put("strike", new Styler((s, o) -> s.applyFormat(ChatFormatting.STRIKETHROUGH), 0)) + .put("magic", new Styler((s, o) -> s.applyFormat(ChatFormatting.OBFUSCATED), 0)) + .build(); + + private final StylerFunc styler; + private final MutableComponent argument; + private final List args; + + public StyledComponent(StylerFunc styler, MutableComponent argument, List args) { + this.styler = styler; + this.argument = argument; + this.args = args; + } + + public MutableComponent style() throws CommandSyntaxException { + return this.argument.setStyle(this.styler.apply(this.argument.getStyle(), this.args)); + } + + private record Styler(StylerFunc operator, int argumentCount, String... suggestions) {} + + @FunctionalInterface + interface StylerFunc { + Style apply(Style style, List args) throws CommandSyntaxException; + } + + private static final Function CLICK_EVENT_ACTION_BY_NAME = StringRepresentable.createNameLookup(ClickEvent.Action.values(), Function.identity()); + + private static ClickEvent parseClickEvent(String name, String value) throws CommandSyntaxException { + ClickEvent.Action action = CLICK_EVENT_ACTION_BY_NAME.apply(name); + if (action == null) { + throw INVALID_CLICK_ACTION.create(name); + } + return new ClickEvent(action, value); + } + + private static HoverEvent parseHoverEvent(String name, String value) throws CommandSyntaxException { + HoverEvent.Action action = HoverEvent.Action.UNSAFE_CODEC.parse(JsonOps.INSTANCE, new JsonPrimitive(name)).result().orElse(null); + if (action == null) { + throw INVALID_HOVER_ACTION.create(name); + } + + JsonElement component = ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, Component.nullToEmpty(value)).getOrThrow(); + HoverEvent.TypedHoverEvent eventData = action.legacyCodec.codec().parse(JsonOps.INSTANCE, component).getOrThrow(error -> INVALID_HOVER_EVENT.create(value)); + return new HoverEvent(eventData); + } + } +} diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index ec1169c12..345b5d96b 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -253,15 +253,19 @@ "commands.client.cancel": "Cancel", "commands.client.crack": "Crack", "commands.client.enable": "Enable", + "commands.client.expectedFormattingCode": "Expected a formatting code", + "commands.client.expectedHexValue": "Expected a hex value", "commands.client.expectedRegex": "Invalid regex %s", "commands.client.invalidArgumentException": "Invalid argument '%s'", "commands.client.invalidClickAction": "Invalid click event action '%s'", + "commands.client.invalidHexValue": "Invalid hex value", "commands.client.invalidHoverAction": "Invalid hover event action '%s'", "commands.client.invalidHoverEvent": "Invalid hover event '%s'", "commands.client.notClient": "Not a client-side command", "commands.client.regexTooSlow": "Regex '%s' was too slow to match", "commands.client.requiresRestart": "This change will take effect after you restart your client", "commands.client.tooFewArguments": "Too few arguments", + "commands.client.unknownFormattingCode": "Unknown formatting code", "chorusManip.landing.success": "Landing on: %d, %d, %d", "chorusManip.landing.failed": "Landing manipulation not possible", diff --git a/src/main/resources/clientcommands.aw b/src/main/resources/clientcommands.aw index 6fe0f89aa..53567ccab 100644 --- a/src/main/resources/clientcommands.aw +++ b/src/main/resources/clientcommands.aw @@ -23,6 +23,7 @@ accessible method net/minecraft/world/entity/projectile/FishingHook canHitEntity # chat accessible method net/minecraft/client/Minecraft openChatScreen (Ljava/lang/String;)V +accessible field net/minecraft/network/chat/TextColor CUSTOM_COLOR_PREFIX Ljava/lang/String; # ckit extendable method net/minecraft/client/gui/screens/inventory/EffectRenderingInventoryScreen renderEffects (Lnet/minecraft/client/gui/GuiGraphics;II)V