diff --git a/changelog.md b/changelog.md index 7d15fa3..85411f7 100644 --- a/changelog.md +++ b/changelog.md @@ -18,6 +18,7 @@ - Refactor StringTextUtils to TextUtils - Restructured the powerhouse `ChatHudMixin#modifyMessage(Text, boolean)` method to be more modular with message reconstruction - Moved the bulk of the `modifyMessage` method to ChatUtils to help development and greatly ease future troubleshooting + - Created a new `ChatUtils#getArg(..)` method to avoid the elusive `ClassCastException`s that kept getting thrown - Tweaked the `MessageHandlerMixin#cacheGameData` method to use built-in methods instead of rewriting the same thing - Removed the `VANILLA_MESSAGE` matcher in `ChatUtils` because it was redundant diff --git a/src/main/java/obro1961/chatpatches/ChatPatches.java b/src/main/java/obro1961/chatpatches/ChatPatches.java index 79f9aff..8aceab0 100644 --- a/src/main/java/obro1961/chatpatches/ChatPatches.java +++ b/src/main/java/obro1961/chatpatches/ChatPatches.java @@ -83,6 +83,25 @@ public void onInitializeClient() { } + /** + * Logs an error-level message telling the user to report + * the given error. The class and method of the caller is + * provided from a {@link StackWalker}. + *

+ * Outputs the following message: + *
+	 * [$class.$method] /!\ Please report this error on GitHub or Discord with the full log file attached! /!\
+	 * (error)
+	 * 
+ */ + public static void logInfoReportMessage(Throwable error) { + StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + String clazz = walker.getCallerClass().getSimpleName(); + String method = walker.walk(frames -> frames.skip(1).findFirst().orElseThrow().getMethodName()); + method = method.isBlank() ? error.getStackTrace()[0].getMethodName() : method; + LOGGER.error("[%s.%s] /!\\ Please report this error on GitHub or Discord with the full log file attached! /!\\".formatted(clazz, method), error); + } + /** * Creates a new Identifier using the ChatPatches mod ID. */ diff --git a/src/main/java/obro1961/chatpatches/config/Config.java b/src/main/java/obro1961/chatpatches/config/Config.java index 577278d..4c4df1e 100644 --- a/src/main/java/obro1961/chatpatches/config/Config.java +++ b/src/main/java/obro1961/chatpatches/config/Config.java @@ -119,28 +119,38 @@ public Style makeHoverStyle(Date when) { * @implNote {@code player} must reference a valid, existing * player entity and have both a valid name and UUID. */ - public MutableText formatPlayername(GameProfile player) { - // todo add some error handling here - PlayerEntity entity = MinecraftClient.getInstance().world.getPlayerByUuid(player.getId()); - Team team = entity.getScoreboard().getPlayerTeam(player.getName()); - Style style = entity.getDisplayName().getStyle().withColor( entity.getTeamColorValue() != 0xffffff ? entity.getTeamColorValue() : chatNameColor ); - - if(team != null) { - // note: doesn't set the style on every append, as it's already set in the parent text. might cause issues? - // if the player is on a team, add the prefix and suffixes from the config AND team (if they exist) to the formatted name - MutableText playername = text(player.getName()); - Text configPrefix = text(chatNameFormat.split("\\$")[0]); - Text configSuffix = text(chatNameFormat.split("\\$")[1] + " "); - - return Text.empty().setStyle(style) - .append(configPrefix) - .append(team.getPrefix()) - .append(playername) - .append(team.getSuffix()) - .append(configSuffix); - } else { - return makeObject(chatNameFormat, player.getName(), "", " ", style); + public MutableText formatPlayername(GameProfile profile) { + Style style = BLANK_STYLE.withColor(chatNameColor); + try { + PlayerEntity entity = MinecraftClient.getInstance().world.getPlayerByUuid(profile.getId()); + Team team = null; + + if(entity != null) { + team = entity.getScoreboard().getPlayerTeam(profile.getName()); + style = entity.getDisplayName().getStyle().withColor( entity.getTeamColorValue() != 0xffffff ? entity.getTeamColorValue() : chatNameColor ); + } + + if(team != null) { + // note: doesn't set the style on every append, as it's already set in the parent text. might cause issues? + // if the player is on a team, add the prefix and suffixes from the config AND team (if they exist) to the formatted name + MutableText playername = text(profile.getName()); + String[] configFormat = chatNameFormat.split("\\$"); + Text configPrefix = text(configFormat[0]); + Text configSuffix = text(configFormat[1] + " "); + + return Text.empty().setStyle(style) + .append(configPrefix) + .append(team.getPrefix()) + .append(playername) + .append(team.getSuffix()) + .append(configSuffix); + } + } catch(Exception e) { + LOGGER.error("[Config.formatPlayername] /!\\ An error occurred while trying to format '{}'s playername /!\\", profile.getName()); + ChatPatches.logInfoReportMessage(e); } + + return makeObject(chatNameFormat, profile.getName(), "", " ", style); } public MutableText makeDupeCounter(int dupes) { @@ -236,7 +246,9 @@ public static ConfigOption getOption(String key) { try { return new ConfigOption<>( (T)config.getClass().getField(key).get(config), (T)config.getClass().getField(key).get(DEFAULTS), key ); } catch(IllegalAccessException | NoSuchFieldException e) { - LOGGER.error("[Config.getOption({})] An error occurred while trying to get an option value, please report this on GitHub:", key, e); + LOGGER.error("[Config.getOption({})] An error occurred while trying to get an option value!", key); + ChatPatches.logInfoReportMessage(e); + return new ConfigOption<>( (T)new Object(), (T)new Object(), key ); } } diff --git a/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java b/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java index 6cb13c2..9306fa0 100644 --- a/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java +++ b/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java @@ -11,11 +11,7 @@ import net.minecraft.client.gui.hud.ChatHudLine; import net.minecraft.client.gui.hud.MessageIndicator; import net.minecraft.client.util.CommandHistoryManager; -import net.minecraft.text.MutableText; -import net.minecraft.text.Style; import net.minecraft.text.Text; -import net.minecraft.text.TranslatableTextContent; -import net.minecraft.util.Util; import net.minecraft.util.math.MathHelper; import obro1961.chatpatches.ChatPatches; import obro1961.chatpatches.accessor.ChatHudAccessor; @@ -33,12 +29,9 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.ArrayList; -import java.util.Date; import java.util.List; import static obro1961.chatpatches.ChatPatches.config; -import static obro1961.chatpatches.ChatPatches.msgData; import static obro1961.chatpatches.util.ChatUtils.MESSAGE_INDEX; import static obro1961.chatpatches.util.ChatUtils.getPart; @@ -134,16 +127,10 @@ private double moveINDHoverText(double e) { /** * Modifies the incoming message by adding timestamps, nicer * player names, hover events, and duplicate counters in conjunction with - * {@link #addCounter(Text, boolean)} - * - * @implNote - *
  • Doesn't modify when {@code refreshing} is true, as that signifies - * re-rendering of chat messages on the hud.
  • - *
  • This method causes all messages passed to it to be formatted in - * a new structure for clear data access. This is mostly done using - * {@link MutableText#append(Text)}, which deliberately puts message - * components at specific indices, all of which are laid out in - * {@link ChatUtils}.
  • + * {@link #addCounter(Text, boolean)}. + *
    + * See {@link ChatUtils#modifyMessage(Text, boolean)} for detailed + * implementation specifications. */ @ModifyVariable( method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", @@ -151,86 +138,7 @@ private double moveINDHoverText(double e) { argsOnly = true ) private Text modifyMessage(Text m, @Local(argsOnly = true) boolean refreshing) { - if( refreshing || Flags.LOADING_CHATLOG.isRaised() ) - return addCounter(m, refreshing); // cancels modifications when loading the chatlog or regenerating visibles - - Style style = m.getStyle(); - boolean lastEmpty = msgData.equals(ChatUtils.NIL_MSG_DATA); - boolean boundary = Flags.BOUNDARY_LINE.isRaised() && config.boundary && !config.vanillaClearing; - Date now = lastEmpty ? new Date() : msgData.timestamp(); - String nowStr = String.valueOf( now.getTime() ); // for copy menu and storing timestamp data! only affects the timestamp - -//ChatPatches.LOGGER.warn("received {} message: '{}'", m.getContent().getClass().getSimpleName(), m.getString()); - - MutableText timestamp = (config.time && !boundary) ? config.makeTimestamp(now).setStyle( config.makeHoverStyle(now) ) : Text.empty().styled(s -> s.withInsertion(nowStr)); - MutableText content = Text.empty().setStyle(style); - - // reconstruct the player message if it's in the vanilla format and it should be reformatted - if(!lastEmpty && !boundary && msgData.vanilla()) { - // if the message is translatable, then we know exactly where everything is - if(m.getContent() instanceof TranslatableTextContent ttc && ttc.getKey().matches("chat.type.(text|team.(text|sent))")) { - String key = ttc.getKey(); - - // adds the team name for team messages - if(key.startsWith("chat.type.team.")) { - MutableText teamPart = Text.empty(); - // adds the preceding arrow for sent team messages - if(key.endsWith("sent")) - teamPart.append(Text.literal("-> ").setStyle(style)); - - // adds the team name for team messages - teamPart.append( ((MutableText)ttc.getArg(0)).append(" ") ); - - content.append(teamPart); - } else { - content.append(""); // if there isn't a team message, add an empty string to keep the index constant - } - - // adds the formatted playername and content for all message types - content.append(config.formatPlayername(msgData.sender())); // sender data is already known - content.append((Text) ttc.getArg(ttc.getArgs().length - 1)); // always at the end - } else { // reconstructs the message if it matches the vanilla format '<%s> %s' but isn't translatable - // collect all message parts into one list, including the root TextContent - // (assuming this accounts for all parts, TextContents, and siblings) - List parts = Util.make(new ArrayList<>(m.getSiblings().size() + 1), a -> { - if(!m.equals(Text.EMPTY)) - a.add( m.copyContentOnly().setStyle(style) ); - - a.addAll( m.getSiblings() ); - }); - - MutableText realContent = Text.empty(); - // find the index of the end of a '<%s> %s' message - Text firstPart = parts.stream().filter(p -> p.getString().contains(">")).findFirst() - .orElseThrow(() -> new IllegalStateException("No closing angle bracket found in vanilla message '" + m.getString() + "' !")); - String afterEndBracket = firstPart.getString().split(">")[1]; // just get the part after the closing bracket, we know the start - - // adds the part after the closing bracket but before any remaining siblings, if it exists - if(!afterEndBracket.isEmpty()) - realContent.append( Text.literal(afterEndBracket).setStyle(firstPart.getStyle()) ); - - // we know everything remaining is message content parts, so add everything - for(int i = parts.indexOf(firstPart) + 1; i < parts.size(); i++) - realContent.append(parts.get(i)); - - content.append(config.formatPlayername(msgData.sender())); // sender data is already known - content.append(realContent); // adds the reconstructed message content - } -//ChatPatches.LOGGER.warn("DID!!! reformat, content: '{}' aka '{}'+{}", content.getString(), content.copyContentOnly().getString(), content.getSiblings());//delete:- - } else { - // don't reformat if it isn't vanilla or needed - content = m.copy(); -//ChatPatches.LOGGER.warn("didn't reformat, content: '{}' aka '{}'+{}", content.getString(), content.copyContentOnly().getString(), content.getSiblings());//delete:- - } - - // assembles constructed message and adds a duplicate counter according to the #addCounter method - Text modified = addCounter( ChatUtils.buildMessage(style, timestamp, content, null), false ); -/*ChatPatches.LOGGER.info("------ parts of message ------\n\ttimestamp: '{}'\n\tmessage: ('{}')\n\t\tteam: '{}'\n\t\tsender: '{}'\n\t\tcontent: '{}'\n\tdupe: '{}'\n------", -ChatUtils.getPart(modified, 0), ChatUtils.getPart(modified, 1), ChatUtils.getMsgPart(modified, 0), ChatUtils.getMsgPart(modified, 1), ChatUtils.getMsgPart(modified, 2), ChatUtils.getPart(modified, 2) -);*/ //delete:- - ChatLog.addMessage(modified); - msgData = ChatUtils.NIL_MSG_DATA; // fixes messages that get around MessageHandlerMixin's data caching, usually thru ChatHud#addMessage (ex. open-to-lan message) - return modified; + return addCounter(ChatUtils.modifyMessage(m, refreshing), refreshing); } @Inject(method = "addToMessageHistory", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/collection/ArrayListDeque;size()I")) @@ -311,10 +219,11 @@ private Text addCounter(Text incoming, boolean refreshing) { } } catch(IndexOutOfBoundsException e) { ChatPatches.LOGGER.error("[ChatHudMixin.addCounter] Couldn't add duplicate counter because message '{}' ({} parts) was not constructed properly.", incoming.getString(), incoming.getSiblings().size()); - ChatPatches.LOGGER.error("[ChatHudMixin.addCounter] This could have also been caused by an issue with the new CompactChat dupe-condensing method."); - ChatPatches.LOGGER.error("[ChatHudMixin.addCounter] Either way, this was caused by a bug or mod incompatibility. Please report this on GitHub or on the Discord!", e); + ChatPatches.LOGGER.error("[ChatHudMixin.addCounter] This could have also been caused by an issue with the new CompactChat dupe-condensing method. Either way,"); + ChatPatches.logInfoReportMessage(e); } catch(Exception e) { - ChatPatches.LOGGER.error("[ChatHudMixin.addCounter] /!\\ Couldn't add duplicate counter because of an unexpected error. Please report this on GitHub or on the Discord! /!\\", e); + ChatPatches.LOGGER.error("[ChatHudMixin.addCounter] /!\\ Couldn't add duplicate counter because of an unexpected error! /!\\"); + ChatPatches.logInfoReportMessage(e); } return incoming; diff --git a/src/main/java/obro1961/chatpatches/util/ChatUtils.java b/src/main/java/obro1961/chatpatches/util/ChatUtils.java index 3d1865c..6bf1226 100644 --- a/src/main/java/obro1961/chatpatches/util/ChatUtils.java +++ b/src/main/java/obro1961/chatpatches/util/ChatUtils.java @@ -5,21 +5,20 @@ import net.minecraft.client.gui.hud.ChatHud; import net.minecraft.client.gui.hud.ChatHudLine; import net.minecraft.client.util.ChatMessages; -import net.minecraft.text.MutableText; -import net.minecraft.text.OrderedText; -import net.minecraft.text.Style; -import net.minecraft.text.Text; +import net.minecraft.text.*; +import net.minecraft.util.Util; import net.minecraft.util.math.MathHelper; +import obro1961.chatpatches.ChatPatches; import obro1961.chatpatches.accessor.ChatHudAccessor; +import obro1961.chatpatches.chatlog.ChatLog; import obro1961.chatpatches.mixin.gui.ChatHudMixin; +import org.jetbrains.annotations.NotNull; import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.UUID; +import java.util.*; import static obro1961.chatpatches.ChatPatches.config; +import static obro1961.chatpatches.ChatPatches.msgData; import static obro1961.chatpatches.util.TextUtils.copyWithoutContent; import static obro1961.chatpatches.util.TextUtils.reorder; @@ -56,6 +55,40 @@ public static Text getMsgPart(Text message, int index) { return getPart(getPart(message, MESSAGE_INDEX), index); } + /** + * Returns a MutableText object representing the argument + * located at the given index of the given + * {@link TranslatableTextContent}. Needed because of a weird + * phenomenon where the {@link TranslatableTextContent#getArg(int)} + * method can return a {@link String} or other non-Text related + * object, which otherwise causes {@link ClassCastException}s. + *

    + * Wraps {@link String}s in {@link Text#literal(String)} + * and nulls in {@link Text#empty()}. + * + * @implNote + * If {@code index} is negative, adds it to the args array + * length. In other words, passing index {@code -n} will + * get the {@code content.getArgs().length-n}th argument. + */ + public static MutableText getArg(TranslatableTextContent content, int index) { + if(index < 0) + index = content.getArgs().length + index; + + Object /* StringVisitable */ arg = content.getArg(index); + + if(arg == null) + return Text.empty(); + else if(arg instanceof Text t) + return (MutableText) t; + else if(arg instanceof StringVisitable sv) + return Text.literal(sv.getString()); + else if(arg instanceof String s) + return Text.literal(s); + else + return Text.empty(); + } + /** * Builds a chat message from the given components. * If anything is {@code null}, it is replaced with @@ -85,19 +118,142 @@ public static MutableText buildMessage(Style rootStyle, Text first, Text second, } /** - * todo doc AFTER todo moving impl here + * Reformats the incoming message {@code m} according to configured + * settings, message data, and at indices specified in this class. + * This method is used in the {@link ChatHudMixin#modifyMessage(Text, boolean)} + * mixin for functionality. + * + * @implNote + *

      + *
    1. Don't modify when {@code refreshing} is true, as that signifies + * re-rendering chat messages, so simply return {@code m}.
    2. + *
    3. Declare relevant variables, most notably the {@code timestamp} + * and {@code content} components.
    4. + *
    5. Reconstruct the player message if it should be reformatted + * (message has player data, not a boundary line, and in vanilla + * format): + *
        + *
      1. If the message is translatable and in a known format: + *
          + *
        1. If the message is a team message, add all related + * team message components.
        2. + *
        3. Add the formatted playername and content.
        4. + *
        + *
      2. + *
      3. Otherwise, the message must be in an unknown format where all + * we know for sure is the format ({@code <$name> $message}): + *
          + *
        1. Collect all message components into a list, including the + * root {@link TextContent} (assuming this accounts for all parts, + * {@link TextContent}s, and siblings).
        2. + *
        3. Find the first part that contains a '>'.
        4. + *
        5. Add the part after the '>' but before any + * remaining siblings, if it exists, to the {@code realContent} + * local Text variable (with the proper Style).
        6. + *
        7. Add every part succeeding the '>' component to + * the {@code realContent} variable.
        8. + *
        9. Add the formatted playername and {@code realContent} + * variable to the actual content.
        10. + *
        + *
      4. + *
      + *
    6. + *
    7. If the message shouldn't be formatted (doesn't satisfy all + * prerequisites), then don't change {@code m} and store it.
    8. + *
    9. Assemble the constructed message and add a duplicate counter + * according to the {@link ChatHudMixin#addCounter(Text, boolean)} method.
    10. + *
    11. Log the modified message in the {@code ChatLog}.
    12. + *
    13. Reset the {@link ChatPatches#msgData} to prevent an uncommon bug.
    14. + *
    15. Return the modified message, regardless of if it was
    16. + *
    */ - public static Text modifyMessage(Text message, boolean vanilla) { - // early if-return checks + public static Text modifyMessage(@NotNull Text m, boolean refreshing) { + if( refreshing || Flags.LOADING_CHATLOG.isRaised() ) + return m; // cancels modifications when loading the chatlog or regenerating visibles + + boolean lastEmpty = msgData.equals(ChatUtils.NIL_MSG_DATA); + boolean boundary = Flags.BOUNDARY_LINE.isRaised() && config.boundary && !config.vanillaClearing; + Date now = lastEmpty ? new Date() : msgData.timestamp(); + String nowStr = String.valueOf(now.getTime()); // for copy menu and storing timestamp data! only affects the timestamp + Style style = m.getStyle(); - // assign variables + MutableText timestamp = null; + MutableText content = m.copy(); - // declare message parts - // if TranslatableTextContent and known keys, store pre-formatted parts instantly - // else do typical formatting stuff (except optimize it to make it actually work and not ugly) + try { + timestamp = (config.time && !boundary) ? config.makeTimestamp(now).setStyle( config.makeHoverStyle(now) ) : Text.empty().styled(s -> s.withInsertion(nowStr)); + content = Text.empty().setStyle(style); + + // reconstruct the player message if it's in the vanilla format and it should be reformatted + if(!lastEmpty && !boundary && msgData.vanilla()) { + // if the message is translatable, then we know exactly where everything is + if(m.getContent() instanceof TranslatableTextContent ttc && ttc.getKey().matches("chat.type.(text|team.(text|sent))")) { + String key = ttc.getKey(); + + // adds the team name for team messages + if(key.startsWith("chat.type.team.")) { + MutableText teamPart = Text.empty(); + // adds the preceding arrow for sent team messages + if(key.endsWith("sent")) + teamPart.append(Text.literal("-> ").setStyle(style)); + + // adds the team name for team messages + teamPart.append(getArg(ttc, 0).append(" ")); + + content.append(teamPart); + } else { + content.append(""); // if there isn't a team message, add an empty string to keep the index constant + } + + // adds the formatted playername and content for all message types + content.append(config.formatPlayername(msgData.sender())); // sender data is already known + content.append(getArg(ttc, -1)); // always at the end + } else { // reconstructs the message if it matches the vanilla format '<%s> %s' but isn't translatable + // collect all message parts into one list, including the root TextContent + // (assuming this accounts for all parts, TextContents, and siblings) + List parts = Util.make(new ArrayList<>(m.getSiblings().size() + 1), a -> { + if(!m.equals(Text.EMPTY)) + a.add( m.copyContentOnly().setStyle(style) ); + + a.addAll( m.getSiblings() ); + }); + + MutableText realContent = Text.empty(); + // find the first index of a '>' in the message, is formatted like '<%s> %s' + Text firstPart = parts.stream().filter(p -> p.getString().contains(">")).findFirst() + .orElseThrow(() -> new IllegalStateException("No closing angle bracket found in vanilla message '" + m.getString() + "' !")); + String afterEndBracket = firstPart.getString().split(">")[1]; // just get the part after the closing bracket, we know the start + + // ignore everything before the '>' because it's the playername, which we already know + // adds the part after the closing bracket but before any remaining siblings, if it exists + if(!afterEndBracket.isEmpty()) + realContent.append( Text.literal(afterEndBracket).setStyle(firstPart.getStyle()) ); + + // we know everything remaining is message content parts, so add everything + for(int i = parts.indexOf(firstPart) + 1; i < parts.size(); i++) + realContent.append(parts.get(i)); + + content.append(config.formatPlayername(msgData.sender())); // sender data is already known + content.append(realContent); // adds the reconstructed message content + } + } else { + // don't reformat if it isn't vanilla or needed + content = m.copy(); + } + } catch(Throwable e) { + ChatPatches.LOGGER.error("[ChatUtils.modifyMessage] An error occurred while modifying message '{}', returning original:", m.getString()); + ChatPatches.LOGGER.debug("[ChatUtils.modifyMessage] \tOriginal message structure: {}", m); + ChatPatches.LOGGER.debug("[ChatUtils.modifyMessage] \tModified message structure:"); + ChatPatches.LOGGER.debug("[ChatUtils.modifyMessage] \t\tTimestamp structure: {}", timestamp); + ChatPatches.LOGGER.debug("[ChatUtils.modifyMessage] \t\tContent structure: {}", content); + ChatPatches.logInfoReportMessage(e); + } - // final cleanup and logging - return message; + // assembles constructed message and adds a duplicate counter according to the #addCounter method + Text modified = ChatUtils.buildMessage(style, timestamp, content, null); + ChatLog.addMessage(modified); + msgData = ChatUtils.NIL_MSG_DATA; // fixes messages that get around MessageHandlerMixin's data caching, usually thru ChatHud#addMessage (ex. open-to-lan message) + return modified; } /**