package com.hypixel.hytale.server.core.modules.i18n;
import com.hypixel.hytale.assetstore.AssetPack;
import com.hypixel.hytale.assetstore.event.LoadedAssetsEvent;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.event.IEventDispatcher;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.logger.sentry.SkipSentryException;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.UpdateType;
import com.hypixel.hytale.protocol.packets.assets.UpdateTranslations;
import com.hypixel.hytale.server.core.HytaleServer;
import com.hypixel.hytale.server.core.HytaleServerConfig;
import com.hypixel.hytale.server.core.asset.AssetModule;
import com.hypixel.hytale.server.core.asset.AssetPackRegisterEvent;
import com.hypixel.hytale.server.core.asset.AssetPackUnregisterEvent;
import com.hypixel.hytale.server.core.asset.LoadAssetEvent;
import com.hypixel.hytale.server.core.asset.monitor.AssetMonitor;
import com.hypixel.hytale.server.core.asset.monitor.AssetMonitorHandler;
import com.hypixel.hytale.server.core.asset.monitor.EventKind;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.Bench;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.CraftingBench;
import com.hypixel.hytale.server.core.asset.type.item.config.FieldcraftCategory;
import com.hypixel.hytale.server.core.asset.type.item.config.ResourceType;
import com.hypixel.hytale.server.core.io.PacketHandler;
import com.hypixel.hytale.server.core.modules.i18n.commands.EnableTmpTagsCommand;
import com.hypixel.hytale.server.core.modules.i18n.commands.InternationalizationCommands;
import com.hypixel.hytale.server.core.modules.i18n.event.MessagesUpdated;
import com.hypixel.hytale.server.core.modules.i18n.parser.LangFileParser;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.Universe;
import com.hypixel.hytale.server.core.util.io.FileUtil;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class I18nModule extends JavaPlugin {
public static final PluginManifest MANIFEST = PluginManifest.corePlugin(I18nModule.class).depends(AssetModule.class).build();
public static final String DEFAULT_LANGUAGE = "en-US";
public static final Path FALLBACK_LANG_PATH = Paths.get("fallback.lang");
public static final String FILE_EXTENSION = ".lang";
public static final String SERVER_ASSETS = "Server";
public static final String LANGUAGE_ASSETS = "Languages";
public static final Path DEFAULT_GENERATED_PATH = Path.of("Server", "Languages", "en-US");
private static I18nModule instance;
private final Map<String, String> fallbacks = new ConcurrentHashMap();
private final Map<String, Map<String, String>> languages = new ConcurrentHashMap();
private final Map<String, Map<String, String>> cachedLanguages = new ConcurrentHashMap();
public static I18nModule get() {
return instance;
}
public I18nModule(@Nonnull JavaPluginInit parent) {
super(parent);
instance = this;
}
protected void setup() {
this.getEventRegistry().register(LoadAssetEvent.class, (event) -> {
for(AssetPack pack : AssetModule.get().getAssetPacks()) {
this.loadMessagesFromPack(pack);
}
});
this.getEventRegistry().register(AssetPackRegisterEvent.class, (event) -> this.loadMessagesFromPack(event.getAssetPack()));
this.getEventRegistry().register(AssetPackUnregisterEvent.class, (event) -> {
});
this.getEventRegistry().register((Class)LoadedAssetsEvent.class, BlockType.class, (event) -> {
Map<String, String> addedMessages = new Object2ObjectOpenHashMap<String, String>();
event.getLoadedAssets().values().forEach((item) -> {
Bench bench = item.getBench();
if (bench != null) {
String id = item.getId();
if (bench instanceof CraftingBench) {
CraftingBench craftingBench = (CraftingBench)bench;
for(CraftingBench.BenchCategory category : craftingBench.getCategories()) {
addedMessages.put("server.items." + id + ".bench.categories." + category.getId() + ".name", category.getName());
if (category.getItemCategories() != null) {
for(CraftingBench.BenchItemCategory itemCategory : category.getItemCategories()) {
addedMessages.put("server.items." + id + ".bench.categories." + category.getId() + ".itemCategories." + itemCategory.getId() + ".name", itemCategory.getName());
}
}
}
}
if (bench.getDescriptiveLabel() != null) {
addedMessages.put("server.items." + id + ".bench.descriptiveLabel", bench.getDescriptiveLabel());
}
}
});
this.addDefaultMessages(addedMessages, event.isInitial());
});
this.getEventRegistry().register((Class)LoadedAssetsEvent.class, FieldcraftCategory.class, (event) -> {
Map<String, String> addedMessages = new Object2ObjectOpenHashMap<String, String>();
event.getLoadedAssets().values().forEach((category) -> {
if (category.getName() != null) {
addedMessages.put("fieldcraftCategories." + category.getId() + ".name", category.getName());
}
});
this.addDefaultMessages(addedMessages, event.isInitial());
});
this.getEventRegistry().register((Class)LoadedAssetsEvent.class, ResourceType.class, (event) -> {
Map<String, String> addedMessages = new Object2ObjectOpenHashMap<String, String>();
event.getLoadedAssets().values().forEach((resourceType) -> {
if (resourceType.getName() != null) {
addedMessages.put("resourceTypes." + resourceType.getId() + ".name", resourceType.getName());
}
if (resourceType.getDescription() != null) {
addedMessages.put("resourceTypes." + resourceType.getId() + ".description", resourceType.getDescription());
}
});
this.addDefaultMessages(addedMessages, event.isInitial());
});
}
protected void start() {
this.getCommandRegistry().registerCommand(new InternationalizationCommands());
this.getCommandRegistry().registerCommand(new EnableTmpTagsCommand());
}
private void loadMessagesFromPack(AssetPack pack) {
Path languagesPath = pack.getRoot().resolve("Server").resolve("Languages");
if (Files.isDirectory(languagesPath, new LinkOption[0])) {
AssetMonitor assetMonitor = AssetModule.get().getAssetMonitor();
if (assetMonitor != null && !pack.isImmutable()) {
assetMonitor.monitorDirectoryFiles(languagesPath, new I18nAssetMonitorHandler(languagesPath));
}
try {
DirectoryStream<Path> directoryStream = Files.newDirectoryStream(languagesPath, (x$0) -> Files.isDirectory(x$0, new LinkOption[0]));
try {
for(Path path : directoryStream) {
try {
String languageKey = path.getFileName().toString();
int entriesCount = this.loadMessages(languageKey, path);
this.getLogger().at(Level.INFO).log("Loaded %d entries for '%s' from %s", entriesCount, languageKey, languagesPath);
} catch (IOException e) {
((HytaleLogger.Api)this.getLogger().at(Level.SEVERE).withCause(e)).log("Failed to load messages from: %s", path);
}
}
} catch (Throwable var12) {
if (directoryStream != null) {
try {
directoryStream.close();
} catch (Throwable var9) {
var12.addSuppressed(var9);
}
}
throw var12;
}
if (directoryStream != null) {
directoryStream.close();
}
} catch (IOException e) {
((HytaleLogger.Api)this.getLogger().at(Level.SEVERE).withCause(e)).log("Failed to load languages from: %s", languagesPath);
}
}
Path fallbackPath = languagesPath.resolve("fallback.lang");
if (Files.exists(fallbackPath, new LinkOption[0])) {
Properties properties = new Properties();
try {
properties.load(Files.newInputStream(fallbackPath));
for(Map.Entry<Object, Object> entry : properties.entrySet()) {
this.fallbacks.put((String)entry.getKey(), (String)entry.getValue());
}
if (!properties.isEmpty()) {
this.getLogger().at(Level.INFO).log("Loaded %d entries from %s", properties.size(), fallbackPath);
}
} catch (IOException e) {
((HytaleLogger.Api)this.getLogger().at(Level.SEVERE).withCause(e)).log("Failed to load fallback languages from: %s", fallbackPath);
}
}
}
@Nonnull
public UpdateTranslations[] getUpdatePacketsForChanges(String languageKey, @Nonnull Map<String, Map<String, String>> changed, @Nonnull Map<String, Map<String, String>> removed) {
Map<String, String> removedMessages = this.getMessages(removed, languageKey);
Map<String, String> changedMessages = this.getMessages(changed, languageKey);
int size = (removedMessages.isEmpty() ? 0 : 1) + (changedMessages.isEmpty() ? 0 : 1);
UpdateTranslations[] packets = new UpdateTranslations[size];
int index = 0;
if (!removedMessages.isEmpty()) {
packets[index++] = new UpdateTranslations(UpdateType.Remove, removedMessages);
}
if (!changedMessages.isEmpty()) {
packets[index] = new UpdateTranslations(UpdateType.AddOrUpdate, changedMessages);
}
return packets;
}
private void addDefaultMessages(@Nonnull Map<String, String> messages, boolean isInitial) {
Map<String, String> languageMap = (Map)this.languages.computeIfAbsent("en-US", (k) -> new ConcurrentHashMap());
for(Map.Entry<String, String> entry : messages.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
languageMap.put((String)entry.getKey(), (String)entry.getValue());
} else {
this.getLogger().at(Level.WARNING).log("Attempted to add invalid default translation message: %s=%s", entry.getKey(), entry.getValue());
}
}
if (!isInitial) {
UpdateTranslations packet = new UpdateTranslations(UpdateType.AddOrUpdate, messages);
Universe.get().broadcastPacketNoCache(packet);
IEventDispatcher<MessagesUpdated, MessagesUpdated> dispatch = HytaleServer.get().getEventBus().dispatchFor(MessagesUpdated.class);
if (dispatch.hasListener()) {
Object2ObjectOpenHashMap<String, Map<String, String>> languageMapping = new Object2ObjectOpenHashMap<String, Map<String, String>>();
languageMapping.put("en-US", messages);
dispatch.dispatch(new MessagesUpdated(languageMapping, new Object2ObjectOpenHashMap()));
}
}
}
private int loadMessages(String languageKey, @Nonnull Path languagePath) throws IOException {
Map<String, String> messages = (Map)this.languages.computeIfAbsent(languageKey, (k) -> new ConcurrentHashMap());
Stream<Path> stream = Files.find(languagePath, 2147483647, (path, attr) -> path.toString().endsWith(".lang") && attr.isRegularFile(), FileUtil.DEFAULT_WALK_TREE_OPTIONS_ARRAY);
int var5;
try {
var5 = stream.mapToInt((path) -> {
String prefix = this.getPrefix(languagePath, path);
return this.loadMessagesFrom(messages, prefix, path);
}).sum();
} catch (Throwable var8) {
if (stream != null) {
try {
stream.close();
} catch (Throwable var7) {
var8.addSuppressed(var7);
}
}
throw var8;
}
if (stream != null) {
stream.close();
}
return var5;
}
private int loadMessagesFrom(@Nonnull Map<String, String> messages, String prefix, @Nonnull Path path) {
Map<String, String> properties;
try {
BufferedReader inputStream = Files.newBufferedReader(path);
try {
properties = LangFileParser.parse(inputStream);
} catch (Throwable var11) {
if (inputStream != null) {
try {
inputStream.close();
} catch (Throwable var10) {
var11.addSuppressed(var10);
}
}
throw var11;
}
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
((HytaleLogger.Api)this.getLogger().at(Level.SEVERE).withCause(new SkipSentryException(e))).log("Error parsing language file: %s", path.toString());
return 0;
}
for(Map.Entry<String, String> entry : properties.entrySet()) {
String key = prefix + "." + (String)entry.getKey();
String value = (String)entry.getValue();
String prev = (String)messages.get(key);
if (prev != null) {
if (!prev.equals(value)) {
this.getLogger().at(Level.WARNING).log("'%s' has multiple definitions: `%s` and `%s`", key, prev, value);
}
} else {
messages.put(key, value);
}
}
return properties.size();
}
@Nonnull
private String getPrefix(@Nonnull Path languagePath, @Nonnull Path path) {
String prefix = "";
Path directory = path.getParent();
if (!languagePath.equals(directory)) {
Path relativePath = languagePath.relativize(directory);
prefix = prefix + relativePath.toString().replace(File.separatorChar, '.') + ".";
}
String name = path.getFileName().toString();
prefix = prefix + name.substring(0, name.length() - ".lang".length());
return prefix;
}
@Nonnull
public Map<String, String> getMessages(String language) {
return (Map)this.cachedLanguages.computeIfAbsent(language, (key) -> Collections.unmodifiableMap(this.getMessages(this.languages, key)));
}
public Map<String, String> getMessages(@Nonnull Map<String, Map<String, String>> languageMap, @Nullable String language) {
if (language == null) {
return this.getMessages(languageMap, "en-US");
} else {
Map<String, String> messages = (Map)languageMap.get(language);
if ("en-US".equals(language)) {
return messages != null ? messages : Collections.emptyMap();
} else {
String fallback = (String)this.fallbacks.getOrDefault(language, "en-US");
Map<String, String> fallbackMessages = this.getMessages(languageMap, fallback);
if (fallbackMessages == null) {
return messages != null ? messages : Collections.emptyMap();
} else if (messages == null) {
return fallbackMessages;
} else {
Object2ObjectOpenHashMap<String, String> map = new Object2ObjectOpenHashMap<String, String>(fallbackMessages);
map.putAll(messages);
return map;
}
}
}
}
public void sendTranslations(@Nonnull PacketHandler packetHandler, String language) {
if (!this.isDisabled()) {
packetHandler.writeNoCache(new UpdateTranslations(UpdateType.Init, this.getMessages(language)));
}
}
@Nullable
public String getMessage(String language, @Nonnull String key) {
HytaleServerConfig config = HytaleServer.get().getConfig();
if (config != null && config.isDisplayTmpTagsInStrings()) {
return (String)this.getMessages(language).get(key);
} else {
String translatedString = (String)this.getMessages(language).get(key);
return translatedString != null ? translatedString.replace("[TMP] ", "").replace("[TMP]", "") : null;
}
}
private class I18nAssetMonitorHandler implements AssetMonitorHandler {
private final Path languagesPath;
public I18nAssetMonitorHandler(Path languagesPath) {
this.languagesPath = languagesPath;
}
public Object getKey() {
return I18nModule.this;
}
public boolean test(Path path, EventKind eventKind) {
return !Files.isDirectory(path, new LinkOption[0]) && path.getFileName().toString().endsWith(".lang") && I18nModule.this.isEnabled();
}
public void accept(Map<Path, EventKind> map) {
Map<String, Map<String, String>> removed = new Object2ObjectOpenHashMap<String, Map<String, String>>();
Map<String, Map<String, String>> changed = new Object2ObjectOpenHashMap<String, Map<String, String>>();
for(Map.Entry<Path, EventKind> entry : map.entrySet()) {
Path path = (Path)entry.getKey();
EventKind eventKind = (EventKind)entry.getValue();
Path normalized = path.toAbsolutePath().normalize();
Path relativized = this.languagesPath.relativize(normalized);
if (I18nModule.FALLBACK_LANG_PATH.equals(relativized)) {
Properties properties = new Properties();
try {
properties.load(Files.newInputStream(path));
} catch (IOException e) {
((HytaleLogger.Api)I18nModule.this.getLogger().at(Level.SEVERE).withCause(e)).log("Failed to load fallback languages from: %s", path);
continue;
}
I18nModule.this.fallbacks.clear();
properties.forEach((keyx, value) -> I18nModule.this.fallbacks.put((String)keyx, (String)value));
} else {
String languageKey = relativized.getName(0).toString();
Path langPath = this.languagesPath.resolve(languageKey).toAbsolutePath().normalize();
String prefix = I18nModule.this.getPrefix(langPath, normalized);
switch (eventKind) {
case ENTRY_MODIFY:
case ENTRY_DELETE:
String prefixWithDot = prefix + ".";
Map<String, String> removedMessages = (Map)removed.computeIfAbsent(languageKey, (k) -> new Object2ObjectOpenHashMap());
Map<String, String> messages = (Map)I18nModule.this.languages.computeIfAbsent(languageKey, (k) -> new ConcurrentHashMap());
Iterator<String> iterator = messages.keySet().iterator();
while(iterator.hasNext()) {
String key = (String)iterator.next();
if (key.startsWith(prefixWithDot)) {
removedMessages.put(key, "");
iterator.remove();
}
}
if (eventKind == EventKind.ENTRY_DELETE) {
break;
}
case ENTRY_CREATE:
Map<String, String> changedMessages = (Map)changed.computeIfAbsent(languageKey, (k) -> new Object2ObjectOpenHashMap());
I18nModule.this.loadMessagesFrom(changedMessages, prefix, path);
}
}
}
if (!removed.isEmpty() || !changed.isEmpty()) {
for(Map.Entry<String, Map<String, String>> changedLang : changed.entrySet()) {
Map<String, String> messages = (Map)I18nModule.this.languages.computeIfAbsent((String)changedLang.getKey(), (k) -> new ConcurrentHashMap());
messages.putAll((Map)changedLang.getValue());
I18nModule.this.cachedLanguages.remove(changedLang.getKey());
}
for(Map.Entry<String, Map<String, String>> removedLang : removed.entrySet()) {
I18nModule.this.cachedLanguages.remove(removedLang.getKey());
Map<String, String> orig = I18nModule.this.getMessages((String)removedLang.getKey());
Map<String, String> changedMessages = (Map)changed.computeIfAbsent((String)removedLang.getKey(), (k) -> new Object2ObjectOpenHashMap());
Iterator<String> iterator = ((Map)removedLang.getValue()).keySet().iterator();
while(iterator.hasNext()) {
String removedKey = (String)iterator.next();
if (changedMessages.containsKey(removedKey)) {
iterator.remove();
} else {
String fallback = (String)orig.get(removedKey);
if (fallback != null) {
iterator.remove();
changedMessages.put(removedKey, fallback);
}
}
}
}
List<PlayerRef> players = Universe.get().getPlayers();
Map<String, UpdateTranslations[]> updatePackets = new Object2ObjectOpenHashMap<String, UpdateTranslations[]>();
for(PlayerRef playerRef : players) {
PacketHandler handler = playerRef.getPacketHandler();
String languageKey = playerRef.getLanguage();
UpdateTranslations[] packets = (UpdateTranslations[])updatePackets.get(languageKey);
if (packets == null) {
packets = I18nModule.this.getUpdatePacketsForChanges(languageKey, changed, removed);
updatePackets.put(languageKey, packets);
}
if (packets.length != 0) {
handler.write((Packet[])packets);
}
}
IEventDispatcher<MessagesUpdated, MessagesUpdated> dispatch = HytaleServer.get().getEventBus().dispatchFor(MessagesUpdated.class);
if (dispatch.hasListener()) {
dispatch.dispatch(new MessagesUpdated(changed, removed));
}
I18nModule.this.getLogger().at(Level.INFO).log("Handled language changes for: %s", changed.keySet());
}
}
}
}