From c735debc825846ae64735c4ca845fa9732cf3831 Mon Sep 17 00:00:00 2001 From: CWX Date: Thu, 18 Jun 2026 18:31:28 +0100 Subject: [PATCH 1/6] add two new types to carry the stats --- .../Models/Eft/Launcher/MiniProfile.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Launcher/MiniProfile.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Launcher/MiniProfile.cs index 710f36a89..35ffdae05 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Launcher/MiniProfile.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Launcher/MiniProfile.cs @@ -39,4 +39,37 @@ public record MiniProfile [JsonPropertyName("sptData")] public Profile.Spt? SptData { get; set; } + + [JsonPropertyName("profileCurrency")] + public ProfileCurrency? ProfileCurrency { get; set; } + + [JsonPropertyName("profileStats")] + public ProfileStats? ProfileStats { get; set; } +} + +public record ProfileStats +{ + [JsonPropertyName("overall")] + public required Dictionary Overall { get; set; } + + [JsonPropertyName("pmc")] + public required Dictionary Pmc { get; set; } + + [JsonPropertyName("scav")] + public required Dictionary Scav { get; set; } +} + +public record ProfileCurrency +{ + [JsonPropertyName("roubles")] + public required int Roubles { get; set; } + + [JsonPropertyName("euros")] + public required int Euros { get; set; } + + [JsonPropertyName("dollars")] + public required int Dollars { get; set; } + + [JsonPropertyName("gp")] + public required int GP { get; set; } } From f4409aa89ba94f1b19f7d7e256807913f3fd46a7 Mon Sep 17 00:00:00 2001 From: CWX Date: Thu, 18 Jun 2026 18:31:44 +0100 Subject: [PATCH 2/6] removed unused method and usings --- .../Controllers/LauncherV2Controller.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs b/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs index 4d5a28a1f..90a296502 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/LauncherV2Controller.cs @@ -1,12 +1,10 @@ using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Launcher; -using SPTarkov.Server.Core.Models.Eft.Profile; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Models.Spt.Tables; using SPTarkov.Server.Core.Servers; -using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Services.Locales; using SPTarkov.Server.Core.Utils; using Info = SPTarkov.Server.Core.Models.Eft.Profile.Info; @@ -164,11 +162,6 @@ protected MongoId GetSessionId(LoginRequestData info) return MongoId.Empty(); } - public SptProfile GetProfile(MongoId sessionId) - { - return saveServer.GetProfile(sessionId); - } - public MiniProfile? GetMiniProfileFromUsername(LoginRequestData info) { return profileController.GetMiniProfile(GetSessionId(info)); From 1226fe2b6a06a9f3d3f139b039eca1899e4bbcfe Mon Sep 17 00:00:00 2001 From: CWX Date: Thu, 18 Jun 2026 18:33:24 +0100 Subject: [PATCH 3/6] add methods to helper to work out money amount, todo other stats --- .../Controllers/ProfileController.cs | 4 + .../Helpers/Profile/ProfileHelper.cs | 125 ++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/ProfileController.cs b/Libraries/SPTarkov.Server.Core/Controllers/ProfileController.cs index 66b1eeda4..1ec8ceb79 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/ProfileController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/ProfileController.cs @@ -72,6 +72,8 @@ public MiniProfile GetMiniProfile(MongoId sessionId) ProfileId = profile.ProfileInfo?.ProfileId ?? string.Empty, InvalidOrUnloadableProfile = profile.ProfileInfo?.InvalidOrUnloadableProfile, SptData = profileHelper.GetDefaultSptDataObject(), + ProfileCurrency = profileHelper.GetAccountCurrency(profile), + ProfileStats = profileHelper.GetProfileStats(profile) }; } @@ -89,6 +91,8 @@ public MiniProfile GetMiniProfile(MongoId sessionId) ProfileId = profile.ProfileInfo?.ProfileId ?? string.Empty, InvalidOrUnloadableProfile = profile.ProfileInfo?.InvalidOrUnloadableProfile, SptData = profile.SptData, + ProfileCurrency = profileHelper.GetAccountCurrency(profile), + ProfileStats = profileHelper.GetProfileStats(profile) }; } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs index 069e3ad2b..b87e19ecc 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs @@ -7,6 +7,7 @@ using SPTarkov.Server.Core.Models.Eft.Profile; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; +using SPTarkov.Server.Core.Models.Spt.Helper; using SPTarkov.Server.Core.Models.Spt.Tables; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services.Locales; @@ -836,4 +837,128 @@ public bool GetProfileTemplateFlagValue(string accountEdition, string flagKey) return matchingProfileTemplate.CustomFlags.GetValueOrDefault(flagKey, false); } + + /// + /// Gets the currencies in the profiles inventory + /// + /// Full profile + /// + public ProfileCurrency GetAccountCurrency(SptProfile profile) + { + var returnObject = new ProfileCurrency + { + Roubles = 0, + Euros = 0, + Dollars = 0, + GP = 0 + }; + + if (profile.CharacterData?.PmcData?.Inventory?.Items is null) + { + return returnObject; + } + + var items = profile.CharacterData?.PmcData?.Inventory.Items; + + foreach (var item in items!) + { + if (item.Template == Money.ROUBLES) + { + if (item.Upd?.StackObjectsCount.HasValue == true) + { + returnObject.Roubles = (int) (returnObject.Roubles + item.Upd.StackObjectsCount); + } + } + + if (item.Template == Money.EUROS) + { + if (item.Upd?.StackObjectsCount.HasValue == true) + { + returnObject.Euros = (int) (returnObject.Euros + item.Upd.StackObjectsCount); + } + } + + if (item.Template == Money.DOLLARS) + { + if (item.Upd?.StackObjectsCount.HasValue == true) + { + returnObject.Dollars = (int) (returnObject.Dollars + item.Upd.StackObjectsCount); + } + } + + if (item.Template == Money.GP) + { + if (item.Upd?.StackObjectsCount.HasValue == true) + { + returnObject.GP = (int) (returnObject.GP + item.Upd.StackObjectsCount); + } + } + } + + return returnObject; + } + + /// + /// TODO: Works out the stats for the profile, BSG's system for this isn't that friendly, so will come back to this + /// Currently returns a template of data for the profiles stats + /// + /// Full profile + /// + public ProfileStats GetProfileStats(SptProfile profile) + { + return new ProfileStats() + { + Overall = new Dictionary() + { + { "Raids", "0" }, + { "Survived", "1" }, + { "KIA", "2" }, + { "Kills", "3" }, + { "RunThrough", "4" }, + { "AWOL", "5" }, + { "MIA", "6" }, + { "KD", "7" }, + { "SurvivalRate", "100%" }, + { "AverageLifeSpan", "00:50" }, + { "Online", "69H00M" }, + { "LeaveRate", "0%" }, + { "SurvivalsInARow", "7" }, + { "AccountLifetime", "67H00M" } + }, + Pmc = new Dictionary() + { + { "Raids", "0" }, + { "Survived", "1" }, + { "KIA", "2" }, + { "Kills", "3" }, + { "RunThrough", "4" }, + { "AWOL", "5" }, + { "MIA", "6" }, + { "KD", "7" }, + { "SurvivalRate", "100%" }, + { "AverageLifeSpan", "00:50" }, + { "Online", "69H00M" }, + { "LeaveRate", "0%" }, + { "SurvivalsInARow", "7" }, + { "AccountLifetime", "67H00M" } + }, + Scav = new Dictionary() + { + { "Raids", "0" }, + { "Survived", "1" }, + { "KIA", "2" }, + { "Kills", "3" }, + { "RunThrough", "4" }, + { "AWOL", "5" }, + { "MIA", "6" }, + { "KD", "7" }, + { "SurvivalRate", "100%" }, + { "AverageLifeSpan", "00:50" }, + { "Online", "69H00M" }, + { "LeaveRate", "0%" }, + { "SurvivalsInARow", "7" }, + { "AccountLifetime", "67H00M" } + } + }; + } } From 36ee2b6d681ea512cc6d9506ed479e273f6a93db Mon Sep 17 00:00:00 2001 From: CWX Date: Thu, 18 Jun 2026 18:34:11 +0100 Subject: [PATCH 4/6] add missing using --- Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs index b87e19ecc..5111dc118 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs @@ -4,6 +4,7 @@ using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; +using SPTarkov.Server.Core.Models.Eft.Launcher; using SPTarkov.Server.Core.Models.Eft.Profile; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; From b7753743d777cc22a4bdd25e87bbfc1772620bdf Mon Sep 17 00:00:00 2001 From: cwx Date: Fri, 17 Jul 2026 20:10:15 +0100 Subject: [PATCH 5/6] Changed The value of CounterKey to Long, this is what it is in the client --- .../SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs index 51f72cde0..bea35f02c 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/BotBase.cs @@ -568,7 +568,7 @@ public record CounterKeyValue { public HashSet? Key { get; set; } - public double? Value { get; set; } + public long? Value { get; set; } } public record Aggressor From 05ec8796baf82d50f49b0683aef7b9330ca124fb Mon Sep 17 00:00:00 2001 From: cwx Date: Fri, 17 Jul 2026 20:11:10 +0100 Subject: [PATCH 6/6] clean up get currencies method, added methods to get profile stats for use in the launcher --- .../Helpers/Profile/ProfileHelper.cs | 243 ++++++++++++------ 1 file changed, 164 insertions(+), 79 deletions(-) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs index 4e8d4e7b1..362c5bac3 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/Profile/ProfileHelper.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Microsoft.Extensions.Logging; using SPTarkov.Common.Models.Logging; using SPTarkov.DI.Annotations; @@ -852,48 +853,31 @@ public ProfileCurrency GetAccountCurrency(SptProfile profile) Roubles = 0, Euros = 0, Dollars = 0, - GP = 0 + GP = 0, }; - if (profile.CharacterData?.PmcData?.Inventory?.Items is null) + foreach (var item in profile.CharacterData?.PmcData?.Inventory?.Items ?? []) { - return returnObject; - } - - var items = profile.CharacterData?.PmcData?.Inventory.Items; + var stackSize = item.GetItemStackSize(); - foreach (var item in items!) - { if (item.Template == Money.ROUBLES) { - if (item.Upd?.StackObjectsCount.HasValue == true) - { - returnObject.Roubles = (int) (returnObject.Roubles + item.Upd.StackObjectsCount); - } + returnObject.Roubles += stackSize; } - if (item.Template == Money.EUROS) + if (item.Template == Money.DOLLARS) { - if (item.Upd?.StackObjectsCount.HasValue == true) - { - returnObject.Euros = (int) (returnObject.Euros + item.Upd.StackObjectsCount); - } + returnObject.Dollars += stackSize; } - if (item.Template == Money.DOLLARS) + if (item.Template == Money.EUROS) { - if (item.Upd?.StackObjectsCount.HasValue == true) - { - returnObject.Dollars = (int) (returnObject.Dollars + item.Upd.StackObjectsCount); - } + returnObject.Euros += stackSize; } if (item.Template == Money.GP) { - if (item.Upd?.StackObjectsCount.HasValue == true) - { - returnObject.GP = (int) (returnObject.GP + item.Upd.StackObjectsCount); - } + returnObject.GP += stackSize; } } @@ -901,66 +885,167 @@ public ProfileCurrency GetAccountCurrency(SptProfile profile) } /// - /// TODO: Works out the stats for the profile, BSG's system for this isn't that friendly, so will come back to this /// Currently returns a template of data for the profiles stats /// /// Full profile /// public ProfileStats GetProfileStats(SptProfile profile) { - return new ProfileStats() + var pmcStats = ReadStats(profile.CharacterData?.PmcData); + var scavStats = ReadStats(profile.CharacterData?.ScavData); + var overallStats = CombineStats(pmcStats, scavStats); + var accountLifetime = FormatOverallLifetime(profile.CharacterData?.PmcData?.Info?.RegistrationDate ?? 0); + + return new ProfileStats { - Overall = new Dictionary() - { - { "Raids", "0" }, - { "Survived", "1" }, - { "KIA", "2" }, - { "Kills", "3" }, - { "RunThrough", "4" }, - { "AWOL", "5" }, - { "MIA", "6" }, - { "KD", "7" }, - { "SurvivalRate", "100%" }, - { "AverageLifeSpan", "00:50" }, - { "Online", "69H00M" }, - { "LeaveRate", "0%" }, - { "SurvivalsInARow", "7" }, - { "AccountLifetime", "67H00M" } - }, - Pmc = new Dictionary() - { - { "Raids", "0" }, - { "Survived", "1" }, - { "KIA", "2" }, - { "Kills", "3" }, - { "RunThrough", "4" }, - { "AWOL", "5" }, - { "MIA", "6" }, - { "KD", "7" }, - { "SurvivalRate", "100%" }, - { "AverageLifeSpan", "00:50" }, - { "Online", "69H00M" }, - { "LeaveRate", "0%" }, - { "SurvivalsInARow", "7" }, - { "AccountLifetime", "67H00M" } - }, - Scav = new Dictionary() - { - { "Raids", "0" }, - { "Survived", "1" }, - { "KIA", "2" }, - { "Kills", "3" }, - { "RunThrough", "4" }, - { "AWOL", "5" }, - { "MIA", "6" }, - { "KD", "7" }, - { "SurvivalRate", "100%" }, - { "AverageLifeSpan", "00:50" }, - { "Online", "69H00M" }, - { "LeaveRate", "0%" }, - { "SurvivalsInARow", "7" }, - { "AccountLifetime", "67H00M" } - } + Overall = FormatStats(overallStats, accountLifetime), + Pmc = FormatStats(pmcStats, accountLifetime), + Scav = FormatStats(scavStats, accountLifetime), + }; + } + + private static CharacterStats ReadStats(PmcData? character) + { + var eft = character?.Stats?.Eft; + var counters = eft?.OverallCounters?.Items; + + var transitCounter = GetStat(counters, "ExitStatus", "Transit"); + + return new CharacterStats + { + Raids = GetStat(counters, "Sessions"), + Survived = GetStat(counters, "ExitStatus", "Survived") + transitCounter, + Killed = GetStat(counters, "ExitStatus", "Killed"), + Deaths = GetStat(counters, "Deaths"), + Kills = GetStat(counters, "Kills"), + RunThrough = GetStat(counters, "ExitStatus", "Runner"), + Awols = GetStat(counters, "ExitStatus", "Left"), + Mias = GetStat(counters, "ExitStatus", "MissingInAction"), + LongestWinStreak = GetStat(counters, "LongestWinStreak"), + TotalInGameTime = eft?.TotalInGameTime ?? 0, + }; + } + + private static CharacterStats CombineStats(CharacterStats pmcStats, CharacterStats scavStats) + { + return new CharacterStats + { + Raids = pmcStats.Raids + scavStats.Raids, + Survived = pmcStats.Survived + scavStats.Survived, + Killed = pmcStats.Killed + scavStats.Killed, + Deaths = pmcStats.Deaths + scavStats.Deaths, + Kills = pmcStats.Kills + scavStats.Kills, + RunThrough = pmcStats.RunThrough + scavStats.RunThrough, + Awols = pmcStats.Awols + scavStats.Awols, + Mias = pmcStats.Mias + scavStats.Mias, + LongestWinStreak = pmcStats.LongestWinStreak + scavStats.LongestWinStreak, + TotalInGameTime = pmcStats.TotalInGameTime + scavStats.TotalInGameTime, + }; + } + + private static Dictionary FormatStats(CharacterStats stats, string accountLifetime) + { + var killDeathRatio = + stats.Deaths > 0 + ? Math.Round(stats.Kills / (double)stats.Deaths, 2).ToString(CultureInfo.InvariantCulture) + : stats.Kills.ToString(CultureInfo.InvariantCulture); + var averageLifeSpan = stats.Raids > 0 ? FormatMinSeconds(stats.TotalInGameTime / stats.Raids) : "-"; + + return new Dictionary + { + { "Raids", stats.Raids.ToString(CultureInfo.InvariantCulture) }, + { "Survived", stats.Survived.ToString(CultureInfo.InvariantCulture) }, + { "KIA", stats.Killed.ToString(CultureInfo.InvariantCulture) }, + { "Kills", stats.Kills.ToString(CultureInfo.InvariantCulture) }, + { "RunThrough", stats.RunThrough.ToString(CultureInfo.InvariantCulture) }, + { "AWOL", stats.Awols.ToString(CultureInfo.InvariantCulture) }, + { "MIA", stats.Mias.ToString(CultureInfo.InvariantCulture) }, + { "KD", killDeathRatio }, + { "SurvivalRate", FormatPercentage(stats.Survived, stats.Raids) }, + { "AverageLifeSpan", averageLifeSpan }, + { "Online", FormatHoursMins(stats.TotalInGameTime) }, + { "LeaveRate", FormatPercentage(stats.Awols, stats.Raids) }, + { "SurvivalsInARow", stats.LongestWinStreak.ToString(CultureInfo.InvariantCulture) }, + { "AccountLifetime", accountLifetime }, }; } + + private static string FormatPercentage(long value, long total) + { + if (total <= 0) + { + return "-"; + } + + var percentage = (long)(value / (double)total * 100); + return $"{percentage.ToString(CultureInfo.InvariantCulture)}%"; + } + + private static long GetStat(IEnumerable? counters, params string[] requiredTags) + { + var value = counters?.FirstOrDefault(counter => counter.Key?.IsSupersetOf(requiredTags) == true)?.Value; + + return value ?? 0; + } + + /// + /// Taken from the client with interpolation instead, returns a format of MM:SS + /// + /// + /// + private static string FormatMinSeconds(long seconds) + { + var timeSpan = TimeSpan.FromSeconds(seconds); + return $"{timeSpan.Minutes:00}:{timeSpan.Seconds:00}"; + } + + /// + /// Taken from the client with interpolation instead, returns a format of HH:MM + /// + /// + /// + private static string FormatHoursMins(long seconds) + { + var timeSpan = TimeSpan.FromSeconds(seconds); + return $"{timeSpan.Hours:00}:{timeSpan.Minutes:00}"; + } + + /// + /// Taken from the client with interpolation instead, returns a format of - or HH:MM or DD or YYDD + /// + /// + /// + private static string FormatOverallLifetime(int regDate) + { + var localDateTimeFromUnixTime = DateTime.UnixEpoch.AddMilliseconds(regDate * 1000.0).ToLocalTime(); + var ts = DateTime.UtcNow - localDateTimeFromUnixTime; + + if (ts <= TimeSpan.FromHours(48)) + { + return $"{ts.TotalHours}h{ts.Minutes:00}m"; + } + + if (ts <= TimeSpan.FromDays(365)) + { + return $"{(int)ts.TotalDays}d"; + } + + var num = (int)(ts.TotalDays / 365); + var num2 = (int)(ts.TotalDays - 365 * num); + return $"{num}y{num2}d"; + } +} + +public record CharacterStats +{ + public long Raids { get; init; } + public long Survived { get; init; } + public long Killed { get; init; } + public long Deaths { get; init; } + public long Kills { get; init; } + public long RunThrough { get; init; } + public long Awols { get; init; } + public long Mias { get; init; } + public long LongestWinStreak { get; init; } + public long TotalInGameTime { get; init; } }