From 06f36703d90b9249605da6275febe4846dca2865 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 13 Feb 2024 23:58:00 -0500 Subject: [PATCH] add migrations for pre-1.6 Data/Blueprints and Data/Boots --- .../Internal/RuntimeMigrationHelper.cs | 21 ++ .../Migrations/Migration_2_0.ForBlueprints.cs | 292 ++++++++++++++++++ .../Migrations/Migration_2_0.ForBoots.cs | 79 +++++ .../Framework/Migrations/Migration_2_0.cs | 2 + 4 files changed, 394 insertions(+) create mode 100644 ContentPatcher/Framework/Migrations/Migration_2_0.ForBlueprints.cs create mode 100644 ContentPatcher/Framework/Migrations/Migration_2_0.ForBoots.cs diff --git a/ContentPatcher/Framework/Migrations/Internal/RuntimeMigrationHelper.cs b/ContentPatcher/Framework/Migrations/Internal/RuntimeMigrationHelper.cs index 0925709d4..5d182a422 100644 --- a/ContentPatcher/Framework/Migrations/Internal/RuntimeMigrationHelper.cs +++ b/ContentPatcher/Framework/Migrations/Internal/RuntimeMigrationHelper.cs @@ -42,5 +42,26 @@ internal static class RuntimeMigrationHelper cache[rawItemId] = metadata.LocalItemId; return metadata.LocalItemId; } + + /// Count the number of fields in a delimited string. + /// The row in which to count fields. + /// The character which delimits fields in the row. + public static int CountFields(string row, char delimiter = '/') + { + int count = 1; // count field before first delimiter + + int lastIndex = -1; + while (true) + { + lastIndex = row.IndexOf('/', lastIndex + 1); + + if (lastIndex == -1) + break; + + count++; + } + + return count; + } } } diff --git a/ContentPatcher/Framework/Migrations/Migration_2_0.ForBlueprints.cs b/ContentPatcher/Framework/Migrations/Migration_2_0.ForBlueprints.cs new file mode 100644 index 000000000..97b0cf521 --- /dev/null +++ b/ContentPatcher/Framework/Migrations/Migration_2_0.ForBlueprints.cs @@ -0,0 +1,292 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using ContentPatcher.Framework.Migrations.Internal; +using ContentPatcher.Framework.Patches; +using Microsoft.Xna.Framework; +using StardewModdingAPI; +using StardewModdingAPI.Framework.Content; +using StardewValley; +using StardewValley.GameData.Buildings; +using StardewTokenParser = StardewValley.TokenizableStrings.TokenParser; + +namespace ContentPatcher.Framework.Migrations +{ + internal partial class Migration_2_0 : BaseRuntimeMigration + { + /// The migration logic to apply pre-1.6 Data/Blueprints patches to Data/Buildings. + private class BlueprintsMigrator : IEditAssetMigrator + { + /********* + ** Fields + *********/ + /// The pre-1.6 asset name. + private const string OldAssetName = "Data/Blueprints"; + + /// The 1.6 asset name. + private const string NewAssetName = "Data/Buildings"; + + + /********* + ** Public methods + *********/ + /// + public bool AppliesTo(IAssetName assetName) + { + return assetName?.IsEquivalentTo(BlueprintsMigrator.OldAssetName, useBaseName: true) is true; + } + + /// + public IAssetName? RedirectTarget(IAssetName assetName, IPatch patch) + { + return new AssetName(BlueprintsMigrator.NewAssetName, null, null); + } + + /// + public bool TryApplyLoadPatch(LoadPatch patch, IAssetName assetName, [NotNullWhen(true)] ref T? asset, out string? error) + { + Dictionary tempData = patch.Load>(this.GetOldAssetName(assetName)); + Dictionary newData = new(); + this.MergeIntoNewFormat(newData, tempData, null); + asset = (T)(object)newData; + + error = null; + return true; + } + + /// + public bool TryApplyEditPatch(EditDataPatch patch, IAssetData asset, out string? error) + { + var data = asset.GetData>(); + Dictionary tempData = this.GetOldFormat(data); + Dictionary tempDataBackup = new(tempData); + patch.Edit>(new FakeAssetData(asset, this.GetOldAssetName(asset.Name), tempData)); + this.MergeIntoNewFormat(data, tempData, tempDataBackup); + + error = null; + return true; + } + + + /********* + ** Private methods + *********/ + /// Get the old asset to edit. + /// The new asset name whose locale to use. + private IAssetName GetOldAssetName(IAssetName newName) + { + return new AssetName(BlueprintsMigrator.OldAssetName, newName.LocaleCode, newName.LanguageCode); + } + + /// Get the pre-1.6 equivalent for the new asset data. + /// The data to convert. + private Dictionary GetOldFormat(IDictionary from) + { + var data = new Dictionary(); + + string[] fields = new string[19]; + foreach ((string key, BuildingData entry) in from) + { + fields[0] = this.GetOldItemsRequiredField(entry); + fields[1] = entry.Size.X.ToString(); + fields[2] = entry.Size.Y.ToString(); + fields[3] = entry.HumanDoor.X.ToString(); + fields[4] = entry.HumanDoor.Y.ToString(); + fields[5] = entry.AnimalDoor.X.ToString(); + fields[6] = entry.AnimalDoor.Y.ToString(); + fields[7] = entry.IndoorMap; + fields[8] = StardewTokenParser.ParseText(entry.Name); + fields[9] = StardewTokenParser.ParseText(entry.Description); + fields[10] = "Buildings"; // unused (blueprintType) + fields[11] = "none"; // unused (nameOfBuildingToUpgrade) + fields[12] = "0"; // unused (sourceRectForMenuView.X) + fields[13] = "0"; // unused (sourceRectForMenuView.Y) + fields[14] = entry.MaxOccupants.ToString(); + fields[15] = "null"; // unused (actionBehavior) + fields[16] = "Farm"; // unused (locations) + fields[17] = entry.BuildCost.ToString(); + fields[18] = entry.MagicalConstruction.ToString().ToLowerInvariant(); + + data[key] = string.Join('/', fields); + } + + return data; + } + + /// Merge pre-1.6 data into the new asset. + /// The asset data to update. + /// The pre-1.6 data to merge into the asset. + /// A copy of before edits were applied. + private void MergeIntoNewFormat(IDictionary asset, IDictionary from, IDictionary? fromBackup) + { + // remove deleted entries + foreach (string key in asset.Keys) + { + if (!from.ContainsKey(key)) + asset.Remove(key); + } + + // apply entries + foreach ((string key, string fromEntry) in from) + { + // get/add target record + bool isNew = false; + if (!asset.TryGetValue(key, out BuildingData? entry)) + { + isNew = true; + entry = new BuildingData() + { + Name = key, + Description = "...", + Texture = $"Buildings\\{key}" + }; + } + + // get backup + string[]? backupFields = null; + if (fromBackup is not null) + { + if (fromBackup.TryGetValue(key, out string? prevRow) && prevRow == fromEntry) + continue; // no changes + backupFields = prevRow?.Split('/'); + } + + // merge fields into new asset + { + string[] fields = fromEntry.Split('/'); + + /* + fields[14] = entry.MaxOccupants.ToString(); + fields[17] = entry.BuildCost.ToString(); + fields[18] = entry.MagicalConstruction.ToString().ToLowerInvariant(); + */ + + string rawItemsRequired = ArgUtility.Get(fields, 0); + if (rawItemsRequired != ArgUtility.Get(backupFields, 0)) + this.MergeItemsRequiredFieldIntoNewFormat(entry, rawItemsRequired); + + entry.Size = new Point( + ArgUtility.GetInt(fields, 1, entry.Size.X), + ArgUtility.GetInt(fields, 2, entry.Size.Y) + ); + entry.HumanDoor = new Point( + ArgUtility.GetInt(fields, 3, entry.HumanDoor.X), + ArgUtility.GetInt(fields, 4, entry.HumanDoor.Y) + ); + entry.AnimalDoor = new Rectangle( + ArgUtility.GetInt(fields, 5, entry.AnimalDoor.X), + ArgUtility.GetInt(fields, 6, entry.AnimalDoor.Y), + 1, + 1 + ); + + entry.IndoorMap = ArgUtility.Get(fields, 7, entry.IndoorMap, allowBlank: false); + if (string.IsNullOrWhiteSpace(entry.IndoorMap) || entry.IndoorMapType == "null") + entry.IndoorMap = null; + + { + string displayName = ArgUtility.Get(fields, 8); + if (!string.IsNullOrWhiteSpace(displayName) && displayName != ArgUtility.Get(backupFields, 8) && displayName != StardewTokenParser.ParseText(entry.Name)) + entry.Name = displayName; + } + + { + string description = ArgUtility.Get(fields, 9); + if (!string.IsNullOrWhiteSpace(description) && description != ArgUtility.Get(backupFields, 9) && description != StardewTokenParser.ParseText(entry.Description)) + entry.Description = description; + } + + entry.MaxOccupants = ArgUtility.GetInt(fields, 14, entry.MaxOccupants); + entry.BuildCost = ArgUtility.GetInt(fields, 17, entry.BuildCost); + entry.MagicalConstruction = ArgUtility.GetBool(fields, 18, entry.MagicalConstruction); + } + + // set value + if (isNew) + asset[key] = entry; + } + } + + /// Get the pre-1.6 'items required' field for the new asset data. + /// The building data. + private string GetOldItemsRequiredField(BuildingData data) + { + if (data.BuildMaterials?.Count is not > 0) + return string.Empty; + + StringBuilder result = new(); + + foreach (BuildingMaterial material in data.BuildMaterials) + { + result + .Append(RuntimeMigrationHelper.ParseObjectId(material.ItemId) ?? material.ItemId) + .Append(' ') + .Append(material.Amount) + .Append(' '); + } + + return result.ToString(0, result.Length - 1); + } + + /// Merge a pre-1.6 'items required' field into the new asset data. + /// The asset entry. + /// The field value. + private void MergeItemsRequiredFieldIntoNewFormat(BuildingData data, string field) + { + string[] fields = field.Split(' '); + + // build list + Dictionary materials = new(); + for (int i = 0; i < fields.Length - 1; i += 2) + { + string itemId = ArgUtility.Get(fields, i, allowBlank: false); + int count = ArgUtility.GetInt(fields, i + 1, 1); + + if (itemId != null) + materials[itemId] = count; + } + + // step 1: remove or update existing entries + if (data.BuildMaterials?.Count > 0) + { + for (int i = 0; i < data.BuildMaterials.Count; i++) + { + var material = data.BuildMaterials[i]; + string itemId = RuntimeMigrationHelper.ParseObjectId(material.ItemId) ?? ItemRegistry.QualifyItemId(material.ItemId); + + // remove if deleted + if (!materials.TryGetValue(itemId, out int count)) + { + data.BuildMaterials.RemoveAt(i); + i--; + } + + // else update + else + { + material.Amount = count; + materials.Remove(itemId); + } + } + } + + // step 2: add any remaining as new entries + if (materials.Count > 0) + { + data.BuildMaterials ??= new(); + + foreach ((string itemId, int amount) in materials) + { + string qualifiedItemId = ItemRegistry.ManuallyQualifyItemId(itemId, ItemRegistry.type_object); + + data.BuildMaterials.Add(new() + { + ItemId = qualifiedItemId, + Amount = amount + }); + } + } + } + } + } +} diff --git a/ContentPatcher/Framework/Migrations/Migration_2_0.ForBoots.cs b/ContentPatcher/Framework/Migrations/Migration_2_0.ForBoots.cs new file mode 100644 index 000000000..151a0dcd4 --- /dev/null +++ b/ContentPatcher/Framework/Migrations/Migration_2_0.ForBoots.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using ContentPatcher.Framework.Migrations.Internal; +using ContentPatcher.Framework.Patches; +using StardewModdingAPI; + +namespace ContentPatcher.Framework.Migrations +{ + internal partial class Migration_2_0 : BaseRuntimeMigration + { + /// The migration logic to apply pre-1.6 Data/Boots patches to the new format. + private class BootsMigrator : IEditAssetMigrator + { + /********* + ** Fields + *********/ + /// The asset name. + private const string AssetName = "Data/Boots"; + + + /********* + ** Public methods + *********/ + /// + public bool AppliesTo(IAssetName assetName) + { + return assetName?.IsEquivalentTo(BootsMigrator.AssetName, useBaseName: true) is true; + } + + /// + public IAssetName? RedirectTarget(IAssetName assetName, IPatch patch) + { + return null; // same asset name + } + + /// + public bool TryApplyLoadPatch(LoadPatch patch, IAssetName assetName, [NotNullWhen(true)] ref T? asset, out string? error) + { + var data = patch.Load>(assetName)!; + this.MigrateData(data); + asset = (T)(object)data; + + error = null; + return true; + } + + /// + public bool TryApplyEditPatch(EditDataPatch patch, IAssetData asset, out string? error) + { + var data = (Dictionary)asset.Data; + patch.Edit>(asset); + this.MigrateData(data); + + error = null; + return true; + } + + + /********* + ** Private methods + *********/ + /// Migrate pre-1.6 data to the new format. + /// The asset data to update. + private void MigrateData(IDictionary asset) + { + foreach ((string key, string fromEntry) in asset) + { + int fieldCount = RuntimeMigrationHelper.CountFields(fromEntry, '/'); + + if (fieldCount == 6) + { + string name = fromEntry[..fromEntry.IndexOf('/')]; + asset[key] = fromEntry + '/' + name; + } + } + } + } + } +} diff --git a/ContentPatcher/Framework/Migrations/Migration_2_0.cs b/ContentPatcher/Framework/Migrations/Migration_2_0.cs index 09c21bcf8..dd3711ea6 100644 --- a/ContentPatcher/Framework/Migrations/Migration_2_0.cs +++ b/ContentPatcher/Framework/Migrations/Migration_2_0.cs @@ -36,6 +36,8 @@ public Migration_2_0() this.Migrators = new IEditAssetMigrator[] { new BigCraftableInformationMigrator(), + new BlueprintsMigrator(), + new BootsMigrator(), new CropsMigrator(), new LocationsMigrator(), new NpcDispositionsMigrator(),