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:
@@ -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
|
||||
},
|
||||
|
||||
+14
-2
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user