make IContentPack file paths case-insensitive
This commit is contained in:
parent
4189e2f3fa
commit
57bc71c7eb
|
@ -8,6 +8,12 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Upcoming release
|
## Upcoming release
|
||||||
|
* For players:
|
||||||
|
* Fixed errors on Linux/Mac due to mods with incorrect filename case.
|
||||||
|
|
||||||
|
* For modders:
|
||||||
|
* All content pack file paths accessed through `IContentPack` are now case-insensitive.
|
||||||
|
|
||||||
* For the web UI:
|
* For the web UI:
|
||||||
* You can now renew the expiry for an uploaded JSON/log file if you need it longer.
|
* You can now renew the expiry for an uploaded JSON/log file if you need it longer.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using StardewModdingAPI.Toolkit.Serialization;
|
using StardewModdingAPI.Toolkit.Serialization;
|
||||||
using StardewModdingAPI.Toolkit.Utilities;
|
using StardewModdingAPI.Toolkit.Utilities;
|
||||||
|
@ -17,6 +18,9 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
|
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
|
||||||
private readonly JsonHelper JsonHelper;
|
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
|
** Accessors
|
||||||
|
@ -47,23 +51,29 @@ namespace StardewModdingAPI.Framework
|
||||||
this.Content = content;
|
this.Content = content;
|
||||||
this.Translation = translation;
|
this.Translation = translation;
|
||||||
this.JsonHelper = jsonHelper;
|
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 />
|
/// <inheritdoc />
|
||||||
public bool HasFile(string path)
|
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 />
|
/// <inheritdoc />
|
||||||
public TModel ReadJsonFile<TModel>(string path) where TModel : class
|
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));
|
FileInfo file = this.GetFile(path);
|
||||||
return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model)
|
return file.Exists && this.JsonHelper.ReadJsonFileIfExists(file.FullName, out TModel model)
|
||||||
? model
|
? model
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
@ -71,21 +81,30 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void WriteJsonFile<TModel>(string path, TModel data) where TModel : class
|
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));
|
FileInfo file = this.GetFile(path, out path);
|
||||||
this.JsonHelper.WriteJsonFile(path, data);
|
this.JsonHelper.WriteJsonFile(file.FullName, data);
|
||||||
|
|
||||||
|
if (!this.RelativePaths.ContainsKey(path))
|
||||||
|
this.RelativePaths[path] = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public T LoadAsset<T>(string key)
|
public T LoadAsset<T>(string key)
|
||||||
{
|
{
|
||||||
|
key = PathUtilities.NormalizePath(key);
|
||||||
|
|
||||||
|
key = this.GetCaseInsensitiveRelativePath(key);
|
||||||
return this.Content.Load<T>(key, ContentSource.ModFolder);
|
return this.Content.Load<T>(key, ContentSource.ModFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetActualAssetKey(string key)
|
public string GetActualAssetKey(string key)
|
||||||
{
|
{
|
||||||
|
key = PathUtilities.NormalizePath(key);
|
||||||
|
|
||||||
|
key = this.GetCaseInsensitiveRelativePath(key);
|
||||||
return this.Content.GetActualAssetKey(key, ContentSource.ModFolder);
|
return this.Content.GetActualAssetKey(key, ContentSource.ModFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,13 +112,32 @@ namespace StardewModdingAPI.Framework
|
||||||
/*********
|
/*********
|
||||||
** Private methods
|
** Private methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Assert that a relative path was passed it to a content pack method.</summary>
|
/// <summary>Get the real relative path from a case-insensitive path.</summary>
|
||||||
/// <param name="path">The path to check.</param>
|
/// <param name="relativePath">The normalized relative path.</param>
|
||||||
/// <param name="methodName">The name of the method which was invoked.</param>
|
private string GetCaseInsensitiveRelativePath(string relativePath)
|
||||||
private void AssertRelativePath(string path, string methodName)
|
|
||||||
{
|
{
|
||||||
if (!PathUtilities.IsSafeRelativePath(path))
|
if (!PathUtilities.IsSafeRelativePath(relativePath))
|
||||||
throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{methodName} with a relative path.");
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,32 +25,32 @@ namespace StardewModdingAPI
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Get whether a given file exists in the content pack.</summary>
|
/// <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);
|
bool HasFile(string path);
|
||||||
|
|
||||||
/// <summary>Read a JSON file from the content pack folder.</summary>
|
/// <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>
|
/// <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>
|
/// <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>
|
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
|
||||||
TModel ReadJsonFile<TModel>(string path) where TModel : class;
|
TModel ReadJsonFile<TModel>(string path) where TModel : class;
|
||||||
|
|
||||||
/// <summary>Save data to a JSON file in the content pack's folder.</summary>
|
/// <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>
|
/// <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>
|
/// <param name="data">The arbitrary data to save.</param>
|
||||||
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
|
/// <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;
|
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>
|
/// <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>
|
/// <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="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>
|
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
|
||||||
T LoadAsset<T>(string key);
|
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>
|
/// <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>
|
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||||
string GetActualAssetKey(string key);
|
string GetActualAssetKey(string key);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue