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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user