Merge branch 'develop' of https://github.com/Pathoschild/SMAPI into develop

 Conflicts:
	src/SMAPI/Framework/SCore.cs
This commit is contained in:
ZaneYork 2020-09-17 09:00:04 +08:00
commit 7eb1b9e372
17 changed files with 181 additions and 171 deletions

View File

@ -69,3 +69,10 @@ csharp_style_inlined_variable_declaration = true:warning
# avoid superfluous braces
csharp_prefer_braces = false:suggestion
##########
## Column guidelines
## documentation: https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines
##########
[*.md]
guidelines = 100

View File

@ -7,6 +7,18 @@
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
-->
## Upcoming release
* For players:
* Fixed errors on Linux/Mac due to mods with incorrect filename case.
* Fixed map rendering crash due to conflict between SMAPI and PyTK.
* Fixed error in heuristically-rewritten mods in rare cases (thanks to ZaneYork!).
* For modders:
* All content pack file paths accessed through `IContentPack` are now case-insensitive.
* For the web UI:
* You can now renew the expiry for an uploaded JSON/log file if you need it longer.
## 3.7.2
Released 08 September 2020 for Stardew Valley 1.4.1 or later.

View File

@ -58,7 +58,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Render the schema validator UI.</summary>
/// <param name="schemaName">The schema name with which to validate the JSON, or 'edit' to return to the edit screen.</param>
/// <param name="id">The stored file ID.</param>
/// <param name="operation">The operation to perform for the selected log ID. This can be 'edit', or any other value to view.</param>
/// <param name="operation">The operation to perform for the selected log ID. This can be 'edit', 'renew', or any other value to view.</param>
[HttpGet]
[Route("json")]
[Route("json/{schemaName}")]
@ -68,8 +68,10 @@ namespace StardewModdingAPI.Web.Controllers
{
// parse arguments
schemaName = this.NormalizeSchemaName(schemaName);
operation = operation?.Trim().ToLower();
bool hasId = !string.IsNullOrWhiteSpace(id);
bool isEditView = !hasId || operation?.Trim().ToLower() == "edit";
bool isEditView = !hasId || operation == "edit";
bool renew = operation == "renew";
// build result model
var result = this.GetModel(id, schemaName, isEditView);
@ -77,7 +79,7 @@ namespace StardewModdingAPI.Web.Controllers
return this.View("Index", result);
// fetch raw JSON
StoredFileInfo file = await this.Storage.GetAsync(id);
StoredFileInfo file = await this.Storage.GetAsync(id, renew);
if (string.IsNullOrWhiteSpace(file.Content))
return this.View("Index", result.SetUploadError("The JSON file seems to be empty."));
result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning);

View File

