update asset propagation for new content API (#766)

This commit is contained in:
Jesse Plamondon-Willard 2022-03-26 17:37:01 -04:00
parent 8d70415376
commit 3a9ea66a20
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
5 changed files with 66 additions and 80 deletions

View File

@ -230,12 +230,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Perform any updates needed when the locale changes.</summary>
public void OnLocaleChanged()
{
// reload affected content
// reset baseline cache
this.ContentManagerLock.InReadLock(() =>
{
foreach (IContentManager contentManager in this.ContentManagers)
contentManager.OnLocaleChanged();
this.VanillaContentManager.Unload();
});
}

View File

@ -137,16 +137,18 @@ namespace StardewModdingAPI.Framework.ContentManagers
try
{
this.LoadExact<T>(localizedName, useCache: useCache);
T data = this.LoadExact<T>(localizedName, useCache: useCache);
LocalizedContentManager.localizedAssetNames[assetName.Name] = localizedName.Name;
return data;
}
catch (ContentLoadException)
{
localizedName = new AssetName(assetName.BaseName + "_international", null, null);
try
{
this.LoadExact<T>(localizedName, useCache: useCache);
T data = this.LoadExact<T>(localizedName, useCache: useCache);
LocalizedContentManager.localizedAssetNames[assetName.Name] = localizedName.Name;
return data;
}
catch (ContentLoadException)
{
@ -158,16 +160,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
// use cached key
string rawName = LocalizedContentManager.localizedAssetNames[assetName.Name];
if (assetName.Name != rawName)
assetName = this.Coordinator.ParseAssetName(assetName.Name);
assetName = this.Coordinator.ParseAssetName(rawName);
return this.LoadExact<T>(assetName, useCache: useCache);
}
/// <inheritdoc />
public abstract T LoadExact<T>(IAssetName assetName, bool useCache);
/// <inheritdoc />
public virtual void OnLocaleChanged() { }
/// <inheritdoc />
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
public string AssertAndNormalizeAssetName(string assetName)

View File

@ -25,9 +25,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>The assets currently being intercepted by <see cref="IAssetLoader"/> instances. This is used to prevent infinite loops when a loader loads a new asset.</summary>
private readonly ContextHash<string> AssetsBeingLoaded = new();
/// <summary>Maps asset names to their localized form, like <c>LooseSprites\Billboard => LooseSprites\Billboard.fr-FR</c> (localized) or <c>Maps\AnimalShop => Maps\AnimalShop</c> (not localized).</summary>
private IDictionary<string, string> LocalizedAssetNames => LocalizedContentManager.localizedAssetNames;
/// <summary>Whether the next load is the first for any game content manager.</summary>
private static bool IsFirstLoad = true;
@ -139,32 +136,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
return data;
}
/// <inheritdoc />
public override void OnLocaleChanged()
{
base.OnLocaleChanged();
// find assets for which a translatable version was loaded
HashSet<string> removeAssetNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string key in this.LocalizedAssetNames.Where(p => p.Key != p.Value).Select(p => p.Key))
{
IAssetName assetName = this.Coordinator.ParseAssetName(key);
removeAssetNames.Add(assetName.BaseName);
}
// invalidate translatable assets
string[] invalidated = this
.InvalidateCache((key, _) =>
removeAssetNames.Contains(key)
|| removeAssetNames.Contains(this.Coordinator.ParseAssetName(key).BaseName)
)
.Select(p => p.Key)
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (invalidated.Any())
this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.");
}
/// <inheritdoc />
public override LocalizedContentManager CreateTemporary()
{

View File

@ -65,8 +65,5 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the invalidated asset names and instances.</returns>
IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false);
/// <summary>Perform any cleanup needed when the locale changes.</summary>
void OnLocaleChanged();
}
}

View File

