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
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ repositories {
name = 'Terraformers'
url = 'https://maven.terraformersmc.com/releases/'
}
maven {
name = 'NotEnoughUpdates'
url = 'https://maven.notenoughupdates.org/releases/'
}
}

loom {
Expand Down Expand Up @@ -50,6 +54,12 @@ dependencies {
implementation "dev.isxander:yet-another-config-lib:${project.yacl_version}"
include "dev.isxander:yet-another-config-lib:${project.yacl_version}"

// MoulConfig (NEU/SkyHanni-style config UI); written in Kotlin, so FLK comes along
implementation "org.notenoughupdates.moulconfig:modern-26.2:${project.moulconfig_version}"
include "org.notenoughupdates.moulconfig:modern-26.2:${project.moulconfig_version}"
implementation "net.fabricmc:fabric-language-kotlin:${project.flk_version}"
include "net.fabricmc:fabric-language-kotlin:${project.flk_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}"
Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ loader_version=0.19.3
loom_version=1.17-SNAPSHOT

# Mod Properties
mod_version=0.2.0-alpha
mod_version=0.3.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
moulconfig_version=4.7.2
flk_version=1.13.13+kotlin.2.4.10
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dev.laybalt.skyblocktranslator.config.ConfigHolder;
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 @@ -31,11 +31,12 @@ public void onInitializeClient() {
KeyMapping.Category.register(Identifier.fromNamespaceAndPath(MOD_ID, "main"))));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
while (openConfig.consumeClick()) {
client.setScreenAndShow(ConfigScreens.create(null));
ConfigHolder.openGui();
}
});

ClientLifecycleEvents.CLIENT_STOPPING.register(client -> TranslationEngine.get().shutdown());
LOGGER.info("SkyBlock Translator initialized (language={}, enabled={})", config.language, config.enabled);
LOGGER.info("SkyBlock Translator initialized (language={}, enabled={})",
config.languageCode(), config.general.enabled);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dev.laybalt.skyblocktranslator.config;

import java.io.File;

import io.github.notenoughupdates.moulconfig.managed.ManagedConfig;
import io.github.notenoughupdates.moulconfig.managed.ManagedConfigBuilder;

import dev.laybalt.skyblocktranslator.pipeline.TranslationEngine;

/**
* Owns the {@link ManagedConfig} wrapper around {@link ModConfig}: MoulConfig
* handles persistence and the editor screen; after every save the translation
* engine is rebuilt so language/provider changes apply immediately.
*/
public final class ConfigHolder {
private static ManagedConfig<ModConfig> managed;

private ConfigHolder() {
}

public static synchronized ManagedConfig<ModConfig> managed() {
if (managed == null) {
File file = ModConfig.directory().resolve("config.json").toFile();
file.getParentFile().mkdirs();
ManagedConfigBuilder<ModConfig> builder = new ManagedConfigBuilder<>(file, ModConfig.class);
builder.setAfterSave(unused -> TranslationEngine.reload());
managed = new ManagedConfig<>(builder);
}
return managed;
}

public static ModConfig get() {
return managed().getInstance();
}

public static void save() {
managed().saveToFile();
}

/** Opens the MoulConfig settings screen. */
public static void openGui() {
managed().openConfigGui();
}
}
216 changes: 146 additions & 70 deletions src/main/java/dev/laybalt/skyblocktranslator/config/ModConfig.java
Original file line number Diff line number Diff line change
@@ -1,94 +1,170 @@
package dev.laybalt.skyblocktranslator.config;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import io.github.notenoughupdates.moulconfig.Config;
import io.github.notenoughupdates.moulconfig.annotations.Category;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorButton;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorSlider;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorText;
import io.github.notenoughupdates.moulconfig.annotations.ConfigOption;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;

import dev.laybalt.skyblocktranslator.SkyblockTranslatorClient;
import dev.laybalt.skyblocktranslator.ui.ConfigScreens;

