diff --git a/Build.props b/Build.props index c9ffda60..f2f94437 100644 --- a/Build.props +++ b/Build.props @@ -1,7 +1,7 @@ - 4.0.10 + 4.0.12 a12b34 0000000000 LOCAL diff --git a/Libraries/SPTarkov.Common/SPTarkov.Common.csproj b/Libraries/SPTarkov.Common/SPTarkov.Common.csproj index 1d913e12..378d0a2a 100644 --- a/Libraries/SPTarkov.Common/SPTarkov.Common.csproj +++ b/Libraries/SPTarkov.Common/SPTarkov.Common.csproj @@ -4,7 +4,7 @@ SPTarkov.Common Single Player Tarkov Common shared library for the Single Player Tarkov projects. - Copyright (c) Single Player Tarkov 2025 + Copyright (c) Single Player Tarkov 2026 LICENSE https://sp-tarkov.com https://github.com/sp-tarkov/server-csharp diff --git a/Libraries/SPTarkov.DI/SPTarkov.DI.csproj b/Libraries/SPTarkov.DI/SPTarkov.DI.csproj index bc264fd0..39a899c9 100644 --- a/Libraries/SPTarkov.DI/SPTarkov.DI.csproj +++ b/Libraries/SPTarkov.DI/SPTarkov.DI.csproj @@ -4,7 +4,7 @@ SPTarkov.DI Single Player Tarkov Dependency injection shared library for the Single Player Tarkov projects. - Copyright (c) Single Player Tarkov 2025 + Copyright (c) Single Player Tarkov 2026 LICENSE https://sp-tarkov.com https://github.com/sp-tarkov/server-csharp diff --git a/Libraries/SPTarkov.Reflection/SPTarkov.Reflection.csproj b/Libraries/SPTarkov.Reflection/SPTarkov.Reflection.csproj index d9f08b18..ef00be8b 100644 --- a/Libraries/SPTarkov.Reflection/SPTarkov.Reflection.csproj +++ b/Libraries/SPTarkov.Reflection/SPTarkov.Reflection.csproj @@ -1,10 +1,10 @@ - + SPTarkov.Reflection Single Player Tarkov Reflection library for the Single Player Tarkov server. - Copyright (c) Single Player Tarkov 2025 + Copyright (c) Single Player Tarkov 2026 LICENSE https://sp-tarkov.com https://github.com/sp-tarkov/server-csharp diff --git a/Libraries/SPTarkov.Server.Assets/SPTarkov.Server.Assets.csproj b/Libraries/SPTarkov.Server.Assets/SPTarkov.Server.Assets.csproj index 005fe28a..5cf34c17 100644 --- a/Libraries/SPTarkov.Server.Assets/SPTarkov.Server.Assets.csproj +++ b/Libraries/SPTarkov.Server.Assets/SPTarkov.Server.Assets.csproj @@ -1,10 +1,10 @@ - + SPTarkov.Server.Assets Single Player Tarkov Asset library for the Single Player Tarkov server. - Copyright (c) Single Player Tarkov 2025 + Copyright (c) Single Player Tarkov 2026 LICENSE https://sp-tarkov.com https://github.com/sp-tarkov/server-csharp diff --git a/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs b/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs index 9edc2b95..7d0de32b 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/DialogueController.cs @@ -248,7 +248,7 @@ public class DialogueController( var fullProfile = saveServer.GetProfile(sessionId); var dialogue = GetDialogByIdFromProfile(fullProfile, request); - if (dialogue.Messages?.Count == 0) + if (dialogue.Messages == null || dialogue.Messages.Count == 0) { return new GetMailDialogViewResponseData { @@ -266,9 +266,9 @@ public class DialogueController( return new GetMailDialogViewResponseData { - Messages = dialogue.Messages, + Messages = GetLimitedMessages(dialogue.Messages, request.Limit, request.Time), Profiles = GetProfilesForMail(fullProfile, dialogue.Users), - HasMessagesWithRewards = MessagesHaveUncollectedRewards(dialogue.Messages!), + HasMessagesWithRewards = MessagesHaveUncollectedRewards(dialogue.Messages), }; } @@ -410,6 +410,49 @@ public class DialogueController( return messages.Any(message => (message.Items?.Data?.Count ?? 0) > 0); } + /// + /// Gets a subset of messages from before a certain time + /// + /// The superset of messages + /// The maximum number of messages to return, null/0 means all + /// Limit to messages before this Unix time (seconds since epoch), null/0 means all + /// List of matching messages + protected List GetLimitedMessages(List allMessages, int? limit, decimal? time) + { + if ((time == null || time == 0) && (limit == null || limit == 0 || limit >= allMessages.Count)) + { + return allMessages; + } + + if (time == null || time == 0) + { + time = timeUtil.GetTimeStamp(); + } + + if (limit == null || limit == 0) + { + limit = int.MaxValue; + } + + List results = []; + for (var i = allMessages.Count - 1; i >= 0; i--) + { + var message = allMessages[i]; + if (message.DateTime <= time) + { + results.Add(message); + + if (results.Count >= limit) + { + break; + } + } + } + + results.Reverse(); // Since we iterated from newest to oldest, reverse so the result is in order + return results; + } + /// /// Handle client/mail/dialog/remove /// Remove an entire dialog with an entity (trader/user) diff --git a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs index 93b6c5e6..c770bf01 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/HideoutController.cs @@ -162,10 +162,7 @@ public class HideoutController( return; } - // Upgrade profile values - profileHideoutArea.Level++; - profileHideoutArea.CompleteTime = 0; - profileHideoutArea.Constructing = false; + var nextLevel = profileHideoutArea.Level + 1; var hideoutData = hideout.Areas.FirstOrDefault(area => area.Type == profileHideoutArea.Type); if (hideoutData is null) @@ -177,12 +174,18 @@ public class HideoutController( } // Apply bonuses - if (!hideoutData.Stages.TryGetValue(profileHideoutArea.Level.ToString(), out var hideoutStage)) + if (!hideoutData.Stages.TryGetValue(nextLevel.ToString(), out var hideoutStage)) { - logger.Error($"Stage level: {profileHideoutArea.Level} not found for area: {request.AreaType}"); + logger.Error($"Stage level: {nextLevel} not found for area: {request.AreaType}"); return; } + + // Upgrade profile values + profileHideoutArea.Level = nextLevel; + profileHideoutArea.CompleteTime = 0; + profileHideoutArea.Constructing = false; + var bonuses = hideoutStage.Bonuses; if (bonuses?.Count > 0) { diff --git a/Libraries/SPTarkov.Server.Core/Controllers/PrestigeController.cs b/Libraries/SPTarkov.Server.Core/Controllers/PrestigeController.cs index e3f7d6a4..2618e867 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/PrestigeController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/PrestigeController.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Common; @@ -67,6 +68,33 @@ public class PrestigeController(ProfileHelper profileHelper, DatabaseService dat profile.SptData.PendingPrestige = pendingPrestige; profile.ProfileInfo.IsWiped = true; + var prestigeLevels = databaseService.GetTemplates().Prestige?.Elements ?? []; + + var prestigeRewards = prestigeLevels + .Slice(0, pendingPrestige.PrestigeLevel.Value) + .SelectMany(prestigeInner => prestigeInner.Rewards); + + var customisationTemplateDb = databaseService.GetTemplates().Customization; + + foreach (var reward in prestigeRewards) + { + if (!MongoId.IsValidMongoId(reward.Target)) + { + continue; + } + + if (!customisationTemplateDb.TryGetValue(reward.Target, out var template)) + { + continue; + } + + // This has to be done before the profile is wiped, as the user can only select a new head during the wipe + if (template.Parent == CustomisationTypeId.HEAD) + { + profileHelper.AddHideoutCustomisationUnlock(profile, reward, CustomisationSource.PRESTIGE); + } + } + await saveServer.SaveProfileAsync(sessionId); } } diff --git a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs index cbd30a25..11983856 100644 --- a/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs +++ b/Libraries/SPTarkov.Server.Core/Controllers/RagfairController.cs @@ -1025,15 +1025,24 @@ public class RagfairController( } // Only reduce time to end if time remaining is greater than what we would set it to - var differenceInSeconds = playerOffer.EndTime - timeUtil.GetTimeStamp(); - if (differenceInSeconds > RagfairConfig.Sell.ExpireSeconds) + var now = timeUtil.GetTimeStamp(); + var configExpireSeconds = RagfairConfig.Sell.ExpireSeconds; + + var differenceInSeconds = playerOffer.EndTime - now; + if (differenceInSeconds > configExpireSeconds) { // `expireSeconds` Default is 71 seconds - var newEndTime = RagfairConfig.Sell.ExpireSeconds + timeUtil.GetTimeStamp(); + // TODO: RagfairConfig.Sell.ExpireSeconds should not exist as it should use + // Globals.Configuration.RagFair.OfferDurationTimeInHourAfterRemove (the value actually used by client) + var newEndTime = configExpireSeconds + now; playerOffer.EndTime = (long?)Math.Round((double)newEndTime); + differenceInSeconds = configExpireSeconds; } - logger.Debug($"Flagged player offer: {offerId} for expiry in: {TimeSpan.FromTicks(playerOffer.EndTime.Value).ToString()}"); + if (logger.IsLogEnabled(LogLevel.Debug) && differenceInSeconds is { } remaining) + { + logger.Debug($"Flagged player: {sessionId} offer: {offerId} for expiry in: {TimeSpan.FromSeconds(remaining).ToString()}"); + } return output; } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs index 491896d4..2cf2017b 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ProfileHelper.cs @@ -684,6 +684,9 @@ public class ProfileHelper( case CustomisationTypeId.UPPER: rewardToStore.Type = CustomisationType.UPPER; break; + case CustomisationTypeId.HEAD: + rewardToStore.Type = CustomisationType.HEAD; + break; default: logger.Error($"Unhandled customisation unlock type: {template.Parent} not added to profile"); return; diff --git a/Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs b/Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs index b829cf25..5fd0eaee 100644 --- a/Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs +++ b/Libraries/SPTarkov.Server.Core/Migration/Migrations/Fixes/InvalidRepeatableQuestFix.cs @@ -33,7 +33,7 @@ public sealed class InvalidRepeatableQuestFix : AbstractProfileMigration } var endTimeNode = quest["endTime"]; - var endTime = endTimeNode?.GetValue() ?? 0; + var endTime = endTimeNode?.GetValue() ?? 0; if (endTime != 0 && quest["changeRequirement"] is null) { @@ -56,7 +56,7 @@ public sealed class InvalidRepeatableQuestFix : AbstractProfileMigration continue; } - var endTime = quest["endTime"]?.GetValue() ?? 0; + var endTime = quest["endTime"]?.GetValue() ?? 0; if (endTime != 0 && quest["changeRequirement"] is null) { diff --git a/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj b/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj index 50d59c2b..f00613f6 100644 --- a/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj +++ b/Libraries/SPTarkov.Server.Core/SPTarkov.Server.Core.csproj @@ -1,10 +1,10 @@ - + SPTarkov.Server.Core Single Player Tarkov Core library for the Single Player Tarkov server. - Copyright (c) Single Player Tarkov 2025 + Copyright (c) Single Player Tarkov 2026 LICENSE https://sp-tarkov.com https://github.com/sp-tarkov/server-csharp diff --git a/Libraries/SPTarkov.Server.Core/Services/BotWeaponModLimitService.cs b/Libraries/SPTarkov.Server.Core/Services/BotWeaponModLimitService.cs index 1d0746fa..7b83341b 100644 --- a/Libraries/SPTarkov.Server.Core/Services/BotWeaponModLimitService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/BotWeaponModLimitService.cs @@ -113,7 +113,13 @@ public class BotWeaponModLimitService(ISptLogger logge var modIsLightOrLaser = itemHelper.IsOfBaseclasses(modTemplate.Id, modLimits.FlashlightLaserBaseTypes); if (modIsLightOrLaser) { - return WeaponModLimitReached(modTemplate.Id, modLimits.FlashlightLaser, modLimits.FlashlightLaserMax ?? 0, botRole); + return WeaponModLimitReached( + modTemplate.Id, + modLimits.FlashlightLaser, + modLimits.FlashlightLaserMax ?? 0, + botRole, + "light/laser" + ); } // Mod is a mount that can hold only flashlights ad limit is reached (don't want to add empty mounts if limit is reached) @@ -137,10 +143,11 @@ public class BotWeaponModLimitService(ISptLogger logge /// current number of this item on gun /// mod limit allowed /// role of bot we're checking weapon of + /// OPTIONAL: Type of mod, scope or lightlaser /// true if limit reached - protected bool WeaponModLimitReached(MongoId modTpl, ItemCount currentCount, int? maxLimit, string botRole) + protected bool WeaponModLimitReached(MongoId modTpl, ItemCount currentCount, int? maxLimit, string botRole, string modType = "scope") { - // No limit + // No limit, ignore if (maxLimit is null or 0) { return false; @@ -151,13 +158,13 @@ public class BotWeaponModLimitService(ISptLogger logge { if (logger.IsLogEnabled(LogLevel.Debug)) { - logger.Debug($"[{botRole}] scope limit reached! tried to add {modTpl} but scope count is {currentCount.Count}"); + logger.Debug($"[{botRole}] {modType} limit reached! tried to add: {modTpl} but {modType} count is: {currentCount.Count}"); } return true; } - // Increment scope count + // Increment mod count limit currentCount.Count++; return false; diff --git a/Libraries/SPTarkov.Server.Core/Services/CircleOfCultistService.cs b/Libraries/SPTarkov.Server.Core/Services/CircleOfCultistService.cs index d0d38f71..d0fde891 100644 --- a/Libraries/SPTarkov.Server.Core/Services/CircleOfCultistService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/CircleOfCultistService.cs @@ -683,14 +683,19 @@ public class CircleOfCultistService( /// Pool to add items to protected void AddTaskItemRequirementsToRewardPool(PmcData pmcData, HashSet itemRewardBlacklist, HashSet rewardPool) { - var activeTasks = pmcData.Quests.Where(quest => quest.Status == QuestStatusEnum.Started); - foreach (var task in activeTasks) + var activeTasks = pmcData.Quests?.Where(quest => quest.Status == QuestStatusEnum.Started); + foreach (var task in activeTasks ?? []) { var questData = questHelper.GetQuestFromDb(task.QId, pmcData); - var handoverConditions = questData.Conditions.AvailableForFinish.Where(condition => condition.ConditionType == "HandoverItem"); - foreach (var condition in handoverConditions) + if (questData is null) { - foreach (var neededItem in condition.Target.List) + logger.Warning($"Could not get quest data for QId {task.QId}."); + continue; + } + var handoverConditions = questData.Conditions.AvailableForFinish?.Where(condition => condition.ConditionType == "HandoverItem"); + foreach (var condition in handoverConditions ?? []) + { + foreach (var neededItem in condition?.Target?.List ?? []) { if (itemRewardBlacklist.Contains(neededItem) || !itemHelper.IsValidItem(neededItem)) { diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index 59dbe831..23af24e4 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -505,7 +505,11 @@ public class LocationLifecycleService( // TODO - Persist each players last visited location history over multiple transits, e.g. using InMemoryCacheService, need to take care to not let data get stored forever // Store transfer data for later use in `startLocalRaid()` when next raid starts request.LocationTransit.SptExitName = request.Results.ExitName; - profileActivityService.GetProfileActivityRaidData(sessionId).LocationTransit = request.LocationTransit; + + // Update raid data with new location data + var profileActivityRaidData = profileActivityService.GetProfileActivityRaidData(sessionId); + profileActivityRaidData.LocationTransit = request.LocationTransit; + profileActivityRaidData.RaidConfiguration?.Location = locationName; } if (!isPmc) diff --git a/Libraries/SPTarkov.Server.Core/Utils/JsonUtil.cs b/Libraries/SPTarkov.Server.Core/Utils/JsonUtil.cs index c5434228..d217c635 100644 --- a/Libraries/SPTarkov.Server.Core/Utils/JsonUtil.cs +++ b/Libraries/SPTarkov.Server.Core/Utils/JsonUtil.cs @@ -24,6 +24,7 @@ public class JsonUtil UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, #endif Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + NewLine = "\n", }; foreach (var registrator in registrators) diff --git a/Libraries/SPTarkov.Server.Web/SPTarkov.Server.Web.csproj b/Libraries/SPTarkov.Server.Web/SPTarkov.Server.Web.csproj index 11188d4b..75e8539a 100644 --- a/Libraries/SPTarkov.Server.Web/SPTarkov.Server.Web.csproj +++ b/Libraries/SPTarkov.Server.Web/SPTarkov.Server.Web.csproj @@ -1,10 +1,10 @@ - + SPTarkov.Server.Web Single Player Tarkov Common shared library for the Single Player Tarkov projects. - Copyright (c) Single Player Tarkov 2025 + Copyright (c) Single Player Tarkov 2026 LICENSE https://sp-tarkov.com https://github.com/sp-tarkov/server-csharp diff --git a/SPTarkov.Server/Modding/ModValidator.cs b/SPTarkov.Server/Modding/ModValidator.cs index 40ee8700..74be15e5 100644 --- a/SPTarkov.Server/Modding/ModValidator.cs +++ b/SPTarkov.Server/Modding/ModValidator.cs @@ -199,7 +199,7 @@ public class ModValidator(ISptLogger logger, ServerLocalisationSer new { name = mod.ModMetadata.Name, - version = $"{mod.ModMetadata.Version} (targets SPT: {mod.ModMetadata.SptVersion})", + version = $"{mod.ModMetadata.Version} (GUID: {mod.ModMetadata.ModGuid} | targets SPT: {mod.ModMetadata.SptVersion})", author = mod.ModMetadata.Author, } ) diff --git a/SPTarkov.Server/Program.cs b/SPTarkov.Server/Program.cs index 5e91a48d..a4265c83 100644 --- a/SPTarkov.Server/Program.cs +++ b/SPTarkov.Server/Program.cs @@ -71,7 +71,7 @@ public static class Program ShowRedConsoleMessage( e, - "The server has unexpectedly stopped, reach out to #spt-support in our Discord server. Include a screenshot of this message + the below error" + "The server has unexpectedly stopped, reach out to #mod-questions-4-0 in our Discord server. Include a screenshot of this message and the surrounding error(s) above and below" ); Console.WriteLine("Press any key to exit..."); Console.ReadLine(); @@ -93,6 +93,7 @@ public static class Program public static async Task StartServer(string[] args) { Console.OutputEncoding = Encoding.UTF8; + SetConsoleOutputMode(); // Some users don't know how to create a shortcut... if (!IsRunFromInstallationFolder()) @@ -157,8 +158,6 @@ public static class Program forwardedHeadersOptions.KnownProxies.Clear(); app.UseForwardedHeaders(forwardedHeadersOptions); - SetConsoleOutputMode(); - await app.Services.GetRequiredService().Startup(); await app.RunAsync(); diff --git a/SPTarkov.Server/SPTarkov.Server.csproj b/SPTarkov.Server/SPTarkov.Server.csproj index e3f0e007..defb7b77 100644 --- a/SPTarkov.Server/SPTarkov.Server.csproj +++ b/SPTarkov.Server/SPTarkov.Server.csproj @@ -4,7 +4,7 @@ SPTarkov.Server Single Player Tarkov Single Player Tarkov server launcher. - Copyright (c) Single Player Tarkov 2025 + Copyright (c) Single Player Tarkov 2026 LICENSE https://sp-tarkov.com https://github.com/sp-tarkov/server-csharp diff --git a/SPTarkov.Server/sptLogger.json b/SPTarkov.Server/sptLogger.json index bbe8dcb0..8ac51817 100644 --- a/SPTarkov.Server/sptLogger.json +++ b/SPTarkov.Server/sptLogger.json @@ -2,7 +2,7 @@ "loggers": [ { "type": "File", - "logLevel": "Trace", + "logLevel": "Info", "format": "[%date% %time%][%level%][%logger%] %message%", "filePath": "./user/logs/spt/", "filePattern": "spt%DATE%.log", @@ -23,7 +23,7 @@ }, { "type": "File", - "logLevel": "Trace", + "logLevel": "Info", "format": "[%date% %time%][%level%][%logger%] %message%", "filePath": "./user/logs/requests/", "filePattern": "requests%DATE%.log",