diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java index 546d36d7f..d08128e94 100644 --- a/src/main/java/emu/grasscutter/tools/Tools.java +++ b/src/main/java/emu/grasscutter/tools/Tools.java @@ -11,7 +11,11 @@ import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; @@ -21,37 +25,53 @@ import emu.grasscutter.data.GameData; import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.data.excels.AvatarData; import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.data.excels.QuestData; import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Language.TextStrings; -import it.unimi.dsi.fastutil.ints.Int2IntSortedMap; import it.unimi.dsi.fastutil.ints.Int2IntRBTreeMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap; +import it.unimi.dsi.fastutil.ints.IntList; +import lombok.val; import static emu.grasscutter.config.Configuration.*; import static emu.grasscutter.utils.FileUtils.getResourcePath; public final class Tools { public static void createGmHandbooks() throws Exception { - final List languages = Language.TextStrings.getLanguages(); - final Int2ObjectMap textMaps = Language.getTextMapStrings(); + val languages = Language.TextStrings.getLanguages(); + val textMaps = Language.getTextMapStrings(); ResourceLoader.loadAll(); - final Int2IntSortedMap avatarNames = new Int2IntRBTreeMap(GameData.getAvatarDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getNameTextMapHash()))); - final Int2IntSortedMap itemNames = new Int2IntRBTreeMap(GameData.getItemDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getNameTextMapHash()))); - final Int2IntSortedMap monsterNames = new Int2IntRBTreeMap(GameData.getMonsterDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getNameTextMapHash()))); - final Int2IntSortedMap mainQuestTitles = new Int2IntRBTreeMap(GameData.getMainQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getTitleTextMapHash()))); - // Int2IntSortedMap questDescs = new Int2IntRBTreeMap(GameData.getQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getDescTextMapHash()))); + val mainQuestTitles = new Int2IntRBTreeMap(GameData.getMainQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getTitleTextMapHash()))); + // val questDescs = new Int2IntRBTreeMap(GameData.getQuestDataMap().int2ObjectEntrySet().stream().collect(Collectors.toMap(e -> (int) e.getIntKey(), e -> (int) e.getValue().getDescTextMapHash()))); + + val avatarDataMap = new Int2ObjectRBTreeMap<>(GameData.getAvatarDataMap()); + val itemDataMap = new Int2ObjectRBTreeMap<>(GameData.getItemDataMap()); + val monsterDataMap = new Int2ObjectRBTreeMap<>(GameData.getMonsterDataMap()); + val sceneDataMap = new Int2ObjectRBTreeMap<>(GameData.getSceneDataMap()); + val questDataMap = new Int2ObjectRBTreeMap<>(GameData.getQuestDataMap()); + + Function getPad = m -> "%" + m.lastKey().toString().length() + "s : "; + + // Create builders and helper functions + val handbookBuilders = IntStream.range(0, TextStrings.NUM_LANGUAGES).mapToObj(i -> new StringBuilder()).toList(); + Consumer newSection = title -> handbookBuilders.forEach(b -> b.append("\n\n// " + title + "\n")); + Consumer newLine = line -> handbookBuilders.forEach(b -> b.append(line + "\n")); + BiConsumer newTranslatedLine = (template, textmapHashes) -> { + val textstrings = textmapHashes.intStream().mapToObj(textMaps::get).toList(); + for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { + String s = template; + for (int j = 0; j < textstrings.size(); j++) + s = s.replace("{"+j+"}", textstrings.get(j).strings[i]); + handbookBuilders.get(i).append(s + "\n"); + } + }; // Preamble - final List handbookBuilders = new ArrayList<>(TextStrings.NUM_LANGUAGES); - final String now = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now()); - for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) - handbookBuilders.add(new StringBuilder() - .append("// Grasscutter " + GameConstants.VERSION + " GM Handbook\n") - .append("// Created " + now + "\n\n") - .append("// Commands\n")); + newLine.accept("// Grasscutter " + GameConstants.VERSION + " GM Handbook"); + newLine.accept("// Created " + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())); + // Commands + newSection.accept("Commands"); final List cmdList = CommandMap.getInstance().getHandlersAsList(); final String padCmdLabel = "%" + cmdList.stream().map(CommandHandler::getLabel).map(String::length).max(Integer::compare).get().toString() + "s : "; for (CommandHandler cmd : cmdList) { @@ -62,42 +82,52 @@ public final class Tools { handbookBuilders.get(i).append(label + desc + "\n"); } } - // Avatars, Items, Monsters - final String[] handbookSections = {"Avatars", "Items", "Monsters"}; - final Int2IntSortedMap[] handbookNames = {avatarNames, itemNames, monsterNames}; - for (int section = 0; section < handbookSections.length; section++) { - final var h = handbookNames[section]; - final String s = "\n\n// " + handbookSections[section] + "\n"; - handbookBuilders.forEach(b -> b.append(s)); - final String padId = "%" + Integer.toString(h.keySet().lastInt()).length() + "s : "; - h.forEach((id, hash) -> { - final String sId = padId.formatted(id); - final TextStrings t = textMaps.get((int) hash); - for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) - handbookBuilders.get(i).append(sId + t.strings[i] + "\n"); - }); - } + // Avatars + newSection.accept("Avatars"); + val avatarPre = getPad.apply(avatarDataMap); + avatarDataMap.forEach((id, data) -> newTranslatedLine.accept(avatarPre.formatted(id) + "{0}", IntList.of((int) data.getNameTextMapHash()))); + // Items + newSection.accept("Items"); + val itemPre = getPad.apply(itemDataMap); + itemDataMap.forEach((id, data) -> { + val nameHash = (int) data.getNameTextMapHash(); + switch (data.getMaterialType()) { + case MATERIAL_BGM: + val bgmNameHash = Optional.ofNullable(data.getItemUse()) + .map(u -> u.get(0)) + .map(u -> u.getUseParam()) + .filter(u -> u.length > 0) + .map(u -> Integer.parseInt(u[0])) + .map(bgmId -> GameData.getHomeWorldBgmDataMap().get(bgmId)) + .map(bgm -> bgm.getBgmNameTextMapHash()); + if (bgmNameHash.isPresent()) { + int trackNameHash = (int) ((long) bgmNameHash.get()); // Textmap hashes are u32, we index as i32 but store most of them as Long :') + if (textMaps.containsKey(trackNameHash)) { + newTranslatedLine.accept(itemPre.formatted(id) + "{0} - {1}", IntList.of(nameHash, trackNameHash)); + return; + } + } // Fall-through + default: + newTranslatedLine.accept(itemPre.formatted(id) + "{0}", IntList.of(nameHash)); + return; + } + }); + // Monsters + newSection.accept("Monsters"); + val monsterPre = getPad.apply(monsterDataMap); + monsterDataMap.forEach((id, data) -> newTranslatedLine.accept( + monsterPre.formatted(id) + data.getMonsterName() + " - {0}", + IntList.of((int) data.getNameTextMapHash()))); // Scenes - no translations - handbookBuilders.forEach(b -> b.append("\n\n// Scenes\n")); - final var sceneDataMap = GameData.getSceneDataMap(); - final String padSceneId = "%" + Integer.toString(sceneDataMap.keySet().intStream().max().getAsInt()).length() + "d : "; - sceneDataMap.keySet().intStream().sorted().forEach(id -> { - final String sId = padSceneId.formatted(id); - final String data = sceneDataMap.get(id).getScriptData(); - handbookBuilders.forEach(b -> b.append(sId + data + "\n")); - }); + newSection.accept("Scenes"); + val padSceneId = getPad.apply(sceneDataMap); + sceneDataMap.forEach((id, data) -> newLine.accept(padSceneId.formatted(id) + data.getScriptData())); // Quests - handbookBuilders.forEach(b -> b.append("\n\n// Quests\n")); - final var questDataMap = GameData.getQuestDataMap(); - final String padQuestId = "%" + Integer.toString(questDataMap.keySet().intStream().max().getAsInt()).length() + "d : "; - questDataMap.keySet().intStream().sorted().forEach(id -> { - final String sId = padQuestId.formatted(id); - final QuestData data = questDataMap.get(id); - final TextStrings title = textMaps.get((int) mainQuestTitles.get(data.getMainId())); - final TextStrings desc = textMaps.get((int) data.getDescTextMapHash()); - for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) - handbookBuilders.get(i).append(sId + title.strings[i] + " - " + desc.strings[i] + "\n"); - }); + newSection.accept("Quests"); + val padQuestId = getPad.apply(questDataMap); + questDataMap.forEach((id, data) -> newTranslatedLine.accept( + padQuestId.formatted(id) + "{0} - {1}", + IntList.of((int) mainQuestTitles.get(data.getMainId()), (int) data.getDescTextMapHash()))); // Write txt files for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) { diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java index d24ef3280..56f1cda1f 100644 --- a/src/main/java/emu/grasscutter/utils/Language.java +++ b/src/main/java/emu/grasscutter/utils/Language.java @@ -20,7 +20,6 @@ import static emu.grasscutter.utils.FileUtils.getResourcePath; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; @@ -46,6 +45,7 @@ public final class Language { private final String languageCode; private final Map translations = new ConcurrentHashMap<>(); + private static boolean scannedTextmaps = false; // Ensure that we don't infinitely rescan on cache misses that don't exist /** * Creates a language instance from a code. @@ -242,7 +242,7 @@ public final class Language { } } - private static final int TEXTMAP_CACHE_VERSION = 0x9CCACE03; + private static final int TEXTMAP_CACHE_VERSION = 0x9CCACE04; @EqualsAndHashCode public static class TextStrings implements Serializable { public static final String[] ARR_LANGUAGES = {"EN", "CHS", "CHT", "JP", "KR", "DE", "ES", "FR", "ID", "PT", "RU", "TH", "VI"}; public static final String[] ARR_GC_LANGUAGES = {"en-US", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "en-US", "es-ES", "fr-FR", "en-US", "en-US", "ru-RU", "en-US", "en-US"}; // TODO: Update the placeholder en-US entries if we ever add GC translations for the missing client languages @@ -385,9 +385,10 @@ public final class Language { } public static TextStrings getTextMapKey(long hash) { - if (textMapStrings == null) + int key = (int) hash; + if ((textMapStrings == null) || (!scannedTextmaps && !textMapStrings.containsKey(key))) loadTextMaps(); - return textMapStrings.get((int) hash); + return textMapStrings.get(key); } public static void loadTextMaps() { @@ -429,6 +430,7 @@ public final class Language { usedHashes.add((int) v.getDescTextMapHash()); }); GameData.getItemDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); + GameData.getHomeWorldBgmDataMap().forEach((k, v) -> usedHashes.add((int) v.getBgmNameTextMapHash())); GameData.getMonsterDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash())); GameData.getMainQuestDataMap().forEach((k, v) -> usedHashes.add((int) v.getTitleTextMapHash())); GameData.getQuestDataMap().forEach((k, v) -> usedHashes.add((int) v.getDescTextMapHash())); @@ -441,6 +443,7 @@ public final class Language { usedHashes.add((int) 2864268523L); // Weapon Event Wish textMapStrings = loadTextMapFiles(usedHashes); + scannedTextmaps = true; try { saveTextMapsCache(textMapStrings); } catch (IOException e) {