Tools.java 13.8 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.config.Configuration.*;
34
import static emu.grasscutter.utils.FileUtils.getResourcePath;
35
import static emu.grasscutter.utils.Language.getTextMapKey;
方块君's avatar
方块君 committed
36

KingRainbow44's avatar
KingRainbow44 committed
37
public final class Tools {
AnimeGitB's avatar
AnimeGitB committed
38
    public static void createGmHandbooks() throws Exception {
39
        val languages = Language.TextStrings.getLanguages();
40

AnimeGitB's avatar
AnimeGitB committed
41
        ResourceLoader.loadAll();
42
43
44
45
46
47
48
49
50
51
52
53
54
        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<SortedMap, String> 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();
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
        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));
72
73
            }
        };
AnimeGitB's avatar
AnimeGitB committed
74

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

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

        // Write txt files
        for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
139
140
141
            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
142
            try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8), false)) {
143
                writer.write(handbookBuilders.get(i).toString());
AnimeGitB's avatar
AnimeGitB committed
144
145
146
147
148
            }
        }
        Grasscutter.getLogger().info("GM Handbooks generated!");
    }

AnimeGitB's avatar
AnimeGitB committed
149
150
151
152
153
154
155
156
157
158
159
160
161
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
    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();
    }

232
    public static void createGachaMappings(Path location) throws IOException {
AnimeGitB's avatar
AnimeGitB committed
233
234
235
236
237
238
239
240
241
242
        ResourceLoader.loadResources();
        List<String> jsons = createGachaMappingJsons();
        StringBuilder sb = new StringBuilder("mappings = {\n");
        for (int i = 0; i < Language.TextStrings.NUM_LANGUAGES; i++) {
            sb.append("\t\"%s\": ".formatted(Language.TextStrings.ARR_GC_LANGUAGES[i].toLowerCase()));  // TODO: change the templates to not use lowercased locale codes
            sb.append(jsons.get(i).replace("\n", "\n\t") + ",\n");
        }
        sb.setLength(sb.length() - 2);  // Delete trailing ",\n"
        sb.append("\n}");

243
244
245
        Files.createDirectories(location.getParent());
        Files.writeString(location, sb);
        Grasscutter.getLogger().info("Mappings generated to " + location);
github-actions's avatar
github-actions committed
246
247
248
249
    }

    public static List<String> getAvailableLanguage() {
        List<String> availableLangList = new ArrayList<>();
250
251
252
253
254
255
256
257
        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
258
259
    }

260
    @Deprecated(forRemoval = true, since = "1.2.3")
github-actions's avatar
github-actions committed
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
    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");
        }

289
        stagedMessage.append("\nYour choice: [EN] ");
github-actions's avatar
github-actions committed
290
291
292
293
294
295

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

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