Merge branch 'develop' of https://github.com/Pathoschild/SMAPI.git into android
# Conflicts: # src/SMAPI/Framework/ContentManagers/ModContentManager.cs
This commit is contained in:
commit
27423f0468
|
@ -3,6 +3,9 @@
|
|||
# Release notes
|
||||
## Upcoming release
|
||||
* For players:
|
||||
* Reduced network traffic for mod broadcasts to players who can't process them.
|
||||
* Fixed update-check errors for recent versions of SMAPI on Android.
|
||||
* Updated compatibility list.
|
||||
* Updated translations. Thanks to xCarloC (added Italian)!
|
||||
|
||||
* For the Save Backup mod:
|
||||
|
@ -10,13 +13,16 @@
|
|||
* Reduced log messages.
|
||||
|
||||
* For modders:
|
||||
* Added support for self-broadcasts through the multiplayer API. (Mods can now send messages to the current machine. That enables simple integrations between mods without needing an API, and lets mods notify a host mod without needing different code depending on whether the current player is the host or a farmhand.)
|
||||
* Added `helper.Input.GetStatus` method to get the low-level status of a button.
|
||||
* Eliminated unneeded network messages when broadcasting to a peer who can't handle the message (e.g. because they don't have SMAPI or don't have the target mod).
|
||||
* Fixed marriage dialogue cleared when propagating dialogue changes.
|
||||
* Added support for [message sending](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Message_sending) to mods on the current computer.
|
||||
* Added `helper.Input.GetStatus` to get the low-level status of a button.
|
||||
* **[Breaking change]** Map tilesheets are no loaded from `Content` if they can't be found in `Content/Maps`. This reflects an upcoming change in the game to delete map tilesheets under `Content`.
|
||||
* Improved map tilesheet errors so they provide more info.
|
||||
* Fixed dialogue propagation clearing marriage dialogue.
|
||||
* Fixed issue where SMAPI didn't call `IAssetEditor` with the actual type if a mod loaded an asset using `content.Load<object>`.
|
||||
|
||||
* For the web UI:
|
||||
* Updated the JSON validator and Content Patcher schema for `.tmx` support.
|
||||
* The mod compatibility page now has a sticky table header.
|
||||
|
||||
* For SMAPI/tool developers:
|
||||
* The SMAPI log now prefixes the OS name with `Android` on Android.
|
||||
|
|
|
@ -41,11 +41,8 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>The cache in which to store mod data.</summary>
|
||||
private readonly IModCacheRepository ModCache;
|
||||
|
||||
/// <summary>The number of minutes successful update checks should be cached before refetching them.</summary>
|
||||
private readonly int SuccessCacheMinutes;
|
||||
|
||||
/// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
|
||||
private readonly int ErrorCacheMinutes;
|
||||
/// <summary>The config settings for mod update checks.</summary>
|
||||
private readonly IOptions<ModUpdateCheckConfig> Config;
|
||||
|
||||
/// <summary>The internal mod metadata list.</summary>
|
||||
private readonly ModDatabase ModDatabase;
|
||||
|
@ -58,21 +55,19 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="environment">The web hosting environment.</param>
|
||||
/// <param name="wikiCache">The cache in which to store wiki data.</param>
|
||||
/// <param name="modCache">The cache in which to store mod metadata.</param>
|
||||
/// <param name="configProvider">The config settings for mod update checks.</param>
|
||||
/// <param name="config">The config settings for mod update checks.</param>
|
||||
/// <param name="chucklefish">The Chucklefish API client.</param>
|
||||
/// <param name="curseForge">The CurseForge API client.</param>
|
||||
/// <param name="github">The GitHub API client.</param>
|
||||
/// <param name="modDrop">The ModDrop API client.</param>
|
||||
/// <param name="nexus">The Nexus API client.</param>
|
||||
public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
|
||||
public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
|
||||
{
|
||||
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
|
||||
ModUpdateCheckConfig config = configProvider.Value;
|
||||
|
||||
this.WikiCache = wikiCache;
|
||||
this.ModCache = modCache;
|
||||
this.SuccessCacheMinutes = config.SuccessCacheMinutes;
|
||||
this.ErrorCacheMinutes = config.ErrorCacheMinutes;
|
||||
this.Config = config;
|
||||
this.Repositories =
|
||||
new IModRepository[]
|
||||
{
|
||||
|
@ -133,6 +128,8 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
ModDataRecord record = this.ModDatabase.Get(search.ID);
|
||||
WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
|
||||
UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
|
||||
ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.InvariantCultureIgnoreCase));
|
||||
bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false;
|
||||
|
||||
// get latest versions
|
||||
ModEntryModel result = new ModEntryModel { ID = search.ID };
|
||||
|
@ -151,7 +148,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
}
|
||||
|
||||
// fetch data
|
||||
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey);
|
||||
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions);
|
||||
if (data.Error != null)
|
||||
{
|
||||
errors.Add(data.Error);
|
||||
|
@ -161,7 +158,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
// handle main version
|
||||
if (data.Version != null)
|
||||
{
|
||||
ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions);
|
||||
ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
|
||||
if (version == null)
|
||||
{
|
||||
errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
|
||||
|
@ -175,7 +172,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
// handle optional version
|
||||
if (data.PreviewVersion != null)
|
||||
{
|
||||
ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions);
|
||||
ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
|
||||
if (version == null)
|
||||
{
|
||||
errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
|
||||
|
@ -215,16 +212,16 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
}
|
||||
|
||||
// special cases
|
||||
if (result.ID == "Pathoschild.SMAPI")
|
||||
if (overrides?.SetUrl != null)
|
||||
{
|
||||
if (main != null)
|
||||
main.Url = "https://smapi.io/";
|
||||
main.Url = overrides.SetUrl;
|
||||
if (optional != null)
|
||||
optional.Url = "https://smapi.io/";
|
||||
optional.Url = overrides.SetUrl;
|
||||
}
|
||||
|
||||
// get recommended update (if any)
|
||||
ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions);
|
||||
ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions);
|
||||
if (apiVersion != null && installedVersion != null)
|
||||
{
|
||||
// get newer versions
|
||||
|
@ -283,10 +280,11 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
|
||||
/// <summary>Get the mod info for an update key.</summary>
|
||||
/// <param name="updateKey">The namespaced update key.</param>
|
||||
private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey)
|
||||
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
|
||||
private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions)
|
||||
{
|
||||
// get mod
|
||||
if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes))
|
||||
if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes))
|
||||
{
|
||||
// get site
|
||||
if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
|
||||
|
@ -298,7 +296,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
{
|
||||
if (result.Version == null)
|
||||
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
|
||||
else if (!SemanticVersion.TryParse(result.Version, out _))
|
||||
else if (!this.TryParseVersion(result.Version, allowNonStandardVersions, out _))
|
||||
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
|
||||
}
|
||||
|
||||
|
@ -357,15 +355,16 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Get a semantic local version for update checks.</summary>
|
||||
/// <param name="version">The version to parse.</param>
|
||||
/// <param name="map">A map of version replacements.</param>
|
||||
private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map)
|
||||
/// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
|
||||
private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
|
||||
{
|
||||
// try mapped version
|
||||
string rawNewVersion = this.GetRawMappedVersion(version, map);
|
||||
if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew))
|
||||
string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard);
|
||||
if (this.TryParseVersion(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew))
|
||||
return parsedNew;
|
||||
|
||||
// return original version
|
||||
return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld)
|
||||
return this.TryParseVersion(version, allowNonStandard, out ISemanticVersion parsedOld)
|
||||
? parsedOld
|
||||
: null;
|
||||
}
|
||||
|
@ -373,7 +372,8 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>Get a semantic local version for update checks.</summary>
|
||||
/// <param name="version">The version to map.</param>
|
||||
/// <param name="map">A map of version replacements.</param>
|
||||
private string GetRawMappedVersion(string version, IDictionary<string, string> map)
|
||||
/// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
|
||||
private string GetRawMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
|
||||
{
|
||||
if (version == null || map == null || !map.Any())
|
||||
return version;
|
||||
|
@ -383,19 +383,31 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
return map[version];
|
||||
|
||||
// match parsed version
|
||||
if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
|
||||
if (this.TryParseVersion(version, allowNonStandard, out ISemanticVersion parsed))
|
||||
{
|
||||
if (map.ContainsKey(parsed.ToString()))
|
||||
return map[parsed.ToString()];
|
||||
|
||||
foreach (var pair in map)
|
||||
{
|
||||
if (SemanticVersion.TryParse(pair.Key, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, out ISemanticVersion newVersion))
|
||||
if (this.TryParseVersion(pair.Key, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && this.TryParseVersion(pair.Value, allowNonStandard, out ISemanticVersion newVersion))
|
||||
return newVersion.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
/// <summary>Try to parse a version string.</summary>
|
||||
/// <param name="version">The version string.</param>
|
||||
/// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
|
||||
/// <param name="parsed">The parsed representation.</param>
|
||||
/// <returns>Returns whether parsing the version succeeded.</returns>
|
||||
public bool TryParseVersion(string version, bool allowNonStandard, out ISemanticVersion parsed)
|
||||
{
|
||||
return allowNonStandard
|
||||
? SemanticVersion.TryParseNonStandard(version, out parsed)
|
||||
: SemanticVersion.TryParse(version, out parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
namespace StardewModdingAPI.Web.Framework.ConfigModels
|
||||
{
|
||||
/// <summary>Override update-check metadata for a mod.</summary>
|
||||
internal class ModOverrideConfig
|
||||
{
|
||||
/// <summary>The unique ID from the mod's manifest.</summary>
|
||||
public string ID { get; set; }
|
||||
|
||||
/// <summary>Whether to allow non-standard versions.</summary>
|
||||
public bool AllowNonStandardVersions { get; set; }
|
||||
|
||||
/// <summary>The mod page URL to use regardless of which site has the update, or <c>null</c> to use the site URL.</summary>
|
||||
public string SetUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -11,5 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
|
||||
/// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
|
||||
public int ErrorCacheMinutes { get; set; }
|
||||
|
||||
/// <summary>Update-check metadata to override.</summary>
|
||||
public ModOverrideConfig[] ModOverrides { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
|
||||
}
|
||||
@section Head {
|
||||
<link rel="stylesheet" href="~/Content/css/mods.css?r=20190302" />
|
||||
<link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tablesorter@2.31.0/dist/js/jquery.tablesorter.combined.min.js" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/mods.js?r=20190302"></script>
|
||||
<script src="~/Content/js/mods.js?r=20200218"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None });
|
||||
|
|
|
@ -64,6 +64,17 @@
|
|||
|
||||
"ModUpdateCheck": {
|
||||
"SuccessCacheMinutes": 60,
|
||||
"ErrorCacheMinutes": 5
|
||||
"ErrorCacheMinutes": 5,
|
||||
"ModOverrides": [
|
||||
{
|
||||
"ID": "Pathoschild.SMAPI",
|
||||
"AllowNonStandardVersions": true,
|
||||
"SetUrl": "https://smapi.io"
|
||||
},
|
||||
{
|
||||
"ID": "MartyrPher.SMAPI-Android-Installer",
|
||||
"AllowNonStandardVersions": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,11 @@ table.wikitable > caption {
|
|||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#mod-list thead tr {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#mod-list th.header {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
|
|
|
@ -102,7 +102,7 @@ smapi.modList = function (mods, enableBeta) {
|
|||
app = new Vue({
|
||||
el: "#app",
|
||||
data: data,
|
||||
mounted: function() {
|
||||
mounted: function () {
|
||||
// enable table sorting
|
||||
$("#mod-list").tablesorter({
|
||||
cssHeader: "header",
|
||||
|
@ -115,11 +115,7 @@ smapi.modList = function (mods, enableBeta) {
|
|||
$("#search-box").focus();
|
||||
|
||||
// jump to anchor (since table is added after page load)
|
||||
if (location.hash) {
|
||||
var row = $(location.hash).get(0);
|
||||
if (row)
|
||||
row.scrollIntoView();
|
||||
}
|
||||
this.fixHashPosition();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
@ -144,6 +140,18 @@ smapi.modList = function (mods, enableBeta) {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fix the window position for the current hash.
|
||||
*/
|
||||
fixHashPosition: function () {
|
||||
if (!location.hash)
|
||||
return;
|
||||
|
||||
var row = $(location.hash);
|
||||
var target = row.prev().get(0) || row.get(0);
|
||||
if (target)
|
||||
target.scrollIntoView();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get whether a mod matches the current filters.
|
||||
|
@ -151,7 +159,7 @@ smapi.modList = function (mods, enableBeta) {
|
|||
* @param {string[]} searchWords The search words to match.
|
||||
* @returns {bool} Whether the mod matches the filters.
|
||||
*/
|
||||
matchesFilters: function(mod, searchWords) {
|
||||
matchesFilters: function (mod, searchWords) {
|
||||
var filters = data.filters;
|
||||
|
||||
// check hash
|
||||
|
@ -249,7 +257,9 @@ smapi.modList = function (mods, enableBeta) {
|
|||
}
|
||||
});
|
||||
app.applyFilters();
|
||||
app.fixHashPosition();
|
||||
window.addEventListener("hashchange", function () {
|
||||
app.applyFilters();
|
||||
app.fixHashPosition();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
"Default | UpdateKey": "Nexus:2341"
|
||||
},
|
||||
|
||||
"TMX Loader": {
|
||||
"TMXL Map Toolkit": {
|
||||
"ID": "Platonymous.TMXLoader",
|
||||
"Default | UpdateKey": "Nexus:1820"
|
||||
},
|
||||
|
@ -129,7 +129,7 @@
|
|||
"Bee House Flower Range Fix": {
|
||||
"ID": "kirbylink.beehousefix",
|
||||
"~ | Status": "Obsolete",
|
||||
"~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4."
|
||||
"~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4."
|
||||
},
|
||||
|
||||
"Colored Chests": {
|
||||
|
@ -153,9 +153,9 @@
|
|||
/*********
|
||||
** Broke in SDV 1.4
|
||||
*********/
|
||||
"Fix Dice": {
|
||||
"ID": "ashley.fixdice",
|
||||
"~1.1.2 | Status": "AssumeBroken" // crashes game on startup
|
||||
"Auto Quality Patch": {
|
||||
"ID": "SilentOak.AutoQualityPatch",
|
||||
"~2.1.3-unofficial.7 | Status": "AssumeBroken" // runtime errors
|
||||
},
|
||||
|
||||
"Fix Dice": {
|
||||
|
|
|
@ -112,9 +112,10 @@ namespace StardewModdingAPI.Framework
|
|||
|
||||
/// <summary>Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary>
|
||||
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
|
||||
/// <param name="modName">The mod display name to show in errors.</param>
|
||||
/// <param name="rootDirectory">The root directory to search for content (or <c>null</c> for the default).</param>
|
||||
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
|
||||
public ModContentManager CreateModContentManager(string name, string rootDirectory, IContentManager gameContentManager)
|
||||
public ModContentManager CreateModContentManager(string name, string modName, string rootDirectory, IContentManager gameContentManager)
|
||||
{
|
||||
return this.ContentManagerLock.InWriteLock(() =>
|
||||
{
|
||||
|
@ -123,6 +124,7 @@ namespace StardewModdingAPI.Framework
|
|||
gameContentManager: gameContentManager,
|
||||
serviceProvider: this.MainContentManager.ServiceProvider,
|
||||
rootDirectory: rootDirectory,
|
||||
modName: modName,
|
||||
currentCulture: this.MainContentManager.CurrentCulture,
|
||||
coordinator: this,
|
||||
monitor: this.Monitor,
|
||||
|
|
|
@ -2,12 +2,15 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Xna.Framework.Content;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using StardewModdingAPI.Framework.Content;
|
||||
using StardewModdingAPI.Framework.Exceptions;
|
||||
using StardewModdingAPI.Framework.Reflection;
|
||||
using StardewModdingAPI.Framework.Utilities;
|
||||
using StardewValley;
|
||||
using xTile;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ContentManagers
|
||||
{
|
||||
|
@ -337,6 +340,20 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
{
|
||||
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
|
||||
|
||||
// special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead.
|
||||
{
|
||||
Type actualType = asset.Data.GetType();
|
||||
Type actualOpenType = actualType.IsGenericType ? actualType.GetGenericTypeDefinition() : null;
|
||||
|
||||
if (typeof(T) != actualType && (actualOpenType == typeof(Dictionary<,>) || actualOpenType == typeof(List<>) || actualType == typeof(Texture2D) || actualType == typeof(Map)))
|
||||
{
|
||||
return (IAssetData)this.GetType()
|
||||
.GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.MakeGenericMethod(actualType)
|
||||
.Invoke(this, new object[] { info, asset });
|
||||
}
|
||||
}
|
||||
|
||||
// edit asset
|
||||
foreach (var entry in this.Editors)
|
||||
{
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Content;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -28,15 +26,15 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
|
||||
private readonly JsonHelper JsonHelper;
|
||||
|
||||
/// <summary>The mod display name to show in errors.</summary>
|
||||
private readonly string ModName;
|
||||
|
||||
/// <summary>The game content manager used for map tilesheets not provided by the mod.</summary>
|
||||
private readonly IContentManager GameContentManager;
|
||||
|
||||
/// <summary>The language code for language-agnostic mod assets.</summary>
|
||||
private readonly LanguageCode DefaultLanguage = Constants.DefaultLanguage;
|
||||
|
||||
/// <summary>Reflector used to access xnbs on Android.
|
||||
private readonly Reflector Reflector;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -45,6 +43,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
|
||||
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
|
||||
/// <param name="serviceProvider">The service provider to use to locate services.</param>
|
||||
/// <param name="modName">The mod display name to show in errors.</param>
|
||||
/// <param name="rootDirectory">The root directory to search for content.</param>
|
||||
/// <param name="currentCulture">The current culture for which to localize content.</param>
|
||||
/// <param name="coordinator">The central coordinator which manages content managers.</param>
|
||||
|
@ -52,12 +51,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <param name="reflection">Simplifies access to private code.</param>
|
||||
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
|
||||
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
|
||||
public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
|
||||
public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
|
||||
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
|
||||
{
|
||||
this.GameContentManager = gameContentManager;
|
||||
this.JsonHelper = jsonHelper;
|
||||
this.Reflector = reflection;
|
||||
this.ModName = modName;
|
||||
}
|
||||
|
||||
/// <summary>Load an asset that has been processed by the content pipeline.</summary>
|
||||
|
@ -132,7 +131,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
this.FixCustomTilesheetPaths(map, relativeMapPath: assetName);
|
||||
}
|
||||
}
|
||||
return this.ModedLoad<T>(assetName, language);
|
||||
break;
|
||||
|
||||
// unpacked data
|
||||
case ".json":
|
||||
|
@ -303,98 +302,99 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
foreach (TileSheet tilesheet in map.TileSheets)
|
||||
{
|
||||
string imageSource = tilesheet.ImageSource;
|
||||
string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
|
||||
|
||||
// validate tilesheet path
|
||||
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains(".."))
|
||||
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../).");
|
||||
|
||||
// get seasonal name (if applicable)
|
||||
string seasonalImageSource = null;
|
||||
if (isOutdoors && Context.IsSaveLoaded && Game1.currentSeason != null)
|
||||
{
|
||||
string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null.");
|
||||
bool hasSeasonalPrefix =
|
||||
filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase)
|
||||
|| filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase)
|
||||
|| filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase)
|
||||
|| filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase);
|
||||
if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_"))
|
||||
{
|
||||
string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase));
|
||||
seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}";
|
||||
}
|
||||
}
|
||||
throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../).");
|
||||
|
||||
// load best match
|
||||
try
|
||||
{
|
||||
string key =
|
||||
this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource)
|
||||
?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
|
||||
if (key != null)
|
||||
{
|
||||
tilesheet.ImageSource = key;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex);
|
||||
}
|
||||
if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, isOutdoors, out string assetName, out string error))
|
||||
throw new SContentLoadException($"{errorPrefix} {error}");
|
||||
|
||||
// none found
|
||||
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.");
|
||||
tilesheet.ImageSource = assetName;
|
||||
}
|
||||
catch (Exception ex) when (!(ex is SContentLoadException))
|
||||
{
|
||||
throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the actual asset name for a tilesheet.</summary>
|
||||
/// <param name="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
|
||||
/// <param name="imageSource">The tilesheet image source to load.</param>
|
||||
/// <returns>Returns the asset name.</returns>
|
||||
/// <param name="originalPath">The tilesheet path to load.</param>
|
||||
/// <param name="willSeasonalize">Whether the game will apply seasonal logic to the tilesheet.</param>
|
||||
/// <param name="assetName">The found asset name.</param>
|
||||
/// <param name="error">A message indicating why the file couldn't be loaded.</param>
|
||||
/// <returns>Returns whether the asset name was found.</returns>
|
||||
/// <remarks>See remarks on <see cref="FixCustomTilesheetPaths"/>.</remarks>
|
||||
private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource)
|
||||
private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string originalPath, bool willSeasonalize, out string assetName, out string error)
|
||||
{
|
||||
if (imageSource == null)
|
||||
return null;
|
||||
assetName = null;
|
||||
error = null;
|
||||
|
||||
// check relative to map file
|
||||
// nothing to do
|
||||
if (string.IsNullOrWhiteSpace(originalPath))
|
||||
{
|
||||
string localKey = Path.Combine(modRelativeMapFolder, imageSource);
|
||||
FileInfo localFile = this.GetModFile(localKey);
|
||||
if (localFile.Exists)
|
||||
return this.GetInternalAssetKey(localKey);
|
||||
assetName = originalPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
// check relative to content folder
|
||||
// parse path
|
||||
string filename = Path.GetFileName(originalPath);
|
||||
bool isSeasonal = filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase)
|
||||
|| filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase)
|
||||
|| filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase)
|
||||
|| filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase);
|
||||
string relativePath = originalPath;
|
||||
if (willSeasonalize && isSeasonal)
|
||||
{
|
||||
foreach (string candidateKey in new[] { imageSource, Path.Combine("Maps", imageSource) })
|
||||
{
|
||||
string contentKey = candidateKey.EndsWith(".png")
|
||||
? candidateKey.Substring(0, candidateKey.Length - 4)
|
||||
: candidateKey;
|
||||
string dirPath = Path.GetDirectoryName(originalPath);
|
||||
relativePath = Path.Combine(dirPath, $"{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
|
||||
return contentKey;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore file-not-found errors
|
||||
// TODO: while it's useful to suppress an asset-not-found error here to avoid
|
||||
// confusion, this is a pretty naive approach. Even if the file doesn't exist,
|
||||
// the file may have been loaded through an IAssetLoader which failed. So even
|
||||
// if the content file doesn't exist, that doesn't mean the error here is a
|
||||
// content-not-found error. Unfortunately XNA doesn't provide a good way to
|
||||
// detect the error type.
|
||||
if (this.GetContentFolderFileExists(contentKey))
|
||||
throw;
|
||||
}
|
||||
// get relative to map file
|
||||
{
|
||||
string localKey = Path.Combine(modRelativeMapFolder, relativePath);
|
||||
if (this.GetModFile(localKey).Exists)
|
||||
{
|
||||
assetName = this.GetInternalAssetKey(localKey);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// get from game assets
|
||||
{
|
||||
string contentKey = Path.Combine("Maps", relativePath);
|
||||
if (contentKey.EndsWith(".png"))
|
||||
contentKey = contentKey.Substring(0, contentKey.Length - 4);
|
||||
|
||||
try
|
||||
{
|
||||
this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
|
||||
assetName = contentKey;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore file-not-found errors
|
||||
// TODO: while it's useful to suppress an asset-not-found error here to avoid
|
||||
// confusion, this is a pretty naive approach. Even if the file doesn't exist,
|
||||
// the file may have been loaded through an IAssetLoader which failed. So even
|
||||
// if the content file doesn't exist, that doesn't mean the error here is a
|
||||
// content-not-found error. Unfortunately XNA doesn't provide a good way to
|
||||
// detect the error type.
|
||||
if (this.GetContentFolderFileExists(contentKey))
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// not found
|
||||
return null;
|
||||
error = "The tilesheet couldn't be found relative to either map file or the game's content folder.";
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a file from the game's content folder exists.</summary>
|
||||
|
@ -409,97 +409,5 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
// get file
|
||||
return new FileInfo(path).Exists;
|
||||
}
|
||||
|
||||
public T ModedLoad<T>(string assetName, LanguageCode language)
|
||||
{
|
||||
if (language != LanguageCode.en)
|
||||
{
|
||||
string key = assetName + "." + this.LanguageCodeString(language);
|
||||
Dictionary<string, bool> _localizedAsset = this.Reflector.GetField<Dictionary<string, bool>>(this, "_localizedAsset").GetValue();
|
||||
if (!_localizedAsset.TryGetValue(key, out bool flag) | flag)
|
||||
{
|
||||
try
|
||||
{
|
||||
_localizedAsset[key] = true;
|
||||
return this.ModedLoad<T>(key);
|
||||
}
|
||||
catch (ContentLoadException)
|
||||
{
|
||||
_localizedAsset[key] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.ModedLoad<T>(assetName);
|
||||
}
|
||||
|
||||
public T ModedLoad<T>(string assetName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetName))
|
||||
{
|
||||
throw new ArgumentNullException("assetName");
|
||||
}
|
||||
T local = default(T);
|
||||
string key = assetName.Replace('\\', '/');
|
||||
Dictionary<string, object> loadedAssets = this.Reflector.GetField<Dictionary<string, object>>(this, "loadedAssets").GetValue();
|
||||
if (loadedAssets.TryGetValue(key, out object obj2) && (obj2 is T))
|
||||
{
|
||||
return (T)obj2;
|
||||
}
|
||||
local = this.ReadAsset<T>(assetName, null);
|
||||
loadedAssets[key] = local;
|
||||
return local;
|
||||
}
|
||||
|
||||
protected override Stream OpenStream(string assetName)
|
||||
{
|
||||
Stream stream;
|
||||
try
|
||||
{
|
||||
stream = new FileStream(Path.Combine(this.RootDirectory, assetName) + ".xnb", FileMode.Open, FileAccess.Read);
|
||||
MemoryStream destination = new MemoryStream();
|
||||
stream.CopyTo(destination);
|
||||
destination.Seek(0L, SeekOrigin.Begin);
|
||||
stream.Close();
|
||||
stream = destination;
|
||||
}
|
||||
catch (Exception exception3)
|
||||
{
|
||||
throw new ContentLoadException("Opening stream error.", exception3);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
protected new T ReadAsset<T>(string assetName, Action<IDisposable> recordDisposableObject)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetName))
|
||||
{
|
||||
throw new ArgumentNullException("assetName");
|
||||
}
|
||||
;
|
||||
string str = assetName;
|
||||
object obj2 = null;
|
||||
if (this.Reflector.GetField<IGraphicsDeviceService>(this, "graphicsDeviceService").GetValue() == null)
|
||||
{
|
||||
this.Reflector.GetField<IGraphicsDeviceService>(this, "graphicsDeviceService").SetValue(this.ServiceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService);
|
||||
}
|
||||
Stream input = this.OpenStream(assetName);
|
||||
using (BinaryReader reader = new BinaryReader(input))
|
||||
{
|
||||
using (ContentReader reader2 = this.Reflector.GetMethod(this, "GetContentReaderFromXnb").Invoke<ContentReader>(assetName, input, reader, recordDisposableObject))
|
||||
{
|
||||
MethodInfo method = reader2.GetType().GetMethod("ReadAsset", BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.NonPublic, null, new Type[] { }, new ParameterModifier[] { });
|
||||
obj2 = method.MakeGenericMethod(new Type[] { typeof(T) }).Invoke(reader2, null);
|
||||
if (obj2 is GraphicsResource graphics)
|
||||
{
|
||||
graphics.Name = str;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (obj2 == null)
|
||||
{
|
||||
throw new Exception("Could not load " + str + " asset!");
|
||||
}
|
||||
return (T)obj2;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.Input
|
|||
public ICursorPosition CursorPosition => this.CursorPositionImpl;
|
||||
|
||||
/// <summary>The buttons which were pressed, held, or released.</summary>
|
||||
public IDictionary<SButton, InputStatus> ActiveButtons { get; private set; } = new Dictionary<SButton, InputStatus>();
|
||||
public IDictionary<SButton, SButtonState> ActiveButtons { get; private set; } = new Dictionary<SButton, SButtonState>();
|
||||
|
||||
/// <summary>The buttons to suppress when the game next handles input. Each button is suppressed until it's released.</summary>
|
||||
public HashSet<SButton> SuppressButtons { get; } = new HashSet<SButton>();
|
||||
|
@ -75,7 +75,7 @@ namespace StardewModdingAPI.Framework.Input
|
|||
[Obsolete("This method should only be called by the game itself.")]
|
||||
public override void Update() { }
|
||||
|
||||
/// <summary>Update the current button statuses for the given tick.</summary>
|
||||
/// <summary>Update the current button states for the given tick.</summary>
|
||||
public void TrueUpdate()
|
||||
{
|
||||
try
|
||||
|
@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Input
|
|||
GamePadState realController = GamePad.GetState(PlayerIndex.One);
|
||||
KeyboardState realKeyboard = Keyboard.GetState();
|
||||
MouseState realMouse = Mouse.GetState();
|
||||
var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
|
||||
var activeButtons = this.DeriveStates(this.ActiveButtons, realKeyboard, realMouse, realController);
|
||||
Vector2 cursorAbsolutePos = new Vector2((realMouse.X * zoomMultiplier) + Game1.viewport.X, (realMouse.Y * zoomMultiplier) + Game1.viewport.Y);
|
||||
Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
|
||||
|
||||
|
@ -102,7 +102,7 @@ namespace StardewModdingAPI.Framework.Input
|
|||
}
|
||||
|
||||
// update suppressed states
|
||||
this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown());
|
||||
this.SuppressButtons.RemoveWhere(p => !this.GetState(activeButtons, p).IsDown());
|
||||
this.UpdateSuppression();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
|
@ -159,7 +159,7 @@ namespace StardewModdingAPI.Framework.Input
|
|||
/// <param name="button">The button to check.</param>
|
||||
public bool IsDown(SButton button)
|
||||
{
|
||||
return this.GetStatus(this.ActiveButtons, button).IsDown();
|
||||
return this.GetState(this.ActiveButtons, button).IsDown();
|
||||
}
|
||||
|
||||
/// <summary>Get whether any of the given buttons were pressed or held.</summary>
|
||||
|
@ -169,11 +169,11 @@ namespace StardewModdingAPI.Framework.Input
|
|||
return buttons.Any(button => this.IsDown(button.ToSButton()));
|
||||
}
|
||||
|
||||
/// <summary>Get the status of a button.</summary>
|
||||
/// <summary>Get the state of a button.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
public InputStatus GetStatus(SButton button)
|
||||
public SButtonState GetState(SButton button)
|
||||
{
|
||||
return this.GetStatus(this.ActiveButtons, button);
|
||||
return this.GetState(this.ActiveButtons, button);
|
||||
}
|
||||
|
||||
|
||||
|
@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Input
|
|||
/// <param name="keyboardState">The game's keyboard state for the current tick.</param>
|
||||
/// <param name="mouseState">The game's mouse state for the current tick.</param>
|
||||
/// <param name="gamePadState">The game's controller state for the current tick.</param>
|
||||
private void SuppressGivenStates(IDictionary<SButton, InputStatus> activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
|
||||
private void SuppressGivenStates(IDictionary<SButton, SButtonState> activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
|
||||
{
|
||||
if (this.SuppressButtons.Count == 0)
|
||||
return;
|
||||
|
@ -252,48 +252,48 @@ namespace StardewModdingAPI.Framework.Input
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the status of all pressed or released buttons relative to their previous status.</summary>
|
||||
/// <param name="previousStatuses">The previous button statuses.</param>
|
||||
/// <summary>Get the state of all pressed or released buttons relative to their previous state.</summary>
|
||||
/// <param name="previousStates">The previous button states.</param>
|
||||
/// <param name="keyboard">The keyboard state.</param>
|
||||
/// <param name="mouse">The mouse state.</param>
|
||||
/// <param name="controller">The controller state.</param>
|
||||
private IDictionary<SButton, InputStatus> DeriveStatuses(IDictionary<SButton, InputStatus> previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller)
|
||||
private IDictionary<SButton, SButtonState> DeriveStates(IDictionary<SButton, SButtonState> previousStates, KeyboardState keyboard, MouseState mouse, GamePadState controller)
|
||||
{
|
||||
IDictionary<SButton, InputStatus> activeButtons = new Dictionary<SButton, InputStatus>();
|
||||
IDictionary<SButton, SButtonState> activeButtons = new Dictionary<SButton, SButtonState>();
|
||||
|
||||
// handle pressed keys
|
||||
SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray();
|
||||
foreach (SButton button in down)
|
||||
activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true);
|
||||
activeButtons[button] = this.DeriveState(this.GetState(previousStates, button), isDown: true);
|
||||
|
||||
// handle released keys
|
||||
foreach (KeyValuePair<SButton, InputStatus> prev in previousStatuses)
|
||||
foreach (KeyValuePair<SButton, SButtonState> prev in previousStates)
|
||||
{
|
||||
if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key))
|
||||
activeButtons[prev.Key] = InputStatus.Released;
|
||||
activeButtons[prev.Key] = SButtonState.Released;
|
||||
}
|
||||
|
||||
return activeButtons;
|
||||
}
|
||||
|
||||
/// <summary>Get the status of a button relative to its previous status.</summary>
|
||||
/// <param name="oldStatus">The previous button status.</param>
|
||||
/// <summary>Get the state of a button relative to its previous state.</summary>
|
||||
/// <param name="oldState">The previous button state.</param>
|
||||
/// <param name="isDown">Whether the button is currently down.</param>
|
||||
private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown)
|
||||
private SButtonState DeriveState(SButtonState oldState, bool isDown)
|
||||
{
|
||||
if (isDown && oldStatus.IsDown())
|
||||
return InputStatus.Held;
|
||||
if (isDown && oldState.IsDown())
|
||||
return SButtonState.Held;
|
||||
if (isDown)
|
||||
return InputStatus.Pressed;
|
||||
return InputStatus.Released;
|
||||
return SButtonState.Pressed;
|
||||
return SButtonState.Released;
|
||||
}
|
||||
|
||||
/// <summary>Get the status of a button.</summary>
|
||||
/// <summary>Get the state of a button.</summary>
|
||||
/// <param name="activeButtons">The current button states to check.</param>
|
||||
/// <param name="button">The button to check.</param>
|
||||
private InputStatus GetStatus(IDictionary<SButton, InputStatus> activeButtons, SButton button)
|
||||
private SButtonState GetState(IDictionary<SButton, SButtonState> activeButtons, SButton button)
|
||||
{
|
||||
return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None;
|
||||
return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.None;
|
||||
}
|
||||
|
||||
/// <summary>Get the buttons pressed in the given stats.</summary>
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
/// <summary>The friendly mod name for use in errors.</summary>
|
||||
private readonly string ModName;
|
||||
|
||||
/// <summary>Encapsulates monitoring and logging for a given module.</summary>
|
||||
/// <summary>Encapsulates monitoring and logging.</summary>
|
||||
private readonly IMonitor Monitor;
|
||||
|
||||
|
||||
|
@ -70,9 +70,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor)
|
||||
: base(modID)
|
||||
{
|
||||
string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
|
||||
|
||||
this.ContentCore = contentCore;
|
||||
this.GameContentManager = contentCore.CreateGameContentManager(this.ContentCore.GetManagedAssetPrefix(modID) + ".content");
|
||||
this.ModContentManager = contentCore.CreateModContentManager(this.ContentCore.GetManagedAssetPrefix(modID), modFolderPath, this.GameContentManager);
|
||||
this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content");
|
||||
this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, this.GameContentManager);
|
||||
this.ModName = modName;
|
||||
this.Monitor = monitor;
|
||||
}
|
||||
|
|
|
@ -51,11 +51,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
this.InputState.SuppressButtons.Add(button);
|
||||
}
|
||||
|
||||
/// <summary>Get the status of a button.</summary>
|
||||
/// <summary>Get the state of a button.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
public InputStatus GetStatus(SButton button)
|
||||
public SButtonState GetState(SButton button)
|
||||
{
|
||||
return this.InputState.GetStatus(button);
|
||||
return this.InputState.GetState(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -667,16 +667,16 @@ namespace StardewModdingAPI.Framework
|
|||
foreach (var pair in inputState.ActiveButtons)
|
||||
{
|
||||
SButton button = pair.Key;
|
||||
InputStatus status = pair.Value;
|
||||
SButtonState status = pair.Value;
|
||||
|
||||
if (status == InputStatus.Pressed)
|
||||
if (status == SButtonState.Pressed)
|
||||
{
|
||||
if (this.Monitor.IsVerbose)
|
||||
this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace);
|
||||
|
||||
events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState));
|
||||
}
|
||||
else if (status == InputStatus.Released)
|
||||
else if (status == SButtonState.Released)
|
||||
{
|
||||
if (this.Monitor.IsVerbose)
|
||||
this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace);
|
||||
|
|
|
@ -18,8 +18,8 @@ namespace StardewModdingAPI
|
|||
/// <param name="button">The button to suppress.</param>
|
||||
void Suppress(SButton button);
|
||||
|
||||
/// <summary>Get the status of a button.</summary>
|
||||
/// <summary>Get the state of a button.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
InputStatus GetStatus(SButton button);
|
||||
SButtonState GetState(SButton button);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>The input status for a button during an update frame.</summary>
|
||||
public enum InputStatus
|
||||
/// <summary>The input state for a button during an update frame.</summary>
|
||||
public enum SButtonState
|
||||
{
|
||||
/// <summary>The button was neither pressed, held, nor released.</summary>
|
||||
None,
|
||||
|
@ -16,14 +16,14 @@ namespace StardewModdingAPI
|
|||
Released
|
||||
}
|
||||
|
||||
/// <summary>Extension methods for <see cref="InputStatus"/>.</summary>
|
||||
/// <summary>Extension methods for <see cref="SButtonState"/>.</summary>
|
||||
internal static class InputStatusExtensions
|
||||
{
|
||||
/// <summary>Whether the button was pressed or held.</summary>
|
||||
/// <param name="status">The button status.</param>
|
||||
public static bool IsDown(this InputStatus status)
|
||||
/// <param name="state">The button state.</param>
|
||||
public static bool IsDown(this SButtonState state)
|
||||
{
|
||||
return status == InputStatus.Held || status == InputStatus.Pressed;
|
||||
return state == SButtonState.Held || state == SButtonState.Pressed;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue