Tools.java 14.5 KB
Newer Older
Melledy's avatar
Melledy committed
1
2
package emu.grasscutter.tools;

3
import java.io.File;
4
import java.io.FileOutputStream;
5
import java.io.IOException;
6
import java.io.OutputStreamWriter;
Melledy's avatar
Melledy committed
7
import java.io.PrintWriter;
8
import java.nio.charset.StandardCharsets;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
Melledy's avatar
Melledy committed
11
12
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
13
import java.util.*;
14
import java.util.function.Function;
AnimeGitB's avatar
AnimeGitB committed
15
import java.util.stream.Collectors;
16
import java.util.stream.IntStream;
17
import java.util.stream.LongStream;
Melledy's avatar
Melledy committed
18

19
import emu.grasscutter.GameConstants;
Melledy's avatar
Melledy committed
20
import emu.grasscutter.Grasscutter;
21
import emu.grasscutter.command.CommandHandler;
22
import emu.grasscutter.command.CommandMap;
23
import emu.grasscutter.data.GameData;
Melledy's avatar
Melledy committed
24
import emu.grasscutter.data.ResourceLoader;
Melledy's avatar
Melledy committed
25
26
import emu.grasscutter.data.excels.AvatarData;
import emu.grasscutter.data.excels.ItemData;
AnimeGitB's avatar
AnimeGitB committed
27
import emu.grasscutter.utils.Language;
AnimeGitB's avatar
AnimeGitB committed
28
import emu.grasscutter.utils.Language.TextStrings;
AnimeGitB's avatar
AnimeGitB committed
29
import it.unimi.dsi.fastutil.ints.Int2IntRBTreeMap;
30
31
import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap;
import lombok.val;
Melledy's avatar
Melledy committed
32

33
import static emu.grasscutter.utils.FileUtils.getResourcePath;
34
import static emu.grasscutter.utils.Language.getTextMapKey;
方块君's avatar
方块君 committed
35

KingRainbow44's avatar
KingRainbow44 committed
36
public final class Tools {
KingRainbow44's avatar
KingRainbow44 committed
37
38
39
40
    /**
     * This generates the GM handbooks with a message by default.
     * @throws Exception If an error occurs while generating the handbooks.
     */
AnimeGitB's avatar
AnimeGitB committed
41
    public static void createGmHandbooks() throws Exception {
KingRainbow44's avatar
KingRainbow44 committed
42
43
44
45
46
47
48
49
50
        Tools.createGmHandbooks(true);
    }

    /**
     * Generates a GM handbook for each language.
     * @param message Should a message be printed to the console?
     * @throws Exception If an error occurs while generating the handbooks.
     */
    public static void createGmHandbooks(boolean message) throws Exception {
51
        val languages = Language.TextStrings.getLanguages();
52

AnimeGitB's avatar
AnimeGitB committed
53
        ResourceLoader.loadAll();
54
55
56
57
58
59
60
61
62
        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());

KingRainbow44's avatar
KingRainbow44 committed
63
        Function<SortedMap<?, ?>, String> getPad = m -> "%" + m.lastKey().toString().length() + "s : ";
64
65
66

        // Create builders and helper functions
        val handbookBuilders = IntStream.range(0, TextStrings.NUM_LANGUAGES).mapToObj(i -> new StringBuilder()).toList();
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
        var h = new Object() {
            void newLine(String line) {
                handbookBuilders.forEach(b -> b.append(line + "\n"));
            }
            void newSection(String title) {
                newLine("\n\n// " + title);
            }
            void newTranslatedLine(String template, TextStrings... textstrings) {
                for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
                    String s = template;
                    for (int j = 0; j < textstrings.length; j++)
                        s = s.replace("{"+j+"}", textstrings[j].strings[i]);
                    handbookBuilders.get(i).append(s + "\n");
                }
            }
            void newTranslatedLine(String template, long... hashes) {
                newTranslatedLine(template, LongStream.of(hashes).mapToObj(hash -> getTextMapKey(hash)).toArray(TextStrings[]::new));
84
85
            }
        };
