diff --git a/Core/Models/Spt/Server/Locations.cs b/Core/Models/Spt/Server/Locations.cs index b3872983..bfd56b14 100644 --- a/Core/Models/Spt/Server/Locations.cs +++ b/Core/Models/Spt/Server/Locations.cs @@ -63,4 +63,22 @@ public class Locations /** Holds a mapping of the linkages between locations on the UI */ [JsonPropertyName("base")] public LocationsBase? Base { get; set; } + + public Eft.Common.Location? this[string key] + { + get + { + return (Eft.Common.Location?) GetType() + .GetProperties() + .First(p => p.Name.ToLower() == key.ToLower()).GetGetMethod()? + .Invoke(this, null) ?? null; + } + set + { + GetType() + .GetProperties() + .First(p => p.Name.ToLower() == key.ToLower()).GetSetMethod()? + .Invoke(this, [value]); + } + } } \ No newline at end of file diff --git a/Core/Models/Spt/Templates/Templates.cs b/Core/Models/Spt/Templates/Templates.cs index 10b15429..941ef11d 100644 --- a/Core/Models/Spt/Templates/Templates.cs +++ b/Core/Models/Spt/Templates/Templates.cs @@ -37,7 +37,7 @@ public class Templates /** Flea prices of items - gathered from online flea market dump */ [JsonPropertyName("prices")] - public Dictionary Prices { get; set; } + public Dictionary Prices { get; set; } /** Default equipment loadouts that show on main inventory screen */ [JsonPropertyName("defaultEquipmentPresets")] diff --git a/Core/Services/DatabaseService.cs b/Core/Services/DatabaseService.cs new file mode 100644 index 00000000..9bcd8841 --- /dev/null +++ b/Core/Services/DatabaseService.cs @@ -0,0 +1,366 @@ +using System.Diagnostics; +using Core.Annotations; +using Core.Models.Eft.Common; +using Core.Models.Eft.Common.Tables; +using Core.Models.Spt.Bots; +using Core.Models.Spt.Config; +using Core.Models.Spt.Server; +using Core.Models.Spt.Templates; +using Core.Servers; +using Core.Utils; +using Hideout = Core.Models.Spt.Hideout.Hideout; +using ILogger = Core.Models.Utils.ILogger; +using Locations = Core.Models.Spt.Server.Locations; + +namespace Core.Services; + +[Injectable(InjectionType.Singleton)] +public class DatabaseService +{ + protected LocationConfig locationConfig; + protected bool isDataValid; + + private readonly ILogger _logger; + private readonly DatabaseServer _databaseServer; + private readonly LocalisationService _localisationService; + private readonly HashUtil _hashUtil; + + public DatabaseService( + ILogger logger, + DatabaseServer databaseServer, + LocalisationService localisationService, + HashUtil hashUtil + ) + { + _logger = logger; + _databaseServer = databaseServer; + _localisationService = localisationService; + _hashUtil = hashUtil; + } + + /** + * @returns assets/database/ + */ + public DatabaseTables GetTables() + { + return _databaseServer.GetTables(); + } + + /** + * @returns assets/database/bots/ + */ + public Bots GetBots() + { + if (_databaseServer.GetTables().bots == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/bots")); + } + + return _databaseServer.GetTables().bots!; + } + + /** + * @returns assets/database/globals.json + */ + public Globals GetGlobals() + { + if (_databaseServer.GetTables().globals == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", + "assets/database/globals.json")); + } + + return _databaseServer.GetTables().globals!; + } + + /** + * @returns assets/database/hideout/ + */ + public Hideout GetHideout() + { + if (_databaseServer.GetTables().hideout == null) + { + throw new Exception( + _localisationService.GetText("database-data_at_path_missing", "assets/database/hideout")); + } + + return _databaseServer.GetTables().hideout!; + } + + /** + * @returns assets/database/locales/ + */ + public LocaleBase GetLocales() + { + if (_databaseServer.GetTables().locales == null) + { + throw new Exception( + _localisationService.GetText("database-data_at_path_missing", "assets/database/locales")); + } + + return _databaseServer.GetTables().locales!; + } + + /** + * @returns assets/database/locations + */ + public Locations GetLocations() + { + if (_databaseServer.GetTables().locations == null) + { + throw new Exception( + _localisationService.GetText("database-data_at_path_missing", "assets/database/locales")); + } + + return _databaseServer.GetTables().locations!; + } + + /** + * Get specific location by its Id + * @param locationId Desired location id + * @returns assets/database/locations/ + */ + public Location GetLocation(string locationId) + { + var locations = GetLocations(); + var desiredLocation = locations[locationId.ToLower()]; + if (desiredLocation == null) + { + throw new Exception(_localisationService.GetText("database-no_location_found_with_id", locationId)); + } + + return desiredLocation; + } + + /** + * @returns assets/database/match/ + */ + public Match GetMatch() + { + if (_databaseServer.GetTables().match == null) + { + throw new Exception( + _localisationService.GetText("database-data_at_path_missing", "assets/database/locales")); + } + + return _databaseServer.GetTables().match!; + } + + /** + * @returns assets/database/server.json + */ + public ServerBase GetServer() + { + if (_databaseServer.GetTables().server == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", + "assets/database/server.json")); + } + + return _databaseServer.GetTables().server!; + } + + /** + * @returns assets/database/settings.json + */ + public SettingsBase GetSettings() + { + if (_databaseServer.GetTables().settings == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", + "assets/database/settings.json")); + } + + return _databaseServer.GetTables().settings!; + } + + /** + * @returns assets/database/templates/ + */ + public Templates GetTemplates() + { + if (_databaseServer.GetTables().templates == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", + "assets/database/templates")); + } + + return _databaseServer.GetTables().templates!; + } + + /** + * @returns assets/database/templates/achievements.json + */ + public List GetAchievements() + { + if (_databaseServer.GetTables().templates?.Achievements == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", + "assets/database/templates/achievements.json")); + } + + return _databaseServer.GetTables().templates?.Achievements!; + } + + /** + * @returns assets/database/templates/customisation.json + */ + public Dictionary GetCustomization() + { + if (_databaseServer.GetTables().templates?.Customization == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", + "assets/database/templates/customization.json")); + } + + return _databaseServer.GetTables().templates?.Customization!; + } + + /** + * @returns assets/database/templates/handbook.json + */ + public HandbookBase GetHandbook() + { + if (_databaseServer.GetTables().templates?.Handbook == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/templates/handbook.json")); + } + + return _databaseServer.GetTables().templates?.Handbook!; + } + + /** + * @returns assets/database/templates/items.json + */ + public Dictionary GetItems() { + if (_databaseServer.GetTables().templates?.Items == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/templates/items.json")); + } + + return _databaseServer.GetTables().templates?.Items!; + } + + /** + * @returns assets/database/templates/prices.json + */ + public Dictionary GetPrices() { + if (_databaseServer.GetTables().templates?.Prices == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/templates/prices.json")); + } + + return _databaseServer.GetTables().templates?.Prices!; + } + + /** + * @returns assets/database/templates/profiles.json + */ + public ProfileTemplates GetProfiles() + { + if (_databaseServer.GetTables().templates?.Profiles == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/templates/profiles.json")); + } + + return _databaseServer.GetTables().templates?.Profiles!; + } + + /** + * @returns assets/database/templates/quests.json + */ + public Dictionary GetQuests() { + if (_databaseServer.GetTables().templates?.Quests == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/templates/quests.json")); + } + + return _databaseServer.GetTables().templates?.Quests!; + } + + /** + * @returns assets/database/traders/ + */ + public Dictionary GetTraders() { + if (_databaseServer.GetTables().traders == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/traders")); + } + + return _databaseServer.GetTables().traders!; + } + + /** + * Get specific trader by their Id + * @param traderId Desired trader id + * @returns assets/database/traders/ + */ + public Trader GetTrader(string traderId) + { + var traders = GetTraders(); + if (!traders.TryGetValue(traderId, out var desiredTrader)) + { + throw new Exception(_localisationService.GetText("database-no_trader_found_with_id", traderId)); + } + + return desiredTrader; + } + + /** + * @returns assets/database/locationServices/ + */ + public LocationServices GetLocationServices() { + if (_databaseServer.GetTables().templates?.LocationServices == null) + { + throw new Exception(_localisationService.GetText("database-data_at_path_missing", "assets/database/locationServices.json")); + } + + return _databaseServer.GetTables().templates?.LocationServices!; + } + + /** + * Validates that the database doesn't contain invalid ID data + */ + public void ValidateDatabase() { + var start = Stopwatch.StartNew(); + + isDataValid = + ValidateTable(GetQuests(), "quest") && + ValidateTable(GetTraders(), "trader") && + ValidateTable(GetItems(), "item") && + ValidateTable(GetCustomization(), "customization"); + + if (!isDataValid) + { + _logger.Error(_localisationService.GetText("database-invalid_data")); + } + + start.Stop(); + _logger.Debug($"ID validation took: {start.ElapsedMilliseconds}ms"); + } + + /** + * Validate that the given table only contains valid MongoIDs + * @param table Table to validate for MongoIDs + * @param tableType The type of table, used in output message + * @returns True if the table only contains valid data + */ + private bool ValidateTable(Dictionary table, string tableType) { + foreach (var keyValuePair in table) + { + if (!_hashUtil.IsValidMongoId(keyValuePair.Key)) + { + _logger.Error($"Invalid {tableType} ID: '{keyValuePair.Key}'"); + return false; + } + } + + return true; + } + + /** + * Check if the database is valid + * @returns True if the database contains valid data, false otherwise + */ + public bool IsDatabaseValid() => isDataValid; +} \ No newline at end of file diff --git a/Core/Utils/App.cs b/Core/Utils/App.cs new file mode 100644 index 00000000..8c73b614 --- /dev/null +++ b/Core/Utils/App.cs @@ -0,0 +1,49 @@ +using Core.Annotations; +using Core.Models.Enums; +using Core.Models.Spt.Config; +using Core.Servers; +using Core.Services; +using ILogger = Core.Models.Utils.ILogger; + +namespace Core.Utils; + +[Injectable(InjectionType.Singleton)] +public class App +{ + protected Dictionary _onUpdateLastRun; + protected CoreConfig _coreConfig; + + private ILogger _logger; + private TimeUtil _timeUtil; + private LocalisationService _localisationService; + private ConfigServer _configServer; + private EncodingUtil _encodingUtil; + private HttpServer _httpServer; + private DatabaseService _databaseService; + private IEnumerable _onLoad; + private IEnumerable _onUpdate; + + public App( + ILogger logger, + TimeUtil timeUtil, + LocalisationService localisationService, + ConfigServer configServer, + EncodingUtil encodingUtil, + HttpServer httpServer, + DatabaseService databaseService, + IEnumerable onLoadComponents, + IEnumerable onUpdateComponents + ) { + _logger = logger; + _timeUtil = timeUtil; + _localisationService = localisationService; + _configServer = configServer; + _encodingUtil = encodingUtil; + _httpServer = httpServer; + _databaseService = databaseService; + _onLoad = onLoadComponents; + _onUpdate = onUpdateComponents; + + _coreConfig = configServer.GetConfig(ConfigTypes.CORE); + } +} \ No newline at end of file diff --git a/Core/Utils/EncodingUtil.cs b/Core/Utils/EncodingUtil.cs new file mode 100644 index 00000000..e023ddc9 --- /dev/null +++ b/Core/Utils/EncodingUtil.cs @@ -0,0 +1,64 @@ +using System.Text; +using Core.Annotations; + +namespace Core.Utils; + +[Injectable(InjectionType.Singleton)] +public class EncodingUtil +{ + public string Encode(string value, EncodeType encode) + { + return encode switch + { + EncodeType.BASE64 => Convert.ToBase64String(Encoding.Default.GetBytes(value)), + EncodeType.HEX => Convert.ToHexString(Encoding.Default.GetBytes(value)), + EncodeType.ASCII => Encoding.ASCII.GetString(Encoding.Default.GetBytes(value)), + EncodeType.UTF8 => Encoding.UTF8.GetString(Encoding.Default.GetBytes(value)), + _ => throw new ArgumentOutOfRangeException(nameof(encode), encode, null) + }; + } + + public string Decode(string value, EncodeType encode) + { + switch (encode) + { + case EncodeType.BASE64: + return Encoding.UTF8.GetString(Convert.FromBase64String(value)); + case EncodeType.HEX: + return Encoding.UTF8.GetString(Convert.FromHexString(value)); + case EncodeType.ASCII: + return Encoding.ASCII.GetString(Encoding.Default.GetBytes(value)); + case EncodeType.UTF8: + return Encoding.UTF8.GetString(Encoding.Default.GetBytes(value)); + default: + throw new ArgumentOutOfRangeException(nameof(encode), encode, null); + } + } + + public string FromBase64(string value) + { + return Decode(value, EncodeType.BASE64); + } + + public string ToBase64(string value) + { + return Encode(value, EncodeType.BASE64); + } + + public string FromHex(string value) + { + return Decode(value, EncodeType.HEX); + } + + public string ToHex(string value) + { + return Encode(value, EncodeType.HEX); + } +} + +public enum EncodeType { + BASE64, + HEX, + ASCII, + UTF8 +} \ No newline at end of file diff --git a/Core/Utils/HashUtil.cs b/Core/Utils/HashUtil.cs index b1a3318c..23de8a29 100644 --- a/Core/Utils/HashUtil.cs +++ b/Core/Utils/HashUtil.cs @@ -6,13 +6,15 @@ using Core.Annotations; namespace Core.Utils; [Injectable(InjectionType.Singleton)] -public partial class HashUtil +public class HashUtil { + private readonly Regex MongoIdRegex = new Regex("^[a-fA-F0-9]{24}$"); + /// /// Create a 24 character id using the sha256 algorithm + current timestamp /// /// 24 character hash - public static string Generate() + public string Generate() { throw new NotImplementedException(); } @@ -22,22 +24,22 @@ public partial class HashUtil /// /// String to check /// True when string is a valid mongo id - public static bool IsValidMongoId(string stringToCheck) + public bool IsValidMongoId(string stringToCheck) { - return MongoIdRegex().IsMatch(stringToCheck); + return MongoIdRegex.IsMatch(stringToCheck); } - public static string GenerateMd5ForData(string data) + public string GenerateMd5ForData(string data) { return GenerateHashForData(HashingAlgorithm.MD5, data); } - public static string GenerateSha1ForData(string data) + public string GenerateSha1ForData(string data) { return GenerateHashForData(HashingAlgorithm.SHA1, data); } - public static string GenerateCrc32ForData(string data) + public string GenerateCrc32ForData(string data) { // TODO: Could not find a ms way of doing this. // May need a custom impl to avoid an external lib. - CJ @@ -51,7 +53,7 @@ public partial class HashUtil /// data to be hashed /// hash value /// thrown if the provided algorithm is not implemented> - public static string GenerateHashForData(HashingAlgorithm algorithm, string data) + public string GenerateHashForData(HashingAlgorithm algorithm, string data) { switch (algorithm) { @@ -71,7 +73,7 @@ public partial class HashUtil /// Generates an account ID for a profile /// /// Generated account ID - public static int GenerateAccountId() + public int GenerateAccountId() { const int min = 1000000; const int max = 1999999; @@ -80,9 +82,6 @@ public partial class HashUtil return random.Next() * (max - min + 1) + min; } - - [GeneratedRegex("^[a-fA-F0-9]{24}$", RegexOptions.IgnoreCase, "en")] - private static partial Regex MongoIdRegex(); } public enum HashingAlgorithm