diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..74ccfc8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[{*.gradle,*.properties,*.java,*.json,*.accesswidener}] +charset = utf-8 +end_of_line = lf +indent_style = tab +insert_final_newline = true +max_line_length = 120 + +[*.properties] +ij_properties_keep_blank_lines = true diff --git a/.gitattributes b/.gitattributes index dfe0770..cf3c948 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,10 @@ -# Auto detect text files and perform LF normalization -* text=auto +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +* test=auto eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0900f62..5c5e0a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,37 +4,27 @@ # against bad commits. name: build -on: [pull_request, push] +on: [ pull_request, push ] jobs: build: - strategy: - matrix: - # Use these Java versions - java: [ - 21, # Current Java LTS & minimum supported by Minecraft - ] - # and run on both Linux and Windows - os: [ubuntu-22.04, windows-2022] - runs-on: ${{ matrix.os }} + runs-on: ubuntu-24.04 steps: - name: checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: validate gradle wrapper - uses: gradle/wrapper-validation-action@v1 - - name: setup jdk ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: gradle/actions/wrapper-validation@v4 + - name: setup jdk + uses: actions/setup-java@v4 with: - java-version: ${{ matrix.java }} + java-version: '23' distribution: 'microsoft' - name: make gradle wrapper executable - if: ${{ runner.os != 'Windows' }} run: chmod +x ./gradlew - - name: build - run: ./gradlew build + - name: build all + run: ./gradlew buildAll - name: capture build artifacts - if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Artifacts path: build/libs/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index c476faf..e32aea1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ hs_err_*.log replay_*.log *.hprof *.jfr + +# file auto-generated by build.gradle for preprocessor +build.properties diff --git a/README.md b/README.md index 9292cd7..374cd53 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,113 @@ -# ChatPlus -![Static Badge](https://img.shields.io/badge/Minecraft_version-1.20.1_%7C_1.20.4_%7C_1.21.4-red?style=flat) +# Chat Plus + +![Static Badge](https://img.shields.io/badge/Minecraft_version-1.19%2B_%7C_1.20%2B_%7C_1.21%2B-red?style=flat) - A Simple mod add bukkit style with an "&" and "[item]" display for Fabric Server. - - It's a **server-side** mod and doesn't need to be installed on the client. +A simple Fabric mod that adds Bukkit-style color codes (using "&") and "[item]" display in chat. + +It can be installed on the **client** or **server**: +- Install on the **server only** if you want players to send styled messages in-game +- Install on the **host client** (the one creating the LAN multiplayer session) for LAN multiplayer functionality +- Install on the **client** for single-player functionality + + + +Compatibility has been implemented for these mods: Styled Chat, Dynmap
+_* Other mods may work without explicit support. Report compatibility requests via [GitHub Issues](https://github.com/CPTProgrammer/ChatPlus)._ ## Screenshots + ![image](https://github.com/CPTProgrammer/ChatPlus/assets/46586216/dcd2ca4b-79e9-4692-a8de-02fddfe4a392) - **^^^ Display the item in main hand** +**^^^ Display the item in the main hand** ![image](https://github.com/CPTProgrammer/ChatPlus/assets/46586216/b0449371-35aa-451b-ab9c-af464f6c597d) - **^^^ Colorful Text** +**^^^ Colorful Text** ![image](https://github.com/CPTProgrammer/ChatPlus/assets/46586216/dc3f0451-cbea-4610-8575-ea5da4e97f0f) - **^^^ Display items in slots** +**^^^ Display items in slots** + +**^^^ Escape character `&`** + + + +## Minecraft Versions + +| Development Version | Compatible Versions | +| ------------------- | ------------------- | +| 1.19 | 1.19 | +| 1.19.1 | 1.19.1 - 1.19.2 | +| 1.19.3 | 1.19.3 - 1.19.4 | +| 1.20 | 1.20 - 1.20.2 | +| 1.20.3 | 1.20.3 - 1.20.6 | +| 1.21 | 1.21 - 1.21.5 | + + + +## Development + +#### Environment + +- Java 23 or higher +- (Optional) Java IDE with Manifold support (e.g., IntelliJ IDEA)
+ _* If using IntelliJ IDEA, the Manifold plugin should be installed_ + +#### Switching Versions + +Set `minecraft_version` in `gradle.properties`, or append `-Pmc=x.x.x` parameter to Gradle commands (e.g., `./gradlew build -Pmc=1.20.3`) to switch Minecraft versions for development. + +> **Note for IDE users:** +> +> After modifying `gradle.properties` or `./properties/*.properties`, remember to reload the Gradle project (or click "Load Gradle Changes" button) + +**Cross-version Testing** + +To test mod compatibility with newer Minecraft versions: + +- Method 1: + - Set `minecraft_version` to target version, and configure matching `test_fabric_api_version` (e.g., `0.121.0+1.21.5`) + - Reload project to download dependencies (for IDE users) + - Place additional test mods in `./run/mods` + - Run Gradle task `runClient` or `runServer` (_May encounter unexpected launch issues - if this occurs, try Method 2_) +- Method 2: Build JAR file and deploy to `mods` folder of the target Minecraft version client/server + +> **Note:** +> +> The `minecraft_version` can be a version not explicitly listed in `./properties/*.properties` files. The `settings.gradle` script will automatically select the nearest compatible properties configuration. + +#### Build + +Run one of the following commands to build the JAR: + +```bash +# Build the JAR for the currently configured Minecraft version +./gradlew build # Windows: .\gradlew build + +# Build for a different Minecraft version, e.g. +./gradlew build -Pmc=1.20.3 +``` + +To build JARs for all Minecraft versions defined in `./properties`: + +```bash +./gradlew buildAll # Windows: .\gradlew buildAll +``` + +> **Note:** +> +> All compiled JARs are output to `./build/libs` + + ## Contributors + + +#### Special Thanks + +- [@SAGUMEDREAM](https://github.com/SAGUMEDREAM) for advice on mixin development. +- [Distant-Horizons-Team/Distant Horizons](https://gitlab.com/distant-horizons-team/distant-horizons/) for inspiring the build configuration design. + diff --git a/build.gradle b/build.gradle index c9af6ae..89efa98 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,20 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import java.nio.file.Paths + plugins { - id 'fabric-loom' version '1.9-SNAPSHOT' - id 'maven-publish' + id "fabric-loom" version "1.10-SNAPSHOT" + id "maven-publish" + + // Manifold preprocessor + id "systems.manifold.manifold-gradle-plugin" version "0.0.2-alpha" } -version = project.mod_version + "-mc" + project.minecraft_version +// Transfer gradle extra properties to root project extra properties +gradle.ext.properties.each { prop -> project.ext.set(prop.key, prop.value) } + +generateBuildProperties(project.minecraftVersions, project.targetVersion) + +version = "${project.mod_version}-mc${project.targetVersion}" group = project.maven_group base { @@ -16,45 +27,120 @@ repositories { // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. // See https://docs.gradle.org/current/userguide/declaring_repositories.html // for more information about repositories. -} -loom { - splitEnvironmentSourceSets() + mavenCentral() - mods { - "chat-plus" { - sourceSet sourceSets.main - sourceSet sourceSets.client + // Modrinth Maven + exclusiveContent { + forRepository { + maven { + name = "Modrinth" + url = "https://api.modrinth.com/maven" + } + } + filter { + includeGroup "maven.modrinth" } } + // Placeholder API (for StyledChat mod) Maven + maven { + url "https://maven.nucleoid.xyz/" + name "Nucleoid" + } + + // Manifold preprocessor + maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } +} + +loom { +// splitEnvironmentSourceSets() +// +// mods { +// "modid" { +// sourceSet sourceSets.main +// sourceSet sourceSets.client +// } +// } } dependencies { - // To change the versions see the gradle.properties file + // To change the versions see the gradle.properties file or the ./properties/${minecraftVersion}.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc:fabric-loader:${project.fabric_loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}+${project.targetVersion}" - // Fabric API. This is technically optional, but you probably want it anyway. - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - - // Uncomment the following line to enable the deprecated Fabric API modules. - // These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time. + // Override Fabric API version for local testing (runClient/runServer) + if (project.hasProperty("test_fabric_api_version")) + modLocalRuntime "net.fabricmc.fabric-api:fabric-api:${project.test_fabric_api_version}" - // modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}" + // Mod Compatibility + if (project.hasProperty("mod_dynmap_version")) + modCompileOnly "maven.modrinth:dynmap:${project.mod_dynmap_version}" + if (project.hasProperty("mod_styled_chat_version")) + modCompileOnly "maven.modrinth:styled-chat:${project.mod_styled_chat_version}" + if (project.hasProperty("mod_styled_chat_placeholder_api_version")) + modCompileOnly "eu.pb4:placeholder-api:${project.mod_styled_chat_placeholder_api_version}" + + // Manifold + annotationProcessor "systems.manifold:manifold-preprocessor:${project.manifold_version}" } processResources { - inputs.property "version", project.version + def resourceTargets = [ + "fabric.mod.json" + ] + def expandProperties = [ + "version": mod_version, + "group": maven_group, + "minecraft_version": minecraft_version, + "java_version": java_version, + "fabric_loader_version": fabric_loader_version, + "fabric_loader_version_range": hasProperty("min_fabric_loader_version") && !min_fabric_loader_version.allWhitespace ? ">=${min_fabric_loader_version}" : "*", + "fabric_api_version": fabric_api_version, + "fabric_api_mod_id": fabric_api_mod_id + ] - filesMatching("fabric.mod.json") { - expand "version": project.version + inputs.properties(expandProperties) + + filesMatching(resourceTargets) { + expand inputs.properties + } +} + +tasks.register("buildAll") { + group = "build" + description = "Builds JARs for all defined Minecraft versions" + doLast { + // Locate the project-specific Java runtime + def javaToolchain = javaToolchains.launcherFor(java.toolchain).get() + def projectJavaHome = javaToolchain.metadata.installationPath + + logger.lifecycle("Using Java ${javaToolchain.metadata.javaRuntimeVersion} (path=\"${projectJavaHome}\")") + + project.minecraftVersions.each { version -> + logger.lifecycle("================ Building for Minecraft ${version} ================") + // TODO: Refactor this (is there a better approach?) + def execOutput = project.providers.exec { + commandLine Paths.get(rootProject.rootDir.absolutePath, DefaultNativePlatform.currentOperatingSystem.windows ? "gradlew.bat" : "gradlew"), + "build", "-Pmc=${version}" + ignoreExitValue true + environment "JAVA_HOME": projectJavaHome + } + def result = execOutput.result.get() + def output = execOutput.standardOutput.asText.get() + def error = execOutput.standardError.asText.get() + logger.lifecycle("[Minecraft ${version}] BUILD ${result.exitValue == 0 ? "SUCCESS" : "FAILED"}. Process exited with code: ${result.exitValue}") + if (result.exitValue != 0) { + logger.lifecycle("[Minecraft ${version}] Error: ${error}") + } + } } } tasks.withType(JavaCompile).configureEach { - it.options.release = 17 + it.options.release = project.java_version.toInteger() } java { @@ -63,20 +149,22 @@ java { // If you remove this line, sources will not be generated. withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + targetCompatibility = sourceCompatibility = JavaVersion.toVersion(project.java_version) } jar { + inputs.property "archivesName", project.base.archivesName + from("LICENSE") { - rename { "${it}_${project.base.archivesName.get()}"} + rename { "${it}_${inputs.properties.archivesName}" } } } // configure the maven publication publishing { publications { - mavenJava(MavenPublication) { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name from components.java } } @@ -88,4 +176,30 @@ publishing { // The repositories here will be used for publishing your artifact, not for // retrieving dependencies. } -} \ No newline at end of file +} + + + +/** ======================================= */ +/** ================ Utils ================ */ +/** ======================================= */ + +/** + *

Generates version mapping properties file for preprocessor with format:

+ * - {@code MC_VER=}
+ * - {@code MC_X_Y_Z=} for each sorted version + * + * @param sortedVersions Pre-ordered list of Minecraft versions + * @param currentVersion Target version to mark as {@code MC_VER} + */ +def generateBuildProperties(List sortedVersions, String currentVersion) { + def currentIndex = sortedVersions.findIndexOf { it == currentVersion } + file(Paths.get(rootDir.absolutePath, "./build.properties")).write("""\ + # ======================================================== + # ====!! AUTO-GENERATED FILE - DO NOT MANUALLY EDIT !!==== + # ======================================================== + + MC_VER=${currentIndex} + ${sortedVersions.indexed().collect { index, version -> "MC_${version.replace('.', '_')}=${index}" }.join("\n")} + """.replace('\t', '')) +} diff --git a/gradle.properties b/gradle.properties index cdd5733..37d19ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,16 +2,19 @@ org.gradle.jvmargs=-Xmx1G org.gradle.parallel=true -# Fabric Properties -# check these on https://fabricmc.net/develop -minecraft_version=1.21.4 -yarn_mappings=1.21.4+build.8 -loader_version=0.16.9 +# Directory path containing version-specific properties files for Minecraft +version_properties_path=./properties # Mod Properties -mod_version=0.21.0 +mod_version=1.0.0 maven_group=cn.revaria.chatplus archives_base_name=chat-plus +# Minecraft +minecraft_version=1.21 + +# Override Fabric API version for local testing (runClient/runServer) +#test_fabric_api_version= + # Dependencies -fabric_version=0.115.0+1.21.4 \ No newline at end of file +manifold_version=2025.1.9 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c..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 cea7a79..e18bc25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca..f3b75f3 100755 --- 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,8 @@ 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\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +147,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 +155,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 +204,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/properties/1.19.1.properties b/properties/1.19.1.properties new file mode 100644 index 0000000..cc671a0 --- /dev/null +++ b/properties/1.19.1.properties @@ -0,0 +1,20 @@ +java_version=17 + +# Fabric Properties +# check these on https://fabricmc.net/develop +yarn_mappings=1.19.1+build.6 +fabric_loader_version=0.16.10 +min_fabric_loader_version= +fabric_api_version=0.58.4 +# Fabric API mod ID conventions: +# - For Minecraft versions <1.19.2: use "fabric" +# - For Minecraft versions >=1.19.2: use "fabric-api" +# Ref: https://wiki.fabricmc.net/tutorial:setup +fabric_api_mod_id=fabric + +# Mod Compatibility +! Dynmap 3.7-beta-4 +mod_dynmap_version=c0uFN8Lt +! Styled Chat 1.4.0+1.19.1 +mod_styled_chat_version=1.4.0+1.19.1 +mod_styled_chat_placeholder_api_version=2.0.0-beta.7+1.19 diff --git a/properties/1.19.3.properties b/properties/1.19.3.properties new file mode 100644 index 0000000..3361bde --- /dev/null +++ b/properties/1.19.3.properties @@ -0,0 +1,20 @@ +java_version=17 + +# Fabric Properties +# check these on https://fabricmc.net/develop +yarn_mappings=1.19.3+build.5 +fabric_loader_version=0.16.10 +min_fabric_loader_version= +fabric_api_version=0.68.1 +# Fabric API mod ID conventions: +# - For Minecraft versions <1.19.2: use "fabric" +# - For Minecraft versions >=1.19.2: use "fabric-api" +# Ref: https://wiki.fabricmc.net/tutorial:setup +fabric_api_mod_id=fabric-api + +# Mod Compatibility +! Dynmap 3.7-beta-4 +mod_dynmap_version=SrVT9jSf +! Styled Chat 2.1.4+1.19.3 +mod_styled_chat_version=2.1.4+1.19.3 +mod_styled_chat_placeholder_api_version=2.0.0-rc.1+1.19.3 diff --git a/properties/1.19.properties b/properties/1.19.properties new file mode 100644 index 0000000..24c37b2 --- /dev/null +++ b/properties/1.19.properties @@ -0,0 +1,20 @@ +java_version=17 + +# Fabric Properties +# check these on https://fabricmc.net/develop +yarn_mappings=1.19+build.4 +fabric_loader_version=0.16.10 +min_fabric_loader_version= +fabric_api_version=0.55.1 +# Fabric API mod ID conventions: +# - For Minecraft versions <1.19.2: use "fabric" +# - For Minecraft versions >=1.19.2: use "fabric-api" +# Ref: https://wiki.fabricmc.net/tutorial:setup +fabric_api_mod_id=fabric + +# Mod Compatibility +! Dynmap 3.7-beta-4 +mod_dynmap_version=nLhQSh2j +! Styled Chat 1.3.3+1.19 +mod_styled_chat_version=1.3.3+1.19 +mod_styled_chat_placeholder_api_version=2.0.0-beta.7+1.19 diff --git a/properties/1.20.3.properties b/properties/1.20.3.properties new file mode 100644 index 0000000..b365d81 --- /dev/null +++ b/properties/1.20.3.properties @@ -0,0 +1,20 @@ +java_version=17 + +# Fabric Properties +# check these on https://fabricmc.net/develop +yarn_mappings=1.20.3+build.1 +fabric_loader_version=0.16.10 +min_fabric_loader_version= +fabric_api_version=0.91.1 +# Fabric API mod ID conventions: +# - For Minecraft versions <1.19.2: use "fabric" +# - For Minecraft versions >=1.19.2: use "fabric-api" +# Ref: https://wiki.fabricmc.net/tutorial:setup +fabric_api_mod_id=fabric-api + +# Mod Compatibility +! Dynmap 3.7-beta-6 +mod_dynmap_version=icNjNwag +! Styled Chat 2.2.0+1.20 +mod_styled_chat_version=2.4.2+1.20.4 +mod_styled_chat_placeholder_api_version=2.4.0-pre.3+1.20.4 diff --git a/properties/1.20.properties b/properties/1.20.properties new file mode 100644 index 0000000..02b47a2 --- /dev/null +++ b/properties/1.20.properties @@ -0,0 +1,20 @@ +java_version=17 + +# Fabric Properties +# check these on https://fabricmc.net/develop +yarn_mappings=1.20+build.1 +fabric_loader_version=0.16.10 +min_fabric_loader_version= +fabric_api_version=0.83.0 +# Fabric API mod ID conventions: +# - For Minecraft versions <1.19.2: use "fabric" +# - For Minecraft versions >=1.19.2: use "fabric-api" +# Ref: https://wiki.fabricmc.net/tutorial:setup +fabric_api_mod_id=fabric-api + +# Mod Compatibility +! Dynmap 3.7-beta-6 +mod_dynmap_version=IIQSYMHC +! Styled Chat 2.2.0+1.20 +mod_styled_chat_version=2.2.0+1.20 +mod_styled_chat_placeholder_api_version=2.1.1+1.20 diff --git a/properties/1.21.properties b/properties/1.21.properties new file mode 100644 index 0000000..6c99113 --- /dev/null +++ b/properties/1.21.properties @@ -0,0 +1,20 @@ +java_version=21 + +# Fabric Properties +# check these on https://fabricmc.net/develop +yarn_mappings=1.21+build.9 +fabric_loader_version=0.16.10 +min_fabric_loader_version= +fabric_api_version=0.100.1 +# Fabric API mod ID conventions: +# - For Minecraft versions <1.19.2: use "fabric" +# - For Minecraft versions >=1.19.2: use "fabric-api" +# Ref: https://wiki.fabricmc.net/tutorial:setup +fabric_api_mod_id=fabric-api + +# Mod Compatibility +! Dynmap 3.7-beta-8 +mod_dynmap_version=1pMUPhY2 +! Styled Chat 2.6.1+1.21 +mod_styled_chat_version=2.6.1+1.21 +mod_styled_chat_placeholder_api_version=2.4.2+1.21 diff --git a/settings.gradle b/settings.gradle index 75c4d72..52788dd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,5 @@ +import java.nio.file.Paths + pluginManagement { repositories { maven { @@ -7,4 +9,92 @@ pluginManagement { mavenCentral() gradlePluginPortal() } -} \ No newline at end of file +} + +/** + * Load compatible properties file for current Minecraft version in {@code version_properties_path} and + * define {@code minecraftVersions}, {@code targetVersion} + */ +def loadProperties() { + def propertyFiles = fileTree(Paths.get(rootDir.absolutePath, version_properties_path as String)).files.name + def minecraftVersions = propertyFiles.collect { it.replaceAll(/\.properties$/, "") } + + minecraftVersions.sort { a, b -> compareMinecraftVersion(a, b) } + gradle.ext.minecraftVersions = minecraftVersions + + // Prefer the version defined by the command line argument -Pmc=x.x.x + def inputVersion = hasProperty("mc") ? + validateMinecraftVersionFormat(mc as String, "Invalid Minecraft version provided via -Pmc=${minecraft_version}") : + validateMinecraftVersionFormat(minecraft_version, "Invalid Minecraft version in gradle.properties: minecraft_version=${minecraft_version}") + gradle.ext.minecraft_version = inputVersion + + def targetVersion = findLatestCompatibleVersion(minecraftVersions, inputVersion) ?: + { throw new GradleException("Unsupported Minecraft version: ${inputVersion}") }() + println "Target Minecraft version: ${targetVersion}" + gradle.ext.targetVersion = targetVersion + + def props = new Properties() + file(Paths.get(rootDir.absolutePath, version_properties_path as String, "${targetVersion}.properties")).withInputStream { props.load(it) } + props.each { prop -> gradle.ext.set(prop.key, prop.value) } +} + +loadProperties() + + + +/** ======================================= */ +/** ================ Utils ================ */ +/** ======================================= */ + +/** + * Finds the latest compatible version that is less than or equal to the target version + * while maintaining matching major and minor version components. + * + * @param sortedVersions A pre-sorted ascending list of semantic version strings
(e.g., {@code ["1.18", "1.18.2", "1.19.1"]}) + * @param version + * + * @return The latest compatible version string, or {@code null} if no matching version is found + */ +static String findLatestCompatibleVersion(List sortedVersions, String version) { + def targetVersionList = version.tokenize(".")*.toInteger() + sortedVersions.findAll { + def versionList = it.tokenize(".")*.toInteger() + compareMinecraftVersion(versionList, targetVersionList) <= 0 && + (targetVersionList[0] == versionList[0] && targetVersionList[1] == versionList[1]) + }.max { a, b -> compareMinecraftVersion(a, b) } +} + +/** + * Compares two Minecraft version + * @return comparison result with equivalent semantics to {@link Integer#compareTo(Integer) compareTo()} + */ +static int compareMinecraftVersion(String versionA, String versionB) { + def versionListA = versionA.tokenize(".")*.toInteger() + def versionListB = versionB.tokenize(".")*.toInteger() + compareMinecraftVersion(versionListA, versionListB) +} +/** + * Compares two Minecraft version + * @return comparison result with equivalent semantics to {@link Integer#compareTo(Integer) compareTo()} + */ +static int compareMinecraftVersion(List versionA, List versionB) { + (versionA[0] <=> versionB[0]) ?: (versionA[1] <=> versionB[1]) ?: + (versionA.size() <=> versionB.size() ?: versionA[2] <=> versionB[2]) +} + +/** + * Validates Minecraft version format ({@code major.minor[.patch]} numeric components).
+ * Throws {@link GradleException} with provided or default message if validation fails. + * + * @param version Version string to validate + * @param errorMessage Optional custom exception message (default: {@code "Invalid Minecraft version: X.Y[.Z]"}) + * @return Original version if valid + * @throws GradleException If version format is invalid + */ +static String validateMinecraftVersionFormat(String version, String errorMessage = null) { + def versionList = version.tokenize(".") + if ((2 <= versionList.size() && versionList.size() <= 3) && versionList.every { it.isInteger() }) { + return version + } + throw new GradleException(errorMessage ?: "Invalid Minecraft version: ${version}") +} diff --git a/src/main/java/cn/revaria/chatplus/ChatPlus.java b/src/main/java/cn/revaria/chatplus/ChatPlus.java index 6d86858..e18f3f2 100644 --- a/src/main/java/cn/revaria/chatplus/ChatPlus.java +++ b/src/main/java/cn/revaria/chatplus/ChatPlus.java @@ -5,10 +5,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ChatPlus implements ModInitializer { + public static final String MOD_ID = "chat-plus"; + // This logger is used to write text to the console and the log file. // It is considered best practice to use your mod id as the logger's name. // That way, it's clear which mod wrote info, warnings, and errors. - public static final Logger LOGGER = LoggerFactory.getLogger("Chat Plus"); + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); @Override public void onInitialize() { @@ -16,6 +18,6 @@ public class ChatPlus implements ModInitializer { // However, some things (like resources) may still be uninitialized. // Proceed with mild caution. - LOGGER.info("§2ChatPlus Loaded!"); + LOGGER.info("Chat Plus loaded"); } -} \ No newline at end of file +} diff --git a/src/main/java/cn/revaria/chatplus/mixin/ChatMixin.java b/src/main/java/cn/revaria/chatplus/mixin/ChatMixin.java new file mode 100644 index 0000000..b557078 --- /dev/null +++ b/src/main/java/cn/revaria/chatplus/mixin/ChatMixin.java @@ -0,0 +1,43 @@ +package cn.revaria.chatplus.mixin; + +#if MC_VER <= MC_1_19 +import net.minecraft.server.filter.FilteredMessage; +import net.minecraft.util.registry.RegistryKey; +#endif + +import cn.revaria.chatplus.plugin.annotation.DisableIfModsLoaded; +import net.minecraft.network.message.MessageType; +import net.minecraft.network.message.SignedMessage; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import static cn.revaria.chatplus.util.TextStyleFormatter.applyStyle; + +@DisableIfModsLoaded("styledchat") +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ChatMixin { + @Redirect(method = "handleDecoratedMessage", at = @At(value = "INVOKE", target = + #if MC_VER <= MC_1_19 + "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/server/filter/FilteredMessage;Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/util/registry/RegistryKey;)V" + #else + "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/network/message/SignedMessage;Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/network/message/MessageType$Parameters;)V" + #endif + )) + #if MC_VER <= MC_1_19 + private void replaceText(PlayerManager instance, FilteredMessage message, ServerPlayerEntity sender, RegistryKey typeKey) { + var newMessage = new FilteredMessage<>( + message.raw().withUnsigned(applyStyle(message.raw().getContent(), sender)), + message.filtered() != null ? message.filtered().withUnsigned(applyStyle(message.filtered().getContent(), sender)) : null + ); + instance.broadcast(newMessage, sender, typeKey); + } + #else + private void replaceText(PlayerManager instance, SignedMessage message, ServerPlayerEntity sender, MessageType.Parameters params) { + instance.broadcast(message.withUnsignedContent(applyStyle(message.getContent(), sender)), sender, params); + } + #endif +} diff --git a/src/main/java/cn/revaria/chatplus/mixin/MixinChat.java b/src/main/java/cn/revaria/chatplus/mixin/MixinChat.java deleted file mode 100644 index 4ff02e4..0000000 --- a/src/main/java/cn/revaria/chatplus/mixin/MixinChat.java +++ /dev/null @@ -1,176 +0,0 @@ -package cn.revaria.chatplus.mixin; - -import net.minecraft.MinecraftVersion; -import net.minecraft.item.ItemStack; -import net.minecraft.network.ClientConnection; -import net.minecraft.network.message.*; -import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.filter.FilteredMessage; -import net.minecraft.server.network.ConnectedClientData; -import net.minecraft.server.network.ServerCommonNetworkHandler; -import net.minecraft.server.network.ServerPlayNetworkHandler; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.text.MutableText; -import net.minecraft.text.Text; -import net.minecraft.util.StringHelper; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Mixin(ServerPlayNetworkHandler.class) -public abstract class MixinChat extends ServerCommonNetworkHandler { - public MixinChat(MinecraftServer server, ClientConnection connection, ConnectedClientData clientData) { - super(server, connection, clientData); - } - - @Final - @Shadow - private MessageChainTaskQueue messageChainTaskQueue; - - @Shadow - public ServerPlayerEntity player; - -// @Shadow protected abstract Optional validateMessage(LastSeenMessageList.Acknowledgment acknowledgment); - - @Shadow protected abstract Optional validateAcknowledgment(LastSeenMessageList.Acknowledgment acknowledgment); - - @Shadow protected abstract SignedMessage getSignedMessage(ChatMessageC2SPacket packet, LastSeenMessageList lastSeenMessages) throws MessageChain.MessageChainException; - - @Shadow protected abstract void handleMessageChainException(MessageChain.MessageChainException exception); - - @Shadow protected abstract void handleDecoratedMessage(SignedMessage message); - - @Shadow protected abstract CompletableFuture filterText(String text); - - @Inject(method = "onChatMessage", at = @At("HEAD"), cancellable = true) - public void onChatMessage(ChatMessageC2SPacket packet, CallbackInfo ci) { - // LOGGER.info("CHAT_MESSAGE: " + packet.chatMessage()); - - if (hasIllegalCharacter(packet.chatMessage())) { - disconnect(Text.translatable("multiplayer.disconnect.illegal_characters")); - } else { - Optional optional = this.validateAcknowledgment(packet.acknowledgment()); - if (optional.isPresent()) { - if (!packet.chatMessage().startsWith("/")){ - - String changedMessage = packet.chatMessage().replace('&', '§'); - String regex = "\\[item(?:=([1-9]))?\\]"; - String[] messages = changedMessage.split(regex, -1); - Deque itemDeque = new ArrayDeque<>(); - - Matcher matcher = Pattern.compile(regex).matcher(changedMessage); - while (matcher.find()){ - String digit = matcher.group(1); - if (digit == null){ - itemDeque.addLast(-1); - }else { - itemDeque.addLast(Integer.parseInt(digit)); - } - } - - MutableText changedText = Text.empty(); - for (String message : messages) { - changedText.append(Text.of(message)); - if (!itemDeque.isEmpty()) { - ItemStack itemStack; - if (itemDeque.getFirst() == -1) { - itemStack = player.getMainHandStack(); - } else { - itemStack = player.getInventory().getStack(itemDeque.getFirst() - 1); - } - changedText.append(itemStack.toHoverableText()); - itemDeque.removeFirst(); - } - } - - try { - SignedMessage signedMessage = getSignedMessage(packet, (LastSeenMessageList) optional.get()); - server.getPlayerManager().broadcast(signedMessage.withUnsignedContent( - changedText - ), player, MessageType.params(MessageType.CHAT, player)); - - // Compatible with mod "Discord Integration" - try { - Class DiscordIntegrationMod = Class.forName("de.erdbeerbaerlp.dcintegration.architectury.DiscordIntegrationMod"); - Method handleChatMessage = DiscordIntegrationMod.getMethod("handleChatMessage", SignedMessage.class, ServerPlayerEntity.class); - handleChatMessage.invoke(null, signedMessage.withUnsignedContent(changedText), player); - } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | - IllegalAccessException ignored) { } - - // Compatible with mod "Dynmap" - String[] minecraftVersion = MinecraftVersion.CURRENT.getName().split("\\."); - int minecraftPatchVersion = minecraftVersion.length == 3 ? Integer.parseInt(minecraftVersion[2]) : 0; - for (int i = minecraftPatchVersion; i >= 0; i--){ - String version = minecraftVersion[0] + "." + minecraftVersion[1] + (i == 0 ? "" : ("." + i)); - try { - /* - Warning: Using reflection can make the code harder to maintain, debug, and understand. - Statement: DynmapMod.plugin.chathandler.handleChat() - */ - Class DynmapMod = Class.forName("org.dynmap.fabric_" + version.replaceAll("\\.", "_") + ".DynmapMod"); - Field pluginField = DynmapMod.getField("plugin"); - Object plugin = pluginField.get(null); - Class pluginClass = plugin.getClass(); - Field chatHandlerField = pluginClass.getDeclaredField("chathandler"); - chatHandlerField.setAccessible(true); - Object chatHandler = chatHandlerField.get(plugin); - Class chatHandlerClass = chatHandler.getClass(); - Method handleChat = chatHandlerClass.getMethod("handleChat", ServerPlayerEntity.class, String.class); - handleChat.invoke(chatHandler, player, signedMessage.withUnsignedContent(changedText).getContent().getString()); - break; - } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException | NullPointerException | - IllegalAccessException | InvocationTargetException ignored) { } - } - - } catch (MessageChain.MessageChainException e) { - handleMessageChainException(e); - } - } else { - this.server.submit(() -> { - SignedMessage signedMessage; - try { - signedMessage = this.getSignedMessage(packet, optional.get()); - } catch (MessageChain.MessageChainException var6) { - handleMessageChainException(var6); - return; - } - - CompletableFuture completableFuture = filterText(signedMessage.getSignedContent()); - Text decoratedMessage = this.server.getMessageDecorator().decorate(this.player, signedMessage.getContent()); - messageChainTaskQueue.append(completableFuture, filteredMessage -> { - SignedMessage message = signedMessage.withUnsignedContent(decoratedMessage).withFilterMask(filteredMessage.mask()); - this.handleDecoratedMessage(message); - }); - }); - } - } - - } - - ci.cancel(); - } - - private static boolean hasIllegalCharacter(String message) { - for(int i = 0; i < message.length(); ++i) { - if (!StringHelper.isValidChar(message.charAt(i))) { - return true; - } - } - - return false; - } -} \ No newline at end of file diff --git a/src/main/java/cn/revaria/chatplus/mixin/compat/DynmapMixin.java b/src/main/java/cn/revaria/chatplus/mixin/compat/DynmapMixin.java new file mode 100644 index 0000000..3f1d734 --- /dev/null +++ b/src/main/java/cn/revaria/chatplus/mixin/compat/DynmapMixin.java @@ -0,0 +1,41 @@ +package cn.revaria.chatplus.mixin.compat; + +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static cn.revaria.chatplus.util.TextStyleFormatter.applyStyle; + +@Pseudo +@Mixin(targets = + #if MC_VER <= MC_1_19 + "org.dynmap.fabric_1_19.DynmapPlugin$ChatHandler" + #elif MC_VER <= MC_1_19_1 + "org.dynmap.fabric_1_19_1.DynmapPlugin$ChatHandler" + #elif MC_VER <= MC_1_19_3 + {"org.dynmap.fabric_1_19_3.DynmapPlugin$ChatHandler", "org.dynmap.fabric_1_19_4.DynmapPlugin$ChatHandler"} + #elif MC_VER <= MC_1_20 + {"org.dynmap.fabric_1_20.DynmapPlugin$ChatHandler", "org.dynmap.fabric_1_20_2.DynmapPlugin$ChatHandler"} + #elif MC_VER <= MC_1_20_3 + {"org.dynmap.fabric_1_20_4.DynmapPlugin$ChatHandler", "org.dynmap.fabric_1_20_6.DynmapPlugin$ChatHandler"} + #elif MC_VER <= MC_1_21 + { + "org.dynmap.fabric_1_21.DynmapPlugin$ChatHandler", + "org.dynmap.fabric_1_21_1.DynmapPlugin$ChatHandler", + "org.dynmap.fabric_1_21_3.DynmapPlugin$ChatHandler", + "org.dynmap.fabric_1_21_5.DynmapPlugin$ChatHandler" + } + #endif +) +public abstract class DynmapMixin { + @Inject(method = "handleChat", at = @At("HEAD"), remap = false) + private void modifyMessage(ServerPlayerEntity player, String message, CallbackInfo ci, @Local(argsOnly = true) LocalRef messageRef) { + messageRef.set(applyStyle(Text.of(message), player).getString()); + } +} diff --git a/src/main/java/cn/revaria/chatplus/mixin/compat/StyledChatMixin.java b/src/main/java/cn/revaria/chatplus/mixin/compat/StyledChatMixin.java new file mode 100644 index 0000000..3ef9c0d --- /dev/null +++ b/src/main/java/cn/revaria/chatplus/mixin/compat/StyledChatMixin.java @@ -0,0 +1,21 @@ +package cn.revaria.chatplus.mixin.compat; + +import eu.pb4.placeholders.api.PlaceholderContext; +import eu.pb4.styledchat.StyledChatUtils; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import static cn.revaria.chatplus.util.TextStyleFormatter.applyStyle; + +@Pseudo +@Mixin(StyledChatUtils.class) +public abstract class StyledChatMixin { + @Inject(method = "formatFor(Leu/pb4/placeholders/api/PlaceholderContext;Ljava/lang/String;)Lnet/minecraft/text/Text;", at = @At("RETURN"), cancellable = true) + private static void modifyText(PlaceholderContext context, String input, CallbackInfoReturnable cir) { + cir.setReturnValue(applyStyle(cir.getReturnValue(), context.player())); + } +} diff --git a/src/main/java/cn/revaria/chatplus/plugin/MixinConfigPlugin.java b/src/main/java/cn/revaria/chatplus/plugin/MixinConfigPlugin.java new file mode 100644 index 0000000..8d14fa2 --- /dev/null +++ b/src/main/java/cn/revaria/chatplus/plugin/MixinConfigPlugin.java @@ -0,0 +1,53 @@ +package cn.revaria.chatplus.plugin; + +import cn.revaria.chatplus.plugin.annotation.DisableIfModsLoaded; +import net.fabricmc.loader.api.FabricLoader; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class MixinConfigPlugin implements IMixinConfigPlugin { + @Override + public void onLoad(String mixinPackage) { + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + try { + Class mixinClass = Class.forName(mixinClassName); + DisableIfModsLoaded annotation = mixinClass.getAnnotation(DisableIfModsLoaded.class); + if (annotation != null) { + return Arrays.stream(annotation.value()).noneMatch(modId -> FabricLoader.getInstance().isModLoaded(modId)); + } + return true; + } catch (Exception e) { + return true; + } + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } +} diff --git a/src/main/java/cn/revaria/chatplus/plugin/annotation/DisableIfModsLoaded.java b/src/main/java/cn/revaria/chatplus/plugin/annotation/DisableIfModsLoaded.java new file mode 100644 index 0000000..d8f0999 --- /dev/null +++ b/src/main/java/cn/revaria/chatplus/plugin/annotation/DisableIfModsLoaded.java @@ -0,0 +1,18 @@ +package cn.revaria.chatplus.plugin.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Conditionally disables the annotated mixin when specified mods are loaded. + * + *

When applied, the mixin config plugin should disable the annotated class + * if any mod ID in the value list is present in the runtime environment.

+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DisableIfModsLoaded { + String[] value(); +} diff --git a/src/main/java/cn/revaria/chatplus/util/TextStyleFormatter.java b/src/main/java/cn/revaria/chatplus/util/TextStyleFormatter.java new file mode 100644 index 0000000..f932bda --- /dev/null +++ b/src/main/java/cn/revaria/chatplus/util/TextStyleFormatter.java @@ -0,0 +1,80 @@ +package cn.revaria.chatplus.util; + +#if MC_VER <= MC_1_20 +import net.minecraft.text.LiteralTextContent; +#else +import net.minecraft.text.PlainTextContent.Literal; +#endif + +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TextStyleFormatter { + public static int MAIN_HAND = -1; // Must be smaller than or equal to 0 + + /** + * Processes text styling with two key functions: + *

+ * 1. Replaces {@code &} with {@code §}, using {@code &&} to escape literal {@code &}
+ * 2. Substitutes {@code [item]} with main-hand item hover text and {@code [item=N]} with Nth slot's hover text + *

+ * Recursively processes nested text components. + * + * @param sourceText Original text to process (supports nested text components) + * @param sourcePlayer Player used for item stack references + * @return Processed text with styling and item hover elements + */ + public static MutableText applyStyle(Text sourceText, ServerPlayerEntity sourcePlayer) { + MutableText sourceMutableText = sourceText.copy(); + + MutableText finalText = Text.empty().setStyle(sourceMutableText.getStyle()); + + if (sourceText.getContent() instanceof #if MC_VER <= MC_1_20 LiteralTextContent #else Literal #endif plainTextContent) { + String changedMessage = plainTextContent.string() + .replace('&', '§') + .replace("§§", "&"); + String regex = "\\[item(?:=([1-9]))?\\]"; + String[] messages = changedMessage.split(regex, -1); + Deque itemDeque = new ArrayDeque<>(); + + Matcher matcher = Pattern.compile(regex).matcher(changedMessage); + while (matcher.find()) { + String digit = matcher.group(1); + if (digit == null) { + itemDeque.addLast(MAIN_HAND); + } else { + itemDeque.addLast(Integer.parseInt(digit)); + } + } + + for (String message : messages) { + finalText.append(Text.literal(message)); + if (!itemDeque.isEmpty()) { + ItemStack itemStack; + if (itemDeque.getFirst() == MAIN_HAND) { + itemStack = sourcePlayer.getMainHandStack(); + } else { + itemStack = sourcePlayer.getInventory().getStack(itemDeque.getFirst() - 1); + } + finalText.append(itemStack.toHoverableText()); + itemDeque.removeFirst(); + } + } + } + + List sourceTexts = sourceMutableText.getSiblings(); + for (Text text : sourceTexts) { + finalText.append(applyStyle(text, sourcePlayer)); + } + + return finalText; + } +} diff --git a/src/main/resources/assets/chat-plus/icon.png b/src/main/resources/assets/chat-plus/icon.png index 128ec65..80f3e18 100644 Binary files a/src/main/resources/assets/chat-plus/icon.png and b/src/main/resources/assets/chat-plus/icon.png differ diff --git a/src/main/resources/chat-plus.mixins.json b/src/main/resources/chat-plus.mixins.json index c1878d2..bbb0e0e 100644 --- a/src/main/resources/chat-plus.mixins.json +++ b/src/main/resources/chat-plus.mixins.json @@ -1,11 +1,13 @@ { "required": true, "package": "cn.revaria.chatplus.mixin", - "compatibilityLevel": "JAVA_17", + "plugin": "cn.revaria.chatplus.plugin.MixinConfigPlugin", "mixins": [ - "MixinChat" + "ChatMixin", + "compat.DynmapMixin", + "compat.StyledChatMixin" ], "injectors": { "defaultRequire": 1 } -} \ No newline at end of file +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 7779036..73c5d4e 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "chat-plus", "version": "${version}", "name": "Chat Plus", - "description": "Add bukkit style with an & and [item] for Fabric Server", + "description": "Add Bukkit-style color codes (using \"&\") and \"[item]\" display in chat.", "authors": [ "Rev_Aria" ], @@ -12,7 +12,7 @@ "sources": "https://github.com/CPTProgrammer/ChatPlus/", "issues": "https://github.com/CPTProgrammer/ChatPlus/issues" }, - "license": "CC0-1.0", + "license": "GPL-3.0-or-later", "icon": "assets/chat-plus/icon.png", "environment": "*", "entrypoints": { @@ -21,19 +21,12 @@ ] }, "mixins": [ - "chat-plus.mixins.json", - { - "config": "chat-plus.client.mixins.json", - "environment": "client" - } + "chat-plus.mixins.json" ], "depends": { - "fabricloader": ">=0.16.9", - "minecraft": "~1.21.4", - "java": ">=17", - "fabric-api": "*" - }, - "suggests": { - "another-mod": "*" + "fabricloader": "${fabric_loader_version_range}", + "minecraft": "~${minecraft_version}", + "java": ">=${java_version}", + "${fabric_api_mod_id}": ">=${fabric_api_version}" } -} \ No newline at end of file +}