AnimeGitB's avatar
AnimeGitB committed
86

AnimeGitB's avatar
AnimeGitB committed
87
        // Preamble
88
89
        h.newLine("// Grasscutter " + GameConstants.VERSION + " GM Handbook");
        h.newLine("// Created " + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now()));
90

AnimeGitB's avatar
AnimeGitB committed
91
        // Commands
92
        h.newSection("Commands");
93
        final List<CommandHandler> cmdList = CommandMap.getInstance().getHandlersAsList();
94
        final String padCmdLabel = "%" + cmdList.stream().map(CommandHandler::getLabel).map(String::length).max(Integer::compare).get().toString() + "s : ";
AnimeGitB's avatar
AnimeGitB committed
95
        for (CommandHandler cmd : cmdList) {
96
97
            final String label = padCmdLabel.formatted(cmd.getLabel());
            final String descKey = cmd.getDescriptionKey();
AnimeGitB's avatar
AnimeGitB committed
98
            for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
99
                String desc = languages.get(i).get(descKey).replace("\n", "\n\t\t\t\t").replace("\t", "    ");
100
                handbookBuilders.get(i).append(label + desc + "\n");
AnimeGitB's avatar
AnimeGitB committed
101
102
            }
        }
103
        // Avatars
104
        h.newSection("Avatars");
105
        val avatarPre = getPad.apply(avatarDataMap);
106
        avatarDataMap.forEach((id, data) -> h.newTranslatedLine(avatarPre.formatted(id) + "{0}", data.getNameTextMapHash()));
107
        // Items
108
        h.newSection("Items");
109
110
        val itemPre = getPad.apply(itemDataMap);
        itemDataMap.forEach((id, data) -> {
111
            val name = getTextMapKey(data.getNameTextMapHash());
112
113
            switch (data.getMaterialType()) {
                case MATERIAL_BGM:
114
                    val bgmName = Optional.ofNullable(data.getItemUse())
115
116
117
118
119
                        .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))
120
121
122
123
124
                        .map(bgm -> bgm.getBgmNameTextMapHash())
                        .map(hash -> getTextMapKey(hash));
                    if (bgmName.isPresent()) {
                        h.newTranslatedLine(itemPre.formatted(id) + "{0} - {1}", name, bgmName.get());
                        return;
125
126
                    }  // Fall-through
                default:
127
                    h.newTranslatedLine(itemPre.formatted(id) + "{0}", name);
128
129
                    return;
            }
AnimeGitB's avatar
AnimeGitB committed
130
        });
131
        // Monsters
132
        h.newSection("Monsters");
133
        val monsterPre = getPad.apply(monsterDataMap);
134
        monsterDataMap.forEach((id, data) -> h.newTranslatedLine(
135
            monsterPre.formatted(id) + data.getMonsterName() + " - {0}",
136
            data.getNameTextMapHash()));
137
        // Scenes - no translations
138
        h.newSection("Scenes");
139
        val padSceneId = getPad.apply(sceneDataMap);
140
        sceneDataMap.forEach((id, data) -> h.newLine(padSceneId.formatted(id) + data.getScriptData()));
AnimeGitB's avatar
AnimeGitB committed
141
        // Quests
142
        h.newSection("Quests");
143
        val padQuestId = getPad.apply(questDataMap);
144
        questDataMap.forEach((id, data) -> h.newTranslatedLine(
145
            padQuestId.formatted(id) + "{0} - {1}",
146
147
            mainQuestTitles.get(data.getMainId()),
            data.getDescTextMapHash()));
AnimeGitB's avatar
AnimeGitB committed
148
149
150

        // Write txt files
        for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
