using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Graphics;
using Netcode;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using StardewValley.BellsAndWhistles;
using StardewValley.Buildings;
using StardewValley.Characters;
using StardewValley.GameData.Movies;
using StardewValley.Locations;
using StardewValley.Menus;
using StardewValley.Network;
using StardewValley.Objects;
using StardewValley.Projectiles;
using StardewValley.TerrainFeatures;
using xTile;
using xTile.Tiles;
namespace StardewModdingAPI.Metadata
{
/// <summary>Propagates changes to core assets to the game state.</summary>
internal class CoreAssetPropagator
{
/*********
** Fields
*********/
/// <summary>Normalizes an asset key to match the cache key and assert that it's valid.</summary>
private readonly Func<string, string> AssertAndNormalizeAssetName;
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection;
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
/// <summary>Optimized bucket categories for batch reloading assets.</summary>
private enum AssetBucket
{
/// <summary>NPC overworld sprites.</summary>
Sprite,
/// <summary>Villager dialogue portraits.</summary>
Portrait,
/// <summary>Any other asset.</summary>
Other
};
/*********
** Public methods
*********/
/// <summary>Initialize the core asset data.</summary>
/// <param name="assertAndNormalizeAssetName">Normalizes an asset key to match the cache key and assert that it's valid.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public CoreAssetPropagator(Func<string, string> assertAndNormalizeAssetName, Reflector reflection, IMonitor monitor)
{
this.AssertAndNormalizeAssetName = assertAndNormalizeAssetName;
this.Reflection = reflection;
this.Monitor = monitor;
}
/// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="assets">The asset keys and types to reload.</param>
/// <returns>Returns a lookup of asset names to whether they've been propagated.</returns>
public IDictionary<string, bool> Propagate(LocalizedContentManager content, IDictionary<string, Type> assets)
{
// group into optimized lists
var buckets = assets.GroupBy(p =>
{
if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters\\Monsters"))
return AssetBucket.Sprite;
if (this.IsInFolder(p.Key, "Portraits"))
return AssetBucket.Portrait;
return AssetBucket.Other;
});
// reload assets
IDictionary<string, bool> propagated = assets.ToDictionary(p => p.Key, p => false, StringComparer.OrdinalIgnoreCase);
foreach (var bucket in buckets)
{
switch (bucket.Key)
{
case AssetBucket.Sprite:
this.ReloadNpcSprites(content, bucket.Select(p => p.Key), propagated);
break;
case AssetBucket.Portrait:
this.ReloadNpcPortraits(content, bucket.Select(p => p.Key), propagated);
break;
default:
foreach (var entry in bucket)
propagated[entry.Key] = this.PropagateOther(content, entry.Key, entry.Value);
break;
}
}
return propagated;
}
/*********
** Private methods
*********/
/// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
/// <param name="type">The asset type to reload.</param>
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
private bool PropagateOther(LocalizedContentManager content, string key, Type type)
{
key = this.AssertAndNormalizeAssetName(key);
/****
** Special case: current map tilesheet
** We only need to do this for the current location, since tilesheets are reloaded when you enter a location.
** Just in case, we should still propagate by key even if a tilesheet is matched.
****/
if (Game1.currentLocation?.map?.TileSheets != null)
{
foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
{
if (this.NormalizeAssetNameIgnoringEmpty(tilesheet.ImageSource)