Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ repositories {
name = 'DevAuth'
url = 'https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1'
}
maven {
name = 'Xander'
url = 'https://maven.isxander.dev/releases'
}
maven {
name = 'Terraformers'
url = 'https://maven.terraformersmc.com/releases/'
}
}

loom {
Expand All @@ -38,6 +46,14 @@ dependencies {

implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"

// Config UI library, bundled into our jar (jar-in-jar)
implementation "dev.isxander:yet-another-config-lib:${project.yacl_version}"
include "dev.isxander:yet-another-config-lib:${project.yacl_version}"

// Mod Menu integration (optional at runtime; present in dev for testing)
compileOnly "com.terraformersmc:modmenu:${project.modmenu_version}"
runtimeOnly "com.terraformersmc:modmenu:${project.modmenu_version}"

// Dev-environment only: real-account login for runClient (not bundled in the jar)
runtimeOnly "me.djtheredstoner:DevAuth-fabric:1.2.2"

Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ loader_version=0.19.3
loom_version=1.17-SNAPSHOT

# Mod Properties
mod_version=0.1.0
mod_version=0.2.0-alpha
maven_group=dev.laybalt

# Dependencies
fabric_api_version=0.155.2+26.2
yacl_version=3.9.6+26.2-fabric
modmenu_version=20.0.1
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package dev.laybalt.skyblocktranslator;

import com.mojang.blaze3d.platform.InputConstants;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.minecraft.client.KeyMapping;
import net.minecraft.resources.Identifier;
import org.lwjgl.glfw.GLFW;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dev.laybalt.skyblocktranslator.config.ModConfig;
import dev.laybalt.skyblocktranslator.pipeline.TranslationEngine;
import dev.laybalt.skyblocktranslator.ui.ConfigScreens;

public class SkyblockTranslatorClient implements ClientModInitializer {
public static final String MOD_ID = "skyblock-translator";
Expand All @@ -16,7 +23,19 @@ public class SkyblockTranslatorClient implements ClientModInitializer {
public void onInitializeClient() {
ModConfig config = ModConfig.get();
TranslationEngine.init();
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> TranslationEngine.get().cache().save());

KeyMapping openConfig = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.skyblock-translator.config",
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_O,
KeyMapping.Category.register(Identifier.fromNamespaceAndPath(MOD_ID, "main"))));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
while (openConfig.consumeClick()) {
client.setScreenAndShow(ConfigScreens.create(null));
}
});