151
152
153
            File GMHandbookOutputpath=new File("./GM Handbook");
            GMHandbookOutputpath.mkdir();
            final String fileName = "./GM Handbook/GM Handbook - %s.txt".formatted(TextStrings.ARR_LANGUAGES[i]);
AnimeGitB's avatar
AnimeGitB committed
154
            try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), false)) {
155
                writer.write(handbookBuilders.get(i).toString());
AnimeGitB's avatar
AnimeGitB committed
156
157
            }
        }
KingRainbow44's avatar
KingRainbow44 committed
158
159

        if (message) Grasscutter.getLogger().info("GM Handbooks generated!");
AnimeGitB's avatar
AnimeGitB committed
160
161
    }

AnimeGitB's avatar
AnimeGitB committed
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
    public static List<String> createGachaMappingJsons() {
        final int NUM_LANGUAGES = Language.TextStrings.NUM_LANGUAGES;
        final Language.TextStrings CHARACTER = Language.getTextMapKey(4233146695L);  // "Character" in EN
        final Language.TextStrings WEAPON = Language.getTextMapKey(4231343903L);  // "Weapon" in EN
        final Language.TextStrings STANDARD_WISH = Language.getTextMapKey(332935371L);  // "Standard Wish" in EN
        final Language.TextStrings CHARACTER_EVENT_WISH = Language.getTextMapKey(2272170627L);  // "Character Event Wish" in EN
        final Language.TextStrings CHARACTER_EVENT_WISH_2 = Language.getTextMapKey(3352513147L);  // "Character Event Wish-2" in EN
        final Language.TextStrings WEAPON_EVENT_WISH = Language.getTextMapKey(2864268523L);  // "Weapon Event Wish" in EN
        final List<StringBuilder> sbs = new ArrayList<>(NUM_LANGUAGES);
        for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
            sbs.add(new StringBuilder("{\n"));  // Web requests should never need Windows line endings

        // Avatars
        GameData.getAvatarDataMap().keySet().intStream().sorted().forEach(id -> {
            AvatarData data = GameData.getAvatarDataMap().get(id);
            int avatarID = data.getId();
            if (avatarID >= 11000000) { // skip test avatar
                return;
            }
            String color = switch (data.getQualityType()) {
                case "QUALITY_PURPLE" -> "purple";
                case "QUALITY_ORANGE" -> "yellow";
                case "QUALITY_BLUE" -> "blue";
                default -> "";
            };
            Language.TextStrings avatarName = Language.getTextMapKey(data.getNameTextMapHash());
            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
                sbs.get(langIdx)
                    .append("\t\"")
                    .append(avatarID % 1000 + 1000)
                    .append("\": [\"")
                    .append(avatarName.get(langIdx))
                    .append(" (")
                    .append(CHARACTER.get(langIdx))
                    .append(")\", \"")
                    .append(color)
                    .append("\"],\n");
            }
        });

        // Weapons
        GameData.getItemDataMap().keySet().intStream().sorted().forEach(id -> {
            ItemData data = GameData.getItemDataMap().get(id);
            if (data.getId() <= 11101 || data.getId() >= 20000) {
                return; //skip non weapon items
            }
            String color = switch (data.getRankLevel()) {
                case 3 -> "blue";
                case 4 -> "purple";
                case 5 -> "yellow";
                default -> null;
            };
            if (color == null) return;  // skip unnecessary entries
            Language.TextStrings weaponName = Language.getTextMapKey(data.getNameTextMapHash());
            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
                sbs.get(langIdx)
                    .append("\t\"")
                    .append(data.getId())
                    .append("\": [\"")
                    .append(weaponName.get(langIdx).replaceAll("\"", "\\\\\""))
                    .append(" (")
                    .append(WEAPON.get(langIdx))
                    .append(")\", \"")
                    .append(color)
                    .append("\"],\n");
            }
        });

        for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
            sbs.get(langIdx)
                .append("\t\"200\": \"")
                .append(STANDARD_WISH.get(langIdx))
                .append("\",\n\t\"301\": \"")
                .append(CHARACTER_EVENT_WISH.get(langIdx))
                .append("\",\n\t\"400\": \"")
                .append(CHARACTER_EVENT_WISH_2.get(langIdx))
                .append("\",\n\t\"302\": \"")
                .append(WEAPON_EVENT_WISH.get(langIdx))
                .append("\"\n}");
        }
        return sbs.stream().map(StringBuilder::toString).toList();
    }