@ -89,6 +89,14 @@ namespace StardewModdingAPI.Metadata
/// <param name="updatedNpcWarps">Whether the NPC pathfinding cache was reloaded.</param>
public void Propagate(IDictionary<IAssetName, Type> assets, bool ignoreWorld, out IDictionary<IAssetName, bool> propagatedAssets, out bool updatedNpcWarps)
{
// get base name lookup
propagatedAssets = assets
.Select(asset => asset.Key.GetBaseAssetName())
.Distinct()
.ToDictionary(name => name, _ => false);
this.Monitor.Log($"Propagating: {propagatedAssets.Keys.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)}", LogLevel.Alert);
// group into optimized lists
var buckets = assets.GroupBy(p =>
{
@ -102,7 +110,6 @@ namespace StardewModdingAPI.Metadata
});
// reload assets
propagatedAssets = assets.ToDictionary(p => p.Key, _ => false);
updatedNpcWarps = false;
foreach (var bucket in buckets)
{
@ -110,12 +117,12 @@ namespace StardewModdingAPI.Metadata
{
case AssetBucket.Sprite:
if (!ignoreWorld)
this.ReloadNpcSprites(bucket.Select(p => p.Key), propagatedAssets);
this.ReloadNpcSprites(propagatedAssets);
break;
case AssetBucket.Portrait:
if (!ignoreWorld)
this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagatedAssets);
this.ReloadNpcPortraits(propagatedAssets);
break;
default:
@ -158,7 +165,7 @@ namespace StardewModdingAPI.Metadata
private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarps)
{
var content = this.MainContentManager;
string key = assetName.Name;
string key = assetName.BaseName;
changedWarps = false;
/****
@ -170,7 +177,7 @@ namespace StardewModdingAPI.Metadata
{
foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
{
if (assetName.IsEquivalentTo(tilesheet.ImageSource))
if (this.IsSameBaseName(assetName, tilesheet.ImageSource))
Game1.mapDisplayDevice.LoadTileSheet(tilesheet);
}
}
@ -188,7 +195,7 @@ namespace StardewModdingAPI.Metadata
{
GameLocation location = info.Location;
if (assetName.IsEquivalentTo(location.mapPath.Value))
if (this.IsSameBaseName(assetName, location.mapPath.Value))
{
static ISet<string> GetWarpSet(GameLocation location)
{
@ -213,7 +220,7 @@ namespace StardewModdingAPI.Metadata
/****
** Propagate by key
****/
switch (assetName.Name.ToLower().Replace("\\", "/")) // normalized key so we can compare statically
switch (assetName.BaseName.ToLower().Replace("\\", "/")) // normalized key so we can compare statically
{
/****
** Animals
@ -624,7 +631,7 @@ namespace StardewModdingAPI.Metadata
{
if (Game1.activeClickableMenu is TitleMenu titleMenu)
{
Texture2D texture = content.Load<Texture2D>(assetName.Name);
Texture2D texture = content.Load<Texture2D>(assetName.BaseName);
titleMenu.titleButtonsTexture = texture;
titleMenu.backButton.texture = texture;
@ -652,13 +659,13 @@ namespace StardewModdingAPI.Metadata
// find matches
TAnimal[] animals = this.GetCharacters()
.OfType<TAnimal>()
.Where(p => assetName.IsEquivalentTo(p.Sprite?.Texture?.Name))
.Where(p => this.IsSameBaseName(assetName, p.Sprite?.Texture?.Name))
.ToArray();
if (!animals.Any())
return false;
// update sprites
Texture2D texture = content.Load<Texture2D>(assetName.Name);
Texture2D texture = content.Load<Texture2D>(assetName.BaseName);
foreach (TAnimal animal in animals)
animal.Sprite.spriteTexture = texture;
return true;
@ -677,7 +684,7 @@ namespace StardewModdingAPI.Metadata
return false;
// update sprites
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
foreach (FarmAnimal animal in animals)
{
// get expected key
@ -689,7 +696,7 @@ namespace StardewModdingAPI.Metadata
expectedKey = $"Animals/{expectedKey}";
// reload asset
if (assetName.IsEquivalentTo(expectedKey))
if (this.IsSameBaseName(assetName, expectedKey))
animal.Sprite.spriteTexture = texture.Value;
}
return texture.IsValueCreated;
@ -705,7 +712,7 @@ namespace StardewModdingAPI.Metadata
bool isPaintMask = assetName.BaseName.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase);
// get building type
string type = Path.GetFileName(assetName.Name)!;
string type = Path.GetFileName(assetName.BaseName)!;
if (isPaintMask)
type = type.Substring(0, type.Length - paintMaskSuffix.Length);
@ -738,7 +745,7 @@ namespace StardewModdingAPI.Metadata
/// <returns>Returns whether any textures were reloaded.</returns>
private bool ReloadChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld)
{
MapSeat.mapChairTexture = content.Load<Texture2D>(assetName.Name);
MapSeat.mapChairTexture = content.Load<Texture2D>(assetName.BaseName);
if (!ignoreWorld)
{
@ -746,7 +753,7 @@ namespace StardewModdingAPI.Metadata
{
foreach (MapSeat seat in location.mapSeats.Where(p => p != null))
{
if (assetName.IsEquivalentTo(seat._loadedTextureFile))
if (this.IsSameBaseName(assetName, seat._loadedTextureFile))
seat.overlayTexture = MapSeat.mapChairTexture;
}
}
@ -767,7 +774,7 @@ namespace StardewModdingAPI.Metadata
from location in this.GetLocations()
where location.critters != null
from Critter critter in location.critters
where assetName.IsEquivalentTo(critter.sprite?.Texture?.Name)
where this.IsSameBaseName(assetName, critter.sprite?.Texture?.Name)
select critter
)
.ToArray();
@ -775,7 +782,7 @@ namespace StardewModdingAPI.Metadata
return 0;
// update sprites
Texture2D texture = content.Load<Texture2D>(assetName.Name);
Texture2D texture = content.Load<Texture2D>(assetName.BaseName);
foreach (var entry in critters)
entry.sprite.spriteTexture = texture;
@ -788,7 +795,7 @@ namespace StardewModdingAPI.Metadata
/// <returns>Returns whether any doors were affected.</returns>
private bool ReloadDoorSprites(LocalizedContentManager content, IAssetName assetName)
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
foreach (GameLocation location in this.GetLocations())
{
@ -802,7 +809,7 @@ namespace StardewModdingAPI.Metadata
continue;
string curKey = this.Reflection.GetField<string>(door.Sprite, "textureName").GetValue();
if (assetName.IsEquivalentTo(curKey))
if (this.IsSameBaseName(assetName, curKey))
door.Sprite.texture = texture.Value;
}
}
@ -862,14 +869,14 @@ namespace StardewModdingAPI.Metadata
(
from location in this.GetLocations()
from grass in location.terrainFeatures.Values.OfType<Grass>()
where assetName.IsEquivalentTo(grass.textureName())
where this.IsSameBaseName(assetName, grass.textureName())
select grass
)
.ToArray();
if (grasses.Any())
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
foreach (Grass grass in grasses)
grass.texture = texture;
return true;
@ -935,7 +942,7 @@ namespace StardewModdingAPI.Metadata
/// <returns>Returns whether any NPCs were affected.</returns>
private bool ReloadNpcDispositions(LocalizedContentManager content, IAssetName assetName)
{
IDictionary<string, string> data = content.Load<Dictionary<string, string>>(assetName.Name);
IDictionary<string, string> data = content.Load<Dictionary<string, string>>(assetName.BaseName);
bool changed = false;
foreach (NPC npc in this.GetCharacters())
{
@ -950,15 +957,14 @@ namespace StardewModdingAPI.Metadata
}
/// <summary>Reload the sprites for matching NPCs.</summary>
/// <param name="keys">The asset keys to reload.</param>
/// <param name="propagated">The asset keys which have been propagated.</param>
private void ReloadNpcSprites(IEnumerable<IAssetName> keys, IDictionary<IAssetName, bool> propagated)
/// <param name="propagated">The asset keys which are being propagated.</param>
private void ReloadNpcSprites(IDictionary<IAssetName, bool> propagated)
{
// get NPCs
var characters =
(
from npc in this.GetCharacters()
let key = this.ParseAssetNameOrNull(npc.Sprite?.Texture?.Name)
let key = this.ParseAssetNameOrNull(npc.Sprite?.Texture?.Name)?.GetBaseAssetName()
where key != null && propagated.ContainsKey(key)
select new { Npc = npc, AssetName = key }
)
@ -969,15 +975,14 @@ namespace StardewModdingAPI.Metadata
// update sprite
foreach (var target in characters)
{
target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.AssetName.Name);
target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.AssetName.BaseName);
propagated[target.AssetName] = true;
}
}
/// <summary>Reload the portraits for matching NPCs.</summary>
/// <param name="keys">The asset key to reload.</param>
/// <param name="propagated">The asset keys which have been propagated.</param>
private void ReloadNpcPortraits(IEnumerable<IAssetName> keys, IDictionary<IAssetName, bool> propagated)
/// <param name="propagated">The asset keys which are being propagated.</param>
private void ReloadNpcPortraits(IDictionary<IAssetName, bool> propagated)
{
// get NPCs
var characters =
@ -985,7 +990,7 @@ namespace StardewModdingAPI.Metadata
from npc in this.GetCharacters()
where npc.isVillager()
let key = this.ParseAssetNameOrNull(npc.Portrait?.Name)
let key = this.ParseAssetNameOrNull(npc.Portrait?.Name)?.GetBaseAssetName()
where key != null && propagated.ContainsKey(key)
select new { Npc = npc, AssetName = key }
)
@ -1005,7 +1010,7 @@ namespace StardewModdingAPI.Metadata
// update portrait
foreach (var target in characters)
{
target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.AssetName.Name);
target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.AssetName.BaseName);
propagated[target.AssetName] = true;
}
}
@ -1017,7 +1022,7 @@ namespace StardewModdingAPI.Metadata
Farmer[] players =
(
from player in Game1.getOnlineFarmers()
where assetName.IsEquivalentTo(player.getTexture())
where this.IsSameBaseName(assetName, player.getTexture())
select player
)
.ToArray();
@ -1037,7 +1042,7 @@ namespace StardewModdingAPI.Metadata
/// <returns>Returns whether any textures were reloaded.</returns>
private bool ReloadSuspensionBridges(LocalizedContentManager content, IAssetName assetName)
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
foreach (GameLocation location in this.GetLocations(buildingInteriors: false))
{
@ -1068,7 +1073,7 @@ namespace StardewModdingAPI.Metadata
if (trees.Any())
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.Name));
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
foreach (Tree tree in trees)
tree.texture = texture;
return true;
@ -1086,7 +1091,7 @@ namespace StardewModdingAPI.Metadata
private bool ReloadNpcDialogue(IAssetName assetName)
{
// get NPCs
string name = Path.GetFileName(assetName.Name);
string name = Path.GetFileName(assetName.BaseName);
NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray();
if (!villagers.Any())
return false;
@ -1116,7 +1121,7 @@ namespace StardewModdingAPI.Metadata
private bool ReloadNpcSchedules(IAssetName assetName)
{
// get NPCs
string name = Path.GetFileName(assetName.Name);
string name = Path.GetFileName(assetName.BaseName);
NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray();
if (!villagers.Any())
return false;
@ -1230,6 +1235,23 @@ namespace StardewModdingAPI.Metadata
}
}
/// <summary>Get whether two asset names are equivalent if you ignore the locale code.</summary>
/// <param name="left">The first value to compare.</param>
/// <param name="right">The second value to compare.</param>
private bool IsSameBaseName(IAssetName left, string right)
{
IAssetName parsedB = this.ParseAssetNameOrNull(right);
return this.IsSameBaseName(left, parsedB);
}
/// <summary>Get whether two asset names are equivalent if you ignore the locale code.</summary>
/// <param name="left">The first value to compare.</param>
/// <param name="right">The second value to compare.</param>
private bool IsSameBaseName(IAssetName left, IAssetName right)
{
return left.IsEquivalentTo(right.BaseName, useBaseName: true);
}
/// <summary>Normalize an asset key to match the cache key and assert that it's valid, but don't raise an error for null or empty values.</summary>
/// <param name="path">The asset key to normalize.</param>
private IAssetName ParseAssetNameOrNull(string path)
@ -1281,7 +1303,7 @@ namespace StardewModdingAPI.Metadata
BuildingPainter.paintMaskLookup = new Dictionary<string, List<List<int>>>(BuildingPainter.paintMaskLookup, StringComparer.OrdinalIgnoreCase);
// remove key from cache
return BuildingPainter.paintMaskLookup.Remove(assetName.Name);
return BuildingPainter.paintMaskLookup.Remove(assetName.BaseName);
}
/// <summary>Metadata about a location used in asset propagation.</summary>