-
-
Notifications
You must be signed in to change notification settings - Fork 378
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add migrations for pre-1.6 Data/ObjectContextTags and Data/Weapons
- Loading branch information
1 parent
3cc6908
commit a54d004
Showing
3 changed files
with
380 additions
and
1 deletion.
There are no files selected for viewing
216 changes: 216 additions & 0 deletions
216
ContentPatcher/Framework/Migrations/Migration_2_0.ForObjectContextTags.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using ContentPatcher.Framework.Migrations.Internal; | ||
using ContentPatcher.Framework.Patches; | ||
using StardewModdingAPI; | ||
using StardewModdingAPI.Framework.Content; | ||
using StardewValley.Extensions; | ||
using StardewValley.GameData.Objects; | ||
|
||
namespace ContentPatcher.Framework.Migrations | ||
{ | ||
internal partial class Migration_2_0 : BaseRuntimeMigration | ||
{ | ||
// | ||
// Known limitation: since we're combining two different assets, it's possible some mods added the context tags | ||
// in Data/ObjectContextTags before adding the objects in Data/ObjectInformation. Unfortunately we can't add | ||
// context tags to an object which doesn't exist yet, so those context tags will be ignored. | ||
// | ||
|
||
/// <summary>The migration logic to apply pre-1.6 <c>Data/ObjectContextTags</c> patches to <c>Data/Objects</c>.</summary> | ||
private class ObjectContextTagsMigrator : IEditAssetMigrator | ||
{ | ||
/********* | ||
** Fields | ||
*********/ | ||
/// <summary>The pre-1.6 asset name.</summary> | ||
private const string OldAssetName = "Data/ObjectContextTags"; | ||
|
||
/// <summary>The 1.6 asset name.</summary> | ||
private const string NewAssetName = "Data/Objects"; | ||
|
||
|
||
/********* | ||
** Public methods | ||
*********/ | ||
/// <inheritdoc /> | ||
public bool AppliesTo(IAssetName assetName) | ||
{ | ||
return assetName?.IsEquivalentTo(ObjectContextTagsMigrator.OldAssetName, useBaseName: true) is true; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public IAssetName? RedirectTarget(IAssetName assetName, IPatch patch) | ||
{ | ||
return new AssetName(ObjectContextTagsMigrator.NewAssetName, null, null); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public bool TryApplyLoadPatch<T>(LoadPatch patch, IAssetName assetName, [NotNullWhen(true)] ref T? asset, out string? error) | ||
{ | ||
// we can't migrate Action: Load patches because the patch won't actually contain any object data | ||
// besides the context tags. | ||
error = $"can't migrate load patches for '{ObjectContextTagsMigrator.OldAssetName}' to Stardew Valley 1.6"; | ||
return false; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public bool TryApplyEditPatch<T>(EditDataPatch patch, IAssetData asset, Action<string, IMonitor> onWarning, out string? error) | ||
{ | ||
var data = asset.GetData<Dictionary<string, ObjectData>>(); | ||
Dictionary<string, string> tempData = this.GetOldFormat(data); | ||
Dictionary<string, string> tempDataBackup = new(tempData); | ||
patch.Edit<Dictionary<string, string>>(new FakeAssetData(asset, this.GetOldAssetName(asset.Name), tempData), onWarning); | ||
this.MergeIntoNewFormat(data, tempData, tempDataBackup, patch.ContentPack.Manifest.UniqueID); | ||
|
||
error = null; | ||
return true; | ||
} | ||
|
||
|
||
/********* | ||
** Private methods | ||
*********/ | ||
/// <summary>Get the old asset to edit.</summary> | ||
/// <param name="newName">The new asset name whose locale to use.</param> | ||
private IAssetName GetOldAssetName(IAssetName newName) | ||
{ | ||
return new AssetName(ObjectContextTagsMigrator.OldAssetName, newName.LocaleCode, newName.LanguageCode); | ||
} | ||
|
||
/// <summary>Get the pre-1.6 equivalent for the new asset data.</summary> | ||
/// <param name="asset">The data to convert.</param> | ||
private Dictionary<string, string> GetOldFormat(IDictionary<string, ObjectData> asset) | ||
{ | ||
var data = new Dictionary<string, string>(); | ||
|
||
foreach ((string objectId, ObjectData entry) in asset) | ||
{ | ||
if (entry.Name is null) | ||
continue; | ||
|
||
string key = this.GetOldEntryKey(objectId, entry); | ||
data[key] = entry.ContextTags?.Count > 0 | ||
? string.Join(", ", entry.ContextTags) | ||
: string.Empty; | ||
} | ||
|
||
return data; | ||
} | ||
|
||
/// <summary>Merge pre-1.6 data into the new asset.</summary> | ||
/// <param name="asset">The asset data to update.</param> | ||
/// <param name="contextTags">The pre-1.6 data to merge into the asset.</param> | ||
/// <param name="contextTagsBackup">A copy of <paramref name="contextTags"/> before edits were applied.</param> | ||
/// <param name="modId">The unique ID for the mod, used in auto-generated entry IDs.</param> | ||
private void MergeIntoNewFormat(IDictionary<string, ObjectData> asset, IDictionary<string, string> contextTags, IDictionary<string, string>? contextTagsBackup, string modId) | ||
{ | ||
// skip if no entries changed | ||
// (We can't remove unchanged entries though, since we need to combine context tags by both ID and name) | ||
if (contextTagsBackup is not null) | ||
{ | ||
bool anyChanged = false; | ||
|
||
foreach ((string oldKey, string rawTags) in contextTags) | ||
{ | ||
if (!contextTagsBackup.TryGetValue(oldKey, out string? prevRawTags) || prevRawTags != rawTags) | ||
{ | ||
anyChanged = true; | ||
break; | ||
} | ||
} | ||
|
||
if (!anyChanged) | ||
return; | ||
} | ||
|
||
// get context tags by item ID | ||
var contextTagsById = new Dictionary<string, HashSet<string>>(); | ||
{ | ||
ILookup<string, string> itemIdsByName = asset.ToLookup(p => p.Value.Name, p => p.Key); | ||
|
||
foreach ((string oldKey, string rawTags) in contextTags) | ||
{ | ||
string[] tags = rawTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.RemoveEmptyEntries); | ||
|
||
// add by ID | ||
if (oldKey.StartsWith("id_")) | ||
{ | ||
if (oldKey.StartsWith("id_o_")) | ||
{ | ||
string objectId = oldKey.Substring("id_o_".Length); | ||
this.TrackRawContextTagsById(contextTagsById, objectId, tags); | ||
} | ||
} | ||
|
||
// else by name | ||
else | ||
{ | ||
foreach (string objectId in itemIdsByName[oldKey]) | ||
this.TrackRawContextTagsById(contextTagsById, objectId, tags); | ||
} | ||
} | ||
} | ||
|
||
// merge into Data/Objects | ||
foreach ((string oldKey, HashSet<string> tags) in contextTagsById) | ||
{ | ||
// get or add matching object record | ||
if (!asset.TryGetValue(oldKey, out ObjectData? entry)) | ||
continue; | ||
|
||
// update context tags | ||
if (tags.Count == 0) | ||
entry.ContextTags?.Clear(); | ||
else | ||
{ | ||
entry.ContextTags ??= new List<string>(); | ||
entry.ContextTags.Clear(); | ||
entry.ContextTags.AddRange(tags); | ||
} | ||
} | ||
} | ||
|
||
/// <summary>Add context tags to a lookup by object ID.</summary> | ||
/// <param name="contextTagsById">The lookup to update.</param> | ||
/// <param name="objectId">The object ID whose context tags to track.</param> | ||
/// <param name="tags">The context tags to track, in addition to any already tracked for the same object ID.</param> | ||
private void TrackRawContextTagsById(Dictionary<string, HashSet<string>> contextTagsById, string objectId, string[] tags) | ||
{ | ||
// merge into previous | ||
if (contextTagsById.TryGetValue(objectId, out HashSet<string>? prevTags)) | ||
prevTags.AddRange(tags); | ||
|
||
// else add new | ||
else | ||
contextTagsById[objectId] = new HashSet<string>(tags); | ||
} | ||
|
||
/// <summary>Get the entry key in <c>Data/ObjectContextTags</c> for an entry.</summary> | ||
/// <param name="objectId">The unique object ID.</param> | ||
/// <param name="entry">The object data.</param> | ||
private string GetOldEntryKey(string objectId, ObjectData entry) | ||
{ | ||
switch (objectId) | ||
{ | ||
case "113": // Chicken Statue | ||
case "126": // Strange Doll #1 | ||
case "127": // Strange Doll #2 | ||
case "340": // Honey | ||
case "342": // Pickles | ||
case "344": // Jelly | ||
case "348": // Wine | ||
case "350": // Juice | ||
case "447": // Aged Roe | ||
case "812": // Roe | ||
return "id_0_" + objectId; // match pre-1.6 key | ||
|
||
default: | ||
return entry.Name ?? "id_0_" + objectId; | ||
} | ||
} | ||
} | ||
} | ||
} |
161 changes: 161 additions & 0 deletions
161
ContentPatcher/Framework/Migrations/Migration_2_0.ForWeapons.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using ContentPatcher.Framework.Migrations.Internal; | ||
using ContentPatcher.Framework.Patches; | ||
using StardewModdingAPI; | ||
using StardewValley; | ||
using StardewValley.GameData.Weapons; | ||
using StardewTokenParser = StardewValley.TokenizableStrings.TokenParser; | ||
|
||
namespace ContentPatcher.Framework.Migrations | ||
{ | ||
internal partial class Migration_2_0 : BaseRuntimeMigration | ||
{ | ||
/// <summary>The migration logic to apply pre-1.6 <c>Data/Weapons</c> patches to the new format.</summary> | ||
private class WeaponsMigrator : IEditAssetMigrator | ||
{ | ||
/********* | ||
** Fields | ||
*********/ | ||
/// <summary>The asset name.</summary> | ||
private const string AssetName = "Data/Weapons"; | ||
|
||
|
||
/********* | ||
** Public methods | ||
*********/ | ||
/// <inheritdoc /> | ||
public bool AppliesTo(IAssetName assetName) | ||
{ | ||
return assetName?.IsEquivalentTo(WeaponsMigrator.AssetName, useBaseName: true) is true; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public IAssetName? RedirectTarget(IAssetName assetName, IPatch patch) | ||
{ | ||
return null; // same asset name | ||
} | ||
|
||
/// <inheritdoc /> | ||
public bool TryApplyLoadPatch<T>(LoadPatch patch, IAssetName assetName, [NotNullWhen(true)] ref T? asset, out string? error) | ||
{ | ||
Dictionary<string, string> tempData = patch.Load<Dictionary<string, string>>(assetName); | ||
Dictionary<string, WeaponData> newData = new(); | ||
this.MergeIntoNewFormat(newData, tempData, null); | ||
asset = (T)(object)newData; | ||
|
||
error = null; | ||
return true; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public bool TryApplyEditPatch<T>(EditDataPatch patch, IAssetData asset, Action<string, IMonitor> onWarning, out string? error) | ||
{ | ||
var data = asset.GetData<Dictionary<string, WeaponData>>(); | ||
Dictionary<string, string> tempData = this.GetOldFormat(data); | ||
Dictionary<string, string> tempDataBackup = new(tempData); | ||
patch.Edit<Dictionary<string, string>>(new FakeAssetData(asset, asset.Name, tempData), onWarning); | ||
this.MergeIntoNewFormat(data, tempData, tempDataBackup); | ||
|
||
error = null; | ||
return true; | ||
} | ||
|
||
|
||
/********* | ||
** Private methods | ||
*********/ | ||
/// <summary>Get the pre-1.6 equivalent for the new asset data.</summary> | ||
/// <param name="from">The data to convert.</param> | ||
private Dictionary<string, string> GetOldFormat(IDictionary<string, WeaponData> from) | ||
{ | ||
var data = new Dictionary<string, string>(); | ||
|
||
string[] fields = new string[15]; | ||
foreach ((string objectId, WeaponData entry) in from) | ||
{ | ||
fields[0] = entry.Name; | ||
fields[1] = StardewTokenParser.ParseText(entry.Description); | ||
fields[2] = entry.MinDamage.ToString(); | ||
fields[3] = entry.MaxDamage.ToString(); | ||
fields[4] = entry.Knockback.ToString(); | ||
fields[5] = entry.Speed.ToString(); | ||
fields[6] = entry.Precision.ToString(); | ||
fields[7] = entry.Defense.ToString(); | ||
fields[8] = entry.Type.ToString(); | ||
fields[9] = entry.MineBaseLevel.ToString(); | ||
fields[10] = entry.MineMinLevel.ToString(); | ||
fields[11] = entry.AreaOfEffect.ToString(); | ||
fields[12] = entry.CritChance.ToString(); | ||
fields[13] = entry.CritMultiplier.ToString(); | ||
fields[14] = StardewTokenParser.ParseText(entry.DisplayName); | ||
|
||
data[objectId] = string.Join('/', fields); | ||
} | ||
|
||
return data; | ||
} | ||
|
||
/// <summary>Merge pre-1.6 data into the new asset.</summary> | ||
/// <param name="asset">The asset data to update.</param> | ||
/// <param name="from">The pre-1.6 data to merge into the asset.</param> | ||
/// <param name="fromBackup">A copy of <paramref name="from"/> before edits were applied.</param> | ||
private void MergeIntoNewFormat(IDictionary<string, WeaponData> asset, IDictionary<string, string> from, IDictionary<string, string>? fromBackup) | ||
{ | ||
// remove deleted entries | ||
foreach (string key in asset.Keys) | ||
{ | ||
if (!from.ContainsKey(key)) | ||
asset.Remove(key); | ||
} | ||
|
||
// apply entries | ||
foreach ((string objectId, string fromEntry) in from) | ||
{ | ||
// skip if unchanged | ||
string[]? backupFields = null; | ||
if (fromBackup is not null) | ||
{ | ||
if (fromBackup.TryGetValue(objectId, out string? prevRow) && prevRow == fromEntry) | ||
continue; // no changes | ||
backupFields = prevRow?.Split('/'); | ||
} | ||
|
||
// get/add target record | ||
bool isNew = false; | ||
if (!asset.TryGetValue(objectId, out WeaponData? entry)) | ||
{ | ||
isNew = true; | ||
entry = new WeaponData(); | ||
} | ||
|
||
// merge fields into new asset | ||
{ | ||
string[] fields = fromEntry.Split('/'); | ||
|
||
entry.Name = ArgUtility.Get(fields, 0, entry.Name, allowBlank: false); | ||
entry.Description = RuntimeMigrationHelper.GetValueForTokenizableFieldIfChanged(ArgUtility.Get(fields, 1), ArgUtility.Get(backupFields, 1), entry.Description); | ||
entry.MinDamage = ArgUtility.GetInt(fields, 2, entry.MinDamage); | ||
entry.MaxDamage = ArgUtility.GetInt(fields, 3, entry.MaxDamage); | ||
entry.Knockback = ArgUtility.GetFloat(fields, 4, entry.Knockback); | ||
entry.Speed = ArgUtility.GetInt(fields, 5, entry.Speed); | ||
entry.Precision = ArgUtility.GetInt(fields, 6, entry.Precision); | ||
entry.Defense = ArgUtility.GetInt(fields, 7, entry.Defense); | ||
entry.Type = ArgUtility.GetInt(fields, 8, entry.Type); | ||
entry.MineBaseLevel = ArgUtility.GetInt(fields, 9, entry.MineBaseLevel); | ||
entry.MineMinLevel = ArgUtility.GetInt(fields, 10, entry.MineMinLevel); | ||
entry.AreaOfEffect = ArgUtility.GetInt(fields, 11, entry.AreaOfEffect); | ||
entry.CritChance = ArgUtility.GetFloat(fields, 12, entry.CritChance); | ||
entry.CritMultiplier = ArgUtility.GetFloat(fields, 13, entry.CritMultiplier); | ||
entry.DisplayName = RuntimeMigrationHelper.GetValueForTokenizableFieldIfChanged(ArgUtility.Get(fields, 14), ArgUtility.Get(backupFields, 14), entry.DisplayName); | ||
} | ||
|
||
// set value | ||
if (isNew) | ||
asset[objectId] = entry; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters