Add new service to handle profile migrations (#468)
* Add new service to handle profile migrations * Handle various null checks * Remove unecessary assignments * Further works on this: - Loads profiles as JObject's initally, so migration can take place on profiles that don't have proper compatability - Adds prerequisite migrations, and sorts them after one another * Throw exception if profile can't be deserialized after migration * Cleanup & use profile version * Further migrations work, support 3.10 & 3.11 profiles upgrading to 4.0 * Update parameter name
This commit is contained in:
@@ -70,12 +70,6 @@ public class GameController(
|
||||
return;
|
||||
}
|
||||
|
||||
fullProfile.SptData ??= new Spt
|
||||
{
|
||||
//TODO: complete
|
||||
Version = "Replace_me",
|
||||
};
|
||||
fullProfile.SptData.Migrations ??= new Dictionary<string, long>();
|
||||
fullProfile.FriendProfileIds ??= [];
|
||||
|
||||
if (fullProfile.ProfileInfo?.IsWiped is not null && fullProfile.ProfileInfo.IsWiped.Value)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration
|
||||
{
|
||||
public abstract class AbstractProfileMigration : IProfileMigration
|
||||
{
|
||||
public abstract string FromVersion { get; }
|
||||
public abstract string ToVersion { get; }
|
||||
public abstract string MigrationName { get; }
|
||||
|
||||
public abstract IEnumerable<Type> PrerequisiteMigrations { get; }
|
||||
|
||||
public abstract bool CanMigrate(
|
||||
JsonObject profile,
|
||||
IEnumerable<IProfileMigration> previouslyRanMigrations
|
||||
);
|
||||
public abstract JsonObject? Migrate(JsonObject profile);
|
||||
|
||||
public virtual bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected SemanticVersioning.Version? GetProfileVersion(JsonObject profile)
|
||||
{
|
||||
var versionString = profile["spt"]?["version"]?.GetValue<string>();
|
||||
|
||||
if (versionString is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var versionNumber = versionString.Split(' ')[0];
|
||||
|
||||
return SemanticVersioning.Version.TryParse(versionNumber, out var version)
|
||||
? version
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration
|
||||
{
|
||||
public interface IProfileMigration
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows for adding checks if the profile in question can migrate
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to check</param>
|
||||
/// <returns>Returns true if the profile can migrate, returns false if not</returns>
|
||||
public bool CanMigrate(
|
||||
JsonObject profile,
|
||||
IEnumerable<IProfileMigration> previouslyRanMigrations
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Migrate the profile, this should be used to handle and fix old data that has been removed from the <see cref="SptProfile"/> record
|
||||
/// or a general incompatibility due to different typing
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to migrate</param>
|
||||
/// <returns>Returns the migrated profile on success, or null if it failed</returns>
|
||||
public JsonObject? Migrate(JsonObject profile);
|
||||
|
||||
/// <summary>
|
||||
/// Handles post migration of the profile, this can be used to fill new types with (old) data gotten from <see cref="Migrate"/>
|
||||
/// </summary>
|
||||
/// <returns>Should return true if successful, should return false if not</returns>
|
||||
public bool PostMigrate(SptProfile profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// In 0.16.1.3.35312 BSG changed this to from an int to a hex64 encoded value.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class HideoutSeed : AbstractProfileMigration
|
||||
{
|
||||
public override string FromVersion
|
||||
{
|
||||
get { return "~3.10"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "3.11"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "HideoutSeed311-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return []; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(
|
||||
JsonObject profile,
|
||||
IEnumerable<IProfileMigration> previouslyRanMigrations
|
||||
)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
var versionMatches = fromRange.IsSatisfied(profileVersion);
|
||||
|
||||
var seedNode = profile["characters"]?["pmc"]?["Hideout"]?["Seed"];
|
||||
|
||||
var seedIsNumeric =
|
||||
seedNode is JsonValue seedValue && seedValue.TryGetValue<long>(out _);
|
||||
|
||||
return versionMatches && seedIsNumeric;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
profile["characters"]!["pmc"]!["Hideout"]!["Seed"] = Convert.ToHexStringLower(
|
||||
RandomNumberGenerator.GetBytes(16)
|
||||
);
|
||||
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// In 16.8.0.37972 BSG added customization for voices, technically this only affects BE profiles, but this should fix these.
|
||||
/// </summary>
|
||||
[Injectable]
|
||||
public class TheVoices(DatabaseService databaseService) : AbstractProfileMigration
|
||||
{
|
||||
public override string FromVersion
|
||||
{
|
||||
get { return "~4.0"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "~4.0"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "TheVoices400"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
// Requires ThreeTenToThreeEleven on legacy profiles, due to that changing customization for the first time
|
||||
get { return [typeof(ThreeTenToThreeEleven)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(
|
||||
JsonObject profile,
|
||||
IEnumerable<IProfileMigration> previouslyRanMigrations
|
||||
)
|
||||
{
|
||||
bool voiceIsMissing = profile["characters"]?["pmc"]?["Customization"]?["Voice"] == null;
|
||||
|
||||
return voiceIsMissing;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
HandlePmcVoice(profile);
|
||||
HandleScavVoice(profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void HandlePmcVoice(JsonObject profileObject)
|
||||
{
|
||||
var pmcInfo = profileObject["characters"]!["pmc"]!["Info"] as JsonObject;
|
||||
|
||||
var oldVoice = pmcInfo["Voice"]?.ToString() ?? "";
|
||||
pmcInfo.Remove("Voice");
|
||||
|
||||
var voiceMongoId = databaseService
|
||||
.GetCustomization()
|
||||
.FirstOrDefault(x => x.Value.Properties.Name == oldVoice)
|
||||
.Key;
|
||||
|
||||
profileObject["characters"]!["pmc"]!["Customization"]!["Voice"] =
|
||||
voiceMongoId.ToString();
|
||||
}
|
||||
|
||||
private void HandleScavVoice(JsonObject profileObject)
|
||||
{
|
||||
var pmcInfo = profileObject["characters"]!["scav"]!["Info"] as JsonObject;
|
||||
|
||||
var oldVoice = pmcInfo["Voice"]?.ToString() ?? "";
|
||||
pmcInfo.Remove("Voice");
|
||||
|
||||
var voiceMongoId = databaseService
|
||||
.GetCustomization()
|
||||
.FirstOrDefault(x => x.Value.Properties.Name == oldVoice)
|
||||
.Key;
|
||||
|
||||
profileObject["characters"]!["scav"]!["Customization"]!["Voice"] =
|
||||
voiceMongoId.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
{
|
||||
[Injectable]
|
||||
public class ThreeElevenToFourZero(Watermark watermark) : AbstractProfileMigration
|
||||
{
|
||||
public override string FromVersion
|
||||
{
|
||||
get { return "~3.11"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "4.0"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "311x-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return [typeof(ThreeTenToThreeEleven)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(
|
||||
JsonObject profile,
|
||||
IEnumerable<IProfileMigration> previouslyRanMigrations
|
||||
)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
|
||||
var versionMatches =
|
||||
fromRange.IsSatisfied(profileVersion)
|
||||
|| PrerequisiteMigrations.All(prereq =>
|
||||
previouslyRanMigrations.Any(r => r.GetType() == prereq)
|
||||
);
|
||||
|
||||
return versionMatches;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (profile["characters"]!["pmc"]!["Hideout"]!["Production"] is JsonObject production)
|
||||
{
|
||||
foreach (var entry in production)
|
||||
{
|
||||
if (
|
||||
entry.Value is JsonObject productionEntry
|
||||
&& productionEntry["StartTimestamp"] is JsonValue startTimestampValue
|
||||
&& startTimestampValue.TryGetValue<string>(out var startTimestampStr)
|
||||
&& long.TryParse(startTimestampStr, out var startTimestampInt)
|
||||
)
|
||||
{
|
||||
productionEntry["StartTimestamp"] = startTimestampInt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Todo: TaskConditionCounters CounterCrafting?
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
profile.SptData.ExtraRepeatableQuests = [];
|
||||
|
||||
profile.SptData.Version = $"{watermark.GetVersionTag()} (Migrated from 3.11)";
|
||||
|
||||
return base.PostMigrate(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Extensions;
|
||||
using SPTarkov.Server.Core.Helpers;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Models.Enums;
|
||||
using SPTarkov.Server.Core.Services;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace SPTarkov.Server.Core.Migration.Migrations
|
||||
{
|
||||
[Injectable]
|
||||
public class ThreeTenToThreeEleven(
|
||||
DatabaseService databaseService,
|
||||
// Yes, referencing the helpers directly causes a circular dependency. Too bad!
|
||||
IServiceProvider serviceProvider
|
||||
) : AbstractProfileMigration
|
||||
{
|
||||
private List<string> _oldSuiteData = [];
|
||||
|
||||
public override string FromVersion
|
||||
{
|
||||
get { return "~3.10"; }
|
||||
}
|
||||
|
||||
public override string ToVersion
|
||||
{
|
||||
get { return "3.11"; }
|
||||
}
|
||||
|
||||
public override string MigrationName
|
||||
{
|
||||
get { return "310x-SPTSharp"; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Type> PrerequisiteMigrations
|
||||
{
|
||||
get { return [typeof(HideoutSeed)]; }
|
||||
}
|
||||
|
||||
public override bool CanMigrate(
|
||||
JsonObject profile,
|
||||
IEnumerable<IProfileMigration> previouslyRanMigrations
|
||||
)
|
||||
{
|
||||
var profileVersion = GetProfileVersion(profile);
|
||||
|
||||
var fromRange = Range.Parse(FromVersion);
|
||||
|
||||
var versionMatches = fromRange.IsSatisfied(profileVersion);
|
||||
|
||||
return versionMatches;
|
||||
}
|
||||
|
||||
public override JsonObject? Migrate(JsonObject profile)
|
||||
{
|
||||
if (profile["suits"] is JsonArray suitsArray)
|
||||
{
|
||||
_oldSuiteData = suitsArray
|
||||
.Select(node => node?.GetValue<string>())
|
||||
.Where(suit => suit != null)
|
||||
.ToList()!;
|
||||
}
|
||||
|
||||
profile.Remove("suits");
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public override bool PostMigrate(SptProfile profile)
|
||||
{
|
||||
if (profile.CustomisationUnlocks is null)
|
||||
{
|
||||
profile.CustomisationUnlocks = [];
|
||||
profile.AddCustomisationUnlocksToProfile();
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Prestige is null)
|
||||
{
|
||||
profile.CharacterData.PmcData.Prestige = [];
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Inventory.HideoutCustomizationStashId is null)
|
||||
{
|
||||
profile.CharacterData.PmcData.Inventory.HideoutCustomizationStashId = new(
|
||||
"676db384777490e23c45b657"
|
||||
);
|
||||
|
||||
//Directly injecting CreateProfileService causes a circular dependency which I can't be bothered to fix just for this
|
||||
serviceProvider
|
||||
.GetService<CreateProfileService>()!
|
||||
.AddMissingInternalContainersToProfile(profile.CharacterData.PmcData);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Hideout.Customization is null)
|
||||
{
|
||||
profile.CharacterData.PmcData.Hideout.Customization = new Dictionary<
|
||||
string,
|
||||
Models.Common.MongoId
|
||||
>
|
||||
{
|
||||
{ "Wall", new("675844bdf94a97cbbe096f1a") },
|
||||
{ "Floor", new("6758443ff94a97cbbe096f18") },
|
||||
{ "Light", new("675fe8abbc3deae49a0b947f") },
|
||||
{ "Ceiling", new("673b3f977038192ee006aa09") },
|
||||
{ "ShootingRangeMark", new("67585d416c72998cf60ed85a") },
|
||||
};
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.Side == "Bear")
|
||||
{
|
||||
ProcessBearProfile(profile);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.Side == "Usec")
|
||||
{
|
||||
ProcessUsecProfile(profile);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Achievements.Count > 0)
|
||||
{
|
||||
var achievementsDb = databaseService.GetTemplates().Achievements;
|
||||
|
||||
foreach (var achievementId in profile.CharacterData.PmcData.Achievements.Keys)
|
||||
{
|
||||
var achievementDb = achievementsDb.FirstOrDefault(a => a.Id == achievementId);
|
||||
var rewards = achievementDb?.Rewards;
|
||||
|
||||
if (rewards == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only hand out the new hideout customization rewards.
|
||||
var filteredRewards = rewards
|
||||
.Where(r => r.Type == RewardType.CustomizationDirect)
|
||||
.ToList();
|
||||
|
||||
//Directly injecting RewardHelper causes a circular dependency which I can't be bothered to fix just for this
|
||||
serviceProvider
|
||||
.GetService<RewardHelper>()!
|
||||
.ApplyRewards(
|
||||
filteredRewards,
|
||||
CustomisationSource.ACHIEVEMENT,
|
||||
profile,
|
||||
profile.CharacterData.PmcData,
|
||||
achievementId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ProcessBearProfile(SptProfile profile)
|
||||
{
|
||||
// Reset clothing customization back to default as customization changed in 3.11
|
||||
profile.CharacterData.PmcData.Customization.Body = new("5cc0858d14c02e000c6bea66");
|
||||
profile.CharacterData.PmcData.Customization.Feet = new("5cc085bb14c02e000e67a5c5");
|
||||
profile.CharacterData.PmcData.Customization.Hands = new("5cc0876314c02e000c6bea6b");
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("674731c8bafff850080488bb");
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "edge_of_darkness")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new(
|
||||
"6746fd09bafff85008048838"
|
||||
);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "unheard_edition")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new(
|
||||
"67471928d17d6431550563b5"
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var oldSuite in _oldSuiteData)
|
||||
{
|
||||
// Default Bear clothing, dont need to add this
|
||||
if (
|
||||
oldSuite == "5cd946231388ce000d572fe3"
|
||||
|| oldSuite == "5cd945d71388ce000a659dfb"
|
||||
|| oldSuite == "666841a02537107dc508b704"
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var trader = databaseService.GetTrader("5ac3b934156ae10c4430e83c");
|
||||
var traderClothing = trader?.Suits?.FirstOrDefault(s => s.SuiteId == oldSuite);
|
||||
|
||||
if (traderClothing != null)
|
||||
{
|
||||
var clothingToAdd = new CustomisationStorage
|
||||
{
|
||||
Id = traderClothing.SuiteId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
};
|
||||
|
||||
profile.CustomisationUnlocks.Add(clothingToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessUsecProfile(SptProfile profile)
|
||||
{
|
||||
// Reset clothing customization back to default as customization changed in 3.11
|
||||
profile.CharacterData.PmcData.Customization.Body = new("5cde95d97d6c8b647a3769b0"); //Usec default clothing
|
||||
profile.CharacterData.PmcData.Customization.Feet = new("5cde95ef7d6c8b04713c4f2d");
|
||||
profile.CharacterData.PmcData.Customization.Hands = new("5cde95fa7d6c8b04737c2d13");
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new("674731d1170146228c0d222a"); //Usec base dogtag
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "edge_of_darkness")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new(
|
||||
"67471938bafff850080488b7"
|
||||
);
|
||||
}
|
||||
|
||||
if (profile.CharacterData.PmcData.Info.GameVersion == "unheard_edition")
|
||||
{
|
||||
profile.CharacterData.PmcData.Customization.DogTag = new(
|
||||
"6747193f170146228c0d2226"
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var oldSuite in _oldSuiteData)
|
||||
{
|
||||
// Default Usec clothing, dont need to add this
|
||||
if (
|
||||
oldSuite == "5cde9ec17d6c8b04723cf479"
|
||||
|| oldSuite == "5cde9e957d6c8b0474535da7"
|
||||
|| oldSuite == "666841a02537107dc508b704"
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var trader = databaseService.GetTrader("5ac3b934156ae10c4430e83c");
|
||||
var traderClothing = trader?.Suits?.FirstOrDefault(s => s.SuiteId == oldSuite);
|
||||
|
||||
if (traderClothing != null)
|
||||
{
|
||||
var clothingToAdd = new CustomisationStorage
|
||||
{
|
||||
Id = traderClothing.SuiteId,
|
||||
Source = CustomisationSource.UNLOCKED_IN_GAME,
|
||||
Type = CustomisationType.SUITE,
|
||||
};
|
||||
|
||||
profile.CustomisationUnlocks.Add(clothingToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.DI;
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
@@ -20,6 +22,7 @@ public class SaveServer(
|
||||
JsonUtil jsonUtil,
|
||||
HashUtil hashUtil,
|
||||
ServerLocalisationService serverLocalisationService,
|
||||
ProfileMigratorService profileMigratorService,
|
||||
ISptLogger<SaveServer> logger,
|
||||
ConfigServer configServer
|
||||
)
|
||||
@@ -220,7 +223,12 @@ public class SaveServer(
|
||||
if (fileUtil.FileExists(filePath))
|
||||
// File found, store in profiles[]
|
||||
{
|
||||
profiles[sessionID] = await jsonUtil.DeserializeFromFileAsync<SptProfile>(filePath);
|
||||
var profile = await jsonUtil.DeserializeFromFileAsync<JsonObject>(filePath);
|
||||
|
||||
if (profile is not null)
|
||||
{
|
||||
profiles[sessionID] = profileMigratorService.HandlePendingMigrations(profile);
|
||||
}
|
||||
}
|
||||
|
||||
// Run callbacks
|
||||
|
||||
@@ -293,7 +293,7 @@ public class CreateProfileService(
|
||||
/// DOES NOT check that stash exists
|
||||
/// </summary>
|
||||
/// <param name="pmcData"> Profile to check </param>
|
||||
protected void AddMissingInternalContainersToProfile(PmcData pmcData)
|
||||
public void AddMissingInternalContainersToProfile(PmcData pmcData)
|
||||
{
|
||||
if (
|
||||
!pmcData.Inventory.Items.Any(item =>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Helpers;
|
||||
using SPTarkov.Server.Core.Migration;
|
||||
using SPTarkov.Server.Core.Models.Eft.Profile;
|
||||
using SPTarkov.Server.Core.Models.Utils;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
|
||||
namespace SPTarkov.Server.Core.Services
|
||||
{
|
||||
[Injectable(InjectionType.Singleton)]
|
||||
public class ProfileMigratorService(
|
||||
IEnumerable<IProfileMigration> profileMigrations,
|
||||
TimeUtil timeUtil,
|
||||
ISptLogger<ProfileMigratorService> logger
|
||||
)
|
||||
{
|
||||
private IEnumerable<AbstractProfileMigration> _sortedMigrations = [];
|
||||
|
||||
public SptProfile HandlePendingMigrations(JsonObject profile)
|
||||
{
|
||||
// On the initial run, begin sorting our migrations
|
||||
// This will sort it so that any non prerequisite migrations go first
|
||||
// And then all of the prerequisite ones.
|
||||
if (!_sortedMigrations.Any())
|
||||
{
|
||||
_sortedMigrations = SortMigrations();
|
||||
}
|
||||
|
||||
var profileId = profile["info"]?["id"]?.GetValue<string>();
|
||||
|
||||
// Profile is due for a wipe or a reset, do not continue here.
|
||||
if (
|
||||
profile["characters"]?["pmc"]?["Info"] == null
|
||||
|| profile["characters"]?["scav"]?["Info"] == null
|
||||
|| (profile["info"]?["wipe"]?.GetValue<bool>() == true)
|
||||
)
|
||||
{
|
||||
return profile.Deserialize<SptProfile>(JsonUtil.JsonSerializerOptionsNoIndent)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Could not deserialize the profile {profileId}"
|
||||
);
|
||||
;
|
||||
}
|
||||
|
||||
var ranMigrations = new List<AbstractProfileMigration>();
|
||||
|
||||
foreach (var profileMigration in _sortedMigrations)
|
||||
{
|
||||
if (profileMigration.CanMigrate(profile, ranMigrations))
|
||||
{
|
||||
logger.Warning(
|
||||
$"{profileId} has a pending profile migration: {profileMigration.MigrationName}"
|
||||
);
|
||||
|
||||
var migratedProfile = profileMigration.Migrate(profile);
|
||||
|
||||
if (migratedProfile is not null)
|
||||
{
|
||||
profile = migratedProfile;
|
||||
|
||||
ranMigrations.Add(profileMigration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var SptReadyProfile =
|
||||
profile.Deserialize<SptProfile>(JsonUtil.JsonSerializerOptionsNoIndent)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Could not deserialize the profile {profileId}"
|
||||
);
|
||||
|
||||
foreach (var ranMigration in ranMigrations)
|
||||
{
|
||||
if (ranMigration.PostMigrate(SptReadyProfile))
|
||||
{
|
||||
logger.Success(
|
||||
$"{profileId} successfully ran profile migration: {ranMigration.MigrationName}"
|
||||
);
|
||||
|
||||
SptReadyProfile.SptData.Migrations.Add(
|
||||
ranMigration.MigrationName,
|
||||
timeUtil.GetTimeStamp()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return SptReadyProfile;
|
||||
}
|
||||
|
||||
protected void SetCompletedMigration(JsonObject profile, string migrationName)
|
||||
{
|
||||
var profileMigrations = profile["spt"]["migrations"] as JsonObject;
|
||||
|
||||
profileMigrations[migrationName] = JsonValue.Create(timeUtil.GetTimeStamp());
|
||||
}
|
||||
|
||||
protected IEnumerable<AbstractProfileMigration> SortMigrations()
|
||||
{
|
||||
var sortedMigrations = new List<AbstractProfileMigration>();
|
||||
var visitedMigrations = new Dictionary<Type, bool>();
|
||||
var migrationDict = profileMigrations
|
||||
.Cast<AbstractProfileMigration>()
|
||||
.ToDictionary(m => m.GetType());
|
||||
|
||||
foreach (var migration in profileMigrations.Cast<AbstractProfileMigration>())
|
||||
{
|
||||
VisitMigrationForSort(
|
||||
migration,
|
||||
migrationDict,
|
||||
visitedMigrations,
|
||||
sortedMigrations
|
||||
);
|
||||
}
|
||||
|
||||
return sortedMigrations;
|
||||
}
|
||||
|
||||
protected void VisitMigrationForSort(
|
||||
AbstractProfileMigration migration,
|
||||
Dictionary<Type, AbstractProfileMigration> migrationTypeDictionary,
|
||||
Dictionary<Type, bool> visitedTypeDictionary,
|
||||
List<AbstractProfileMigration> sortedMigrations
|
||||
)
|
||||
{
|
||||
var migrationType = migration.GetType();
|
||||
|
||||
if (visitedTypeDictionary.TryGetValue(migrationType, out var isVisited))
|
||||
{
|
||||
if (isVisited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Big error, two migrations should never depend on one another
|
||||
throw new InvalidOperationException(
|
||||
$"Cycle detected in migration prerequisites involving: {migrationType.Name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Mark the current migration type for visiting
|
||||
visitedTypeDictionary[migrationType] = false;
|
||||
|
||||
foreach (var prerequisiteType in migration.PrerequisiteMigrations)
|
||||
{
|
||||
if (!migrationTypeDictionary.TryGetValue(prerequisiteType, out var prereqMigration))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Visit the next prerequisite
|
||||
VisitMigrationForSort(
|
||||
prereqMigration,
|
||||
migrationTypeDictionary,
|
||||
visitedTypeDictionary,
|
||||
sortedMigrations
|
||||
);
|
||||
}
|
||||
|
||||
// Done visiting, mark it as fully visited and add it to the sorted migrations
|
||||
visitedTypeDictionary[migrationType] = true;
|
||||
sortedMigrations.Add(migration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ namespace SPTarkov.Server.Core.Utils;
|
||||
[Injectable(InjectionType.Singleton)]
|
||||
public class JsonUtil
|
||||
{
|
||||
private static JsonSerializerOptions? _jsonSerializerOptionsIndented;
|
||||
private static JsonSerializerOptions? _jsonSerializerOptionsNoIndent;
|
||||
public static JsonSerializerOptions? JsonSerializerOptionsIndented { get; private set; }
|
||||
public static JsonSerializerOptions? JsonSerializerOptionsNoIndent { get; private set; }
|
||||
|
||||
public JsonUtil(IEnumerable<IJsonConverterRegistrator> registrators)
|
||||
{
|
||||
_jsonSerializerOptionsNoIndent = new JsonSerializerOptions()
|
||||
JsonSerializerOptionsNoIndent = new JsonSerializerOptions()
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
@@ -26,11 +26,11 @@ public class JsonUtil
|
||||
{
|
||||
foreach (var converter in registrator.GetJsonConverters())
|
||||
{
|
||||
_jsonSerializerOptionsNoIndent.Converters.Add(converter);
|
||||
JsonSerializerOptionsNoIndent.Converters.Add(converter);
|
||||
}
|
||||
}
|
||||
|
||||
_jsonSerializerOptionsIndented = new JsonSerializerOptions(_jsonSerializerOptionsNoIndent)
|
||||
JsonSerializerOptionsIndented = new JsonSerializerOptions(JsonSerializerOptionsNoIndent)
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
@@ -46,7 +46,7 @@ public class JsonUtil
|
||||
{
|
||||
return string.IsNullOrEmpty(json)
|
||||
? default
|
||||
: JsonSerializer.Deserialize<T>(json, _jsonSerializerOptionsNoIndent);
|
||||
: JsonSerializer.Deserialize<T>(json, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -59,7 +59,7 @@ public class JsonUtil
|
||||
{
|
||||
return string.IsNullOrEmpty(json)
|
||||
? null
|
||||
: JsonSerializer.Deserialize(json, type, _jsonSerializerOptionsNoIndent);
|
||||
: JsonSerializer.Deserialize(json, type, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,7 +76,7 @@ public class JsonUtil
|
||||
|
||||
using (FileStream fs = new(file, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(fs, _jsonSerializerOptionsNoIndent);
|
||||
return JsonSerializer.Deserialize<T>(fs, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public class JsonUtil
|
||||
useAsync: true
|
||||
);
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<T>(fs, _jsonSerializerOptionsNoIndent);
|
||||
return await JsonSerializer.DeserializeAsync<T>(fs, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -119,7 +119,7 @@ public class JsonUtil
|
||||
|
||||
using (FileStream fs = new(file, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
return JsonSerializer.Deserialize(fs, type, _jsonSerializerOptionsNoIndent);
|
||||
return JsonSerializer.Deserialize(fs, type, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ public class JsonUtil
|
||||
useAsync: true
|
||||
);
|
||||
|
||||
return await JsonSerializer.DeserializeAsync(fs, type, _jsonSerializerOptionsNoIndent);
|
||||
return await JsonSerializer.DeserializeAsync(fs, type, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -156,7 +156,7 @@ public class JsonUtil
|
||||
/// <returns></returns>
|
||||
public object? DeserializeFromFileStream(FileStream fs, Type type)
|
||||
{
|
||||
return JsonSerializer.Deserialize(fs, type, _jsonSerializerOptionsNoIndent);
|
||||
return JsonSerializer.Deserialize(fs, type, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,7 +167,7 @@ public class JsonUtil
|
||||
/// <returns></returns>
|
||||
public async Task<object?> DeserializeFromFileStreamAsync(FileStream fs, Type type)
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync(fs, type, _jsonSerializerOptionsNoIndent);
|
||||
return await JsonSerializer.DeserializeAsync(fs, type, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -177,7 +177,7 @@ public class JsonUtil
|
||||
/// <returns>T</returns>
|
||||
public async Task<T?> DeserializeFromMemoryStreamAsync<T>(MemoryStream ms)
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<T>(ms, _jsonSerializerOptionsNoIndent);
|
||||
return await JsonSerializer.DeserializeAsync<T>(ms, JsonSerializerOptionsNoIndent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -193,7 +193,7 @@ public class JsonUtil
|
||||
? null
|
||||
: JsonSerializer.Serialize(
|
||||
obj,
|
||||
indented ? _jsonSerializerOptionsIndented : _jsonSerializerOptionsNoIndent
|
||||
indented ? JsonSerializerOptionsIndented : JsonSerializerOptionsNoIndent
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ public class JsonUtil
|
||||
: JsonSerializer.Serialize(
|
||||
obj,
|
||||
type,
|
||||
indented ? _jsonSerializerOptionsIndented : _jsonSerializerOptionsNoIndent
|
||||
indented ? JsonSerializerOptionsIndented : JsonSerializerOptionsNoIndent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user