From 966562bc0810a5d2f6a8adbf48ad6d1cab967006 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 10 Jan 2025 13:49:28 +0000 Subject: [PATCH] more work --- Core/Callbacks/LauncherCallbacks.cs | 20 +- Core/Controllers/LauncherController.cs | 4 +- Core/DI/Router.cs | 40 ++- Core/Models/Common/MinMax.cs | 4 +- Core/Models/Eft/Common/Tables/Item.cs | 5 +- Core/Models/Eft/Launcher/LoginRequestData.cs | 5 +- Core/Models/Spt/Config/AirdropConfig.cs | 5 +- Core/Models/Spt/Config/BotConfig.cs | 34 ++- Core/Models/Spt/Config/BotDurability.cs | 31 ++- Core/Models/Spt/Config/GiftsConfig.cs | 6 +- Core/Models/Spt/Config/InventoryConfig.cs | 5 +- Core/Models/Spt/Config/LocationConfig.cs | 5 +- Core/Models/Spt/Config/LostOnDeathConfig.cs | 8 +- Core/Models/Spt/Config/MatchConfig.cs | 5 +- Core/Models/Spt/Config/PlayerScavConfig.cs | 5 +- Core/Models/Spt/Config/PmcConfig.cs | 11 +- Core/Models/Spt/Config/QuestConfig.cs | 18 +- Core/Models/Spt/Config/RagfairConfig.cs | 18 +- Core/Models/Spt/Config/RepairConfig.cs | 12 +- Core/Models/Spt/Config/SeasonalEventConfig.cs | 11 +- Core/Models/Spt/Config/TraderConfig.cs | 8 +- Core/Models/Spt/Dialog/SendMessageDetails.cs | 5 +- Core/Routers/Dynamic/HttpDynamicRouter.cs | 9 +- Core/Routers/HttpRouter.cs | 7 +- Core/Routers/Static/LauncherStaticRouter.cs | 107 +++---- Core/Servers/ConfigServer.cs | 14 +- Core/Servers/Http/SptHttpListener.cs | 10 +- Core/Servers/SaveServer.cs | 261 ++++++++++++++++++ Core/Services/I18nService.cs | 10 +- Core/Services/LocalisationService.cs | 6 +- Core/Utils/FileUtil.cs | 38 +++ Core/Utils/HttpResponseUtil.cs | 8 +- Core/Utils/ImporterUtil.cs | 15 +- .../Utils/Json/Converters/EftEnumConverter.cs | 40 +++ .../StringToNumberFactoryConverter.cs | 3 +- Core/Utils/JsonUtil.cs | 43 ++- Core/Utils/Watermark.cs | 188 +++++++++++++ Server/Program.cs | 5 +- 38 files changed, 880 insertions(+), 149 deletions(-) create mode 100644 Core/Servers/SaveServer.cs create mode 100644 Core/Utils/Json/Converters/EftEnumConverter.cs create mode 100644 Core/Utils/Watermark.cs diff --git a/Core/Callbacks/LauncherCallbacks.cs b/Core/Callbacks/LauncherCallbacks.cs index d672d167..a616dd84 100644 --- a/Core/Callbacks/LauncherCallbacks.cs +++ b/Core/Callbacks/LauncherCallbacks.cs @@ -1,12 +1,26 @@ -using Core.Models.Eft.Common; +using Core.Annotations; +using Core.Controllers; +using Core.Models.Eft.Common; using Core.Models.Eft.Launcher; +using Core.Servers; +using Core.Utils; namespace Core.Callbacks; +[Injectable] public class LauncherCallbacks { - public LauncherCallbacks() + protected HttpResponseUtil _httpResponseUtil; + protected LauncherController _launcherController; + protected SaveServer _saveServer; + protected Watermark _watermark; + public LauncherCallbacks( + HttpResponseUtil httpResponse, + LauncherController launcherController, + SaveServer saveServer, + Watermark watermark) { + } public string Connect() @@ -73,4 +87,4 @@ public class LauncherCallbacks { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/Core/Controllers/LauncherController.cs b/Core/Controllers/LauncherController.cs index 50f2aab5..787783cc 100644 --- a/Core/Controllers/LauncherController.cs +++ b/Core/Controllers/LauncherController.cs @@ -1,9 +1,11 @@ +using Core.Annotations; using Core.Models.Eft.Launcher; using Core.Models.Eft.Profile; using Core.Models.Spt.Mod; namespace Core.Controllers; +[Injectable] public class LauncherController { /// @@ -144,4 +146,4 @@ public class LauncherController { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/Core/DI/Router.cs b/Core/DI/Router.cs index 68756a31..1865fdf8 100644 --- a/Core/DI/Router.cs +++ b/Core/DI/Router.cs @@ -1,7 +1,9 @@ +using System.Text.Json; using Core.Models.Eft.Common; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Profile; using Core.Models.Utils; +using Core.Utils; namespace Core.DI; @@ -39,22 +41,27 @@ public abstract class Router .Where((r) => !r.dynamic) .Any((r) => r.route == url); } - - public abstract Type? GetBodyDeserializationType(); } public abstract class StaticRouter : Router { - private List> actions; + private List actions; + private JsonUtil _jsonUtil; - public StaticRouter(List> routes) : base() + public StaticRouter(JsonUtil jsonUtil, List routes) : base() { actions = routes; + _jsonUtil = jsonUtil; } - public object HandleStatic(string url, IRequestData? info, string sessionID, string output) + public object HandleStatic(string url, string? body, string sessionID, string output) { - return actions.Single(route => route.url == url).action(url, info, sessionID, output); + var action = actions.Single(route => route.url == url); + var type = action.bodyType; + IRequestData? info = null; + if (type != null && !string.IsNullOrEmpty(body)) + info = (IRequestData?) _jsonUtil.Deserialize(body, type); + return action.action(url, info, sessionID, output); } protected override List GetHandledRoutes() @@ -65,16 +72,23 @@ public abstract class StaticRouter : Router public abstract class DynamicRouter : Router { - private List> actions; + private List actions; + private JsonUtil _jsonUtil; - public DynamicRouter(List> routes) : base() + public DynamicRouter(JsonUtil jsonUtil, List routes) : base() { actions = routes; + _jsonUtil = jsonUtil; } - public object HandleDynamic(string url, IRequestData? info, string sessionID, string output) + public object HandleDynamic(string url, string? body, string sessionID, string output) { - return actions.First(r => url.Contains(r.url)).action(url, info, sessionID, output); + var action = actions.First(r => url.Contains(r.url)); + var type = action.bodyType; + IRequestData? info = null; + if (type != null && !string.IsNullOrEmpty(body)) + info = (IRequestData?) _jsonUtil.Deserialize(body, type); + return action.action(url, info, sessionID, output); } protected override List GetHandledRoutes() @@ -103,5 +117,9 @@ public abstract class SaveLoadRouter : Router public record HandledRoute(string route, bool dynamic); -public record RouteAction(string url, Func action) where T : IRequestData; +public record RouteAction( + string url, + Func action, + Type? bodyType = null +); //public action: (url: string, info: any, sessionID: string, output: string) => Promise, diff --git a/Core/Models/Common/MinMax.cs b/Core/Models/Common/MinMax.cs index c229d2f7..5aa35d40 100644 --- a/Core/Models/Common/MinMax.cs +++ b/Core/Models/Common/MinMax.cs @@ -4,9 +4,11 @@ namespace Core.Models.Common; public class MinMax { + [JsonPropertyName("type")] + public string? Type { get; set; } [JsonPropertyName("max")] public double? Max { get; set; } [JsonPropertyName("min")] public double? Min { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Common/Tables/Item.cs b/Core/Models/Eft/Common/Tables/Item.cs index 954e7bd5..e018f0ad 100644 --- a/Core/Models/Eft/Common/Tables/Item.cs +++ b/Core/Models/Eft/Common/Tables/Item.cs @@ -18,6 +18,9 @@ public class Item [JsonPropertyName("location")] public object? Location { get; set; } // TODO: Can be IItemLocation or number + + [JsonPropertyName("desc")] + public string? Desc { get; set; } [JsonPropertyName("upd")] public Upd? Update { get; set; } @@ -254,4 +257,4 @@ public class UpdCultistAmulet { [JsonPropertyName("NumberOfUsages")] public double? NumberOfUsages { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Eft/Launcher/LoginRequestData.cs b/Core/Models/Eft/Launcher/LoginRequestData.cs index 5545bfc4..a17c130f 100644 --- a/Core/Models/Eft/Launcher/LoginRequestData.cs +++ b/Core/Models/Eft/Launcher/LoginRequestData.cs @@ -1,12 +1,13 @@ using System.Text.Json.Serialization; +using Core.Models.Utils; namespace Core.Models.Eft.Launcher; -public class LoginRequestData +public class LoginRequestData : IRequestData { [JsonPropertyName("username")] public string? Username { get; set; } [JsonPropertyName("password")] public string? Password { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/AirdropConfig.cs b/Core/Models/Spt/Config/AirdropConfig.cs index dff76614..60e2ff93 100644 --- a/Core/Models/Spt/Config/AirdropConfig.cs +++ b/Core/Models/Spt/Config/AirdropConfig.cs @@ -58,7 +58,8 @@ public class AirdropChancePercent /// public class AirdropLoot { - [JsonPropertyName("Icon")] + [JsonPropertyName("icon")] + [JsonConverter(typeof(JsonStringEnumConverter))] public AirdropTypeEnum Icon { get; set; } /// @@ -126,4 +127,4 @@ public class AirdropLoot [JsonPropertyName("forcedLoot")] public Dictionary? ForcedLoot { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/BotConfig.cs b/Core/Models/Spt/Config/BotConfig.cs index 905056f9..5ca80ef9 100644 --- a/Core/Models/Spt/Config/BotConfig.cs +++ b/Core/Models/Spt/Config/BotConfig.cs @@ -138,6 +138,15 @@ public class PresetBatch [JsonPropertyName("bossKnight")] public int BossKnight { get; set; } + [JsonPropertyName("bossZryachiy")] + public int BossZryachiy { get; set; } + + [JsonPropertyName("bossKolontay")] + public int BossKolontay { get; set; } + + [JsonPropertyName("bossPartisan")] + public int BossPartisan { get; set; } + [JsonPropertyName("bossTest")] public int BossTest { get; set; } @@ -180,6 +189,21 @@ public class PresetBatch [JsonPropertyName("followerBoar")] public int FollowerBoar { get; set; } + [JsonPropertyName("followerBoarClose1")] + public int FollowerBoarClose1 { get; set; } + + [JsonPropertyName("followerBoarClose2")] + public int FollowerBoarClose2 { get; set; } + + [JsonPropertyName("followerZryachiy")] + public int FollowerZryachiy { get; set; } + + [JsonPropertyName("followerKolontayAssault")] + public int FollowerKolontayAssault { get; set; } + + [JsonPropertyName("followerKolontaySecurity")] + public int FollowerKolontaySecurity { get; set; } + [JsonPropertyName("marksman")] public int Marksman { get; set; } @@ -221,6 +245,12 @@ public class PresetBatch [JsonPropertyName("pmcBEAR")] public int PmcBEAR { get; set; } + + [JsonPropertyName("shooterBTR")] + public int ShooterBTR { get; set; } + + [JsonExtensionData] + public IDictionary AdditionalData { get; set; } } public class WalletLootSettings @@ -288,7 +318,7 @@ public class EquipmentFilters /// /// Chance NODS are down/active during the day /// - [JsonPropertyName("NvgIsActiveChanceDayPercent")] + [JsonPropertyName("nvgIsActiveChanceDayPercent")] public float? NvgIsActiveChanceDayPercent { get; set; } /// @@ -522,4 +552,4 @@ public class RandomisedResourceValues /// [JsonPropertyName("chanceMaxResourcePercent")] public float ChanceMaxResourcePercent { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/BotDurability.cs b/Core/Models/Spt/Config/BotDurability.cs index c4cd51f7..b45fa1d6 100644 --- a/Core/Models/Spt/Config/BotDurability.cs +++ b/Core/Models/Spt/Config/BotDurability.cs @@ -11,43 +11,43 @@ public class BotDurability public PmcDurability Pmc { get; set; } [JsonPropertyName("boss")] - public BotDurability Boss { get; set; } + public PmcDurability Boss { get; set; } [JsonPropertyName("follower")] - public BotDurability Follower { get; set; } + public PmcDurability Follower { get; set; } [JsonPropertyName("assault")] - public BotDurability Assault { get; set; } + public PmcDurability Assault { get; set; } [JsonPropertyName("cursedassault")] - public BotDurability CursedAssault { get; set; } + public PmcDurability CursedAssault { get; set; } [JsonPropertyName("marksman")] - public BotDurability Marksman { get; set; } + public PmcDurability Marksman { get; set; } [JsonPropertyName("pmcbot")] - public BotDurability PmcBot { get; set; } + public PmcDurability PmcBot { get; set; } [JsonPropertyName("arenafighterevent")] - public BotDurability ArenaFighterEvent { get; set; } + public PmcDurability ArenaFighterEvent { get; set; } [JsonPropertyName("arenafighter")] - public BotDurability ArenaFighter { get; set; } + public PmcDurability ArenaFighter { get; set; } [JsonPropertyName("crazyassaultevent")] - public BotDurability CrazyAssaultEvent { get; set; } + public PmcDurability CrazyAssaultEvent { get; set; } [JsonPropertyName("exusec")] - public BotDurability Exusec { get; set; } + public PmcDurability Exusec { get; set; } [JsonPropertyName("gifter")] - public BotDurability Gifter { get; set; } + public PmcDurability Gifter { get; set; } [JsonPropertyName("sectantpriest")] - public BotDurability SectantPriest { get; set; } + public PmcDurability SectantPriest { get; set; } [JsonPropertyName("sectantwarrior")] - public BotDurability SectantWarrior { get; set; } + public PmcDurability SectantWarrior { get; set; } } /** Durability values to be used when a more specific bot type can't be found */ @@ -82,6 +82,9 @@ public class PmcDurabilityArmor [JsonPropertyName("minDelta")] public double MinDelta { get; set; } + + [JsonPropertyName("minLimitPercent")] + public double MinLimitPercent { get; set; } } public class ArmorDurability @@ -112,4 +115,4 @@ public class WeaponDurability [JsonPropertyName("minLimitPercent")] public double MinLimitPercent { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/GiftsConfig.cs b/Core/Models/Spt/Config/GiftsConfig.cs index 61e6e6ee..ceeebed8 100644 --- a/Core/Models/Spt/Config/GiftsConfig.cs +++ b/Core/Models/Spt/Config/GiftsConfig.cs @@ -73,4 +73,8 @@ public class Gift [JsonPropertyName("maxToSendPlayer")] public int? MaxToSendPlayer { get; set; } -} \ No newline at end of file + + + [JsonPropertyName("maxToSendToPlayer")] + public int? MaxToSendToPlayer { get; set; } +} diff --git a/Core/Models/Spt/Config/InventoryConfig.cs b/Core/Models/Spt/Config/InventoryConfig.cs index 1e16431d..66d747d5 100644 --- a/Core/Models/Spt/Config/InventoryConfig.cs +++ b/Core/Models/Spt/Config/InventoryConfig.cs @@ -33,6 +33,9 @@ public class InventoryConfig : BaseConfig public class RewardDetails { + [JsonPropertyName("_type")] + public string? Type { get; set; } + [JsonPropertyName("rewardCount")] public int RewardCount { get; set; } @@ -69,4 +72,4 @@ public class SealedAirdropContainerSettings [JsonPropertyName("allowBossItems")] public bool AllowBossItems { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/LocationConfig.cs b/Core/Models/Spt/Config/LocationConfig.cs index 4ea124c4..35576369 100644 --- a/Core/Models/Spt/Config/LocationConfig.cs +++ b/Core/Models/Spt/Config/LocationConfig.cs @@ -254,6 +254,9 @@ public class LootMultiplier [JsonPropertyName("sandbox")] public double Sandbox { get; set; } + + [JsonPropertyName("sandbox_high")] + public double SandboxHigh { get; set; } } public class ContainerRandomisationSettings @@ -316,4 +319,4 @@ public class ScavRaidTimeLocationSettings /** Should bot waves be removed / spawn times be adjusted */ [JsonPropertyName("adjustWaves")] public bool AdjustWaves { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/LostOnDeathConfig.cs b/Core/Models/Spt/Config/LostOnDeathConfig.cs index 19176c31..972fec44 100644 --- a/Core/Models/Spt/Config/LostOnDeathConfig.cs +++ b/Core/Models/Spt/Config/LostOnDeathConfig.cs @@ -60,4 +60,10 @@ public class LostEquipment [JsonPropertyName("Scabbard")] public bool Scabbard { get; set; } -} \ No newline at end of file + + [JsonPropertyName("Compass")] + public bool Compass { get; set; } + + [JsonPropertyName("SecuredContainer")] + public bool SecuredContainer { get; set; } +} diff --git a/Core/Models/Spt/Config/MatchConfig.cs b/Core/Models/Spt/Config/MatchConfig.cs index 46807fe3..7bca1837 100644 --- a/Core/Models/Spt/Config/MatchConfig.cs +++ b/Core/Models/Spt/Config/MatchConfig.cs @@ -9,4 +9,7 @@ public class MatchConfig : BaseConfig [JsonPropertyName("enabled")] public bool Enabled { get; set; } -} \ No newline at end of file + + [JsonPropertyName("randomiseMapContainers")] + public Dictionary RandomiseMapContainers { get; set; } +} diff --git a/Core/Models/Spt/Config/PlayerScavConfig.cs b/Core/Models/Spt/Config/PlayerScavConfig.cs index bc52102e..370e1329 100644 --- a/Core/Models/Spt/Config/PlayerScavConfig.cs +++ b/Core/Models/Spt/Config/PlayerScavConfig.cs @@ -26,6 +26,9 @@ public class KarmaLevel [JsonPropertyName("equipmentBlacklist")] public Dictionary EquipmentBlacklist { get; set; } + [JsonPropertyName("labsAccessCardChancePercent")] + public double? LabsAccessCardChancePercent { get; set; } + [JsonPropertyName("lootItemsToAddChancePercent")] public Dictionary LootItemsToAddChancePercent { get; set; } } @@ -58,4 +61,4 @@ public class ItemLimits [JsonPropertyName("grenades")] public GenerationData Grenades { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/PmcConfig.cs b/Core/Models/Spt/Config/PmcConfig.cs index a7afabd0..e2ab433b 100644 --- a/Core/Models/Spt/Config/PmcConfig.cs +++ b/Core/Models/Spt/Config/PmcConfig.cs @@ -53,6 +53,9 @@ public class PmcConfig : BaseConfig [JsonPropertyName("looseWeaponInBackpackLootMinMax")] public MinMax LooseWeaponInBackpackLootMinMax { get; set; } + [JsonPropertyName("_isUsec")] + public string? IsUsecDescription { get; set; } + /** Percentage chance PMC will be USEC */ [JsonPropertyName("isUsec")] public double IsUsec { get; set; } @@ -65,6 +68,9 @@ public class PmcConfig : BaseConfig [JsonPropertyName("bearType")] public string BearType { get; set; } + [JsonPropertyName("_pmcType")] + public string? PmcTypeDescription { get; set; } + /** What 'brain' does a PMC use, keyed by map and side (USEC/BEAR) key: map location, value: type for usec/bear */ [JsonPropertyName("pmcType")] public Dictionary>> PmcType { get; set; } @@ -106,6 +112,9 @@ public class PmcConfig : BaseConfig /** Should secure container loot from usec.json/bear.json be added to pmc bots secure */ [JsonPropertyName("addSecureContainerLootFromBotConfig")] public bool AddSecureContainerLootFromBotConfig { get; set; } + + [JsonPropertyName("addPrefixToSameNamePMCAsPlayerChance")] + public int? AddPrefixToSameNamePMCAsPlayerChance { get; set; } } public class HostilitySettings @@ -159,4 +168,4 @@ public class IMinMaxLootValue : MinMax { [JsonPropertyName("value")] public double Value { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/QuestConfig.cs b/Core/Models/Spt/Config/QuestConfig.cs index c7830a02..7994515c 100644 --- a/Core/Models/Spt/Config/QuestConfig.cs +++ b/Core/Models/Spt/Config/QuestConfig.cs @@ -56,14 +56,17 @@ public class PlayerTypeQuestIds public class QuestTypeIds { - [JsonPropertyName("Elimination")] + [JsonPropertyName("elimination")] public string Elimination { get; set; } - [JsonPropertyName("Completion")] + [JsonPropertyName("completion")] public string Completion { get; set; } - [JsonPropertyName("Exploration")] + [JsonPropertyName("exploration")] public string Exploration { get; set; } + + [JsonPropertyName("pickup")] + public string Pickup { get; set; } } public class EventQuestData @@ -173,6 +176,8 @@ public class RewardScaling public class TraderWhitelist { + [JsonPropertyName("name")] + public string Name { get; set; } [JsonPropertyName("traderId")] public string TraderId { get; set; } @@ -253,6 +258,11 @@ public class Pickup : BaseQuestConfig { [JsonPropertyName("ItemTypeToFetchWithMaxCount")] public List ItemTypeToFetchWithMaxCount { get; set; } + + public List ItemTypesToFetch { get; set; } + + [JsonPropertyName("maxItemFetchCount")] + public int? MaxItemFetchCount { get; set; } } public class PickupTypeWithMaxCount @@ -370,4 +380,4 @@ public class ProbabilityObject [JsonPropertyName("data")] public object Data { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/RagfairConfig.cs b/Core/Models/Spt/Config/RagfairConfig.cs index 49498e51..79034349 100644 --- a/Core/Models/Spt/Config/RagfairConfig.cs +++ b/Core/Models/Spt/Config/RagfairConfig.cs @@ -116,7 +116,7 @@ public class Dynamic [JsonPropertyName("condition")] /** Settings to control the durability range of item items listed on flea */ - public Condition Condition { get; set; } + public Dictionary Condition { get; set; } [JsonPropertyName("stackablePercent")] /** Size stackable items should be listed for in percent of max stack size */ @@ -138,6 +138,9 @@ public class Dynamic /** A multipler to apply to individual tpls price just prior to item quality adjustment */ public Dictionary ItemPriceMultiplier { get; set; } + [JsonPropertyName("_currencies")] + public string? CurrenciesDescription { get; set; } + [JsonPropertyName("currencies")] /** Percentages to sell offers in each currency */ public Dictionary Currencies { get; set; } @@ -265,6 +268,9 @@ public class Condition [JsonPropertyName("max")] public MinMax Max { get; set; } + + [JsonPropertyName("_name")] + public string Name { get; set; } } public class RagfairBlacklist @@ -352,6 +358,9 @@ public class UnreasonableModPrices /// [JsonPropertyName("newPriceHandbookMultiplier")] public int NewPriceHandbookMultiplier { get; set; } + + [JsonPropertyName("itemType")] + public string ItemType { get; set; } } public class ArmorSettings @@ -386,8 +395,9 @@ public class TieredFlea [JsonPropertyName("unlocksType")] public Dictionary UnlocksType { get; set; } - public bool AmmoTiersEnabled { get; set; } - [JsonPropertyName("ammoTplUnlocks")] public Dictionary AmmoTplUnlocks { get; set; } -} \ No newline at end of file + + [JsonPropertyName("ammoTiersEnabled")] + public bool AmmoTiersEnabled { get; set; } +} diff --git a/Core/Models/Spt/Config/RepairConfig.cs b/Core/Models/Spt/Config/RepairConfig.cs index a585e591..145ad5be 100644 --- a/Core/Models/Spt/Config/RepairConfig.cs +++ b/Core/Models/Spt/Config/RepairConfig.cs @@ -82,6 +82,12 @@ public class RepairKit [JsonPropertyName("weapon")] public BonusSettings Weapon { get; set; } + + [JsonPropertyName("vest")] + public BonusSettings Vest { get; set; } + + [JsonPropertyName("headwear")] + public BonusSettings Headwear { get; set; } } public class BonusSettings @@ -92,10 +98,10 @@ public class BonusSettings [JsonPropertyName("bonusTypeWeight")] public Dictionary BonusTypeWeight { get; set; } - [JsonPropertyName("common")] + [JsonPropertyName("Common")] public Dictionary Common { get; set; } - [JsonPropertyName("rare")] + [JsonPropertyName("Rare")] public Dictionary Rare { get; set; } } @@ -107,4 +113,4 @@ public class BonusValues /** What dura is buff active between (min max of current max) */ [JsonPropertyName("activeDurabilityPercentMinMax")] public MinMax ActiveDurabilityPercentMinMax { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/SeasonalEventConfig.cs b/Core/Models/Spt/Config/SeasonalEventConfig.cs index 81eb8aa7..8a117b3e 100644 --- a/Core/Models/Spt/Config/SeasonalEventConfig.cs +++ b/Core/Models/Spt/Config/SeasonalEventConfig.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using Core.Models.Eft.Common; using Core.Models.Enums; +using Core.Utils.Json.Converters; namespace Core.Models.Spt.Config; @@ -20,6 +21,7 @@ public class SeasonalEventConfig : BaseConfig [JsonPropertyName("eventLoot")] public Dictionary>>> EventLoot { get; set; } + [JsonPropertyName("events")] public List Events { get; set; } [JsonPropertyName("eventBotMapping")] @@ -59,19 +61,26 @@ public class SeasonalEvent public SeasonalEventType Type { get; set; } [JsonPropertyName("startDay")] + [JsonConverter(typeof(StringToNumberFactoryConverter))] public int StartDay { get; set; } [JsonPropertyName("startMonth")] + [JsonConverter(typeof(StringToNumberFactoryConverter))] public int StartMonth { get; set; } [JsonPropertyName("endDay")] + [JsonConverter(typeof(StringToNumberFactoryConverter))] public int EndDay { get; set; } [JsonPropertyName("endMonth")] + [JsonConverter(typeof(StringToNumberFactoryConverter))] public int EndMonth { get; set; } [JsonPropertyName("settings")] public Dictionary Settings { get; set; } // TODO: Type was Record + + [JsonPropertyName("setting")] + public Dictionary SettingsDoNOTUse { set => Settings = value; } } public class SeasonalEventSettings @@ -102,4 +111,4 @@ public class GifterSetting [JsonPropertyName("spawnChance")] public int SpawnChance { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Config/TraderConfig.cs b/Core/Models/Spt/Config/TraderConfig.cs index f565ee92..74d834a2 100644 --- a/Core/Models/Spt/Config/TraderConfig.cs +++ b/Core/Models/Spt/Config/TraderConfig.cs @@ -34,6 +34,9 @@ public class TraderConfig : BaseConfig public class UpdateTime { + [JsonPropertyName("_name")] + public string Name { get; set; } + [JsonPropertyName("traderId")] public string TraderId { get; set; } @@ -139,6 +142,9 @@ public class CoopExtractReward : LootRequest { [JsonPropertyName("sendGift")] public bool SendGift { get; set; } + + [JsonPropertyName("useRewarditemBlacklist")] + public bool UseRewarditemBlacklist { get; set; } [JsonPropertyName("messageLocaleIds")] public List MessageLocaleIds { get; set; } @@ -171,4 +177,4 @@ public class ModdedTraders /** Trader Ids to enable the clothing service for */ [JsonPropertyName("clothingService")] public List ClothingService { get; set; } -} \ No newline at end of file +} diff --git a/Core/Models/Spt/Dialog/SendMessageDetails.cs b/Core/Models/Spt/Dialog/SendMessageDetails.cs index ec0c2ed1..d8e2ebc3 100644 --- a/Core/Models/Spt/Dialog/SendMessageDetails.cs +++ b/Core/Models/Spt/Dialog/SendMessageDetails.cs @@ -93,6 +93,9 @@ public class ProfileChangeEvent [JsonPropertyName("entity")] public string? Entity { get; set; } + + [JsonPropertyName("data")] + public string? Data { get; set; } } public enum ProfileChangeEventType @@ -105,4 +108,4 @@ public enum ProfileChangeEventType UnlockTrader, AssortmentUnlockRule, HideoutAreaLevel -} \ No newline at end of file +} diff --git a/Core/Routers/Dynamic/HttpDynamicRouter.cs b/Core/Routers/Dynamic/HttpDynamicRouter.cs index a97bc98e..9c659fdc 100644 --- a/Core/Routers/Dynamic/HttpDynamicRouter.cs +++ b/Core/Routers/Dynamic/HttpDynamicRouter.cs @@ -1,12 +1,14 @@ using Core.Annotations; using Core.DI; +using Core.Utils; namespace Core.Routers.Dynamic; [Injectable(InjectableTypeOverride = typeof(DynamicRouter))] public class HttpDynamicRouter : DynamicRouter { - public HttpDynamicRouter(ImageRouter imageRouter) : base( + public HttpDynamicRouter(ImageRouter imageRouter, JsonUtil jsonUtil) : base( + jsonUtil, [ new(".jpg", (_, _, _, _) => imageRouter.GetImage()), new(".png", (_, _, _, _) => imageRouter.GetImage()), @@ -15,9 +17,4 @@ public class HttpDynamicRouter : DynamicRouter ) { } - - public override Type? GetBodyDeserializationType() - { - return null; - } } diff --git a/Core/Routers/HttpRouter.cs b/Core/Routers/HttpRouter.cs index b66d8103..a05ef642 100644 --- a/Core/Routers/HttpRouter.cs +++ b/Core/Routers/HttpRouter.cs @@ -73,13 +73,10 @@ public class HttpRouter foreach (var route in routers) { if (route.CanHandle(url, dynamic)) { - var type = route.GetBodyDeserializationType(); - if (type != null && !string.IsNullOrEmpty(body)) - deserializedObject = JsonSerializer.Deserialize(body, type); if (dynamic) { - wrapper.Output = (route as DynamicRouter).HandleDynamic(url, deserializedObject, sessionID, wrapper.Output) as string; + wrapper.Output = (route as DynamicRouter).HandleDynamic(url, body, sessionID, wrapper.Output) as string; } else { - wrapper.Output = (route as StaticRouter).HandleStatic(url, deserializedObject, sessionID, wrapper.Output) as string; + wrapper.Output = (route as StaticRouter).HandleStatic(url, body, sessionID, wrapper.Output) as string; } matched = true; } diff --git a/Core/Routers/Static/LauncherStaticRouter.cs b/Core/Routers/Static/LauncherStaticRouter.cs index 97d1528f..1aa5ae37 100644 --- a/Core/Routers/Static/LauncherStaticRouter.cs +++ b/Core/Routers/Static/LauncherStaticRouter.cs @@ -2,59 +2,68 @@ using Core.Annotations; using Core.Callbacks; using Core.DI; using Core.Models.Eft.Common; +using Core.Models.Eft.Launcher; +using Core.Utils; namespace Core.Routers.Static; [Injectable(InjectableTypeOverride = typeof(StaticRouter))] -public class LauncherStaticRouter : StaticRouter { - - public LauncherStaticRouter(LauncherCallbacks launcherCallbacks) : base([ - new RouteAction( - "/launcher/ping", - (url, _, sessionID, _) => launcherCallbacks.Ping(url, null, sessionID)), - new RouteAction( - "/launcher/server/connect", - (_, _, _, _) => launcherCallbacks.Connect()), - new RouteAction( - "/launcher/profile/login", - (url, info, sessionID, _) => launcherCallbacks.Login(url, info, sessionID)), - new RouteAction( - "/launcher/profile/register", - (url, info, sessionID, _) => launcherCallbacks.Register(url, info, sessionID)), - new RouteAction( - "/launcher/profile/get", - (url, info, sessionID, _) => launcherCallbacks.Get(url, info, sessionID)), - new RouteAction( - "/launcher/profile/change/username", - (url, info, sessionID, _) => launcherCallbacks.ChangeUsername(url, info, sessionID)), - new RouteAction( - "/launcher/profile/change/password", - (url, info, sessionID, _) => launcherCallbacks.ChangePassword(url, info, sessionID)), - new RouteAction( - "/launcher/profile/change/wipe", - (url, info, sessionID, _) => launcherCallbacks.Wipe(url, info, sessionID)), - new RouteAction( - "/launcher/profile/remove", - (url, info, sessionID, _) => launcherCallbacks.RemoveProfile(url, info, sessionID)), - new RouteAction( - "/launcher/profile/compatibleTarkovVersion", - (_, _, _, _) => launcherCallbacks.GetCompatibleTarkovVersion()), - new RouteAction( - "/launcher/server/version", - (_, _, _, _) => launcherCallbacks.GetServerVersion()), - new RouteAction( - "/launcher/server/loadedServerMods", - (_, _, _, _) => launcherCallbacks.GetLoadedServerMods()), - new RouteAction( - "/launcher/server/serverModsUsedByProfile", - (url, info, sessionID, _) => launcherCallbacks.GetServerModsProfileUsed(url, info, sessionID)), - ]) +public class LauncherStaticRouter : StaticRouter +{ + public LauncherStaticRouter(LauncherCallbacks launcherCallbacks, JsonUtil jsonUtil) : base( + jsonUtil, + [ + new RouteAction( + "/launcher/ping", + (url, _, sessionID, _) => launcherCallbacks.Ping(url, null, sessionID)), + new RouteAction( + "/launcher/server/connect", + (_, _, _, _) => launcherCallbacks.Connect()), + new RouteAction( + "/launcher/profile/login", + (url, info, sessionID, _) => launcherCallbacks.Login(url, info as LoginRequestData, sessionID), + typeof(LoginRequestData)), + new RouteAction( + "/launcher/profile/register", + (url, info, sessionID, _) => launcherCallbacks.Register(url, info as RegisterData, sessionID), + typeof(RegisterData)), + new RouteAction( + "/launcher/profile/get", + (url, info, sessionID, _) => launcherCallbacks.Get(url, info as LoginRequestData, sessionID), + typeof(LoginRequestData)), + new RouteAction( + "/launcher/profile/change/username", + (url, info, sessionID, _) => + launcherCallbacks.ChangeUsername(url, info as ChangeRequestData, sessionID), + typeof(ChangeRequestData)), + new RouteAction( + "/launcher/profile/change/password", + (url, info, sessionID, _) => + launcherCallbacks.ChangePassword(url, info as ChangeRequestData, sessionID), + typeof(ChangeRequestData)), + new RouteAction( + "/launcher/profile/change/wipe", + (url, info, sessionID, _) => launcherCallbacks.Wipe(url, info as RegisterData, sessionID), + typeof(RegisterData)), + new RouteAction( + "/launcher/profile/remove", + (url, info, sessionID, _) => launcherCallbacks.RemoveProfile(url, info as RemoveProfileData, sessionID), + typeof(RemoveProfileData)), + new RouteAction( + "/launcher/profile/compatibleTarkovVersion", + (_, _, _, _) => launcherCallbacks.GetCompatibleTarkovVersion()), + new RouteAction( + "/launcher/server/version", + (_, _, _, _) => launcherCallbacks.GetServerVersion()), + new RouteAction( + "/launcher/server/loadedServerMods", + (_, _, _, _) => launcherCallbacks.GetLoadedServerMods()), + new RouteAction( + "/launcher/server/serverModsUsedByProfile", + (url, info, sessionID, _) => + launcherCallbacks.GetServerModsProfileUsed(url, info as EmptyRequestData, sessionID), + typeof(EmptyRequestData)) + ]) { } - - public override Type? GetBodyDeserializationType() - { - throw new NotImplementedException(); - } -} } diff --git a/Core/Servers/ConfigServer.cs b/Core/Servers/ConfigServer.cs index 92c8d4bd..1231bc25 100644 --- a/Core/Servers/ConfigServer.cs +++ b/Core/Servers/ConfigServer.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; using Core.Annotations; using Core.Models.Enums; using Core.Models.Spt.Config; +using Core.Utils; using ILogger = Core.Models.Utils.ILogger; namespace Core.Servers; @@ -11,16 +12,18 @@ namespace Core.Servers; [Injectable(InjectionType.Singleton)] public class ConfigServer { - private ILogger _logger; + protected ILogger _logger; + protected JsonUtil _jsonUtil; protected Dictionary configs = new(); protected readonly string[] acceptableFileExtensions = [".json", ".jsonc"]; public ConfigServer( - ILogger logger - // TODO: We need JsonUtil here => JsonUtil jsonUtil, + ILogger logger, + JsonUtil jsonUtil ) { _logger = logger; + _jsonUtil = jsonUtil; Initialize(); } @@ -50,8 +53,7 @@ public class ConfigServer { var fileContent = File.ReadAllText(file); var type = GetConfigTypeByFilename(file); - var deserializedContent = - JsonSerializer.Deserialize(fileContent, type, new JsonSerializerOptions() { Converters = { new JsonStringEnumConverter() } }); + var deserializedContent = _jsonUtil.Deserialize(fileContent, type); if (deserializedContent == null) { @@ -78,4 +80,4 @@ public class ConfigServer .First(en => en.GetValue().Contains(Path.GetFileNameWithoutExtension(filename))); return type.GetConfigType(); } -} \ No newline at end of file +} diff --git a/Core/Servers/Http/SptHttpListener.cs b/Core/Servers/Http/SptHttpListener.cs index b54f144c..434cfa1d 100644 --- a/Core/Servers/Http/SptHttpListener.cs +++ b/Core/Servers/Http/SptHttpListener.cs @@ -19,12 +19,13 @@ public class SptHttpListener : IHttpListener protected readonly ILogger _logger; protected readonly HttpResponseUtil _httpResponseUtil; protected readonly LocalisationService _localisationService; + protected readonly JsonUtil _jsonUtil; public SptHttpListener( HttpRouter httpRouter, // TODO: delay required IEnumerable serializers, ILogger logger, // TODO: requestsLogger: ILogger, - // TODO: JsonUtil jsonUtil, + JsonUtil jsonUtil, HttpResponseUtil httpHttpResponseUtil, LocalisationService localisationService ) @@ -34,6 +35,7 @@ public class SptHttpListener : IHttpListener _logger = logger; _httpResponseUtil = httpHttpResponseUtil; _localisationService = localisationService; + _jsonUtil = jsonUtil; } private static readonly ImmutableHashSet SupportedMethods = ["GET", "PUT", "POST"]; @@ -98,8 +100,8 @@ public class SptHttpListener : IHttpListener ) { if (body == null) - body = "{}"; - var bodyInfo = JsonSerializer.Serialize(body); + body = new object(); + var bodyInfo = _jsonUtil.Serialize(body); if (IsDebugRequest(req)) { // Send only raw response without transformation @@ -160,7 +162,7 @@ public class SptHttpListener : IHttpListener /* route doesn't exist or response is not properly set up */ if (string.IsNullOrEmpty(output)) { _logger.Error(_localisationService.GetText("unhandled_response", req.Path)); - _logger.Info(JsonSerializer.Serialize(deserializedObject)); + _logger.Info(_jsonUtil.Serialize(deserializedObject)); output = _httpResponseUtil.GetBody(null, 404, $"UNHANDLED RESPONSE: {req.Path}"); } return output; diff --git a/Core/Servers/SaveServer.cs b/Core/Servers/SaveServer.cs new file mode 100644 index 00000000..bec7c83a --- /dev/null +++ b/Core/Servers/SaveServer.cs @@ -0,0 +1,261 @@ +using System.Diagnostics; +using System.Runtime.InteropServices.JavaScript; +using Core.Annotations; +using Core.DI; +using Core.Models.Eft.Common; +using Core.Models.Eft.Profile; +using Core.Models.Enums; +using Core.Models.Spt.Config; +using Core.Services; +using Core.Utils; +using ILogger = Core.Models.Utils.ILogger; + +namespace Core.Servers; + +[Injectable(InjectionType.Singleton)] +public class SaveServer +{ + protected string profileFilepath = "user/profiles/"; + + protected Dictionary profiles = new(); + + // onLoad = require("../bindings/SaveLoad"); + protected readonly Dictionary> onBeforeSaveCallbacks = new(); + protected Dictionary saveMd5 = new(); + + protected readonly FileUtil _vfs; + protected readonly IEnumerable _saveLoadRouters; + protected readonly JsonUtil _jsonUtil; + protected readonly HashUtil _hashUtil; + protected readonly LocalisationService _localisationService; + protected readonly ILogger _logger; + protected readonly ConfigServer _configServer; + + public SaveServer( + FileUtil vfs, + IEnumerable saveLoadRouters, + JsonUtil jsonUtil, + HashUtil hashUtil, + LocalisationService localisationService, + ILogger logger, + ConfigServer configServer + ) + { + _vfs = vfs; + _saveLoadRouters = saveLoadRouters; + _jsonUtil = jsonUtil; + _hashUtil = hashUtil; + _localisationService = localisationService; + _logger = logger; + _configServer = configServer; + } + + /** + * Add callback to occur prior to saving profile changes + * @param id Id for save callback + * @param callback Callback to execute prior to running SaveServer.saveProfile() + */ + public void AddBeforeSaveCallback(string id, Func callback) + { + onBeforeSaveCallbacks[id] = callback; + } + + /** + * Remove a callback from being executed prior to saving profile in SaveServer.saveProfile() + * @param id Id of callback to remove + */ + public void RemoveBeforeSaveCallback(string id) + { + if (onBeforeSaveCallbacks.ContainsKey(id)) + onBeforeSaveCallbacks.Remove(id); + } + + /** + * Load all profiles in /user/profiles folder into memory (this.profiles) + */ + public void Load() + { + // get files to load + if (!_vfs.DirectoryExists(profileFilepath)) + { + _vfs.CreateDirectory(profileFilepath); + } + + var files = _vfs.GetFiles(profileFilepath).Where((item) => _vfs.GetFileExtension(item) == "json"); + + // load profiles + var stopwatch = Stopwatch.StartNew(); + foreach (var file in files) { + LoadProfile(_vfs.StripExtension(file)); + } + stopwatch.Stop(); + _logger.Debug($"{files.Count()} Profiles took: {stopwatch.ElapsedMilliseconds}ms to load."); + } + + /** + * Save changes for each profile from memory into user/profiles json + */ + public void Save() + { + // Save every profile + var totalTime = 0L; + foreach ( var sessionID in profiles) { + totalTime += SaveProfile(sessionID.Key); + } + + _logger.Debug($"Saved {profiles.Count} profiles, took: {totalTime}ms", false); + } + + /** + * Get a player profile from memory + * @param sessionId Session id + * @returns ISptProfile + */ + public SptProfile GetProfile(string sessionId) + { + if (!string.IsNullOrEmpty(sessionId)) + { + throw new Exception("session id provided was empty, did you restart the server while the game was running?"); + } + + if (profiles == null || profiles.Count == 0) + { + throw new Exception($"no profiles found in saveServer with id: ${sessionId}"); + } + + if (!profiles.TryGetValue(sessionId, out var sptProfile)) + throw new Exception($"no profile found for sessionId: {sessionId}"); + + return sptProfile; + } + + public bool ProfileExists(string id) + { + return profiles.ContainsKey(id); + } + + /** + * Get all profiles from memory + * @returns Dictionary of ISptProfile + */ + public Dictionary GetProfiles() => profiles; + + /** + * Delete a profile by id + * @param sessionID Id of profile to remove + * @returns true when deleted, false when profile not found + */ + public bool DeleteProfileById(string sessionID) + { + if (profiles.ContainsKey(sessionID)) + { + profiles.Remove(sessionID); + return true; + } + + return false; + } + + /** + * Create a new profile in memory with empty pmc/scav objects + * @param profileInfo Basic profile data + */ + public void CreateProfile(Info profileInfo) + { + if (profiles.ContainsKey(profileInfo.ProfileId)) + { + throw new Exception($"profile already exists for sessionId: {profileInfo.ProfileId}"); + } + + profiles.Add(profileInfo.ProfileId, + new SptProfile() + { + ProfileInfo = profileInfo, + CharacterData = new Characters() { PmcData = new PmcData(), ScavData = new PmcData() } + }); + } + + /** + * Add full profile in memory by key (info.id) + * @param profileDetails Profile to save + */ + public void AddProfile(SptProfile profileDetails) + { + profiles.Add(profileDetails.ProfileInfo.ProfileId, profileDetails); + } + + /** + * Look up profile json in user/profiles by id and store in memory + * Execute saveLoadRouters callbacks after being loaded into memory + * @param sessionID Id of profile to store in memory + */ + public void LoadProfile(string sessionID) + { + var filename = $"{sessionID}.json"; + var filePath = $"{profileFilepath}{filename}"; + if (_vfs.FileExists(filePath)) + { + // File found, store in profiles[] + profiles[sessionID] = _jsonUtil.Deserialize(_vfs.ReadFile(filePath)); + } + + // Run callbacks + foreach (var callback in _saveLoadRouters) { + profiles[sessionID] = callback.HandleLoad(GetProfile(sessionID)); + } + } + + /** + * Save changes from in-memory profile to user/profiles json + * Execute onBeforeSaveCallbacks callbacks prior to being saved to json + * @param sessionID profile id (user/profiles/id.json) + * @returns time taken to save in MS + */ + public long SaveProfile(string sessionID) + { + var filePath = $"{profileFilepath}{sessionID}.json"; + + // Run pre-save callbacks before we save into json + foreach ( var callback in onBeforeSaveCallbacks) + { + var previous = profiles[sessionID]; + try + { + profiles[sessionID] = onBeforeSaveCallbacks[callback.Key](profiles[sessionID]); + } + catch (Exception e) + { + _logger.Error(_localisationService.GetText("profile_save_callback_error", new { callback, error = e })); + profiles[sessionID] = previous; + } + } + + var start = Stopwatch.StartNew(); + var jsonProfile = _jsonUtil.Serialize(profiles[sessionID], !_configServer.GetConfig(ConfigTypes.CORE).Features.CompressProfile); + var fmd5 = _hashUtil.GenerateMd5ForData(jsonProfile); + if (!saveMd5.TryGetValue(sessionID, out var currentMd5) || currentMd5 != fmd5) { + saveMd5[sessionID] = fmd5; + // save profile to disk + _vfs.WriteFile(filePath, jsonProfile); + } + + start.Stop(); + return start.ElapsedMilliseconds; + } + + /** + * Remove a physical profile json from user/profiles + * @param sessionID Profile id to remove + * @returns true if file no longer exists + */ + public bool RemoveProfile(string sessionID) + { + var file = $"{profileFilepath}{sessionID}.json"; + if (profiles.ContainsKey(sessionID)) + { + profiles.Remove(sessionID); + _vfs.DeleteFile(file); + } + return !_vfs.FileExists(file); + } +} diff --git a/Core/Services/I18nService.cs b/Core/Services/I18nService.cs index a9bcbf8e..3b189e9d 100644 --- a/Core/Services/I18nService.cs +++ b/Core/Services/I18nService.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Core.Utils; namespace Core.Services; @@ -8,17 +9,18 @@ public class I18nService private Dictionary _fallbacks; private string _defaultLocale; private string _directory; - + private JsonUtil _jsonUtil; private string _setLocale; private Dictionary> _loadedLocales = new(); - public I18nService(List locales, Dictionary fallbacks, string defaultLocale, string directory) + public I18nService(JsonUtil jsonUtil, List locales, Dictionary fallbacks, string defaultLocale, string directory) { _locales = locales; _fallbacks = fallbacks; _defaultLocale = defaultLocale; _directory = directory; + _jsonUtil = jsonUtil; Initialize(); } @@ -30,7 +32,7 @@ public class I18nService throw new Exception($"Localisation files in directory {_directory} not found."); foreach (var file in files) _loadedLocales.Add(Path.GetFileNameWithoutExtension(file), - JsonSerializer.Deserialize>(File.ReadAllText(file)) ?? new Dictionary()); + _jsonUtil.Deserialize>(File.ReadAllText(file)) ?? new Dictionary()); if (!_loadedLocales.ContainsKey(_defaultLocale)) throw new Exception($"The default locale '{_defaultLocale}' does not exist on the loaded locales."); @@ -76,4 +78,4 @@ public class I18nService // TODO: Deal with arguments return GetLocalised(key); } -} \ No newline at end of file +} diff --git a/Core/Services/LocalisationService.cs b/Core/Services/LocalisationService.cs index 0ba6e197..98c45e73 100644 --- a/Core/Services/LocalisationService.cs +++ b/Core/Services/LocalisationService.cs @@ -18,7 +18,8 @@ public class LocalisationService ILogger logger, RandomUtil randomUtil, DatabaseServer databaseServer, - LocaleService localeService + LocaleService localeService, + JsonUtil jsonUtil ) { _logger = logger; @@ -26,6 +27,7 @@ public class LocalisationService _databaseServer = databaseServer; _localeService = localeService; _i18nService = new I18nService( + jsonUtil, localeService.GetServerSupportedLocales(), localeService.GetLocaleFallbacks(), "en", @@ -48,4 +50,4 @@ public class LocalisationService { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/Core/Utils/FileUtil.cs b/Core/Utils/FileUtil.cs index 79ffca10..1e29003d 100644 --- a/Core/Utils/FileUtil.cs +++ b/Core/Utils/FileUtil.cs @@ -29,4 +29,42 @@ public class FileUtil { return keepPath ? path.Split('.').First() : Path.GetFileNameWithoutExtension(path); } + + public bool DirectoryExists(string path) + { + return Directory.Exists(path); + } + + public void CreateDirectory(string path) + { + Directory.CreateDirectory(path); + } + + + public bool FileExists(string path) + { + return File.Exists(path); + } + + public string ReadFile(string path) + { + return File.ReadAllText(path); + } + + public void WriteFile(string filePath, string jsonProfile) + { + if (!FileExists(filePath)) + CreateFile(filePath); + File.WriteAllText(filePath, jsonProfile); + } + + private void CreateFile(string filePath) + { + File.Create(filePath); + } + + public void DeleteFile(string filePath) + { + File.Delete(filePath); + } } diff --git a/Core/Utils/HttpResponseUtil.cs b/Core/Utils/HttpResponseUtil.cs index 3473263b..edb218ae 100644 --- a/Core/Utils/HttpResponseUtil.cs +++ b/Core/Utils/HttpResponseUtil.cs @@ -13,13 +13,15 @@ namespace Core.Utils; public class HttpResponseUtil { protected readonly LocalisationService _localisationService; + protected readonly JsonUtil _jsonUtil; public HttpResponseUtil( - // JsonUtil jsonUtil, + JsonUtil jsonUtil, LocalisationService localisationService ) { _localisationService = localisationService; + _jsonUtil = jsonUtil; } private readonly ImmutableList _cleanupRegexList = @@ -49,7 +51,7 @@ public class HttpResponseUtil */ public string NoBody(T data) { - return ClearString(JsonSerializer.Serialize(data)); + return ClearString(_jsonUtil.Serialize(data)); } /** @@ -68,7 +70,7 @@ public class HttpResponseUtil public string GetUnclearedBody(T? data, int err = 0, string? errmsg = null) { - return JsonSerializer.Serialize(new GetBodyResponseData { Err = err, ErrMsg = errmsg, Data = data }); + return _jsonUtil.Serialize(new GetBodyResponseData { Err = err, ErrMsg = errmsg, Data = data }); } public string EmptyResponse() diff --git a/Core/Utils/ImporterUtil.cs b/Core/Utils/ImporterUtil.cs index b257b7e3..b16afbd6 100644 --- a/Core/Utils/ImporterUtil.cs +++ b/Core/Utils/ImporterUtil.cs @@ -10,17 +10,14 @@ namespace Core.Utils; public class ImporterUtil { private readonly FileUtil _fileUtil; + private readonly JsonUtil _jsonUtil; private readonly HashSet filesToIgnore = ["bearsuits.json", "usecsuits.json", "archivedquests.json"]; - - private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions - { - UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, Converters = { new ListOrTConverterFactory(), new DictionaryOrListConverter() } - }; - public ImporterUtil(FileUtil fileUtil) + public ImporterUtil(FileUtil fileUtil, JsonUtil jsonUtil) { _fileUtil = fileUtil; + _jsonUtil = jsonUtil; } /** @@ -64,7 +61,7 @@ public class ImporterUtil ); try { - var fileDeserialized = JsonSerializer.Deserialize(fileData, propertyType, jsonSerializerOptions); + var fileDeserialized = _jsonUtil.Deserialize(fileData, propertyType); if (onObjectDeserialized != null) onObjectDeserialized(file, fileDeserialized); @@ -159,7 +156,7 @@ public class ImporterUtil if (matchedProperty == null) throw new Exception($"Unable to find property '{Path.GetFileNameWithoutExtension(file)}' for type '{loadedType.Name}'"); var propertyType = matchedProperty.PropertyType; - var fileDeserialized = JsonSerializer.Deserialize(fileData, propertyType); + var fileDeserialized = _jsonUtil.Deserialize(fileData, propertyType); onObjectDeserialized(file, fileDeserialized); matchedProperty.GetSetMethod().Invoke(result, [fileDeserialized]); @@ -198,7 +195,7 @@ public class ImporterUtil promises.Add(File.ReadAllTextAsync(fileNode).ContinueWith(fd => { onReadCallback(fileNode, fd.Result); - return JsonSerializer.Deserialize(fd.Result, typeof(object)); + return _jsonUtil.Deserialize(fd.Result, typeof(object)); })); /* this.vfs diff --git a/Core/Utils/Json/Converters/EftEnumConverter.cs b/Core/Utils/Json/Converters/EftEnumConverter.cs new file mode 100644 index 00000000..4a649240 --- /dev/null +++ b/Core/Utils/Json/Converters/EftEnumConverter.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Core.Utils.Json.Converters; + +public class EftEnumConverter : JsonConverter +{ + private static readonly JsonSerializerOptions _options = new() {Converters = { new JsonStringEnumConverter() }}; + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String || reader.TokenType == JsonTokenType.PropertyName) + { + var str = reader.GetString(); + return (T)Enum.Parse(typeof(T), str, true); + } + + if (reader.TokenType == JsonTokenType.Number) + { + var str = reader.GetInt32().ToString(); + return (T)Enum.Parse(typeof(T), str, true); + } + return default; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, _options); + } + + public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Read(ref reader, typeToConvert, options); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, _options); + } +} diff --git a/Core/Utils/Json/Converters/StringToNumberFactoryConverter.cs b/Core/Utils/Json/Converters/StringToNumberFactoryConverter.cs index 8812fb4b..52791e64 100644 --- a/Core/Utils/Json/Converters/StringToNumberFactoryConverter.cs +++ b/Core/Utils/Json/Converters/StringToNumberFactoryConverter.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; @@ -39,7 +40,7 @@ public class StringToNumberFactoryConverter : JsonConverterFactory } catch (Exception ex) { - Console.WriteLine($"Tried to convert {value} into {type.Name} but failed to parse it, null value will be used instead."); + Debug.WriteLine($"Tried to convert {value} into {type.Name} but failed to parse it, null value will be used instead."); } return default; diff --git a/Core/Utils/JsonUtil.cs b/Core/Utils/JsonUtil.cs index 474bc078..a2627e3b 100644 --- a/Core/Utils/JsonUtil.cs +++ b/Core/Utils/JsonUtil.cs @@ -1,8 +1,49 @@ +using System.Text.Json; +using System.Text.Json.Serialization; using Core.Annotations; +using Core.Models.Enums; +using Core.Models.Spt.Dialog; +using Core.Utils.Json.Converters; namespace Core.Utils; [Injectable(InjectionType.Singleton)] public class JsonUtil { -} \ No newline at end of file + private readonly JsonSerializerOptions jsonSerializerOptions = new() + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + Converters = + { + new ListOrTConverterFactory(), + new DictionaryOrListConverter(), + new EftEnumConverter(), + new EftEnumConverter(), + new EftEnumConverter(), + new EftEnumConverter() + } + }; + + public T? Deserialize(string? json) + { + return string.IsNullOrEmpty(json) ? default : JsonSerializer.Deserialize(json, jsonSerializerOptions); + } + + public object? Deserialize(string? json, Type type) + { + return string.IsNullOrEmpty(json) ? null : JsonSerializer.Deserialize(json, type, jsonSerializerOptions); + } + + + public string? Serialize(T? obj, bool indented = false) + { + jsonSerializerOptions.WriteIndented = indented; + return obj == null ? null : JsonSerializer.Serialize(obj, jsonSerializerOptions); + } + + public string? Serialize(object? obj, Type type, bool indented = false) + { + jsonSerializerOptions.WriteIndented = indented; + return obj == null ? null : JsonSerializer.Serialize(obj, type, jsonSerializerOptions); + } +} diff --git a/Core/Utils/Watermark.cs b/Core/Utils/Watermark.cs new file mode 100644 index 00000000..934a285d --- /dev/null +++ b/Core/Utils/Watermark.cs @@ -0,0 +1,188 @@ +using Core.Annotations; +using Core.Models.Enums; +using Core.Models.Logging; +using Core.Models.Spt.Config; +using Core.Servers; +using Core.Services; + +namespace Core.Utils; + +[Injectable] +public class WatermarkLocale { + protected List description; + protected List warning; + protected List modding; + + public WatermarkLocale(LocalisationService localisationService) { + description = [ + localisationService.GetText("watermark-discord_url"), + "", + localisationService.GetText("watermark-free_of_charge"), + localisationService.GetText("watermark-paid_scammed"), + localisationService.GetText("watermark-commercial_use_prohibited"), + ]; + warning = [ + "", + localisationService.GetText("watermark-testing_build"), + localisationService.GetText("watermark-no_support"), + "", + $"{localisationService.GetText("watermark-report_issues_to")}:", + localisationService.GetText("watermark-issue_tracker_url"), + "", + localisationService.GetText("watermark-use_at_own_risk"), + ]; + modding = [ + "", + localisationService.GetText("watermark-modding_disabled"), + "", + localisationService.GetText("watermark-not_an_issue"), + localisationService.GetText("watermark-do_not_report"), + ]; + } + + public List GetDescription() => description; + public List GetWarning() => warning; + public List GetModding() => modding; +} + +[Injectable] +public class Watermark { + protected CoreConfig sptConfig; + protected List text = []; + protected string versionLabel = ""; + + protected Models.Utils.ILogger _logger; + protected ConfigServer _configServer; + protected LocalisationService _localisationService; + protected WatermarkLocale _watermarkLocale; + public Watermark( + Models.Utils.ILogger logger, + ConfigServer configServer, + LocalisationService localisationService, + WatermarkLocale watermarkLocale + ) { + _logger = logger; + _configServer = configServer; + _localisationService = localisationService; + _watermarkLocale = watermarkLocale; + sptConfig = _configServer.GetConfig(ConfigTypes.CORE); + } + + public void Initialize() + { + var description = _watermarkLocale.GetDescription(); + var warning = _watermarkLocale.GetWarning(); + var modding = _watermarkLocale.GetModding(); + var versionTag = GetVersionTag(); + + versionLabel = $"{sptConfig.ProjectName} {versionTag}"; + + text = [versionLabel]; + text = [..text, ..description]; + + /* + if (ProgramStatics.DEBUG) { + text = text.concat([...warning]); + } + if (!ProgramStatics.MODS) { + text = text.concat([...modding]); + } + */ + + if (sptConfig.CustomWatermarkLocaleKeys?.Count > 0) { + foreach (var key in sptConfig.CustomWatermarkLocaleKeys) { + text.AddRange(["", _localisationService.GetText(key)]); + } + } + + SetTitle(); + ResetCursor(); + Draw(); + } + + /** + * Get a version string (x.x.x) or (x.x.x-BLEEDINGEDGE) OR (X.X.X (18xxx)) + * @param withEftVersion Include the eft version this spt version was made for + * @returns string + */ + public string GetVersionTag(bool withEftVersion = false) { + var sptVersion = /*ProgramStatics.SPT_VERSION ||*/ sptConfig.SptVersion; + var versionTag = /*ProgramStatics.DEBUG&*/ $"{sptVersion} - {_localisationService.GetText("bleeding_edge_build")}"; + + if (withEftVersion) { + var tarkovVersion = sptConfig.CompatibleTarkovVersion.Split(".").First(); + return $"{versionTag} ({tarkovVersion})"; + } + + return versionTag; + } + + /** + * Handle singleplayer/settings/version + * Get text shown in game on screen, can't be translated as it breaks bsgs client when certian characters are used + * @returns string + */ + public string GetInGameVersionLabel() + { + var sptVersion = /*ProgramStatics.SPT_VERSION ||*/ sptConfig.SptVersion; + var versionTag = /*ProgramStatics.DEBUG ? */ + $"{sptVersion} - BLEEDINGEDGE { /*ProgramStatics.COMMIT?.slice(0, 6) ?? */""}"; + //: `${sptVersion} - ${ProgramStatics.COMMIT?.slice(0, 6) ?? ""}`; + + return $"{sptConfig.ProjectName} {versionTag}"; + } + + /** Set window title */ + protected void SetTitle() + { + Console.Title = versionLabel; + } + + /** Reset console cursor to top */ + protected void ResetCursor() + { + /* + if (!ProgramStatics.COMPILED) { + process.stdout.write("\u001B[2J\u001B[0;0f"); + } + */ + } + + /** Draw the watermark */ + protected void Draw() + { + var result = new List(); + + // Calculate size, add 10% for spacing to the right + var longestLength = text.Aggregate((a, b) => a.Length > b.Length ? a : b).Length * 1.1; + + // Create line of - to add top/bottom of watermark + var line = ""; + for (var i = 0; i < longestLength; ++i) { + line += "─"; + } + + // Opening line + result.Add($"┌─{line}─┐"); + + // Add content of watermark to screen + foreach (var watermarkText in text) { + var spacingSize = longestLength - watermarkText.Length; + var textWithRightPadding = watermarkText; + + for (var i = 0; i < spacingSize; ++i) { + textWithRightPadding += " "; + } + + result.Add($"│ {textWithRightPadding} │"); + } + + // Closing line + result.Add($"└─{line}─┘"); + + // Log watermark to screen + foreach (var text in result) { + _logger.LogWithColor(text, LogTextColor.Yellow); + } + } +} diff --git a/Server/Program.cs b/Server/Program.cs index b71ce40c..8b146f57 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -19,6 +19,9 @@ public static class Program { var serviceProvider = builder.Services.BuildServiceProvider(); + var watermark = serviceProvider.GetService(); + watermark.Initialize(); + // TODO: var preSptModLoader = serviceProvider.GetService(); var app = serviceProvider.GetService(); var appContext = serviceProvider.GetService(); appContext.AddValue(ContextVariableType.APP_BUILDER, builder); @@ -78,4 +81,4 @@ public static class Program RegisterComponents(builderServices, typeof(App).Assembly.GetTypes() .Where(type => Attribute.IsDefined(type, typeof(Injectable)))); } -} \ No newline at end of file +}