/**
* Mod configuration, stored as {@code config/skyblock-translator/config.json}.
* A missing file is created with defaults on first launch.
* Mod configuration, rendered with MoulConfig (the NEU/SkyHanni config UI) and
* persisted by {@link ConfigHolder} as {@code config/skyblock-translator/config.json}.
*
* <p>Dropdowns are index-based; use {@link #languageCode()} and
* {@link #onlineProviderCode()} instead of reading the raw indices.
*/
public final class ModConfig {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static ModConfig instance;

public boolean enabled = true;
/** Dictionary/cache language, matches the dict folder name. */
public String language = "ru_ru";
/** Only translate while connected to Hypixel. Turn off to test in singleplayer. */
public boolean onlyOnHypixel = true;
public boolean translateItems = true;
public boolean translateMenus = true;
/** 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;
public class ModConfig extends Config {
/** Order must match the "Language" dropdown values below. */
private static final String[] LANGUAGE_CODES = {
"ru_ru", "uk_ua", "de_de", "fr_fr", "es_es", "pt_br", "pl_pl", "tr_tr", "zh_cn", "ja_jp", "ko_kr"
};

@Expose
@Category(name = "General", desc = "Master switch, language and where to translate")
public General general = new General();

@Expose
@Category(name = "Surfaces", desc = "What gets translated")
public Surfaces surfaces = new Surfaces();

@Expose
@Category(name = "Online Translation", desc = "Free machine translation for unknown text")
public Online online = new Online();

/** Reserved for Phase 3 (premium cloud translation). */
@Expose
public String premiumKey = "";

public static ModConfig get() {
if (instance == null) {
instance = load();
}
return instance;
public static class General {
@Expose
@ConfigOption(name = "Enable Translation", desc = "Master switch for the whole mod.")
@ConfigEditorBoolean
public boolean enabled = true;

@Expose
@ConfigOption(name = "Language", desc = "Target language. Russian ships with a curated dictionary; other languages rely on online translation.")
@ConfigEditorDropdown(values = {
"Русский", "Українська", "Deutsch", "Français", "Español",
"Português (BR)", "Polski", "Türkçe", "中文", "日本語", "한국어"
})
public int languageIndex = 0;

@Expose
@ConfigOption(name = "Only on Hypixel", desc = "Translate only while connected to Hypixel. Disable to test in singleplayer.")
@ConfigEditorBoolean
public boolean onlyOnHypixel = true;

@ConfigOption(name = "Translation Editor", desc = "Edit the most recent strings by hand. Your edits become overrides and always win over the dictionary and the machine translation.")
@ConfigEditorButton(runnableId = ModConfig.OPEN_EDITOR_RUNNABLE, buttonText = "OPEN")
public int openEditor = 0;
}

public static Path directory() {
return FabricLoader.getInstance().getConfigDir().resolve("skyblock-translator");
public static class Surfaces {
@Expose
@ConfigOption(name = "Item Tooltips", desc = "Item names and lore in container screens.")
@ConfigEditorBoolean
public boolean items = true;

@Expose
@ConfigOption(name = "Menu Titles", desc = "Container titles and labels.")
@ConfigEditorBoolean
public boolean menus = true;

@Expose
@ConfigOption(name = "NPC Dialogues & System Messages", desc = "Server messages in chat: dialogues, quests, announcements.")
@ConfigEditorBoolean
public boolean dialogs = true;

@Expose
@ConfigOption(name = "Player Chat", desc = "Messages written by other players.")
@ConfigEditorBoolean
public boolean playerChat = false;

@Expose
@ConfigOption(name = "Scoreboard Sidebar", desc = "The SKYBLOCK sidebar on the right.")
@ConfigEditorBoolean
public boolean scoreboard = true;

@Expose
@ConfigOption(name = "Tab List", desc = "Tab entries, header and footer. Player names are never sent to an online translator.")
@ConfigEditorBoolean
public boolean tabList = true;

@Expose
@ConfigOption(name = "Boss Bar", desc = "Event and objective banners at the top of the screen.")
@ConfigEditorBoolean
public boolean bossBar = true;
}

private static Path file() {
return directory().resolve("config.json");
public static class Online {
@Expose
@ConfigOption(name = "Online Translation", desc = "Translate unknown strings through an online service and cache the result locally. With this off, only dictionaries and the cache are used.")
@ConfigEditorBoolean
public boolean translateOnline = true;

@Expose
@ConfigOption(name = "Provider", desc = "Google — free, works out of the box. LibreTranslate — your own instance (URL below).")
@ConfigEditorDropdown(values = {"Google (free)", "LibreTranslate"})
public int providerIndex = 0;

@Expose
@ConfigOption(name = "Daily Request Limit", desc = "Maximum online translation requests per day. Cached translations don't count.")
@ConfigEditorSlider(minValue = 0, maxValue = 100000, minStep = 500)
public int dailyBudget = 2000;

@Expose
@ConfigOption(name = "LibreTranslate URL", desc = "Instance address, e.g. https://libretranslate.example.com")
@ConfigEditorText
public String libreUrl = "";

@Expose
@ConfigOption(name = "LibreTranslate API Key", desc = "Leave empty if the instance doesn't require one.")
@ConfigEditorText
public String libreApiKey = "";

@Expose
@ConfigOption(name = "Collect Untranslated Strings", desc = "Append unknown templates to untranslated-<lang>.txt — used to grow the bundled dictionaries.")
@ConfigEditorBoolean
public boolean dumpUntranslated = true;
}

private static ModConfig load() {
Path file = file();
if (Files.exists(file)) {
try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
ModConfig loaded = GSON.fromJson(reader, ModConfig.class);
if (loaded != null) {
return loaded;
}
} catch (IOException | RuntimeException e) {
SkyblockTranslatorClient.LOGGER.error("Failed to load config, using defaults", e);
}
static final int OPEN_EDITOR_RUNNABLE = 0;

@Override
public void executeRunnable(int runnableId) {
if (runnableId == OPEN_EDITOR_RUNNABLE) {
Minecraft.getInstance().setScreenAndShow(ConfigScreens.createEditor(null));
}
ModConfig fresh = new ModConfig();
fresh.save();
return fresh;
}

public String languageCode() {
int idx = general.languageIndex;
return LANGUAGE_CODES[idx >= 0 && idx < LANGUAGE_CODES.length ? idx : 0];
}

public String onlineProviderCode() {
return online.providerIndex == 1 ? "libretranslate" : "google";
}

public static ModConfig get() {
return ConfigHolder.get();
}

public static Path directory() {
return FabricLoader.getInstance().getConfigDir().resolve("skyblock-translator");
}

public void save() {
try {
Files.createDirectories(directory());
try (Writer writer = Files.newBufferedWriter(file(), StandardCharsets.UTF_8)) {
GSON.toJson(this, writer);
}
} catch (IOException e) {
SkyblockTranslatorClient.LOGGER.error("Failed to save config", e);
}
ConfigHolder.save();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ private HypixelDetector() {

public static boolean isTranslationActive() {
ModConfig config = ModConfig.get();
if (!config.enabled) {
if (!config.general.enabled) {
return false;
}
if (!config.onlyOnHypixel) {
if (!config.general.onlyOnHypixel) {
return true;
}
ServerData server = Minecraft.getInstance().getCurrentServer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
public abstract class AbstractContainerScreenMixin {
@Inject(method = "getTooltipFromContainerItem", at = @At("RETURN"), cancellable = true)
private void skyblockTranslator$translateTooltip(ItemStack stack, CallbackInfoReturnable<List<Component>> cir) {
if (!ModConfig.get().translateItems || !HypixelDetector.isTranslationActive()) {
if (!ModConfig.get().surfaces.items || !HypixelDetector.isTranslationActive()) {
return;
}
List<Component> lines = cir.getReturnValue();
Expand All @@ -51,7 +51,7 @@ public abstract class AbstractContainerScreenMixin {
index = 1
)
private Component skyblockTranslator$translateLabel(Component original) {
if (!ModConfig.get().translateMenus || !HypixelDetector.isTranslationActive()) {
if (!ModConfig.get().surfaces.menus || !HypixelDetector.isTranslationActive()) {
return original;
}
return TranslationEngine.get().translate(original);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public abstract class BossHealthOverlayMixin {
index = 1
)
private Component skyblockTranslator$translateBossBar(Component original) {
if (!ModConfig.get().translateBossBar || !HypixelDetector.isTranslationActive()) {
if (!ModConfig.get().surfaces.bossBar || !HypixelDetector.isTranslationActive()) {
return original;
}
return TranslationEngine.get().translate(original);
Expand Down
Loading