Tools.java 14 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
15
16
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
AnimeGitB's avatar
AnimeGitB committed
17
import java.util.stream.Collectors;
18
import java.util.stream.IntStream;
Melledy's avatar
Melledy committed
19

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

35
import static emu.grasscutter.config.Configuration.*;
36
import static emu.grasscutter.utils.FileUtils.getResourcePath;
方块君's avatar
方块君 committed
37

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

AnimeGitB's avatar
AnimeGitB committed
43
        ResourceLoader.loadAll();
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
        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();
        Consumer<String> newSection = title -> handbookBuilders.forEach(b -> b.append("\n\n// " + title + "\n"));
        Consumer<String> newLine = line -> handbookBuilders.forEach(b -> b.append(line + "\n"));
        BiConsumer<String, IntList> 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");
            }
        };
AnimeGitB's avatar
AnimeGitB committed
68

AnimeGitB's avatar
AnimeGitB committed
69
        // Preamble
70
71
72
        newLine.accept("// Grasscutter " + GameConstants.VERSION + " GM Handbook");
        newLine.accept("// Created " + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now()));

AnimeGitB's avatar
AnimeGitB committed
73
        // Commands
74
        newSection.accept("Commands");
75
        final List<CommandHandler> cmdList = CommandMap.getInstance().getHandlersAsList();
76
        final String padCmdLabel = "%" + cmdList.stream().map(CommandHandler::getLabel).map(String::length).max(Integer::compare).get().toString() + "s : ";
AnimeGitB's avatar
AnimeGitB committed
77
        for (CommandHandler cmd : cmdList) {
78
79
            final String label = padCmdLabel.formatted(cmd.getLabel());
            final String descKey = cmd.getDescriptionKey();
AnimeGitB's avatar
AnimeGitB committed
80
            for (int i = 0; i < TextStrings.NUM_LANGUAGES; i++) {
81
                String desc = languages.get(i).get(descKey).replace("\n", "\n\t\t\t\t").replace("\t", "    ");
82
                handbookBuilders.get(i).append(label + desc + "\n");
AnimeGitB's avatar
AnimeGitB committed
83
84
            }
        }
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
        // 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;
            }
AnimeGitB's avatar
AnimeGitB committed
114
        });
115
116
117
118
119
120
121
122
123
124
        // 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
        newSection.accept("Scenes");
        val padSceneId = getPad.apply(sceneDataMap);
        sceneDataMap.forEach((id, data) -> newLine.accept(padSceneId.formatted(id) + data.getScriptData()));
AnimeGitB's avatar
AnimeGitB committed
125
        // Quests
126
127
128
129
130
        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())));
AnimeGitB's avatar
AnimeGitB committed
131
132
133

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

AnimeGitB's avatar
AnimeGitB committed
144
145
146
147
148
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
    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();
    }

227
    public static void createGachaMappings(Path location) throws IOException {
AnimeGitB's avatar
AnimeGitB committed
228
229
230
231
232
233
234
235
236
237
        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}");

238
239
240
        Files.createDirectories(location.getParent());
        Files.writeString(location, sb);
        Grasscutter.getLogger().info("Mappings generated to " + location);
github-actions's avatar
github-actions committed
241
242
243
244
    }

    public static List<String> getAvailableLanguage() {
        List<String> availableLangList = new ArrayList<>();
245
246
247
248
249
250
251
252
        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
253
254
    }

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

284
        stagedMessage.append("\nYour choice: [EN] ");
github-actions's avatar
github-actions committed
285
286
287
288
289
290

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

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