diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/ISSUE_TEMPLATE/bug-report--latest-mod-versions-only-.md b/.github/ISSUE_TEMPLATE/bug-report--latest-mod-versions-only-.md index ec02b14..f9fb69d 100644 --- a/.github/ISSUE_TEMPLATE/bug-report--latest-mod-versions-only-.md +++ b/.github/ISSUE_TEMPLATE/bug-report--latest-mod-versions-only-.md @@ -1,7 +1,7 @@ --- name: Bug report (latest versions ONLY) about: Create a bug report to get the bug fixed as soon as possible. -title: Bug summary goes here - Affected mod version(s) +title: Bug summary - Mod version - MC version labels: bug assignees: '' @@ -11,25 +11,24 @@ assignees: '' As detailed and descriptive as you can make it. If possible, attach photos and/or videos. ### How to reproduce -An ordered list of steps of how and where you experienced the bug. +An ordered list of steps of how and where you experienced the bug; include what you were doing before, side effects, etc. ### Expected result Exactly what you expected to happen, in detail. ### Log file link -Don't paste the whole log here!! Just copy and paste it into a site like `mclo.gs` or `pastebin.com`. Logs are located in the Minecraft directory folder, under `/logs` and sometimes `/crash-reports`. +Don't paste the whole log here!! Just copy and paste it into https://mclo.gs, or if your launcher supports it just upload and paste the copied link. Logs are located in your Minecraft directory under `/logs` and `/crash-reports`. ### Specs and Details Required information: - Computer OS - Mod version -- Fabric and API versions -- YetAnotherConfigLib and Mod Menu versions (if installed) -- List of all installed mods (can be a list *copied* from a log file **OR** a screenshot of the mods folder) +- Fabric, Fabric API, and YetAnotherConfigLib versions +- Mod Menu / Catalogue and Menulogue versions (if installed) Optional but suggested information: - `chatpatches.json` file (if pasted use the format: \`\`\`json\n\\n\`\`\`) -- `chatlog.json` file (DO NOT PASTE) +- `chatlog.json` file (DO NOT PASTE, put a link using a site like https://pastebin.com) ### Additional context -Anything else that could possibly be relevant to this issue. For example, what happened leading up to the incident. +Anything else that could possibly be relevant to this issue. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c82e8d4..2971bd3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,25 +21,26 @@ jobs: strategy: matrix: java: [ - 17 # Current Java LTS & minimum supported by Minecraft - #,21 # Latest Java LTS + 21 # Latest Java LTS & minimum supported by Minecraft ] - # and run on the latest dists of Linux, MacOs, and Windows + # and run on the latest dists of Linux, MacOS, and Windows os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - name: checkout repository uses: actions/checkout@v4 - - name: validate gradle wrapper - uses: gradle/wrapper-validation-action@v2 - - name: setup jdk ${{ matrix.java }} uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} distribution: temurin + - name: setup gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: wrapper + - name: make gradle wrapper executable if: ${{ runner.os != 'Windows' }} run: chmod +x ./gradlew @@ -48,11 +49,11 @@ jobs: run: ./gradlew build --info --stacktrace - name: capture build artifacts - if: ${{ runner.os == 'Linux' && matrix.java == '17' }} # Only upload artifacts built from latest java on one OS + if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS uses: actions/upload-artifact@v4 with: name: jars path: | build/libs/ build/devlibs/ - !build/devlibs/*-sources.jar + !build/devlibs/*-sources.jar \ No newline at end of file diff --git a/README.md b/README.md index 409cf84..0910b7c 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Once you contribute, [join the Discord server](https://discord.gg/3MqBvNEyMz) so | Save interval (minutes) | `0` | How long should the Chat Log wait before saving to disk? This is in minutes, the minimum is 1. Set to 0 to only save when paused (warning: setting to <5 will lag a lot). All values try to save on exit. | `text.chatpatches.chatlogSaveInterval` | | Ignore hide message packet | `true` | Should hide message packets that delete chat messages be ignored? | `text.chatpatches.chatHidePacket` | | Override chat width | `0` | The width of the chat box. This overrides vanilla's default and allows for a much larger width. Set to 0 to use the vanilla setting and not override it. | `text.chatpatches.chatWidth` | +| Override chat height | `0` | The width of the chat box. This overrides vanilla's default and allows for a much larger height. Set to 0 to use the vanilla setting and not override it. | `text.chatpatches.chatHeight` | | Maximum chat messages | `16384` | The max amount of chat messages allowed to save. Vanilla caps it at 100, this mod can increase it up to 32,767. Keep in mind a higher max equals higher memory usage. | `text.chatpatches.chatMaxMessages` | | Playername text | `"<$>"` | The text that replaces the playername in chat messages. Vanilla is '<$>', name only is '$'; where '$' is a placeholder for the playername. Only applies to player-sent messages. | `text.chatpatches.chatNameFormat` | | Playername color | `0xffffff` | The color that's filled in where it would otherwise be blank white in the resulting formatted playername. To use this with other formatting modifiers, use '&r' in the decoration text option. | `text.chatpatches.chatNameColor` | @@ -188,7 +189,24 @@ https://github.com/mrbuilder1961/ChatPatches/actions/runs/8310511511/artifacts/1 7. Now you should be good to go! Launching the game should now load the beta version. If you experience any issues, make sure to report them as soon as possible wherever you were given the link (here on GitHub or [the Discord](https://discord.gg/3MqBvNEyMz)). +### How to find mod(s) causing compatibility issues (binary search) +*(taken from the Fabric Discord bot, not claiming ownership, just sharing knowledge!)* + +> A binary search can be used to quickly find a specific mod causing trouble, which can be especially useful when logs don't give a conclusive answer to your issue. + +> Start by removing or disabling half of your mods, then test if the problem still occurs. If it does, remove half of the remaining mods and test again. If it doesn't, add back half of the mods you just removed. + +> Keep in mind you don't have to stick strictly to halves each time, and may have to enable some library mods like Fabric API out of order. + +> By repeating this on an increasingly smaller set of mods, you'll find the problematic mod within a few iterations. + +Make sure you leave Chat Patches and it's dependencies installed when you're looking for the problematic mod, otherwise it won't do anything to help diagnose the issue. + ## Sponsor me! - Ko-Fi: https://ko-fi.com/obro1961 -[![15% off your first month with code OBRO15!](https://i.imgur.com/9mjs77B.png)](https://billing.kinetichosting.net/aff.php?aff=234) \ No newline at end of file +[![15% off your first month with code OBRO15!](https://i.imgur.com/9mjs77B.png)](https://billing.kinetichosting.net/aff.php?aff=234) + +### (not sponsored; highly encouraged) +[![Use your voice, help the families of Gaza](https://github.com/user-attachments/assets/111407d3-0018-4ac9-b9db-2e515e2e54a0) +](https://linktr.ee/opolivebranch) diff --git a/build.gradle b/build.gradle index 5bd5253..076accc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,13 @@ import groovy.json.JsonSlurper -import me.modmuss50.mpp.ReleaseType import java.util.function.BiFunction import java.util.function.Function import java.util.stream.Stream plugins { - id "fabric-loom" version "1.6-SNAPSHOT" + id "fabric-loom" version "1.7-SNAPSHOT" id "maven-publish" - id "me.modmuss50.mod-publish-plugin" version "0.5.1" + id "me.modmuss50.mod-publish-plugin" version "0.7.4" } group = project.group @@ -19,8 +18,8 @@ base { } // global publishing variables - shouldPublish defaults to true, but can be set to false by using `-PshouldPublish=false` -boolean shouldPublish = Boolean.parseBoolean( providers.gradleProperty("shouldPublish").getOrElse("true") ) -String changelogText = "Changelog could not be found... 😬" +def shouldPublish = Boolean.parseBoolean( providers.gradleProperty("shouldPublish").getOrElse("true") ) +def changelogText = "Changelog could not be found... 😬" repositories { mavenCentral() @@ -50,14 +49,14 @@ processResources { } */ } - File changelog = file("changelog.md") + def changelog = file("changelog.md") if( changelog.exists() ) { // replaces github issue numbers with links file("changelog.md").text = changelog.text.replaceAll("##(\\d+)", "[#\$1](https://www.github.com/mrbuilder1961/ChatPatches/issues/\$1)") // hackily gets the first changelog entry - String newEntryTitle = "## Chat Patches `" + version + "`" - int prevEntryIndex = file("changelog.md").text.replaceFirst(newEntryTitle, "").indexOf("## Chat Patches `") + newEntryTitle.length() - 2 + def newEntryTitle = "## Chat Patches `" + version + "`" + def prevEntryIndex = file("changelog.md").text.replaceFirst(newEntryTitle, "").indexOf("## Chat Patches `") + newEntryTitle.length() - 2 changelogText = file("changelog.md").text.substring(0, prevEntryIndex).replaceFirst("# Changelog\\s+", "") @@ -70,22 +69,28 @@ processResources { } println() } + } + + if(changelogText.length() > 2000) { + def more = "..\n***.. and more! Check out the full release notes on GitHub!***" + changelogText = changelogText.substring(0, 2000 - more.length()).stripTrailing() + more + println "Trimmed changelog, was over 2000 characters" } if(!shouldPublish) - println "Using changelog text:\n>${changelogText}<" + println "Using changelog text:\n>$changelogText<" } tasks.withType(JavaCompile).configureEach { - it.options.release = 17 + it.options.release = 21 } java { withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } jar { @@ -96,7 +101,8 @@ jar { publishing { publications { - mavenJava(MavenPublication) { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name from components.java } } @@ -112,16 +118,17 @@ publishMods { Function> propListGetter = (String propertyName) -> rootProject[propertyName] != null ? Stream.of( rootProject[propertyName].toString().split(",") ).filter{!it.isBlank()}.toList() : new ArrayList<>() BiFunction tokenGetter = (String tokenName, boolean valid) -> - valid && secrets.exists() ? Objects.requireNonNullElse(new JsonSlurper().parse (secrets)[tokenName], "-") : "-" + valid && secrets.exists() ? Objects.requireNonNullElse(new JsonSlurper().parse(secrets)[tokenName], "-") : "-" + // gradle.properties vars - ReleaseType phase = ReleaseType.valueOf( rootProject.phase.toString().toUpperCase() ) + String phase = rootProject.phase.toString().toLowerCase() List loaders = propListGetter.apply("loaders") // Arrays.asList( rootProject.loaders.toString().split(",") ) List targets = propListGetter.apply("targets") // Arrays.asList( rootProject.targets.toString().split(",") ) String[] required = propListGetter.apply("required").toArray() String[] optionals = propListGetter.apply("optionals").toArray() String[] incompatibles = propListGetter.apply("incompatibles").toArray() String[] embedded = propListGetter.apply("embedded").toArray() - String branch = targets.get(targets.size() - 1) // last target is the branch + String branch = rootProject.branch.toString() // tokens - token name and if it should return the token or "-" String cfToken = tokenGetter.apply("curseforge", shouldPublish) String mrToken = tokenGetter.apply("modrinth", shouldPublish) @@ -133,7 +140,7 @@ publishMods { displayName = v.toString() file = remapJar.archiveFile // Forge would use jar (NeoForge? i think so..) changelog = changelogText - type = phase + type = (phase.matches("release|stable") ? STABLE : phase.matches("beta") ? BETA : ALPHA) modLoaders = loaders dryRun = !shouldPublish @@ -151,6 +158,7 @@ publishMods { projectSlug = archives_base_name minecraftVersions.addAll(targets) + clientRequired = true requires(required) optional(optionals) diff --git a/changelog.md b/changelog.md index 26db212..478b4ac 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # Changelog +## Chat Patches `204.6.10` for Minecraft 1.20.4 on Fabric, Quilt +- Synced `213.6.0` to 1.20.4 + ## Chat Patches `204.6.5` for Minecraft 1.20.4 on Fabric, Quilt - Fixed certain messages from showing up blank and logging `ArrayIndexOutOfBoundsException`s ([#156](https://www.github.com/mrbuilder1961/ChatPatches/issues/146)) - Fixed normal duplicate messages not having a counter (1.20.4 only, [#157](https://www.github.com/mrbuilder1961/ChatPatches/issues/157)) @@ -248,4 +251,4 @@ This update officially fixes [#4](https://www.github.com/mrbuilder1961/ChatPatch ## (TEMPLATE) Chat Patches `{version}` for Minecraft {targets} on {loaders} - Changes go here -Typically as a list but not required! +Typically as a list but not required! \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 4154be8..c86197d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,23 +5,26 @@ org.gradle.parallel=true # Mod properties archives_base_name=chatpatches group=obro1961.chatpatches -version=204.6.5 +version=204.6.10 cfId=560042 mrId=MOqt4Z5n # Required dependencies - https://fabricmc.net/develop minecraft=1.20.4 yarn=+build.3 -loader=0.15.11 -api=0.97.0+1.20.4 +loader=0.16.9 +api=0.97.2+1.20.4 # Dependencies - modmenu: https://modrinth.com/mod/modmenu/versions?l=fabric and yacl: https://modrinth.com/mod/yacl/versions?l=fabric modmenu=9.0.0 -yacl=3.4.2+1.20.4 +yacl=3.5.0+1.20.4 -# Publishing metadata - phase can be ALPHA, BETA, or STABLE +# Publishing metadata +# /- Typically the latest version, but can be 1.18.x for example +branch=1.20.4 targets=1.20.4 loaders=Fabric,Quilt +# /- ALPHA, BETA, or STABLE phase=STABLE # relations required=fabric-api,yacl diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 81c736b..9355b41 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fcb6fca..f5feea6 100644 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,7 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/src/main/java/obro1961/chatpatches/ChatPatches.java b/src/main/java/obro1961/chatpatches/ChatPatches.java index 8aceab0..6694930 100644 --- a/src/main/java/obro1961/chatpatches/ChatPatches.java +++ b/src/main/java/obro1961/chatpatches/ChatPatches.java @@ -1,7 +1,9 @@ package obro1961.chatpatches; +import com.google.gson.JsonElement; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; @@ -13,18 +15,15 @@ import obro1961.chatpatches.chatlog.ChatLog; import obro1961.chatpatches.config.Config; import obro1961.chatpatches.util.ChatUtils; -import obro1961.chatpatches.util.Flags; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.Objects; -import java.util.function.Supplier; public class ChatPatches implements ClientModInitializer { - public static final Supplier TIME_FORMATTER = () -> new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); - public static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger("Chat Patches"); public static final String MOD_ID = "chatpatches"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); public static Config config = Config.create(); /** Contains the sender and timestamp data of the last received chat message. */ @@ -32,101 +31,127 @@ public class ChatPatches implements ClientModInitializer { private static String lastWorld = ""; + /** + * Returns a {@code chatpatches:${path}} + * {@link Identifier}. + */ + public static Identifier id(String path) { + // unfortunately this method in 1.20.6 is method_43902 + // but in 1.21 it's method_60655, making it incompatible + // this is grinding my gears bc the code is identical ToT + return Identifier.of(MOD_ID, path); + } + @Override public void onInitializeClient() { /* * ChatLog saving events, run if config.chatlog is true: - * CLIENT_STOPPING - Always saves - * SCREEN_AFTER_INIT - Saves if there is no save interval AND if the screen is the OptionsScreen (paused) - * START_CLIENT_TICK - Ticks the save counter, saves if the counter is 0, resets if <0 - * MinecraftClientMixin#saveChatlogOnCrash - Always saves + * DISCONNECT - Always saves EXCEPT on (most?) server crashes + * SCREEN_AFTER_INIT - Saves if the save interval is enabled AND if the screen is paused (GameMenuScreen) + * END_WORLD_TICK - Ticks the save counter and saves if it's enabled and the internal counter equals zero */ - ClientLifecycleEvents.CLIENT_STOPPING.register(client -> ChatLog.serialize(false)); + + + // according to my testing, this event works as needed when the game disconnects and on crashes if the game is functional at that point + // testing details (server=hypixel): normal disconnects work on both world and server, manual F3+C crash works on world but NOT server + // honestly I don't care if it fails on crashes, its fixable a) through the save interval or b) by fixing the crash's source + ClientPlayConnectionEvents.DISCONNECT.register((network, client) -> ChatLog.serialize()); ScreenEvents.AFTER_INIT.register((client, screen, sW, sH) -> { - // saves the chat log if [the save interval is 0] AND [the pause menu is showing OR the game isn't focused] + // saves the chat log if [the save interval is disabled] AND [the pause menu is showing OR the game isn't focused] if( config.chatlogSaveInterval == 0 && (screen instanceof GameMenuScreen || !client.isWindowFocused()) ) - ChatLog.serialize(false); + ChatLog.serialize(); }); - ClientTickEvents.START_CLIENT_TICK.register(client -> ChatLog.tickSaveCounter()); + ClientTickEvents.END_WORLD_TICK.register(world -> ChatLog.tickSaveCounter()); // registers the cached message file importer and boundary sender ClientPlayConnectionEvents.JOIN.register((network, packetSender, client) -> { - if(config.chatlog && !ChatLog.loaded) { + if(!ChatLog.loaded && config.chatlog) { ChatLog.deserialize(); ChatLog.restore(client); } - ChatHudAccessor chatHud = ChatHudAccessor.from(client); + //prepub move all this to Config or sm? feels out of place... + ChatHudAccessor chat = (ChatHudAccessor) client.inGameHud.getChatHud(); String current = currentWorldName(client); // continues if the boundary line is enabled, >0 messages sent, and if the last and current worlds were servers, that they aren't the same - if( config.boundary && !chatHud.chatpatches$getMessages().isEmpty() && (!current.startsWith("S_") || !lastWorld.startsWith("S_") || !current.equals(lastWorld)) ) { + if( config.boundary && !config.vanillaClearing && !chat.chatpatches$getMessages().isEmpty() && (!current.startsWith("S_") || !lastWorld.startsWith("S_") || !current.equals(lastWorld)) ) { try { String levelName = (lastWorld = current).substring(2); // makes a variable to update lastWorld in a cleaner way + boolean time = config.time; - Flags.BOUNDARY_LINE.raise(); + config.time = false; // disables the time so the boundary line doesn't have a timestamp client.inGameHud.getChatHud().addMessage( config.makeBoundaryLine(levelName) ); - Flags.BOUNDARY_LINE.lower(); - + config.time = time; // re-enables the time accordingly } catch(Exception e) { LOGGER.warn("[ChatPatches.boundary] An error occurred while adding the boundary line:", e); } } // sets all messages (restored and boundary line) to a addedTime of 0 to prevent instant rendering (#42) - if(ChatLog.loaded && Flags.INIT.isRaised()) { - chatHud.chatpatches$getVisibleMessages().replaceAll(ln -> new ChatHudLine.Visible(0, ln.content(), ln.indicator(), ln.endOfEntry())); - Flags.INIT.lower(); - } + // only replaces messages that would render instantly to save performance on large chat logs + // no longer ran once per game, but once per join (#151) (note: if you open the chat and then close it, the messages will reappear) + int t = client.inGameHud.getTicks(); + chat.chatpatches$getVisibleMessages().replaceAll(ln -> (t - ln.addedTime() < 200) ? new ChatHudLine.Visible(0, ln.content(), ln.indicator(), ln.endOfEntry()) : ln); }); LOGGER.info("[ChatPatches()] Finished setting up!"); } + /** + * Returns the current ClientWorld's name. For singleplayer, + * returns the level name. For multiplayer, returns the + * server entry name. Falls back on the IP if it was + * direct-connect. Leads with "C_" or "S_" depending + * on the source of the ClientWorld. + * @param client A non-null MinecraftClient that must be in-game. + * @return (C or S) + "_" + (current world name) + */ + @SuppressWarnings("DataFlowIssue") // getServer and getCurrentServerEntry are not null if isIntegratedServerRunning is true + public static String currentWorldName(@NotNull MinecraftClient client) { + Objects.requireNonNull(client, "MinecraftClient must exist to access client data:"); + String entryName; + + return client.isIntegratedServerRunning() + ? "C_" + client.getServer().getSaveProperties().getLevelName() + : (entryName = client.getCurrentServerEntry().name) == null || entryName.isBlank() // check if null/empty then use IP + ? "S_" + client.getCurrentServerEntry().address + : "S_" + client.getCurrentServerEntry().name + ; + } /** * 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: + * + *

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) { + public static void logReportMsg(@NotNull X 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; + + if(method.isBlank()) + method = error.getStackTrace()[0].getMethodName(); + 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. + * Executes {@link #logReportMsg(Throwable)} + * and throws the passed error. */ - public static Identifier id(String path) { - return new Identifier(MOD_ID, path); + public static X logAndThrowReportMsg(@NotNull X error) throws X { + logReportMsg(error); + throw error; } - /** - * Returns the current ClientWorld's name. For singleplayer, - * returns the level name. For multiplayer, returns the - * server entry name. Falls back on the IP if it was - * direct-connect. Leads with "C_" or "S_" depending - * on the source of the ClientWorld. - * @param client A non-null MinecraftClient that must be in-game. - * @return (C or S) + "_" + (current world name) - */ - @SuppressWarnings("DataFlowIssue") // getServer and getCurrentServerEntry are not null if isIntegratedServerRunning is true - public static String currentWorldName(@NotNull MinecraftClient client) { - Objects.requireNonNull(client, "MinecraftClient must exist to access client data:"); - String entryName; - - return client.isIntegratedServerRunning() - ? "C_" + client.getServer().getSaveProperties().getLevelName() - : (entryName = client.getCurrentServerEntry().name) == null || entryName.isBlank() // check if null/empty then use IP - ? "S_" + client.getCurrentServerEntry().address - : "S_" + client.getCurrentServerEntry().name; + // 1.20.5+ needs the RegistryOps instance + public static DynamicOps jsonOps() throws NullPointerException { + return JsonOps.INSTANCE; } } \ No newline at end of file diff --git a/src/main/java/obro1961/chatpatches/accessor/ChatHudAccessor.java b/src/main/java/obro1961/chatpatches/accessor/ChatHudAccessor.java index fed3e40..b58aa70 100644 --- a/src/main/java/obro1961/chatpatches/accessor/ChatHudAccessor.java +++ b/src/main/java/obro1961/chatpatches/accessor/ChatHudAccessor.java @@ -1,43 +1,32 @@ package obro1961.chatpatches.accessor; -import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.hud.ChatHud; import net.minecraft.client.gui.hud.ChatHudLine; -import obro1961.chatpatches.mixin.gui.ChatHudMixin; import java.util.List; -/** - * An access-widening interface used with {@link ChatHudMixin} - * to access necessary fields and methods w/o an extra - * accessor mixin. To get an instance, use - * {@link ChatHudAccessor#from(ChatHud)} or - * {@link ChatHudAccessor#from(MinecraftClient)}. - */ public interface ChatHudAccessor { - // these two methods avoid needing to cast everywhere because it looks ugly - static ChatHudAccessor from(ChatHud chatHud) { - return ((ChatHudAccessor) chatHud); - } - static ChatHudAccessor from(MinecraftClient client) { - return from(client.inGameHud.getChatHud()); - } - // unique methods, not present in ChatHud /** {@link ChatHud#messages} */ List chatpatches$getMessages(); + /** {@link ChatHud#visibleMessages} */ List chatpatches$getVisibleMessages(); + /** {@link ChatHud#scrolledLines} */ int chatpatches$getScrolledLines(); - // renamed methods to access-widen them from ChatHud + + // proxy methods for accessing the originals in ChatHud /** {@link ChatHud#getMessageLineIndex(double, double)} */ int chatpatches$getMessageLineIndex(double x, double y); + /** {@link ChatHud#toChatLineX(double)} */ double chatpatches$toChatLineX(double x); + /** {@link ChatHud#toChatLineY(double)} */ double chatpatches$toChatLineY(double y); + /** {@link ChatHud#getLineHeight()} */ int chatpatches$getLineHeight(); } \ No newline at end of file diff --git a/src/main/java/obro1961/chatpatches/accessor/ChatScreenAccessor.java b/src/main/java/obro1961/chatpatches/accessor/ChatScreenAccessor.java index 50dc53c..4dbda32 100644 --- a/src/main/java/obro1961/chatpatches/accessor/ChatScreenAccessor.java +++ b/src/main/java/obro1961/chatpatches/accessor/ChatScreenAccessor.java @@ -1,11 +1,5 @@ package obro1961.chatpatches.accessor; -import net.minecraft.client.gui.screen.ChatScreen; - public interface ChatScreenAccessor { - static ChatScreenAccessor from(ChatScreen chatScreen) { - return ((ChatScreenAccessor) chatScreen); - } - void chatpatches$clearMessageDraft(); -} +} \ No newline at end of file diff --git a/src/main/java/obro1961/chatpatches/chatlog/ChatLog.java b/src/main/java/obro1961/chatpatches/chatlog/ChatLog.java index d71b014..e57e72c 100644 --- a/src/main/java/obro1961/chatpatches/chatlog/ChatLog.java +++ b/src/main/java/obro1961/chatpatches/chatlog/ChatLog.java @@ -1,18 +1,23 @@ package obro1961.chatpatches.chatlog; import com.google.common.collect.Lists; -import com.google.gson.Gson; -import com.google.gson.InstanceCreator; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonSerializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.hud.MessageIndicator; import net.minecraft.client.resource.language.I18n; import net.minecraft.text.Text; +import net.minecraft.text.TextCodecs; +import net.minecraft.util.JsonHelper; +import net.minecraft.util.Util; import obro1961.chatpatches.ChatPatches; import obro1961.chatpatches.config.Config; -import obro1961.chatpatches.util.Flags; import java.io.IOException; import java.nio.charset.Charset; @@ -20,43 +25,59 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.List; +import java.util.ArrayList; +import java.util.function.Function; +import static obro1961.chatpatches.ChatPatches.LOGGER; import static obro1961.chatpatches.ChatPatches.config; /** * Represents the chat log file in the run directory located at {@link ChatLog#PATH}. + * Contains methods for serializing, deserializing, accessing, modifying, and + * backing up the data. */ public class ChatLog { public static final Path PATH = FabricLoader.getInstance().getGameDir().resolve("logs").resolve("chatlog.json"); public static final MessageIndicator RESTORED_TEXT = new MessageIndicator(0x382fb5, null, null, I18n.translate("text.chatpatches.restored")); - private static final Gson GSON = new com.google.gson.GsonBuilder() - .registerTypeAdapter(Text.class, (JsonSerializer) (src, type, context) -> Text.Serialization.toJsonTree(src)) - .registerTypeAdapter(Text.class, (JsonDeserializer) (json, type, context) -> Text.Serialization.fromJsonTree(json)) - .registerTypeAdapter(Text.class, (InstanceCreator) type -> Text.empty()) - .create(); + public static boolean loaded = false; + public static int ticksUntilSave = config.chatlogSaveInterval * 60 * 20; // convert minutes to ticks - private static boolean savedAfterCrash = false; + private static boolean restoring = false; private static ChatLog.Data data = new Data(); private static int lastHistoryCount = -1, lastMessageCount = -1; - public static boolean loaded = false; - public static int ticksUntilSave = config.chatlogSaveInterval * 60 * 20; // convert minutes to ticks - - /** Micro class for serializing, used separately from ChatLog for simplification */ + /** Simplified serializing class */ private static class Data { public static final String EMPTY_DATA = "{\"history\":[],\"messages\":[]}"; // prevents a few errors if the channel doesn't initialize public static final int DEFAULT_SIZE = 100; - - public List messages; - public List history; + /** + * Codec for serializing and deserializing chat log data. + * Has entries for the {@link #messages} and {@link #history}, + * and calls {@link Codec#xmap(Function, Function)} to make the + * lists mutable. + */ + public static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group( + Codec.list(TextCodecs.CODEC).xmap(ArrayList::new, Function.identity()).fieldOf("messages").forGetter(data -> data.messages), + Codec.list(Codec.STRING).xmap(ArrayList::new, Function.identity()).fieldOf("history").forGetter(data -> data.history) + ).apply(inst, (messages, history) -> Util.make(new Data(), data -> { + data.messages = messages; + data.history = history; + }))); + + public ArrayList messages; + public ArrayList history; private Data() { messages = Lists.newArrayListWithExpectedSize(DEFAULT_SIZE); history = Lists.newArrayListWithExpectedSize(DEFAULT_SIZE); } + + private Data(boolean done) { + this(); + loaded = done; + } } @@ -65,23 +86,28 @@ private Data() { * * @implNote *
    - *
  1. Checks if the file at {@link #PATH} exists. - *
  2. If it doesn't exist, {@code rawData} just uses {@link Data#EMPTY_DATA}. - *
  3. If it does exist, it will convert the ChatLog file to UTF-8 if it isn't already and save it to {@code rawData}. - *
  4. If {@code rawData} contains invalid data, resets {@link #data} to a default, empty {@link Data} object. - *
  5. Then it uses {@link #GSON} to convert {@code rawData} into a usable {@link Data} object. - *
  6. Removes any overflowing messages. - *
  7. If it successfully resolved, then returns and logs a message. + *
  8. Checks if the file at {@link #PATH} exists.
  9. + *
  10. If it doesn't exist, sets {@link #data} to an empty object and returns.
  11. + *
  12. If it does exist, converts the ChatLog file to UTF-8 if necessary and loads it into {@code rawData}.
  13. + *
  14. If {@code rawData} contains invalid data, resets {@link #data}.
  15. + *
  16. Transforms any legacy UUID int arrays into a stringified format
  17. + *
  18. Then uses {@link Data#CODEC} to parse {@code rawData} into a usable {@link Data} object.
  19. + *
  20. Removes any overflowing messages.
  21. + *
  22. If any errors are thrown, logs the issue and backs up the broken file just in case.
  23. + *
  24. Otherwise, logs a message noting how many entries were loaded.
  25. + *
*/ public static void deserialize() { String rawData = Data.EMPTY_DATA; + long start = System.currentTimeMillis(); + LOGGER.info("[ChatLog.deserialize] Reading..."); + if(Files.exists(PATH)) { try { rawData = Files.readString(PATH); - } catch(MalformedInputException notUTF8) { // thrown if the file is not encoded with UTF-8 - ChatPatches.LOGGER.warn("[ChatLog.deserialize] ChatLog file encoding was '{}', not UTF-8. Complex text characters may have been replaced with question marks.", Charset.defaultCharset().name()); + LOGGER.warn("[ChatLog.deserialize] Chat log file encoding was '{}', not UTF-8. Complex text characters may have been replaced with question marks.", Charset.defaultCharset().name()); try { // force-writes the string as UTF-8 @@ -89,111 +115,135 @@ public static void deserialize() { rawData = Files.readString(PATH); } catch(IOException ioexc) { - ChatPatches.LOGGER.error("[ChatLog.deserialize] Couldn't rewrite the ChatLog at '{}', resetting:", PATH, ioexc); + LOGGER.error("[ChatLog.deserialize] Couldn't rewrite the chat log at '{}', resetting:", PATH, ioexc); // final attempt to reset the file try { rawData = Data.EMPTY_DATA; // just in case of corruption from previous failures Files.writeString(PATH, Data.EMPTY_DATA, StandardOpenOption.TRUNCATE_EXISTING); } catch(IOException ioerr) { - ChatPatches.LOGGER.error("[ChatLog.deserialize] Couldn't reset the ChatLog at '{}':", PATH, ioerr); + LOGGER.error("[ChatLog.deserialize] Couldn't reset the chat log at '{}':", PATH, ioerr); } } } catch(IOException e) { - ChatPatches.LOGGER.error("[ChatLog.deserialize] Couldn't access the ChatLog at '{}':", PATH, e); + LOGGER.error("[ChatLog.deserialize] Couldn't access the chat log at '{}':", PATH, e); rawData = Data.EMPTY_DATA; // just in case of corruption from failures } } else { - data = new Data(); - loaded = true; - return; + // intentionally creates invalid data to prompt the next if block to + // generate a blank one and log it, instead of it happening twice + rawData = ""; } - // if the file has invalid data (doesn't start with a '{'), reset it + // ignore invalid files if( rawData.length() < 2 || !rawData.startsWith("{") ) { - data = new Data(); - loaded = true; - + data = new Data(true); + LOGGER.info("[ChatLog.deserialize] No chat log found at '{}', generated a blank one for {} initial messages in {} seconds", + PATH, Data.DEFAULT_SIZE, (System.currentTimeMillis() - start) / 1000.0 + ); return; } try { - data = GSON.fromJson(rawData, Data.class); - removeOverflowData(); - } catch(com.google.gson.JsonSyntaxException e) { - ChatPatches.LOGGER.error("[ChatLog.deserialize] Tried to read the ChatLog and found an error, loading an empty one: ", e); + JsonObject jsonData = JsonHelper.deserialize(rawData); + data = Data.CODEC.parse(ChatPatches.jsonOps(), jsonData).resultOrPartial(e -> { + throw new JsonSyntaxException(e); + }).orElseThrow(); + + // the sublist indices make sure to only keep the newest data and remove the oldest + // NOTE: the chat log system has the oldest messages at 0, but vanilla has the newest at 0 + if(messageCount() > config.chatMaxMessages) + data.messages = (ArrayList)data.messages.subList( messageCount() - config.chatMaxMessages, messageCount() ); + if(historyCount() > config.chatMaxMessages) + data.history = (ArrayList)data.history.subList( historyCount() - config.chatMaxMessages, historyCount() ); - data = new Data(); loaded = true; + } catch(Exception e) { + LOGGER.error("[ChatLog.deserialize] Tried to read the chat log and found an error, backing it up and loading an empty one:", e); + + backup(); + + data = new Data(true); return; } - loaded = true; - - ChatPatches.LOGGER.info("[ChatLog.deserialize] Read the chat log containing {} messages and {} sent messages from '{}'", - data.messages.size(), data.history.size(), - PATH - ); + LOGGER.info("[ChatLog.deserialize] Read the chat log containing {} messages and {} sent messages from '{}' in {} seconds", + messageCount(), historyCount(), PATH, (System.currentTimeMillis() - start) / 1000.0 + ); } /** - * Saves the chat log to {@link #PATH}. Only saves if {@link Config#chatlog} is true, - * it isn't crashing again, and if there is *new* data to save. - * - * @param crashing If the game is crashing. If true, it will only save if {@link #savedAfterCrash} - * is false AND if {@link Config#chatlogSaveInterval} is 0. + * Saves the chat log to {@link #PATH}. Only saves if {@link Config#chatlog} is true + * and if there is *new* data to save. As of 1.20.5, also requires the player to + * be in-game during the saving process, so the registry-synced TextCodec can be + * used (#180). */ - public static void serialize(boolean crashing) { + public static void serialize() { if(!config.chatlog) return; - if(crashing && savedAfterCrash) - return; if(data.messages.isEmpty() && data.history.isEmpty()) return; // don't overwrite the file with an empty one if there's nothing to save + if(messageCount() == lastMessageCount && historyCount() == lastHistoryCount) + return; // don't save if there's no new data + + long start = System.currentTimeMillis(); + LOGGER.info("[ChatLog.serialize] Saving..."); - if(data.messages.size() == lastMessageCount && data.history.size() == lastHistoryCount) - return; // don't save if there's no new data AND if the path is the default one (not a backup) - removeOverflowData(); // don't save more than the max amount of messages + // catch the NPE and cancel IF it's thrown + DynamicOps registeredOps; + try { + registeredOps = ChatPatches.jsonOps(); + } catch(NullPointerException npe) { + ChatPatches.logReportMsg(npe); + dumpData(); + return; + } try { - final String str = GSON.toJson(data, Data.class); - Files.writeString(PATH, str, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + JsonElement json = Data.CODEC.encodeStart(registeredOps, data) + .resultOrPartial(e -> ChatPatches.logReportMsg(new JsonParseException(e))) + .orElseThrow(); - lastHistoryCount = data.history.size(); - lastMessageCount = data.messages.size(); + Files.writeString(PATH, JsonHelper.toSortedString(json), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - ChatPatches.LOGGER.info("[ChatLog.serialize] Saved the chat log containing {} messages and {} sent messages to '{}'", data.messages.size(), data.history.size(), PATH); - } catch(IOException e) { - ChatPatches.LOGGER.error("[ChatLog.serialize] An I/O error occurred while trying to save the chat log:", e); + lastHistoryCount = historyCount(); + lastMessageCount = messageCount(); + LOGGER.info("[ChatLog.serialize] Saved the chat log containing {} messages and {} sent messages to '{}' in {} seconds", + messageCount(), historyCount(), PATH, (System.currentTimeMillis() - start) / 1000.0 + ); + + // temporarily removed the ugly ConcurrentModificationException catch block bc it's ugly and not a real solution: + // fixme! - } finally { - if(crashing) - savedAfterCrash = true; + } catch(IOException | RuntimeException e) { + LOGGER.error("[ChatLog.serialize] An I/O or unexpected runtime error occurred while trying to save the chat log:", e); + dumpData(); } } + /** * Creates a backup of the current chat log file - * located at {@link #PATH} and saves it - * as "chatlog_" + current time + ".json" in the - * same directory as the original file. - * If an error occurs, a warning will be logged. + * located at {@link #PATH} and saves it as + * {@code chatlog_${current_time}.json} in the + * same directory as the original file. If an + * error occurs, a warning will be logged. * Doesn't modify the current chat log. */ public static void backup() { - try { - Files.copy(PATH, PATH.resolveSibling( "chatlog_" + ChatPatches.TIME_FORMATTER.get() + ".json" )); - } catch(IOException e) { - ChatPatches.LOGGER.warn("[ChatLog.backup] Couldn't backup the chat log at '{}':", PATH, e); - } - } + try { + Files.copy(PATH, PATH.resolveSibling( "chatlog_" + Util.getFormattedCurrentTime() + ".json" )); + } catch(IOException e) { + LOGGER.warn("[ChatLog.backup] Couldn't backup the chat log at '{}':", PATH, e); + } + } /** Restores the chat log from {@link #data} into Minecraft. */ public static void restore(MinecraftClient client) { - Flags.LOADING_CHATLOG.raise(); + restoring = true; if(!data.history.isEmpty()) data.history.forEach(client.inGameHud.getChatHud()::addToMessageHistory); @@ -201,9 +251,9 @@ public static void restore(MinecraftClient client) { if(!data.messages.isEmpty()) data.messages.forEach(msg -> client.inGameHud.getChatHud().addMessage(msg, null, RESTORED_TEXT)); - Flags.LOADING_CHATLOG.lower(); + restoring = false; - ChatPatches.LOGGER.info("[ChatLog.restore] Restored {} messages and {} history messages from '{}' into Minecraft!", data.messages.size(), data.history.size(), PATH); + LOGGER.info("[ChatLog.restore] Restored {} messages and {} history messages from '{}' into Minecraft!", messageCount(), historyCount(), PATH); } /** @@ -220,7 +270,7 @@ public static void restore(MinecraftClient client) { */ public static void tickSaveCounter() { if(config.chatlogSaveInterval > 0 && ticksUntilSave == 0) - serialize(false); + serialize(); ticksUntilSave--; @@ -228,27 +278,41 @@ public static void tickSaveCounter() { ticksUntilSave = config.chatlogSaveInterval * 60 * 20; } + /** + * Dumps chat log data into the debug log. Note: + * Assumes the Text codec is unusable, so instead + * maps each message using {@link Text#getString()}. + */ + @SuppressWarnings("StringConcatenationArgumentToLogCall") + public static void dumpData() { + LOGGER.debug("[ChatLog.dumpData] " + Data.EMPTY_DATA.replaceAll("\\[]", "{}"), data.history, data.messages.stream().map(Text::getString).toList()); + } + + public static void addMessage(Text msg) { - if(data.messages.size() > config.chatMaxMessages) - data.messages.remove(0); + if(restoring) + return; + if(messageCount() > config.chatMaxMessages) + data.messages.removeFirst(); data.messages.add(msg); } public static void addHistory(String msg) { - if(data.history.size() > config.chatMaxMessages) - data.history.remove(0); + if(restoring) + return; + if(historyCount() > config.chatMaxMessages) + data.history.removeFirst(); data.history.add(msg); } - public static void removeOverflowData() { - // the sublist indices make sure to only keep the newest data and remove the oldest - if(data.messages.size() > config.chatMaxMessages) - data.messages = data.messages.subList( data.messages.size() - config.chatMaxMessages, data.messages.size() ); - - if(data.history.size() > config.chatMaxMessages) - data.history = data.history.subList( data.history.size() - config.chatMaxMessages, data.history.size() ); - } + /** + * Returns if the chat log is currently + * being restored into the chat. Used + * to prevent logging and modifying + * restored messages. + */ + public static boolean isRestoring() { return restoring; } public static void clearMessages() { data.messages.clear(); diff --git a/src/main/java/obro1961/chatpatches/config/Config.java b/src/main/java/obro1961/chatpatches/config/Config.java index f3f7169..6488b88 100644 --- a/src/main/java/obro1961/chatpatches/config/Config.java +++ b/src/main/java/obro1961/chatpatches/config/Config.java @@ -6,11 +6,16 @@ import com.google.gson.JsonSyntaxException; import com.mojang.authlib.GameProfile; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.SharedConstants; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ConfirmLinkScreen; +import net.minecraft.client.gui.screen.ConfirmScreen; import net.minecraft.client.gui.screen.Screen; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.scoreboard.Team; +import net.minecraft.screen.ScreenTexts; import net.minecraft.text.*; +import net.minecraft.util.Util; import obro1961.chatpatches.ChatPatches; import obro1961.chatpatches.util.ChatUtils; @@ -35,7 +40,17 @@ public class Config { public static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("chatpatches.json"); public static final Config DEFAULTS = new Config(); + /** + * List of mods installed that, in one way or another, + * cause modified messages to have an extra space in + * between the timestamp and message content. While + * I could open issues to deal with this issue, it's + * much easier to just remove the space, especially + * if it's unintentionally my fault. + */ + //public static final Stream EXTRA_SPACE_MODS = Stream.of("styledchat"); + private static final FabricLoader FABRIC = FabricLoader.getInstance(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); // categories: time, hover, counter, counter.compact, boundary, chatlog, chat.hud, chat.screen, copy @@ -45,7 +60,7 @@ public class Config { public boolean counterCompact = false; public int counterCompactDistance = 0; public boolean boundary = true; public String boundaryFormat = "&8[&r$&8]"; public int boundaryColor = 0x55ffff; public boolean chatlog = true; public int chatlogSaveInterval = 0; - public boolean chatHidePacket = true; public int chatWidth = 0, chatMaxMessages = 16384; public String chatNameFormat = "<$>"; public int chatNameColor = 0xffffff; + public boolean chatHidePacket = true; public int chatWidth = 0, chatHeight = 0, chatMaxMessages = 16384; public String chatNameFormat = "<$>"; public int chatNameColor = 0xffffff; public int shiftChat = 10; public boolean messageDrafting = false, onlyInvasiveDrafting = false, searchDrafting = true, hideSearchButton = false, vanillaClearing = false; public int copyColor = 0x55ffff; public String copyReplyFormat = "/msg $ "; @@ -54,8 +69,7 @@ public class Config { * on installed mods. Should only be called once. */ public static Config create() { - FabricLoader fbr = FabricLoader.getInstance(); - boolean accessibleInGame = fbr.isModLoaded("modmenu") || (fbr.isModLoaded("catalogue") && fbr.isModLoaded("menulogue")); + boolean accessibleInGame = FABRIC.isModLoaded("modmenu") || (FABRIC.isModLoaded("catalogue") && FABRIC.isModLoaded("menulogue")); config = accessibleInGame ? new YACLConfig() : DEFAULTS; read(); @@ -65,10 +79,23 @@ public static Config create() { } - public /*static*/ Screen getConfigScreen(Screen parent) { - // idea: make this return a new YACL screen here if bool in #create() is true - // instead of making a new config object - return null; + public Screen getConfigScreen(Screen parent) { + MinecraftClient mc = MinecraftClient.getInstance(); + boolean suggestYACL = SharedConstants.getProtocolVersion() >= 759; // 1.19 or higher + String link = "https://modrinth.com/mod/" + (suggestYACL ? "yacl" : "cloth-config"); + + return new ConfirmScreen( + clicked -> { + if(clicked) + ConfirmLinkScreen.open(parent, link); + else + mc.setScreen(parent); + }, + Text.translatable("text.chatpatches.help.missing"), + Text.translatable("text.chatpatches.desc.help.missing", (suggestYACL ? "YACL" : "Cloth Config")), + ScreenTexts.CONTINUE, + ScreenTexts.BACK + ); } @@ -145,12 +172,12 @@ public MutableText formatPlayername(GameProfile profile) { .append(team.getSuffix()) .append(configSuffix); } - } catch(Exception e) { + } catch(RuntimeException e) { LOGGER.error("[Config.formatPlayername] /!\\ An error occurred while trying to format '{}'s playername /!\\", profile.getName()); - ChatPatches.logInfoReportMessage(e); + ChatPatches.logReportMsg(e); } - return makeObject(chatNameFormat, profile.getName(), "", " ", style); + return makeObject(chatNameFormat, profile.getName(), "", /*EXTRA_SPACE_MODS.anyMatch(FABRIC::isModLoaded) ? "" :*/ " ", style); } public MutableText makeDupeCounter(int dupes) { @@ -214,7 +241,7 @@ public static void reset() { */ public static void writeCopy() { try { - Files.copy(PATH, PATH.resolveSibling( "chatpatches_" + ChatPatches.TIME_FORMATTER.get() + ".json" )); + Files.copy(PATH, PATH.resolveSibling( "chatpatches_" + Util.getFormattedCurrentTime() + ".json" )); } catch(IOException e) { LOGGER.warn("[Config.writeCopy] An error occurred trying to write a copy of the original config file:", e); } @@ -247,7 +274,7 @@ public static ConfigOption getOption(String key) { 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!", key); - ChatPatches.logInfoReportMessage(e); + ChatPatches.logReportMsg(e); return new ConfigOption<>( (T)new Object(), (T)new Object(), key ); } diff --git a/src/main/java/obro1961/chatpatches/config/YACLConfig.java b/src/main/java/obro1961/chatpatches/config/YACLConfig.java index 6b91fea..cc0fced 100644 --- a/src/main/java/obro1961/chatpatches/config/YACLConfig.java +++ b/src/main/java/obro1961/chatpatches/config/YACLConfig.java @@ -1,6 +1,5 @@ package obro1961.chatpatches.config; -import com.google.common.collect.Lists; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.*; import dev.isxander.yacl3.gui.YACLScreen; @@ -16,10 +15,10 @@ import net.minecraft.util.Util; import obro1961.chatpatches.ChatPatches; import obro1961.chatpatches.chatlog.ChatLog; -import obro1961.chatpatches.util.Flags; import java.awt.*; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; @@ -32,20 +31,20 @@ public class YACLConfig extends Config { @Override public Screen getConfigScreen(Screen parent) { - List> timeOpts = Lists.newArrayList(); - List> hoverOpts = Lists.newArrayList(); - List> counterOpts = Lists.newArrayList(); - List> compactChatOpts = Lists.newArrayList(); - List> boundaryOpts = Lists.newArrayList(); - List> chatlogOpts = Lists.newArrayList(); - List> chatlogActions = Lists.newArrayList(); - List> chatNameOpts = Lists.newArrayList(); - List> chatHudOpts = Lists.newArrayList(); - List> chatScreenOpts = Lists.newArrayList(); - List> copyMenuOpts = Lists.newArrayList(); + List> timeOpts = new ArrayList<>(), + hoverOpts = new ArrayList<>(), + counterOpts = new ArrayList<>(), + compactChatOpts = new ArrayList<>(), + boundaryOpts = new ArrayList<>(), + chatlogOpts = new ArrayList<>(), + chatlogActions = new ArrayList<>(), + chatNameOpts = new ArrayList<>(), + chatHudOpts = new ArrayList<>(), + chatScreenOpts = new ArrayList<>(), + copyMenuOpts = new ArrayList<>(); Config.getOptions().forEach(opt -> { - String key = opt.key; // to fix "local variable opt.key must be final or effectively final" + String key = opt.key; // effectively final String cat = key.split("[A-Z]")[0]; if( key.contains("counterCompact") ) cat = "compact"; @@ -117,7 +116,10 @@ public void set(Object value) { .category( category("boundary", boundaryOpts) ) .category( category("chatlog", chatlogOpts, group("chatlog.actions", chatlogActions, null)) ) .category( category("chat", List.of(), - group("chat.name", chatNameOpts, null), group("chat.hud", chatHudOpts, null), group("chat.screen", chatScreenOpts, null)) ) + group("chat.name", chatNameOpts, null), + group("chat.hud", chatHudOpts, null), + group("chat.screen", chatScreenOpts, null) + )) .category( category("copy", copyMenuOpts) ) .category( @@ -141,12 +143,6 @@ public void set(Object value) { category( "debug", List.of( - Option.createBuilder() - .name( Text.literal("Edit Bit Flags (%d^10, %s^2)".formatted(Flags.flags, Integer.toBinaryString(Flags.flags))) ) - .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(0, 0b1111).step(1)) - .binding( Flags.flags, () -> Flags.flags, inc -> Flags.flags = inc ) - .build(), - ButtonOption.createBuilder() .name( Text.literal("Print GitHub Option table") ) .action((screen, option) -> { @@ -157,7 +153,7 @@ public void set(Object value) { I18n.translate("text.chatpatches." + opt.key), ( opt.getType().equals(Integer.class) && opt.key.contains("Color") ) - ? "`0x%06x`".formatted(opt.def) + ? "`0x%06x`".formatted( (int)opt.def ) : (opt.getType().equals(String.class)) ? "`\"" + opt.def + "\"`" : "`" + opt.def + "`", @@ -167,7 +163,7 @@ public void set(Object value) { )) ); - ChatPatches.LOGGER.warn("[YACLConfig.printGithubTables]" + str); + ChatPatches.LOGGER.warn("[YACLConfig.printGithubTables] {}", str); }) .build() ) @@ -207,7 +203,7 @@ private static BiConsumer getAction(String key) { ChatLog.deserialize(); ChatLog.restore(MinecraftClient.getInstance()); } else if(key.equals("chatlogSave")) { - ChatLog.serialize(false); + ChatLog.serialize(); } else if(key.equals("chatlogBackup")) { ChatLog.backup(); } else if(key.equals("chatlogOpenFolder")) { @@ -260,6 +256,7 @@ private static int getMinOrMax(String key, boolean min) { case "counterCompactDistance" -> 1024; case "chatlogSaveInterval" -> 180; case "chatWidth" -> MinecraftClient.getInstance().getWindow().getScaledWidth() - 12; // offset length calc'd from ChatHud#render aka magic # + case "chatHeight" -> MinecraftClient.getInstance().getWindow().getScaledHeight() - 12; // only issue w ^^^ is if the window is resized while the config screen is open the max value will be incorrect // other issue could be with the future config redo, as annotation constraints must be *constant* case "chatMaxMessages" -> Short.MAX_VALUE; @@ -312,8 +309,6 @@ private static OptionDescription desc(ConfigOption opt) { try { if( MinecraftClient.getInstance().getResourceManager().getResource(id).isPresent() ) builder.webpImage(id); - else - ChatPatches.LOGGER.debug("[YACLConfig.desc] No .{} image found for '{}'", ext, opt.key.replaceAll("([A-Z])", "_$1").toLowerCase()); } catch(Throwable e) { ChatPatches.LOGGER.error("[YACLConfig.desc] An error occurred while trying to use '{}:{}' :", ChatPatches.MOD_ID, image, e); } @@ -331,6 +326,7 @@ private static ButtonOption action(String key, Object... args) { .name(Text.translatable( "text.chatpatches." + key, (args[0].equals(-1) ? new Object[0] : args) )) // args or nothing .description(desc( new ConfigOption<>(o, o, key) )) .action(getAction(key)) + .available( !key.matches("chatlog(Load|Save)") || MinecraftClient.getInstance().world != null ) // must be in-game to load/save .build(); } } \ No newline at end of file diff --git a/src/main/java/obro1961/chatpatches/gui/MenuButtonWidget.java b/src/main/java/obro1961/chatpatches/gui/MenuButtonWidget.java index e82dadd..05f5bc3 100644 --- a/src/main/java/obro1961/chatpatches/gui/MenuButtonWidget.java +++ b/src/main/java/obro1961/chatpatches/gui/MenuButtonWidget.java @@ -2,6 +2,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.PlayerSkinDrawer; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.TextIconButtonWidget; @@ -174,14 +175,7 @@ public void render(DrawContext drawContext, int mX, int mY, float delta) { // height: effectively height+1p (with coords, height-1p); button.render(drawContext, mX, mY, delta); - if(skinTexture != null && skinTexture.texture() != null) { - // thank you to dzwdz's Chat Heads for most of the code to draw the skin texture! - - // draw base layer, then the hat - int x = anchor.x + xOffset + 1; - int y = anchor.y + yOffset + 1; - drawContext.drawTexture(skinTexture.texture(), x, y, 16, 16, 8, 8, 8, 8, 64, 64); - drawContext.drawTexture(skinTexture.texture(), x, y, 16, 16, 40, 8, 8, 8, 64, 64); - } + if(skinTexture != null && skinTexture.texture() != null) + PlayerSkinDrawer.draw(drawContext, skinTexture, anchor.x + xOffset + 1, anchor.y + yOffset + 1, 16); } } \ No newline at end of file diff --git a/src/main/java/obro1961/chatpatches/mixin/MinecraftClientMixin.java b/src/main/java/obro1961/chatpatches/mixin/MinecraftClientMixin.java deleted file mode 100644 index d33687d..0000000 --- a/src/main/java/obro1961/chatpatches/mixin/MinecraftClientMixin.java +++ /dev/null @@ -1,37 +0,0 @@ -package obro1961.chatpatches.mixin; - -import net.fabricmc.api.EnvType; -import net.fabricmc.api.Environment; -import net.minecraft.client.MinecraftClient; -import obro1961.chatpatches.chatlog.ChatLog; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -/** - * An ugly but necessary mixin for a couple random things. - */ -@Environment(EnvType.CLIENT) -@Mixin(MinecraftClient.class) -public abstract class MinecraftClientMixin { - - /** Injects callbacks to game exit events so cached data can still be saved */ - @Inject(method = "run", at = { - @At( - value = "INVOKE", - target = "Lnet/minecraft/client/MinecraftClient;addDetailsToCrashReport(Lnet/minecraft/util/crash/CrashReport;)Lnet/minecraft/util/crash/CrashReport;" - ), - @At( - value = "INVOKE_ASSIGN", - target = "Lnet/minecraft/client/MinecraftClient;addDetailsToCrashReport(Lnet/minecraft/util/crash/CrashReport;)Lnet/minecraft/util/crash/CrashReport;" - ), - @At( - value = "INVOKE", - target = "Lnet/minecraft/client/MinecraftClient;cleanUpAfterCrash()V" - ) - }) - private void saveChatlogOnCrash(CallbackInfo ci) { - ChatLog.serialize(true); - } -} diff --git a/src/main/java/obro1961/chatpatches/mixin/chat/MessageHandlerMixin.java b/src/main/java/obro1961/chatpatches/mixin/chat/MessageHandlerMixin.java index cf50dc4..2d65514 100644 --- a/src/main/java/obro1961/chatpatches/mixin/chat/MessageHandlerMixin.java +++ b/src/main/java/obro1961/chatpatches/mixin/chat/MessageHandlerMixin.java @@ -15,6 +15,7 @@ import org.apache.commons.lang3.StringUtils; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -23,11 +24,12 @@ import java.util.UUID; /** - * A mixin used to cache the metadata of the most recent message - * received by the client. This is used in - * {@link ChatHudMixin#modifyMessage(Text, boolean)} - * to provide more accurate timestamp data, the correct player - * name, and the player's UUID. + * Allows caching metadata of the most recent + * message received by the client. This is used in + * {@link ChatHudMixin#modifyMessage(Text)} + * to provide more accurate timestamp data, + * correct player data, and control how the + * message should be modified. */ @Environment(EnvType.CLIENT) @Mixin(MessageHandler.class) @@ -42,10 +44,9 @@ public abstract class MessageHandlerMixin { @Inject(method = "onChatMessage", at = @At("HEAD")) private void cacheChatData(SignedMessage message, GameProfile sender, MessageType.Parameters params, CallbackInfo ci) { // only logs the metadata if it was a player-sent message (otherwise tries to format some commands like /msg and /me) - if( params.type().chat().translationKey().equals("chat.type.text") ) - ChatPatches.msgData = new ChatUtils.MessageData(sender, Date.from(message.getTimestamp()), true); - else - ChatPatches.msgData = ChatUtils.NIL_MSG_DATA; + ChatPatches.msgData = params.type().chat().translationKey().equals("chat.type.text") + ? new ChatUtils.MessageData(sender, Date.from(message.getTimestamp()), isVanilla(params.applyChatDecoration(message.getContent()))) + : ChatUtils.NIL_MSG_DATA; } /** @@ -58,7 +59,26 @@ private void cacheGameData(Text message, boolean overlay, CallbackInfo ci) { UUID id = extractSender(message); ChatPatches.msgData = !id.equals(Util.NIL_UUID) - ? new ChatUtils.MessageData(new GameProfile(id, name), new Date(), true) + ? new ChatUtils.MessageData(new GameProfile(id, name), new Date(), isVanilla(message)) : ChatUtils.NIL_MSG_DATA; } + + + /** + * Returns true if the given Text is in the vanilla message + * format of "{@code message}" (as specified in + * {@link ChatUtils#VANILLA_FORMAT}). This should be + * true for every message sent by a player, which are the + * only messages that need to be heavily modified. In + * {@link ChatUtils#modifyMessage(Text, boolean)}. + * + * @apiNote When called in the chat message handler, the + * message passed should be + * {@code params.applyChatDecoration(message.getContent())} + * to properly include the playername. + */ + @Unique + private boolean isVanilla(Text message) { + return message.getString().matches(ChatUtils.VANILLA_FORMAT); + } } \ No newline at end of file diff --git a/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java b/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java index 91aa313..8e3ae65 100644 --- a/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java +++ b/src/main/java/obro1961/chatpatches/mixin/gui/ChatHudMixin.java @@ -3,7 +3,6 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; -import com.llamalad7.mixinextras.sugar.Local; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; @@ -18,8 +17,6 @@ import obro1961.chatpatches.chatlog.ChatLog; import obro1961.chatpatches.config.Config; import obro1961.chatpatches.util.ChatUtils; -import obro1961.chatpatches.util.Flags; -import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -100,12 +97,18 @@ private int moreWidth(int defaultWidth) { return config.chatWidth > 0 ? config.chatWidth : defaultWidth; } + /** allows for a chat height larger than the default */ + @ModifyReturnValue(method = "getHeight()I", at = @At("RETURN")) + private int moreHeight(int defaultHeight) { + return config.chatHeight > 0 ? config.chatHeight : defaultHeight; + } + /** * These methods shift most of the chat hud by * {@link Config#shiftChat}, including the text * and scroll bar, by shifting the y position of the chat. * - * @implNote Target:
{@code int m = MathHelper.floor((float)(l - 40) / f);} + *

Target: {@code int m = MathHelper.floor((float)(l - 40) / f);} */ @ModifyVariable(method = "render", at = @At("STORE"), ordinal = 7) private int moveChat(int m) { @@ -113,15 +116,17 @@ private int moveChat(int m) { } /** - * Moves the message indicator and hover tooltip - * by {@link Config#shiftChat} to correctly shift - * the chat with the other components. - * Targets two methods because the first part of both - * methods are identical. + * Moves the chat line by {@link Config#shiftChat} to + * correctly shift the chat with the other components. + * Used by the {@link ChatHud} to correctly render + * message indicators and chat hover tooltips when + * needed in the shifted position. + * + *

Target: {@code double d = this.client.getWindow().getScaledHeight() - y - 40.0;} */ - @ModifyVariable(method = {"getIndicatorAt", "getTextStyleAt"}, argsOnly = true, at = @At("HEAD"), ordinal = 1) - private double moveINDHoverText(double e) { - return e + ( config.shiftChat * this.getChatScale() ); + @ModifyVariable(method = "toChatLineY", at = @At("HEAD"), argsOnly = true) + private double moveChatLineY(double y) { + return y + config.shiftChat; } @@ -134,18 +139,17 @@ private double moveINDHoverText(double e) { * implementation specifications. */ @ModifyVariable( - method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", + method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V", at = @At("HEAD"), argsOnly = true ) - private Text modifyMessage(Text m, @Local(argsOnly = true) boolean refreshing) { - return addCounter(ChatUtils.modifyMessage(m, refreshing), refreshing); + private Text modifyMessage(Text m) { + return addCounter(ChatUtils.modifyMessage(m, false), false); } @Inject(method = "addToMessageHistory", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/collection/ArrayListDeque;size()I")) private void addHistory(String message, CallbackInfo ci) { - if( !Flags.LOADING_CHATLOG.isRaised() ) - ChatLog.addHistory(message); + ChatLog.addHistory(message); } /** Disables logging commands to the vanilla command log if the Chat Patches' ChatLog is enabled. */ @@ -155,8 +159,8 @@ private boolean disableCommandLog(CommandHistoryManager manager, String message) } @Inject(method = "logChatMessage", at = @At("HEAD"), cancellable = true) - private void ignoreRestoredMessages(Text message, @Nullable MessageIndicator indicator, CallbackInfo ci) { - if( Flags.LOADING_CHATLOG.isRaised() && indicator != null ) + private void ignoreRestoredMessages(Text message, MessageIndicator indicator, CallbackInfo ci) { + if(ChatLog.isRestoring() && indicator != null) ci.cancel(); } @@ -178,14 +182,14 @@ private void ignoreRestoredMessages(Text message, @Nullable MessageIndicator ind *

  • If a message was the same, call {@link ChatUtils#tryCondenseMessage(Text, int)}, * which ultimately removes that message and its visibles.
  • * - *
  • Return the (potentially) condensed message, to later be formatted further in {@link #modifyMessage(Text, boolean)}
  • + *
  • Return the (potentially) condensed message, to later be formatted further in {@link #modifyMessage(Text)}
  • * * (Wraps the entire method in a try-catch to prevent any errors accidentally disabling the chat.) * * @apiNote This injector is pretty ugly and could definitely be cleaner and more concise, but I'm going to deal with it * in the future when I API-ify the rest of the mod. When that happens, this flag-add-flag-cancel method will be replaced * with a simple (enormous) method call alongside - * {@link #modifyMessage(Text, boolean)} in a @{@link ModifyVariable} + * {@link #modifyMessage(Text)} in a @{@link ModifyVariable} * handler. (NOTE: as of v202.6.0, this is partially done already thanks to #132) */ @Unique @@ -221,10 +225,10 @@ 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. Either way,"); - ChatPatches.logInfoReportMessage(e); + ChatPatches.logReportMsg(e); } catch(Exception e) { ChatPatches.LOGGER.error("[ChatHudMixin.addCounter] /!\\ Couldn't add duplicate counter because of an unexpected error! /!\\"); - ChatPatches.logInfoReportMessage(e); + ChatPatches.logReportMsg(e); } return incoming; diff --git a/src/main/java/obro1961/chatpatches/mixin/gui/ChatScreenMixin.java b/src/main/java/obro1961/chatpatches/mixin/gui/ChatScreenMixin.java index 5733009..e64aa90 100644 --- a/src/main/java/obro1961/chatpatches/mixin/gui/ChatScreenMixin.java +++ b/src/main/java/obro1961/chatpatches/mixin/gui/ChatScreenMixin.java @@ -1,6 +1,7 @@ package obro1961.chatpatches.mixin.gui; import com.google.common.collect.Lists; +import com.google.gson.JsonParseException; import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; @@ -22,12 +23,11 @@ import net.minecraft.client.util.ChatMessages; import net.minecraft.client.util.SkinTextures; import net.minecraft.network.message.MessageSignatureData; -import net.minecraft.text.HoverEvent; -import net.minecraft.text.OrderedText; -import net.minecraft.text.Style; -import net.minecraft.text.Text; +import net.minecraft.text.*; import net.minecraft.util.Formatting; +import net.minecraft.util.JsonHelper; import net.minecraft.util.math.MathHelper; +import obro1961.chatpatches.ChatPatches; import obro1961.chatpatches.accessor.ChatHudAccessor; import obro1961.chatpatches.accessor.ChatScreenAccessor; import obro1961.chatpatches.config.ChatSearchSetting; @@ -100,20 +100,21 @@ public abstract class ChatScreenMixin extends Screen implements ChatScreenAccess // search stuff @Unique private static boolean showSearch = true; @Unique private static boolean showSettingsMenu = false; // note: doesn't really need to be static + @Unique private static String searchDraft = ""; // copy menu stuff @Unique private static boolean showCopyMenu = false; // true when a message was right-clicked on @Unique private static ChatHudLine selectedLine = NIL_HUD_LINE; @Unique private static Map mainButtons = new LinkedHashMap<>(); // buttons that appear on the initial click @Unique private static Map hoverButtons = new LinkedHashMap<>(); // buttons that are revealed on hover @Unique private static List hoveredVisibles = new ArrayList<>(); - // drafting (todo: can we remove these and instead use `originalChatText`?) - @Unique private static String searchDraft = ""; - @Unique private static String messageDraft = ""; - + @Unique private static String messageDraft = ""; // needed instead of originalChatText to be accessible in all ChatScreen instances + // more search stuff @Unique private TextFieldWidget searchField; @Unique private SearchButtonWidget searchButton; @Unique private PatternSyntaxException searchError; + @SuppressWarnings("MissingUnique") //@Shadow + @NotNull protected MinecraftClient client = MinecraftClient.getInstance(); // removes false NPE warnings @Shadow protected TextFieldWidget chatField; @Shadow private String originalChatText; @@ -177,7 +178,13 @@ protected void initSearchStuff(CallbackInfo ci) { // hover menu buttons, column two hoverButtons.put(COPY_RAW_STRING, of(1, COPY_RAW_STRING, () -> Formatting.strip( selectedLine.content().getString() ))); hoverButtons.put(COPY_FORMATTED_STRING, of(1, COPY_FORMATTED_STRING, () -> TextUtils.reorder( selectedLine.content().asOrderedText(), true ))); - hoverButtons.put(COPY_JSON_STRING, of(1, COPY_JSON_STRING, () -> Text.Serialization.toJsonString(selectedLine.content()))); + hoverButtons.put(COPY_JSON_STRING, of(1, COPY_JSON_STRING, + () -> TextCodecs.CODEC.encodeStart(ChatPatches.jsonOps(), selectedLine.content()) + .resultOrPartial(e -> ChatPatches.logReportMsg(new JsonParseException(e))) + .map(JsonHelper::toSortedString) + .orElse("/!\\ An error occurred! Please open an issue on the Chat Patches GitHub and attach your log file /!\\") + ) + ); hoverButtons.put(COPY_LINK_N.apply(0), of(1, COPY_LINK_N.apply(0), () -> "")); hoverButtons.put(COPY_TIMESTAMP_TEXT, of(1, COPY_TIMESTAMP_TEXT, () -> getPart(selectedLine.content(), TIMESTAMP_INDEX).getString())); hoverButtons.put(COPY_TIMESTAMP_HOVER_TEXT, of(1, COPY_TIMESTAMP_HOVER_TEXT, () -> { @@ -223,6 +230,9 @@ protected void initSearchStuff(CallbackInfo ci) { /** * @implNote Rendering order: *
      + *
    1. (Shifts everything backwards into the Z axis to + * not render over the ChatInputSuggestor and suggestion + * text)
    2. *
    3. The {@link #searchButton}
    4. *
    5. If the search bar should show:
    6. *
        @@ -230,7 +240,7 @@ protected void initSearchStuff(CallbackInfo ci) { *
      1. The {@link #searchField} itself
      2. *
      3. If it isn't null, the {@link #searchError}
      4. *
      - *
    7. If the settings menu should show:
    8. + *
    9. If the settings menu should show:
    10. *
        *
      1. The settings menu background
      2. *
      3. The setting buttons themselves
      4. @@ -242,35 +252,38 @@ protected void initSearchStuff(CallbackInfo ci) { *
      *
    */ - @Inject(method = "render", at = @At("HEAD")) - public void renderSearchStuff(DrawContext drawContext, int mX, int mY, float delta, CallbackInfo ci) { + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;render(Lnet/minecraft/client/gui/DrawContext;IIF)V")) + private void renderSearchAndContextMenuStuff(DrawContext context, int mX, int mY, float delta, CallbackInfo ci) { + context.getMatrices().push(); + context.getMatrices().translate(0, 0, -1); // easiest fix to render everything effectively under the ChatInputSuggestor (#186) + if(showSearch && !config.hideSearchButton) { - drawContext.fill(SEARCH_X - 2, height + SEARCH_Y_OFFSET - 2, (int) (width * (SEARCH_W_MULT + 0.06)), height + SEARCH_Y_OFFSET + SEARCH_H - 2, client.options.getTextBackgroundColor(Integer.MIN_VALUE)); - searchField.render(drawContext, mX, mY, delta); + context.fill(SEARCH_X - 2, height + SEARCH_Y_OFFSET - 2, (int) (width * (SEARCH_W_MULT + 0.06)), height + SEARCH_Y_OFFSET + SEARCH_H - 2, client.options.getTextBackgroundColor(Integer.MIN_VALUE)); + searchField.render(context, mX, mY, delta); // renders a suggestion-esq error message if the regex search is invalid if(searchError != null) { int x = searchField.getX() + 8 + (int) (width * SEARCH_W_MULT); - drawContext.drawTextWithShadow(textRenderer, searchError.getMessage().split( System.lineSeparator() )[0], x, searchField.getY(), 0xD00000); + context.drawTextWithShadow(textRenderer, searchError.getMessage().split( System.lineSeparator() )[0], x, searchField.getY(), 0xD00000); } } // renders the bg and the buttons for the settings menu if(showSettingsMenu && !config.hideSearchButton) { - drawContext.drawTexture( + context.drawTexture( id("textures/gui/search_settings_panel.png"), MENU_X, height + MENU_Y_OFFSET, 0, 0, MENU_WIDTH, MENU_HEIGHT, MENU_WIDTH, MENU_HEIGHT ); - caseSensitive.button.render(drawContext, mX, mY, delta); - modifiers.button.render(drawContext, mX, mY, delta); - regex.button.render(drawContext, mX, mY, delta); + caseSensitive.button.render(context, mX, mY, delta); + modifiers.button.render(context, mX, mY, delta); + regex.button.render(context, mX, mY, delta); } // renders the copy menu's selection box and menu buttons if( showCopyMenu && !hoveredVisibles.isEmpty() && !isMouseOverSettingsMenu(mX, mY) ) { ChatHud chatHud = client.inGameHud.getChatHud(); - ChatHudAccessor chat = ChatHudAccessor.from(chatHud); + ChatHudAccessor chat = (ChatHudAccessor) chatHud; List visibles = chat.chatpatches$getVisibleMessages(); @@ -284,8 +297,8 @@ public void renderSearchStuff(DrawContext drawContext, int mX, int mY, float del int i = visibles.indexOf( hoveredVisibles.get(hoveredParts - 1) ) - chat.chatpatches$getScrolledLines(); int hoveredY = sH - (i * lH) - shift; - drawContext.getMatrices().push(); - drawContext.getMatrices().scale((float) s, (float) s, 1.0f); + context.getMatrices().push(); + context.getMatrices().scale((float) s, (float) s, 1.0f); int borderW = sW + 8; int scissorY1 = MathHelper.floor((sH - (chatHud.getVisibleLineCount() * lH) - shift - 1) * s); @@ -294,21 +307,23 @@ public void renderSearchStuff(DrawContext drawContext, int mX, int mY, float del int selectionH = (lH * hoveredParts) + 1; // cuts off any of the selection rect that goes past the chat hud - drawContext.enableScissor(0, scissorY1, borderW, scissorY2); - drawContext.drawBorder(0, selectionY1, borderW, selectionH, config.copyColor + 0xff000000); - drawContext.disableScissor(); + context.enableScissor(0, scissorY1, borderW, scissorY2); + context.drawBorder(0, selectionY1, borderW, selectionH, config.copyColor + 0xff000000); + context.disableScissor(); - drawContext.getMatrices().pop(); + context.getMatrices().pop(); - mainButtons.values().forEach(menuButton -> menuButton.render(drawContext, mX, mY, delta)); + mainButtons.values().forEach(menuButton -> menuButton.render(context, mX, mY, delta)); hoverButtons.values().forEach(menuButton -> { if(menuButton.is( COPY_LINK_N.apply(0) )) - mainButtons.get(COPY_MENU_LINKS).children.forEach(linkButton -> linkButton.render(drawContext, mX, mY, delta)); + mainButtons.get(COPY_MENU_LINKS).children.forEach(linkButton -> linkButton.render(context, mX, mY, delta)); else - menuButton.render(drawContext, mX, mY, delta); + menuButton.render(context, mX, mY, delta); }); } + + context.getMatrices().pop(); } /** @@ -382,6 +397,14 @@ private Style fixMenuClickthroughStyle(ChatScreen screen, double mX, double mY, else return getTextStyleAt.call(screen, mX, mY); } + + @Inject(method = {"mouseClicked", "keyPressed"}, at = @At("RETURN")) + private void closeMenuOnInput(CallbackInfoReturnable cir) { + // hide copy menu if any other element was clicked first + if(cir.getReturnValue() && showCopyMenu) + showCopyMenu = false; + } + /** * Returns {@code true} if the mouse clicked on any of the following: *
      @@ -401,8 +424,12 @@ private Style fixMenuClickthroughStyle(ChatScreen screen, double mX, double mY, */ @Inject(method = "mouseClicked", at = @At("TAIL"), cancellable = true) public void afterClickBtn(double mX, double mY, int button, CallbackInfoReturnable cir) { - if(cir.getReturnValue()) + if(cir.getReturnValue()) { + if(showCopyMenu) // hide copy menu if any other element was clicked first + showCopyMenu = false; + return; + } if(searchField.mouseClicked(mX, mY, button)) cir.setReturnValue(true); @@ -511,10 +538,10 @@ private boolean isMouseOverCopyMenu(double mX, double mY) { if(mX < 0 || mY < 0) return new ArrayList<>(0); - final ChatHudAccessor chatHud = ChatHudAccessor.from(client); - final List visibles = chatHud.chatpatches$getVisibleMessages(); + final ChatHudAccessor chat = (ChatHudAccessor) client.inGameHud.getChatHud(); + final List visibles = chat.chatpatches$getVisibleMessages(); // using LineIndex instead of Index bc during testing they both returned the same value; LineIndex has less code - final int hoveredIndex = chatHud.chatpatches$getMessageLineIndex(chatHud.chatpatches$toChatLineX(mX), chatHud.chatpatches$toChatLineY(mY + config.shiftChat)); + final int hoveredIndex = chat.chatpatches$getMessageLineIndex(chat.chatpatches$toChatLineX(mX), chat.chatpatches$toChatLineY(mY)); if(hoveredIndex == -1) return new ArrayList<>(0); @@ -575,11 +602,11 @@ private boolean loadCopyMenu(double mX, double mY) { ChatHud chatHud = client.inGameHud.getChatHud(); - ChatHudAccessor chat = ChatHudAccessor.from(chatHud); + ChatHudAccessor chat = (ChatHudAccessor) chatHud; String hMF = TextUtils.reorder( hoveredVisibles.get(0).content(), false ); String hoveredMessageFirst = hMF.isEmpty() ? "\n" : hMF; // fixes messages starting with newlines not being detected - /* warning: longer messages sometimes fail because extra spaces appear to be added, + /* note: longer messages sometimes fail because extra spaces appear to be added, so i switched it to a startsWith() bc the first one never has extra spaces. /!\ can probably still fail /!\ */ // get hovered message index (messages) for all copying data selectedLine = @@ -700,9 +727,9 @@ private void onSearchFieldUpdate(String text) { } else if(!searchResults.isEmpty()) { // mark the text green if there are results, and only show those searchField.setEditableColor(0x55FF55); - ChatHudAccessor chatHud = ChatHudAccessor.from(client); - chatHud.chatpatches$getVisibleMessages().clear(); - chatHud.chatpatches$getVisibleMessages().addAll(searchResults); + ChatHudAccessor chat = (ChatHudAccessor) client.inGameHud.getChatHud(); + chat.chatpatches$getVisibleMessages().clear(); + chat.chatpatches$getVisibleMessages().addAll(searchResults); } } else { client.inGameHud.getChatHud().reset(); @@ -725,11 +752,11 @@ private void onSearchFieldUpdate(String text) { */ @Unique private List filterMessages(String target) { - final ChatHudAccessor chatHud = ChatHudAccessor.from(client); + final ChatHudAccessor chat = (ChatHudAccessor) client.inGameHud.getChatHud(); if(target == null) - return List.of(); //createVisibles( chatHud.chatpatches$getMessages() ); + return List.of(); //createVisibles( chat.chatpatches$getMessages() ); - List msgs = Lists.newArrayList( chatHud.chatpatches$getMessages() ); + List msgs = Lists.newArrayList( chat.chatpatches$getMessages() ); msgs.removeIf(hudLn -> { String content = TextUtils.reorder(hudLn.content().asOrderedText(), modifiers.on); @@ -753,7 +780,7 @@ private List filterMessages(String target) { /** * Creates a new list of to-be-rendered chat messages from the given list * of chat messages. The steps to achieving this are largely based on - * the first half of the {@link ChatHud#addMessage(Text, MessageSignatureData, int, MessageIndicator, boolean)} + * the first half of the {@link ChatHud#addMessage(Text, MessageSignatureData, MessageIndicator)} * method, specifically everything before the {@code while} loop. */ @Unique diff --git a/src/main/java/obro1961/chatpatches/mixin/gui/ScreenMixin.java b/src/main/java/obro1961/chatpatches/mixin/gui/ScreenMixin.java index cd5a620..f2e55f6 100644 --- a/src/main/java/obro1961/chatpatches/mixin/gui/ScreenMixin.java +++ b/src/main/java/obro1961/chatpatches/mixin/gui/ScreenMixin.java @@ -28,6 +28,6 @@ private void clearMessageDraft(int keyCode, int scanCode, int modifiers, Callbac return; if(((Screen)(Object)this) instanceof ChatScreen chatScreen) - ChatScreenAccessor.from(chatScreen).chatpatches$clearMessageDraft(); + ((ChatScreenAccessor)chatScreen).chatpatches$clearMessageDraft(); } } \ No newline at end of file diff --git a/src/main/java/obro1961/chatpatches/util/ChatUtils.java b/src/main/java/obro1961/chatpatches/util/ChatUtils.java index c51191c..82690ef 100644 --- a/src/main/java/obro1961/chatpatches/util/ChatUtils.java +++ b/src/main/java/obro1961/chatpatches/util/ChatUtils.java @@ -17,8 +17,7 @@ import java.time.Instant; import java.util.*; -import static obro1961.chatpatches.ChatPatches.config; -import static obro1961.chatpatches.ChatPatches.msgData; +import static obro1961.chatpatches.ChatPatches.*; import static obro1961.chatpatches.util.TextUtils.copyWithoutContent; import static obro1961.chatpatches.util.TextUtils.reorder; @@ -30,6 +29,8 @@ public class ChatUtils { public static final MessageData NIL_MSG_DATA = new MessageData(new GameProfile(ChatUtils.NIL_UUID, ""), Date.from(Instant.EPOCH), false); public static final int TIMESTAMP_INDEX = 0, MESSAGE_INDEX = 1, DUPE_INDEX = 2; // indices of all main (modified message) components public static final int MSG_TEAM_INDEX = 0, MSG_SENDER_INDEX = 1, MSG_CONTENT_INDEX = 2; // indices of all MESSAGE_INDEX components + /** Matches only an entire vanilla player message. */ + public static final String VANILLA_FORMAT = "(?i)^<[a-z0-9_]{3,16}>\\s.+$"; /** * Returns the message component at the given index; @@ -56,7 +57,7 @@ public static Text getMsgPart(Text message, int index) { } /** - * Returns a MutableText object representing the argument + * Returns a MutableText object of the argument * located at the given index of the given * {@link TranslatableTextContent}. Needed because of a weird * phenomenon where the {@link TranslatableTextContent#getArg(int)} @@ -67,7 +68,7 @@ public static Text getMsgPart(Text message, int index) { * and nulls in {@link Text#empty()}. * * @implNote - * If {@code index} is negative, adds it to the args array + * If {@code index} is negative, it's added to the args array * length. In other words, passing index {@code -n} will * get the {@code content.getArgs().length-n}th argument. */ @@ -75,18 +76,12 @@ 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(); + return switch( content.getArgs()[index] ) { + case Text t -> (MutableText) t; + case StringVisitable sv -> Text.literal(sv.getString()); + case String s -> Text.literal(s); + default -> Text.empty(); + }; } /** @@ -120,13 +115,14 @@ public static MutableText buildMessage(Style rootStyle, Text first, Text second, /** * 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)} + * This method is used in the {@link ChatHudMixin#modifyMessage(Text)} * 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. + * re-rendering chat messages; nor when the chat log is restoring, + * so simply return {@code m}. *
      3. Declare relevant variables, most notably the {@code timestamp} * and {@code content} components.
      4. *
      5. Reconstruct the player message if it should be reformatted @@ -168,11 +164,11 @@ public static MutableText buildMessage(Style rootStyle, Text first, Text second, *
      */ public static Text modifyMessage(@NotNull Text m, boolean refreshing) { - if( refreshing || Flags.LOADING_CHATLOG.isRaised() ) + if( refreshing || ChatLog.isRestoring() ) return m; // cancels modifications when loading the chatlog or regenerating visibles + boolean errorThrown = false; 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(); @@ -181,11 +177,13 @@ public static Text modifyMessage(@NotNull Text m, boolean refreshing) { MutableText content = m.copy(); try { - timestamp = (config.time && !boundary) ? config.makeTimestamp(now).setStyle( config.makeHoverStyle(now) ) : Text.empty().styled(s -> s.withInsertion(nowStr)); + timestamp = config.time ? 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()) { + // reconstruct the player message if it's in the vanilla format and should be reformatted + // the msgData vanilla means the original message was vanilla-formatted, and the regex check means it still is. + // see Xaero's Minimap waypoint sharing for more information (#158) + if(!lastEmpty && msgData.vanilla() && m.getString().matches(VANILLA_FORMAT)) { // 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(); @@ -198,7 +196,7 @@ public static Text modifyMessage(@NotNull Text m, boolean refreshing) { teamPart.append(Text.literal("-> ").setStyle(style)); // adds the team name for team messages - teamPart.append(getArg(ttc, 0).append(" ")); + teamPart.append(getArg(ttc, MSG_TEAM_INDEX).copy().append(" ")); // copy() fixes (#199) content.append(teamPart); } else { @@ -207,8 +205,8 @@ public static Text modifyMessage(@NotNull Text m, boolean refreshing) { // 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 + content.append(getArg(ttc, -1)); // always at the end, sometimes but not necessarily always at MSG_CONTENT_INDEX + } else { // reconstructs the message if it matches the vanilla format 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 -> { @@ -220,8 +218,13 @@ public static Text modifyMessage(@NotNull Text m, boolean refreshing) { 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() + "' !")); + Text firstPart = parts.stream() + .filter(p -> p.getString().contains(">")) + .findFirst() + .orElseThrow(() -> ChatPatches.logAndThrowReportMsg( + new IllegalStateException("No closing angle bracket found in vanilla message '" + m.getString() + "'!") + )); + String[] endBracketSplit = firstPart.getString().split(">"); // part of #156 AIOOBE i=1 fix String afterEndBracket = endBracketSplit.length > 1 ? endBracketSplit[1] : ""; // just get the part after the closing bracket, we know the start @@ -241,20 +244,33 @@ public static Text modifyMessage(@NotNull Text m, boolean refreshing) { // 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); + } catch(Exception e) { + LOGGER.error("[ChatUtils.modifyMessage] An error occurred while modifying message '{}', returning original:", m.getString()); + LOGGER.debug("[ChatUtils.modifyMessage] \tOriginal message structure: {}", m); + LOGGER.debug("[ChatUtils.modifyMessage] \tModified message structure:"); + LOGGER.debug("[ChatUtils.modifyMessage] \t\tTimestamp structure: {}", timestamp); + LOGGER.debug("[ChatUtils.modifyMessage] \t\tContent structure: {}", content); + ChatPatches.logReportMsg(e); + + errorThrown = true; } - // 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; + try { + // assembles constructed message + 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; + } catch(RuntimeException e) { + if(errorThrown) + ChatLog.addMessage(m); // in this case we already knew about the error, so do what we haven't yet: log the original message! + else + ChatPatches.logReportMsg(e); // here we never had an error, so something went wrong: log the error! + + msgData = ChatUtils.NIL_MSG_DATA; + return m; // return original bc we don't want to brick the chat due to pesky exceptions! + } } /** @@ -288,13 +304,18 @@ public static Text modifyMessage(@NotNull Text m, boolean refreshing) { public static Text tryCondenseMessage(Text incoming, int index) { final MinecraftClient client = MinecraftClient.getInstance(); final ChatHud chatHud = client.inGameHud.getChatHud(); - final ChatHudAccessor chat = ChatHudAccessor.from(chatHud); + final ChatHudAccessor chat = (ChatHudAccessor) chatHud; final List messages = chat.chatpatches$getMessages(); final List visibleMessages = chat.chatpatches$getVisibleMessages(); + // just in case the incoming message is a literal string text w no sibs, + // we can reformat it as to not throw any annoying errors down the line + if(incoming.getContent() instanceof PlainTextContent && incoming.getSiblings().isEmpty()) + incoming = buildMessage(null, null, incoming, null); + ChatHudLine comparingLine = messages.get(index); // message being compared List comparingParts = comparingLine.content().getSiblings(); - List incomingParts = new ArrayList<>( incoming.getSiblings() ); // prevents UOEs (1.20.3+ only) + List incomingParts = new ArrayList<>( incoming.getSiblings() ); // prevents UOEs for 1.20.3+ // IF the comparing and incoming message bodies are case-insensitively equal, @@ -338,7 +359,7 @@ public static Text tryCondenseMessage(Text incoming, int index) { while( !visibleMessages.isEmpty() && !visibleMessages.get(0).endOfEntry() ) visibleMessages.remove(0); } -ChatPatches.LOGGER.warn("new counter: '{}' index: {}", incomingParts.get(DUPE_INDEX).getString(), index); + // according to some testing, modifying incomingParts DOES modify incoming.getSiblings(), so all changes are taken care of! // ^ IGNORE ABOVE COMMENT ^ we have since wrapped incomingParts in a new ArrayList to prevent UOEs, so this is no longer true return TextUtils.newText(incoming.getContent(), incomingParts, incoming.getStyle()); diff --git a/src/main/java/obro1961/chatpatches/util/TextUtils.java b/src/main/java/obro1961/chatpatches/util/TextUtils.java index 348145f..f49f155 100644 --- a/src/main/java/obro1961/chatpatches/util/TextUtils.java +++ b/src/main/java/obro1961/chatpatches/util/TextUtils.java @@ -5,6 +5,7 @@ import net.minecraft.util.Formatting; import java.util.List; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -117,7 +118,7 @@ public static String getFormattingCodes(Style style) { style.getColor() != null ? (color = Formatting.byName(style.getColor().getName())) != null ? "&" + color.getCode() - : "&" + style.getColor().getHexCode() + : String.format(Locale.ROOT, "&#%06X", style.getColor().getRgb()) // as of 1.21.1, #getHexColor is private. WHAT THE FUCK : "&r" ); codes.append( style.isBold() ? "&l" : "" ); diff --git a/src/main/resources/assets/chatpatches/lang/en_us.json b/src/main/resources/assets/chatpatches/lang/en_us.json index e015bf8..9296084 100644 --- a/src/main/resources/assets/chatpatches/lang/en_us.json +++ b/src/main/resources/assets/chatpatches/lang/en_us.json @@ -75,17 +75,18 @@ "text.chatpatches.chatlogBackup": "Backup", "text.chatpatches.chatlogOpenFolder": "Open folder", "text.chatpatches.desc.chatlog": "Should the chat be saved into a log so it can be re-added back into the chat in later game sessions?", - "text.chatpatches.desc.chatlogSaveInterval": "How long should the Chat Log wait before saving to disk? This is in minutes, the minimum is 1. Set to 0 to only save when paused (warning: setting to <5 will lag a lot). All values try to save on exit.", + "text.chatpatches.desc.chatlogSaveInterval": "How long should the Chat Log wait before saving to disk? This is in minutes, the minimum is 1. Set to 0 to only save when paused (warning: setting to <5 will lag a lot). All values try to save during crashes, and all save on game exit.", "text.chatpatches.desc.chatlogClear": "Press to clear the chat log. Note that this does not clear the chat itself, only the log. §c§lThis cannot be undone!§r", "text.chatpatches.desc.chatlogClearHistory": "Press to clear only the chat log's sent messages. Note that this does not clear the chat itself, only the log. §c§lThis cannot be undone!§r", "text.chatpatches.desc.chatlogClearMessages": "Press to clear only the chat log's regular messages. Note that this does not clear the chat itself, only the log. §c§lThis cannot be undone!§r", - "text.chatpatches.desc.chatlogLoad": "Press to load the chat log into the chat. (§6Warning: this does NOT clear the chat beforehand, all messages will be appended to the end!§r)", - "text.chatpatches.desc.chatlogSave": "Press to save the chat log to disk. §c§lThis will overwrite any data currently stored in the file.§r", - "text.chatpatches.desc.chatlogBackup": "Press to backup the chat log to disk. This will create a new file with 'backup_' plus the current date formatted in the 'yyyy-MM-dd_HH-mm-ss' pattern as the name; located in the same directory as the regular chat log.", + "text.chatpatches.desc.chatlogLoad": "Press to load the chat log into the chat. (§6Warning: this does NOT clear the chat beforehand, all messages will be appended to the end!§r§f) §3You must be in-game to execute this action.§r", + "text.chatpatches.desc.chatlogSave": "Press to save the chat log to disk. §c§lThis will overwrite any data currently stored in the file. §r§3You must be in-game to execute this action.§r", + "text.chatpatches.desc.chatlogBackup": "Press to backup the chat log to disk. This will create a new file with 'backup_' plus the current date formatted in the 'yyyy-MM-dd_HH.mm.ss' pattern as the name; located in the same directory as the regular chat log.", "text.chatpatches.desc.chatlogOpenFolder": "Press to open §9/logs/§f§r§f, the folder where the chat log and any backups of it are stored.", "text.chatpatches.chatHidePacket": "Ignore hide message packet", "text.chatpatches.chatWidth": "Override chat width", + "text.chatpatches.chatHeight": "Override chat height", "text.chatpatches.chatMaxMessages": "Maximum chat messages", "text.chatpatches.chatNameFormat": "Playername text", "text.chatpatches.chatNameColor": "Playername color", @@ -97,6 +98,7 @@ "text.chatpatches.vanillaClearing": "Vanilla chat clearing", "text.chatpatches.desc.chatHidePacket": "Should hide message packets that delete chat messages be ignored?", "text.chatpatches.desc.chatWidth": "The width of the chat box. This overrides vanilla's default and allows for a much larger width. Set to 0 to use the vanilla setting and not override it.", + "text.chatpatches.desc.chatHeight": "The height of the chat box. This overrides vanilla's default and allows for a much larger height. Set to 0 to use the vanilla setting and not override it.", "text.chatpatches.desc.chatMaxMessages": "The max amount of chat messages allowed to save. Vanilla caps it at 100, this mod can increase it up to 32,767. Keep in mind a higher max equals higher memory usage.", "text.chatpatches.desc.chatNameFormat": "The text that replaces the playername in chat messages. Vanilla is '<$>', name only is '$'; where '$' is a placeholder for the playername. Only applies to player-sent messages.", "text.chatpatches.desc.chatNameColor": "The color that's filled in where it would otherwise be blank white in the resulting formatted playername. To use this with other formatting modifiers, use '&r' in the decoration text option.", @@ -113,7 +115,9 @@ "text.chatpatches.help.regex": "For regular expression information, click me!", "text.chatpatches.help.regexTester": "For testing regular expressions, click me!", "text.chatpatches.help.reloadConfig": "Reload Config", + "text.chatpatches.help.missing": "§cCannot open config screen", "text.chatpatches.desc.help.reloadConfig": "Press to read the config file and load any changes. §c§lThis will override any unsaved changes made here!§r§f You need to exit and re-open this screen to see the changes take effect.", + "text.chatpatches.desc.help.missing": "§7Please install %s §7to access the config in-game!", "text.chatpatches.search.suggestion": "Search...", diff --git a/src/main/resources/assets/chatpatches/lang/ko_kr.json b/src/main/resources/assets/chatpatches/lang/ko_kr.json new file mode 100644 index 0000000..3bbaa5e --- /dev/null +++ b/src/main/resources/assets/chatpatches/lang/ko_kr.json @@ -0,0 +1,151 @@ +{ + "modmenu.descriptionTranslation.chatpatches": "채팅을 보다 커스텀 할 수 있는 다기능 클라이언트 모드입니다. 보다 자세한 정보는 설정과 GitHub 페이지를 참고해주세요! \n디스코드 가입은 https://discord.gg/3MqBvNEyMz!", + + "text.chatpatches.title": "Chat Patches 설정", + + "text.chatpatches.category.time": "타임스탬프(시간 표기)", + "text.chatpatches.category.hover": "타임스탬프 호버 표시", + "text.chatpatches.category.counter": "중복 메시지 카운터", + "text.chatpatches.category.counter.compact": "§9CompactChat§r의 중복 카운터", + "text.chatpatches.category.boundary": "세션 경계", + "text.chatpatches.category.chatlog": "채팅 기록", + "text.chatpatches.category.chatlog.actions": "작업", + "text.chatpatches.category.chat": "채팅 인터페이스", + "text.chatpatches.category.chat.hud": "HUD (Heads Up Display)", + "text.chatpatches.category.chat.name": "플레이어 이름", + "text.chatpatches.category.chat.screen": "스크린", + "text.chatpatches.category.copy": "복사 메뉴", + "text.chatpatches.category.help": "도움말", + "text.chatpatches.category.debug": "디버그", + "text.chatpatches.category.desc.counter.compact": "§9§nCompactChat§r 모드를 기반으로 한 보다 \"조작적으로\" 중복 채팅을 정리하는 옵션입니다.", + "text.chatpatches.category.desc.chatlog.actions": "채팅 기록에서 수행할 수 있는 실행 가능한 작업. §c§경고: §r§c이러한 작업은 되돌릴 수 없습니다!§r", + "text.chatpatches.category.desc.chat": "채팅 HUD, 채팅 화면 및 메시지와 관련된 설정입니다.", + "text.chatpatches.category.desc.chat.hud": "채팅 화면을 열지 않았을 때도 표시되는 부분에 관한 설정. 채팅 전체에 관련된 설정도 포함하고 있습니다.", + "text.chatpatches.category.desc.chat.name": "바닐라 채팅 메시지에서 볼 수 있는 플레이어 이름에 대한 옵션입니다.", + "text.chatpatches.category.desc.chat.screen": "채팅 화면 관련 설정입니다.", + "text.chatpatches.category.desc.copy": "채팅 메시지를 우클릭했을 때 표시되는 선택 상자와 복사 메뉴에 관한 설정입니다.", + "text.chatpatches.category.desc.help": "이 모드의 일부 사용법을 설명하고 있는 웹 사이트입니다.", + "text.chatpatches.category.desc.debug": "개발을 위한 잡다한 것들?", + + "text.chatpatches.time": "타임스탬프 표시", + "text.chatpatches.timeDate": "타임스탬프 문자열", + "text.chatpatches.timeFormat": "타임스탬프 장식 문자열", + "text.chatpatches.timeColor": "타임스탬프 색", + "text.chatpatches.desc.time": "모든 메시지의 맨 앞에 타임스탬프를 표시하겠습니까?", + "text.chatpatches.desc.timeDate": "타임스탬프의 문자열 서식을 지정하는 설정입니다. 자세한 내용은 도움말 > 날짜 및 시간 서식을 참조하십시오.", + "text.chatpatches.desc.timeFormat": "타임스탬프를 생성하는 ['&'+서식 코드]로 구성된 문자열. '$'에는 이 위에서 설정한 서식이 있는 타임스탬프가 표시됩니다.", + "text.chatpatches.desc.timeColor": "생성되는 타임스탬프의 장식이 없는 부분에 붙이는 색. 이것을 다른 서식 코드와 함께 사용하는 경우, 장식의 문자열 설정에서 '&r'을 사용하세요.", + + "text.chatpatches.hover": "호버 표시", + "text.chatpatches.hoverDate": "호버 문자열", + "text.chatpatches.hoverFormat": "호버 장식 문자열", + "text.chatpatches.hoverColor": "호버 문자 색", + "text.chatpatches.desc.hover": "커서를 채팅 타임스탬프에 올릴 때 더 자세한 정보를 표시할건가요?", + "text.chatpatches.desc.hoverDate": "호버 창의 긴 날짜 표기의 문자열 서식을 지정하는 설정입니다. 자세한 내용은 도움말 > 날짜 및 시간 서식을 참조하십시오.", + "text.chatpatches.desc.hoverFormat": "호버의 상세한 정보를 생성하는 ['&'+서식 코드]로 구성된 문자열. \n'$'에는 이 위에서 설정한 서식이 있는 호버가 표시됩니다.", + "text.chatpatches.desc.hoverColor": "생성되는 타임스탬프의 장식이 없는 부분에 붙이는 색. 이것을 다른 서식 코드와 함께 사용하는 경우, 장식의 문자열 설정에서 '&r'을 사용하세요.", + + "text.chatpatches.counter": "중복 메시지 카운터 표시", + "text.chatpatches.counterFormat": "중복 메시지 카운터 문자열", + "text.chatpatches.counterColor": "중복 메시지 카운터 색", + "text.chatpatches.counterCheckStyle": "확인 서식 코드", + "text.chatpatches.counterCompact": "CompactChat 방식 표시", + "text.chatpatches.counterCompactDistance": "메시지 확인 범위", + "text.chatpatches.desc.counter": "중복 메시지 뒤에 중복 횟수를 표기할까요? .\n 주의: CompactChat의 옵션을 사용하기 위해서는 이 설정도 켤 필요가 있습니다.", + "text.chatpatches.desc.counterFormat": "메시지의 끝에 추가되어 그 중복 횟수를 나타내는 ['&'+서식 코드]로 구성된 문자열. \n'$'에는 중복 횟수가 표시됩니다. CompactChat 방식을 켠 경우, 같이 적용됩니다.", + "text.chatpatches.desc.counterColor": "중복 횟수의 장식이 없는 부분에 붙이는 색. 이것을 다른 서식 코드와 함께 사용하는 경우, 장식의 문자열 설정에서 '&r'을 사용하세요. CompactChat 방식을 켠 경우, 같이 적용됩니다.", + "text.chatpatches.desc.counterCheckStyle": "중복 여부를 확인할 때 메시지 카운터에서 서식 코드를 확인합니까? 예를 들어, \"§e§labc§r\" 와 \"abc\" 는 중복으로 간주되지 않습니다. CompactChat 방식을 활성화 한 경우, 동일하게 적용됩니다.", + "text.chatpatches.desc.counterCompact": "중복 메시지 카운터의 동작을 CompactChat 모드와 동일하게 합니까?", + "text.chatpatches.desc.counterCompactDistance": "메시지를 한 묶음으로 하는 채팅 범위. '-1'로 하면 모든 메시지 리스트, '0'으로 하면 한 번에 표시할 수 있는 최대 수의 메시지를 대상으로 합니다. 한 번에 표시할 수 있는 메시지 수는 채팅의 설정과 GUI의 크기에 기반합니다.\n 예를 들어 이 수치를 '7'로 만들면, 다음에 수신할 메시지를 기존의 7개의 메시지 중 하나로 정리하려고 합니다.", + + "text.chatpatches.boundary": "경계 표시", + "text.chatpatches.boundaryFormat": "경계 문자열", + "text.chatpatches.boundaryColor": "경계 색", + "text.chatpatches.desc.boundary": "월드/서버에서 나간 후 다른 세계/서버에 입장 채팅창에 경계선을 표시할까요?", + "text.chatpatches.desc.boundaryFormat": "경계선의 문자열 서식을 지정하는 ['&'+서식 코드]로 구성된 문자열.\n'$'에는 월드 이름이 위치하며 자동으로 줄 바꿈 됩니다.", + "text.chatpatches.desc.boundaryColor": "생성되는 경계선의 장식이 없는 부분에 붙이는 색. 이것을 다른 서식 코드와 함께 사용하는 경우, 장식의 문자열 설정에서 '&r'을 사용하세요.", + + "text.chatpatches.chatlog": "채팅 기록 표시", + "text.chatpatches.chatlogSaveInterval": "저장 간격 (분)", + "text.chatpatches.chatlogClear": "모든 메시지 지우기", + "text.chatpatches.chatlogClearHistory": "보낸 메시지 §6§l%s§r개 지우기", + "text.chatpatches.chatlogClearMessages": "받은 메시지 §6§l%s§r개 지우기", + "text.chatpatches.chatlogLoad": "불러오기", + "text.chatpatches.chatlogSave": "저장하기", + "text.chatpatches.chatlogBackup": "백업하기", + "text.chatpatches.chatlogOpenFolder": "폴더 열기", + "text.chatpatches.desc.chatlog": "다른 게임 세션에서 복원할 수 있도록 채팅을 기록에 저장할까요?", + "text.chatpatches.desc.chatlogSaveInterval": "채팅 기록를 디스크에 저장할 때 시간 간격을 설정해주세요. 이는 분 단위이며 최소값은 1입니다. 일시 중지 시에만 저장하려면 0으로 설정하십시오(경고: 5분 이내로 설정하면 많이 렉이 걸릴 수 있음). 모든 값은 종료 시에 저장하려고 합니다.", + "text.chatpatches.desc.chatlogClear": "클릭하면 채팅 기록이 지워집니다.. 이렇게 하면 채팅 자체가 지워지는 것이 아니라 기록만 지워집니다. §c§l한 번 실행하면 절대 되돌릴 수 없습니다!§r", + "text.chatpatches.desc.chatlogClearHistory": "클릭하면 채팅 기록의 보낸 메시지만 지워집니다. 이렇게 하면 채팅 자체가 지워지는 것이 아니라 기록만 지워집니다. §c§l한 번 실행하면 절대 되돌릴 수 없습니다!§r", + "text.chatpatches.desc.chatlogClearMessages": "클릭하면 채팅 기록의 일반 메시지만 지워집니다. 이렇게 하면 채팅 자체가 지워지는 것이 아니라 기록만 지워집니다. §c§l한 번 실행하면 절대 되돌릴 수 없습니다!§r", + "text.chatpatches.desc.chatlogLoad": "클릭하면 채팅 기록을 채팅에 불러옵니다. (§6경고: 채팅이 미리 지워지지 않습니다. 모든 메시지가 끝에 추가됩니다!§r)", + "text.chatpatches.desc.chatlogSave": "클릭하면 채팅 기록을 디스크에 저장합니다. §c§l현재 파일에 저장된 모든 데이터를 덮어씁니다.§r", + "text.chatpatches.desc.chatlogBackup": "클릭하면 채팅 기록를 디스크에 백업합니다. 그러면 'backup_' 와 이름과 같은 'yyyy-MM-ddd_HH-mm-ss' 패턴으로 포맷된 현재 날짜가 포함된 새 파일이 생성됩니다. 일반 채팅 기록와 동일한 디렉토리에 있습니다.", + "text.chatpatches.desc.chatlogOpenFolder": "클릭하면 채팅 기록과 기록의 백업이 저장 되는 폴더인 §9<마인크래프트 저장 경로>/logs/§f§r§f를 엽니다.", + + "text.chatpatches.chatHidePacket": "숨김 메시지 패킷 무시", + "text.chatpatches.chatWidth": "채팅창 너비 덮어쓰기", + "text.chatpatches.chatMaxMessages": "최대 메시지 수", + "text.chatpatches.chatNameFormat": "플레이어 이름 문자열", + "text.chatpatches.chatNameColor": "플레이어 이름 색", + "text.chatpatches.shiftChat": "채팅창 이동", + "text.chatpatches.messageDrafting": "채팅 입력 유지", + "text.chatpatches.onlyInvasiveDrafting": "예기치 않게 중단된 채팅 입력 유지", + "text.chatpatches.searchDrafting": "찾기 입력 유지", + "text.chatpatches.hideSearchButton": "찾기 버튼 숨기기", + "text.chatpatches.vanillaClearing": "바닐라처럼 채팅 지우기", + "text.chatpatches.desc.chatHidePacket": "채팅 메시지를 삭제하는 숨김 메시지 패킷을 무시합니까?", + "text.chatpatches.desc.chatWidth": "채팅창의 너비 설정, 바닐라 기본 설정을 덮어쓰고 더 넓은 너비로 만들 수 있습니다. '0'으로 하면 바닐라 설정을 적용합니다.", + "text.chatpatches.desc.chatMaxMessages": "저장할 수 있는 최대 메시지 수를 설정합니다. 바닐라에서의 최대는 100개지만, 이 모드에서는 32,767개까지 늘릴 수 있습니다. 다만, 최대 수를 크게 할수록 메모리 사용량이 많아지는 것에 주의해 주세요.", + "text.chatpatches.desc.chatNameFormat": "채팅 메시지에 플레이어 이름을 대체하는 문자열. 바닐라에선 '<$>'이고, '$'에 이름이 위치합니다. 플레이어가 직접 보낸 메시지에만 적용됩니다.", + "text.chatpatches.desc.chatNameColor": "설정이 없는 경우 형식이 지정된 플레이어 이름이 빈 흰색이 됩니다. 이것을 다른 서식 코드와 함께 사용하는 경우, 장식의 문자열 설정에서 '&r'을 사용하세요.", + "text.chatpatches.desc.shiftChat": "방어구나 체력 게이지에 겹치지 않도록 채팅창을 위로 올립니다. 기본값은 10, 올리지 않을 경우는 '0'으로 합니다..", + "text.chatpatches.desc.messageDrafting": "채팅을 닫고 다시 열었을 때 채팅 입력란의 글자를 유지할까요?", + "text.chatpatches.desc.onlyInvasiveDrafting": "예기치 않게 중단된 채팅 입력란의 글자를 유지할까요? 예를 들어, 서버가 GUI를 열어 채팅을 종료하거나, 플레이어가 다른 차원으로 이동하거나, 플레이어가 사망한 경우가 있습니다.", + "text.chatpatches.desc.searchDrafting": "채팅을 닫고 다시 열었을 때 검색창의 글자를 유지할까요?", + "text.chatpatches.desc.hideSearchButton": "검색 버튼을 숨길까요? 검색 버튼을 숨기면 모든 검색 기능이 비활성화된다는 점을 유의해 주세요.", + "text.chatpatches.desc.vanillaClearing": "바닐라와 마찬가지로 월드 서버에서 나갈 때 채팅을 삭제할지 여부를 결정할 수 있습니다. 활성화하면 채팅과 채팅 기록이 전부 삭제될 것이라는 점에 주의해 주세요.", + + "text.chatpatches.help.dateFormat": "날짜 및 시간 서식 정보를 보려면 여기를 클릭하세요!", + "text.chatpatches.help.formatCodes": "서식 코드 문자를 사용하는 방법과 각 문자의 기능에 대해 알고 싶으면 여기를 클릭하세요!", + "text.chatpatches.help.faq": "자주 묻는 질문(FAQ)을 보려면 여기를 클릭하세요!", + "text.chatpatches.help.regex": "정규 표현에 대해 더 많은 정보를 얻고 싶으면 여기를 클릭하세요!", + "text.chatpatches.help.regexTester": "정규 표현을 테스트해보고 싶으면 여기를 클릭하세요!", + "text.chatpatches.help.reloadConfig": "설정 리로드", + "text.chatpatches.desc.help.reloadConfig": "설정 파일을 읽고 변경 사항을 로드하려면 누르세요. §c§l이 작업은 여기서 저장되지 않은 변경 사항을 덮어씁니다!§r§f 변경 사항이 적용되려면 이 화면을 종료하고 다시 열어야 합니다.", + + + "text.chatpatches.search.suggestion": "검색...", + "text.chatpatches.search.caseSensitive": "대소문자 구분", + "text.chatpatches.search.modifiers": "서식 코드", + "text.chatpatches.search.regex": "정규 표현", + "text.chatpatches.search.desc": "§b좌클릭§r으로 검색창,§d우클릭§r으로 설정 메뉴의 표시를 바꿉니다.\n\n§a녹색§r은 찾는 것이 있을 때,§e노란색§r은 찾는 것이 없을 때,§c빨간색§r은 검색이 무효일 때를 나타냅니다.", + "text.chatpatches.search.desc.caseSensitive": "검색에서 대문자와 소문자를 구분할까요?\n\n구별하지 않는 경우 \"abc\"는 자신 이외에 \"Abc\" 또는 \"ABC\"도 동일하게 검색되지만, 구별하는 경우\"abc\"만 검색됩니다.\n§l§o주의:§r 이것은 정규 표현에도 적용됩니다.", + "text.chatpatches.search.desc.modifiers": "검색의 일치 판정에서 서식 코드를 포함시킬까요?\n\n서식 코드를 포함하지 않는 경우, \"abc\"는 \"abc\" 또는 \"§e§labc§r\"와 일치하지만, 서식 코드를 포함하는 경우에는 \"abc\"만 일치하고, \"§e§labc§r\"에는 일치하지 않습니다.\n§l§o주의:§r 16진수 색 코드는\"&#§cRR§aGG§9BB§r\"의 형식으로 일치합니다만,§n이 기능은 미완성입니다§f.", + "text.chatpatches.search.desc.regex": "검색에서 정규 표현을 사용할까요?\n\n정규 표현의 사용법에 대해서는, 설정 화면의 도움말 탭을 참조해 주세요.\n§l§o주의:§r Java의 정규 표현은 §n완전 일치를 요구§f하기 때문에, 앞에는\"^.*\", 뒤에는\".*$\"을 붙이는 것을 잊지 마세요.", + + + "text.chatpatches.copyReplyFormat": "답장 텍스트 형식", + "text.chatpatches.copyColor": "메시지 선택 색", + "text.chatpatches.desc.copyReplyFormat": "채팅 메시지를 오른쪽 클릭하고 '답장' 버튼을 클릭한 후 채팅 상자에 채워지는 텍스트입니다. '$'는 필수이며, 메시지 발신자의 이름으로 대체됩니다.", + "text.chatpatches.desc.copyColor": "채팅 메시지를 마우스 오른쪽 버튼으로 클릭하면 표시되는 선택 상자의 색입니다.", + + "text.chatpatches.copy.copied": "복사된 메시지 요소: \"%s\"!", + "text.chatpatches.copy.copyString": "문자열 복사 >", + "text.chatpatches.copy.rawString": "원시", + "text.chatpatches.copy.formattedString": "서식", + "text.chatpatches.copy.jsonString": "JSON", + "text.chatpatches.copy.links": "링크 복사 >", + "text.chatpatches.copy.linkN": "링크 %d", + "text.chatpatches.copy.timestamp": "타임스탬프 복사 >", + "text.chatpatches.copy.timestampText": "문자열", + "text.chatpatches.copy.timestampHoverText": "호버 텍스트", + "text.chatpatches.copy.unix": "Unix 시간 복사", + "text.chatpatches.copy.sender": "발신자 복사 >", + "text.chatpatches.copy.name": "유저 이름", + "text.chatpatches.copy.uuid": "UUID", + "text.chatpatches.copy.reply": "답장", + + "text.chatpatches.restored": "복원됨" +} \ No newline at end of file diff --git a/src/main/resources/assets/chatpatches/lang/pt_br.json b/src/main/resources/assets/chatpatches/lang/pt_br.json index 4e68d9a..ed93b4a 100644 --- a/src/main/resources/assets/chatpatches/lang/pt_br.json +++ b/src/main/resources/assets/chatpatches/lang/pt_br.json @@ -1,47 +1,153 @@ { - "text.chatpatches.title": "Onde está a configuração do meu histórico de bate-papo", + "modmenu.descriptionTranslation.chatpatches": "Um mod de chat versátil para o cliente que torna o chat mais personalizável. Confira a configuração e a página do GitHub para mais informações!\nEntre no Discord em https://discord.gg/3MqBvNEyMz!", + + "text.chatpatches.title": "Configuração do Chat Patches", "text.chatpatches.category.time": "Carimbo de data e hora", - "text.chatpatches.category.hover": "Informações de foco", - "text.chatpatches.category.counter": "contador duplicado", - "text.chatpatches.category.boundary": "Limite da Sessão", - "text.chatpatches.category.chat": "Interface de bate-papo", + "text.chatpatches.category.hover": "Informação de data ao passar o mouse", + "text.chatpatches.category.counter": "Contador de duplicatas", + "text.chatpatches.category.counter.compact": "Contador de duplicatas do §9CompactChat§r", + "text.chatpatches.category.boundary": "Limite de sessão", + "text.chatpatches.category.chatlog": "Registro de chat", + "text.chatpatches.category.chatlog.actions": "Ações", + "text.chatpatches.category.chat": "Interface de chat", + "text.chatpatches.category.chat.hud": "HUD (exibição de cabeçalho)", + "text.chatpatches.category.chat.name": "Nome do jogador", + "text.chatpatches.category.chat.screen": "Tela", + "text.chatpatches.category.copy": "Menu de cópia", "text.chatpatches.category.help": "Ajuda", + "text.chatpatches.category.debug": "Debug", + "text.chatpatches.category.desc.counter.compact": "Opções sobre o método de duplicação de chat \"manipulativo\", baseado no mod §9§nCompactChat§r.", + "text.chatpatches.category.desc.chatlog.actions": "Ações executáveis que podem ser realizadas no registro de chat. §c§lATENÇÃO: §r§cEssas ações não podem ser desfeitas!§r", + "text.chatpatches.category.desc.chat": "Qualquer coisa relacionada ao HUD do chat, à tela de chat ou às mensagens.", + "text.chatpatches.category.desc.chat.hud": "Opções sobre o widget que aparece mesmo quando a tela de chat não está aberta. Também inclui configurações relacionadas ao chat como um todo.", + "text.chatpatches.category.desc.chat.name": "Opções sobre os nomes dos jogadores vistos nas mensagens padrão do chat.", + "text.chatpatches.category.desc.chat.screen": "Opções específicas de tela.", + "text.chatpatches.category.desc.copy": "Opções sobre a caixa de seleção e o menu de cópia que aparecem ao clicar com o botão direito nas mensagens de chat.", + "text.chatpatches.category.desc.help": "Executáveis e sites que explicam como usar partes do mod!", + "text.chatpatches.category.desc.debug": "Coisas aleatórias para desenvolvimento?", + + "text.chatpatches.time": "Alternar carimbo de data", + "text.chatpatches.timeDate": "Texto de carimbo de data", + "text.chatpatches.timeFormat": "Texto decorativo do carimbo de data", + "text.chatpatches.timeColor": "Cor do carimbo de data", + "text.chatpatches.desc.time": "Exibir um carimbo de data na frente de todas as mensagens?", + "text.chatpatches.desc.timeDate": "O texto que é formatado em um carimbo de data. Consulte a seção Ajuda > Formatação de Data e Hora para mais informações.", + "text.chatpatches.desc.timeFormat": "A string de '&' + modificadores de código de formatação e texto decorativo que cria o carimbo de data. '$' é um marcador de posição para a opção de carimbo de data formatado acima desta.", + "text.chatpatches.desc.timeColor": "A cor preenchida onde de outra forma ficaria em branco no carimbo de data resultante. Para usar com outros modificadores de formatação, use '&r' na opção de texto decorativo.", + + "text.chatpatches.hover": "Alternar ao passar o mouse", + "text.chatpatches.hoverDate": "Texto ao passar o mouse", + "text.chatpatches.hoverFormat": "Texto decorativo ao passar o mouse", + "text.chatpatches.hoverColor": "Cor do texto ao passar o mouse", + "text.chatpatches.desc.hover": "Exibir uma descrição de hora mais detalhada ao passar o mouse sobre o carimbo de data no chat?", + "text.chatpatches.desc.hoverDate": "O texto que é formatado em uma string de data/hora mais longa. Consulte a seção Ajuda > Formatação de Data e Hora para mais informações.", + "text.chatpatches.desc.hoverFormat": "A string de '&' + modificadores de código de formatação e texto decorativo que cria a descrição detalhada ao passar o mouse. Requer um '$' como marcador de posição para a opção de carimbo de data formatado acima desta.", + "text.chatpatches.desc.hoverColor": "A cor preenchida onde de outra forma ficaria em branco no texto resultante ao passar o mouse. Para usar com outros modificadores de formatação, use '&r' na opção de texto decorativo.", - "text.chatpatches.time": "Alternar carimbo de data/hora", - "text.chatpatches.timeDate": "Texto do carimbo de data/hora", - "text.chatpatches.timeFormat": "Modificadores de carimbo de data/hora", - "text.chatpatches.timeColor": "Cor do carimbo de data/hora", - "text.chatpatches.desc.time": "Um carimbo de data/hora deve aparecer na frente de todas as mensagens?", - "text.chatpatches.desc.timeDate": "O texto que é usado para o carimbo de data/hora. Para texto dinâmico, pesquise online por 'string de formato de data java'.", - "text.chatpatches.desc.timeFormat": "A string de e comercial (&) e códigos de formatação que se aplicam à string de tempo.", - "text.chatpatches.desc.timeColor": "A cor que é 'preenchida' quando aplicável no texto do carimbo de data/hora.", - - "text.chatpatches.hover": "Passe o mouse", - "text.chatpatches.hoverDate": "Passe o texto", - "text.chatpatches.desc.hover": "O texto que aparece quando você passa o mouse sobre o carimbo de data/hora deve mostrar?", - "text.chatpatches.desc.hoverDate": "O texto que aparece ao passar o mouse sobre o texto do carimbo de data/hora. Para texto dinâmico, pesquise online por 'string de formato de data java'.", - - "text.chatpatches.counter": "Alternância do contador de mensagens", + "text.chatpatches.counter": "Alternar contador de mensagens", "text.chatpatches.counterFormat": "Texto do contador de mensagens", "text.chatpatches.counterColor": "Cor do contador de mensagens", - "text.chatpatches.desc.counter": "Um contador de mensagens deve ser exibido após uma mensagem para indicar que várias foram enviadas?", - "text.chatpatches.desc.counterFormat": "O texto adicionado ao final de uma mensagem para indicar que várias duplicatas foram enviadas. Deve incluir um $ para o número de duplicados.", - "text.chatpatches.desc.counterColor": "A cor que é 'preenchida' quando aplicável no texto do contador, por exemplo '&r' será substituída por esta cor.", + "text.chatpatches.counterCheckStyle": "Verificar modificadores de formatação", + "text.chatpatches.counterCompact": "Alternar método CompactChat", + "text.chatpatches.counterCompactDistance": "Distância de verificação de mensagens", + "text.chatpatches.desc.counter": "Exibir um contador de mensagens após mensagens duplicadas para indicar múltiplos envios? Nota: isso precisa estar habilitado para que as opções do CompactChat funcionem.", + "text.chatpatches.desc.counterFormat": "O texto que é adicionado ao final de uma mensagem para indicar múltiplos envios duplicados. Requer um '$' para o número de duplicatas e também suporta '&' + modificadores de código de formatação. Ainda se aplica ao método CompactChat, se habilitado.", + "text.chatpatches.desc.counterColor": "A cor preenchida onde de outra forma ficaria em branco no contador de duplicatas resultante. Para usar com outros modificadores de formatação, use '&r' na opção de texto decorativo. Ainda se aplica ao método CompactChat, se habilitado.", + "text.chatpatches.desc.counterCheckStyle": "O contador de mensagens deve verificar modificadores de formatação ao verificar duplicatas? Por exemplo, \"§e§labc§r\" e \"abc\" não seriam considerados duplicatas se verdadeiro. Ainda se aplica ao método CompactChat, se habilitado.", + "text.chatpatches.desc.counterCompact": "O contador de mensagens deve se comportar como o do mod CompactChat?", + "text.chatpatches.desc.counterCompactDistance": "A distância (inclusiva) em que as mensagens serão verificadas para compactação. Definir como '-1' tentará toda a lista de mensagens, e '0' tentará usar o máximo de mensagens visíveis com base nas escalas de chat e de interface. Por exemplo, definir para '7' tentará compactar a próxima mensagem recebida em uma das últimas 7 mensagens já presentes.", "text.chatpatches.boundary": "Alternar limite", - "text.chatpatches.boundaryFormat": "Texto de limite", + "text.chatpatches.boundaryFormat": "Texto do limite", "text.chatpatches.boundaryColor": "Cor do limite", - "text.chatpatches.desc.boundary": "Uma linha de limite deve aparecer depois de usar o bate-papo, sair e entrar novamente mais tarde?", - "text.chatpatches.desc.boundaryFormat": "O texto que é mostrado quando uma linha de limite é inserida no chat, adicione e comercial (&) seguido de um código de formatação para embelezá-lo.", - "text.chatpatches.desc.boundaryColor": "A cor que é 'preenchida' onde aplicável no texto de limite, por exemplo '&r' será substituída por esta cor.", + "text.chatpatches.desc.boundary": "Exibir uma linha limite ao conversar, sair de um mundo/servidor e depois entrar em outro mundo/servidor?", + "text.chatpatches.desc.boundaryFormat": "O texto formatado e usado para a linha limite. Suporta '&' + modificadores de código de formatação, '\\n' para novas linhas e '$' como marcador de posição para o nome do mundo.", + "text.chatpatches.desc.boundaryColor": "A cor preenchida onde de outra forma ficaria em branco na linha limite resultante. Para usar com outros modificadores de formatação, use '&r' na opção de texto decorativo.", - "text.chatpatches.chatLog": "Alternar registro de bate-papo", - "text.chatpatches.chatMaxMessages": "Máximo de mensagens de bate-papo", + "text.chatpatches.chatlog": "Alternar registro de chat", + "text.chatpatches.chatlogSaveInterval": "Intervalo de salvamento (minutos)", + "text.chatpatches.chatlogClear": "Limpar todas as mensagens", + "text.chatpatches.chatlogClearHistory": "Limpar §6§l%s§r mensagens enviadas", + "text.chatpatches.chatlogClearMessages": "Limpar §6§l%s§r mensagens recebidas", + "text.chatpatches.chatlogLoad": "Carregar", + "text.chatpatches.chatlogSave": "Salvar", + "text.chatpatches.chatlogBackup": "Backup", + "text.chatpatches.chatlogOpenFolder": "Abrir pasta", + "text.chatpatches.desc.chatlog": "O chat deve ser salvo em um registro para ser re-adicionado ao chat em sessões de jogo futuras?", + "text.chatpatches.desc.chatlogSaveInterval": "Quanto tempo o registro de chat deve esperar antes de salvar no disco? O mínimo é 1 minuto. Defina como 0 para salvar apenas quando pausado (aviso: configurar para menos de 5 causará muito atraso). Todos os valores tentam salvar em caso de falhas e todos salvam ao sair do jogo.", + "text.chatpatches.desc.chatlogClear": "Pressione para limpar o log do chat. Observe que isso não limpa o chat em si, apenas o log. §c§lIsso não pode ser desfeito!§r", + "text.chatpatches.desc.chatlogClearHistory": "Pressione para limpar apenas as mensagens enviadas no log do chat. Observe que isso não limpa o chat em si, apenas o log. §c§lIsso não pode ser desfeito!§r", + "text.chatpatches.desc.chatlogClearMessages": "Pressione para limpar apenas as mensagens regulares no log do chat. Observe que isso não limpa o chat em si, apenas o log. §c§lIsso não pode ser desfeito!§r", + "text.chatpatches.desc.chatlogLoad": "Pressione para carregar o log do chat no chat. (§6Aviso: isso NÃO limpa o chat antes, todas as mensagens serão adicionadas ao final!§r§f) §3Você deve estar no jogo para executar esta ação.§r", + "text.chatpatches.desc.chatlogSave": "Pressione para salvar o log do chat no disco. §c§lIsso sobrescreverá qualquer dado armazenado no arquivo. §r§3Você deve estar no jogo para executar esta ação.§r", + "text.chatpatches.desc.chatlogBackup": "Pressione para fazer backup do log do chat no disco. Isso criará um novo arquivo com 'backup_' mais a data atual formatada no padrão 'yyyy-MM-dd_HH-mm-ss' como o nome; localizado no mesmo diretório do log regular do chat.", + "text.chatpatches.desc.chatlogOpenFolder": "Pressione para abrir §9/logs/§f§r§f, a pasta onde o log do chat e quaisquer backups são armazenados.", + + "text.chatpatches.chatHidePacket": "Ignorar pacote de ocultação de mensagens", + "text.chatpatches.chatWidth": "Substituir largura do chat", + "text.chatpatches.chatHeight": "Substituir altura do chat", + "text.chatpatches.chatMaxMessages": "Máximo de mensagens do chat", "text.chatpatches.chatNameFormat": "Texto do nome do jogador", - "text.chatpatches.shiftChat": "Interface de bate-papo de turno", - "text.chatpatches.desc.chatLog": "O bate-papo deve ser salvo em um log para que possa ser adicionado novamente ao bate-papo em outra sessão de jogo?", - "text.chatpatches.desc.chatMaxMessages": "A quantidade máxima de mensagens de chat permitidas para processar. Vanilla tem esse conjunto como 100, o máximo permitido é 32.767. Tenha em mente que quanto maior o valor, mais memória o chat requer.", - "text.chatpatches.desc.chatNameFormat": "O texto que substitui o nome do jogador nas mensagens de bate-papo. Vanilla é <$>, sem colchetes é $; onde $ representa o nome do jogador. Aplica-se apenas a mensagens do jogador.", - "text.chatpatches.desc.shiftChat": "Essa interface de bate-papo deve ser aumentada em cerca de 10 pixels para não obstruir a barra de blindagem?" + "text.chatpatches.chatNameColor": "Cor do nome do jogador", + "text.chatpatches.shiftChat": "Deslocar chat", + "text.chatpatches.messageDrafting": "Alternar rascunho do chat", + "text.chatpatches.onlyInvasiveDrafting": "Alternar rascunho invasivo apenas", + "text.chatpatches.searchDrafting": "Alternar rascunho de pesquisa", + "text.chatpatches.hideSearchButton": "Ocultar botão de pesquisa", + "text.chatpatches.vanillaClearing": "Limpeza padrão do chat", + "text.chatpatches.desc.chatHidePacket": "Os pacotes de mensagem de ocultação que excluem mensagens de chat devem ser ignorados?", + "text.chatpatches.desc.chatWidth": "A largura da caixa de chat. Isso substitui o padrão do jogo e permite uma largura muito maior. Defina como 0 para usar a configuração padrão do jogo e não substituí-la.", + "text.chatpatches.desc.chatHeight": "A altura da caixa de chat. Isso substitui o padrão do jogo e permite uma altura muito maior. Defina como 0 para usar a configuração padrão do jogo e não substituí-la.", + "text.chatpatches.desc.chatMaxMessages": "A quantidade máxima de mensagens do chat que podem ser salvas. O padrão é 100, mas este mod pode aumentar para até 32.767. Lembre-se de que um limite maior consome mais memória.", + "text.chatpatches.desc.chatNameFormat": "O texto que substitui o nome do jogador nas mensagens do chat. O padrão é '<$>', apenas o nome é '$'; onde '$' é um marcador para o nome do jogador. Aplica-se apenas a mensagens enviadas por jogadores.", + "text.chatpatches.desc.chatNameColor": "A cor que é preenchida onde, de outra forma, seria branco. Para usar isso com outros modificadores de formatação, use '&r' na opção de texto decorativo.", + "text.chatpatches.desc.shiftChat": "Desloca a interface do chat para cima para não obstruir a barra de armadura e/ou vida. O padrão é 10, defina como 0 para não deslocar.", + "text.chatpatches.desc.messageDrafting": "Qualquer texto no campo de chat deve permanecer após fechar e reabrir o chat?", + "text.chatpatches.desc.onlyInvasiveDrafting": "O texto no campo de chat deve persistir apenas quando o chat é fechado inesperadamente? Por exemplo, quando o chat é fechado pelo servidor ao abrir uma GUI, o jogador muda de dimensão ou morre.", + "text.chatpatches.desc.searchDrafting": "Qualquer texto no campo de pesquisa deve permanecer após fechar e reabrir o chat?", + "text.chatpatches.desc.hideSearchButton": "O botão de pesquisa deve ser ocultado? Observe que isso desativa toda a funcionalidade de pesquisa.", + "text.chatpatches.desc.vanillaClearing": "As mensagens de chat devem ser limpas após sair de um mundo/servidor como no padrão? Observe que isso apaga completamente tanto o chat quanto o log do chat.", + + "text.chatpatches.help.dateFormat": "Para informações sobre formatação de data e hora, clique aqui!", + "text.chatpatches.help.formatCodes": "Para caracteres de código de formatação e o que eles fazem, clique aqui!", + "text.chatpatches.help.faq": "Para perguntas frequentes (FAQ), clique aqui!", + "text.chatpatches.help.regex": "Para informações sobre expressões regulares, clique aqui!", + "text.chatpatches.help.regexTester": "Para testar expressões regulares, clique aqui!", + "text.chatpatches.help.reloadConfig": "Recarregar Configuração", + "text.chatpatches.help.missing": "§cNão é possível abrir a tela de configuração", + "text.chatpatches.desc.help.reloadConfig": "Pressione para ler o arquivo de configuração e carregar quaisquer alterações. §c§lIsso substituirá quaisquer mudanças não salvas feitas aqui!§r§f Você precisa sair e reabrir esta tela para ver as mudanças.", + "text.chatpatches.desc.help.missing": "§7Por favor, instale %s §7para acessar a configuração no jogo!", + + "text.chatpatches.search.suggestion": "Pesquisar...", + "text.chatpatches.search.caseSensitive": "Diferenciar maiúsculas/minúsculas", + "text.chatpatches.search.modifiers": "Modificadores", + "text.chatpatches.search.regex": "Regex", + "text.chatpatches.search.desc": "§bClique com o botão esquerdo§r para alternar a barra de pesquisa, ou §dclique com o botão direito§r para alternar o menu de configurações.\n\n§aVerde§r significa que a pesquisa correspondeu, §eamarelo§r significa que nada correspondeu, e §cvermelho§r significa que a pesquisa é inválida.", + "text.chatpatches.search.desc.caseSensitive": "A consulta de pesquisa deve corresponder apenas ao texto com a mesma capitalização?\n\n\"abc\" corresponderia a si mesmo, \"Abc\" e \"ABC\"; sensível a maiúsculas/minúsculas corresponderia apenas a \"abc\".\n§l§oNOTA:§r Isso também se aplica à pesquisa regex.", + "text.chatpatches.search.desc.modifiers": "A consulta de pesquisa deve permitir corresponder ao texto de acordo com modificadores de formatação?\n\n\"abc\" corresponderia a \"abc\" e \"§e§labc§r\"; com modificadores só corresponderia a \"abc\" e não \"§e§labc§r\".\nNOTA: Cores hexadecimais §opodem§r ser correspondidas no formato \"&#§cRR§aGG§9BB§r\", no entanto §nestá em beta§f.", + "text.chatpatches.search.desc.regex": "A consulta de pesquisa deve ser usada como uma §2reg§rular §2ex§rpressão?\n\nPara saber como usar regex, veja a seção de ajuda na configuração.\n§l§oNOTA:§r Regex em Java §nrequer uma correspondência completa§f, então lembre-se de colocar \"^.*\" no início e \".*$\" no final.", + + "text.chatpatches.copyReplyFormat": "Formato do texto de resposta", + "text.chatpatches.copyColor": "Cor de seleção de mensagem", + "text.chatpatches.desc.copyReplyFormat": "O texto que é preenchido na caixa de chat após clicar com o botão direito em uma mensagem de chat e clicar no botão 'Responder' no menu de cópia. '$' é necessário e será preenchido com o nome do remetente.", + "text.chatpatches.desc.copyColor": "A cor da caixa de seleção que é mostrada após clicar com o botão direito em uma mensagem de chat.", + + "text.chatpatches.copy.copied": "Componente de mensagem copiado: \"%s\"!", + "text.chatpatches.copy.copyString": "Copiar Texto >", + "text.chatpatches.copy.rawString": "Bruto", + "text.chatpatches.copy.formattedString": "Formatado", + "text.chatpatches.copy.jsonString": "JSON", + "text.chatpatches.copy.links": "Copiar Link(s) >", + "text.chatpatches.copy.linkN": "Link %d", + "text.chatpatches.copy.timestamp": "Copiar Carimbo de Tempo >", + "text.chatpatches.copy.timestampText": "Texto", + "text.chatpatches.copy.timestampHoverText": "Texto ao Passar o Mouse", + "text.chatpatches.copy.unix": "Copiar Hora Unix", + "text.chatpatches.copy.sender": "Copiar remetente >", + "text.chatpatches.copy.name": "Nome de usuário", + "text.chatpatches.copy.uuid": "UUID", + "text.chatpatches.copy.reply": "Responder", + + "text.chatpatches.restored": "Restaurado" } \ No newline at end of file diff --git a/src/main/resources/chatpatches.mixins.json b/src/main/resources/chatpatches.mixins.json index abcdb5d..f3e8c9c 100644 --- a/src/main/resources/chatpatches.mixins.json +++ b/src/main/resources/chatpatches.mixins.json @@ -2,12 +2,11 @@ "required": true, "minVersion": "0.8", "package": "obro1961.chatpatches.mixin", - "compatibilityLevel": "JAVA_17", + "compatibilityLevel": "JAVA_21", "verbose": true, "mixinPriority": 2000, "mixins": [], "client": [ - "MinecraftClientMixin", "chat.MessageHandlerMixin", "gui.ChatHudMixin", "gui.ChatScreenMixin", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 4a5d0ca..5bc34be 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -4,7 +4,7 @@ "version": "${version}", "name": "Chat Patches", - "description": "A versatile client-side chat mod that makes the chat more useful and customizable! Check out the config and the GitHub page for more info!\nJoin the Discord at https://discord.gg/3MqBvNEyMz!", + "description": "A versatile client-side chat mod that makes the chat more useful and customizable! Check out the config and the GitHub page for more info! Join the Discord at https://discord.gg/3MqBvNEyMz!", "authors": [ "OBro1961" ], @@ -17,7 +17,11 @@ "Fiz-Victor (bugfixes)", "co-91 (ja_jp)", "JustALittleWolf (#118, #123, #131, #132)", - "SmajloSlovakian (#139-141)" + "SmajloSlovakian (#139-141)", + "DylanKeir (#190)", + "Nooiee (ko_kr/#193)", + "suoyukii (zh_cn/#203)", + "demorogabrtz (pt_br/#204)" ], "contact": { "homepage": "https://modrinth.com/mod/MOqt4Z5n", @@ -42,16 +46,14 @@ ], "depends": { - "java": ">=17", - "minecraft": [ "1.20.3", "1.20.4" ], - "fabricloader": ">=0.15.0", + "java": ">=21", + "minecraft": [ "1.20.4" ], + "fabricloader": ">=0.16.9", "fabric-api": "*", "yet_another_config_lib_v3": "*" }, "recommends": { - "modmenu": "*", - "catalogue": "*", - "menulogue": "*" + "modmenu": "*" }, "suggests": { "nochatreports": "*"