Skip to content

Commit

Permalink
add migrations for pre-1.6 Data/ObjectContextTags and Data/Weapons
Browse files Browse the repository at this point in the history
  • Loading branch information
Pathoschild committed Feb 15, 2024
1 parent 12f842a commit 5a3ba6f
Show file tree
Hide file tree
Showing 3 changed files with 380 additions and 1 deletion.
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 ContentPatcher/Framework/Migrations/Migration_2_0.ForWeapons.cs
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.MigrateLiteralTextToTokenizableField(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.MigrateLiteralTextToTokenizableField(ArgUtility.Get(fields, 14), ArgUtility.Get(backupFields, 14), entry.DisplayName);
}

// set value
if (isNew)
asset[objectId] = entry;
}
}
}
}
}
4 changes: 3 additions & 1 deletion ContentPatcher/Framework/Migrations/Migration_2_0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public Migration_2_0()
new CropsMigrator(),
new LocationsMigrator(),
new NpcDispositionsMigrator(),
new ObjectInformationMigrator()
new ObjectContextTagsMigrator(),
new ObjectInformationMigrator(),
new WeaponsMigrator()
];
}

Expand Down

0 comments on commit 5a3ba6f

Please sign in to comment.