diff --git a/Core/Models/Enums/ConfigTypes.cs b/Core/Models/Enums/ConfigTypes.cs index 66cfac6a..e3cdfad5 100644 --- a/Core/Models/Enums/ConfigTypes.cs +++ b/Core/Models/Enums/ConfigTypes.cs @@ -1,6 +1,107 @@ -namespace Core.Models.Enums; +using Core.Models.Spt.Config; + +namespace Core.Models.Enums; + +public static class ConfigTypesExtension +{ + public static string GetValue(this ConfigTypes type) + { + return type switch + { + ConfigTypes.AIRDROP => "spt-airdrop", + ConfigTypes.BACKUP => "spt-backup", + ConfigTypes.BOT => "spt-bot", + ConfigTypes.PMC => "spt-pmc", + ConfigTypes.CORE => "spt-core", + ConfigTypes.HEALTH => "spt-health", + ConfigTypes.HIDEOUT => "spt-hideout", + ConfigTypes.HTTP => "spt-http", + ConfigTypes.IN_RAID => "spt-inraid", + ConfigTypes.INSURANCE => "spt-insurance", + ConfigTypes.INVENTORY => "spt-inventory", + ConfigTypes.ITEM => "spt-item", + ConfigTypes.LOCALE => "spt-locale", + ConfigTypes.LOCATION => "spt-location", + ConfigTypes.LOOT => "spt-loot", + ConfigTypes.MATCH => "spt-match", + ConfigTypes.PLAYERSCAV => "spt-playerscav", + ConfigTypes.PMC_CHAT_RESPONSE => "spt-pmcchatresponse", + ConfigTypes.QUEST => "spt-quest", + ConfigTypes.RAGFAIR => "spt-ragfair", + ConfigTypes.REPAIR => "spt-repair", + ConfigTypes.SCAVCASE => "spt-scavcase", + ConfigTypes.TRADER => "spt-trader", + ConfigTypes.WEATHER => "spt-weather", + ConfigTypes.SEASONAL_EVENT => "spt-seasonalevents", + ConfigTypes.LOST_ON_DEATH => "spt-lostondeath", + ConfigTypes.GIFTS => "spt-gifts", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } + + public static Type GetConfigType(this ConfigTypes type) + { + return type switch + { + ConfigTypes.AIRDROP => typeof(AirdropConfig), + ConfigTypes.BACKUP => typeof(BackupConfig), + ConfigTypes.BOT => typeof(BotConfig), + ConfigTypes.PMC => typeof(PmcConfig), + ConfigTypes.CORE => typeof(CoreConfig), + ConfigTypes.HEALTH => typeof(HealthConfig), + ConfigTypes.HIDEOUT => typeof(HideoutConfig), + ConfigTypes.HTTP => typeof(HttpConfig), + ConfigTypes.IN_RAID => typeof(InRaidConfig), + ConfigTypes.INSURANCE => typeof(InsuranceConfig), + ConfigTypes.INVENTORY => typeof(InventoryConfig), + ConfigTypes.ITEM => typeof(ItemConfig), + ConfigTypes.LOCALE => typeof(LocaleConfig), + ConfigTypes.LOCATION => typeof(LocationConfig), + ConfigTypes.LOOT => typeof(LootConfig), + ConfigTypes.MATCH => typeof(MatchConfig), + ConfigTypes.PLAYERSCAV => typeof(PlayerScavConfig), + ConfigTypes.PMC_CHAT_RESPONSE => typeof(PmcChatResponse), + ConfigTypes.QUEST => typeof(QuestConfig), + ConfigTypes.RAGFAIR => typeof(RagfairConfig), + ConfigTypes.REPAIR => typeof(RepairConfig), + ConfigTypes.SCAVCASE => typeof(ScavCaseConfig), + ConfigTypes.TRADER => typeof(TraderConfig), + ConfigTypes.WEATHER => typeof(WeatherConfig), + ConfigTypes.SEASONAL_EVENT => typeof(SeasonalEventConfig), + ConfigTypes.LOST_ON_DEATH => typeof(LostOnDeathConfig), + ConfigTypes.GIFTS => typeof(GiftsConfig), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } +} public enum ConfigTypes { - + AIRDROP, + BACKUP, + BOT, + PMC, + CORE, + HEALTH, + HIDEOUT, + HTTP, + IN_RAID, + INSURANCE, + INVENTORY, + ITEM, + LOCALE, + LOCATION, + LOOT, + MATCH, + PLAYERSCAV, + PMC_CHAT_RESPONSE, + QUEST, + RAGFAIR, + REPAIR, + SCAVCASE, + TRADER, + WEATHER, + SEASONAL_EVENT, + LOST_ON_DEATH, + GIFTS } \ No newline at end of file diff --git a/Core/Models/Spt/Config/GiftsConfig.cs b/Core/Models/Spt/Config/GiftsConfig.cs index 31e09253..33cbb220 100644 --- a/Core/Models/Spt/Config/GiftsConfig.cs +++ b/Core/Models/Spt/Config/GiftsConfig.cs @@ -6,6 +6,15 @@ using Core.Models.Spt.Dialog; namespace Core.Models.Spt.Config; +public class GiftsConfig : BaseConfig +{ + [JsonPropertyName("kind")] + public string Kind { get; set; } = "spt-gifts"; + + [JsonPropertyName("gifts")] + public Dictionary Gifts { get; set; } +} + public class Gift { /// diff --git a/Core/Models/Spt/Config/MatchConfig.cs b/Core/Models/Spt/Config/MatchConfig.cs index 2a3a86d0..45a9ce58 100644 --- a/Core/Models/Spt/Config/MatchConfig.cs +++ b/Core/Models/Spt/Config/MatchConfig.cs @@ -2,7 +2,7 @@ namespace Core.Models.Spt.Config; -public class MatchConfig +public class MatchConfig : BaseConfig { [JsonPropertyName("kind")] public string Kind { get; set; } = "spt-match"; diff --git a/Core/Servers/ConfigServer.cs b/Core/Servers/ConfigServer.cs index 50813186..8c6503b2 100644 --- a/Core/Servers/ConfigServer.cs +++ b/Core/Servers/ConfigServer.cs @@ -1,8 +1,84 @@ -using Core.Annotations; +using System.Runtime.InteropServices.JavaScript; +using System.Text.Json; +using Core.Annotations; +using Core.Models.Enums; +using Core.Models.Spt.Config; +using ILogger = Core.Models.Utils.ILogger; namespace Core.Servers; [Injectable(InjectionType.Singleton)] public class ConfigServer { + private ILogger _logger; + protected Dictionary configs; + protected readonly string[] acceptableFileExtensions = ["json", "jsonc"]; + + public ConfigServer( + ILogger logger + // TODO: We need JsonUtil here => JsonUtil jsonUtil, + ) + { + _logger = logger; + Initialize(); + } + + public T GetConfig(ConfigTypes configType) where T : BaseConfig + { + if (!configs.ContainsKey(configType.GetValue())) + { + throw new Exception($"Config: {configType} is undefined. Ensure you have not broken it via editing"); + } + + return configs[configType.GetValue()] as T; + } + + public T GetConfigByString(string configType) where T : BaseConfig + { + return configs[configType] as T; + } + + public void Initialize() + { + _logger.Debug("Importing configs..."); + + // Get all filepaths + var filepath = "./assets/configs/"; + var files = Directory.GetFiles(filepath); + + // Add file content to result + foreach (var file in files) + { + if (acceptableFileExtensions.Contains(Path.GetExtension(file))) + { + var fileContent = File.ReadAllText(file); + var type = GetConfigTypeByFilename(file); + var deserializedContent = JsonSerializer.Deserialize(fileContent, type); + + if (deserializedContent == null) + { + _logger.Error($"Config file: {file} is corrupt. Use a site like: https://jsonlint.com to find the issue."); + throw new Exception($"Server will not run until the: {file} config error mentioned above is fixed"); + } + + this.configs[$"spt-{Path.GetFileNameWithoutExtension(file)}"] = deserializedContent; + } + } + + /** TODO: deal with this: + this.logger.info(`Commit hash: ${ + globalThis.G_COMMIT || "DEBUG" + }`); + this.logger.info(`Build date: ${ + globalThis.G_BUILDTIME || "DEBUG" + }`); + **/ + } + + private Type GetConfigTypeByFilename(string filename) + { + var type = Enum.GetValues() + .First(en => en.GetValue().Contains(Path.GetFileNameWithoutExtension(filename))); + return type.GetConfigType(); + } } \ No newline at end of file diff --git a/Core/Servers/DatabaseServer.cs b/Core/Servers/DatabaseServer.cs index ea6a69a4..2f7dab14 100644 --- a/Core/Servers/DatabaseServer.cs +++ b/Core/Servers/DatabaseServer.cs @@ -1,6 +1,12 @@ -namespace Core.Servers; +using Core.Models.Spt.Server; + +namespace Core.Servers; public class DatabaseServer { - + protected DatabaseTables tableData = new(); + + public DatabaseTables GetTables() => tableData; + + public void SetTables(DatabaseTables tables) => tableData = tables; } \ No newline at end of file diff --git a/Core/Servers/HttpServer.cs b/Core/Servers/HttpServer.cs index c4d9b749..1e468087 100644 --- a/Core/Servers/HttpServer.cs +++ b/Core/Servers/HttpServer.cs @@ -1,10 +1,11 @@ using System.Net.WebSockets; using Core.Context; -using Core.Models.Config; using Core.Servers.Http; using Core.Services; using Microsoft.Extensions.Primitives; using Core.Annotations; +using Core.Models.Enums; +using Core.Models.Spt.Config; using ILogger = Core.Models.Utils.ILogger; namespace Core.Servers; @@ -38,8 +39,7 @@ public class HttpServer _webSocketServer = webSocketServer; _httpListeners = httpListeners; - // TODO: hook up the config server to put the HttpConfig here - httpConfig = new HttpConfig() { Ip = "127.0.0.1", Port = 80, LogRequests = true}; + httpConfig = _configServer.GetConfig(ConfigTypes.HTTP); } public void Load(WebApplicationBuilder builder) @@ -89,13 +89,13 @@ public class HttpServer var isLocalRequest = IsLocalRequest(clientIp); if (isLocalRequest.HasValue) { if (isLocalRequest.Value) { - _logger.Info(_localisationService.GetText("client_request", req.url)); - } else { + _logger.Info(_localisationService.GetText("client_request", context.Request.Path.Value)); + } else + { _logger.Info( - _localisationService.GetText("client_request_ip", { - ip: clientIp, - url: req.url.replaceAll("/", "\\"), // Localisation service escapes `/` into hex code `/` - }), + _localisationService.GetText("client_request_ip", + new Dictionary + { { "ip", clientIp }, { "url", context.Request.Path.Value } }) ); } } diff --git a/Core/Services/I18nService.cs b/Core/Services/I18nService.cs index ee792fde..e9908423 100644 --- a/Core/Services/I18nService.cs +++ b/Core/Services/I18nService.cs @@ -4,7 +4,7 @@ namespace Core.Services; public class I18nService { - private string[] _locales; + private List _locales; private Dictionary _fallbacks; private string _defaultLocale; private string _directory; @@ -13,7 +13,7 @@ public class I18nService private Dictionary> _loadedLocales = new(); - public I18nService(string[] locales, Dictionary fallbacks, string defaultLocale, string directory) + public I18nService(List locales, Dictionary fallbacks, string defaultLocale, string directory) { _locales = locales; _fallbacks = fallbacks; @@ -30,9 +30,12 @@ public class I18nService throw new Exception($"Localisation files in directory {_directory} not found."); foreach (var file in files) { - _loadedLocales.Add(Path.GetFileName(file), + _loadedLocales.Add(Path.GetFileNameWithoutExtension(file), JsonSerializer.Deserialize>(File.ReadAllText(file)) ?? new Dictionary()); } + + if (!_loadedLocales.ContainsKey(_defaultLocale)) + throw new Exception($"The default locale '{_defaultLocale}' does not exist on the loaded locales."); } public void SetLocale(string locale) @@ -49,16 +52,28 @@ public class I18nService throw new Exception($"Locale '{locale}' was not defined, and the found fallback locale did not match any of the loaded locales."); _setLocale = foundFallbackLocale; } + + _setLocale = _defaultLocale; } } public string GetLocalised(string key) { - return null; + if (!_loadedLocales.TryGetValue(_setLocale, out var locales)) + return key; + if (!locales.TryGetValue(key, out var value)) + { + _loadedLocales.TryGetValue(_defaultLocale, out var defaults); + defaults.TryGetValue(key, out value); + return value ?? key; + } + + return value; } public string GetLocalised(string key, params object[] args) { - return null; + // TODO: Deal with arguments + return GetLocalised(key); } } \ No newline at end of file diff --git a/Core/Services/LocaleService.cs b/Core/Services/LocaleService.cs index ac343072..d91733cc 100644 --- a/Core/Services/LocaleService.cs +++ b/Core/Services/LocaleService.cs @@ -1,4 +1,7 @@ -using Core.Annotations; +using System.Globalization; +using Core.Annotations; +using Core.Models.Enums; +using Core.Models.Spt.Config; using Core.Servers; using ILogger = Core.Models.Utils.ILogger; @@ -7,6 +10,7 @@ namespace Core.Services; [Injectable(InjectionType.Singleton)] public class LocaleService { + private readonly LocaleConfig _localeConfig; private readonly ILogger _logger; private readonly DatabaseServer _databaseServer; private readonly ConfigServer _configServer; @@ -16,6 +20,7 @@ public class LocaleService _logger = logger; _databaseServer = databaseServer; _configServer = configServer; + _localeConfig = configServer.GetConfig(ConfigTypes.LOCALE); } /** @@ -24,18 +29,16 @@ public class LocaleService */ public Dictionary GetLocaleDb() { - /* - const desiredLocale = this.databaseServer.getTables().locales.global[this.getDesiredGameLocale()]; - if (desiredLocale) { - return desiredLocale; - } + var desiredLocale = _databaseServer.GetTables().locales.Global[GetDesiredGameLocale()]; + if (desiredLocale != null) + { + return desiredLocale; + } - this.logger.warning( - `Unable to find desired locale file using locale: ${this.getDesiredGameLocale()} from config/locale.json, falling back to 'en'`, - ); + _logger.Warning( + $"Unable to find desired locale file using locale: {GetDesiredGameLocale()} from config/locale.json, falling back to 'en'"); - return this.databaseServer.getTables().locales.global.en; - */ + return _databaseServer.GetTables().locales.Global["en"]; } /** @@ -45,13 +48,12 @@ public class LocaleService */ public string GetDesiredGameLocale() { - /* - if (this.localeConfig.gameLocale.toLowerCase() === "system") { - return this.getPlatformForClientLocale(); + if (_localeConfig.GameLocale.ToLower() == "system") + { + return GetPlatformForClientLocale(); } - return this.localeConfig.gameLocale.toLowerCase(); - */ + return _localeConfig.GameLocale.ToLower(); } /** @@ -61,22 +63,21 @@ public class LocaleService */ public string GetDesiredServerLocale() { - /* - if (this.localeConfig.serverLocale.toLowerCase() === "system") { - return this.getPlatformForServerLocale(); + if (_localeConfig.ServerLocale.ToLower() == "system") + { + return GetPlatformForServerLocale(); } - return this.localeConfig.serverLocale.toLowerCase(); - */ + return _localeConfig.ServerLocale.ToLower(); } /** * Get array of languages supported for localisation * @returns array of locales e.g. en/fr/cn */ - public ICollection GetServerSupportedLocales() + public List GetServerSupportedLocales() { - // return this.localeConfig.serverSupportedLocales; + return _localeConfig.ServerSupportedLocales; } /** @@ -85,7 +86,7 @@ public class LocaleService */ public Dictionary GetLocaleFallbacks() { - // return this.localeConfig.fallbacks; + return _localeConfig.Fallbacks; } /** @@ -94,20 +95,22 @@ public class LocaleService */ public string GetPlatformForServerLocale() { - /* - const platformLocale = this.getPlatformLocale(); - if (!platformLocale) { - this.logger.warning("System language could not be found, falling back to english"); - + var platformLocale = GetPlatformLocale(); + if (platformLocale == null) + { + _logger.Warning("System language could not be found, falling back to english"); return "en"; } - const baseNameCode = platformLocale.baseName.toLowerCase(); - if (!this.localeConfig.serverSupportedLocales.includes(baseNameCode)) { - // Chek if base language (e.g. CN / EN / DE) exists - const languageCode = platformLocale.language.toLocaleLowerCase(); - if (this.localeConfig.serverSupportedLocales.includes(languageCode)) { - if (baseNameCode === "zh") { + var baseNameCode = platformLocale.TwoLetterISOLanguageName.ToLower(); + if (!_localeConfig.ServerSupportedLocales.Contains(baseNameCode)) + { + // Check if base language (e.g. CN / EN / DE) exists + var languageCode = platformLocale.Name.ToLower(); + if (_localeConfig.ServerSupportedLocales.Contains(languageCode)) + { + if (baseNameCode == "zh") + { // Handle edge case of zh return "zh-cn"; } @@ -115,18 +118,18 @@ public class LocaleService return languageCode; } - if (baseNameCode === "pt") { + if (baseNameCode == "pt") + { // Handle edge case of pt return "pt-pt"; } - this.logger.warning(`Unsupported system language found: ${baseNameCode}, falling back to english`); + _logger.Warning($"Unsupported system language found: {baseNameCode}, falling back to english"); return "en"; } return baseNameCode; - */ } /** @@ -135,44 +138,49 @@ public class LocaleService */ protected string GetPlatformForClientLocale() { - /* - const platformLocale = this.getPlatformLocale(); - if (!platformLocale) { - this.logger.warning("System language could not be found, falling back to english"); + var platformLocale = GetPlatformLocale(); + if (platformLocale == null) + { + _logger.Warning("System language could not be found, falling back to english"); return "en"; } - const locales = this.databaseServer.getTables().locales; - const baseNameCode = platformLocale.baseName?.toLocaleLowerCase(); - if (baseNameCode && locales.global[baseNameCode]) { + var locales = _databaseServer.GetTables().locales; + var baseNameCode = platformLocale.TwoLetterISOLanguageName.ToLower(); + if (locales.Global.ContainsKey(baseNameCode)) + { return baseNameCode; } - const languageCode = platformLocale.language?.toLowerCase(); - if (languageCode && locales.global[languageCode]) { + var languageCode = platformLocale.Name.ToLower(); + if (locales.Global.ContainsKey(languageCode)) + { return languageCode; } + /* const regionCode = platformLocale.region?.toLocaleLowerCase(); if (regionCode && locales.global[regionCode]) { return regionCode; } + */ // BSG map DE to GE some reason - if (platformLocale.language === "de") { + if (baseNameCode == "de") + { return "ge"; } - this.logger.warning(`Unsupported system language found: ${languageCode}, falling back to english`); + _logger.Warning($"Unsupported system language found: {languageCode}, falling back to english"); return "en"; - */ } /** * This is in a function so we can overwrite it during testing * @returns The current platform locale - protected getPlatformLocale(): Intl.Locale { - return new Intl.Locale(Intl.DateTimeFormat().resolvedOptions().locale); - } */ + protected CultureInfo GetPlatformLocale() + { + return CultureInfo.InstalledUICulture; + } } \ No newline at end of file diff --git a/Core/Services/LocalisationService.cs b/Core/Services/LocalisationService.cs index cbf8dfef..1130e576 100644 --- a/Core/Services/LocalisationService.cs +++ b/Core/Services/LocalisationService.cs @@ -25,9 +25,13 @@ public class LocalisationService _randomUtil = randomUtil; _databaseServer = databaseServer; _localeService = localeService; - _i18nService = new I18nService(); - - File.ReadAllText() + _i18nService = new I18nService( + localeService.GetServerSupportedLocales(), + localeService.GetLocaleFallbacks(), + "en", + "./Assets/database/locales/server" + ); + _i18nService.SetLocale(localeService.GetDesiredServerLocale()); } public string GetText(string key, object? args = null) diff --git a/Core/Utils/JsonUtil.cs b/Core/Utils/JsonUtil.cs new file mode 100644 index 00000000..18312a98 --- /dev/null +++ b/Core/Utils/JsonUtil.cs @@ -0,0 +1,10 @@ +using Core.Annotations; + +namespace Core.Utils; + +[Injectable(InjectionType.Singleton)] +public class JsonUtil +{ + + +} \ No newline at end of file