diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f7fe06..4387627 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - minecraft_version: [1.20.1, 1.20.4, 1.21.0] + minecraft_version: [1.20.1, 1.20.4, 1.21.0, 1.21.2] env: project_name: fabric-${{ matrix.minecraft_version }} artifact_dir: modules/fabric-${{ matrix.minecraft_version }}/build/libs @@ -57,7 +57,7 @@ jobs: run: ./gradlew :${{ env.project_name }}:build --no-daemon - name: capture build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: artifacts-${{ matrix.minecraft_version }} path: ${{ env.artifact_dir }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58e6289..a5b6bbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - minecraft_version: [1.20.1, 1.20.4, 1.21.0] + minecraft_version: [1.20.1, 1.20.4, 1.21.0, 1.21.2] env: project_name: fabric-${{ matrix.minecraft_version }} artifact_dir: modules/fabric-${{ matrix.minecraft_version }}/build/libs @@ -102,7 +102,7 @@ jobs: MOD_VERSION: ${{ steps.version.outputs.release }} - name: capture build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: artifacts-${{ matrix.minecraft_version }} path: ${{ env.artifact_dir }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 467ff48..95da826 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,39 +28,8 @@ // Disable the java formatter because we are using prettier "java.format.enabled": false, "java.maxConcurrentBuilds": 4, - "java.project.sourcePaths": ["modules/fabric-1.20.4/src", "modules/fabric-1.21.0/src"], + "java.project.sourcePaths": ["modules/fabric-1.20.1/src", "modules/fabric-1.20.4/src", "modules/fabric-1.21.0/src", "modules/fabric-1.21.2/src"], "java.configuration.detectJdksAtStart": true, "java.gradle.buildServer.enabled": "on", "java.references.includeDecompiledSources": true, - "todo-tree.highlights.defaultHighlight": { - "background": "#000000", - "fontWeight": "900", - "opacity": 0 - }, - "todohighlight.defaultStyle": { - "backgroundColor": "editor.background" - }, - // https://code.visualstudio.com/api/references/theme-color - "todohighlight.keywords": [ - { - "backgroundColor": "editor.background", - "color": "yellow", - "text": "TODO:" - }, - { - "backgroundColor": "editor.background", - "color": "orange", - "text": "FIXME:" - }, - { - "backgroundColor": "editor.background", - "color": "red", - "text": "HACK:" - }, - { - "backgroundColor": "editor.background", - "color": "#00FF00", - "text": "HINT:" - } - ] } diff --git a/build.gradle b/build.gradle index 9af3abd..b60554e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { // https://fabricmc.net/wiki/documentation:fabric_loom - id 'fabric-loom' version '1.7-SNAPSHOT' apply false + id 'fabric-loom' version '1.9-SNAPSHOT' apply false id "org.jetbrains.gradle.plugin.idea-ext" version "1.1.7" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..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.8-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/modules/fabric-1.20.1/gradle.properties b/modules/fabric-1.20.1/gradle.properties index bd95208..340495f 100644 --- a/modules/fabric-1.20.1/gradle.properties +++ b/modules/fabric-1.20.1/gradle.properties @@ -20,4 +20,4 @@ auth_me_version = 7.0.2+1.20 expanded_storage_version = 10.3.0-beta.5+fabric # Unit Testing: https://mvnrepository.com/artifact/org.junit/junit-bom -junit_bom_version = 5.11.0-M2 +junit_bom_version = 5.11.4 diff --git a/modules/fabric-1.20.4/gradle.properties b/modules/fabric-1.20.4/gradle.properties index e2ac305..fb726a7 100644 --- a/modules/fabric-1.20.4/gradle.properties +++ b/modules/fabric-1.20.4/gradle.properties @@ -20,4 +20,4 @@ auth_me_version = 8.0.0+1.20.4 expanded_storage_version = 12.1.0-beta.4+fabric # Unit Testing: https://mvnrepository.com/artifact/org.junit/junit-bom -junit_bom_version = 5.11.0-M2 +junit_bom_version = 5.11.4 diff --git a/modules/fabric-1.21.0/gradle.properties b/modules/fabric-1.21.0/gradle.properties index b0057c9..94a11a8 100644 --- a/modules/fabric-1.21.0/gradle.properties +++ b/modules/fabric-1.21.0/gradle.properties @@ -1,23 +1,23 @@ # Fabric Properties: https://fabricmc.net/develop -minecraft_version=1.21 -yarn_mappings=1.21+build.7 -loader_version=0.15.11 +minecraft_version=1.21.1 +yarn_mappings=1.21.1+build.3 +loader_version=0.16.10 -#Fabric api -fabric_version=0.100.4+1.21 +# Fabric API +fabric_version=0.115.0+1.21.1 # Shedaniel API's: https://linkie.shedaniel.me/dependencies -modmenu_version = 11.0.1 -cloth_config_version = 15.0.127 +modmenu_version = 11.0.3 +cloth_config_version = 15.0.140 # Local dev mod versions -rei_version = 16.0.729 +rei_version = 16.0.797 // lazydfu_version = 0.1.3 -lithium_version = mc1.21-0.12.7 -sodium_version = mc1.21-0.5.11 -architectury_api_version = 13.0.3+fabric -auth_me_version = 8.0.0+1.21 -expanded_storage_version = 14.0.0+fabric +lithium_version = mc1.21.1-0.14.7-fabric +sodium_version = mc1.21.1-0.6.7-fabric +architectury_api_version = 13.0.8+fabric +auth_me_version = v9.0.1+1.21.1 +expanded_storage_version = 3a5207ab384845f8aa0ead7776bff68c # Unit Testing: https://mvnrepository.com/artifact/org.junit/junit-bom -junit_bom_version = 5.11.0-M2 +junit_bom_version = 5.11.4 diff --git a/modules/fabric-1.21.2/build.gradle b/modules/fabric-1.21.2/build.gradle new file mode 100644 index 0000000..4a05e95 --- /dev/null +++ b/modules/fabric-1.21.2/build.gradle @@ -0,0 +1,135 @@ +plugins { + id 'fabric-loom' + id 'maven-publish' + id 'idea' +} + +String buildNumber = System.getenv("GITHUB_RUN_NUMBER") +Boolean appendBuildNumber = System.getenv("APPEND_BUILD_NUMBER") == "true" && buildNumber != null +String modVersion = System.getenv("MOD_VERSION") ?: "0.0.1" + +version = modVersion + + (appendBuildNumber ? "-build${buildNumber}" : "") + + "+${project.minecraft_version}" + +base { + archivesName = "${project.mod_slug}-fabric" +} + +loom { + splitEnvironmentSourceSets() + + mods { + "textutilities" { + sourceSet sourceSets.main + sourceSet sourceSets.client + } + } + + // https://github.com/SpongePowered/Mixin/wiki/Mixin-Java-System-Properties + def applyCommonRunConfig = { run -> + // run.runDir = "run/${project.minecraft_version}/${run.name}" + // run.vmArg('-XX:+ShowCodeDetailsInExceptionMessages') + run.property("mixin.debug.export", "true") + run.property("mixin.debug.export.decompile", "true") + run.property("mixin.dumpTargetOnFailure", "true") + run.property("mixin.debug.verify", "true") + run.property("mixin.debug.verbose", "true") + run.property("mixin.env.remapRefMap", "true") + run.property("mixin.checks", "true") + run.property("mixin.hotSwap", "true") + } + + runs { + client { + runDir = "run/${project.minecraft_version}/client" + vmArg "-XX:+ShowCodeDetailsInExceptionMessages" + applyCommonRunConfig(client) + } + server { + runDir = "run/${project.minecraft_version}/server" + vmArg "-XX:+ShowCodeDetailsInExceptionMessages" + applyCommonRunConfig(server) + } + } +} + +sourceSets { + test { + compileClasspath += sourceSets.client.compileClasspath + runtimeClasspath += sourceSets.client.runtimeClasspath + } +} + +dependencies { + 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-api:fabric-api:${project.fabric_version}" + modImplementation("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { + exclude(group: "net.fabricmc.fabric-api") + } + + // ====== Mod Compatibility (optional dependencies) ====== + modCompileOnly "com.terraformersmc:modmenu:${project.modmenu_version}" + + // ======= Unit Tests ====== + testImplementation(platform("org.junit:junit-bom:${project.junit_bom_version}")) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(sourceSets.client.output) + + // ====== Mods that are useful for local development ====== + // modLocalRuntime("maven.modrinth:lazydfu:${project.lazydfu_version}") + modLocalRuntime("maven.modrinth:lithium:${project.lithium_version}") + modLocalRuntime("maven.modrinth:sodium:${project.sodium_version}") + // modLocalRuntime("maven.modrinth:starlight:${project.starlight_version}") + modLocalRuntime("maven.modrinth:architectury-api:${project.architectury_api_version}") + modLocalRuntime("maven.modrinth:auth-me:${project.auth_me_version}") + modLocalRuntime("maven.modrinth:expanded-storage:${project.expanded_storage_version}") +} + +processResources { + Map properties = new HashMap<>() + properties.put("version", project.version) + properties.put("mod_id", project.mod_id) + properties.put("mod_name", project.mod_name) + properties.put("mod_slug", project.mod_slug) + properties.put("mod_author", project.mod_author) + properties.put("loader_version", project.loader_version) + properties.put("fabric_version", project.fabric_version) + properties.put("minecraft_version", project.minecraft_version) + // properties.put("java_version", sourceCompatibility) + + properties.each { k, v -> inputs.property(k, v) } + + filesMatching("fabric.mod.json") { + expand properties + } +} + +tasks.withType(JavaCompile).configureEach { + // Minecraft 1.21 upwards uses Java 21. + it.options.release = 21 +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + from("LICENSE") { + rename { "${it}_${project.base.archivesName.get()}"} + } +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} diff --git a/modules/fabric-1.21.2/gradle.properties b/modules/fabric-1.21.2/gradle.properties new file mode 100644 index 0000000..a0b6637 --- /dev/null +++ b/modules/fabric-1.21.2/gradle.properties @@ -0,0 +1,23 @@ +# Fabric Properties: https://fabricmc.net/develop +minecraft_version=1.21.4 +yarn_mappings=1.21.4+build.8 +loader_version=0.16.10 + +# Fabric API +fabric_version=0.117.0+1.21.4 + +# Shedaniel API's: https://linkie.shedaniel.me/dependencies +modmenu_version = 13.0.2 +cloth_config_version = 17.0.144 + +# Local dev mod versions +rei_version = 18.0.796+fabric +// lazydfu_version = 0.1.3 +lithium_version = mc1.21.4-0.14.7-fabric +sodium_version = mc1.21.4-0.6.7-fabric +architectury_api_version = 15.0.1+fabric +auth_me_version = v9.0.1+1.21.4 +expanded_storage_version = eed6ef0400f1dabb19acbf56edc9aa99 + +# Unit Testing: https://mvnrepository.com/artifact/org.junit/junit-bom +junit_bom_version = 5.11.4 diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/TextUtilitiesClient.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/TextUtilitiesClient.java new file mode 100644 index 0000000..7da61fb --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/TextUtilitiesClient.java @@ -0,0 +1,15 @@ +package io.chaws.textutilities.client; + +import io.chaws.textutilities.client.handlers.FormatButtonsHandler; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +@Environment(EnvType.CLIENT) +public class TextUtilitiesClient implements ClientModInitializer { + + @Override + public void onInitializeClient() { + FormatButtonsHandler.initialize(); + } +} diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/config/ModMenuIntegration.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/config/ModMenuIntegration.java new file mode 100644 index 0000000..9eab9a1 --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/config/ModMenuIntegration.java @@ -0,0 +1,16 @@ +package io.chaws.textutilities.client.config; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import io.chaws.textutilities.config.TextUtilitiesConfig; +import me.shedaniel.autoconfig.AutoConfig; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +@Environment(EnvType.CLIENT) +public class ModMenuIntegration implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return parent -> AutoConfig.getConfigScreen(TextUtilitiesConfig.class, parent).get(); + } +} diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/handlers/FormatButtonsHandler.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/handlers/FormatButtonsHandler.java new file mode 100644 index 0000000..9df11dc --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/handlers/FormatButtonsHandler.java @@ -0,0 +1,190 @@ +package io.chaws.textutilities.client.handlers; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.ImmutableList; +import io.chaws.textutilities.TextUtilities; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.AnvilScreen; +import net.minecraft.client.gui.screen.ingame.BookEditScreen; +import net.minecraft.client.gui.screen.ingame.HangingSignEditScreen; +import net.minecraft.client.gui.screen.ingame.SignEditScreen; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; + +@Environment(EnvType.CLIENT) +public class FormatButtonsHandler { + + private static final ImmutableList colorFormattings = ImmutableList.of( + Formatting.BLACK, + Formatting.DARK_GRAY, + Formatting.DARK_BLUE, + Formatting.BLUE, + Formatting.DARK_GREEN, + Formatting.GREEN, + Formatting.DARK_AQUA, + Formatting.AQUA, + Formatting.DARK_RED, + Formatting.RED, + Formatting.DARK_PURPLE, + Formatting.LIGHT_PURPLE, + Formatting.GOLD, + Formatting.YELLOW, + Formatting.GRAY, + Formatting.WHITE + ); + + private static final ImmutableList modifierFormattings = ImmutableList.of( + Formatting.BOLD, + Formatting.ITALIC, + Formatting.UNDERLINE, + Formatting.STRIKETHROUGH, + Formatting.OBFUSCATED, + Formatting.RESET + ); + + public static void initialize() { + ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> + onScreenOpened(screen) + ); + } + + private static void onScreenOpened(Screen screen) { + var config = TextUtilities.getConfig(); + + // TODO: Make the x and y offset of the screens configurable. + var xOffsetFromCenter = 0; + var yOffset = 0; + + if (screen instanceof SignEditScreen || + screen instanceof HangingSignEditScreen) { + if (!config.signFormattingEnabled) { + return; + } + + xOffsetFromCenter += 50; + yOffset += 70; + } else if (screen instanceof BookEditScreen) { + if (!config.bookFormattingEnabled) { + return; + } + + xOffsetFromCenter += 70; + yOffset += 20; + } else if (screen instanceof AnvilScreen) { + if (!config.anvilFormattingEnabled) { + return; + } + + xOffsetFromCenter += 85; + yOffset += (screen.height / 2) - 80; + } else { + // Not a supported screen. + return; + } + + var colorButtons = getFormatButtons( + screen, + colorFormattings, + (screen.width / 2) - (120 + xOffsetFromCenter), + yOffset, + 4 + ); + + var modifierButtons = getFormatButtons( + screen, + modifierFormattings, + (screen.width / 2) + (xOffsetFromCenter), + yOffset, + 6 + ); + + var screenButtons = Screens.getButtons(screen); + screenButtons.addAll(colorButtons); + screenButtons.addAll(modifierButtons); + } + + private static List getFormatButtons( + Screen screen, + List formats, + int x, + int yOffset, + int rows + ) { + List list = new ArrayList<>(); + var i = 0; + var gap = 0; + var buttonSize = 20; + + for (var formatting : formats) { + var buttonX = x + (i / rows + 1) * (buttonSize + gap); + var buttonY = i % rows * (buttonSize + gap) + yOffset; + + list.add( + getFormatButton( + screen, + buttonX, + buttonY, + buttonSize, + buttonSize, + formatting + ) + ); + + i++; + } + + return list; + } + + private static ButtonWidget getFormatButton( + Screen screen, + int buttonX, + int buttonY, + int buttonWidth, + int buttonHeight, + Formatting formatting + ) { + if (formatting.isModifier() || formatting == Formatting.RESET) { + var label = formatting.toString().concat(formatting.getName()); + return ButtonWidget + .builder( + Text.literal(label), + cod -> { + screen.charTyped(Formatting.FORMATTING_CODE_PREFIX, 0); + screen.charTyped(formatting.getCode(), 0); + } + ) + .position(buttonX, buttonY) + .size(buttonWidth * 4, buttonHeight) + .tooltip(Tooltip.of(Text.literal(label))) + .build(); + } + + return ButtonWidget + .builder( + Text.literal(formatting.toString().concat("⬛")), + cod -> { + screen.charTyped(Formatting.FORMATTING_CODE_PREFIX, 0); + screen.charTyped(formatting.getCode(), 0); + } + ) + .position(buttonX, buttonY) + .size(buttonWidth, buttonHeight) + .tooltip( + Tooltip.of( + Text.literal(formatting.toString().concat(formatting.getName())) + ) + ) + .build(); + } +} diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/AnvilScreenMixin.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/AnvilScreenMixin.java new file mode 100644 index 0000000..7e44cae --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/AnvilScreenMixin.java @@ -0,0 +1,174 @@ +package io.chaws.textutilities.client.mixin; + +import io.chaws.textutilities.TextUtilities; +import io.chaws.textutilities.utils.FormattingUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.ingame.AnvilScreen; +import net.minecraft.client.gui.screen.ingame.ForgingScreen; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.AnvilScreenHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Environment(EnvType.CLIENT) +@Mixin(AnvilScreen.class) +public abstract class AnvilScreenMixin + extends ForgingScreen { + + public AnvilScreenMixin( + AnvilScreenHandler handler, + PlayerInventory playerInventory, + Text title, + Identifier texture + ) { + super(handler, playerInventory, title, texture); + } + + @Shadow + private TextFieldWidget nameField; + + private AnvilScreen getAnvilScreen() { + return ((AnvilScreen) (Object) this); + } + + @Inject(method = "setup", at = @At(value = "TAIL")) + protected void setup(CallbackInfo ci) { + // Defaults to: OrderedText.styledForwardsVisitedString(string, Style.EMPTY); + this.nameField.setRenderTextProvider((abc, def) -> + Text.literal(abc).asOrderedText() + ); + } + + @Inject(method = "keyPressed", at = @At("HEAD")) + private void inject( + int keyCode, + int scanCode, + int modifiers, + CallbackInfoReturnable ci + ) { + if (!TextUtilities.getConfig().anvilFormattingEnabled) { + return; + } + + this.getAnvilScreen().setFocused(this.nameField); + } + + // FIXME: This type has changed, need to fix... + // @Inject(method = "onSlotUpdate", at = @At(value = "TAIL")) + // private void onSlotUpdate( + // ScreenHandler handler, + // int slotId, + // ItemStack stack, + // CallbackInfo ci + // ) { + // if (!TextUtilities.getConfig().anvilFormattingEnabled) { + // return; + // } + + // if (slotId != 0) { + // return; + // } + + // var displayElement = stack.getSubNbt(ItemStack.DISPLAY_KEY); + // if (displayElement == null) { + // return; + // } + + // var nameElement = displayElement.get(ItemStack.NAME_KEY); + // if (nameElement == null) { + // return; + // } + + // var json = nameElement.asString(); + // var text = Text.Serialization.fromJson(json); + // if (text == null) { + // return; + // } + + // var sb = new StringBuilder(); + // text.visit( + // (StringVisitable.StyledVisitor) (style, asString) -> { + // var color = style.getColor(); + // if (color != null) { + // var formatting = Formatting.byName(color.getName()); + // if (formatting != null) { + // sb.append(formatting); + // } + // } + + // if (style.isBold()) { + // sb.append(Formatting.BOLD); + // } + + // if (style.isItalic()) { + // sb.append(Formatting.ITALIC); + // } + + // if (style.isUnderlined()) { + // sb.append(Formatting.UNDERLINE); + // } + + // if (style.isStrikethrough()) { + // sb.append(Formatting.STRIKETHROUGH); + // } + + // if (style.isObfuscated()) { + // sb.append(Formatting.OBFUSCATED); + // } + + // if (style.isEmpty()) { + // sb.append(Formatting.RESET); + // } + + // sb.append(asString); + // return Optional.empty(); + // }, + // Style.EMPTY + // ); + + // var formattedName = sb.toString(); + // this.nameField.setText(formattedName); + // } + + // @ModifyArg(method = "onRenamed", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;sendPacket(Lnet/minecraft/network/Packet;)V")) + // private Packet onRenamed_sendPacket(Packet packet) { + // if (!(packet instanceof RenameItemC2SPacket renameItemC2SPacket)) { + // return packet; + // } + // + // var name = renameItemC2SPacket.getName(); + // name = FormattingUtils.replaceBuiltInPrefixWithConfiguredPrefix(name); + // return new RenameItemC2SPacket(name); + // } + + @ModifyVariable(method = "onRenamed", at = @At("HEAD"), argsOnly = true) + private String onRenamed(String name) { + if (!TextUtilities.getConfig().anvilFormattingEnabled) { + return name; + } + + return FormattingUtils.replaceBuiltInPrefixWithConfiguredPrefix(name); + } + + @ModifyArg( + method = "onSlotUpdate", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/widget/TextFieldWidget;setText(Ljava/lang/String;)V" + ) + ) + private String onSlotUpdate_TextFieldWidget_setText(String name) { + if (!TextUtilities.getConfig().anvilFormattingEnabled) { + return name; + } + + return FormattingUtils.replaceConfiguredPrefixWithBuiltInPrefix(name); + } +} diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/ChatHudMixin.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/ChatHudMixin.java new file mode 100644 index 0000000..e9452e4 --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/ChatHudMixin.java @@ -0,0 +1,24 @@ +package io.chaws.textutilities.client.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.util.math.MathHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Environment(EnvType.CLIENT) +@Mixin(ChatHud.class) +public class ChatHudMixin { + @Inject(method = "getWidth(D)I", at = @At("HEAD"), cancellable = true) + private static void getWidth(double widthOption, CallbackInfoReturnable ci) { + ci.setReturnValue(MathHelper.floor(widthOption * 480.0 + 40.0)); + } + + @Inject(method = "getHeight(D)I", at = @At("HEAD"), cancellable = true) + private static void getHeight(double heightOption, CallbackInfoReturnable ci) { + ci.setReturnValue(MathHelper.floor(heightOption * 360.0 + 20.0)); + } +} diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/SignEditScreenMixin.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/SignEditScreenMixin.java new file mode 100644 index 0000000..26555cd --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/SignEditScreenMixin.java @@ -0,0 +1,38 @@ +package io.chaws.textutilities.client.mixin; + +import io.chaws.textutilities.TextUtilities; +import io.chaws.textutilities.utils.FormattingUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.ingame.AbstractSignEditScreen; +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; + +@Environment(EnvType.CLIENT) +@Mixin(AbstractSignEditScreen.class) +public class SignEditScreenMixin { + @Final @Shadow + private String[] messages; + + @Inject(method = "init", at = @At("TAIL")) + private void init(CallbackInfo ci) { + if (!TextUtilities.getConfig().signFormattingEnabled) { + return; + } + + FormattingUtils.replaceConfiguredPrefixWithBuiltInPrefix(messages); + } + + @Inject(method = "removed", at = @At("HEAD")) + private void removed(CallbackInfo ci) { + if (!TextUtilities.getConfig().signFormattingEnabled) { + return; + } + + FormattingUtils.replaceBuiltInPrefixWithConfiguredPrefix(messages); + } +} diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/TextFieldWidgetMixin.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/TextFieldWidgetMixin.java new file mode 100644 index 0000000..2b734bc --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/mixin/TextFieldWidgetMixin.java @@ -0,0 +1,21 @@ +package io.chaws.textutilities.client.mixin; + +import io.chaws.textutilities.utils.FormattingUtils; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.widget.TextFieldWidget; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Environment(EnvType.CLIENT) +@Mixin(TextFieldWidget.class) +public class TextFieldWidgetMixin { + @Redirect(method = "renderWidget", at = @At(value = "INVOKE", target = "Ljava/lang/String;substring(I)Ljava/lang/String;", ordinal = 1)) + private String appendFormatting(String string, int i) { + var strings = FormattingUtils.splitWithFormatting(string, i); + + return FormattingUtils.getLastFormattingCodes(strings.getLeft(), 2) + .concat(strings.getRight()); + } +} diff --git a/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/utils/ChatUtils.java b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/utils/ChatUtils.java new file mode 100644 index 0000000..5bf00d8 --- /dev/null +++ b/modules/fabric-1.21.2/src/client/java/io/chaws/textutilities/client/utils/ChatUtils.java @@ -0,0 +1,32 @@ +package io.chaws.textutilities.client.utils; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; + +public class ChatUtils { + public static void sendMessage(String message) { + sendMessage(Text.of(message)); + } + + public static void sendMessage(Text message) { + var mc = MinecraftClient.getInstance(); + if (mc == null || mc.player == null || !mc.isInSingleplayer()) { + return; + } + + mc.player.sendMessage(message, false); + } + + public static void sendMessage(String message, boolean overlay) { + sendMessage(Text.of(message), overlay); + } + + public static void sendMessage(Text message, boolean overlay) { + var mc = MinecraftClient.getInstance(); + if (mc == null || mc.player == null || !mc.isInSingleplayer()) { + return; + } + + mc.player.sendMessage(message, overlay); + } +} diff --git a/modules/fabric-1.21.2/src/client/resources/textutilities.client.mixins.json b/modules/fabric-1.21.2/src/client/resources/textutilities.client.mixins.json new file mode 100644 index 0000000..2df7b29 --- /dev/null +++ b/modules/fabric-1.21.2/src/client/resources/textutilities.client.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "io.chaws.textutilities.client.mixin", + "compatibilityLevel": "JAVA_21", + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "AnvilScreenMixin", + "ChatHudMixin", + "SignEditScreenMixin", + "TextFieldWidgetMixin" + ] +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/TextUtilities.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/TextUtilities.java new file mode 100644 index 0000000..d0e4d05 --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/TextUtilities.java @@ -0,0 +1,25 @@ +package io.chaws.textutilities; + +import io.chaws.textutilities.config.TextUtilitiesConfig; +import io.chaws.textutilities.handlers.ClickThroughHandler; +import io.chaws.textutilities.handlers.SignEditHandler; +import me.shedaniel.autoconfig.AutoConfig; +import me.shedaniel.autoconfig.serializer.Toml4jConfigSerializer; +import net.fabricmc.api.ModInitializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TextUtilities implements ModInitializer { + public static final Logger logger = LoggerFactory.getLogger("textutilities"); + + public static TextUtilitiesConfig getConfig() { + return AutoConfig.getConfigHolder(TextUtilitiesConfig.class).getConfig(); + } + + @Override + public void onInitialize() { + AutoConfig.register(TextUtilitiesConfig.class, Toml4jConfigSerializer::new); + SignEditHandler.initialize(); + ClickThroughHandler.initialize(); + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/config/TextUtilitiesConfig.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/config/TextUtilitiesConfig.java new file mode 100644 index 0000000..6a4068c --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/config/TextUtilitiesConfig.java @@ -0,0 +1,62 @@ +package io.chaws.textutilities.config; + +import com.google.common.collect.Lists; +import me.shedaniel.autoconfig.ConfigData; +import me.shedaniel.autoconfig.annotation.Config; +import me.shedaniel.autoconfig.annotation.ConfigEntry; +import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +@Config(name = "textutilities") +public class TextUtilitiesConfig implements ConfigData { + @ConfigEntry.Gui.Tooltip + @Comment("The formatting code type to use. Vanilla = §, Ampersand = &") + public String formattingCodePrefix = String.valueOf(Formatting.FORMATTING_CODE_PREFIX); + + @ConfigEntry.Gui.Tooltip + @Comment("Show or hide the formatting buttons in the sign edit screen") + public boolean signFormattingEnabled = true; + + @ConfigEntry.Gui.Tooltip + @Comment("Show or hide the formatting buttons in the book edit screen") + public boolean bookFormattingEnabled = true; + + @ConfigEntry.Gui.Tooltip + @Comment("Show or hide the formatting buttons in the anvil screen") + public boolean anvilFormattingEnabled = true; + + @ConfigEntry.Gui.Tooltip + @Comment("When enabled, clicking a sign with another sign will open the edit window") + public boolean signEditingEnabled = true; + + @ConfigEntry.Gui.Tooltip + @Comment("When enabled, clicking on a sign will open the container it is attached to") + public boolean signClickThroughEnabled = true; + + @ConfigEntry.Gui.Tooltip + @Comment("When enabled, clicking on an item frame will open the container it is attached to") + public boolean itemFrameClickThroughEnabled = true; + + @Comment("Additional item identifiers of blocks or entities to allow clicking through.") + public List additionalClickThroughIdentifiers = Lists.newArrayList( + "create:placard" + ); + + public boolean formattingDisabled() { + return !this.signFormattingEnabled && !this.bookFormattingEnabled && !this.anvilFormattingEnabled; + } + public char getFormattingCodePrefix() { + return this.formattingCodePrefix.charAt(0); + } + + @Override + public void validatePostLoad() throws ValidationException { + if (this.formattingCodePrefix == null || this.formattingCodePrefix.length() > 1) { + throw new ValidationException("Formatting code should be 1 character long."); + } + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/handlers/ClickThroughHandler.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/handlers/ClickThroughHandler.java new file mode 100644 index 0000000..46e11eb --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/handlers/ClickThroughHandler.java @@ -0,0 +1,203 @@ +package io.chaws.textutilities.handlers; + +import static io.chaws.textutilities.utils.PlayerUtils.*; + +import io.chaws.textutilities.TextUtilities; +import io.chaws.textutilities.config.TextUtilitiesConfig; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; + +import net.minecraft.util.math.Vec3i; + +import org.jetbrains.annotations.Nullable; +import net.minecraft.block.Block; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.decoration.AbstractDecorationEntity; +import net.minecraft.entity.decoration.ItemFrameEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Items; +import net.minecraft.registry.Registries; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; + +public class ClickThroughHandler { + public static void initialize() { + UseBlockCallback.EVENT.register(ClickThroughHandler::handleUseBlock); + UseEntityCallback.EVENT.register(ClickThroughHandler::handleUseEntity); + } + + private static ActionResult handleUseBlock( + final PlayerEntity player, + final World world, + final Hand hand, + final BlockHitResult hitResult + ) { + if (player.isSneaking()) { + return ActionResult.PASS; + } + + var config = TextUtilities.getConfig(); + + var clickedSide = hitResult.getSide(); + var clickedBlockPos = hitResult.getBlockPos(); + + return tryClickThrough(world, player, config, clickedBlockPos, clickedSide) + ? ActionResult.SUCCESS + : ActionResult.PASS; + } + + private static ActionResult handleUseEntity( + final PlayerEntity player, + final World world, + final Hand hand, + final Entity entity, + final @Nullable EntityHitResult entityHitResult + ) { + if (player.isSneaking()) { + return ActionResult.PASS; + } + + if (entityHitResult == null) { + return ActionResult.PASS; + } + + var config = TextUtilities.getConfig(); + + if (!canClickThroughEntity(config, entity)) { + return ActionResult.PASS; + } + + var entityFacing = entity.getHorizontalFacing(); + + if (entity instanceof AbstractDecorationEntity decorationEntity) { + var attachedBlockPos = decorationEntity.getAttachedBlockPos(); + // .add(entityFacing.getOpposite().getVector()); + + useBlock(world, player, attachedBlockPos, entityFacing); + + return ActionResult.SUCCESS; + } + + //HACK: Need to find a better way of detecting the side of the entity that was clicked + var exactHitPosition = entityHitResult.getPos(); + var hitPosition = new Vec3i( + (int)Math.round(exactHitPosition.x), + (int)Math.round(exactHitPosition.y), + (int)Math.round(exactHitPosition.z) + ); + + var clickedSide = player.getHorizontalFacing().getOpposite(); + var clickedEntityPos = new BlockPos(hitPosition); + + return tryClickThrough(world, player, config, clickedEntityPos, clickedSide) + ? ActionResult.SUCCESS + : ActionResult.PASS; + } + + private static boolean tryClickThrough( + final World world, + final PlayerEntity player, + final TextUtilitiesConfig config, + final BlockPos clickedBlockPos, + final Direction clickedBlockSide + ) { + var playerFacing = player.getHorizontalFacing().getVector(); + var attachedBlockPos = clickedBlockPos.add(playerFacing); + var blockState = world.getBlockState(clickedBlockPos); + + var blockEntity = world.getBlockEntity(clickedBlockPos); + if (blockEntity != null && canClickThroughBlockEntity(player, config, blockEntity)) { + useBlock(world, player, attachedBlockPos, clickedBlockSide); + return true; + } + + var block = blockState.getBlock(); + if (canClickThroughBlock(config, block)) { + useBlock(world, player, attachedBlockPos, clickedBlockSide); + return true; + } + + return false; + } + + private static boolean canClickThroughBlock( + final TextUtilitiesConfig config, + final Block block + ) { + var blockId = Registries.BLOCK.getId(block); + return config.additionalClickThroughIdentifiers.contains(blockId.toString()); + } + + private static boolean canClickThroughBlockEntity( + final PlayerEntity player, + final TextUtilitiesConfig config, + final BlockEntity blockEntity + ) { + if (blockEntity instanceof SignBlockEntity) { + // Dyes and Ink Sacs can be applied to signs directly when they are in the main hand + return + config.signClickThroughEnabled && + !isHoldingSignChangingItem(player, Hand.MAIN_HAND) && + // TODO: Remove this once we know sign entity inherits SignChangingItem + !isHoldingSign(player, Hand.MAIN_HAND); + } + + var blockEntityType = blockEntity.getType(); + var blockIdentifier = BlockEntityType.getId(blockEntityType); + if (blockIdentifier == null) { + return false; + } + + return config.additionalClickThroughIdentifiers.contains(blockIdentifier.toString()); + } + + private static boolean canClickThroughEntity( + final TextUtilitiesConfig config, + final Entity entity + ) { + if (entity instanceof ItemFrameEntity itemFrameEntity) { + if (!config.itemFrameClickThroughEnabled) { + return false; + } + + // If the item frame has no item attached to it, + // attach the item and don't click through to the chest. + var attachedItem = itemFrameEntity.getHeldItemStack(); + return !attachedItem.isOf(Items.AIR); + } + + var entityType = entity.getType(); + var entityIdentifier = EntityType.getId(entityType); + if (entityIdentifier == null) { + return false; + } + + return config.additionalClickThroughIdentifiers.contains(entityIdentifier.toString()); + } + + private static void useBlock( + final World world, + final PlayerEntity player, + final BlockPos attachedBlockPos, + final Direction clickedSide + ) { + var attachedBlockState = world.getBlockState(attachedBlockPos); + var attachedBlockHitResult = new BlockHitResult( + attachedBlockPos.toCenterPos(), + clickedSide, + attachedBlockPos, + false + ); + + attachedBlockState.onUse(world, player, attachedBlockHitResult); + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/handlers/SignEditHandler.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/handlers/SignEditHandler.java new file mode 100644 index 0000000..e5a0b8f --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/handlers/SignEditHandler.java @@ -0,0 +1,73 @@ +package io.chaws.textutilities.handlers; + +import io.chaws.textutilities.TextUtilities; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.world.World; + +import static io.chaws.textutilities.utils.PlayerUtils.*; + +public class SignEditHandler { + public static void initialize() { + UseBlockCallback.EVENT.register(SignEditHandler::onUseSignBlock); + } + + private static ActionResult onUseSignBlock( + final PlayerEntity player, + final World world, + final Hand hand, + final BlockHitResult hitResult + ) { + // We only want to listen to the server side event + if (world.isClient) { + return ActionResult.PASS; + } + + if (player.isSneaking()) { + return ActionResult.PASS; + } + + if (!TextUtilities.getConfig().signEditingEnabled) { + return ActionResult.PASS; + } + + var blockPos = hitResult.getBlockPos(); + var blockEntity = world.getBlockEntity(blockPos); + + if (!(blockEntity instanceof SignBlockEntity signBlock)) { + return ActionResult.PASS; + } + + if (signBlock.isWaxed()) { + player.sendMessage(Text.literal("Waxed signs cannot be edited."), true); + return ActionResult.PASS; + } + + if (!isHoldingSign(player)) { + return ActionResult.PASS; + } + + // Dyes, Ink Sacs, etc can be applied to signs directly when they are in the main hand + if (isHoldingSignChangingItem(player, Hand.MAIN_HAND)) { + return ActionResult.PASS; + } + + var editorId = signBlock.getEditor(); + var playerId = player.getUuid(); + + if (editorId != null && editorId != playerId) { + player.sendMessage(Text.literal("Sign is being edited by someone else."), true); + return ActionResult.FAIL; + } + + // Set the editor to the player to allow them to edit the sign + signBlock.setEditor(playerId); + player.openEditSignScreen(signBlock, signBlock.isPlayerFacingFront(player)); + return ActionResult.SUCCESS; + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/mixin/FormattingMixin.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/mixin/FormattingMixin.java new file mode 100644 index 0000000..99313ca --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/mixin/FormattingMixin.java @@ -0,0 +1,22 @@ +package io.chaws.textutilities.mixin; + +import io.chaws.textutilities.TextUtilities; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Formatting.class) +public class FormattingMixin { + @Inject(at = {@At("HEAD")}, method = {"strip"}, cancellable = true) + private static void strip(@Nullable String string, CallbackInfoReturnable<@Nullable String> ci) { + if (TextUtilities.getConfig().formattingDisabled()) { + return; + } + + // Don't strip the formatting codes out of strings + ci.setReturnValue(string); + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/mixin/StringHelperMixin.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/mixin/StringHelperMixin.java new file mode 100644 index 0000000..e16b962 --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/mixin/StringHelperMixin.java @@ -0,0 +1,25 @@ +package io.chaws.textutilities.mixin; + +import io.chaws.textutilities.TextUtilities; +import net.minecraft.util.Formatting; +import net.minecraft.util.StringHelper; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(StringHelper.class) +public class StringHelperMixin { + @Inject(method = "isValidChar", at = @At("HEAD"), cancellable = true) + private static void isValidChar(char p, CallbackInfoReturnable ci) { + if (TextUtilities.getConfig().formattingDisabled()) { + return; + } + + // Allow for items and signs to contain the formatting code prefix + if (p == Formatting.FORMATTING_CODE_PREFIX) { + ci.setReturnValue(true); + } + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/FormattingUtils.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/FormattingUtils.java new file mode 100644 index 0000000..982ad1e --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/FormattingUtils.java @@ -0,0 +1,165 @@ +package io.chaws.textutilities.utils; + +import io.chaws.textutilities.TextUtilities; +import net.minecraft.util.Formatting; +import net.minecraft.util.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.Stack; + +public class FormattingUtils { + public static String replaceConfiguredPrefixWithBuiltInPrefix(String value) { + var config = TextUtilities.getConfig(); + var formattingCodePrefix = config.getFormattingCodePrefix(); + if (formattingCodePrefix == Formatting.FORMATTING_CODE_PREFIX) { + return value; + } + + return value.replace( + formattingCodePrefix, + Formatting.FORMATTING_CODE_PREFIX + ); + } + + public static void replaceConfiguredPrefixWithBuiltInPrefix(String[] values) { + var config = TextUtilities.getConfig(); + var formattingCodePrefix = config.getFormattingCodePrefix(); + if (formattingCodePrefix == Formatting.FORMATTING_CODE_PREFIX) { + return; + } + + for (var i = 0; i < values.length; i++) { + values[i] = values[i].replace( + config.getFormattingCodePrefix(), + Formatting.FORMATTING_CODE_PREFIX + ); + } + } + + public static String replaceBuiltInPrefixWithConfiguredPrefix(String value) { + var config = TextUtilities.getConfig(); + var formattingCodePrefix = config.getFormattingCodePrefix(); + if (formattingCodePrefix == Formatting.FORMATTING_CODE_PREFIX) { + return value; + } + + return value.replace( + Formatting.FORMATTING_CODE_PREFIX, + formattingCodePrefix + ); + } + + public static void replaceBuiltInPrefixWithConfiguredPrefix(String[] values) { + var config = TextUtilities.getConfig(); + var formattingCodePrefix = config.getFormattingCodePrefix(); + if (formattingCodePrefix == Formatting.FORMATTING_CODE_PREFIX) { + return; + } + + for (var i = 0; i < values.length; i++) { + values[i] = values[i].replace( + Formatting.FORMATTING_CODE_PREFIX, + config.getFormattingCodePrefix() + ); + } + } + + public static Optional getFormattingCode(@Nullable String string, int startIndex) { + if (string == null) { + return Optional.empty(); + } + + if (startIndex < 0) { + return Optional.empty(); + } + + var length = string.length(); + var lastIndex = length - 1; + var nextIndex = startIndex + 1; + + if (startIndex > lastIndex || nextIndex > lastIndex) { + return Optional.empty(); + } + + var left = string.charAt(startIndex); + if (left != Formatting.FORMATTING_CODE_PREFIX) { + return Optional.empty(); + } + + var right = string.charAt(nextIndex); + var formatting = Formatting.byCode(right); + if (formatting == null) { + return Optional.empty(); + } + + return Optional.of(formatting); + } + + public static @NotNull Pair splitWithFormatting(String string, int index) { + var previousIndex = index - 1; + var formattingCode = getFormattingCode(string, previousIndex); + if (formattingCode.isPresent()) { + return new Pair<>( + string.substring(0, previousIndex), + string.substring(previousIndex) + ); + } + + return new Pair<>( + string.substring(0, index), + string.substring(index) + ); + } + + public static @NotNull String getLastFormattingCodes(@Nullable String string, int count) { + if (string == null || string.isEmpty()) { + return ""; + } + + if (count <= 0) { + return ""; + } + + var formattingCodes = new Stack(); + + for (var rightIndex = string.length() - 1; rightIndex >= 0; rightIndex--) { + if (formattingCodes.size() == count) { + break; + } + + var leftIndex = rightIndex - 1; + if (leftIndex < 0) { + break; + } + + var leftChar = string.charAt(leftIndex); + if (leftChar != Formatting.FORMATTING_CODE_PREFIX) { + continue; + } + + var rightChar = string.charAt(rightIndex); + var formatting = Formatting.byCode(rightChar); + if (formatting == null) { + continue; + } + + formattingCodes.push(formatting); + } + + var sb = new StringBuilder(); + + while (!formattingCodes.empty()) { + var formattingCode = formattingCodes.pop(); + sb.append(formattingCode.toString()); + } + + var lastChar = string.charAt(string.length() - 1); + if (lastChar == Formatting.FORMATTING_CODE_PREFIX) { + sb.append(lastChar); + } + + return sb.toString(); + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/ItemFrameEntityUtils.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/ItemFrameEntityUtils.java new file mode 100644 index 0000000..38d9399 --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/ItemFrameEntityUtils.java @@ -0,0 +1,14 @@ +package io.chaws.textutilities.utils; + +import net.minecraft.entity.decoration.ItemFrameEntity; + +public class ItemFrameEntityUtils { + public static void rotateItemCounterClockwise(ItemFrameEntity itemFrameEntity) { + var itemRotation = itemFrameEntity.getRotation(); + if (itemRotation == 0 || itemRotation == 8) { + itemFrameEntity.setRotation(7); + } else { + itemFrameEntity.setRotation(itemRotation - 1); + } + } +} diff --git a/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/PlayerUtils.java b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/PlayerUtils.java new file mode 100644 index 0000000..cb94f0a --- /dev/null +++ b/modules/fabric-1.21.2/src/main/java/io/chaws/textutilities/utils/PlayerUtils.java @@ -0,0 +1,34 @@ +package io.chaws.textutilities.utils; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.*; +import net.minecraft.util.Hand; + +import java.util.function.Predicate; + +public class PlayerUtils { + public static boolean isHolding(PlayerEntity player, Hand hand, Item item) { + return isHolding(player, hand, holding -> holding.isOf(item)); + } + + public static boolean isHolding(PlayerEntity player, Hand hand, Predicate predicate) { + var holding = hand == Hand.MAIN_HAND ? player.getMainHandStack() : player.getOffHandStack(); + return predicate.test(holding); + } + + public static boolean isHoldingSign(PlayerEntity player) { + return player.isHolding(x -> x.getItem() instanceof SignItem); + } + + public static boolean isHoldingSign(PlayerEntity player, Hand hand) { + return isHolding(player, hand, x -> x.getItem() instanceof SignItem); + } + + public static boolean isHoldingDye(PlayerEntity player, Hand hand) { + return isHolding(player, hand, x -> x.getItem() instanceof DyeItem); + } + + public static boolean isHoldingSignChangingItem(PlayerEntity player, Hand hand) { + return isHolding(player, hand, x -> x.getItem() instanceof SignChangingItem); + } +} diff --git a/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon-curseforge.png b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon-curseforge.png new file mode 100644 index 0000000..3d554d7 Binary files /dev/null and b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon-curseforge.png differ diff --git a/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon.pdn b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon.pdn new file mode 100644 index 0000000..e4fa515 Binary files /dev/null and b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon.pdn differ diff --git a/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon.png b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon.png new file mode 100644 index 0000000..abcb3c7 Binary files /dev/null and b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/icon.png differ diff --git a/modules/fabric-1.21.2/src/main/resources/assets/textutilities/lang/en_us.json b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/lang/en_us.json new file mode 100644 index 0000000..aea1dc7 --- /dev/null +++ b/modules/fabric-1.21.2/src/main/resources/assets/textutilities/lang/en_us.json @@ -0,0 +1,19 @@ +{ + "text.autoconfig.textutilities.title": "Text Utilities", + "text.autoconfig.textutilities.option.formattingCodePrefix": "Formatting code prefix", + "text.autoconfig.textutilities.option.formattingCodePrefix.@Tooltip": "The formatting code type to use. Vanilla = §, Ampersand = &", + "text.autoconfig.textutilities.option.signFormattingEnabled": "Sign formatting enabled", + "text.autoconfig.textutilities.option.signFormattingEnabled.@Tooltip": "Show or hide the formatting buttons in the sign edit screen", + "text.autoconfig.textutilities.option.bookFormattingEnabled": "Book formatting enabled", + "text.autoconfig.textutilities.option.bookFormattingEnabled.@Tooltip": "Show or hide the formatting buttons in the book edit screen", + "text.autoconfig.textutilities.option.anvilFormattingEnabled": "Anvil formatting enabled", + "text.autoconfig.textutilities.option.anvilFormattingEnabled.@Tooltip": "Show or hide the formatting buttons in the anvil screen", + "text.autoconfig.textutilities.option.signEditingEnabled": "Sign editing enabled", + "text.autoconfig.textutilities.option.signEditingEnabled.@Tooltip": "When enabled, clicking a sign with another sign will open the edit window", + "text.autoconfig.textutilities.option.signClickThroughEnabled": "Sign click-through enabled", + "text.autoconfig.textutilities.option.signClickThroughEnabled.@Tooltip": "When enabled, clicking on a sign will open the container it is attached to", + "text.autoconfig.textutilities.option.itemFrameClickThroughEnabled": "Item Frame click-through enabled", + "text.autoconfig.textutilities.option.itemFrameClickThroughEnabled.@Tooltip": "When enabled, clicking on an item frame will open the container it is attached to", + "text.autoconfig.textutilities.option.additionalClickThroughIdentifiers": "Additional click-through blocks/entities", + "text.autoconfig.textutilities.option.additionalClickThroughIdentifiers.@Tooltip": "Additional item identifiers of blocks or entities to allow clicking through." +} diff --git a/modules/fabric-1.21.2/src/main/resources/fabric.mod.json b/modules/fabric-1.21.2/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..98d73cf --- /dev/null +++ b/modules/fabric-1.21.2/src/main/resources/fabric.mod.json @@ -0,0 +1,61 @@ +{ + "schemaVersion": 1, + "id": "textutilities", + "version": "${version}", + "name": "Text Utilities", + "description": "Adds buttons to format and color the text in signs, books, and anvils.", + "authors": [ + "chaws" + ], + "contact": { + "homepage": "https://modrinth.com/mod/text-utilities", + "sources": "https://github.com/ChristopherHaws/mc-text-utilities", + "issues": "https://github.com/ChristopherHaws/mc-text-utilities/issues" + }, + "license": "LGPL-3", + "icon": "assets/textutilities/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "io.chaws.textutilities.TextUtilities" + ], + "client": [ + "io.chaws.textutilities.client.TextUtilitiesClient" + ], + "modmenu": [ + "io.chaws.textutilities.client.config.ModMenuIntegration" + ] + }, + "mixins": [ + "textutilities.mixins.json", + { + "config": "textutilities.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "java": ">=21", + "minecraft": ">=1.21.2", + "fabric": "*", + "fabricloader": ">=0.15", + "cloth-config": "*" + }, + "suggests": { + "modmenu": ">=11" + }, + "conflicts": { + "clickthrough": "*", + "clickthrough2.0": "*" + }, + "custom": { + "modmanager": { + "modrinth": "1bHGioWI", + "curseforge": 700684 + }, + "modmenu": { + "links": { + "modmenu.discord": "https://discord.gg/kQjty3rfJd" + } + } + } +} diff --git a/modules/fabric-1.21.2/src/main/resources/textutilities.mixins.json b/modules/fabric-1.21.2/src/main/resources/textutilities.mixins.json new file mode 100644 index 0000000..163291c --- /dev/null +++ b/modules/fabric-1.21.2/src/main/resources/textutilities.mixins.json @@ -0,0 +1,10 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "io.chaws.textutilities.mixin", + "compatibilityLevel": "JAVA_21", + "injectors": { + "defaultRequire": 1 + }, + "mixins": ["FormattingMixin", "StringHelperMixin"] +} diff --git a/modules/fabric-1.21.2/src/test/java/io/chaws/textutilities/utils/FormattingUtilsTests.java b/modules/fabric-1.21.2/src/test/java/io/chaws/textutilities/utils/FormattingUtilsTests.java new file mode 100644 index 0000000..31e5ad3 --- /dev/null +++ b/modules/fabric-1.21.2/src/test/java/io/chaws/textutilities/utils/FormattingUtilsTests.java @@ -0,0 +1,126 @@ +package io.chaws.textutilities.utils; + +import org.junit.jupiter.api.Test; + +import static io.chaws.textutilities.utils.FormattingUtils.splitWithFormatting; +import static net.minecraft.util.Formatting.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FormattingUtilsTests { + @Test + public void getLastFormattingCodes_single() { + var formatString = BLUE + ITALIC.toString() + "test"; + var result = FormattingUtils.getLastFormattingCodes(formatString, 1); + + assertEquals(result, ITALIC.toString()); + } + + @Test + public void getLastFormattingCodes_single_endsWithFormattingPrefix() { + var formatString = BLUE + ITALIC.toString() + "test" + FORMATTING_CODE_PREFIX; + var result = FormattingUtils.getLastFormattingCodes(formatString, 1); + + assertEquals(result, ITALIC.toString() + FORMATTING_CODE_PREFIX); + } + + @Test + public void getLastFormattingCodes_multiple() { + var formatString = BLUE + ITALIC.toString() + "test"; + var result = FormattingUtils.getLastFormattingCodes(formatString, 2); + + assertEquals(result, BLUE + ITALIC.toString()); + } + + @Test + public void getLastFormattingCodes_multiple_split() { + var formatString = BLUE + "blue" + ITALIC + "italic"; + var result = FormattingUtils.getLastFormattingCodes(formatString, 2); + + assertEquals(result, BLUE + ITALIC.toString()); + } + + @Test + public void getLastFormattingCodes_zero() { + var formatString = BLUE + ITALIC.toString() + "test"; + var result = FormattingUtils.getLastFormattingCodes(formatString, 0); + + assertEquals(result, ""); + } + + @Test + public void getLastFormattingCodes_nill() { + var result = FormattingUtils.getLastFormattingCodes(null, 1); + + assertEquals(result, ""); + } + + @Test + public void getLastFormattingCodes_noneFound() { + var result = FormattingUtils.getLastFormattingCodes("test", 1); + + assertEquals(result, ""); + } + + @Test + public void splitWithFormatting_splitCharIsStartOfFormatCode() { + var formatting = "red" + WHITE + "blue"; + var pair = splitWithFormatting(formatting, formatting.indexOf(FORMATTING_CODE_PREFIX)); + + assertEquals("red", pair.getLeft()); + assertEquals(WHITE + "blue", pair.getRight()); + } + + @Test + public void splitWithFormatting_splitCharIsEndOfFormatCode() { + var formatting = "red" + WHITE + "blue"; + var pair = splitWithFormatting(formatting, formatting.indexOf(FORMATTING_CODE_PREFIX) + 1); + + assertEquals("red", pair.getLeft()); + assertEquals(WHITE + "blue", pair.getRight()); + } + + @Test + public void splitWithFormatting_onlyFormattingCodePrefix_0() { + var formatting = String.valueOf(FORMATTING_CODE_PREFIX); + var pair = splitWithFormatting(formatting, 0); + + assertEquals("", pair.getLeft()); + assertEquals(formatting, pair.getRight()); + } + + @Test + public void splitWithFormatting_onlyFormattingCodePrefix_1() { + var formatting = String.valueOf(FORMATTING_CODE_PREFIX); + var pair = splitWithFormatting(formatting, 1); + + assertEquals(formatting, pair.getLeft()); + assertEquals("", pair.getRight()); + } + + @Test + public void splitWithFormatting_onlyFormattingCode_0() { + var formatting = BOLD.toString(); + var pair = splitWithFormatting(formatting, 0); + + assertEquals("", pair.getLeft()); + assertEquals(formatting, pair.getRight()); + } + + @Test + public void splitWithFormatting_onlyFormattingCode_1() { + var formatting = BOLD.toString(); + var pair = splitWithFormatting(formatting, 1); + + assertEquals("", pair.getLeft()); + assertEquals(formatting, pair.getRight()); + } + + @Test + public void splitWithFormatting_endsWithFormattingCodePrefix() { + var formatting = "01" + FORMATTING_CODE_PREFIX; + var pair = splitWithFormatting(formatting, 2); + + assertEquals("01", pair.getLeft()); + assertEquals(String.valueOf(FORMATTING_CODE_PREFIX), pair.getRight()); + } +} diff --git a/settings.gradle b/settings.gradle index d429b9a..d8ca9f1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,9 @@ project(':fabric-1.20.4').projectDir = file("modules/fabric-1.20.4") include "fabric-1.21.0" project(':fabric-1.21.0').projectDir = file("modules/fabric-1.21.0") +include "fabric-1.21.2" +project(':fabric-1.21.2').projectDir = file("modules/fabric-1.21.2") + // This should match the folder name of the project, or else IDEA may complain (see https://youtrack.jetbrains.com/issue/IDEA-317606) // https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:naming_recommendations rootProject.name = 'text-utilities'