diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index 731d0425..5b65a1a6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -20,9 +20,8 @@ import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.datadriven.GuidePageResourceSelector; +import com.hfstudio.guidenh.guide.internal.localization.GuideLanguageIndex; import com.hfstudio.guidenh.guide.internal.localization.GuideLocalizedPageSourceResolver; -import com.hfstudio.guidenh.guide.internal.localization.GuidePageLanguageIndex; -import com.hfstudio.guidenh.guide.internal.localization.GuideResourceLanguageIndex; import com.hfstudio.guidenh.guide.internal.recipe.NeiAnimationTicker; import com.hfstudio.guidenh.guide.internal.recipe.RecipeCache; import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; @@ -54,8 +53,7 @@ public static void reloadGuides(IResourceManager resourceManager) { NeiAnimationTicker.clear(); GuidePageTexture.clear(); GuideResourceAccess.clearCache(); - GuidePageLanguageIndex.clear(); - GuideResourceLanguageIndex.clear(); + GuideLanguageIndex.clear(); GuideLatexTextureCache.INSTANCE.clearAll(); GuideSceneStructureCache.global() .clear(); @@ -75,7 +73,7 @@ public static void reloadGuides(IResourceManager resourceManager) { // Build the small runtime localization snapshot while the reload is already in progress. // This prevents the first Ponder/GameScene opened after reload from synchronously scanning // every language file on the client thread. - GuideResourceLanguageIndex.warm(language); + GuideLanguageIndex.indexLanguage(language); for (var guide : GuideRegistry.getAll()) { var pages = loadPages( diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java index bcb88776..b53a0fce 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -20,6 +21,7 @@ import java.util.concurrent.TimeUnit; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StringTranslate; import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; @@ -28,9 +30,8 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.hfstudio.guidenh.guide.GuidePageChange; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; +import com.hfstudio.guidenh.guide.internal.localization.GuideLanguageIndex; import com.hfstudio.guidenh.guide.internal.localization.GuideLocalizedPageSourceResolver; -import com.hfstudio.guidenh.guide.internal.localization.GuidePageLanguageIndex; -import com.hfstudio.guidenh.guide.internal.localization.GuideResourceLanguageIndex; import com.hfstudio.guidenh.guide.internal.util.LangUtil; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -356,8 +357,7 @@ public List takeChanges() { } } if (shouldClearLanguageCache) { - GuidePageLanguageIndex.clear(); - GuideResourceLanguageIndex.clear(); + GuideLanguageIndex.clear(); } for (PageReloadRequest request : requests) { queueReloadedPages(loadAll(request.namespace())); @@ -751,7 +751,7 @@ private PageSource resolveActivePageSource(ResourceLocation pageId, String curre } try (InputStream input = Files.newInputStream(langFilePath)) { - Map entries = GuidePageLanguageIndex.readPageKeys(input); + Map entries = readPageKeys(input); return entries.get(GuideLocalizedPageSourceResolver.buildLangKey(contentRootFolder, pageId)); } catch (IOException e) { GuideDebugLog @@ -821,7 +821,7 @@ private Map loadLocalizedSourceOverridesForNamespace(String sour } try (InputStream input = Files.newInputStream(langFilePath)) { - return GuidePageLanguageIndex.readPageKeys(input); + return readPageKeys(input); } catch (IOException e) { GuideDebugLog .warn("[GuideNH] [GuideSourceWatcher] Failed to read localized page lang file {}", langFilePath, e); @@ -925,4 +925,18 @@ private GuideDevelopmentSourceLayout detectSourceLayout(Path folder) { private static GuideDevelopmentSourceLayout detectSourceLayout(Path folder, String contentRootFolder) { return GuideDevelopmentSourceLayout.detect(folder, contentRootFolder); } + + private static Map readPageKeys(InputStream input) { + Map source = StringTranslate.parseLangFile(input); + Map filtered = new LinkedHashMap<>(); + if (source.isEmpty()) return filtered; + + for (var entry : source.entrySet()) { + if (GuideLanguageIndex.isPageLangKey(entry.getKey())) { + filtered.put(entry.getKey(), entry.getValue()); + } + } + + return filtered; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java index bb3ef752..ef85924e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java @@ -1,11 +1,8 @@ package com.hfstudio.guidenh.guide.internal.datadriven; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import java.lang.reflect.Field; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -18,7 +15,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Pattern; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.AbstractResourcePack; @@ -27,7 +23,6 @@ import net.minecraft.client.resources.IResourcePack; import net.minecraft.client.resources.SimpleReloadableResourceManager; import net.minecraft.util.ResourceLocation; -import net.minecraft.util.StringTranslate; import org.jetbrains.annotations.Nullable; @@ -49,20 +44,10 @@ public class DataDrivenGuideLoader { - // Matches the numeric format placeholders normalized by StringTranslate.parseLangFile. - private static final Pattern NUMERIC_LANG_VARIABLE = Pattern.compile("%(\\d+\\$)?[\\d.]*[df]"); - public static final String AUTO_GUIDE_FOLDER = "guidenh"; - public static final String LANGUAGE_FOLDER_PREFIX = "_"; private static final String DEFAULT_LANGUAGE = "en_us"; - public record PackCandidate(IResourcePack pack, int loadPriority, int order) { - - boolean shouldReplace(PackCandidate previous) { - return loadPriority > previous.loadPriority() - || loadPriority == previous.loadPriority() && order > previous.order(); - } - } + public record PackCandidate(IResourcePack pack, ResourceLocation resourceLocation, int order) {} public record ScanResult(Map guides, Map> pagePaths, Map> discoveredLanguages) {} @@ -79,7 +64,6 @@ boolean matches(File root) { private static final Map, Field> LOOSE_ROOT_FIELDS = new IdentityHashMap<>(); private static volatile List lastActiveResourcePacks = List.of(); private static volatile List lastResourceManagerResourcePacks = List.of(); - private static volatile Map> lastResourceManagerDomainsByPack = Map.of(); private static final Map> pagePackIndex = new ConcurrentHashMap<>(); private static final Map> assetPackIndex = new ConcurrentHashMap<>(); private static volatile boolean indexReady = false; @@ -102,12 +86,7 @@ public static ScanResult scanAndBuildAll(String folder) { public static ScanResult scanAndBuildAll(String folder, Iterable activeResourcePacks) { var resolvedPacks = toList(activeResourcePacks); - pagePackIndex.clear(); - assetPackIndex.clear(); - PACK_LANG_FILE_PATHS.clear(); - GuideResourcePackScanner.clearCaches(); - indexReady = false; - pagePackOrder.set(0); + clearCaches(); var pagePaths = new LinkedHashMap>(); var discoveredLanguages = new LinkedHashMap>(); @@ -219,17 +198,18 @@ private static void applyPackScan(IResourcePack resourcePack, String folder, Pac private static void addPackCandidates(Map> index, IResourcePack resourcePack, String folder, PackEntry entry) { - index - .computeIfAbsent( - new ResourceLocation(entry.namespace(), folder + "/" + entry.language() + "/" + entry.relativePath()), - k -> new ArrayList<>()) - .add(new PackCandidate(resourcePack, entry.loadPriority(), pagePackOrder.getAndIncrement())); + var resourceLocation = new ResourceLocation( + entry.namespace(), + folder + "/" + entry.language() + "/" + entry.relativePath()); + + index.computeIfAbsent(resourceLocation, k -> new ArrayList<>()) + .add(new PackCandidate(resourcePack, resourceLocation, pagePackOrder.getAndIncrement())); index .computeIfAbsent( new ResourceLocation(entry.namespace(), folder + "/" + entry.relativePath()), k -> new ArrayList<>()) - .add(new PackCandidate(resourcePack, entry.loadPriority(), pagePackOrder.getAndIncrement())); + .add(new PackCandidate(resourcePack, resourceLocation, pagePackOrder.getAndIncrement())); } public static List getLangFilePaths(IResourcePack resourcePack) { @@ -254,106 +234,6 @@ private static File normalizePackRoot(File resourcePackFile) { .toFile(); } - public static Map readLangFile(IResourcePack resourcePack, String entryPath) { - ResourceLocation location = getLangResourceLocation(entryPath); - if (location == null || !resourceExists(resourcePack, location)) return Map.of(); - try (var input = resourcePack.getInputStream(location)) { - return StringTranslate.parseLangFile(input); - } catch (IOException | RuntimeException e) { - return Map.of(); - } - } - - /** - * Reads runtime language entries once for the localization index. Large page-body entries are - * intentionally skipped; they are resolved by {@code GuidePageLanguageIndex} on demand. - */ - public static Map readRuntimeLangValues(IResourcePack resourcePack, String entryPath) { - ResourceLocation location = getLangResourceLocation(entryPath); - if (location == null || !resourceExists(resourcePack, location)) return Map.of(); - var values = new LinkedHashMap(); - try (var input = resourcePack.getInputStream(location); - var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - // StringTranslate.parseLangFile accepts the first '=' separator. Keep the same - // format while avoiding a parser/stream allocation for every entry in large files. - if (line.startsWith("\uFEFF")) line = line.substring(1); - if (line.isEmpty() || line.startsWith("#")) continue; - int separator = line.indexOf('='); - if (separator <= 0) continue; - String key = line.substring(0, separator); - if (key.startsWith("guidenh.page.")) continue; - String value = line.substring(separator + 1); - values.put( - key, - value.indexOf('%') >= 0 ? NUMERIC_LANG_VARIABLE.matcher(value) - .replaceAll("%$1s") : value); - } - } catch (IOException | RuntimeException ignored) {} - return values.isEmpty() ? Map.of() : Map.copyOf(values); - } - - public static @Nullable String readLangValue(IResourcePack resourcePack, String entryPath, String key) { - ResourceLocation location = getLangResourceLocation(entryPath); - if (location == null || !resourceExists(resourcePack, location)) return null; - try (var input = resourcePack.getInputStream(location); - var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith("\uFEFF")) { - line = line.substring(1); - } - if (line.startsWith("#")) continue; - int separator = line.indexOf('='); - if (separator > 0 && key.equals(line.substring(0, separator))) { - // The line has already been split using the same first '=' rule as - // StringTranslate.parseLangFile; avoid reparsing a one-line stream. - String value = line.substring(separator + 1); - return value.indexOf('%') >= 0 ? NUMERIC_LANG_VARIABLE.matcher(value) - .replaceAll("%$1s") : value; - } - } - } catch (IOException | RuntimeException ignored) {} - return null; - } - - /** Reads only keys from a language file, avoiding retention of large translated page bodies. */ - public static Set readLangKeys(IResourcePack resourcePack, String entryPath) { - ResourceLocation location = getLangResourceLocation(entryPath); - if (location == null || !resourceExists(resourcePack, location)) return Set.of(); - var keys = new LinkedHashSet(); - try (var input = resourcePack.getInputStream(location); - var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith("\uFEFF")) line = line.substring(1); - if (line.startsWith("#")) continue; - int separator = line.indexOf('='); - if (separator > 0) keys.add(line.substring(0, separator)); - } - } catch (IOException | RuntimeException ignored) {} - return keys.isEmpty() ? Set.of() : Set.copyOf(keys); - } - - private static @Nullable ResourceLocation getLangResourceLocation(@Nullable String entryPath) { - if (entryPath == null || !entryPath.endsWith(".lang")) return null; - var afterAssets = entryPath.startsWith("assets/") ? entryPath.substring("assets/".length()) : entryPath; - var firstSlash = afterAssets.indexOf('/'); - return firstSlash > 0 - ? new ResourceLocation(afterAssets.substring(0, firstSlash), afterAssets.substring(firstSlash + 1)) - : null; - } - - private static boolean resourceExists(IResourcePack resourcePack, ResourceLocation location) { - try { - return resourcePack.resourceExists(location); - } catch (RuntimeException ignored) { - // Third-party resource packs occasionally implement a missing resource as a runtime failure. - return false; - } - } - public static Set discoverPagePaths(ResourceLocation guideId, String folder) { return discoverPagePaths(guideId, folder, getActiveResourcePacks()); } @@ -414,7 +294,6 @@ public static List getActiveResourcePacks(IResourceManager resour var domainsByPack = new IdentityHashMap>(); addResourceManagerResourcePacks(resourceManager, resourceManagerResourcePacks, domainsByPack); lastResourceManagerResourcePacks = List.copyOf(resourceManagerResourcePacks); - lastResourceManagerDomainsByPack = freezeDomainsByPack(domainsByPack); resourcePacks.addAll(resourceManagerResourcePacks); addConfiguredResourcePacks(resourcePacks); var resolved = new ArrayList<>(resourcePacks); @@ -529,11 +408,6 @@ private static void addResourceManagerResourcePacks(IResourceManager resourceMan } } - private static Set getResourceDomains(IResourcePack resourcePack) { - Set cached = lastResourceManagerDomainsByPack.get(resourcePack); - return cached != null ? cached : resourcePack.getResourceDomains(); - } - private static Map> freezeDomainsByPack( IdentityHashMap> domainsByPack) { if (domainsByPack.isEmpty()) return Map.of(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuidePageResourceSelector.java b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuidePageResourceSelector.java index d6ef2330..2b1350a5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuidePageResourceSelector.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuidePageResourceSelector.java @@ -1,5 +1,10 @@ package com.hfstudio.guidenh.guide.internal.datadriven; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.List; @@ -10,7 +15,6 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.compiler.Frontmatter; -import com.hfstudio.guidenh.guide.compiler.PageCompiler; public class GuidePageResourceSelector { @@ -34,15 +38,29 @@ private GuidePageResourceSelector() {} Iterable resourcePacks) { // O(1) index lookup List candidates = DataDrivenGuideLoader.getCandidatesFor(sourceId); + if (candidates != null && !candidates.isEmpty()) { - DataDrivenGuideLoader.PackCandidate best = candidates.get(0); + if (candidates.size() == 1) { + return new SelectedPack( + sourceId, + candidates.getFirst() + .pack()); + } + + DataDrivenGuideLoader.PackCandidate best = candidates.getFirst(); + int bestPriority = readLoadPriority(best.pack(), best.resourceLocation()); + for (int i = 1; i < candidates.size(); i++) { - if (candidates.get(i) - .shouldReplace(best)) { - best = candidates.get(i); + DataDrivenGuideLoader.PackCandidate candidate = candidates.get(i); + int priority = readLoadPriority(candidate.pack(), candidate.resourceLocation()); + + if (priority > bestPriority || priority == bestPriority && candidate.order() > best.order()) { + best = candidate; + bestPriority = priority; } } - return new SelectedPack(sourceId, best.pack(), best.loadPriority()); + + return new SelectedPack(sourceId, best.pack()); } // Index says it doesn't exist — fast null @@ -60,34 +78,30 @@ private GuidePageResourceSelector() {} */ private static @Nullable SelectedPack selectFullScan(ResourceLocation sourceId, Iterable resourcePacks) { + SelectedPack winner = null; - byte[] winnerBytes = null; + int winnerPriority = 0; + int winnerOrder = -1; int order = 0; + for (IResourcePack resourcePack : resourcePacks) { byte[] bytes = DataDrivenGuideLoader.readBytes(resourcePack, sourceId); if (bytes == null) { continue; } + int candidateOrder = order++; int candidatePriority = readLoadPriority(sourceId, bytes); - if (winner == null) { - winner = new SelectedPack(sourceId, resourcePack, candidatePriority); - winnerBytes = bytes; - continue; - } - DataDrivenGuideLoader.PackCandidate candidate = new DataDrivenGuideLoader.PackCandidate( - resourcePack, - candidatePriority, - candidateOrder); - DataDrivenGuideLoader.PackCandidate current = new DataDrivenGuideLoader.PackCandidate( - winner.pack(), - winner.loadPriority(), - order - 2); - if (candidate.shouldReplace(current)) { - winner = new SelectedPack(sourceId, resourcePack, candidatePriority); - winnerBytes = bytes; + + if (winner == null || candidatePriority > winnerPriority + || candidatePriority == winnerPriority && candidateOrder > winnerOrder) { + + winner = new SelectedPack(sourceId, resourcePack); + winnerPriority = candidatePriority; + winnerOrder = candidateOrder; } } + return winner; } @@ -96,9 +110,8 @@ private GuidePageResourceSelector() {} * Used by the editor and runtime navigation where the caller has a * localized → default → raw fallback chain. *

- * This does NOT use the index — it does a targeted scan of only the given - * candidate IDs. For bulk page loading during reload, use {@link #select} - * instead. + * Uses the resource-pack index when available and falls back to a targeted + * scan before the index has been built. */ public static @Nullable SelectedPack selectFirstPresent(Iterable resourcePacks, ResourceLocation... sourceIds) { @@ -114,10 +127,8 @@ private GuidePageResourceSelector() {} if (candidates != null && !candidates.isEmpty()) { return new SelectedPack( sourceId, - candidates.get(0) - .pack(), - candidates.get(0) - .loadPriority()); + candidates.getFirst() + .pack()); } } return null; @@ -133,7 +144,7 @@ private GuidePageResourceSelector() {} for (ResourceLocation sourceId : sourceIds) { if (sourceId == null) continue; if (DataDrivenGuideLoader.readBytes(resourcePack, sourceId) != null) { - return new SelectedPack(sourceId, resourcePack, 0); + return new SelectedPack(sourceId, resourcePack); } } } @@ -150,50 +161,48 @@ private GuidePageResourceSelector() {} * Used during full-scan fallback and by MediaWikiSpecialDataIndexer. */ public static int readLoadPriority(ResourceLocation sourceId, byte[] bytes) { - String source = new String(bytes, StandardCharsets.UTF_8); - String yamlText = PageCompiler.extractFrontmatterText(PageCompiler.normalizeLineEndings(stripBom(source))); - if (yamlText == null) { - return 0; - } + return readLoadPriority(sourceId, new ByteArrayInputStream(bytes)); + } + + private static int readLoadPriority(IResourcePack resourcePack, ResourceLocation sourceId) { try { - var frontmatter = Frontmatter.parse(sourceId, yamlText); - var navigation = frontmatter.navigationEntry(); - return navigation != null ? navigation.loadPriority() : 0; - } catch (Exception ignored) { + return readLoadPriority(sourceId, resourcePack.getInputStream(sourceId)); + } catch (IOException | RuntimeException ignored) { return 0; } } - private static String stripBom(String source) { - return source.startsWith("") ? source.substring(1) : source; + private static int readLoadPriority(ResourceLocation sourceId, InputStream input) { + try (input; var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String firstLine = reader.readLine(); + if (!"---".equals(firstLine) && !"\uFEFF---".equals(firstLine)) { + return 0; + } + + var frontmatter = new StringBuilder(); + + String line; + while ((line = reader.readLine()) != null) { + if ("---".equals(line)) { + var navigation = Frontmatter.parse(sourceId, frontmatter.toString()) + .navigationEntry(); + + return navigation != null ? navigation.loadPriority() : 0; + } + + if (!frontmatter.isEmpty()) { + frontmatter.append('\n'); + } + frontmatter.append(line); + } + } catch (Exception ignored) {} + + return 0; } /** * A resource location found in a specific resource pack. - *

- * Unlike the old {@code SelectedPageResource}, this does NOT carry the page bytes — - * the caller is expected to call {@link DataDrivenGuideLoader#readBytes} separately. */ @Desugar - public record SelectedPack(ResourceLocation sourceId, IResourcePack pack, int loadPriority) {} - - /** - * @deprecated Use {@link SelectedPack} instead. Bytes are no longer included; - * read them separately via {@link DataDrivenGuideLoader#readBytes}. - */ - @Deprecated - @Desugar - public record SelectedPageResource(ResourceLocation sourceId, IResourcePack resourcePack, byte[] bytes, - int loadPriority, int order) { - - public boolean shouldReplace(SelectedPageResource previous) { - return loadPriority > previous.loadPriority() - || loadPriority == previous.loadPriority() && order > previous.order(); - } - - public SelectedPageResource withLoadPriority(int resolvedLoadPriority) { - return loadPriority == resolvedLoadPriority ? this - : new SelectedPageResource(sourceId, resourcePack, bytes, resolvedLoadPriority, order); - } - } + public record SelectedPack(ResourceLocation sourceId, IResourcePack pack) {} } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuideResourcePackScanner.java b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuideResourcePackScanner.java index c1a3ef3e..f4bcc461 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuideResourcePackScanner.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/GuideResourcePackScanner.java @@ -1,11 +1,7 @@ package com.hfstudio.guidenh.guide.internal.datadriven; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -21,7 +17,6 @@ import org.jetbrains.annotations.Nullable; -import com.hfstudio.guidenh.guide.compiler.Frontmatter; import com.hfstudio.guidenh.guide.internal.util.LangUtil; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -31,7 +26,7 @@ final class GuideResourcePackScanner { record GuideLanguage(String namespace, String language) {} - record PackEntry(String namespace, String language, String relativePath, int loadPriority) {} + record PackEntry(String namespace, String language, String relativePath) {} record PackScan(List entries, List langPaths, List languages) {} @@ -49,7 +44,9 @@ static void clearCaches() { var entries = new ArrayList(); var langPaths = new ArrayList(); var languages = new LinkedHashSet(); - var prefix = "assets/"; + + final String assetsPrefix = "assets/"; + final String guidePrefix = folder + "/"; try (var zip = new ZipFile(resourcePackFile)) { var zipEntries = zip.entries(); @@ -57,50 +54,45 @@ static void clearCaches() { var entry = zipEntries.nextElement(); if (entry.isDirectory()) continue; - var path = entry.getName(); - if (!path.startsWith(prefix)) continue; + String path = entry.getName(); + if (!path.startsWith(assetsPrefix)) continue; - var afterAssets = path.substring(prefix.length()); - var firstSlash = afterAssets.indexOf('/'); - if (firstSlash <= 0) continue; + int namespaceStart = assetsPrefix.length(); + int namespaceEnd = path.indexOf('/', namespaceStart); + if (namespaceEnd <= namespaceStart) continue; - var namespace = afterAssets.substring(0, firstSlash); - var afterNamespace = afterAssets.substring(firstSlash + 1); + int resourcePathStart = namespaceEnd + 1; if (path.endsWith(".lang")) { - var resourceLocation = new ResourceLocation(namespace, afterNamespace); - if (resourceExists(resourcePack, resourceLocation)) { + String namespace = path.substring(namespaceStart, namespaceEnd); + String resourcePath = path.substring(resourcePathStart); + + if (resourceExists(resourcePack, new ResourceLocation(namespace, resourcePath))) { langPaths.add(path); } continue; } - if (!afterNamespace.startsWith(folder + "/")) continue; - - var resourceLocation = new ResourceLocation(namespace, afterNamespace); - if (!resourceExists(resourcePack, resourceLocation)) continue; + if (!path.startsWith(guidePrefix, resourcePathStart)) continue; - var afterFolder = afterNamespace.substring(folder.length() + 1); - var slashIndex = afterFolder.indexOf('/'); - if (slashIndex <= 0) continue; + int languageStart = resourcePathStart + guidePrefix.length(); + int languageEnd = path.indexOf('/', languageStart); + if (languageEnd <= languageStart) continue; - var language = afterFolder.substring(0, slashIndex); + String language = path.substring(languageStart, languageEnd); if (!isLanguageFolder(language)) continue; - var relativePath = afterFolder.substring(slashIndex + 1); - if (relativePath.isEmpty()) continue; + String namespace = path.substring(namespaceStart, namespaceEnd); + String resourcePath = path.substring(resourcePathStart); - if (path.endsWith(".md")) { - int loadPriority; - try { - loadPriority = parseLoadPriority(zip.getInputStream(entry), resourceLocation); - } catch (IOException e) { - loadPriority = 0; - } + if (!resourceExists(resourcePack, new ResourceLocation(namespace, resourcePath))) { + continue; + } - entries.add(new PackEntry(namespace, language, relativePath, loadPriority)); + String relativePath = path.substring(languageEnd + 1); + + entries.add(new PackEntry(namespace, language, relativePath)); + if (path.endsWith(".md")) { languages.add(new GuideLanguage(namespace, toLanguageCode(language))); - } else { - entries.add(new PackEntry(namespace, language, relativePath, 0)); } } } catch (IOException e) { @@ -150,16 +142,10 @@ private static void scanGuideDirectory(NamespaceRoot namespaceRoot, String folde if (relativePath.isEmpty() || relativePath.endsWith(".lang")) return; if (File.separatorChar != '/') relativePath = relativePath.replace(File.separatorChar, '/'); - int loadPriority = 0; + entries.add(new PackEntry(namespaceRoot.namespace(), language, relativePath)); if (relativePath.endsWith(".md")) { - var location = new ResourceLocation( - namespaceRoot.namespace(), - folder + "/" + language + "/" + relativePath); - loadPriority = parseLoadPriority(path, location); - languages.add(new GuideLanguage(namespaceRoot.namespace(), toLanguageCode(language))); } - entries.add(new PackEntry(namespaceRoot.namespace(), language, relativePath, loadPriority)); }); } catch (IOException e) { GuideDebugLog @@ -189,46 +175,6 @@ private static void collectNamespaceLangPaths(File resourcePackRoot, File namesp } } - private static int parseLoadPriority(Path path, ResourceLocation location) { - try { - return parseLoadPriority(Files.newInputStream(path), location); - } catch (IOException ignored) { - return 0; - } - } - - /** - * Reads only the leading YAML frontmatter needed to resolve navigation load priority. - */ - private static int parseLoadPriority(InputStream input, ResourceLocation location) { - try (input; var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { - String firstLine = reader.readLine(); - if (firstLine != null && firstLine.startsWith("\uFEFF")) { - firstLine = firstLine.substring(1); - } - if (!"---".equals(firstLine)) { - return 0; - } - - var frontmatter = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - if ("---".equals(line)) { - var navigation = Frontmatter.parse(location, frontmatter.toString()) - .navigationEntry(); - return navigation != null ? navigation.loadPriority() : 0; - } - - if (!frontmatter.isEmpty()) { - frontmatter.append('\n'); - } - frontmatter.append(line); - } - } catch (Exception ignored) {} - - return 0; - } - static List guideRootCandidates(File resourcePackRoot, String namespace, String folder) { var candidates = new LinkedHashMap(3); for (NamespaceRoot namespaceRoot : discoverNamespaceRoots(resourcePackRoot)) { @@ -237,17 +183,17 @@ static List guideRootCandidates(File resourcePackRoot, String namespace, S addGuideRootCandidates(candidates, namespaceRoot, folder); } } - addGuideRootCandidate(candidates, resourcePackRoot, "assets/" + namespace + "/" + folder + "/"); - addGuideRootCandidate(candidates, resourcePackRoot, namespace + "/" + folder + "/"); + addGuideRootCandidate(candidates, resourcePackRoot, "assets/" + namespace + "/" + folder); + addGuideRootCandidate(candidates, resourcePackRoot, namespace + "/" + folder); if (folder.equals(namespace)) { - addGuideRootCandidate(candidates, resourcePackRoot, folder + "/"); + addGuideRootCandidate(candidates, resourcePackRoot, folder); } return List.copyOf(candidates.values()); } private static void addGuideRootCandidates(LinkedHashMap candidates, NamespaceRoot namespaceRoot, String folder) { - addGuideRootCandidate(candidates, namespaceRoot.directory(), folder + "/"); + addGuideRootCandidate(candidates, namespaceRoot.directory(), folder); if (folder.equals(namespaceRoot.namespace()) && namespaceRoot.allowDirectoryAsGuideRoot()) { addGuideRootCandidate(candidates, namespaceRoot.directory(), ""); } @@ -333,14 +279,18 @@ static void scanZipPagePaths(File resourcePackFile, String prefix, Set p while (entries.hasMoreElements()) { var entry = entries.nextElement(); if (entry.isDirectory()) continue; + var path = entry.getName(); if (!path.startsWith(prefix) || !path.endsWith(".md")) continue; - var relative = path.substring(prefix.length()); - var slashIndex = relative.indexOf('/'); - if (slashIndex <= 0) continue; - if (!isLanguageFolder(relative.substring(0, slashIndex))) continue; - var pagePath = relative.substring(slashIndex + 1); - if (!pagePath.isEmpty()) pagePaths.add(pagePath); + + int languageStart = prefix.length(); + int languageEnd = path.indexOf('/', languageStart); + if (languageEnd <= languageStart) continue; + + var language = path.substring(languageStart, languageEnd); + if (!isLanguageFolder(language)) continue; + + pagePaths.add(path.substring(languageEnd + 1)); } } catch (IOException e) { GuideDebugLog.warn( diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java index 619be20e..5b3f3665 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java @@ -30,7 +30,7 @@ import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorElementType; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneNodeModel; -import com.hfstudio.guidenh.guide.internal.localization.GuideResourceLanguageIndex; +import com.hfstudio.guidenh.guide.internal.localization.GuideLanguageIndex; import com.hfstudio.guidenh.guide.internal.structure.GuideTextNbtCodec; import com.hfstudio.guidenh.guide.internal.util.LangUtil; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; @@ -738,7 +738,7 @@ private String resolveAnnotationText(SceneEditorElementModel element) { } String textKey = normalizeAttribute(element.getTextKey()); if (textKey != null) { - String localized = GuideResourceLanguageIndex.getValue(LangUtil.getCurrentLanguage(), textKey); + String localized = GuideLanguageIndex.getValue(LangUtil.getCurrentLanguage(), textKey); if (localized != null && !localized.isEmpty()) { return localized; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLanguageIndex.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLanguageIndex.java new file mode 100644 index 00000000..ef2f0772 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLanguageIndex.java @@ -0,0 +1,218 @@ +package com.hfstudio.guidenh.guide.internal.localization; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.util.ResourceLocation; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; +import com.hfstudio.guidenh.guide.internal.util.LangUtil; + +/** + * Shared language index for normal runtime strings and large localized guide pages. + *

+ * Each matching .lang file is scanned once per language. Normal strings are retained in the + * runtime snapshot, while page translations retain only their source locations and are read on + * demand to keep large page bodies out of the index. + */ +public class GuideLanguageIndex { + + private static final String PAGE_LANG_KEY_PREFIX = "guidenh.page."; + // Matches the numeric format placeholders normalized by StringTranslate.parseLangFile. + private static final Pattern NUMERIC_LANG_VARIABLE = Pattern.compile("%(\\d+\\$)?[\\d.]*[df]"); + private static final Object LOCK = new Object(); + + private static volatile Map indexedLanguages = Map.of(); + + private GuideLanguageIndex() {} + + private record PageSource(IResourcePack pack, String path) {} + + private record IndexedLanguage(Map plainTranslations, Map> pageSources) {} + + public static void clear() { + synchronized (LOCK) { + indexedLanguages = Map.of(); + } + } + + public static void indexLanguage(@Nullable String language) { + if (language == null) return; + getOrBuild(LangUtil.normalizeLanguage(language)); + } + + public static @Nullable String getValue(String language, String key) { + if (key == null || key.isEmpty()) { + return null; + } + + String normalized = LangUtil.normalizeLanguage(language); + return getOrBuild(normalized).plainTranslations() + .get(key); + } + + public static @Nullable String getPageValue(String language, String key) { + if (!isPageLangKey(key)) { + return null; + } + + String normalized = LangUtil.normalizeLanguage(language); + return loadPageValue(getOrBuild(normalized), key); + } + + public static boolean isPageLangKey(@Nullable String key) { + return key != null && key.startsWith(PAGE_LANG_KEY_PREFIX); + } + + private static IndexedLanguage getOrBuild(String language) { + IndexedLanguage data = indexedLanguages.get(language); + if (data != null) return data; + + synchronized (LOCK) { + data = indexedLanguages.get(language); + if (data != null) return data; + + data = buildIndex(language); + Map updated = new HashMap<>(indexedLanguages); + updated.put(language, data); + indexedLanguages = updated; + return data; + } + } + + private static IndexedLanguage buildIndex(String language) { + Map plainTranslations = new LinkedHashMap<>(); + Map> pageSources = new LinkedHashMap<>(); + + for (IResourcePack pack : DataDrivenGuideLoader.getLastActiveResourcePacks()) { + for (String path : DataDrivenGuideLoader.getLangFilePaths(pack)) { + int fileNameStart = path.lastIndexOf('/') + 1; + if (fileNameStart <= 0 || !isMatchingLangFile(path.substring(fileNameStart), language)) { + continue; + } + + indexLangFile(pack, path, plainTranslations, pageSources); + } + } + + return new IndexedLanguage(plainTranslations, pageSources); + } + + private static @Nullable String loadPageValue(IndexedLanguage data, String key) { + List matchingSources = data.pageSources() + .get(key); + if (matchingSources == null) { + return null; + } + + String result = null; + for (PageSource source : matchingSources) { + String value = readLangValue(source.pack(), source.path(), key); + if (value != null) { + // Sources are collected in effective pack order; later values override earlier ones. + result = value; + } + } + return result; + } + + private static void indexLangFile(IResourcePack resourcePack, String entryPath, + Map plainTranslations, Map> pageSources) { + + ResourceLocation location = getLangResourceLocation(entryPath); + if (location == null || !resourceExists(resourcePack, location)) { + return; + } + + try (var input = resourcePack.getInputStream(location); + var reader = new InputStreamReader(input, StandardCharsets.UTF_8)) { + + var entries = new LangEntryReader(reader); + PageSource pageSource = null; + + while (entries.next()) { + String key = entries.key(); + + if (isPageLangKey(key)) { + if (pageSource == null) { + pageSource = new PageSource(resourcePack, entryPath); + } + + List sources = pageSources.computeIfAbsent(key, k -> new ArrayList<>()); + // A key can theoretically occur more than once in one file + if (sources.isEmpty() || sources.getLast() != pageSource) { + sources.add(pageSource); + } + continue; + } + + plainTranslations.put(key, normalizeTranslation(entries.readValue())); + } + } catch (IOException | RuntimeException ignored) {} + } + + private static @Nullable String readLangValue(IResourcePack resourcePack, String entryPath, String key) { + ResourceLocation location = getLangResourceLocation(entryPath); + if (location == null || !resourceExists(resourcePack, location)) { + return null; + } + + try (var input = resourcePack.getInputStream(location); + var reader = new InputStreamReader(input, StandardCharsets.UTF_8)) { + var entries = new LangEntryReader(reader); + + while (entries.next()) { + if (key.equals(entries.key())) { + return normalizeTranslation(entries.readValue()); + } + } + } catch (IOException | RuntimeException ignored) {} + + return null; + } + + private static String normalizeTranslation(String value) { + if (value.indexOf('%') >= 0) { + return NUMERIC_LANG_VARIABLE.matcher(value) + .replaceAll("%$1s"); + } + return value; + } + + private static @Nullable ResourceLocation getLangResourceLocation(@Nullable String entryPath) { + if (entryPath == null || !entryPath.endsWith(".lang")) return null; + if (entryPath.startsWith("assets/")) entryPath = entryPath.substring("assets/".length()); + + int firstSlash = entryPath.indexOf('/'); + if (firstSlash <= 0) return null; + + return new ResourceLocation(entryPath.substring(0, firstSlash), entryPath.substring(firstSlash + 1)); + } + + private static boolean resourceExists(IResourcePack resourcePack, ResourceLocation location) { + try { + return resourcePack.resourceExists(location); + } catch (RuntimeException ignored) { + // Third-party resource packs occasionally implement a missing resource as a runtime failure. + return false; + } + } + + private static boolean isMatchingLangFile(String fileName, String language) { + if (!fileName.endsWith(".lang")) { + return false; + } + return LangUtil.normalizeLanguage(fileName.substring(0, fileName.length() - 5)) + .equals(language); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java index c5803ead..a0fe16ef 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java @@ -147,7 +147,7 @@ public static String buildLangKey(String contentRootFolder, ResourceLocation pag private static @Nullable String findLocalizedPageSource(String langKey, String language) { String normalizedLanguage = LangUtil.normalizeLanguage(language); - String localized = GuidePageLanguageIndex.getValue(normalizedLanguage, langKey); + String localized = GuideLanguageIndex.getPageValue(normalizedLanguage, langKey); if (hasText(localized)) { return decodeNewlines(localized); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java deleted file mode 100644 index 6b520889..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.localization; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import net.minecraft.client.resources.IResourcePack; -import net.minecraft.util.StringTranslate; - -import org.jetbrains.annotations.Nullable; - -import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; -import com.hfstudio.guidenh.guide.internal.util.LangUtil; - -/** - * Bounded, key-addressed page localization cache. Page translations can be very large, so indexing - * their complete text during every resource reload is intentionally avoided. - */ -public class GuidePageLanguageIndex { - - private static final String PAGE_LANG_KEY_PREFIX = "guidenh.page."; - /** - * Language files are indexed by key, but their translated page bodies are not retained here. - * This prevents a cache miss from reopening every .lang file while keeping the resident - * memory bounded to file locations and the existing value LRU. - */ - private static final Map>> KEY_SOURCES = new LinkedHashMap<>(); - private static final Set INDEXED_LANGUAGES = new LinkedHashSet<>(); - - private GuidePageLanguageIndex() {} - - private record LangSource(IResourcePack pack, String path) {} - - public static void clear() { - synchronized (KEY_SOURCES) { - KEY_SOURCES.clear(); - INDEXED_LANGUAGES.clear(); - } - } - - /** - * Retained for development-source updates. Normal resource reloads no longer call this method. - */ - public static void preload(Map> keysByLanguage) { - // Page bodies are intentionally never preloaded: they can be large and are owned by resident pages. - } - - public static @Nullable String getValue(String language, String key) { - if (!isPageLangKey(key)) { - return null; - } - String normalizedLanguage = LangUtil.normalizeLanguage(language); - ensureIndexed(normalizedLanguage); - return loadValue(normalizedLanguage, key); - } - - public static boolean isPageLangKey(@Nullable String key) { - return key != null && key.startsWith(PAGE_LANG_KEY_PREFIX); - } - - public static Map readPageKeys(InputStream input) { - Map source = StringTranslate.parseLangFile(input); - if (source.isEmpty()) { - return Map.of(); - } - Map filtered = new LinkedHashMap<>(); - for (var entry : source.entrySet()) { - if (isPageLangKey(entry.getKey())) { - filtered.put(entry.getKey(), entry.getValue()); - } - } - return filtered.isEmpty() ? Map.of() : filtered; - } - - private static @Nullable String loadValue(String language, String key) { - String result = null; - Map> sources; - synchronized (KEY_SOURCES) { - sources = KEY_SOURCES.get(language); - } - if (sources == null) { - return null; - } - List matchingSources = sources.get(key); - if (matchingSources == null) { - return null; - } - for (LangSource source : matchingSources) { - String value = DataDrivenGuideLoader.readLangValue(source.pack(), source.path(), key); - if (value != null) { - // Sources are collected in effective pack order; later values override earlier ones. - result = value; - } - } - return result; - } - - /** Builds the key-to-file index once per language without retaining translated page bodies. */ - private static void ensureIndexed(String language) { - synchronized (KEY_SOURCES) { - if (INDEXED_LANGUAGES.contains(language)) { - return; - } - Map> sources = new LinkedHashMap<>(); - for (IResourcePack resourcePack : DataDrivenGuideLoader.getLastActiveResourcePacks()) { - for (String path : DataDrivenGuideLoader.getLangFilePaths(resourcePack)) { - int fileNameStart = path.lastIndexOf('/') + 1; - if (fileNameStart <= 0 || !isMatchingLangFile(path.substring(fileNameStart), language)) { - continue; - } - LangSource source = new LangSource(resourcePack, path); - for (String key : DataDrivenGuideLoader.readLangKeys(resourcePack, path)) { - if (isPageLangKey(key)) { - sources.computeIfAbsent(key, ignored -> new ArrayList<>()) - .add(source); - } - } - } - } - KEY_SOURCES.put(language, sources); - INDEXED_LANGUAGES.add(language); - } - } - - private static boolean isMatchingLangFile(String fileName, String language) { - if (!fileName.endsWith(".lang")) { - return false; - } - return LangUtil.normalizeLanguage(fileName.substring(0, fileName.length() - 5)) - .equals(language); - } - -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java deleted file mode 100644 index 9a282f0b..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.localization; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import net.minecraft.client.resources.IResourcePack; - -import org.jetbrains.annotations.Nullable; - -import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; -import com.hfstudio.guidenh.guide.internal.util.LangUtil; - -/** Snapshot index for runtime language values, rebuilt when resources are reloaded. */ -public class GuideResourceLanguageIndex { - - private static final Object LOCK = new Object(); - /** Immutable language snapshots; each .lang file is read at most once per reload. */ - private static volatile Map> VALUES = Map.of(); - - private GuideResourceLanguageIndex() {} - - public static void clear() { - synchronized (LOCK) { - VALUES = Map.of(); - } - } - - /** Builds the runtime key index for a language. Safe to call during reload or on demand. */ - public static void warm(@Nullable String language) { - if (language == null) return; - ensureIndexed(LangUtil.normalizeLanguage(language)); - } - - public static @Nullable String getValue(String language, String key) { - if (key == null || key.isEmpty()) { - return null; - } - String normalized = LangUtil.normalizeLanguage(language); - ensureIndexed(normalized); - return VALUES.getOrDefault(normalized, Map.of()) - .get(key); - } - - private static void ensureIndexed(String language) { - if (VALUES.containsKey(language)) return; - synchronized (LOCK) { - if (VALUES.containsKey(language)) return; - Map values = new LinkedHashMap<>(); - for (IResourcePack pack : DataDrivenGuideLoader.getLastActiveResourcePacks()) { - for (String path : DataDrivenGuideLoader.getLangFilePaths(pack)) { - int fileNameStart = path.lastIndexOf('/') + 1; - if (fileNameStart <= 0) continue; - String fileName = path.substring(fileNameStart); - if (!fileName.endsWith(".lang") - || !LangUtil.normalizeLanguage(fileName.substring(0, fileName.length() - 5)) - .equals(language)) { - continue; - } - // Iterate in effective pack order; later packs override earlier values. - values.putAll(DataDrivenGuideLoader.readRuntimeLangValues(pack, path)); - } - } - Map> updated = new HashMap<>(VALUES); - updated.put(language, values.isEmpty() ? Map.of() : Map.copyOf(values)); - VALUES = Map.copyOf(updated); - } - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/LangEntryReader.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/LangEntryReader.java new file mode 100644 index 00000000..ef6c7ae8 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/LangEntryReader.java @@ -0,0 +1,175 @@ +package com.hfstudio.guidenh.guide.internal.localization; + +import java.io.IOException; +import java.io.Reader; + +class LangEntryReader { + + private final Reader reader; + private final char[] chars = new char[8192]; + private final StringBuilder text = new StringBuilder(128); + + private int position; + private int limit; + + private String key; + private boolean valuePending; + + LangEntryReader(Reader reader) { + this.reader = reader; + } + + public boolean next() throws IOException { + if (valuePending) { + skipValue(); + } + + key = null; + + lines: while (ensureAvailable()) { + text.setLength(0); + + if (chars[position] == '\uFEFF') { + position++; + if (!ensureAvailable()) { + return false; + } + } + + if (chars[position] == '#') { + skipToNextLine(); + continue; + } + + while (true) { + if (!ensureAvailable()) { + return false; + } + + int start = position; + + while (position < limit) { + char c = chars[position]; + + if (c == '=') { + int length = position - start; + + if (text.isEmpty() && length == 0) { + position++; + skipToNextLine(); + continue lines; + } + + if (text.isEmpty()) { + key = new String(chars, start, length); + } else { + text.append(chars, start, length); + key = text.toString(); + } + + position++; + valuePending = true; + return true; + } + + if (c == '\r' || c == '\n') { + position++; + consumeLineEnd(c); + continue lines; + } + + position++; + } + + // Only needed when a key happens to cross a reader-buffer boundary + text.append(chars, start, position - start); + } + } + + return false; + } + + public String key() { + if (key == null) { + throw new IllegalStateException("No current language entry"); + } + return key; + } + + public String readValue() throws IOException { + if (!valuePending) { + throw new IllegalStateException("Current language entry has no unread value"); + } + + text.setLength(0); + + while (ensureAvailable()) { + int start = position; + + while (position < limit) { + char c = chars[position]; + if (c == '\r' || c == '\n') { + break; + } + position++; + } + + if (position > start) { + text.append(chars, start, position - start); + } + + if (position < limit) { + int lineEnd = chars[position++]; + consumeLineEnd(lineEnd); + + valuePending = false; + return text.toString(); + } + } + + valuePending = false; + return text.toString(); + } + + private void skipValue() throws IOException { + skipToNextLine(); + valuePending = false; + } + + private void skipToNextLine() throws IOException { + while (ensureAvailable()) { + while (position < limit) { + char c = chars[position++]; + + if (c == '\n') { + return; + } + + if (c == '\r') { + consumeLineEnd(c); + return; + } + } + } + } + + private void consumeLineEnd(int c) throws IOException { + if (c != '\r') { + return; + } + + if (ensureAvailable() && chars[position] == '\n') { + position++; + } + } + + private boolean ensureAvailable() throws IOException { + if (position < limit) { + return true; + } + + limit = reader.read(chars); + position = 0; + return limit != -1; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/element/TextAnnotationElementCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/element/TextAnnotationElementCompiler.java index 21c68c2b..9eb3af39 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/element/TextAnnotationElementCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/element/TextAnnotationElementCompiler.java @@ -12,7 +12,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; import com.hfstudio.guidenh.guide.document.LytErrorSink; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.internal.localization.GuideResourceLanguageIndex; +import com.hfstudio.guidenh.guide.internal.localization.GuideLanguageIndex; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.scene.CameraSettings; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; @@ -166,7 +166,7 @@ private static String resolveLocalizedText(PageCompiler compiler, LytErrorSink e if (normalizedKey.isEmpty()) { return fallbackText; } - String localized = GuideResourceLanguageIndex.getValue(compiler.getLanguage(), normalizedKey); + String localized = GuideLanguageIndex.getValue(compiler.getLanguage(), normalizedKey); return localized != null && !localized.isEmpty() ? localized : fallbackText; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderLocalizationResolver.java b/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderLocalizationResolver.java index f2a29aa8..1cea510f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderLocalizationResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderLocalizationResolver.java @@ -4,7 +4,7 @@ import org.jetbrains.annotations.Nullable; -import com.hfstudio.guidenh.guide.internal.localization.GuideResourceLanguageIndex; +import com.hfstudio.guidenh.guide.internal.localization.GuideLanguageIndex; import com.hfstudio.guidenh.guide.internal.util.LangUtil; public class PonderLocalizationResolver { @@ -44,7 +44,7 @@ private static void localizeAnnotations(List annotatio if (normalizedKey.isEmpty()) { return null; } - String localized = GuideResourceLanguageIndex.getValue(language, normalizedKey); + String localized = GuideLanguageIndex.getValue(language, normalizedKey); return localized != null && !localized.isEmpty() ? localized : null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTagRenderer.java index 14664c9b..739f0d62 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTagRenderer.java @@ -14,7 +14,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; -import com.hfstudio.guidenh.guide.internal.localization.GuideResourceLanguageIndex; +import com.hfstudio.guidenh.guide.internal.localization.GuideLanguageIndex; import com.hfstudio.guidenh.guide.internal.util.LangUtil; import com.hfstudio.guidenh.guide.scene.StructureLibSceneCondition; import com.hfstudio.guidenh.guide.scene.annotation.InWorldBoxAnnotation; @@ -610,7 +610,7 @@ private String resolveTextAnnotationPlainText(MdxJsxElementFields flowElement) { String textKey = readOptional(flowElement, "textKey"); if (textKey != null && !textKey.trim() .isEmpty()) { - String localized = GuideResourceLanguageIndex.getValue(LangUtil.getCurrentLanguage(), textKey.trim()); + String localized = GuideLanguageIndex.getValue(LangUtil.getCurrentLanguage(), textKey.trim()); if (localized != null && !localized.isEmpty()) { return localized; }