ClientLifecycleEvents.CLIENT_STOPPING.register(client -> TranslationEngine.get().shutdown());
LOGGER.info("SkyBlock Translator initialized (language={}, enabled={})", config.language, config.enabled);
}
}
18 changes: 16 additions & 2 deletions src/main/java/dev/laybalt/skyblocktranslator/config/ModConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,22 @@ public final class ModConfig {
public boolean onlyOnHypixel = true;
public boolean translateItems = true;
public boolean translateMenus = true;
/** Reserved for Phase 2 (chat / NPC dialogue translation). */
public boolean translateChat = false;
/** NPC dialogues and other server system messages in chat. */
public boolean translateDialogs = true;
/** Messages written by other players; off by default. */
public boolean translatePlayerChat = false;
public boolean translateScoreboard = true;
/** Tab entries never use online MT (player names must not be machine-translated). */
public boolean translateTabList = true;
public boolean translateBossBar = true;
/** Send unknown strings to an online translator and cache the results. */
public boolean translateOnline = true;
/** "google" (free, zero setup) or "libretranslate" (own instance below). */
public String onlineProvider = "google";
public String libreTranslateUrl = "";
public String libreTranslateApiKey = "";
/** Max online translation requests per day. */
public int dailyOnlineBudget = 2000;
/** Append unknown templates to config/skyblock-translator/untranslated-<lang>.txt. */
public boolean dumpUntranslated = true;
/** Reserved for Phase 3 (premium cloud translation). */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.laybalt.skyblocktranslator.mixin;

import net.minecraft.client.gui.components.BossHealthOverlay;
import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;

import dev.laybalt.skyblocktranslator.config.ModConfig;
import dev.laybalt.skyblocktranslator.detect.HypixelDetector;
import dev.laybalt.skyblocktranslator.pipeline.TranslationEngine;

/** Boss bar titles (SkyBlock uses them for event/objective banners). */
@Mixin(BossHealthOverlay.class)
public abstract class BossHealthOverlayMixin {
@ModifyArg(
method = "extractRenderState",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;text(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;III)V"
),
index = 1
)
private Component skyblockTranslator$translateBossBar(Component original) {
if (!ModConfig.get().translateBossBar || !HypixelDetector.isTranslationActive()) {
return original;
}
return TranslationEngine.get().translate(original);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.laybalt.skyblocktranslator.mixin;

import net.minecraft.client.gui.components.ChatComponent;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;

import dev.laybalt.skyblocktranslator.config.ModConfig;
import dev.laybalt.skyblocktranslator.detect.HypixelDetector;
import dev.laybalt.skyblocktranslator.pipeline.TranslationEngine;

/**
* Chat translation. NPC dialogues on SkyBlock arrive as server system messages,
* so {@code addServerSystemMessage} covers dialogues, quest text and
* announcements; player messages are a separate (default-off) toggle.
*
* <p>Messages carrying click/hover events are left untouched for now: our
* translated component is a flat literal and would drop the interaction.
*/
@Mixin(ChatComponent.class)
public abstract class ChatComponentMixin {
@ModifyVariable(method = "addServerSystemMessage", at = @At("HEAD"), argsOnly = true)
private Component skyblockTranslator$translateSystem(Component message) {
if (!ModConfig.get().translateDialogs || !HypixelDetector.isTranslationActive()
|| hasInteraction(message)) {
return message;
}
return TranslationEngine.get().translate(message);
}

@ModifyVariable(method = "addPlayerMessage", at = @At("HEAD"), argsOnly = true)
private Component skyblockTranslator$translatePlayer(Component message) {
if (!ModConfig.get().translatePlayerChat || !HypixelDetector.isTranslationActive()
|| hasInteraction(message)) {
return message;
}
return TranslationEngine.get().translate(message);
}

private static boolean hasInteraction(Component component) {
Style style = component.getStyle();
if (style.getClickEvent() != null || style.getHoverEvent() != null) {
return true;
}
for (Component sibling : component.getSiblings()) {
if (hasInteraction(sibling)) {
return true;
}
}
return false;
}
}
36 changes: 36 additions & 0 deletions src/main/java/dev/laybalt/skyblocktranslator/mixin/HudMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package dev.laybalt.skyblocktranslator.mixin;

import net.minecraft.client.gui.Hud;
import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;

import dev.laybalt.skyblocktranslator.config.ModConfig;
import dev.laybalt.skyblocktranslator.detect.HypixelDetector;
import dev.laybalt.skyblocktranslator.pipeline.TranslationEngine;

/**
* Scoreboard sidebar translation. Every text draw inside
* {@code displayScoreboardSidebar} (title, line names, score values) goes
* through {@code GuiGraphicsExtractor.text(Font, Component, IIIZ)}; swapping the
* component translates exactly what is on screen. Score values are numbers and
* pass through the engine untouched.
*/
@Mixin(Hud.class)
public abstract class HudMixin {
@ModifyArg(
method = "displayScoreboardSidebar",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;text(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;IIIZ)V"
),
index = 1
)
private Component skyblockTranslator$translateSidebar(Component original) {
if (!ModConfig.get().translateScoreboard || !HypixelDetector.isTranslationActive()) {
return original;
}
return TranslationEngine.get().translate(original);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dev.laybalt.skyblocktranslator.mixin;

import net.minecraft.client.gui.components.PlayerTabOverlay;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.network.chat.Component;
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.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

import dev.laybalt.skyblocktranslator.config.ModConfig;
import dev.laybalt.skyblocktranslator.detect.HypixelDetector;
import dev.laybalt.skyblocktranslator.pipeline.TranslationEngine;

/**
* Tab list translation. SkyBlock fills the tab with fake entries whose display
* names are info lines, but real player names appear there too — so entries are
* translated with MT disabled (dictionary/cache/overrides only, names never get
* sent to an online translator). Header and footer are pure info text and may
* use the full pipeline.
*/
@Mixin(PlayerTabOverlay.class)
public abstract class PlayerTabOverlayMixin {
@Inject(method = "getNameForDisplay", at = @At("RETURN"), cancellable = true)
private void skyblockTranslator$translateEntry(PlayerInfo info, CallbackInfoReturnable<Component> cir) {
if (!ModConfig.get().translateTabList || !HypixelDetector.isTranslationActive()) {
return;
}
Component original = cir.getReturnValue();
Component translated = TranslationEngine.get().translate(original, false);
if (translated != original) {
cir.setReturnValue(translated);
}
}

@ModifyVariable(method = "setHeader", at = @At("HEAD"), argsOnly = true)
private Component skyblockTranslator$translateHeader(Component header) {
return translateInfo(header);
}

@ModifyVariable(method = "setFooter", at = @At("HEAD"), argsOnly = true)
private Component skyblockTranslator$translateFooter(Component footer) {
return translateInfo(footer);
}

private static Component translateInfo(Component component) {
if (component == null || !ModConfig.get().translateTabList || !HypixelDetector.isTranslationActive()) {
return component;
}
return TranslationEngine.get().translate(component);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,40 @@ public static String leadingCodes(String legacy) {
}
return legacy.substring(0, i);
}

/**
* A code-delimited run of a legacy string. {@code codes} is the (possibly empty)
* §-run that precedes {@code text}; {@code obfuscated} means §k is in effect,
* i.e. the text is decorative garbage that must never be translated.
*/
public record Segment(String codes, String text, boolean obfuscated) {
}

/** Splits a legacy string into styled segments, tracking §k state across resets. */
public static java.util.List<Segment> segments(String legacy) {
java.util.List<Segment> out = new java.util.ArrayList<>();
boolean obfuscated = false;
int i = 0;
while (i < legacy.length()) {
StringBuilder codes = new StringBuilder();
while (i + 1 < legacy.length() && legacy.charAt(i) == SECTION) {
char code = Character.toLowerCase(legacy.charAt(i + 1));
if (code == 'k') {
obfuscated = true;
} else if (code == 'r' || (code >= '0' && code <= '9') || (code >= 'a' && code <= 'f')) {
obfuscated = false; // colors and reset clear formatting
}
codes.append(legacy, i, i + 2);
i += 2;
}
int start = i;
while (i < legacy.length() && legacy.charAt(i) != SECTION) {
i++;
}
if (codes.length() > 0 || i > start) {
out.add(new Segment(codes.toString(), legacy.substring(start, i), obfuscated));
}
}
return out;
}
}
Loading