@ -40,17 +40,18 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Render the log parser UI.</summary>
/// <param name="id">The stored file ID.</param>
/// <param name="raw">Whether to display the raw unparsed log.</param>
/// <param name="renew">Whether to reset the log expiry.</param>
[HttpGet]
[Route("log")]
[Route("log/{id}")]
public async Task<ViewResult> Index(string id = null, bool raw = false)
public async Task<ViewResult> Index(string id = null, bool raw = false, bool renew = false)
{
// fresh page
if (string.IsNullOrWhiteSpace(id))
return this.View("Index", this.GetModel(id));
// log page
StoredFileInfo file = await this.Storage.GetAsync(id);
StoredFileInfo file = await this.Storage.GetAsync(id, renew);
ParsedLog log = file.Success
? new LogParser().Parse(file.Content)
: new ParsedLog { IsValid = false, Error = file.Error };

View File

@ -13,6 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Storage
/// <summary>Fetch raw text from storage.</summary>
/// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param>
Task<StoredFileInfo> GetAsync(string id);
/// <param name="renew">Whether to reset the file expiry.</param>
Task<StoredFileInfo> GetAsync(string id, bool renew);
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
@ -48,10 +49,7 @@ namespace StardewModdingAPI.Web.Framework.Storage
this.GzipHelper = gzipHelper;
}
/// <summary>Save a text file to storage.</summary>
/// <param name="content">The content to upload.</param>
/// <param name="compress">Whether to gzip the text.</param>
/// <returns>Returns metadata about the save attempt.</returns>
/// <inheritdoc />
public async Task<UploadResult> SaveAsync(string content, bool compress = true)
{
string id = Guid.NewGuid().ToString("N");
@ -84,9 +82,8 @@ namespace StardewModdingAPI.Web.Framework.Storage
}
}
/// <summary>Fetch raw text from storage.</summary>
/// <param name="id">The storage ID returned by <see cref="SaveAsync"/>.</param>
public async Task<StoredFileInfo> GetAsync(string id)
/// <inheritdoc />
public async Task<StoredFileInfo> GetAsync(string id, bool renew)
{
// fetch from blob storage
if (Guid.TryParseExact(id, "N", out Guid _))
@ -96,14 +93,21 @@ namespace StardewModdingAPI.Web.Framework.Storage
{
try
{
// get client
BlobClient blob = this.GetAzureBlobClient(id);
// extend expiry
if (renew)
await blob.SetMetadataAsync(new Dictionary<string, string> { ["expiryRenewed"] = DateTime.UtcNow.ToString("O") }); // change the blob's last-modified date (the specific property set doesn't matter)
// fetch file
Response<BlobDownloadInfo> response = await blob.DownloadAsync();
using BlobDownloadInfo result = response.Value;
using StreamReader reader = new StreamReader(result.Content);
DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ExpiryDays);
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
// build model
return new StoredFileInfo
{
Success = true,
@ -125,13 +129,26 @@ namespace StardewModdingAPI.Web.Framework.Storage
// local filesystem for testing
else
{
// get file
FileInfo file = new FileInfo(this.GetDevFilePath(id));
if (file.Exists)
{
if (file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow)
if (file.Exists && file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow) // expired
file.Delete();
else
if (!file.Exists)
{
return new StoredFileInfo
{
Error = "There's no file with that ID."
};
}
// renew
if (renew)
{
File.SetLastWriteTimeUtc(file.FullName, DateTime.UtcNow);
file.Refresh();
}
// build model
return new StoredFileInfo
{
Success = true,
@ -141,12 +158,6 @@ namespace StardewModdingAPI.Web.Framework.Storage
};
}
}
return new StoredFileInfo
{
Error = "There's no file with that ID."
};
}
}
// get from Pastebin
else

View File

@ -76,7 +76,7 @@ else if (!Model.IsEditView && Model.PasteID != null)
<div class="save-metadata" v-pre>
@if (Model.Expiry != null)
{
<text>This JSON file will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()). </text>
<text>This JSON file will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()) (<a href="@(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = this.Model.SchemaName, id = this.Model.PasteID, operation = "renew" }))">renew</a>).</text>
}
<!--@Model.UploadWarning-->
</div>

View File

