package emu.grasscutter.tools; import emu.grasscutter.command.Command; import emu.grasscutter.command.Command.TargetRequirement; import emu.grasscutter.command.CommandMap; import emu.grasscutter.data.GameData; import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.props.SceneType; import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.Language; import lombok.AllArgsConstructor; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public interface Dumpers { // See `src/handbook/data/README.md` for attributions. /** * Fetches the description of a command. * * @param locale The locale to use. * @param command The command to get the description of. * @return The description of the command. */ private static String commandDescription(String locale, Command command) { try { // Get the language by the locale. var language = Language.getLanguage(locale); if (language == null) throw new IllegalArgumentException("Invalid language."); return language.get("commands." + command.label() + ".description"); } catch (IllegalArgumentException ignored) { return command.label(); } } /** * Encodes the dump into comma separated values. * * @param dump The dump to encode. * @return The encoded dump. */ private static String miniEncode(Map dump) { return dump.entrySet().stream() .map(entry -> entry.getKey() + "," + entry.getValue().toString()) .collect(Collectors.joining("\n")); } /** * Encodes the dump into comma separated values. * * @param dump The dump to encode. * @return The encoded dump. */ private static String miniEncode(Map dump, String... headers) { return String.join(",", headers) + "\n" + Dumpers.miniEncode(dump); } /** * Dumps all commands to a JSON file. * * @param locale The language to dump the commands in. */ static void dumpCommands(String locale) { // Check that commands are registered. var commandMap = CommandMap.getInstance(); if (commandMap == null) commandMap = new CommandMap(true); // Convert all registered commands to an info map. var dump = new HashMap(); commandMap .getAnnotationsAsList() .forEach( command -> { var description = Dumpers.commandDescription(locale, command); var labels = new ArrayList() { { this.add(command.label()); this.addAll(List.of(command.aliases())); } }; // Add the command info to the list. dump.put( command.label(), new CommandInfo( labels, description, List.of(command.usage()), List.of(command.permission(), command.permissionTargeted()), command.targetRequirement())); }); try { // Create a file for the dump. var file = new File("commands.json"); if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); if (!file.exists() && !file.createNewFile()) throw new RuntimeException("Failed to create file."); // Write the dump to the file. Files.writeString(file.toPath(), JsonUtils.encode(dump)); } catch (IOException ignored) { throw new RuntimeException("Failed to write to file."); } } /** * Dumps all avatars to a CSV file. * * @param locale The language to dump the avatars in. */ static void dumpAvatars(String locale) { // Reload resources. ResourceLoader.loadAll(); Language.loadTextMaps(); // Convert all known avatars to an avatar map. var dump = new HashMap(); GameData.getAvatarDataMap() .forEach( (id, avatar) -> { var langHash = avatar.getNameTextMapHash(); dump.put( id, new AvatarInfo( langHash == 0 ? avatar.getName() : Language.getTextMapKey(langHash).get(locale), avatar.getQualityType().equals("QUALITY_PURPLE") ? Quality.EPIC : Quality.LEGENDARY)); }); try { // Create a file for the dump. var file = new File("avatars.csv"); if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); if (!file.exists() && !file.createNewFile()) throw new RuntimeException("Failed to create file."); // Write the dump to the file. Files.writeString(file.toPath(), Dumpers.miniEncode(dump)); } catch (IOException ignored) { throw new RuntimeException("Failed to write to file."); } } /** * Dumps all items to a CSVv file. * * @param locale The language to dump the items in. */ static void dumpItems(String locale) { // Reload resources. ResourceLoader.loadAll(); Language.loadTextMaps(); // Convert all known items to an item map. var originalDump = new ArrayList(); GameData.getItemDataMap() .forEach( (id, item) -> originalDump.add( new ItemInfo( id, Language.getTextMapKey(item.getNameTextMapHash()).get(locale), Quality.from(item.getRankLevel()), item.getItemType(), item.getIcon().length() > 0 ? item.getIcon().substring(3) : ""))); // Create a new dump with filtered duplicates. var names = new ArrayList(); var dump = new HashMap(); originalDump.forEach( item -> { // Validate the item. if (item.name.contains("[CHS]")) return; if (names.contains(item.name)) return; if (dump.containsKey(item.id)) return; // Add the item to the dump. names.add(item.name); dump.put(item.id, item); }); try { // Create a file for the dump. var file = new File("items.csv"); if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); if (!file.exists() && !file.createNewFile()) throw new RuntimeException("Failed to create file."); // Write the dump to the file. Files.writeString(file.toPath(), Dumpers.miniEncode(dump)); } catch (IOException ignored) { throw new RuntimeException("Failed to write to file."); } } /** Dumps all scenes to a CSV file. */ static void dumpScenes() { // Reload resources. ResourceLoader.loadAll(); Language.loadTextMaps(); // Convert all known scenes to a scene map. var dump = new HashMap(); GameData.getSceneDataMap() .forEach( (id, scene) -> dump.put(id, new SceneInfo(scene.getScriptData(), scene.getSceneType()))); try { // Create a file for the dump. var file = new File("scenes.csv"); if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); if (!file.exists() && !file.createNewFile()) throw new RuntimeException("Failed to create file."); // Write the dump to the file. Files.writeString(file.toPath(), Dumpers.miniEncode(dump)); } catch (IOException ignored) { throw new RuntimeException("Failed to write to file."); } } /** * Dumps all entities to a CSV file. * * @param locale The language to dump the entities in. */ static void dumpEntities(String locale) { // Reload resources. ResourceLoader.loadAll(); Language.loadTextMaps(); // Convert all known avatars to an avatar map. var dump = new HashMap(); GameData.getMonsterDataMap() .forEach( (id, monster) -> { var langHash = monster.getNameTextMapHash(); dump.put( id, new EntityInfo( langHash == 0 ? monster.getMonsterName() : Language.getTextMapKey(langHash).get(locale), monster.getMonsterName())); }); try { // Create a file for the dump. var file = new File("entities.csv"); if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); if (!file.exists() && !file.createNewFile()) throw new RuntimeException("Failed to create file."); // Write the dump to the file. Files.writeString(file.toPath(), Dumpers.miniEncode(dump)); } catch (IOException ignored) { throw new RuntimeException("Failed to write to file."); } } /** * Dumps all quests to a JSON file. * * @param locale The language to dump the quests in. */ static void dumpQuests(String locale) { // Reload resources. ResourceLoader.loadAll(); Language.loadTextMaps(); // Convert all known quests to a quest map. var dump = new HashMap(); GameData.getQuestDataMap().forEach((id, quest) -> { var langHash = quest.getDescTextMapHash(); dump.put(id, new QuestInfo( langHash == 0 ? "Unknown" : Language.getTextMapKey(langHash).get(locale) .replaceAll(",", "\\\\"), quest.getMainId() )); }); // Convert all known main quests into a quest map. var mainDump = new HashMap(); GameData.getMainQuestDataMap().forEach((id, mainQuest) -> { var langHash = mainQuest.getTitleTextMapHash(); mainDump.put(id, new MainQuestInfo( langHash == 0 ? "Unknown" : Language.getTextMapKey(langHash).get(locale) .replaceAll(",", "\\\\") )); }); try { // Create a file for the dump. var file = new File("quests.csv"); if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); if (!file.exists() && !file.createNewFile()) throw new RuntimeException("Failed to create file."); // Write the dump to the file. Files.writeString(file.toPath(), Dumpers.miniEncode(dump, "id", "description", "mainId")); } catch (IOException ignored) { throw new RuntimeException("Failed to write to file."); } try { // Create a file for the dump. var file = new File("mainquests.csv"); if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); if (!file.exists() && !file.createNewFile()) throw new RuntimeException("Failed to create file."); // Write the dump to the file. Files.writeString(file.toPath(), Dumpers.miniEncode(mainDump, "id", "title")); } catch (IOException ignored) { throw new RuntimeException("Failed to write to file."); } } @AllArgsConstructor class CommandInfo { public List name; public String description; public List usage; public List permission; public TargetRequirement target; } @AllArgsConstructor class AvatarInfo { public String name; public Quality quality; @Override public String toString() { return this.name + "," + this.quality; } } @AllArgsConstructor class ItemInfo { public Integer id; public String name; public Quality quality; public ItemType type; public String icon; @Override public String toString() { return this.name + "," + this.quality + "," + this.type + "," + this.icon; } } @AllArgsConstructor class SceneInfo { public String identifier; public SceneType type; @Override public String toString() { return this.identifier + "," + this.type; } } @AllArgsConstructor class EntityInfo { public String name; public String internal; @Override public String toString() { return this.name + "," + this.internal; } } @AllArgsConstructor class MainQuestInfo { public String title; @Override public String toString() { return this.title; } } @AllArgsConstructor class QuestInfo { public String description; public int mainQuest; @Override public String toString() { return this.description + "," + this.mainQuest; } } enum Quality { LEGENDARY, EPIC, RARE, UNCOMMON, COMMON, UNKNOWN; /** * Convert a rank level to a quality. * * @param rankLevel The rank level to convert. * @return The quality. */ static Quality from(int rankLevel) { return switch (rankLevel) { case 0 -> UNKNOWN; case 1 -> COMMON; case 2 -> UNCOMMON; case 3 -> RARE; case 4 -> EPIC; default -> LEGENDARY; }; } } }