Merge tag '4.0.12'
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- SPT specific -->
|
||||
<SptVersion Condition="'$(SptVersion)' == ''">4.0.10</SptVersion>
|
||||
<SptVersion Condition="'$(SptVersion)' == ''">4.0.12</SptVersion>
|
||||
<SptCommit Condition="'$(SptCommit)' == ''">a12b34</SptCommit>
|
||||
<SptBuildTime Condition="'$(SptBuildTime)' == ''">0000000000</SptBuildTime>
|
||||
<SptBuildType Condition="'$(SptBuildType)' == ''">LOCAL</SptBuildType>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<PackageId>SPTarkov.Common</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Common shared library for the Single Player Tarkov projects.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2026</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<PackageId>SPTarkov.DI</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Dependency injection shared library for the Single Player Tarkov projects.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2026</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\Build.props" />
|
||||
<PropertyGroup>
|
||||
<PackageId>SPTarkov.Reflection</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Reflection library for the Single Player Tarkov server.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2026</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\Build.props" />
|
||||
<PropertyGroup>
|
||||
<PackageId>SPTarkov.Server.Assets</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Asset library for the Single Player Tarkov server.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2026</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a subset of messages from before a certain time
|
||||
/// </summary>
|
||||
/// <param name="allMessages">The superset of messages</param>
|
||||
/// <param name="limit">The maximum number of messages to return, null/0 means all</param>
|
||||
/// <param name="time">Limit to messages before this Unix time (seconds since epoch), null/0 means all</param>
|
||||
/// <returns>List of matching messages</returns>
|
||||
protected List<Message> GetLimitedMessages(List<Message> 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<Message> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle client/mail/dialog/remove
|
||||
/// Remove an entire dialog with an entity (trader/user)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ public sealed class InvalidRepeatableQuestFix : AbstractProfileMigration
|
||||
}
|
||||
|
||||
var endTimeNode = quest["endTime"];
|
||||
var endTime = endTimeNode?.GetValue<int>() ?? 0;
|
||||
var endTime = endTimeNode?.GetValue<long>() ?? 0;
|
||||
|
||||
if (endTime != 0 && quest["changeRequirement"] is null)
|
||||
{
|
||||
@@ -56,7 +56,7 @@ public sealed class InvalidRepeatableQuestFix : AbstractProfileMigration
|
||||
continue;
|
||||
}
|
||||
|
||||
var endTime = quest["endTime"]?.GetValue<int>() ?? 0;
|
||||
var endTime = quest["endTime"]?.GetValue<long>() ?? 0;
|
||||
|
||||
if (endTime != 0 && quest["changeRequirement"] is null)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\Build.props" />
|
||||
<PropertyGroup>
|
||||
<PackageId>SPTarkov.Server.Core</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Core library for the Single Player Tarkov server.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2026</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
|
||||
@@ -113,7 +113,13 @@ public class BotWeaponModLimitService(ISptLogger<BotWeaponModLimitService> 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<BotWeaponModLimitService> logge
|
||||
/// <param name="currentCount">current number of this item on gun</param>
|
||||
/// <param name="maxLimit">mod limit allowed</param>
|
||||
/// <param name="botRole">role of bot we're checking weapon of</param>
|
||||
/// <param name="modType">OPTIONAL: Type of mod, scope or lightlaser</param>
|
||||
/// <returns>true if limit reached</returns>
|
||||
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<BotWeaponModLimitService> 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;
|
||||
|
||||
@@ -683,14 +683,19 @@ public class CircleOfCultistService(
|
||||
/// <param name="rewardPool">Pool to add items to</param>
|
||||
protected void AddTaskItemRequirementsToRewardPool(PmcData pmcData, HashSet<MongoId> itemRewardBlacklist, HashSet<MongoId> 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))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class JsonUtil
|
||||
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
|
||||
#endif
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
NewLine = "\n",
|
||||
};
|
||||
|
||||
foreach (var registrator in registrators)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="..\..\Build.props" />
|
||||
<PropertyGroup>
|
||||
<PackageId>SPTarkov.Server.Web</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Common shared library for the Single Player Tarkov projects.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2026</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
|
||||
@@ -199,7 +199,7 @@ public class ModValidator(ISptLogger<ModValidator> 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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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<SptServerStartupService>().Startup();
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<PackageId>SPTarkov.Server</PackageId>
|
||||
<Authors>Single Player Tarkov</Authors>
|
||||
<Description>Single Player Tarkov server launcher.</Description>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
|
||||
<Copyright>Copyright (c) Single Player Tarkov 2026</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user