@ -78,7 +78,7 @@ else if (Model.ParsedLog?.IsValid == true)
<div class="save-metadata" v-pre>
@if (Model.Expiry != null)
{
<text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()).</text>
<text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()) (<a href="@(this.Url.PlainAction("Index", "LogParser", new { id = this.Model.PasteID, renew = true }))">renew</a>).</text>
}
</div>
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
@ -17,6 +18,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
/// <summary>A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/Mac.</summary>
private readonly IDictionary<string, string> RelativePaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/*********
** Accessors
@ -47,23 +51,29 @@ namespace StardewModdingAPI.Framework
this.Content = content;
this.Translation = translation;
this.JsonHelper = jsonHelper;
foreach (string path in Directory.EnumerateFiles(this.DirectoryPath, "*", SearchOption.AllDirectories))
{
string relativePath = path.Substring(this.DirectoryPath.Length + 1);
this.RelativePaths[relativePath] = relativePath;
}
}
/// <inheritdoc />
public bool HasFile(string path)
{
this.AssertRelativePath(path, nameof(this.HasFile));
path = PathUtilities.NormalizePath(path);
return File.Exists(Path.Combine(this.DirectoryPath, path));
return this.GetFile(path).Exists;
}
/// <inheritdoc />
public TModel ReadJsonFile<TModel>(string path) where TModel : class
{
this.AssertRelativePath(path, nameof(this.ReadJsonFile));
path = PathUtilities.NormalizePath(path);
path = Path.Combine(this.DirectoryPath, PathUtilities.NormalizePath(path));
return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model)
FileInfo file = this.GetFile(path);
return file.Exists && this.JsonHelper.ReadJsonFileIfExists(file.FullName, out TModel model)
? model
: null;
}
@ -71,21 +81,30 @@ namespace StardewModdingAPI.Framework
/// <inheritdoc />
public void WriteJsonFile<TModel>(string path, TModel data) where TModel : class
{
this.AssertRelativePath(path, nameof(this.WriteJsonFile));
path = PathUtilities.NormalizePath(path);
path = Path.Combine(this.DirectoryPath, PathUtilities.NormalizePath(path));
this.JsonHelper.WriteJsonFile(path, data);
FileInfo file = this.GetFile(path, out path);
this.JsonHelper.WriteJsonFile(file.FullName, data);
if (!this.RelativePaths.ContainsKey(path))
this.RelativePaths[path] = path;
}
/// <inheritdoc />
public T LoadAsset<T>(string key)
{
key = PathUtilities.NormalizePath(key);
key = this.GetCaseInsensitiveRelativePath(key);
return this.Content.Load<T>(key, ContentSource.ModFolder);
}
/// <inheritdoc />
public string GetActualAssetKey(string key)
{
key = PathUtilities.NormalizePath(key);
key = this.GetCaseInsensitiveRelativePath(key);
return this.Content.GetActualAssetKey(key, ContentSource.ModFolder);
}
@ -93,13 +112,32 @@ namespace StardewModdingAPI.Framework
/*********
** Private methods
*********/
/// <summary>Assert that a relative path was passed it to a content pack method.</summary>
/// <param name="path">The path to check.</param>
/// <param name="methodName">The name of the method which was invoked.</param>
private void AssertRelativePath(string path, string methodName)
/// <summary>Get the real relative path from a case-insensitive path.</summary>
/// <param name="relativePath">The normalized relative path.</param>
private string GetCaseInsensitiveRelativePath(string relativePath)
{
if (!PathUtilities.IsSafeRelativePath(path))
throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{methodName} with a relative path.");
if (!PathUtilities.IsSafeRelativePath(relativePath))
throw new InvalidOperationException($"You must call {nameof(IContentPack)} methods with a relative path.");
return this.RelativePaths.TryGetValue(relativePath, out string caseInsensitivePath)
? caseInsensitivePath
: relativePath;
}
/// <summary>Get the underlying file info.</summary>
/// <param name="relativePath">The normalized file path relative to the content pack directory.</param>
private FileInfo GetFile(string relativePath)
{
return this.GetFile(relativePath, out _);
}
/// <summary>Get the underlying file info.</summary>
/// <param name="relativePath">The normalized file path relative to the content pack directory.</param>
/// <param name="actualRelativePath">The relative path after case-insensitive matching.</param>
private FileInfo GetFile(string relativePath, out string actualRelativePath)
{
actualRelativePath = this.GetCaseInsensitiveRelativePath(relativePath);
return new FileInfo(Path.Combine(this.DirectoryPath, actualRelativePath));
}
}
}

View File

@ -338,7 +338,6 @@ namespace StardewModdingAPI.Framework.ModLoading
// find or rewrite code
InstructionMetadata instructionMetadata = new InstructionMetadata(this.Monitor);
IInstructionHandler[] handlers = instructionMetadata.GetHandlers(this.ParanoidMode, platformChanged).ToArray();
IInstructionHandler[] finalHandlers = instructionMetadata.GetFinalHandlers().ToArray();
RecursiveRewriter rewriter = new RecursiveRewriter(
module: module,
rewriteType: (type, replaceWith) =>
@ -353,11 +352,6 @@ namespace StardewModdingAPI.Framework.ModLoading
bool rewritten = false;
foreach (IInstructionHandler handler in handlers)
rewritten |= handler.Handle(module, cil, instruction);
if (rewritten)
{
foreach (IInstructionHandler handler in finalHandlers)
handler.Handle(module, cil, instruction);
}
return rewritten;
}
);
@ -369,11 +363,6 @@ namespace StardewModdingAPI.Framework.ModLoading
foreach (var flag in handler.Flags)
this.ProcessInstructionHandleResult(mod, handler, flag, loggedMessages, logPrefix, filename);
}
foreach (IInstructionHandler handler in finalHandlers)
{
foreach (var flag in handler.Flags)
this.ProcessInstructionHandleResult(mod, handler, flag, loggedMessages, logPrefix, filename);
}
return platformChanged || anyRewritten;
}

View File

