Merge 4.0.3 changes to main (#674)

* Fix exception sometimes thrown on save
- Switch back from File.Rename to File.Move, as Rename is throwing exceptions on some users systems

* Change BTR skin to tarcola during Christmas event

* Added comment

* Remove unused using

* Add wipe Response model

* formatting and add Wipe Endpoint to V2

* Format Style Fixes

* Merge pull request #669 from sp-tarkov/Assembly-ref-validation

Validate core assembly reference when loading mods

* removed zombies from customs and interchange + increased infection across other maps that have zombie kill quests

* Don't apply hostility changes to maps without zombies during halloween

`ReplaceBotHostiltiy` has optional map whitelist param

* Updated hostility values for maps with infection:
bosses = hostile to player not to pmc bots
followers = hostile to player not to pmc bots
pmcs = hostile to player + always hostile to scavs
scavs = hostile to player and pmc bots
raiders = hostile to player and pmc bots

Adjusted infection rates to just maps with zombie kill quests

* Format Style Fixes

* Added missing values for event bosses

* Format Style Fixes

* Added missing values for `ravangezryachiyevent`
Fixed preset typo `bossTagillaAgro`

* Format Style Fixes

* Flagged `Night of The Cult` as halloween quest

* Fixed incorrect logic

* Enabled `Night of The Cult` bosses to spawn

* Format Style Fixes

* Addd a new ReleaseCheckService to notify users of updates (#670)

* Addd a new ReleaseCheckService to notify users of updates
- Pulls the latest release from GitHub API to compare the tag against the users current SPT version
- Runs at the very end of the startup process to avoid being pushed off screen by mod logging
- Only notifies of patch version increments, not major or minor increments
- Links the release notes so users can Ctrl+Click to open directly to the upgrade page
- Is run on its own thread, and discards all errors, so as to not impact users without an internet connection or ability to access GitHub

* Formatting

* Use record for the ReleaseInformation class

---------

Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com>

* ProfileDataService changes:

Added `ClearProfileData()`
Replaced filepath access with `Path.Combine`
Reduced various sources of duplication

* Adjusted `Goons` spawn chance to 20% across `Customs/Lighthouse/Woods/Shoreline`

* Account for compound items in DialogHelper.GetMessageItemContents

* Generate weapon/armor price based on the child item price total

* Added halloween event bosses to april event

* Flagged infected spawns as `ForceSpawn` and ``

* Add migration for invalid pockets

* Default assign IEnumerable

* Post raid effect fixes:
When exiting raid with severe muscle pain, prevent client instructing server to add mild muscle pain
When exiting a raid with effect that has a timer, decrease timer value by amount of time spent in raid

* Updated nuget packages

* Fixed player scav not having correct HP values on limbs #642

* Remove unused record

* Revert "Updated nuget packages"

This reverts commit f6d9d461a6.

* Added `IMP mine detector` to reward and flea blacklist

* Fixed weapon builds not overwriting existing #654

Cleaned up `SaveWeaponBuild` and `SaveEquipmentBuild`

---------

Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com>
Co-authored-by: Chomp <27521899+chompDev@users.noreply.github.com>
Co-authored-by: Chomp <dev@dev.sp-tarkov.com>
Co-authored-by: CWX <CWXDEV@outlook.com>
Co-authored-by: sp-tarkov-bot <singleplayertarkov@gmail.com>
Co-authored-by: Cj <161484149+CJ-SPT@users.noreply.github.com>
Co-authored-by: Tyfon <29051038+tyfon7@users.noreply.github.com>
Co-authored-by: Archangel <jesse@archangel.wtf>
This commit is contained in:
DrakiaXYZ
2025-10-31 14:55:07 -07:00
committed by GitHub
parent 7825b8cb06
commit d2e2f04c93
34 changed files with 1269 additions and 877 deletions
@@ -296,6 +296,20 @@
"minLimitPercent": 15
}
},
"ravangezryachiyevent": {
"armor": {
"maxDelta": 10,
"minDelta": 0,
"minLimitPercent": 15
},
"weapon": {
"highestMax": 100,
"lowestMax": 80,
"maxDelta": 40,
"minDelta": 20,
"minLimitPercent": 15
}
},
"sectantpriest": {
"armor": {
"maxDelta": 10,
@@ -324,6 +338,48 @@
"minLimitPercent": 15
}
},
"sectantoni": {
"armor": {
"maxDelta": 10,
"minDelta": 0,
"minLimitPercent": 15
},
"weapon": {
"highestMax": 100,
"lowestMax": 90,
"maxDelta": 10,
"minDelta": 0,
"minLimitPercent": 15
}
},
"sectantpredvestnik": {
"armor": {
"maxDelta": 10,
"minDelta": 0,
"minLimitPercent": 15
},
"weapon": {
"highestMax": 100,
"lowestMax": 90,
"maxDelta": 10,
"minDelta": 0,
"minLimitPercent": 15
}
},
"sectantprizrak": {
"armor": {
"maxDelta": 10,
"minDelta": 0,
"minLimitPercent": 15
},
"weapon": {
"highestMax": 100,
"lowestMax": 90,
"maxDelta": 10,
"minDelta": 0,
"minLimitPercent": 15
}
},
"shooterbtr": {
"armor": {
"maxDelta": 0,
@@ -2758,7 +2814,7 @@
"bossPartisan": 5,
"bossSanitar": 5,
"bossTagilla": 5,
"bossTagillaAgro": 5,
"bossTagillAagro": 5,
"bossTest": 10,
"bossZryachiy": 5,
"crazyAssaultEvent": 15,
@@ -592,7 +592,8 @@
"64d4b23dc1b37504b41ac2b6",
"678f84bb9e85556ca60f0362",
"67ab3d4b83869afd170fdd3f",
"678fa929819ddc4c350c0317"
"678fa929819ddc4c350c0317",
"5af056f186f7746da511291f"
],
"rewardItemTypeBlacklist": [
"65649eb40bf0ed77b8044453"
@@ -172,7 +172,7 @@
},
"66e3e2ee2136472d220bcb36": {
"name": "Night of The Cult",
"season": "None",
"season": "Halloween",
"startTimestamp": 1341615600000,
"endTimestamp": "",
"yearly": false
@@ -37,7 +37,8 @@
"65702610cfc010a0f5006a41",
"5737250c2459776125652acc",
"657023a9126cc4a57d0e17a6",
"5c1127d0d174af29be75cf68"
"5c1127d0d174af29be75cf68",
"5af056f186f7746da511291f"
],
"customItemCategoryList": [],
"damagedAmmoPacks": true,
@@ -346,6 +347,7 @@
"useTraderPriceForOffersIfHigher": true,
"generateBaseFleaPrices": {
"useHandbookPrice": true,
"generatePresetPriceByChildren": true,
"priceMultiplier": 1.5,
"itemTplMultiplierOverride": {
"5780cf7f2459777de4559322": 1.8
File diff suppressed because it is too large Load Diff
@@ -2124,7 +2124,9 @@
},
"experience": {
"aggressorBonus": {
"normal": 0.05
"normal": 0.05,
"easy": 0.05,
"hard": 0.05
},
"level": {
"max": 1,
@@ -2134,10 +2136,20 @@
"normal": {
"max": 6666,
"min": 6666
},
"easy": {
"max": 6666,
"min": 6666
},
"hard": {
"max": 6666,
"min": 6666
}
},
"standingForKill": {
"normal": -0.2
"normal": -0.2,
"easy": -0.2,
"hard": -0.2
},
"useSimpleAnimator": false
},
@@ -26,13 +26,25 @@
"normal": {
"min": 2100,
"max": 2100
},
"easy": {
"min": 2100,
"max": 2100
},
"hard": {
"min": 2100,
"max": 2100
}
},
"standingForKill": {
"normal": 0
"normal": 0,
"easy": 0,
"hard": 0
},
"aggressorBonus": {
"normal": 0
"normal": 0,
"easy": 0,
"hard": 0
},
"useSimpleAnimator": false
},
@@ -25,13 +25,25 @@
"normal": {
"min": 2300,
"max": 2300
},
"easy": {
"min": 2300,
"max": 2300
},
"hard": {
"min": 2300,
"max": 2300
}
},
"standingForKill": {
"normal": 0
"normal": 0,
"easy": 0,
"hard": 0
},
"aggressorBonus": {
"normal": 0
"normal": 0,
"easy": 0,
"hard": 0
},
"useSimpleAnimator": false
},
@@ -26,13 +26,25 @@
"normal": {
"min": 2100,
"max": 2100
},
"easy": {
"min": 2100,
"max": 2100
},
"hard": {
"min": 2100,
"max": 2100
}
},
"standingForKill": {
"normal": 0
"normal": 0,
"easy": 0,
"hard": 0
},
"aggressorBonus": {
"normal": 0
"normal": 0,
"easy": 0,
"hard": 0
},
"useSimpleAnimator": false
},
@@ -138,7 +138,7 @@
"TriggerName": ""
},
{
"BossChance": 18,
"BossChance": 20,
"BossDifficult": "normal",
"BossEscortAmount": "2",
"BossEscortDifficult": "normal",
@@ -84,7 +84,7 @@
"TriggerName": ""
},
{
"BossChance": 50,
"BossChance": 20,
"BossDifficult": "normal",
"BossEscortAmount": "2",
"BossEscortDifficult": "normal",
@@ -110,7 +110,7 @@
"TriggerName": ""
},
{
"BossChance": 25,
"BossChance": 20,
"BossDifficult": "normal",
"BossEscortAmount": "0",
"BossEscortDifficult": "normal",
@@ -1,5 +1,6 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Controllers;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Launcher;
using SPTarkov.Server.Core.Models.Spt.Launcher;
using SPTarkov.Server.Core.Utils;
@@ -71,10 +72,19 @@ public class LauncherV2Callbacks(
);
}
public ValueTask<string> Profile(LoginRequestData sessionId)
public ValueTask<string> Profile(LoginRequestData username)
{
return new ValueTask<string>(
httpResponseUtil.NoBody(new LauncherV2ProfileResponse { Response = launcherV2Controller.GetMiniProfileFromUsername(sessionId) })
httpResponseUtil.NoBody(new LauncherV2ProfileResponse { Response = launcherV2Controller.GetMiniProfileFromUsername(username) })
);
}
public ValueTask<string> Wipe(RegisterData info)
{
return new ValueTask<string>(
httpResponseUtil.NoBody(
new LauncherV2WipeResponse { Response = launcherV2Controller.Wipe(info), Profiles = profileController.GetMiniProfiles() }
)
);
}
}
@@ -82,11 +82,11 @@ public class BuildController(
/// <param name="request"></param>
public void SaveWeaponBuild(MongoId sessionId, PresetBuildActionRequestData request)
{
var pmcData = profileHelper.GetPmcProfile(sessionId);
var profile = profileHelper.GetFullProfile(sessionId);
// Replace duplicate Id's. The first item is the base item.
// The root ID and the base item ID need to match.
request.Items = itemHelper.ReplaceIDs(request.Items, pmcData);
request.Items = itemHelper.ReplaceIDs(request.Items, profile.CharacterData.PmcData);
request.Root = request.Items.FirstOrDefault().Id;
// Create new object ready to save into profile userbuilds.weaponBuilds
@@ -98,15 +98,13 @@ public class BuildController(
Items = request.Items.ToList(),
};
var profile = profileHelper.GetFullProfile(sessionId);
var savedWeaponBuilds = profile.UserBuildData.WeaponBuilds;
var existingBuild = savedWeaponBuilds.FirstOrDefault(x => x.Id == request.Id);
var existingBuild = savedWeaponBuilds.FirstOrDefault(build => build.Name == request.Name || build.Id == request.Id);
if (existingBuild is not null)
{
// exists, replace
profile.UserBuildData.WeaponBuilds.Remove(existingBuild);
profile.UserBuildData.WeaponBuilds.Add(existingBuild);
profile.UserBuildData.WeaponBuilds.Add(newBuild);
}
else
{
@@ -123,13 +121,12 @@ public class BuildController(
public void SaveEquipmentBuild(MongoId sessionID, PresetBuildActionRequestData request)
{
var profile = profileHelper.GetFullProfile(sessionID);
var pmcData = profile.CharacterData.PmcData;
var existingSavedEquipmentBuilds = saveServer.GetProfile(sessionID).UserBuildData.EquipmentBuilds;
// Replace duplicate ID's. The first item is the base item.
// Root ID and the base item ID need to match.
request.Items = itemHelper.ReplaceIDs(request.Items, pmcData);
request.Items = itemHelper.ReplaceIDs(request.Items, profile.CharacterData.PmcData);
var newBuild = new EquipmentBuild
{
@@ -8,6 +8,7 @@ using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Services;
using SPTarkov.Server.Core.Services.Mod;
using SPTarkov.Server.Core.Utils;
using Info = SPTarkov.Server.Core.Models.Eft.Profile.Info;
@@ -22,6 +23,7 @@ public class LauncherController(
ProfileHelper profileHelper,
DatabaseService databaseService,
ServerLocalisationService serverLocalisationService,
ProfileDataService profileDataService,
ConfigServer configServer
)
{
@@ -168,6 +170,9 @@ public class LauncherController(
var profileInfo = saveServer.GetProfile(sessionId).ProfileInfo;
profileInfo!.Edition = info.Edition;
profileInfo.IsWiped = true;
// Clear any data modders may have stored
profileDataService.ClearProfileData(sessionId);
}
return sessionId;
@@ -173,4 +173,27 @@ public class LauncherV2Controller(
{
return profileController.GetMiniProfile(GetSessionId(info));
}
public bool Wipe(RegisterData info)
{
if (!CoreConfig.AllowProfileWipe)
{
return false;
}
var sessionId = Login(info);
if (!sessionId)
{
var profileInfo = saveServer
.GetProfiles()
.FirstOrDefault(x => x.Value.ProfileInfo?.Username == info.Username)
.Value.ProfileInfo;
profileInfo!.Edition = info.Edition;
profileInfo.IsWiped = true;
}
return sessionId;
}
}
@@ -70,6 +70,12 @@ public class BotGenerator(
bot = GenerateBot(sessionId, bot, botTemplate, botGenDetails);
// Pscavs in live have same limb hp as their pmc character
if (profile?.Health?.BodyParts is not null)
{
CopyLimbHpValuesToBot(bot, profile.Health.BodyParts);
}
// Sets the name after scav name shown in parentheses
bot.Info.MainProfileNickname = profile.Info.Nickname;
@@ -104,10 +110,19 @@ public class BotGenerator(
WishList = bot.WishList,
MoneyTransferLimitData = bot.MoneyTransferLimitData,
IsPmc = bot.IsPmc,
Prestige = new Dictionary<string, long>(),
Prestige = [],
};
}
protected void CopyLimbHpValuesToBot(BotBase bot, Dictionary<string, BodyPartHealth> bodyParts)
{
foreach (var (partName, partProperties) in bodyParts)
{
bot.Health.BodyParts[partName].Health.Maximum = partProperties.Health.Maximum;
bot.Health.BodyParts[partName].Health.Current = bot.Health.BodyParts[partName].Health.Maximum;
}
}
/// <summary>
/// Create 1 bot of the type/side/difficulty defined in botGenerationDetails
/// </summary>
@@ -473,7 +473,7 @@ public class RagfairOfferGenerator(
// Not barter or pack offer
// Apply randomised properties
RandomiseOfferItemUpdProperties(sellerId, itemWithChildren, itemToSellDetails, offerCreator);
barterScheme = CreateCurrencyBarterScheme(itemWithChildren, isPackOffer);
barterScheme = CreateCurrencyBarterScheme(itemWithChildren, false);
}
var createOfferDetails = new CreateFleaOfferDetails
@@ -487,6 +487,7 @@ public class RagfairOfferGenerator(
Creator = offerCreator,
SellInOnePiece = isPackOffer, // sellAsOnePiece - pack offer
};
CreateAndAddFleaOffer(createOfferDetails);
}
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Eft.Profile;
@@ -67,9 +68,10 @@ public class DialogueHelper(ISptLogger<DialogueHelper> logger, ProfileHelper pro
message.Items ??= new MessageItems();
message.Items.Data ??= [];
// Check reward count when item being moved isn't in reward list
// Check reward count when item being moved (and its children) isn't in reward list
// If count is 0, it means after this move occurs the reward array will be empty and all rewards collected
var remainingItems = message.Items.Data.Where(x => x.Id != itemId);
var itemWithChildren = message.Items.Data.GetItemWithChildren(itemId);
var remainingItems = message.Items.Data.Except(itemWithChildren);
if (!remainingItems.Any())
{
message.RewardCollected = true;
@@ -3,7 +3,6 @@ using SPTarkov.Server.Core.Exceptions.Helpers;
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.Health;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers;
@@ -158,18 +157,39 @@ public class HealthHelper(ISptLogger<HealthHelper> logger, TimeUtil timeUtil, Co
// Process each effect for each part
foreach (var (key, effectDetails) in partProperties.Effects ?? [])
{
// Null guard
matchingProfilePart.Effects ??= new Dictionary<string, BodyPartEffectProperties?>();
// Have effects we need to add, init effect array
matchingProfilePart.Effects ??= [];
// Effect already exists on limb in server profile, skip
if (
key.Equals("MildMusclePain", StringComparison.OrdinalIgnoreCase)
&& matchingProfilePart.Effects.ContainsKey("SevereMusclePain")
)
{
// Edge case - client is trying to add mild pain when server already has severe, don't allow this
continue;
}
// Effect on limb already exists in server profile, handle differently
if (matchingProfilePart.Effects.ContainsKey(key))
{
// Edge case - effect already exists at destination, but we don't want to overwrite details
matchingProfilePart.Effects.TryGetValue(key, out var matchingEffectOnServer);
// Edge case - effect already exists at destination, but we don't want to overwrite details e.g. Exhaustion
if (effectsToSkip is not null && effectsToSkip.Contains(key))
{
matchingProfilePart.Effects[key] = null;
}
// Effect time has decreased while in raid, persist this reduction into profile
if (
effectDetails?.Time is not null
&& matchingEffectOnServer?.Time is not null
&& effectDetails.Time < matchingEffectOnServer.Time
)
{
matchingEffectOnServer.Time = effectDetails.Time;
}
continue;
}
@@ -6,7 +6,7 @@ namespace SPTarkov.Server.Core.Migration;
public abstract class AbstractProfileMigration : IProfileMigration
{
public virtual string MigrationName { get; }
public virtual IEnumerable<Type> PrerequisiteMigrations { get; }
public virtual IEnumerable<Type> PrerequisiteMigrations { get; } = [];
public abstract string FromVersion { get; }
public abstract string ToVersion { get; }
@@ -0,0 +1,279 @@
using System.Text.Json.Nodes;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Enums;
using SPTarkov.Server.Core.Services;
namespace SPTarkov.Server.Core.Migration.Migrations;
[Injectable]
public class InvalidPocketFix(DatabaseService databaseService) : AbstractProfileMigration
{
public const string DEFAULT_POCKETS = "627a4e6b255f7527fb05a0f6";
public const string UNHEARD_POCKETS = "65e080be269cbd5c5005e529";
public override string FromVersion
{
get { return "~4.0"; }
}
public override string ToVersion
{
get { return "~4.0"; }
}
public override string MigrationName
{
get { return "InvalidPocketFix"; }
}
private enum PocketStatus
{
Valid,
Missing,
Invalid,
}
private PocketStatus GetPmcPocketStatus(JsonObject profile)
{
if (profile["characters"]?["pmc"]?["Inventory"]?["items"] is not JsonArray items)
{
// Uninitialized profile, just pass valid
return PocketStatus.Valid;
}
foreach (var itemNode in items)
{
if (itemNode is not JsonObject itemObj)
{
continue;
}
if (
itemObj.TryGetPropertyValue("slotId", out var slotNode)
&& slotNode is JsonValue slotValue
&& slotValue.TryGetValue<string>(out var slotId)
&& slotId == "Pockets"
)
{
if (
itemObj.TryGetPropertyValue("_tpl", out var tplNode)
&& tplNode is JsonValue tplValue
&& tplValue.TryGetValue<string>(out var template)
)
{
return databaseService.GetItems().ContainsKey(template) ? PocketStatus.Valid : PocketStatus.Invalid;
}
}
}
return PocketStatus.Missing;
}
private PocketStatus GetScavPocketStatus(JsonObject profile)
{
if (profile["characters"]?["scav"]?["Inventory"]?["items"] is not JsonArray items)
{
// Uninitialized profile, just pass valid
return PocketStatus.Valid;
}
foreach (var itemNode in items)
{
if (itemNode is not JsonObject itemObj)
{
continue;
}
if (
itemObj.TryGetPropertyValue("slotId", out var slotNode)
&& slotNode is JsonValue slotValue
&& slotValue.TryGetValue<string>(out var slotId)
&& slotId == "Pockets"
)
{
if (
itemObj.TryGetPropertyValue("_tpl", out var tplNode)
&& tplNode is JsonValue tplValue
&& tplValue.TryGetValue<string>(out var template)
)
{
return databaseService.GetItems().ContainsKey(template) ? PocketStatus.Valid : PocketStatus.Invalid;
}
}
}
return PocketStatus.Missing;
}
private bool HasCompletedOldPatterns(JsonObject profile)
{
if (profile["characters"]?["pmc"]?["Quests"] is not JsonArray quests)
{
return false;
}
foreach (var questNode in quests)
{
if (questNode is not JsonObject questObj)
{
continue;
}
if (
questObj.TryGetPropertyValue("qid", out var qIdNode)
&& qIdNode is JsonValue qIdValue
&& qIdValue.TryGetValue<string>(out var qId)
&& qId == QuestTpl.OLD_PATTERNS.ToString()
&& questObj.TryGetPropertyValue("status", out var statusNode)
&& statusNode is JsonValue statusValue
&& statusValue.TryGetValue<string>(out var status)
&& status.Equals(nameof(QuestStatusEnum.Success), StringComparison.OrdinalIgnoreCase)
)
{
return true;
}
}
return false;
}
private bool IsUnheardProfile(JsonObject profile)
{
var gameVersion = profile?["characters"]?["pmc"]?["Info"]?["GameVersion"]?.GetValue<string>();
if (!string.IsNullOrEmpty(gameVersion))
{
return gameVersion.Equals("unheard_edition", StringComparison.OrdinalIgnoreCase);
}
return false;
}
private JsonObject CreatePocketItem(string parentId, string defaultPocketTpl)
{
return new JsonObject
{
["_id"] = new MongoId().ToString(),
["_tpl"] = defaultPocketTpl,
["parentId"] = parentId,
["slotId"] = "Pockets",
};
}
// Set slotId to hideout, parentId to sorting table & remove location so that the sorting table will automatically pick a location
private void MoveItemsToSortingTable(JsonArray items, string sortingTableId)
{
foreach (var item in items.OfType<JsonObject>())
{
if (
item.TryGetPropertyValue("slotId", out var slotNode)
&& slotNode is JsonValue slotNodeValue
&& slotNodeValue.TryGetValue<string>(out var slotId)
&& (
(
slotId.StartsWith("pocket", StringComparison.OrdinalIgnoreCase)
// Exclude the pcokets itself
&& !slotId.Equals("Pockets", StringComparison.OrdinalIgnoreCase)
)
// Special slots are also keyed to the pockets
|| slotId.StartsWith("SpecialSlot", StringComparison.OrdinalIgnoreCase)
)
)
{
item["slotId"] = "hideout";
item["parentId"] = sortingTableId;
item.Remove("location");
}
}
}
public override bool CanMigrate(JsonObject profile, IEnumerable<IProfileMigration> previouslyRanMigrations)
{
if (GetPmcPocketStatus(profile) != PocketStatus.Valid || GetScavPocketStatus(profile) != PocketStatus.Valid)
{
return true;
}
return false;
}
public override JsonObject? Migrate(JsonObject profile)
{
var pmcPocketStatus = GetPmcPocketStatus(profile);
var scavPocketStatus = GetScavPocketStatus(profile);
if (pmcPocketStatus != PocketStatus.Valid)
{
var items = profile["characters"]?["pmc"]?["Inventory"]?["items"] as JsonArray;
var pmcInventory = profile["characters"]?["pmc"]?["Inventory"] as JsonObject;
var pmcSortingTable = pmcInventory?["sortingTable"]?.GetValue<string>()!;
var pmcEquipment = pmcInventory?["equipment"]?.GetValue<string>();
var pmcPocketTpl = DEFAULT_POCKETS;
if (IsUnheardProfile(profile) || HasCompletedOldPatterns(profile))
{
pmcPocketTpl = UNHEARD_POCKETS;
}
if (pmcPocketStatus == PocketStatus.Missing)
{
if (items != null && pmcEquipment != null)
{
items.Add(CreatePocketItem(pmcEquipment, pmcPocketTpl));
MoveItemsToSortingTable(items, pmcSortingTable);
}
}
else if (pmcPocketStatus == PocketStatus.Invalid)
{
foreach (var item in items.OfType<JsonObject>())
{
if (
item.TryGetPropertyValue("slotId", out var slotNode)
&& slotNode is JsonValue slotNodeValue
&& slotNodeValue.TryGetValue<string>(out var slotId)
&& slotId == "Pockets"
)
{
item["_tpl"] = pmcPocketTpl;
MoveItemsToSortingTable(items, pmcSortingTable);
}
}
}
}
if (scavPocketStatus != PocketStatus.Valid)
{
var scavItems = profile["characters"]?["scav"]?["Inventory"]?["items"] as JsonArray;
var scavInventory = profile["characters"]?["scav"]?["Inventory"] as JsonObject;
var scavEquipment = scavInventory?["equipment"]?.GetValue<string>();
if (scavPocketStatus == PocketStatus.Missing)
{
if (scavItems != null && scavEquipment != null)
{
scavItems.Add(CreatePocketItem(scavEquipment, DEFAULT_POCKETS));
}
}
else if (scavPocketStatus == PocketStatus.Invalid)
{
foreach (var item in scavItems.OfType<JsonObject>())
{
if (
item.TryGetPropertyValue("slotId", out var slotNode)
&& slotNode is JsonValue slotNodeValue
&& slotNodeValue.TryGetValue<string>(out var slotId)
&& slotId == "Pockets"
)
{
item["_tpl"] = DEFAULT_POCKETS;
}
}
}
}
return base.Migrate(profile);
}
}
@@ -2929,6 +2929,9 @@ public record BTRSettings
public record BtrMapConfig
{
/// <summary>
/// Known values: Tarcola, Cleare, Dirt, HeavyDirt
/// </summary>
[JsonPropertyName("BtrSkin")]
public string BtrSkin { get; set; }
@@ -1,399 +0,0 @@
using System.Text.Json.Serialization;
namespace SPTarkov.Server.Core.Models.Eft.Common.Tables;
public record BotCore
{
[JsonPropertyName("SAVAGE_KILL_DIST")]
public double? SavageKillDistance { get; set; }
[JsonPropertyName("SOUND_DOOR_BREACH_METERS")]
public double? SoundDoorBreachMeters { get; set; }
[JsonPropertyName("SOUND_DOOR_OPEN_METERS")]
public double? SoundDoorOpenMeters { get; set; }
[JsonPropertyName("STEP_NOISE_DELTA")]
public double? StepNoiseDelta { get; set; }
[JsonPropertyName("JUMP_NOISE_DELTA")]
public double? JumpNoiseDelta { get; set; }
[JsonPropertyName("GUNSHOT_SPREAD")]
public double? GunshotSpread { get; set; }
[JsonPropertyName("GUNSHOT_SPREAD_SILENCE")]
public double? GunshotSpreadSilence { get; set; }
[JsonPropertyName("BASE_WALK_SPEREAD2")]
public double? BaseWalkSpread2 { get; set; }
[JsonPropertyName("MOVE_SPEED_COEF_MAX")]
public double? MoveSpeedCoefficientMax { get; set; }
[JsonPropertyName("SPEED_SERV_SOUND_COEF_A")]
public double? SpeedServiceSoundCoefficientA { get; set; }
[JsonPropertyName("SPEED_SERV_SOUND_COEF_B")]
public double? SpeedServiceSoundCoefficientB { get; set; }
[JsonPropertyName("G")]
public double? Gravity { get; set; }
[JsonPropertyName("STAY_COEF")]
public double? StayCoefficient { get; set; }
[JsonPropertyName("SIT_COEF")]
public double? SitCoefficient { get; set; }
[JsonPropertyName("LAY_COEF")]
public double? LayCoefficient { get; set; }
[JsonPropertyName("MAX_ITERATIONS")]
public double? MaxIterations { get; set; }
[JsonPropertyName("START_DIST_TO_COV")]
public double? StartDistanceToCover { get; set; }
[JsonPropertyName("MAX_DIST_TO_COV")]
public double? MaxDistanceToCover { get; set; }
[JsonPropertyName("STAY_HEIGHT")]
public double? StayHeight { get; set; }
[JsonPropertyName("CLOSE_POINTS")]
public double? ClosePoints { get; set; }
[JsonPropertyName("COUNT_TURNS")]
public double? CountTurns { get; set; }
[JsonPropertyName("SIMPLE_POINT_LIFE_TIME_SEC")]
public double? SimplePointLifetimeSeconds { get; set; }
[JsonPropertyName("DANGER_POINT_LIFE_TIME_SEC")]
public double? DangerPointLifetimeSeconds { get; set; }
[JsonPropertyName("DANGER_POWER")]
public double? DangerPower { get; set; }
[JsonPropertyName("COVER_DIST_CLOSE")]
public double? CoverDistanceClose { get; set; }
[JsonPropertyName("GOOD_DIST_TO_POINT")]
public double? GoodDistanceToPoint { get; set; }
[JsonPropertyName("COVER_TOOFAR_FROM_BOSS")]
public double? CoverTooFarFromBoss { get; set; }
[JsonPropertyName("COVER_TOOFAR_FROM_BOSS_SQRT")]
public double? CoverTooFarFromBossSqrt { get; set; }
[JsonPropertyName("MAX_Y_DIFF_TO_PROTECT")]
public double? MaxYDifferenceToProtect { get; set; }
[JsonPropertyName("FLARE_POWER")]
public double? FlarePower { get; set; }
[JsonPropertyName("MOVE_COEF")]
public double? MoveCoefficient { get; set; }
[JsonPropertyName("PRONE_POSE")]
public double? PronePose { get; set; }
[JsonPropertyName("LOWER_POSE")]
public double? LowerPose { get; set; }
[JsonPropertyName("MAX_POSE")]
public double? MaxPose { get; set; }
[JsonPropertyName("FLARE_TIME")]
public double? FlareTime { get; set; }
[JsonPropertyName("MAX_REQUESTS__PER_GROUP")]
public double? MaxRequestsPerGroup { get; set; }
[JsonPropertyName("UPDATE_GOAL_TIMER_SEC")]
public double? UpdateGoalTimerSeconds { get; set; }
[JsonPropertyName("DIST_NOT_TO_GROUP")]
public double? DistanceNotToGroup { get; set; }
[JsonPropertyName("DIST_NOT_TO_GROUP_SQR")]
public double? DistanceNotToGroupSquared { get; set; }
[JsonPropertyName("LAST_SEEN_POS_LIFETIME")]
public double? LastSeenPositionLifetime { get; set; }
[JsonPropertyName("DELTA_GRENADE_START_TIME")]
public double? DeltaGrenadeStartTime { get; set; }
[JsonPropertyName("DELTA_GRENADE_END_TIME")]
public double? DeltaGrenadeEndTime { get; set; }
[JsonPropertyName("DELTA_GRENADE_RUN_DIST")]
public double? DeltaGrenadeRunDistance { get; set; }
[JsonPropertyName("DELTA_GRENADE_RUN_DIST_SQRT")]
public double? DeltaGrenadeRunDistanceSqrt { get; set; }
[JsonPropertyName("PATROL_MIN_LIGHT_DIST")]
public double? PatrolMinimumLightDistance { get; set; }
[JsonPropertyName("HOLD_MIN_LIGHT_DIST")]
public double? HoldMinimumLightDistance { get; set; }
[JsonPropertyName("STANDART_BOT_PAUSE_DOOR")]
public double? StandardBotPauseDoor { get; set; }
[JsonPropertyName("ARMOR_CLASS_COEF")]
public double? ArmorClassCoefficient { get; set; }
[JsonPropertyName("SHOTGUN_POWER")]
public double? ShotgunPower { get; set; }
[JsonPropertyName("RIFLE_POWER")]
public double? RiflePower { get; set; }
[JsonPropertyName("PISTOL_POWER")]
public double? PistolPower { get; set; }
[JsonPropertyName("SMG_POWER")]
public double? SMGPower { get; set; }
[JsonPropertyName("SNIPE_POWER")]
public double? SniperPower { get; set; }
[JsonPropertyName("GESTUS_PERIOD_SEC")]
public double? GestusPeriodSeconds { get; set; }
[JsonPropertyName("GESTUS_AIMING_DELAY")]
public double? GestusAimingDelay { get; set; }
[JsonPropertyName("GESTUS_REQUEST_LIFETIME")]
public double? GestusRequestLifetime { get; set; }
[JsonPropertyName("GESTUS_FIRST_STAGE_MAX_TIME")]
public double? GestusFirstStageMaxTime { get; set; }
[JsonPropertyName("GESTUS_SECOND_STAGE_MAX_TIME")]
public double? GestusSecondStageMaxTime { get; set; }
[JsonPropertyName("GESTUS_MAX_ANSWERS")]
public double? GestusMaxAnswers { get; set; }
[JsonPropertyName("GESTUS_FUCK_TO_SHOOT")]
public double? GestusFuckToShoot { get; set; }
[JsonPropertyName("GESTUS_DIST_ANSWERS")]
public double? GestusDistanceAnswers { get; set; }
[JsonPropertyName("GESTUS_DIST_ANSWERS_SQRT")]
public double? GestusDistanceAnswersSqrt { get; set; }
[JsonPropertyName("GESTUS_ANYWAY_CHANCE")]
public double? GestusAnywayChance { get; set; }
[JsonPropertyName("TALK_DELAY")]
public double? TalkDelay { get; set; }
[JsonPropertyName("CAN_SHOOT_TO_HEAD")]
public bool? CanShootToHead { get; set; }
[JsonPropertyName("CAN_TILT")]
public bool? CanTilt { get; set; }
[JsonPropertyName("TILT_CHANCE")]
public double? TiltChance { get; set; }
[JsonPropertyName("MIN_BLOCK_DIST")]
public double? MinimumBlockDistance { get; set; }
[JsonPropertyName("MIN_BLOCK_TIME")]
public double? MinimumBlockTime { get; set; }
[JsonPropertyName("COVER_SECONDS_AFTER_LOSE_VISION")]
public double? CoverSecondsAfterLoseVision { get; set; }
[JsonPropertyName("MIN_ARG_COEF")]
public double? MinimumArgumentCoefficient { get; set; }
[JsonPropertyName("MAX_ARG_COEF")]
public double? MaximumArgumentCoefficient { get; set; }
[JsonPropertyName("DEAD_AGR_DIST")]
public double? DeadAgrDistance { get; set; }
[JsonPropertyName("MAX_DANGER_CARE_DIST_SQRT")]
public double? MaxDangerCareDistanceSqrt { get; set; }
[JsonPropertyName("MAX_DANGER_CARE_DIST")]
public double? MaxDangerCareDistance { get; set; }
[JsonPropertyName("MIN_MAX_PERSON_SEARCH")]
public double? MinimumMaximumPersonSearch { get; set; }
[JsonPropertyName("PERCENT_PERSON_SEARCH")]
public double? PercentPersonSearch { get; set; }
[JsonPropertyName("LOOK_ANYSIDE_BY_WALL_SEC_OF_ENEMY")]
public double? LookAnySideByWallSecondsOfEnemy { get; set; }
[JsonPropertyName("CLOSE_TO_WALL_ROTATE_BY_WALL_SQRT")]
public double? CloseToWallRotateByWallSqrt { get; set; }
[JsonPropertyName("SHOOT_TO_CHANGE_RND_PART_MIN")]
public double? ShootToChangeRandomPartMinimum { get; set; }
[JsonPropertyName("SHOOT_TO_CHANGE_RND_PART_MAX")]
public double? ShootToChangeRandomPartMaximum { get; set; }
[JsonPropertyName("SHOOT_TO_CHANGE_RND_PART_DELTA")]
public double? ShootToChangeRandomPartDelta { get; set; }
[JsonPropertyName("FORMUL_COEF_DELTA_DIST")]
public double? FormulaCoefficientDeltaDistance { get; set; }
[JsonPropertyName("FORMUL_COEF_DELTA_SHOOT")]
public double? FormulaCoefficientDeltaShoot { get; set; }
[JsonPropertyName("FORMUL_COEF_DELTA_FRIEND_COVER")]
public double? FormulaCoefficientDeltaFriendCover { get; set; }
[JsonPropertyName("SUSPETION_POINT_DIST_CHECK")]
public double? SuspicionPointDistanceCheck { get; set; }
[JsonPropertyName("MAX_BASE_REQUESTS_PER_PLAYER")]
public double? MaxBaseRequestsPerPlayer { get; set; }
[JsonPropertyName("MAX_HOLD_REQUESTS_PER_PLAYER")]
public double? MaxHoldRequestsPerPlayer { get; set; }
[JsonPropertyName("MAX_GO_TO_REQUESTS_PER_PLAYER")]
public double? MaxGoToRequestsPerPlayer { get; set; }
[JsonPropertyName("MAX_COME_WITH_ME_REQUESTS_PER_PLAYER")]
public double? MaxComeWithMeRequestsPerPlayer { get; set; }
[JsonPropertyName("CORE_POINT_MAX_VALUE")]
public double? CorePointMaxValue { get; set; }
[JsonPropertyName("CORE_POINTS_MAX")]
public double? CorePointsMax { get; set; }
[JsonPropertyName("CORE_POINTS_MIN")]
public double? CorePointsMin { get; set; }
[JsonPropertyName("BORN_POISTS_FREE_ONLY_FAREST_BOT")]
public bool? BornPointsFreeOnlyFarthestBot { get; set; }
[JsonPropertyName("BORN_POINSTS_FREE_ONLY_FAREST_PLAYER")]
public bool? BornPointsFreeOnlyFarthestPlayer { get; set; }
[JsonPropertyName("SCAV_GROUPS_TOGETHER")]
public bool? ScavGroupsTogether { get; set; }
[JsonPropertyName("LAY_DOWN_ANG_SHOOT")]
public double? LayDownAngleShoot { get; set; }
[JsonPropertyName("HOLD_REQUEST_TIME_SEC")]
public double? HoldRequestTimeSeconds { get; set; }
[JsonPropertyName("TRIGGERS_DOWN_TO_RUN_WHEN_MOVE")]
public double? TriggersDownToRunWhenMove { get; set; }
[JsonPropertyName("MIN_DIST_TO_RUN_WHILE_ATTACK_MOVING")]
public double? MinimumDistanceToRunWhileAttackingMoving { get; set; }
[JsonPropertyName("MIN_DIST_TO_RUN_WHILE_ATTACK_MOVING_OTHER_ENEMIS")]
public double? MinimumDistanceToRunWhileAttackingMovingOtherEnemies { get; set; }
[JsonPropertyName("MIN_DIST_TO_STOP_RUN")]
public double? MinimumDistanceToStopRunning { get; set; }
[JsonPropertyName("JUMP_SPREAD_DIST")]
public double? JumpSpreadDistance { get; set; }
[JsonPropertyName("LOOK_TIMES_TO_KILL")]
public double? LookTimesToKill { get; set; }
[JsonPropertyName("COME_INSIDE_TIMES")]
public double? ComeInsideTimes { get; set; }
[JsonPropertyName("TOTAL_TIME_KILL")]
public double? TotalTimeKill { get; set; }
[JsonPropertyName("TOTAL_TIME_KILL_AFTER_WARN")]
public double? TotalTimeKillAfterWarning { get; set; }
[JsonPropertyName("MOVING_AIM_COEF")]
public double? MovingAimCoefficient { get; set; }
[JsonPropertyName("VERTICAL_DIST_TO_IGNORE_SOUND")]
public double? VerticalDistanceToIgnoreSound { get; set; }
[JsonPropertyName("DEFENCE_LEVEL_SHIFT")]
public double? DefenseLevelShift { get; set; }
[JsonPropertyName("MIN_DIST_CLOSE_DEF")]
public double? MinimumDistanceCloseDefense { get; set; }
[JsonPropertyName("USE_ID_PRIOR_WHO_GO")]
public bool? UseIdPriorWhoGoes { get; set; }
[JsonPropertyName("SMOKE_GRENADE_RADIUS_COEF")]
public double? SmokeGrenadeRadiusCoefficient { get; set; }
[JsonPropertyName("GRENADE_PRECISION")]
public double? GrenadePrecision { get; set; }
[JsonPropertyName("MAX_WARNS_BEFORE_KILL")]
public double? MaxWarningsBeforeKill { get; set; }
[JsonPropertyName("CARE_ENEMY_ONLY_TIME")]
public double? CareEnemyOnlyTime { get; set; }
[JsonPropertyName("MIDDLE_POINT_COEF")]
public double? MiddlePointCoefficient { get; set; }
[JsonPropertyName("MAIN_TACTIC_ONLY_ATTACK")]
public bool? MainTacticOnlyAttack { get; set; }
[JsonPropertyName("LAST_DAMAGE_ACTIVE")]
public double? LastDamageActive { get; set; }
[JsonPropertyName("SHALL_DIE_IF_NOT_INITED")]
public bool? ShallDieIfNotInitialized { get; set; }
[JsonPropertyName("CHECK_BOT_INIT_TIME_SEC")]
public double? CheckBotInitializationTimeSeconds { get; set; }
[JsonPropertyName("WEAPON_ROOT_Y_OFFSET")]
public double? WeaponRootYOffset { get; set; }
[JsonPropertyName("DELTA_SUPRESS_DISTANCE_SQRT")]
public double? DeltaSuppressDistanceSqrt { get; set; }
[JsonPropertyName("DELTA_SUPRESS_DISTANCE")]
public double? DeltaSuppressDistance { get; set; }
[JsonPropertyName("WAVE_COEF_LOW")]
public double? WaveCoefficientLow { get; set; }
[JsonPropertyName("WAVE_COEF_MID")]
public double? WaveCoefficientMid { get; set; }
[JsonPropertyName("WAVE_COEF_HIGH")]
public double? WaveCoefficientHigh { get; set; }
[JsonPropertyName("WAVE_COEF_HORDE")]
public double? WaveCoefficientHorde { get; set; }
[JsonPropertyName("WAVE_ONLY_AS_ONLINE")]
public bool? WaveOnlyAsOnline { get; set; }
[JsonPropertyName("LOCAL_BOTS_COUNT")]
public double? LocalBotsCount { get; set; }
[JsonPropertyName("AXE_MAN_KILLS_END")]
public double? AxeManKillsEnd { get; set; }
}
@@ -269,6 +269,12 @@ public record GenerateFleaPrices
[JsonPropertyName("hideoutCraftMultiplier")]
public double HideoutCraftMultiplier { get; set; }
/// <summary>
/// Should weapons/armors have their price generated by totalling its child items
/// </summary>
[JsonPropertyName("generatePresetPriceByChildren")]
public bool GeneratePresetPriceByChildren { get; set; }
}
public record PriceRanges
@@ -1,4 +1,3 @@
using System.Text.Json.Serialization;
using SPTarkov.Server.Core.Models.Eft.Launcher;
using SPTarkov.Server.Core.Models.Utils;
@@ -0,0 +1,11 @@
using SPTarkov.Server.Core.Models.Eft.Launcher;
using SPTarkov.Server.Core.Models.Utils;
namespace SPTarkov.Server.Core.Models.Spt.Launcher;
public record LauncherV2WipeResponse : IRequestData
{
public required bool Response { get; set; }
public required List<MiniProfile> Profiles { get; set; }
}
@@ -12,32 +12,15 @@ public class LauncherV2StaticRouter(LauncherV2Callbacks launcherV2Callbacks, Jso
: StaticRouter(
jsonUtil,
[
new RouteAction<EmptyRequestData>("/launcher/v2/ping", async (url, _, sessionID, _) => await launcherV2Callbacks.Ping()),
new RouteAction<EmptyRequestData>("/launcher/v2/types", async (url, _, sessionID, _) => await launcherV2Callbacks.Types()),
new RouteAction<LoginRequestData>(
"/launcher/v2/login",
async (url, info, sessionID, _) => await launcherV2Callbacks.Login(info)
),
new RouteAction<RegisterData>(
"/launcher/v2/register",
async (url, info, sessionID, _) => await launcherV2Callbacks.Register(info)
),
new RouteAction<LoginRequestData>(
"/launcher/v2/remove",
async (url, info, sessionID, _) => await launcherV2Callbacks.Remove(info)
),
new RouteAction<EmptyRequestData>(
"/launcher/v2/version",
async (url, _, sessionID, _) => await launcherV2Callbacks.CompatibleVersion()
),
new RouteAction<EmptyRequestData>("/launcher/v2/mods", async (url, _, sessionID, _) => await launcherV2Callbacks.Mods()),
new RouteAction<EmptyRequestData>(
"/launcher/v2/profiles",
async (url, _, sessionID, _) => await launcherV2Callbacks.Profiles()
),
new RouteAction<LoginRequestData>(
"/launcher/v2/profile",
async (url, info, sessionID, _) => await launcherV2Callbacks.Profile(info)
),
new RouteAction<EmptyRequestData>("/launcher/v2/ping", async (_, _, _, _) => await launcherV2Callbacks.Ping()),
new RouteAction<EmptyRequestData>("/launcher/v2/types", async (_, _, _, _) => await launcherV2Callbacks.Types()),
new RouteAction<LoginRequestData>("/launcher/v2/login", async (_, info, _, _) => await launcherV2Callbacks.Login(info)),
new RouteAction<RegisterData>("/launcher/v2/register", async (_, info, _, _) => await launcherV2Callbacks.Register(info)),
new RouteAction<LoginRequestData>("/launcher/v2/remove", async (_, info, _, _) => await launcherV2Callbacks.Remove(info)),
new RouteAction<EmptyRequestData>("/launcher/v2/version", async (_, _, _, _) => await launcherV2Callbacks.CompatibleVersion()),
new RouteAction<EmptyRequestData>("/launcher/v2/mods", async (_, _, _, _) => await launcherV2Callbacks.Mods()),
new RouteAction<EmptyRequestData>("/launcher/v2/profiles", async (_, _, _, _) => await launcherV2Callbacks.Profiles()),
new RouteAction<LoginRequestData>("/launcher/v2/profile", async (_, info, _, _) => await launcherV2Callbacks.Profile(info)),
new RouteAction<RegisterData>("/launcher/v2/wipe", async (_, info, _, _) => await launcherV2Callbacks.Wipe(info)),
]
) { }
@@ -11,22 +11,27 @@ public class ProfileDataService(ISptLogger<ProfileDataService> logger, FileUtil
protected const string ProfileDataFilepath = "user/profileData/";
private readonly ConcurrentDictionary<string, object> _profileDataCache = new();
/// <summary>
/// Check if a specfici mod file exists for a profile
/// </summary>
/// <param name="profileId">Profile to look up</param>
/// <param name="modKey">Name of json file to look up</param>
public bool ProfileDataExists(string profileId, string modKey)
{
return fileUtil.FileExists($"{ProfileDataFilepath}{profileId}/{modKey}.json");
return fileUtil.FileExists(Path.Combine(ProfileDataFilepath, profileId, $"{modKey}.json"));
}
public T? GetProfileData<T>(string profileId, string modKey)
{
var profileDataKey = $"{profileId}:{modKey}";
var profileDataKey = GetCacheKey(profileId, modKey);
if (!_profileDataCache.TryGetValue(profileDataKey, out var value))
{
if (fileUtil.FileExists($"{ProfileDataFilepath}{profileId}/{modKey}.json"))
if (ProfileDataExists(profileId, modKey))
{
value = jsonUtil.Deserialize<T>(fileUtil.ReadFile($"{ProfileDataFilepath}{profileId}/{modKey}.json"));
value = jsonUtil.Deserialize<T>(fileUtil.ReadFile(Path.Combine(ProfileDataFilepath, profileId, $"{modKey}.json")));
if (value != null)
{
_profileDataCache[$"{profileId}:{modKey}"] = value;
_profileDataCache[GetCacheKey(profileId, modKey)] = value;
}
}
else
@@ -42,13 +47,45 @@ public class ProfileDataService(ISptLogger<ProfileDataService> logger, FileUtil
{
ArgumentNullException.ThrowIfNull(profileData);
var data = jsonUtil.Serialize(profileData, profileData.GetType(), true);
if (data == null)
var data =
jsonUtil.Serialize(profileData, profileData.GetType(), true)
?? throw new Exception("The profile data when serialized resulted in a null value");
_profileDataCache[GetCacheKey(profileId, modKey)] = profileData;
fileUtil.WriteFile(Path.Combine(ProfileDataFilepath, profileId, $"{modKey}.json"), data);
}
/// <summary>
/// Clear all data for a profile
/// </summary>
/// <param name="profileId">Id of profile to delete files for</param>
public void ClearProfileData(string profileId)
{
if (!fileUtil.DirectoryExists(Path.Combine(ProfileDataFilepath, profileId)))
{
throw new Exception("The profile data when serialized resulted in a null value");
return;
}
_profileDataCache[$"{profileId}:{modKey}"] = profileData;
fileUtil.WriteFile($"{ProfileDataFilepath}{profileId}/{modKey}.json", data);
var profileFiles = fileUtil.GetFiles(Path.Combine(ProfileDataFilepath, profileId));
foreach (var filepPath in profileFiles)
{
fileUtil.DeleteFile(filepPath);
}
var keysInCacheToRemove = _profileDataCache.Keys.Where(key => key.StartsWith($"{profileId}:")).ToList(); // ToList so we can iterate over results without modifying collection
foreach (var key in keysInCacheToRemove)
{
_profileDataCache.TryRemove(key, out _);
}
}
/// <summary>
/// Get the cache key in specific format
/// </summary>
protected string GetCacheKey(string profileId, string modKey)
{
return $"{profileId}:{modKey}";
}
}
@@ -272,7 +272,7 @@ public class RagfairPriceService(
continue;
}
price += GetDynamicItemPrice(item.Template, desiredCurrency, item, offerItems, isPackOffer).Value;
price += GetDynamicItemPrice(item.Template, desiredCurrency, item, offerItems, isPackOffer) ?? 0;
// Check if the item is a weapon preset.
if (item?.Upd?.SptPresetId is not null && presetHelper.IsPresetBaseClass(item.Upd.SptPresetId.Value, BaseClasses.WEAPON))
@@ -329,7 +329,11 @@ public class RagfairPriceService(
&& presetHelper.IsPresetBaseClass(item.Upd.SptPresetId.Value, BaseClasses.WEAPON)
)
{
price = GetWeaponPresetPrice(item, offerItems, price);
price =
RagfairConfig.Dynamic.GenerateBaseFleaPrices.UseHandbookPrice
&& RagfairConfig.Dynamic.GenerateBaseFleaPrices.GeneratePresetPriceByChildren
? GetPresetPriceByChildren(offerItems)
: GetWeaponPresetPrice(item, offerItems, price);
isPreset = true;
}
@@ -522,6 +526,28 @@ public class RagfairPriceService(
return existingPrice + extraModsPrice;
}
/// <summary>
/// Calculate the cost of a weapon preset by adding together the price of its mods
/// </summary>
/// <param name="weaponWithChildren">weapon plus mods</param>
/// <returns>price of weapon in roubles</returns>
protected double GetPresetPriceByChildren(IEnumerable<Item> weaponWithChildren)
{
var priceTotal = 0d;
foreach (var item in weaponWithChildren)
{
// Root item uses static price
if (item.ParentId == null)
{
priceTotal += GetStaticPriceForItem(item.Template) ?? 0;
}
priceTotal += GetFleaPriceForItem(item.Template);
}
return priceTotal;
}
/// <summary>
/// Get the highest price for an item that is stored in handbook or trader assorts
/// </summary>
@@ -0,0 +1,76 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Utils;
using Range = SemanticVersioning.Range;
using Version = SemanticVersioning.Version;
namespace SPTarkov.Server.Core.Services;
// Note: We want to run after all mods, to avoid this being lost in mod log
// spam, so we purposely use MaxValue here
[Injectable(TypePriority = int.MaxValue)]
internal class ReleaseCheckService(ISptLogger<ReleaseCheckService> logger) : IOnLoad
{
public Task OnLoad()
{
// Run in a new task so we don't hold the main thread at all, this isn't super critical
_ = Task.Run(CheckForUpdate);
return Task.CompletedTask;
}
private async Task CheckForUpdate()
{
try
{
var httpClient = new HttpClient();
// These headers are _required_ by GitHub API
httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("SP-Tarkov");
httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
// TODO: We could probably throw this into a config somewhere, for now hard code it
var release = await httpClient.GetFromJsonAsync<ReleaseInformation>(
"https://api.github.com/repos/sp-tarkov/build/releases/latest"
);
if (release != null)
{
Version latestVersion = new(release.Version);
Version currentVersion = ProgramStatics.SPT_VERSION();
Range currentVersionRange = new($"~{currentVersion.Major}.{currentVersion.Minor}.0");
// First make sure the latest release is in our range, this stops "4.1.0" from being detected as a valid upgrade for "4.0.1"
if (!currentVersionRange.IsSatisfied(latestVersion))
{
return;
}
// Notify the user if an upgrade is available
if (latestVersion > currentVersion)
{
logger.Warning($"A new version of SPT is available! SPT v{release.Version}");
logger.Warning($"Released {release.ReleaseDate.ToLocalTime()}");
logger.Warning($"Release Notes: {release.DownloadUrl}");
}
}
}
// We ignore errors, this isn't critical to run, and we don't want to scare users
catch { }
}
private record ReleaseInformation
{
[JsonPropertyName("tag_name")]
public required string Version { get; init; }
[JsonPropertyName("html_url")]
public required string DownloadUrl { get; init; }
[JsonPropertyName("published_at")]
public required DateTime ReleaseDate { get; init; }
}
}
@@ -407,6 +407,7 @@ public class SeasonalEventService(
AddEventBossesToMaps("halloweensummon");
EnableHalloweenSummonEvent();
AddPumpkinsToScavBackpacks();
AddEventBossesToMaps("halloweennightcult");
RenameBitcoin();
if (eventType.Settings is not null && eventType.Settings.ReplaceBotHostility.GetValueOrDefault(false))
{
@@ -457,7 +458,10 @@ public class SeasonalEventService(
if (eventType.Settings?.ReplaceBotHostility ?? false)
{
ReplaceBotHostility(SeasonalEventConfig.HostilitySettingsForEvent.FirstOrDefault(x => x.Key == "zombies").Value);
ReplaceBotHostility(
SeasonalEventConfig.HostilitySettingsForEvent.FirstOrDefault(x => x.Key == "zombies").Value,
GetLocationsWithZombies(eventType.Settings.ZombieSettings.MapInfectionAmount)
);
}
if (eventType.Settings?.AdjustBotAppearances ?? false)
@@ -477,6 +481,8 @@ public class SeasonalEventService(
{
usec.BotAppearance.Head[new MongoId("6644d2da35d958070c02642c")] = 30;
}
AddEventBossesToMaps("halloweennightcult");
}
protected void ApplyChristmasEvent(SeasonalEvent eventType, Config globalConfig)
@@ -507,6 +513,23 @@ public class SeasonalEventService(
{
AdjustBotAppearanceValues(eventType.Type);
}
ChangeBtrToTarColaSkin();
}
private void ChangeBtrToTarColaSkin()
{
var btrSettings = databaseService.GetGlobals().Configuration.BTRSettings;
if (btrSettings.MapsConfigs.TryGetValue("Woods", out var woodsBtrSettings))
{
woodsBtrSettings.BtrSkin = "Tarcola";
}
if (btrSettings.MapsConfigs.TryGetValue("TarkovStreets", out var streetsBtrSettings))
{
streetsBtrSettings.BtrSkin = "Tarcola";
}
}
protected void ApplyNewYearsEvent(SeasonalEvent eventType, Config globalConfig)
@@ -585,7 +608,10 @@ public class SeasonalEventService(
}
}
protected void ReplaceBotHostility(Dictionary<string, List<AdditionalHostilitySettings>> hostilitySettings)
protected void ReplaceBotHostility(
Dictionary<string, List<AdditionalHostilitySettings>> hostilitySettings,
HashSet<string>? locationWhitelist = null
)
{
var locations = databaseService.GetLocations().GetDictionary();
var ignoreList = LocationConfig.NonMaps;
@@ -613,6 +639,11 @@ public class SeasonalEventService(
}
}
if (locationWhitelist is not null && !locationWhitelist.Contains(locationName))
{
continue;
}
foreach (var settings in newHostilitySettings)
{
var matchingBaseSettings = locationBase.Base.BotLocationModifier?.AdditionalHostilitySettings?.FirstOrDefault(x =>
@@ -870,7 +901,7 @@ public class SeasonalEventService(
continue;
}
if (mapIdWhitelist is null || !mapIdWhitelist.Contains(locationKey))
if (mapIdWhitelist is not null && !mapIdWhitelist.Contains(locationKey))
{
continue;
}
@@ -881,7 +912,7 @@ public class SeasonalEventService(
{
if (mapBosses.All(bossSpawn => bossSpawn.BossName != boss.BossName))
{
// Zombie doesn't exist in maps boss list yet, add
// Boss doesn't exist in maps boss list yet, add
mapBosses.Add(boss);
}
}
@@ -135,14 +135,7 @@ public class FileUtil
}
// Overwrite over the old file
if (File.Exists(filePath))
{
File.Replace(tempFilePath, filePath, null);
}
else
{
File.Move(tempFilePath, filePath);
}
File.Move(tempFilePath, filePath, overwrite: true);
}
catch
{
+39
View File
@@ -18,6 +18,12 @@ public class ModValidator(ISptLogger<ModValidator> logger, ServerLocalisationSer
return [];
}
// Validate all assemblies for references. This will deprecate AbstractMetadata semver checks in 4.1
foreach (var mod in mods)
{
ValidateCoreAssemblyReference(mod);
}
logger.Info(localisationService.GetText("modloader-loading_mods", mods.Count()));
// Validate and remove broken mods from mod list
@@ -147,6 +153,39 @@ public class ModValidator(ISptLogger<ModValidator> logger, ServerLocalisationSer
return true;
}
/// <summary>
/// Validate that the SPTarkov.Server.Core assembly is compatible with this mod. Semver is not enough.<br/>
///
/// Throws an exception if the mod was built for a newer SPT version than the current running SPT version
/// </summary>
/// <param name="mod">mod to validate</param>
protected void ValidateCoreAssemblyReference(SptMod mod)
{
var sptVersion = ProgramStatics.SPT_VERSION();
var modName = $"{mod.ModMetadata.Author}-{mod.ModMetadata.Name}";
foreach (var assembly in mod.Assemblies)
{
var sptCoreAsmRefVersion = assembly
.GetReferencedAssemblies()
.FirstOrDefault(asm => asm.Name == "SPTarkov.Server.Core")
?.Version?.ToString();
if (sptCoreAsmRefVersion is null)
{
continue;
}
var modRefVersion = new SemanticVersioning.Version(sptCoreAsmRefVersion?[..^2]!);
if (modRefVersion > sptVersion)
{
throw new Exception(
$"Mod: {modName} requires a minimum SPT version of `{modRefVersion}`, but you are running `{sptVersion}`. Please update SPT to use this mod."
);
}
}
}
/// <summary>
/// Add into class property "Imported"
/// </summary>