245
    public static void createGachaMappings(Path location) throws IOException {
AnimeGitB's avatar
AnimeGitB committed
246
247
        ResourceLoader.loadResources();
        List<String> jsons = createGachaMappingJsons();
248
        var usedLocales = new HashSet<String>();
AnimeGitB's avatar
AnimeGitB committed
249
250
        StringBuilder sb = new StringBuilder("mappings = {\n");
        for (int i = 0; i < Language.TextStrings.NUM_LANGUAGES; i++) {
251
252
253
254
255
            String locale = Language.TextStrings.ARR_GC_LANGUAGES[i].toLowerCase();  // TODO: change the templates to not use lowercased locale codes
            if (usedLocales.add(locale)) {  // Some locales fallback to en-us, we don't want to redefine en-us with vietnamese strings
                sb.append("\t\"%s\": ".formatted(locale));
                sb.append(jsons.get(i).replace("\n", "\n\t") + ",\n");
            }
AnimeGitB's avatar
AnimeGitB committed
256
257
258
259
        }
        sb.setLength(sb.length() - 2);  // Delete trailing ",\n"
        sb.append("\n}");

260
261
262
        Files.createDirectories(location.getParent());
        Files.writeString(location, sb);
        Grasscutter.getLogger().info("Mappings generated to " + location);
github-actions's avatar
github-actions committed
263
264
265
266
    }

    public static List<String> getAvailableLanguage() {
        List<String> availableLangList = new ArrayList<>();
267
268
269
270
271
272
273
274
        try {
            Files.newDirectoryStream(getResourcePath("TextMap"), "TextMap*.json").forEach(path -> {
                availableLangList.add(path.getFileName().toString().replace("TextMap", "").replace(".json", "").toLowerCase());
            });
        } catch (IOException e) {
            Grasscutter.getLogger().error("Failed to get available languages:", e);
        }
        return availableLangList;
github-actions's avatar
github-actions committed
275
276
    }

277
    @Deprecated(forRemoval = true, since = "1.2.3")
github-actions's avatar
github-actions committed
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
    public static String getLanguageOption() {
        List<String> availableLangList = getAvailableLanguage();

        // Use system out for better format
        if (availableLangList.size() == 1) {
            return availableLangList.get(0).toUpperCase();
        }
        StringBuilder stagedMessage = new StringBuilder();
        stagedMessage.append("The following languages mappings are available, please select one: [default: EN] \n");

        StringBuilder groupedLangList = new StringBuilder(">\t"); String input;
        int groupedLangCount = 0;

        for (String availableLanguage: availableLangList) {
            groupedLangCount++;
            groupedLangList.append(availableLanguage).append("\t");

            if (groupedLangCount == 6) {
                stagedMessage.append(groupedLangList).append("\n");
                groupedLangCount = 0;
                groupedLangList = new StringBuilder(">\t");
            }
        }

        if (groupedLangCount > 0) {
            stagedMessage.append(groupedLangList).append("\n");
        }

306
        stagedMessage.append("\nYour choice: [EN] ");
github-actions's avatar
github-actions committed
307
308
309
310
311
312

        input = Grasscutter.getConsole().readLine(stagedMessage.toString());
        if (availableLangList.contains(input.toLowerCase())) {
            return input.toUpperCase();
        }

313
        Grasscutter.getLogger().info("Invalid option. Will use EN (English) as fallback."); return "EN";
github-actions's avatar
github-actions committed
314
    }
315
}