@ -111,21 +111,39 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
foreach (VariableDefinition variable in method.Body.Variables)
changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType);
// check CIL instructions
// rewrite CIL instructions
ILProcessor cil = method.Body.GetILProcessor();
Collection<Instruction> instructions = cil.Body.Instructions;
bool addedInstructions = false;
for (int i = 0; i < instructions.Count; i++)
{
var instruction = instructions[i];
if (instruction.OpCode.Code == Code.Nop)
continue;
int oldCount = cil.Body.Instructions.Count;
changed |= this.RewriteInstruction(instruction, cil, newInstruction =>
{
changed = true;
cil.Replace(instruction, newInstruction);
instruction = newInstruction;
});
if (cil.Body.Instructions.Count > oldCount)
addedInstructions = true;
}
// special case: added instructions may cause an instruction to be out of range
// of a short jump that references it
if (addedInstructions)
{
foreach (var instruction in instructions)
{
var longJumpCode = RewriteHelper.GetEquivalentLongJumpCode(instruction.OpCode);
if (longJumpCode != null)
instruction.OpCode = longJumpCode.Value;
}
changed = true;
}
}
}

View File

@ -97,6 +97,30 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
};
}
/// <summary>Get the long equivalent for a short-jump op code.</summary>
/// <param name="shortJumpCode">The short-jump op code.</param>
/// <returns>Returns the instruction, or <c>null</c> if it isn't a short jump.</returns>
public static OpCode? GetEquivalentLongJumpCode(OpCode shortJumpCode)
{
return shortJumpCode.Code switch
{
Code.Beq_S => OpCodes.Beq,
Code.Bge_S => OpCodes.Bge,
Code.Bge_Un_S => OpCodes.Bge_Un,
Code.Bgt_S => OpCodes.Bgt,
Code.Bgt_Un_S => OpCodes.Bgt_Un,
Code.Ble_S => OpCodes.Ble,
Code.Ble_Un_S => OpCodes.Ble_Un,
Code.Blt_S => OpCodes.Blt,
Code.Blt_Un_S => OpCodes.Blt_Un,
Code.Bne_Un_S => OpCodes.Bne_Un,
Code.Br_S => OpCodes.Br,
Code.Brfalse_S => OpCodes.Brfalse,
Code.Brtrue_S => OpCodes.Brtrue,
_ => (OpCode?)null
};
}
/// <summary>Get whether a type matches a type reference.</summary>
/// <param name="type">The defined type.</param>
/// <param name="reference">The type reference.</param>

View File

@ -1,100 +0,0 @@
using Mono.Cecil;
using Mono.Cecil.Cil;
using StardewModdingAPI.Framework.ModLoading.Framework;
namespace StardewModdingAPI.Framework.ModLoading.Rewriters
{
/// <summary>Rewrites method references from one parent type to another if the signatures match.</summary>
internal class BrokenShortJumpRewriter : BaseInstructionHandler
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="fromType">The type whose methods to remap.</param>
/// <param name="toType">The type with methods to map to.</param>
/// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
public BrokenShortJumpRewriter(string nounPhrase = null)
: base(nounPhrase ?? $"method's short jump")
{
}
/// <inheritdoc />
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
// get method ref
bool rewritten = false;
// dynamic inserted instruction
if (instruction.Offset == 0 && instruction.Previous != null)
return false;
// rewrite does not insert instruction
if (!(instruction.Previous != null && instruction.Previous.Offset == 0
|| instruction.Next != null && instruction.Next.Offset == 0))
return false;
foreach (var ins in cil.Body.Instructions)
{
// dynamic inserted instruction
if(ins.Offset == 0)
continue;
OpCode targetOp = this.RewriteTarget(ins);
// not a jump instruction
if (targetOp == OpCodes.Nop)
continue;
Instruction insJumpTo = (Instruction) ins.Operand;
// jump forward
if (insJumpTo.Offset > ins.Offset)
{
// jump across the rewrite point
if(instruction.Offset > ins.Offset && instruction.Offset < insJumpTo.Offset)
{
// rewrite
ins.OpCode = targetOp;
rewritten = true;
}
}
// jump backward
else
{
// jump across the rewrite point
if(instruction.Offset > insJumpTo.Offset && instruction.Offset < ins.Offset)
{
// rewrite
ins.OpCode = targetOp;
rewritten = true;
}
}
}
if(rewritten)
return this.MarkRewritten();
return false;
}
/*********
** Private methods
*********/
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="methodRef">The method reference.</param>
private OpCode RewriteTarget(Instruction instruction)
{
return instruction.OpCode.Code switch
{
Code.Beq_S => OpCodes.Beq,
Code.Bge_S => OpCodes.Bge,
Code.Bgt_S => OpCodes.Bgt,
Code.Ble_S => OpCodes.Ble,
Code.Blt_S => OpCodes.Blt,
Code.Br_S => OpCodes.Br,
Code.Brfalse_S => OpCodes.Brfalse,
Code.Brtrue_S => OpCodes.Brtrue,
Code.Bge_Un_S => OpCodes.Bge_Un,
Code.Bgt_Un_S => OpCodes.Bgt_Un,
Code.Ble_Un_S => OpCodes.Ble_Un,
Code.Blt_Un_S => OpCodes.Blt_Un,
Code.Bne_Un_S => OpCodes.Bne_Un,
_ => OpCodes.Nop
};
}
}
}

