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
-
+# Chat Plus
+
+
- 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
+

- **^^^ Display the item in main hand**
+**^^^ Display the item in the main hand**

- **^^^ Colorful Text**
+**^^^ Colorful Text**

- **^^^ 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
+}