Files
DrakiaXYZ d2e2f04c93 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>
2025-10-31 14:55:07 -07:00

280 lines
9.2 KiB
C#

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);
}
}