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,25 +129,32 @@ namespace StardewModdingAPI.Web.Framework.Storage
// local filesystem for testing
else
{
+ // get file
FileInfo file = new FileInfo(this.GetDevFilePath(id));
- if (file.Exists)
+ if (file.Exists && file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow) // expired
+ file.Delete();
+ if (!file.Exists)
{
- if (file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow)
- file.Delete();
- else
+ return new StoredFileInfo
{
- return new StoredFileInfo
- {
- Success = true,
- Content = File.ReadAllText(file.FullName),
- Expiry = DateTime.UtcNow.AddDays(this.ExpiryDays),
- Warning = "This file was saved temporarily to the local computer. This should only happen in a local development environment."
- };
- }
+ Error = "There's no file with that ID."
+ };
}
+
+ // renew
+ if (renew)
+ {
+ File.SetLastWriteTimeUtc(file.FullName, DateTime.UtcNow);
+ file.Refresh();
+ }
+
+ // build model
return new StoredFileInfo
{
- Error = "There's no file with that ID."
+ Success = true,
+ Content = File.ReadAllText(file.FullName),
+ Expiry = DateTime.UtcNow.AddDays(this.ExpiryDays),
+ Warning = "This file was saved temporarily to the local computer. This should only happen in a local development environment."
};
}
}
diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
index 7b89a23d..1db79857 100644
--- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
+++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
@@ -76,7 +76,7 @@ else if (!Model.IsEditView && Model.PasteID != null)
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 71e12d47..d4ff4f10 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -78,7 +78,7 @@ else if (Model.ParsedLog?.IsValid == true)
}
diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs
index 65abba5b..161fdbe4 100644
--- a/src/SMAPI/Framework/ContentPack.cs
+++ b/src/SMAPI/Framework/ContentPack.cs
@@ -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
/// Encapsulates SMAPI's JSON file parsing.
private readonly JsonHelper JsonHelper;
+ /// A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/Mac.
+ private readonly IDictionary RelativePaths = new Dictionary(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;
+ }
}
///
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;
}
///
public TModel ReadJsonFile(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
///
public void WriteJsonFile(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;
}
///
public T LoadAsset(string key)
{
+ key = PathUtilities.NormalizePath(key);
+
+ key = this.GetCaseInsensitiveRelativePath(key);
return this.Content.Load(key, ContentSource.ModFolder);
}
///
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
*********/
- /// Assert that a relative path was passed it to a content pack method.
- /// The path to check.
- /// The name of the method which was invoked.
- private void AssertRelativePath(string path, string methodName)
+ /// Get the real relative path from a case-insensitive path.
+ /// The normalized relative path.
+ 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;
+ }
+
+ /// Get the underlying file info.
+ /// The normalized file path relative to the content pack directory.
+ private FileInfo GetFile(string relativePath)
+ {
+ return this.GetFile(relativePath, out _);
+ }
+
+ /// Get the underlying file info.
+ /// The normalized file path relative to the content pack directory.
+ /// The relative path after case-insensitive matching.
+ private FileInfo GetFile(string relativePath, out string actualRelativePath)
+ {
+ actualRelativePath = this.GetCaseInsensitiveRelativePath(relativePath);
+ return new FileInfo(Path.Combine(this.DirectoryPath, actualRelativePath));
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index c59fdbdf..36d28695 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -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;
}
diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs
index ea29550a..10f68f0d 100644
--- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs
@@ -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 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;
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
index d46aee0c..4a45844a 100644
--- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
+++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
@@ -97,6 +97,30 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
};
}
+ /// Get the long equivalent for a short-jump op code.
+ /// The short-jump op code.
+ /// Returns the instruction, or null if it isn't a short jump.
+ 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
+ };
+ }
+
/// Get whether a type matches a type reference.
/// The defined type.
/// The type reference.
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/BrokenShortJumpRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/BrokenShortJumpRewriter.cs
deleted file mode 100644
index 056ab1f8..00000000
--- a/src/SMAPI/Framework/ModLoading/Rewriters/BrokenShortJumpRewriter.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using Mono.Cecil;
-using Mono.Cecil.Cil;
-using StardewModdingAPI.Framework.ModLoading.Framework;
-
-namespace StardewModdingAPI.Framework.ModLoading.Rewriters
-{
- /// Rewrites method references from one parent type to another if the signatures match.
- internal class BrokenShortJumpRewriter : BaseInstructionHandler
- {
- /*********
- ** Public methods
- *********/
- /// Construct an instance.
- /// The type whose methods to remap.
- /// The type with methods to map to.
- /// A brief noun phrase indicating what the instruction finder matches (or null to generate one).
- public BrokenShortJumpRewriter(string nounPhrase = null)
- : base(nounPhrase ?? $"method's short jump")
- {
- }
-
- ///
- 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
- *********/
- /// Get whether a CIL instruction matches.
- /// The method reference.
- 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
- };
- }
- }
-}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index a903f345..8ae7b759 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -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;
}
+ /// Get the map display device which applies SMAPI features like tile rotation to loaded maps.
+ /// This is separate to let mods like PyTK wrap it with their own functionality.
+ private IDisplayDevice GetMapDisplayDevice()
+ {
+ return new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice);
+ }
+
/// Get the absolute path to the next available log file.
private string GetLogPath()
{
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 7db4090f..73d74ccc 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -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())
diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs
index c0479eae..9cc64dcd 100644
--- a/src/SMAPI/IContentPack.cs
+++ b/src/SMAPI/IContentPack.cs
@@ -25,32 +25,32 @@ namespace StardewModdingAPI
** Public methods
*********/
/// Get whether a given file exists in the content pack.
- /// The file path to check.
+ /// The relative file path within the content pack (case-insensitive).
bool HasFile(string path);
/// Read a JSON file from the content pack folder.
/// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.
- /// The file path relative to the content pack directory.
+ /// The relative file path within the content pack (case-insensitive).
/// Returns the deserialized model, or null if the file doesn't exist or is empty.
/// The is not relative or contains directory climbing (../).
TModel ReadJsonFile(string path) where TModel : class;
/// Save data to a JSON file in the content pack's folder.
/// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.
- /// The file path relative to the mod folder.
+ /// The relative file path within the content pack (case-insensitive).
/// The arbitrary data to save.
/// The is not relative or contains directory climbing (../).
void WriteJsonFile(string path, TModel data) where TModel : class;
/// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop.
/// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline.
- /// The local path to a content file relative to the content pack folder.
+ /// The relative file path within the content pack (case-insensitive).
/// The is empty or contains invalid characters.
/// The content asset couldn't be loaded (e.g. because it doesn't exist).
T LoadAsset(string key);
/// 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.
- /// The the local path to a content file relative to the content pack folder.
+ /// The relative file path within the content pack (case-insensitive).
/// The is empty or contains invalid characters.
string GetActualAssetKey(string key);
}
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 1f46c33f..6481ba97 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -165,11 +165,5 @@ namespace StardewModdingAPI.Metadata
yield return new TypeFinder(typeof(System.Diagnostics.Process).FullName, InstructionHandleResult.DetectedShellAccess);
}
}
- /// Get rewriters which do final action for CIL code which been rewritten.
- public IEnumerable GetFinalHandlers()
- {
- yield return new BrokenShortJumpRewriter();
- }
-
}
}