Files
Grasscutter/src/main/java/emu/grasscutter/utils/lang/Language.java
T
Magix 9e5b57a043 Merge unstable into development (#2173)
* Remove more scene synchronized

* Fix worktop options not appearing

* Format code [skip actions]

* Fix delay with server tasks

* Format code [skip actions]

* Fully fix fairy clock (#2146)

* Fix scene transition

* fully fix fairy clock

* Re-add call to `Player#updatePlayerGameTime`

* Format code [skip actions]

* Initialize the script loader in `ResourceLoader#loadAll`

* Fix region removal checking

* Format code [skip actions]

* Use Lombok's `EqualsAndHashCode` for comparing scene regions

* Format code [skip actions]

* Move 'invalid gather object' to `trace`

* Add more information to the 'unknown condition handler' message

* Move invalid ability action to trace

* Make `KcpTunnel` public

* Validate the NPC being talked to

* Format code [skip actions]

* NPCs are not spawned server side; change logic to handle it

* Format code [skip actions]

* unload scene when there are no players (#2147)

* unload scene when there are no players

* Update src/main/java/emu/grasscutter/game/world/Scene.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Check if a command should be copied or HTTP should be used

* Lint Code [skip actions]

* Fix character names rendering incorrectly

* Add basic troubleshooting command

* Implement handbook teleporting

also a few formatting changes and sort data by logical sense

* Fix listener `ConcurrentModificationException` issue

* Add color change to `Join the Community!`

* Lint Code [skip actions]

* Make clickable buttons appear clickable

* Remove 'Mechanicus' entities from the list of entities

* Format code [skip actions]

* Fix going back returning a blank screen

* Implement entity spawning

* Add setting level to entity card

* Add support for 'plain text' mode

* Make descriptions of objects scrollable

* Lint Code [skip actions]

* Format code [skip actions]

* Change the way existing hooks work

* Format code [skip actions]

* Upgrade Javalin to 5.5.0 & Fix project warnings

* Upgrade logging libraries

* Fix gacha mappings static file issue

* Add temporary backwards compatability for `ServerHelper`

* Format code [skip actions]

* Remove artifact signatures from VCS

* Fix forge queue data protocol definition

* Run `spotlessApply`

* Format code [skip actions]

* Download data required for building artifacts

* Add call for Facebook logins

* Add the wiki page as a submodule

* Format code [skip actions]

* Update translation (#2150)

* Update translation

* Update translation

* Separate the dispatch and game servers (pt. 1)

gacha is still broken, handbook still needs to be done

* Format code [skip actions]

* Separate the dispatch and game servers (pt. 2)

this commit fixes the gacha page

* Add description for '/troubleshoot'

* Set default avatar talent level to 10

* Separate the dispatch and game servers (pt. 3)

implement handbook across servers!

* Format code [skip actions]

* Update GitHub Actions to use 'download-file' over 'wget'

* Gm handbook lmao (#2149)

* Fix font issue

* Fix avatars

* Fix text overflow in commands

* Fix virtualized lists and items page 😭😭

* magix why 💀

* use hover style in all minicards

* button

* remove console.log

* lint

* Add icons

* magix asked

* Fix overflow padding issue

* Fix achievement text overflow

* remove icons from repo

* Change command icon

* Add the wiki page as a submodule

* total magix moment

* fix text overflow in commands

* Fix discord button

* Make text scale on Minicard

* import icons and font from another source

* Add hover effects to siebar buttons

* move font and readme to submodule repo

* Make data folder a submodule

* import icons and font from data submodule

* Update README.md

* total magix moment

* magix moment v2

* submodule change

* Import `.webp` files

* Resize `HomeButton`

* Fix 'Copy Command' reappearing after changing pages

---------

Co-authored-by: KingRainbow44 <kobedo11@gmail.com>

* Lint Code [skip actions]

* Download data for the build, not for the lint

* format imports

this is really just to see if build handbook works kek

* Implement proper handbook authentication (pt. 1)

* Implement proper handbook authentication (pt. 2)

* Format code [skip actions]

* Add quest data dumping for the handbook

* Change colors to fit _something suitable_

* Format code [skip actions]

* Fix force pushing to branches after linting

* Fix logic of `SetPlayerPropReq`

* Move more group loading to `trace`

* Add handbook IP authentication in hybrid mode

* Fix player level up not displaying on the client properly

* Format code [skip actions]

* Fix game time locking

* Format code [skip actions]

* Update player properties

* Format code [skip actions]

* Move `warn`s for groups to `debug`

* Fix player pausing

* Move more logs to `trace`

* Use `removeItemById` for deleting items via quests

* Clean up logger more

* Pause in-game time when the world is paused

* Format code [skip actions]

* More player property documentation

* Multi-threaded resource loading

* Format code [skip actions]

* Add quest widgets

* Add quests page (basic impl.)

* Add/fix colors

also fix tailwind

* Remove banned packets

client modifications already perform the job of blocking malicious packets from being executed, no point in having this if self-windy is wanted

* Re-add `BeginCameraSceneLookNotify`

* Fix being unable to attack (#2157)

* Add `PlayerOpenChestEvent`

* Add methods to get players from the server

* Add static methods to register an event handler

* Add `PlayerEnterDungeonEvent`

* Remove legacy documentation from `PlayerMoveEvent`

* Add `PlayerChatEvent`

* Add defaults to `Position`

* Clean up `.utils`

* Revert `Multi-threaded resource loading`

* Fix changing target UID when talking to the server

* Lint Code [skip actions]

* Format code [skip actions]

* fix NPC talk triggering main quest in 46101 (#2158)

Make it so that only talks where the param matches the talkId are checked.

* Format code [skip actions]

* Partially fix Chasing Shadows (#2159)

* Partially fix Chasing Shadows

* Go ahead and move it before the return before Magix tells me to.

* Format code [skip actions]

* Bring back period lol (#2160)

* Disable SNI for the HTTPS server

* Add `EntityCreationEvent`

* Add initial startup message

this is so the server appears like its preparing to start

* Format code [skip actions]

* Enable debug mode for plugin loggers if enabled for the primary logger

* Add documentation about `WorldAreaConfigData`

* Make more fields in excels accessible

* Remove deprecated fields from `GetShopRsp`

* Run `spotlessApply` on definitions

* Add `PlayerEnterAreaEvent`

* Optimize event calls

* Fix event invokes

* Format code [skip actions]

* Remove manual autofinish for main quests. (#2162)

* Add world areas to the textmap cache

* Format code [skip actions]

* Don't overdefine variables in extended classes (#2163)

* Add dumper for world areas

* Format code [skip actions]

* instantiate personalLineList (#2165)

* Fix protocol definitions

thank you Nazrin! (+ hiro for raw definitions)

* Fix the background color leaking from the character widget

* Change HTML spacing to 2 spaces

* Implement hiding widgets

* Change scrollbar to a vibrant color

* Add _some_ scaling to the home buttons and its text

* Build the handbook with Gradle

* Fix the 'finer details' with the handbook UI

* Lint Code [skip actions]

* Fix target destination for the Gradle-built handbook

* Implement fetching a player across servers & Add a chainable JsonObject

useful for plugins! might be used in grasscutter eventually

* Fix GitHub actions

* Fix event calling & canceling

* Run `spotlessApply`

* Rename fields (might be wrong)

* Add/update all/more protocol definitions

* Add/update all/more protocol definitions

* Remove outdated packet

* Fix protocol definitions

* Format code [skip actions]

* Implement some lua variables for less console spam (#2172)

* Implement some lua variables for less console spam

* Add GetHostQuestState

This fixes some chapter 3 stuff.

* Format code [skip actions]

* Fix merge import

* Format code [skip actions]

* Fully fix fairy clock for real this time (#2167)

* Fully fix fairy clock For real this time

* Make it so relogging keeps the time lock state.

* Refactor out questLockTime

* Per Hartie, the client packet needs to be changed too

* Update src/main/java/emu/grasscutter/game/world/World.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update src/main/java/emu/grasscutter/server/packet/recv/HandlerClientLockGameTimeNotify.java

* Remove all code not needed to get clock working

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Implement a proper ability system (#2166)

* Apply fix `21dec2fe`

* Apply fix `89d01d5f`

* Apply fix `d900f154`

this one was already implemented; updated to use call from previous commit

* Ability changing commit

TODO: change info to debug

* Remove use of deprecated methods/fields

* Temp commit v2
(Adding LoseHP and some fixes)

* Oopsie

* Probably fix monster battle

* Fix issue with reflecting into fields

* Fix some things

* Fix ability names for 3.6 resources

* Improve logging

---------

Co-authored-by: StartForKiller <jesussanz2003@gmail.com>

* Format code [skip actions]

* Add system for sending messages between servers

* Format some code

* Remove protocol definitions from Spotless

* Default debug to false; enable with `-debug`

* Implement completely useless global value copying

* HACK: Return the avatar which holds the weapon when the weapon is referred to by ID

* Add properties to `AbilityModifier`

* Change the way HTML is served after authentication

* Use thread executors to speed up the database loading process

* Format code [skip actions]

* Add system for setting handbook address and port

* Lint Code [skip actions]

* Format code [skip actions]

* Fix game-related data not saving

* Format code [skip actions]

* Fix handbook server details

* Lint Code [skip actions]

* Format code [skip actions]

* Use the headers provided by a context to get the IP address

should acknowledge #1975

* Format code [skip actions]

* Move more logs to `trace`

* Format code [skip actions]

* more trace

* Fix something and implement weapon entities

* Format code [skip actions]

* Fix `EntityWeapon`

* Remove deprecated API & Fix resource checking

* Fix unnecessary warning for first-time setup

* Implement handbook request limiting

* Format code [skip actions]

* Fix new avatar weapons being null

* Format code [skip actions]

* Fix issue with 35303 being un-completable & Try to fix fulfilled quest conditions being met

* Load activity config on server startup

* Require plugins to specify an API version and match with the server

* Add default open state ignore list

* Format code [skip actions]

* Quick fix for questing, needs more investigation
This would make the questing work again

* Remove existing hack for 35303

* Fix ignored open states from being set

* Format code [skip actions]

* fix the stupidest bug ive ever seen

* Optimize player kicking on server close

* Format code [skip actions]

* Re-add hack to fix 35303

* Update GitHub actions

* Format code [skip actions]

* Potentially fix issues with regions

* Download additional handbook data

* Revert "Potentially fix issues with regions"

This reverts commit 84e3823695.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: scooterboo <lewasite@yahoo.com>
Co-authored-by: Tesutarin <105267106+Tesutarin@users.noreply.github.com>
Co-authored-by: Scald <104459145+Arikatsu@users.noreply.github.com>
Co-authored-by: StartForKiller <jesussanz2003@gmail.com>
2023-05-31 23:48:16 -04:00

527 lines
22 KiB
Java

package emu.grasscutter.utils.lang;
import static emu.grasscutter.config.Configuration.FALLBACK_LANGUAGE;
import static emu.grasscutter.utils.FileUtils.*;
import com.google.gson.*;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.*;
import emu.grasscutter.data.excels.achievement.AchievementData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.utils.*;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.objects.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.*;
import java.util.stream.*;
import lombok.EqualsAndHashCode;
public final class Language {
private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>();
private static final int TEXTMAP_CACHE_VERSION = 0x9CCACE04;
private static final Pattern textMapKeyValueRegex = Pattern.compile("\"(\\d+)\": \"(.+)\"");
private static final Path TEXTMAP_CACHE_PATH = getCachePath("TextMap/TextMapCache.bin");
private static boolean scannedTextmaps =
false; // Ensure that we don't infinitely rescan on cache misses that don't exist
private static Int2ObjectMap<TextStrings> textMapStrings;
private final String languageCode;
private final Map<String, String> translations = new ConcurrentHashMap<>();
/** Reads a file and creates a language instance. */
private Language(LanguageStreamDescription description) {
languageCode = description.getLanguageCode();
try {
var object =
JsonUtils.decode(
Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class);
object
.entrySet()
.forEach(entry -> putFlattenedKey(translations, entry.getKey(), entry.getValue()));
} catch (Exception exception) {
Grasscutter.getLogger()
.warn("Failed to load language file: " + description.getLanguageCode(), exception);
}
}
/**
* Creates a language instance from a code.
*
* @param langCode The language code.
* @return A language instance.
*/
public static Language getLanguage(String langCode) {
if (cachedLanguages.containsKey(langCode)) {
return cachedLanguages.get(langCode);
}
var fallbackLanguageCode = Utils.getLanguageCode(FALLBACK_LANGUAGE);
var description = getLanguageFileDescription(langCode, fallbackLanguageCode);
var actualLanguageCode = description.getLanguageCode();
Language languageInst;
if (description.getLanguageFile() != null) {
languageInst = new Language(description);
cachedLanguages.put(actualLanguageCode, languageInst);
} else {
languageInst = cachedLanguages.get(actualLanguageCode);
cachedLanguages.put(langCode, languageInst);
}
return languageInst;
}
/**
* Returns the translated value from the key while substituting arguments.
*
* @param key The key of the translated value to return.
* @param args The arguments to substitute.
* @return A translated value with arguments substituted.
*/
public static String translate(String key, Object... args) {
String translated = Grasscutter.getLanguage().get(key);
for (int i = 0; i < args.length; i++) {
args[i] =
switch (args[i].getClass().getSimpleName()) {
case "String" -> args[i];
case "TextStrings" -> ((TextStrings) args[i])
.get(0)
.replace("\\\\n", "\\n"); // TODO: Change this to server language
default -> args[i].toString();
};
}
try {
return translated.formatted(args);
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to format string: " + key, exception);
return translated;
}
}
/**
* Returns the translated value from the key while substituting arguments.
*
* @param locale The locale to use.
* @param key The key of the translated value to return.
* @param args The arguments to substitute.
* @return A translated value with arguments substituted.
*/
public static String translate(Locale locale, String key, Object... args) {
if (locale == null) {
return translate(key, args);
}
var langCode = Utils.getLanguageCode(locale);
var translated = getLanguage(langCode).get(key);
for (var i = 0; i < args.length; i++) {
args[i] =
switch (args[i].getClass().getSimpleName()) {
case "String" -> args[i];
case "TextStrings" -> ((TextStrings) args[i])
.getGC(langCode)
.replace("\\\\n", "\n"); // Note that we don't unescape \n for server console
default -> args[i].toString();
};
}
try {
return translated.formatted(args);
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to format string: " + key, exception);
return translated;
}
}
/**
* Returns the translated value from the key while substituting arguments.
*
* @param player Target player
* @param key The key of the translated value to return.
* @param args The arguments to substitute.
* @return A translated value with arguments substituted.
*/
public static String translate(Player player, String key, Object... args) {
if (player == null) {
return translate(key, args);
}
return translate(player.getAccount().getLocale(), key, args);
}
/**
* Recursive helper function to flatten a Json tree Converts input like {"foo": {"bar": "baz"}} to
* {"foo.bar": "baz"}
*
* @param map The map to insert the keys into
* @param key The flattened key of the current element
* @param element The current element
*/
private static void putFlattenedKey(Map<String, String> map, String key, JsonElement element) {
if (element.isJsonObject()) {
element
.getAsJsonObject()
.entrySet()
.forEach(
entry -> {
String keyPrefix = key.isEmpty() ? "" : key + ".";
putFlattenedKey(map, keyPrefix + entry.getKey(), entry.getValue());
});
} else {
map.put(key, element.getAsString());
}
}
/**
* create a LanguageStreamDescription
*
* @param languageCode The name of the language code.
* @param fallbackLanguageCode The name of the fallback language code.
*/
private static LanguageStreamDescription getLanguageFileDescription(
String languageCode, String fallbackLanguageCode) {
var fileName = languageCode + ".json";
var fallback = fallbackLanguageCode + ".json";
String actualLanguageCode = languageCode;
InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName);
if (file == null) { // Provided fallback language.
Grasscutter.getLogger()
.warn("Failed to load language file: " + fileName + ", falling back to: " + fallback);
actualLanguageCode = fallbackLanguageCode;
if (cachedLanguages.containsKey(actualLanguageCode)) {
return new LanguageStreamDescription(actualLanguageCode, null);
}
file = Grasscutter.class.getResourceAsStream("/languages/" + fallback);
}
if (file == null) { // Fallback the fallback language.
Grasscutter.getLogger()
.warn("Failed to load language file: " + fallback + ", falling back to: en-US.json");
actualLanguageCode = "en-US";
if (cachedLanguages.containsKey(actualLanguageCode)) {
return new LanguageStreamDescription(actualLanguageCode, null);
}
file = Grasscutter.class.getResourceAsStream("/languages/en-US.json");
}
if (file == null)
throw new RuntimeException(
"Unable to load the primary, fallback, and 'en-US' language files.");
return new LanguageStreamDescription(actualLanguageCode, file);
}
private static Int2ObjectMap<String> loadTextMapFile(String language, IntSet nameHashes) {
Int2ObjectMap<String> output = new Int2ObjectOpenHashMap<>();
try (BufferedReader file =
Files.newBufferedReader(
getResourcePath("TextMap/TextMap" + language + ".json"), StandardCharsets.UTF_8)) {
Matcher matcher = textMapKeyValueRegex.matcher("");
return new Int2ObjectOpenHashMap<>(
file.lines()
.sequential()
.map(matcher::reset) // Side effects, but it's faster than making a new one
.filter(Matcher::find)
.filter(
m ->
nameHashes.contains(
(int) Long.parseLong(m.group(1)))) // TODO: Cache this parse somehow
.collect(
Collectors.toMap(
m -> (int) Long.parseLong(m.group(1)),
m -> m.group(2).replace("\\\"", "\""))));
} catch (Exception e) {
Grasscutter.getLogger().error("Error loading textmap: " + language);
Grasscutter.getLogger().error(e.toString());
}
return output;
}
private static Int2ObjectMap<TextStrings> loadTextMapFiles(IntSet nameHashes) {
Map<Integer, Int2ObjectMap<String>>
mapLanguageMaps = // Separate step to process the textmaps in parallel
TextStrings.LIST_LANGUAGES.parallelStream()
.collect(
Collectors.toConcurrentMap(
s -> TextStrings.MAP_LANGUAGES.getInt(s),
s -> loadTextMapFile(s, nameHashes)));
List<Int2ObjectMap<String>> languageMaps =
IntStream.range(0, TextStrings.NUM_LANGUAGES)
.mapToObj(i -> mapLanguageMaps.get(i))
.collect(Collectors.toList());
Map<TextStrings, TextStrings> canonicalTextStrings = new HashMap<>();
return new Int2ObjectOpenHashMap<TextStrings>(
nameHashes
.intStream()
.boxed()
.collect(
Collectors.toMap(
key -> key,
key -> {
TextStrings t =
new TextStrings(
IntStream.range(0, TextStrings.NUM_LANGUAGES)
.mapToObj(i -> languageMaps.get(i).get((int) key))
.collect(Collectors.toList()),
key);
return canonicalTextStrings.computeIfAbsent(t, x -> t);
})));
}
@SuppressWarnings("unchecked")
private static Int2ObjectMap<TextStrings> loadTextMapsCache() throws Exception {
try (ObjectInputStream file =
new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(TEXTMAP_CACHE_PATH), 0x100000))) {
final int fileVersion = file.readInt();
if (fileVersion != TEXTMAP_CACHE_VERSION) throw new Exception("Invalid cache version");
return (Int2ObjectMap<TextStrings>) file.readObject();
}
}
private static void saveTextMapsCache(Int2ObjectMap<TextStrings> input) throws IOException {
Files.createDirectories(TEXTMAP_CACHE_PATH.getParent());
try (var file =
new ObjectOutputStream(
new BufferedOutputStream(
Files.newOutputStream(TEXTMAP_CACHE_PATH, StandardOpenOption.CREATE), 0x100000))) {
file.writeInt(TEXTMAP_CACHE_VERSION);
file.writeObject(input);
}
}
@Deprecated(forRemoval = true)
public static Int2ObjectMap<TextStrings> getTextMapStrings() {
if (textMapStrings == null) loadTextMaps();
return textMapStrings;
}
public static TextStrings getTextMapKey(int key) {
if ((textMapStrings == null) || (!scannedTextmaps && !textMapStrings.containsKey(key)))
loadTextMaps();
return textMapStrings.get(key);
}
public static TextStrings getTextMapKey(long hash) {
return getTextMapKey((int) hash);
}
/** Loads game text maps with caching. */
public static void loadTextMaps() {
Language.loadTextMaps(false);
}
/**
* Loads game language data (text maps).
*
* @param bypassCache Should the cache be bypassed?
*/
public static void loadTextMaps(boolean bypassCache) {
// Check system timestamps on cache and resources
if (!bypassCache)
try {
long cacheModified = Files.getLastModifiedTime(TEXTMAP_CACHE_PATH).toMillis();
long textmapsModified =
Files.list(getResourcePath("TextMap"))
.filter(path -> path.toString().endsWith(".json"))
.map(
path -> {
try {
return Files.getLastModifiedTime(path).toMillis();
} catch (Exception ignored) {
Grasscutter.getLogger()
.debug("Exception while checking modified time: ", path);
return Long.MAX_VALUE; // Don't use cache, something has gone wrong
}
})
.max(Long::compare)
.get();
Grasscutter.getLogger()
.debug(
"Cache modified %d, textmap modified %d"
.formatted(cacheModified, textmapsModified));
if (textmapsModified < cacheModified) {
// Try loading from cache
Grasscutter.getLogger().debug("Loading cached 'TextMaps'...");
textMapStrings = loadTextMapsCache();
return;
}
} catch (NoSuchFileException ignored) {
// Cache doesn't exist, generate it.
} catch (Exception exception) {
Grasscutter.getLogger().error("Error loading textmaps cache: " + exception.toString());
}
// Regenerate cache
Grasscutter.getLogger().debug("Generating TextMaps cache");
ResourceLoader.loadAll();
IntSet usedHashes = new IntOpenHashSet();
GameData.getAchievementDataMap().values().stream()
.filter(AchievementData::isUsed)
.forEach(
a -> {
usedHashes.add((int) a.getTitleTextMapHash());
usedHashes.add((int) a.getDescTextMapHash());
});
GameData.getAvatarDataMap().forEach((k, v) -> usedHashes.add((int) v.getNameTextMapHash()));
GameData.getAvatarSkillDataMap()
.forEach(
(k, v) -> {
usedHashes.add((int) v.getNameTextMapHash());
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()));
GameData.getWorldAreaDataMap().forEach((k, v) -> usedHashes.add((int) v.getTextMapHash()));
// Incidental strings
usedHashes.add((int) 4233146695L); // Character
usedHashes.add((int) 4231343903L); // Weapon
usedHashes.add((int) 332935371L); // Standard Wish
usedHashes.add((int) 2272170627L); // Character Event Wish
usedHashes.add((int) 3352513147L); // Character Event Wish-2
usedHashes.add((int) 2864268523L); // Weapon Event Wish
textMapStrings = loadTextMapFiles(usedHashes);
scannedTextmaps = true;
try {
saveTextMapsCache(textMapStrings);
} catch (IOException e) {
Grasscutter.getLogger().error("Failed to save TextMap cache: ", e);
}
}
/** get language code */
public String getLanguageCode() {
return languageCode;
}
/**
* Returns the value (as a string) from a nested key.
*
* @param key The key to look for.
* @return The value (as a string) from a nested key.
*/
public String get(String key) {
if (translations.containsKey(key)) return translations.get(key);
String valueNotFoundPattern = "This value does not exist. Please report this to the Discord: ";
String result = valueNotFoundPattern + key;
if (!languageCode.equals("en-US")) {
String englishValue = getLanguage("en-US").get(key);
if (!englishValue.contains(valueNotFoundPattern)) {
result += "\nhere is english version:\n" + englishValue;
}
}
return result;
}
private static class LanguageStreamDescription {
private final String languageCode;
private final InputStream languageFile;
public LanguageStreamDescription(String languageCode, InputStream languageFile) {
this.languageCode = languageCode;
this.languageFile = languageFile;
}
public String getLanguageCode() {
return languageCode;
}
public InputStream getLanguageFile() {
return languageFile;
}
}
@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
public static final int NUM_LANGUAGES = ARR_LANGUAGES.length;
public static final List<String> LIST_LANGUAGES = Arrays.asList(ARR_LANGUAGES);
public static final Object2IntMap<String>
MAP_LANGUAGES = // Map "EN": 0, "CHS": 1, ..., "VI": 12
new Object2IntOpenHashMap<>(
IntStream.range(0, ARR_LANGUAGES.length)
.boxed()
.collect(Collectors.toMap(i -> ARR_LANGUAGES[i], i -> i)));
public static final Object2IntMap<String> MAP_GC_LANGUAGES = // Map "en-US": 0, "zh-CN": 1, ...
new Object2IntOpenHashMap<>(
IntStream.range(0, ARR_GC_LANGUAGES.length)
.boxed()
.collect(
Collectors.toMap(
i -> ARR_GC_LANGUAGES[i],
i -> i,
(i1, i2) -> i1))); // Have to handle duplicates referring back to the first
public String[] strings = new String[ARR_LANGUAGES.length];
public TextStrings() {}
public TextStrings(String init) {
for (int i = 0; i < NUM_LANGUAGES; i++) this.strings[i] = init;
}
public TextStrings(List<String> strings, int key) {
// Some hashes don't have strings for some languages :(
String nullReplacement = "[N/A] %d".formatted((long) key & 0xFFFFFFFFL);
for (int i = 0; i < NUM_LANGUAGES; i++) { // Find first non-null if there is any
String s = strings.get(i);
if (s != null) {
nullReplacement = "[%s] - %s".formatted(ARR_LANGUAGES[i], s);
break;
}
}
for (int i = 0; i < NUM_LANGUAGES; i++) {
String s = strings.get(i);
if (s != null) this.strings[i] = s;
else this.strings[i] = nullReplacement;
}
}
public static List<Language> getLanguages() {
return Arrays.stream(ARR_GC_LANGUAGES).map(Language::getLanguage).toList();
}
public String get(int languageIndex) {
return strings[languageIndex];
}
public String get(String languageCode) {
return strings[MAP_LANGUAGES.getOrDefault(languageCode, 0)];
}
public String getGC(String languageCode) {
return strings[MAP_GC_LANGUAGES.getOrDefault(languageCode, 0)];
}
public boolean set(String languageCode, String string) {
int index = MAP_LANGUAGES.getOrDefault(languageCode, -1);
if (index < 0) return false;
strings[index] = string;
return true;
}
}
}