View File

@ -51,6 +51,7 @@ using StardewValley;
using StardewValley.Menus;
using StardewValley.Mobile;
#endif
using xTile.Display;
using SObject = StardewValley.Object;
namespace StardewModdingAPI.Framework
@ -517,8 +518,13 @@ namespace StardewModdingAPI.Framework
SCore.PerformanceMonitor.PrintQueuedAlerts();
// reapply overrides
if (this.JustReturnedToTitle && !(Game1.mapDisplayDevice is SDisplayDevice))
Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice);
if (this.JustReturnedToTitle)
{
if (!(Game1.mapDisplayDevice is SDisplayDevice))
Game1.mapDisplayDevice = this.GetMapDisplayDevice();
this.JustReturnedToTitle = false;
}
/*********
** First-tick initialization
@ -1094,7 +1100,7 @@ namespace StardewModdingAPI.Framework
}
catch (Exception ex)
{
this.LogManager.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error);
this.LogManager.MonitorForGame.Log($"An error occurred in the base update loop: {ex.GetLogSummary()}", LogLevel.Error);
}
events.UnvalidatedUpdateTicked.RaiseEmpty();
@ -1111,7 +1117,7 @@ namespace StardewModdingAPI.Framework
catch (Exception ex)
{
// log error
this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error);
this.Monitor.Log($"An error occurred in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error);
// exit if irrecoverable
if (!this.UpdateCrashTimer.Decrement())
@ -1863,6 +1869,13 @@ namespace StardewModdingAPI.Framework
return translations;
}
/// <summary>Get the map display device which applies SMAPI features like tile rotation to loaded maps.</summary>
/// <remarks>This is separate to let mods like PyTK wrap it with their own functionality.</remarks>
private IDisplayDevice GetMapDisplayDevice()
{
return new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice);
}
/// <summary>Get the absolute path to the next available log file.</summary>
private string GetLogPath()
{

View File

@ -312,7 +312,7 @@ namespace StardewModdingAPI.Framework
catch (Exception ex)
{
// log error
this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error);
this.Monitor.Log($"An error occurred in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error);
// exit if irrecoverable
if (!this.DrawCrashTimer.Decrement())

View File

@ -25,32 +25,32 @@ namespace StardewModdingAPI
** Public methods
*********/
/// <summary>Get whether a given file exists in the content pack.</summary>
/// <param name="path">The file path to check.</param>
/// <param name="path">The relative file path within the content pack (case-insensitive).</param>
bool HasFile(string path);
/// <summary>Read a JSON file from the content pack folder.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="path">The file path relative to the content pack directory.</param>
/// <param name="path">The relative file path within the content pack (case-insensitive).</param>
/// <returns>Returns the deserialized model, or <c>null</c> if the file doesn't exist or is empty.</returns>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
TModel ReadJsonFile<TModel>(string path) where TModel : class;
/// <summary>Save data to a JSON file in the content pack's folder.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="path">The file path relative to the mod folder.</param>
/// <param name="path">The relative file path within the content pack (case-insensitive).</param>
/// <param name="data">The arbitrary data to save.</param>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
void WriteJsonFile<TModel>(string path, TModel data) where TModel : class;
/// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
/// <param name="key">The local path to a content file relative to the content pack folder.</param>
/// <param name="key">The relative file path within the content pack (case-insensitive).</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
T LoadAsset<T>(string key);
/// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary>
/// <param name="key">The the local path to a content file relative to the content pack folder.</param>
/// <param name="key">The relative file path within the content pack (case-insensitive).</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
string GetActualAssetKey(string key);
}

View File

@ -165,11 +165,5 @@ namespace StardewModdingAPI.Metadata
yield return new TypeFinder(typeof(System.Diagnostics.Process).FullName, InstructionHandleResult.DetectedShellAccess);
}
}
/// <summary>Get rewriters which do final action for CIL code which been rewritten.</summary>
public IEnumerable<IInstructionHandler> GetFinalHandlers()
{
yield return new BrokenShortJumpRewriter();
}
}
}