summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-05-22 14:38:57 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-05-22 14:38:57 -0400
commit2ab2182645179129997eac3fccb63f6f0683dbe1 (patch)
treeddea4c6e4531a10c698d9757a57ae24c6732bff7 /src
parent5731b015a0c548ac72e0d7ce9c4153aa52da3562 (diff)
parent336cc1cc0f250c96ee23d45e1e08569b67a2e562 (diff)
downloadSMAPI-2ab2182645179129997eac3fccb63f6f0683dbe1.tar.gz
SMAPI-2ab2182645179129997eac3fccb63f6f0683dbe1.tar.bz2
SMAPI-2ab2182645179129997eac3fccb63f6f0683dbe1.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs25
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs7
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs41
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs8
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs3
-rw-r--r--src/SMAPI.Web/Startup.cs3
-rw-r--r--src/SMAPI.Web/appsettings.json3
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs36
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs27
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs2
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs11
-rw-r--r--src/SMAPI/Framework/Utilities/TickCacheDictionary.cs26
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs607
17 files changed, 459 insertions, 354 deletions
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 78d26802..7b403a75 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.14.4",
+ "Version": "3.14.5",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.14.4"
+ "MinimumApiVersion": "3.14.5"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index be1030f4..2ac959bb 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
- "Version": "3.14.4",
+ "Version": "3.14.5",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.14.4"
+ "MinimumApiVersion": "3.14.5"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 8a50162e..707b6d8a 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.14.4",
+ "Version": "3.14.5",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.14.4"
+ "MinimumApiVersion": "3.14.5"
}
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
index d351b42d..9b4f2580 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Pathoschild.Http.Client;
@@ -33,9 +34,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
/// <summary>Construct an instance.</summary>
/// <param name="userAgent">The user agent for the API client.</param>
/// <param name="apiUrl">The base URL for the CurseForge API.</param>
- public CurseForgeClient(string userAgent, string apiUrl)
+ /// <param name="apiKey">The API authentication key.</param>
+ public CurseForgeClient(string userAgent, string apiUrl, string apiKey)
{
- this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent);
+ this.Client = new FluentClient(apiUrl)
+ .SetUserAgent(userAgent)
+ .AddDefault(request => request.WithHeader("x-api-key", apiKey));
}
/// <summary>Get update check info about a mod.</summary>
@@ -49,11 +53,18 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID.");
// get raw data
- ModModel? mod = await this.Client
- .GetAsync($"addon/{parsedId}")
- .As<ModModel?>();
- if (mod == null)
+ ModModel? mod;
+ try
+ {
+ ResponseModel<ModModel> response = await this.Client
+ .GetAsync($"mods/{parsedId}")
+ .As<ResponseModel<ModModel>>();
+ mod = response.Data;
+ }
+ catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
+ {
return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID.");
+ }
// get downloads
List<IModDownload> downloads = new List<IModDownload>();
@@ -65,7 +76,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
}
// return info
- return page.SetInfo(name: mod.Name, version: null, url: mod.WebsiteUrl, downloads: downloads);
+ return page.SetInfo(name: mod.Name, version: null, url: mod.Links.WebsiteUrl, downloads: downloads);
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs
new file mode 100644
index 00000000..2f9abe4f
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs
@@ -0,0 +1,7 @@
+namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
+{
+ /// <summary>A list of links for a mod.</summary>
+ /// <param name="WebsiteUrl">The URL for the CurseForge mod page.</param>
+ /// <param name="SourceUrl">The URL for the mod's source code, if any.</param>
+ public record ModLinksModel(string WebsiteUrl, string? SourceUrl);
+}
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
index fd7796f2..7018be54 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
@@ -1,38 +1,9 @@
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
{
- /// <summary>An mod from the CurseForge API.</summary>
- public class ModModel
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The mod's unique ID on CurseForge.</summary>
- public int ID { get; }
-
- /// <summary>The mod name.</summary>
- public string Name { get; }
-
- /// <summary>The web URL for the mod page.</summary>
- public string WebsiteUrl { get; }
-
- /// <summary>The available file downloads.</summary>
- public ModFileModel[] LatestFiles { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="id">The mod's unique ID on CurseForge.</param>
- /// <param name="name">The mod name.</param>
- /// <param name="websiteUrl">The web URL for the mod page.</param>
- /// <param name="latestFiles">The available file downloads.</param>
- public ModModel(int id, string name, string websiteUrl, ModFileModel[] latestFiles)
- {
- this.ID = id;
- this.Name = name;
- this.WebsiteUrl = websiteUrl;
- this.LatestFiles = latestFiles;
- }
- }
+ /// <summary>A mod from the CurseForge API.</summary>
+ /// <param name="Id">The mod's unique ID on CurseForge.</param>
+ /// <param name="Name">The mod name.</param>
+ /// <param name="LatestFiles">The available file downloads.</param>
+ /// <param name="Links">The URLs for this mod.</param>
+ public record ModModel(int Id, string Name, ModFileModel[] LatestFiles, ModLinksModel Links);
}
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs
new file mode 100644
index 00000000..4d538a93
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs
@@ -0,0 +1,8 @@
+using Newtonsoft.Json;
+
+namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
+{
+ /// <summary>A response from the CurseForge API.</summary>
+ /// <param name="Data">The data returned by the API.</param>
+ public record ResponseModel<TData>(TData Data);
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
index b582b2b0..ebb3618a 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
@@ -42,6 +42,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The base URL for the CurseForge API.</summary>
public string CurseForgeBaseUrl { get; set; } = null!;
+ /// <summary>The API authentication key for the CurseForge API.</summary>
+ public string CurseForgeApiKey { get; set; } = null!;
+
/****
** GitHub
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 2693aa90..9980d00c 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -111,7 +111,8 @@ namespace StardewModdingAPI.Web
services.AddSingleton<ICurseForgeClient>(new CurseForgeClient(
userAgent: userAgent,
- apiUrl: api.CurseForgeBaseUrl
+ apiUrl: api.CurseForgeBaseUrl,
+ apiKey: api.CurseForgeApiKey
));
services.AddSingleton<IGitHubClient>(new GitHubClient(
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 1231f824..10fcbe9c 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -31,7 +31,8 @@
"ChucklefishBaseUrl": "https://community.playstarbound.com",
"ChucklefishModPageUrlFormat": "resources/{0}",
- "CurseForgeBaseUrl": "https://addons-ecs.forgesvc.net/api/v2/",
+ "CurseForgeBaseUrl": "https://api.curseforge.com/v1/",
+ "CurseForgeApiKey": null,
"GitHubBaseUrl": "https://api.github.com",
"GitHubAcceptHeader": "application/vnd.github.v3+json",
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 357b8db8..b2916a8d 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -50,7 +50,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.14.4";
+ internal static string RawApiVersion = "3.14.5";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 2b13f57a..fc61b44b 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -151,8 +151,23 @@ namespace StardewModdingAPI.Framework
onAssetLoaded: onAssetLoaded
)
);
+
+ var contentManagerForAssetPropagation = new GameContentManagerForAssetPropagation(
+ name: nameof(GameContentManagerForAssetPropagation),
+ serviceProvider: serviceProvider,
+ rootDirectory: rootDirectory,
+ currentCulture: currentCulture,
+ coordinator: this,
+ monitor: monitor,
+ reflection: reflection,
+ onDisposing: this.OnDisposing,
+ onLoadingFirstAsset: onLoadingFirstAsset,
+ onAssetLoaded: onAssetLoaded
+ );
+ this.ContentManagers.Add(contentManagerForAssetPropagation);
+
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
- this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, this.Monitor, reflection, name => this.ParseAssetName(name, allowLocales: true));
+ this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, name => this.ParseAssetName(name, allowLocales: true));
this.LocaleCodes = new Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>>(() => this.GetLocaleCodes(customLanguages: Enumerable.Empty<ModLanguage>()));
}
@@ -379,14 +394,31 @@ namespace StardewModdingAPI.Framework
// cached assets
foreach (IContentManager contentManager in this.ContentManagers)
{
- foreach ((string key, object asset) in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose))
+ foreach ((string key, object asset) in contentManager.GetCachedAssets())
{
+ if (!predicate(contentManager, key, asset.GetType()))
+ continue;
+
AssetName assetName = this.ParseAssetName(key, allowLocales: true);
+ contentManager.InvalidateCache(assetName, dispose);
+
if (!invalidatedAssets.ContainsKey(assetName))
invalidatedAssets[assetName] = asset.GetType();
}
}
+ // forget localized flags
+ // A mod might provide a localized variant of a normally non-localized asset (like
+ // `Maps/MovieTheater.fr-FR`). When the asset is invalidated, we need to recheck
+ // whether the asset is localized in case it stops providing it.
+ foreach (IAssetName assetName in invalidatedAssets.Keys)
+ {
+ LocalizedContentManager.localizedAssetNames.Remove(assetName.Name);
+
+ if (LocalizedContentManager.localizedAssetNames.TryGetValue(assetName.BaseName, out string? targetForBaseKey) && targetForBaseKey == assetName.Name)
+ LocalizedContentManager.localizedAssetNames.Remove(assetName.BaseName);
+ }
+
// special case: maps may be loaded through a temporary content manager that's removed while the map is still in use.
// This notably affects the town and farmhouse maps.
if (Game1.locations != null)
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index 575d252e..ddc02a8c 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -231,24 +231,21 @@ namespace StardewModdingAPI.Framework.ContentManagers
** Cache invalidation
****/
/// <inheritdoc />
- public IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
+ public IEnumerable<KeyValuePair<string, object>> GetCachedAssets()
{
- IDictionary<string, object> removeAssets = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
- this.Cache.Remove((key, asset) =>
- {
- string baseAssetName = this.Coordinator.ParseAssetName(key, allowLocales: this.TryLocalizeKeys).BaseName;
+ foreach (string key in this.Cache.Keys)
+ yield return new(key, this.Cache[key]);
+ }
- // check if asset should be removed
- bool remove = removeAssets.ContainsKey(baseAssetName);
- if (!remove && predicate(baseAssetName, asset.GetType()))
- {
- removeAssets[baseAssetName] = asset;
- remove = true;
- }
- return remove;
- }, dispose);
+ /// <inheritdoc />
+ public bool InvalidateCache(IAssetName assetName, bool dispose = false)
+ {
+ if (!this.Cache.ContainsKey(assetName.Name))
+ return false;
- return removeAssets;
+ // remove from cache
+ this.Cache.Remove(assetName.Name, dispose);
+ return true;
}
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 1c603f85..4390d472 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -75,7 +75,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// custom asset from a loader
string locale = this.GetLocale();
IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName);
- AssetOperationGroup? operations = this.Coordinator.GetAssetOperations<object>(info);
+ AssetOperationGroup? operations = this.Coordinator.GetAssetOperations<T>(info);
if (operations?.LoadOperations.Count > 0)
{
if (!this.AssertMaxOneRequiredLoader(info, operations.LoadOperations, out string? error))
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index ac67cad5..f2e3b9f0 100644
--- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <typeparam name="T">The expected asset type.</typeparam>
/// <param name="assetName">The normalized asset name.</param>
bool DoesAssetExist<T>(IAssetName assetName)
- where T: notnull;
+ where T : notnull;
/// <summary>Load an asset through the content pipeline, using a localized variant of the <paramref name="assetName"/> if available.</summary>
/// <typeparam name="T">The type of asset to load.</typeparam>
@@ -65,10 +65,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
bool IsLoaded(IAssetName assetName);
+ /// <summary>Get all assets in the cache.</summary>
+ IEnumerable<KeyValuePair<string, object>> GetCachedAssets();
+
/// <summary>Purge matched assets from the cache.</summary>
- /// <param name="predicate">Matches the asset keys to invalidate.</param>
+ /// <param name="assetName">The asset name to dispose.</param>
/// <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);
+ /// <returns>Returns whether the asset was in the cache.</returns>
+ bool InvalidateCache(IAssetName assetName, bool dispose = false);
}
}
diff --git a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
index 20d206e2..7732ace8 100644
--- a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
+++ b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
@@ -48,4 +48,30 @@ namespace StardewModdingAPI.Framework.Utilities
return this.Cache.Remove(cacheKey);
}
}
+
+ /// <summary>An in-memory dictionary cache that stores data for the duration of a game update tick.</summary>
+ /// <typeparam name="TKey">The dictionary key type.</typeparam>
+ internal class TickCacheDictionary<TKey> : TickCacheDictionary<TKey, object>
+ where TKey : notnull
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get a value from the cache, fetching it first if it's not cached yet.</summary>
+ /// <param name="cacheKey">The unique key for the cached value.</param>
+ /// <param name="get">Get the latest data if it's not in the cache yet.</param>
+ public TValue GetOrSet<TValue>(TKey cacheKey, Func<TValue> get)
+ {
+ object? value = base.GetOrSet(cacheKey, () => get()!);
+
+ try
+ {
+ return (TValue)value;
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidCastException($"Can't cast value of the '{cacheKey}' cache entry from {value?.GetType().FullName ?? "null"} to {typeof(TValue).FullName}.", ex);
+ }
+ }
+ }
}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index e014f9a9..8ed6b591 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -5,7 +5,9 @@ using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
@@ -32,6 +34,9 @@ namespace StardewModdingAPI.Metadata
/// <summary>The main content manager through which to reload assets.</summary>
private readonly LocalizedContentManager MainContentManager;
+ /// <summary>An internal content manager used only for asset propagation. See remarks on <see cref="GameContentManagerForAssetPropagation"/>.</summary>
+ private readonly GameContentManagerForAssetPropagation DisposableContentManager;
+
/// <summary>Writes messages to the console.</summary>
private readonly IMonitor Monitor;
@@ -54,18 +59,23 @@ namespace StardewModdingAPI.Metadata
Other
};
+ /// <summary>A cache of world data fetched for the current tick.</summary>
+ private readonly TickCacheDictionary<string> WorldCache = new();
+
/*********
** Public methods
*********/
/// <summary>Initialize the core asset data.</summary>
/// <param name="mainContent">The main content manager through which to reload assets.</param>
+ /// <param name="disposableContent">An internal content manager used only for asset propagation.</param>
/// <param name="monitor">Writes messages to the console.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="parseAssetName">Parse a raw asset name.</param>
- public CoreAssetPropagator(LocalizedContentManager mainContent, IMonitor monitor, Reflector reflection, Func<string, IAssetName> parseAssetName)
+ public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Reflector reflection, Func<string, IAssetName> parseAssetName)
{
this.MainContentManager = mainContent;
+ this.DisposableContentManager = disposableContent;
this.Monitor = monitor;
this.Reflection = reflection;
this.ParseAssetName = parseAssetName;
@@ -104,12 +114,12 @@ namespace StardewModdingAPI.Metadata
{
case AssetBucket.Sprite:
if (!ignoreWorld)
- this.ReloadNpcSprites(propagatedAssets);
+ this.UpdateNpcSprites(propagatedAssets);
break;
case AssetBucket.Portrait:
if (!ignoreWorld)
- this.ReloadNpcPortraits(propagatedAssets);
+ this.UpdateNpcPortraits(propagatedAssets);
break;
default:
@@ -192,7 +202,7 @@ namespace StardewModdingAPI.Metadata
}
var oldWarps = GetWarpSet(location);
- this.ReloadMap(info);
+ this.UpdateMap(info);
var newWarps = GetWarpSet(location);
changedWarps = changedWarps || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p));
@@ -213,7 +223,7 @@ namespace StardewModdingAPI.Metadata
** Animals
****/
case "animals/horse":
- return !ignoreWorld && this.ReloadPetOrHorseSprites<Horse>(content, assetName);
+ return !ignoreWorld && this.UpdatePetOrHorseSprites<Horse>(assetName);
/****
** Buildings
@@ -243,7 +253,7 @@ namespace StardewModdingAPI.Metadata
case "characters/farmer/farmer_base_bald":
case "characters/farmer/farmer_girl_base":
case "characters/farmer/farmer_girl_base_bald":
- return !ignoreWorld && this.ReloadPlayerSprites(assetName);
+ return !ignoreWorld && this.UpdatePlayerSprites(assetName);
case "characters/farmer/hairstyles": // Game1.LoadContent
FarmerRenderer.hairStylesTexture = this.LoadTexture(key);
@@ -295,10 +305,10 @@ namespace StardewModdingAPI.Metadata
return true;
case "data/farmanimals": // FarmAnimal constructor
- return !ignoreWorld && this.ReloadFarmAnimalData();
+ return !ignoreWorld && this.UpdateFarmAnimalData();
case "data/hairdata": // Farmer.GetHairStyleMetadataFile
- return this.ReloadHairData();
+ return this.UpdateHairData();
case "data/movies": // MovieTheater.GetMovieData
case "data/moviesreactions": // MovieTheater.GetMovieReactions
@@ -306,7 +316,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "data/npcdispositions": // NPC constructor
- return !ignoreWorld && this.ReloadNpcDispositions(content, assetName);
+ return !ignoreWorld && this.UpdateNpcDispositions(content, assetName);
case "data/npcgifttastes": // Game1.LoadContent
Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
@@ -386,7 +396,7 @@ namespace StardewModdingAPI.Metadata
}
if (!ignoreWorld)
- this.ReloadDoorSprites(content, assetName);
+ this.UpdateDoorSprites(content, assetName);
return true;
case "loosesprites/cursors2": // Game1.LoadContent
@@ -418,7 +428,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "loosesprites/suspensionbridge": // SuspensionBridge constructor
- return !ignoreWorld && this.ReloadSuspensionBridges(content, assetName);
+ return !ignoreWorld && this.UpdateSuspensionBridges(content, assetName);
/****
** Content\Maps
@@ -449,13 +459,13 @@ namespace StardewModdingAPI.Metadata
return false;
case "minigames/titlebuttons": // TitleMenu
- return this.ReloadTitleButtons(content, assetName);
+ return this.UpdateTitleButtons(content, assetName);
/****
** Content\Strings
****/
case "strings/stringsfromcsfiles":
- return this.ReloadStringsFromCsFiles(content);
+ return this.UpdateStringsFromCsFiles(content);
/****
** Content\TileSheets
@@ -473,14 +483,14 @@ namespace StardewModdingAPI.Metadata
return true;
case "tilesheets/chairtiles": // Game1.LoadContent
- return this.ReloadChairTiles(content, assetName, ignoreWorld);
+ return this.UpdateChairTiles(content, assetName, ignoreWorld);
case "tilesheets/craftables": // Game1.LoadContent
Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
return true;
case "tilesheets/critters": // Critter constructor
- return !ignoreWorld && this.ReloadCritterTextures(content, assetName) > 0;
+ return !ignoreWorld && this.UpdateCritterTextures(assetName);
case "tilesheets/crops": // Game1.LoadContent
Game1.cropSpriteSheet = content.Load<Texture2D>(key);
@@ -534,7 +544,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "terrainfeatures/grass": // from Grass
- return !ignoreWorld && this.ReloadGrassTextures(content, assetName);
+ return !ignoreWorld && this.UpdateGrassTextures(content, assetName);
case "terrainfeatures/hoedirt": // from HoeDirt
HoeDirt.lightTexture = content.Load<Texture2D>(key);
@@ -549,27 +559,27 @@ namespace StardewModdingAPI.Metadata
return true;
case "terrainfeatures/mushroom_tree": // from Tree
- return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.mushroomTree);
+ return !ignoreWorld && this.UpdateTreeTextures(Tree.mushroomTree);
case "terrainfeatures/tree_palm": // from Tree
- return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.palmTree);
+ return !ignoreWorld && this.UpdateTreeTextures(Tree.palmTree);
case "terrainfeatures/tree1_fall": // from Tree
case "terrainfeatures/tree1_spring": // from Tree
case "terrainfeatures/tree1_summer": // from Tree
case "terrainfeatures/tree1_winter": // from Tree
- return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.bushyTree);
+ return !ignoreWorld && this.UpdateTreeTextures(Tree.bushyTree);
case "terrainfeatures/tree2_fall": // from Tree
case "terrainfeatures/tree2_spring": // from Tree
case "terrainfeatures/tree2_summer": // from Tree
case "terrainfeatures/tree2_winter": // from Tree
- return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.leafyTree);
+ return !ignoreWorld && this.UpdateTreeTextures(Tree.leafyTree);
case "terrainfeatures/tree3_fall": // from Tree
case "terrainfeatures/tree3_spring": // from Tree
case "terrainfeatures/tree3_winter": // from Tree
- return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.pineTree);
+ return !ignoreWorld && this.UpdateTreeTextures(Tree.pineTree);
}
/****
@@ -579,24 +589,24 @@ namespace StardewModdingAPI.Metadata
{
// dynamic textures
if (assetName.StartsWith("animals/cat"))
- return this.ReloadPetOrHorseSprites<Cat>(content, assetName);
+ return this.UpdatePetOrHorseSprites<Cat>(assetName);
if (assetName.StartsWith("animals/dog"))
- return this.ReloadPetOrHorseSprites<Dog>(content, assetName);
+ return this.UpdatePetOrHorseSprites<Dog>(assetName);
if (assetName.IsDirectlyUnderPath("Animals"))
- return this.ReloadFarmAnimalSprites(content, assetName);
+ return this.UpdateFarmAnimalSprites(assetName);
if (assetName.IsDirectlyUnderPath("Buildings"))
- return this.ReloadBuildings(assetName);
+ return this.UpdateBuildings(assetName);
if (assetName.StartsWith("LooseSprites/Fence"))
- return this.ReloadFenceTextures(assetName);
+ return this.UpdateFenceTextures(assetName);
// dynamic data
if (assetName.IsDirectlyUnderPath("Characters/Dialogue"))
- return this.ReloadNpcDialogue(assetName);
+ return this.UpdateNpcDialogue(assetName);
if (assetName.IsDirectlyUnderPath("Characters/schedules"))
- return this.ReloadNpcSchedules(assetName);
+ return this.UpdateNpcSchedules(assetName);
}
return false;
@@ -607,14 +617,14 @@ namespace StardewModdingAPI.Metadata
** Private methods
*********/
/****
- ** Reload texture methods
+ ** Update texture methods
****/
- /// <summary>Reload buttons on the title screen.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
+ /// <summary>Update buttons on the title screen.</summary>
+ /// <param name="content">The content manager through which to update the asset.</param>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
/// <remarks>Derived from the <see cref="TitleMenu"/> constructor and <see cref="TitleMenu.setUpIcons"/>.</remarks>
- private bool ReloadTitleButtons(LocalizedContentManager content, IAssetName assetName)
+ private bool UpdateTitleButtons(LocalizedContentManager content, IAssetName assetName)
{
if (Game1.activeClickableMenu is TitleMenu titleMenu)
{
@@ -635,35 +645,31 @@ namespace StardewModdingAPI.Metadata
return false;
}
- /// <summary>Reload the sprites for matching pets or horses.</summary>
+ /// <summary>Update the sprites for matching pets or horses.</summary>
/// <typeparam name="TAnimal">The animal type.</typeparam>
- /// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadPetOrHorseSprites<TAnimal>(LocalizedContentManager content, IAssetName assetName)
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdatePetOrHorseSprites<TAnimal>(IAssetName assetName)
where TAnimal : NPC
{
// find matches
TAnimal[] animals = this.GetCharacters()
.OfType<TAnimal>()
- .Where(p => this.IsSameBaseName(assetName, p.Sprite?.Texture?.Name))
+ .Where(p => this.IsSameBaseName(assetName, p.Sprite?.spriteTexture?.Name))
.ToArray();
- if (!animals.Any())
- return false;
// update sprites
- Texture2D texture = content.Load<Texture2D>(assetName.BaseName);
+ bool changed = false;
foreach (TAnimal animal in animals)
- animal.Sprite.spriteTexture = texture;
- return true;
+ changed |= this.MarkSpriteDirty(animal.Sprite);
+ return changed;
}
- /// <summary>Reload the sprites for matching farm animals.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
+ /// <summary>Update the sprites for matching farm animals.</summary>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
/// <remarks>Derived from <see cref="FarmAnimal.reload"/>.</remarks>
- private bool ReloadFarmAnimalSprites(LocalizedContentManager content, IAssetName assetName)
+ private bool UpdateFarmAnimalSprites(IAssetName assetName)
{
// find matches
FarmAnimal[] animals = this.GetFarmAnimals().ToArray();
@@ -671,7 +677,7 @@ namespace StardewModdingAPI.Metadata
return false;
// update sprites
- Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
+ bool changed = true;
foreach (FarmAnimal animal in animals)
{
// get expected key
@@ -684,15 +690,15 @@ namespace StardewModdingAPI.Metadata
// reload asset
if (this.IsSameBaseName(assetName, expectedKey))
- animal.Sprite.spriteTexture = texture.Value;
+ changed |= this.MarkSpriteDirty(animal.Sprite);
}
- return texture.IsValueCreated;
+ return changed;
}
- /// <summary>Reload building textures.</summary>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadBuildings(IAssetName assetName)
+ /// <summary>Update building textures.</summary>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdateBuildings(IAssetName assetName)
{
// get paint mask info
const string paintMaskSuffix = "_PaintMask";
@@ -701,7 +707,7 @@ namespace StardewModdingAPI.Metadata
// get building type
string type = Path.GetFileName(assetName.BaseName);
if (isPaintMask)
- type = type.Substring(0, type.Length - paintMaskSuffix.Length);
+ type = type[..^paintMaskSuffix.Length];
// get buildings
Building[] buildings = this.GetLocations(buildingInteriors: false)
@@ -725,12 +731,12 @@ namespace StardewModdingAPI.Metadata
return removedFromCache;
}
- /// <summary>Reload map seat textures.</summary>
+ /// <summary>Update map seat textures.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
+ /// <param name="assetName">The asset name to update.</param>
/// <param name="ignoreWorld">Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld)
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdateChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld)
{
MapSeat.mapChairTexture = content.Load<Texture2D>(assetName.BaseName);
@@ -741,7 +747,7 @@ namespace StardewModdingAPI.Metadata
foreach (MapSeat seat in location.mapSeats.Where(p => p != null))
{
if (this.IsSameBaseName(assetName, seat._loadedTextureFile))
- seat.overlayTexture = MapSeat.mapChairTexture;
+ seat._loadedTextureFile = null;
}
}
}
@@ -749,11 +755,10 @@ namespace StardewModdingAPI.Metadata
return true;
}
- /// <summary>Reload critter textures.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns the number of reloaded assets.</returns>
- private int ReloadCritterTextures(LocalizedContentManager content, IAssetName assetName)
+ /// <summary>Update critter textures.</summary>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdateCritterTextures(IAssetName assetName)
{
// get critters
Critter[] critters =
@@ -761,26 +766,23 @@ namespace StardewModdingAPI.Metadata
from location in this.GetLocations()
where location.critters != null
from Critter critter in location.critters
- where this.IsSameBaseName(assetName, critter.sprite?.Texture?.Name)
+ where this.IsSameBaseName(assetName, critter.sprite?.spriteTexture?.Name)
select critter
)
.ToArray();
- if (!critters.Any())
- return 0;
// update sprites
- Texture2D texture = content.Load<Texture2D>(assetName.BaseName);
+ bool changed = false;
foreach (Critter entry in critters)
- entry.sprite.spriteTexture = texture;
-
- return critters.Length;
+ changed |= this.MarkSpriteDirty(entry.sprite);
+ return changed;
}
- /// <summary>Reload the sprites for interior doors.</summary>
+ /// <summary>Update the sprites for interior doors.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any doors were affected.</returns>
- private bool ReloadDoorSprites(LocalizedContentManager content, IAssetName assetName)
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private void UpdateDoorSprites(LocalizedContentManager content, IAssetName assetName)
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
@@ -800,32 +802,15 @@ namespace StardewModdingAPI.Metadata
door.Sprite.texture = texture.Value;
}
}
-
- return texture.IsValueCreated;
}
- /// <summary>Reload the data for matching farm animals.</summary>
- /// <returns>Returns whether any farm animals were affected.</returns>
- /// <remarks>Derived from the <see cref="FarmAnimal"/> constructor.</remarks>
- private bool ReloadFarmAnimalData()
- {
- bool changed = false;
- foreach (FarmAnimal animal in this.GetFarmAnimals())
- {
- animal.reloadData();
- changed = true;
- }
-
- return changed;
- }
-
- /// <summary>Reload the sprites for a fence type.</summary>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadFenceTextures(IAssetName assetName)
+ /// <summary>Update the sprites for a fence type.</summary>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdateFenceTextures(IAssetName assetName)
{
// get fence type (e.g. LooseSprites/Fence3 => 3)
- if (!int.TryParse(this.GetSegments(assetName.BaseName)[1].Substring("Fence".Length), out int fenceType))
+ if (!int.TryParse(this.GetSegments(assetName.BaseName)[1]["Fence".Length..], out int fenceType))
return false;
// get fences
@@ -841,132 +826,69 @@ namespace StardewModdingAPI.Metadata
.ToArray();
// update fence textures
+ bool changed = false;
foreach (Fence fence in fences)
- fence.fenceTexture = new Lazy<Texture2D>(fence.loadFenceTexture);
- return true;
+ {
+ if (fence.fenceTexture.IsValueCreated)
+ {
+ fence.fenceTexture = new Lazy<Texture2D>(fence.loadFenceTexture);
+ changed = true;
+ }
+ }
+ return changed;
}
- /// <summary>Reload tree textures.</summary>
+ /// <summary>Update tree textures.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadGrassTextures(LocalizedContentManager content, IAssetName assetName)
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdateGrassTextures(LocalizedContentManager content, IAssetName assetName)
{
Grass[] grasses =
(
- from location in this.GetLocations()
- from grass in location.terrainFeatures.Values.OfType<Grass>()
+ from grass in this.GetTerrainFeatures().OfType<Grass>()
where this.IsSameBaseName(assetName, grass.textureName())
select grass
)
.ToArray();
- if (grasses.Any())
- {
- Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
- foreach (Grass grass in grasses)
- grass.texture = texture;
- return true;
- }
-
- return false;
- }
-
- /// <summary>Reload hair style metadata.</summary>
- /// <returns>Returns whether any assets were reloaded.</returns>
- /// <remarks>Derived from the <see cref="Farmer.GetHairStyleMetadataFile"/> and <see cref="Farmer.GetHairStyleMetadata"/>.</remarks>
- private bool ReloadHairData()
- {
- if (Farmer.hairStyleMetadataFile == null)
- return false;
-
- Farmer.hairStyleMetadataFile = null;
- Farmer.allHairStyleIndices = null;
- Farmer.hairStyleMetadata.Clear();
-
- return true;
- }
-
- /// <summary>Reload the map for a location.</summary>
- /// <param name="locationInfo">The location whose map to reload.</param>
- private void ReloadMap(LocationInfo locationInfo)
- {
- GameLocation location = locationInfo.Location;
- Vector2? playerPos = Game1.player?.Position;
-
- // reload map
- location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
- location.reloadMap();
-
- // reload interior doors
- location.interiorDoors.Clear();
- location.interiorDoors.ResetSharedState(); // load doors from map properties
- location.interiorDoors.ResetLocalState(); // reapply door tiles
-
- // reapply map changes (after reloading doors so they apply theirs too)
- location.MakeMapModifications(force: true);
-
- // update for changes
- location.updateWarps();
- location.updateDoors();
- locationInfo.ParentBuilding?.updateInteriorWarps();
-
- // reset player position
- // The game may move the player as part of the map changes, even if they're not in that
- // location. That's not needed in this case, and it can have weird effects like players
- // warping onto the wrong tile (or even off-screen) if a patch changes the farmhouse
- // map on location change.
- if (playerPos.HasValue)
- Game1.player!.Position = playerPos.Value;
- }
-
- /// <summary>Reload the disposition data for matching NPCs.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <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.BaseName);
bool changed = false;
- foreach (NPC npc in this.GetCharacters())
+ foreach (Grass grass in grasses)
{
- if (npc.isVillager() && data.ContainsKey(npc.Name))
+ if (grass.texture.IsValueCreated)
{
- npc.reloadData();
+ grass.texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
changed = true;
}
}
-
return changed;
}
- /// <summary>Reload the sprites for matching NPCs.</summary>
- /// <param name="propagated">The asset keys which are being propagated.</param>
- private void ReloadNpcSprites(IDictionary<IAssetName, bool> propagated)
+ /// <summary>Update the sprites for matching NPCs.</summary>
+ /// <param name="propagated">The asset names being propagated.</param>
+ private void UpdateNpcSprites(IDictionary<IAssetName, bool> propagated)
{
// get NPCs
var characters =
(
from npc in this.GetCharacters()
- let key = this.ParseAssetNameOrNull(npc.Sprite?.Texture?.Name)?.GetBaseAssetName()
+ let key = this.ParseAssetNameOrNull(npc.Sprite?.spriteTexture?.Name)?.GetBaseAssetName()
where key != null && propagated.ContainsKey(key)
select new { Npc = npc, AssetName = key }
)
.ToArray();
- if (!characters.Any())
- return;
// update sprite
foreach (var target in characters)
{
- target.Npc.Sprite.spriteTexture = this.LoadTexture(target.AssetName.BaseName);
- propagated[target.AssetName] = true;
+ if (this.MarkSpriteDirty(target.Npc.Sprite))
+ propagated[target.AssetName] = true;
}
}
- /// <summary>Reload the portraits for matching NPCs.</summary>
- /// <param name="propagated">The asset keys which are being propagated.</param>
- private void ReloadNpcPortraits(IDictionary<IAssetName, bool> propagated)
+ /// <summary>Update the portraits for matching NPCs.</summary>
+ /// <param name="propagated">The asset names being propagated.</param>
+ private void UpdateNpcPortraits(IDictionary<IAssetName, bool> propagated)
{
// get NPCs
var characters =
@@ -998,14 +920,14 @@ namespace StardewModdingAPI.Metadata
// update portrait
foreach (var target in characters)
{
- target.Npc.Portrait = this.LoadTexture(target.AssetName.BaseName);
+ target.Npc.resetPortrait();
propagated[target.AssetName] = true;
}
}
- /// <summary>Reload the sprites for matching players.</summary>
- /// <param name="assetName">The asset name to reload.</param>
- private bool ReloadPlayerSprites(IAssetName assetName)
+ /// <summary>Update the sprites for matching players.</summary>
+ /// <param name="assetName">The asset name to update.</param>
+ private bool UpdatePlayerSprites(IAssetName assetName)
{
Farmer[] players =
(
@@ -1026,11 +948,11 @@ namespace StardewModdingAPI.Metadata
return players.Any();
}
- /// <summary>Reload suspension bridge textures.</summary>
+ /// <summary>Update suspension bridge textures.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadSuspensionBridges(LocalizedContentManager content, IAssetName assetName)
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdateSuspensionBridges(LocalizedContentManager content, IAssetName assetName)
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
@@ -1054,36 +976,79 @@ namespace StardewModdingAPI.Metadata
return texture.IsValueCreated;
}
- /// <summary>Reload tree textures.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
- /// <param name="assetName">The asset name to reload.</param>
- /// <param name="type">The type to reload.</param>
- /// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadTreeTextures(LocalizedContentManager content, IAssetName assetName, int type)
+ /// <summary>Update tree textures.</summary>
+ /// <param name="type">The type to update.</param>
+ /// <returns>Returns whether any references were updated.</returns>
+ private bool UpdateTreeTextures(int type)
{
- Tree[] trees = this.GetLocations()
- .SelectMany(p => p.terrainFeatures.Values.OfType<Tree>())
+ Tree[] trees = this
+ .GetTerrainFeatures()
+ .OfType<Tree>()
.Where(tree => tree.treeType.Value == type)
.ToArray();
- if (trees.Any())
+ bool changed = false;
+ foreach (Tree tree in trees)
{
- Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(assetName.BaseName));
- foreach (Tree tree in trees)
- tree.texture = texture;
- return true;
+ if (tree.texture.IsValueCreated)
+ {
+ this.Reflection.GetMethod(tree, "resetTexture").Invoke();
+ changed = true;
+ }
}
+ return changed;
+ }
- return false;
+ /// <summary>Mark an animated sprite's texture dirty, so it's reloaded next time it's rendered.</summary>
+ /// <param name="sprite">The animated sprite to change.</param>
+ /// <returns>Returns whether the sprite was changed.</returns>
+ private bool MarkSpriteDirty(AnimatedSprite sprite)
+ {
+ if (sprite.loadedTexture is null && sprite.spriteTexture is null)
+ return false;
+
+ sprite.loadedTexture = null;
+ sprite.spriteTexture = null;
+ return true;
}
/****
- ** Reload data methods
+ ** Update data methods
****/
- /// <summary>Reload the dialogue data for matching NPCs.</summary>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any assets were reloaded.</returns>
- private bool ReloadNpcDialogue(IAssetName assetName)
+ /// <summary>Update the data for matching farm animals.</summary>
+ /// <returns>Returns whether any farm animals were updated.</returns>
+ /// <remarks>Derived from the <see cref="FarmAnimal"/> constructor.</remarks>
+ private bool UpdateFarmAnimalData()
+ {
+ bool changed = false;
+ foreach (FarmAnimal animal in this.GetFarmAnimals())
+ {
+ animal.reloadData();
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ /// <summary>Update hair style metadata.</summary>
+ /// <returns>Returns whether any data was updated.</returns>
+ /// <remarks>Derived from the <see cref="Farmer.GetHairStyleMetadataFile"/> and <see cref="Farmer.GetHairStyleMetadata"/>.</remarks>
+ private bool UpdateHairData()
+ {
+ if (Farmer.hairStyleMetadataFile == null)
+ return false;
+
+ Farmer.hairStyleMetadataFile = null;
+ Farmer.allHairStyleIndices = null;
+ Farmer.hairStyleMetadata.Clear();
+
+ return true;
+ }
+
+ /// <summary>Update the dialogue data for matching NPCs.</summary>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any NPCs were updated.</returns>
+ private bool UpdateNpcDialogue(IAssetName assetName)
{
// get NPCs
string name = Path.GetFileName(assetName.BaseName);
@@ -1110,10 +1075,30 @@ namespace StardewModdingAPI.Metadata
return true;
}
- /// <summary>Reload the schedules for matching NPCs.</summary>
- /// <param name="assetName">The asset name to reload.</param>
- /// <returns>Returns whether any assets were reloaded.</returns>
- private bool ReloadNpcSchedules(IAssetName assetName)
+ /// <summary>Update the disposition data for matching NPCs.</summary>
+ /// <param name="content">The content manager through which to reload the asset.</param>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any NPCs were updated.</returns>
+ private bool UpdateNpcDispositions(LocalizedContentManager content, IAssetName assetName)
+ {
+ IDictionary<string, string> data = content.Load<Dictionary<string, string>>(assetName.BaseName);
+ bool changed = false;
+ foreach (NPC npc in this.GetCharacters())
+ {
+ if (npc.isVillager() && data.ContainsKey(npc.Name))
+ {
+ npc.reloadData();
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ /// <summary>Update the schedules for matching NPCs.</summary>
+ /// <param name="assetName">The asset name to update.</param>
+ /// <returns>Returns whether any NPCs were updated.</returns>
+ private bool UpdateNpcSchedules(IAssetName assetName)
{
// get NPCs
string name = Path.GetFileName(assetName.BaseName);
@@ -1144,11 +1129,11 @@ namespace StardewModdingAPI.Metadata
return true;
}
- /// <summary>Reload cached translations from the <c>Strings\StringsFromCSFiles</c> asset.</summary>
+ /// <summary>Update cached translations from the <c>Strings\StringsFromCSFiles</c> asset.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
- /// <returns>Returns whether any data was reloaded.</returns>
+ /// <returns>Returns whether any data was updated.</returns>
/// <remarks>Derived from the <see cref="Game1.TranslateFields"/>.</remarks>
- private bool ReloadStringsFromCsFiles(LocalizedContentManager content)
+ private bool UpdateStringsFromCsFiles(LocalizedContentManager content)
{
Game1.samBandName = content.LoadString("Strings/StringsFromCSFiles:Game1.cs.2156");
Game1.elliottBookName = content.LoadString("Strings/StringsFromCSFiles:Game1.cs.2157");
@@ -1166,68 +1151,149 @@ namespace StardewModdingAPI.Metadata
}
/****
+ ** Update map methods
+ ****/
+ /// <summary>Update the map for a location.</summary>
+ /// <param name="locationInfo">The location whose map to update.</param>
+ private void UpdateMap(LocationInfo locationInfo)
+ {
+ GameLocation location = locationInfo.Location;
+ Vector2? playerPos = Game1.player?.Position;
+
+ // reload map
+ location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
+ location.reloadMap();
+
+ // reload interior doors
+ location.interiorDoors.Clear();
+ location.interiorDoors.ResetSharedState(); // load doors from map properties
+ location.interiorDoors.ResetLocalState(); // reapply door tiles
+
+ // reapply map changes (after reloading doors so they apply theirs too)
+ location.MakeMapModifications(force: true);
+
+ // update for changes
+ location.updateWarps();
+ location.updateDoors();
+ locationInfo.ParentBuilding?.updateInteriorWarps();
+
+ // reset player position
+ // The game may move the player as part of the map changes, even if they're not in that
+ // location. That's not needed in this case, and it can have weird effects like players
+ // warping onto the wrong tile (or even off-screen) if a patch changes the farmhouse
+ // map on location change.
+ if (playerPos.HasValue)
+ Game1.player!.Position = playerPos.Value;
+ }
+
+ /****
** Helpers
****/
/// <summary>Get all NPCs in the game (excluding farm animals).</summary>
private IEnumerable<NPC> GetCharacters()
{
- foreach (NPC character in this.GetLocations().SelectMany(p => p.characters))
- yield return character;
+ return this.WorldCache.GetOrSet(
+ nameof(this.GetCharacters),
+ () =>
+ {
+ List<NPC> characters = new();
- if (Game1.CurrentEvent?.actors != null)
- {
- foreach (NPC character in Game1.CurrentEvent.actors)
- yield return character;
- }
+ foreach (NPC character in this.GetLocations().SelectMany(p => p.characters))
+ characters.Add(character);
+
+ if (Game1.CurrentEvent?.actors != null)
+ {
+ foreach (NPC character in Game1.CurrentEvent.actors)
+ characters.Add(character);
+ }
+
+ return characters;
+ }
+ );
}
/// <summary>Get all farm animals in the game.</summary>
private IEnumerable<FarmAnimal> GetFarmAnimals()
{
- foreach (GameLocation location in this.GetLocations())
- {
- if (location is Farm farm)
+ return this.WorldCache.GetOrSet(
+ nameof(this.GetFarmAnimals),
+ () =>
{
- foreach (FarmAnimal animal in farm.animals.Values)
- yield return animal;
+ List<FarmAnimal> animals = new();
+
+ foreach (GameLocation location in this.GetLocations())
+ {
+ if (location is Farm farm)
+ {
+ foreach (FarmAnimal animal in farm.animals.Values)
+ animals.Add(animal);
+ }
+ else if (location is AnimalHouse animalHouse)
+ {
+ foreach (FarmAnimal animal in animalHouse.animals.Values)
+ animals.Add(animal);
+ }
+ }
+
+ return animals;
}
- else if (location is AnimalHouse animalHouse)
- foreach (FarmAnimal animal in animalHouse.animals.Values)
- yield return animal;
- }
+ );
}
/// <summary>Get all locations in the game.</summary>
/// <param name="buildingInteriors">Whether to also get the interior locations for constructable buildings.</param>
private IEnumerable<GameLocation> GetLocations(bool buildingInteriors = true)
{
- return this.GetLocationsWithInfo(buildingInteriors).Select(info => info.Location);
+ return this.WorldCache.GetOrSet(
+ $"{nameof(this.GetLocations)}_{buildingInteriors}",
+ () => this.GetLocationsWithInfo(buildingInteriors).Select(info => info.Location).ToArray()
+ );
}
/// <summary>Get all locations in the game.</summary>
/// <param name="buildingInteriors">Whether to also get the interior locations for constructable buildings.</param>
private IEnumerable<LocationInfo> GetLocationsWithInfo(bool buildingInteriors = true)
{
- // get available root locations
- IEnumerable<GameLocation> rootLocations = Game1.locations;
- if (SaveGame.loaded?.locations != null)
- rootLocations = rootLocations.Concat(SaveGame.loaded.locations);
+ return this.WorldCache.GetOrSet(
+ $"{nameof(this.GetLocationsWithInfo)}_{buildingInteriors}",
+ () =>
+ {
+ List<LocationInfo> locations = new();
- // yield root + child locations
- foreach (GameLocation location in rootLocations)
- {
- yield return new LocationInfo(location, null);
+ // get root locations
+ foreach (GameLocation location in Game1.locations)
+ locations.Add(new LocationInfo(location, null));
+ if (SaveGame.loaded?.locations != null)
+ {
+ foreach (GameLocation location in SaveGame.loaded.locations)
+ locations.Add(new LocationInfo(location, null));
+ }
- if (buildingInteriors && location is BuildableGameLocation buildableLocation)
- {
- foreach (Building building in buildableLocation.buildings)
+ // get child locations
+ if (buildingInteriors)
{
- GameLocation? indoors = building.indoors.Value;
- if (indoors != null)
- yield return new LocationInfo(indoors, building);
+ foreach (BuildableGameLocation location in locations.Select(p => p.Location).OfType<BuildableGameLocation>().ToArray())
+ {
+ foreach (Building building in location.buildings)
+ {
+ GameLocation indoors = building.indoors.Value;
+ if (indoors is not null)
+ locations.Add(new LocationInfo(indoors, building));
+ }
+ }
}
- }
- }
+
+ return locations;
+ });
+ }
+
+ /// <summary>Get all terrain features in the game.</summary>
+ private IEnumerable<TerrainFeature> GetTerrainFeatures()
+ {
+ return this.WorldCache.GetOrSet(
+ $"{nameof(this.GetTerrainFeatures)}",
+ () => this.GetLocations().SelectMany(p => p.terrainFeatures.Values).ToArray()
+ );
}
/// <summary>Get whether two asset names are equivalent if you ignore the locale code.</summary>
@@ -1293,29 +1359,8 @@ namespace StardewModdingAPI.Metadata
}
/// <summary>Metadata about a location used in asset propagation.</summary>
- private readonly struct LocationInfo
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The location instance.</summary>
- public GameLocation Location { get; }
-
- /// <summary>The building which contains the location, if any.</summary>
- public Building? ParentBuilding { get; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="location">The location instance.</param>
- /// <param name="parentBuilding">The building which contains the location, if any.</param>
- public LocationInfo(GameLocation location, Building? parentBuilding)
- {
- this.Location = location;
- this.ParentBuilding = parentBuilding;
- }
- }
+ /// <param name="Location">The location instance.</param>
+ /// <param name="ParentBuilding">The building which contains the location, if any.</param>
+ private record LocationInfo(GameLocation Location, Building? ParentBuilding);
}
}