diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-08-25 21:54:00 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-08-25 21:54:00 -0400 |
commit | 211f89821e34bb55a6266384d9bac68ec0c64744 (patch) | |
tree | aa0d9915b01aad436ed6b1a2028602c048666457 /src/SMAPI | |
parent | 80d3dd1f786f7e5846f9adb7f7a4d82e5b9b92fd (diff) | |
parent | 31ac964a8b19623b0472931403a33d51db6fb271 (diff) | |
download | SMAPI-211f89821e34bb55a6266384d9bac68ec0c64744.tar.gz SMAPI-211f89821e34bb55a6266384d9bac68ec0c64744.tar.bz2 SMAPI-211f89821e34bb55a6266384d9bac68ec0c64744.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI')
-rw-r--r-- | src/SMAPI/Constants.cs | 29 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/LogManager.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/DataHelper.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/TranslationHelper.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs | 16 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 38 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs | 72 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs | 54 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs | 40 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 15 | ||||
-rw-r--r-- | src/SMAPI/Framework/Translator.cs | 65 | ||||
-rw-r--r-- | src/SMAPI/IDataHelper.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/ITranslationHelper.cs | 7 |
13 files changed, 315 insertions, 46 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 6cbdeb8e..cce267ba 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Mono.Cecil; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; @@ -61,7 +62,7 @@ namespace StardewModdingAPI internal static int? LogScreenId { get; set; } /// <summary>SMAPI's current raw semantic version.</summary> - internal static string RawApiVersion = "3.12.2"; + internal static string RawApiVersion = "3.12.3"; } /// <summary>Contains SMAPI's constants and assumptions.</summary> @@ -229,6 +230,32 @@ namespace StardewModdingAPI } } + /// <summary>Configure the Mono.Cecil assembly resolver.</summary> + /// <param name="resolver">The assembly resolver.</param> + internal static void ConfigureAssemblyResolver(AssemblyDefinitionResolver resolver) + { + // add search paths + resolver.AddSearchDirectory(Constants.ExecutionPath); + resolver.AddSearchDirectory(Constants.InternalFilesPath); + + // add SMAPI explicitly + // Normally this would be handled automatically by the search paths, but for some reason there's a specific + // case involving unofficial 64-bit Stardew Valley when launched through Steam (for some players only) + // where Mono.Cecil can't resolve references to SMAPI. + resolver.Add(AssemblyDefinition.ReadAssembly(typeof(SGame).Assembly.Location)); + + // make sure game assembly names can be resolved + // The game assembly can have one of three names depending how the mod was compiled: + // - 'StardewValley': assembly name on Linux/macOS; + // - 'Stardew Valley': assembly name on Windows; + // - 'Netcode': an assembly that's separate on Windows only. + resolver.Add(AssemblyDefinition.ReadAssembly(typeof(Game1).Assembly.Location), "StardewValley", "Stardew Valley" +#if !SMAPI_FOR_WINDOWS + , "Netcode" +#endif + ); + } + /// <summary>Get metadata for mapping assemblies to the current platform.</summary> /// <param name="targetPlatform">The target game platform.</param> /// <param name="framework">The game framework running the game.</param> diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index a3d4f23d..c6faa90d 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -109,9 +109,12 @@ namespace StardewModdingAPI.Framework.Logging output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); Console.SetOut(output); - // enable Unicode handling + // enable Unicode handling on Windows + // (the terminal defaults to UTF-8 on Linux/macOS) +#if SMAPI_FOR_WINDOWS Console.InputEncoding = Encoding.Unicode; Console.OutputEncoding = Encoding.Unicode; +#endif } /// <summary>Get a monitor instance derived from SMAPI's current settings.</summary> @@ -162,8 +165,6 @@ namespace StardewModdingAPI.Framework.Logging // keep console thread alive while the game is running while (continueWhile()) Thread.Sleep(1000 / 10); - if (inputThread.ThreadState == ThreadState.Running) - inputThread.Abort(); } /// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary> diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 0fe3209f..4cbfd73f 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -58,7 +58,11 @@ namespace StardewModdingAPI.Framework.ModHelpers throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing)."); path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path)); - this.JsonHelper.WriteJsonFile(path, data); + + if (data != null) + this.JsonHelper.WriteJsonFile(path, data); + else + File.Delete(path); } /**** diff --git a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs index a88ca9c9..869664fe 100644 --- a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs @@ -55,6 +55,12 @@ namespace StardewModdingAPI.Framework.ModHelpers return this.Translator.Get(key, tokens); } + /// <inheritdoc /> + public IDictionary<string, Translation> GetInAllLocales(string key, bool withFallback = false) + { + return this.Translator.GetInAllLocales(key, withFallback); + } + /// <summary>Set the translations to use.</summary> /// <param name="translations">The translations to use.</param> internal TranslationHelper SetTranslations(IDictionary<string, IDictionary<string, string>> translations) diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index aefb0126..b3415609 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -21,11 +21,17 @@ namespace StardewModdingAPI.Framework.ModLoading public void Add(params AssemblyDefinition[] assemblies) { foreach (AssemblyDefinition assembly in assemblies) - { - this.RegisterAssembly(assembly); - this.Lookup[assembly.Name.Name] = assembly; - this.Lookup[assembly.Name.FullName] = assembly; - } + this.Add(assembly, assembly.Name.Name, assembly.Name.FullName); + } + + /// <summary>Add known assemblies to the resolver.</summary> + /// <param name="assembly">The assembly to add.</param> + /// <param name="names">The assembly names for which it should be returned.</param> + public void Add(AssemblyDefinition assembly, params string[] names) + { + this.RegisterAssembly(assembly); + foreach (string name in names) + this.Lookup[name] = assembly; } /// <summary>Resolve an assembly reference.</summary> diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 2b71038a..e1ad9d37 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -7,6 +7,7 @@ using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.Symbols; using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Utilities; @@ -34,6 +35,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>A minimal assembly definition resolver which resolves references to known loaded assemblies.</summary> private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver; + /// <summary>Provides assembly symbol readers for Mono.Cecil.</summary> + private readonly SymbolReaderProvider SymbolReaderProvider = new SymbolReaderProvider(); + + /// <summary>Provides assembly symbol writers for Mono.Cecil.</summary> + private readonly SymbolWriterProvider SymbolWriterProvider = new SymbolWriterProvider(); + /// <summary>The objects to dispose as part of this instance.</summary> private readonly HashSet<IDisposable> Disposables = new HashSet<IDisposable>(); @@ -59,9 +66,7 @@ namespace StardewModdingAPI.Framework.ModLoading // init resolver this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); - this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); - this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); - this.AssemblyDefinitionResolver.Add(AssemblyDefinition.ReadAssembly(typeof(SGame).Assembly.Location)); // for some reason Mono.Cecil can't resolve SMAPI in very specific cases involving unofficial 64-bit Stardew Valley when launched through Steam (for some players only) + Constants.ConfigureAssemblyResolver(this.AssemblyDefinitionResolver); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary<string, Assembly>(); @@ -136,20 +141,12 @@ namespace StardewModdingAPI.Framework.ModLoading if (!oneAssembly) this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace); - // load PDB file if present - byte[] symbols; - { - string symbolsPath = Path.Combine(Path.GetDirectoryName(assemblyPath), Path.GetFileNameWithoutExtension(assemblyPath)) + ".pdb"; - symbols = File.Exists(symbolsPath) - ? File.ReadAllBytes(symbolsPath) - : null; - } - // load assembly - using MemoryStream outStream = new MemoryStream(); - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes, symbols); + using MemoryStream outAssemblyStream = new MemoryStream(); + using MemoryStream outSymbolStream = new MemoryStream(); + assembly.Definition.Write(outAssemblyStream, new WriterParameters { WriteSymbols = true, SymbolStream = outSymbolStream, SymbolWriterProvider = this.SymbolWriterProvider }); + byte[] bytes = outAssemblyStream.ToArray(); + lastAssembly = Assembly.Load(bytes, outSymbolStream.ToArray()); } else { @@ -236,10 +233,15 @@ namespace StardewModdingAPI.Framework.ModLoading if (!file.Exists) yield break; // not a local assembly - // read assembly + // read assembly and symbols byte[] assemblyBytes = File.ReadAllBytes(file.FullName); Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes)); - AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true })); + { + FileInfo symbolsFile = new FileInfo(Path.Combine(Path.GetDirectoryName(file.FullName)!, Path.GetFileNameWithoutExtension(file.FullName)) + ".pdb"); + if (symbolsFile.Exists) + this.SymbolReaderProvider.TryAddSymbolData(file.Name, () => this.TrackForDisposal(symbolsFile.OpenRead())); + } + AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true, ReadSymbols = true, SymbolReaderProvider = this.SymbolReaderProvider })); // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs new file mode 100644 index 00000000..2171895d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs @@ -0,0 +1,72 @@ +using System.IO; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Pdb; + +namespace StardewModdingAPI.Framework.ModLoading.Symbols +{ + /// <summary>Reads symbol data for an assembly.</summary> + internal class SymbolReader : ISymbolReader + { + /********* + ** Fields + *********/ + /// <summary>The module for which to read symbols.</summary> + private readonly ModuleDefinition Module; + + /// <summary>The symbol file stream.</summary> + private readonly Stream Stream; + + /// <summary>The underlying symbol reader.</summary> + private ISymbolReader Reader; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="module">The module for which to read symbols.</param> + /// <param name="stream">The symbol file stream.</param> + public SymbolReader(ModuleDefinition module, Stream stream) + { + this.Module = module; + this.Stream = stream; + this.Reader = new NativePdbReaderProvider().GetSymbolReader(module, stream); + } + + /// <summary>Get the symbol writer provider for the assembly.</summary> + public ISymbolWriterProvider GetWriterProvider() + { + return new PortablePdbWriterProvider(); + } + + /// <summary>Process a debug header in the symbol file.</summary> + /// <param name="header">The debug header.</param> + public bool ProcessDebugHeader(ImageDebugHeader header) + { + try + { + return this.Reader.ProcessDebugHeader(header); + } + catch + { + this.Reader.Dispose(); + this.Reader = new PortablePdbReaderProvider().GetSymbolReader(this.Module, this.Stream); + return this.Reader.ProcessDebugHeader(header); + } + } + + /// <summary>Read the method debug information for a method in the assembly.</summary> + /// <param name="method">The method definition.</param> + public MethodDebugInformation Read(MethodDefinition method) + { + return this.Reader.Read(method); + } + + /// <inheritdoc /> + public void Dispose() + { + this.Reader.Dispose(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs new file mode 100644 index 00000000..44074337 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Symbols +{ + /// <summary>Provides assembly symbol readers for Mono.Cecil.</summary> + internal class SymbolReaderProvider : ISymbolReaderProvider + { + /********* + ** Fields + *********/ + /// <summary>The underlying symbol reader provider.</summary> + private readonly ISymbolReaderProvider BaseProvider = new DefaultSymbolReaderProvider(throwIfNoSymbol: false); + + /// <summary>The symbol data loaded by absolute assembly path.</summary> + private readonly Dictionary<string, Stream> SymbolsByAssemblyPath = new Dictionary<string, Stream>(StringComparer.OrdinalIgnoreCase); + + + /********* + ** Public methods + *********/ + /// <summary>Add the symbol file for a given assembly name, if it's not already registered.</summary> + /// <param name="fileName">The assembly file name.</param> + /// <param name="getSymbolStream">Get the raw file stream for the symbols.</param> + public void TryAddSymbolData(string fileName, Func<Stream> getSymbolStream) + { + if (!this.SymbolsByAssemblyPath.ContainsKey(fileName)) + this.SymbolsByAssemblyPath.Add(fileName, getSymbolStream()); + } + + /// <summary>Get a symbol reader for a given module and assembly name.</summary> + /// <param name="module">The loaded assembly module.</param> + /// <param name="fileName">The assembly file name.</param> + public ISymbolReader GetSymbolReader(ModuleDefinition module, string fileName) + { + return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream symbolData) + ? new SymbolReader(module, symbolData) + : this.BaseProvider.GetSymbolReader(module, fileName); + } + + /// <summary>Get a symbol reader for a given module and symbol stream.</summary> + /// <param name="module">The loaded assembly module.</param> + /// <param name="symbolStream">The loaded symbol file stream.</param> + public ISymbolReader GetSymbolReader(ModuleDefinition module, Stream symbolStream) + { + return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream symbolData) + ? new SymbolReader(module, symbolData) + : this.BaseProvider.GetSymbolReader(module, symbolStream); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs new file mode 100644 index 00000000..8f7e05d1 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs @@ -0,0 +1,40 @@ +using System.IO; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Symbols +{ + /// <summary>Provides assembly symbol writers for Mono.Cecil.</summary> + internal class SymbolWriterProvider : ISymbolWriterProvider + { + /********* + ** Fields + *********/ + /// <summary>The default symbol writer provider.</summary> + private readonly ISymbolWriterProvider DefaultProvider = new DefaultSymbolWriterProvider(); + + /// <summary>The symbol writer provider for the portable PDB format.</summary> + private readonly ISymbolWriterProvider PortablePdbProvider = new PortablePdbWriterProvider(); + + + /********* + ** Public methods + *********/ + /// <summary>Get a symbol writer for a given module and assembly path.</summary> + /// <param name="module">The loaded assembly module.</param> + /// <param name="fileName">The assembly name.</param> + public ISymbolWriter GetSymbolWriter(ModuleDefinition module, string fileName) + { + return this.DefaultProvider.GetSymbolWriter(module, fileName); + } + + /// <summary>Get a symbol writer for a given module and symbol stream.</summary> + /// <param name="module">The loaded assembly module.</param> + /// <param name="symbolStream">The loaded symbol file stream.</param> + public ISymbolWriter GetSymbolWriter(ModuleDefinition module, Stream symbolStream) + { + // Not implemented in default native pdb writer, so fallback to portable + return this.PortablePdbProvider.GetSymbolWriter(module, symbolStream); + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5fb4aa03..dff0fc55 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1704,12 +1704,21 @@ namespace StardewModdingAPI.Framework // load as mod else { + // get mod info IManifest manifest = mod.Manifest; + string assemblyPath = Path.Combine(mod.DirectoryPath, manifest.EntryDll); + + // assert 64-bit +#if SMAPI_FOR_WINDOWS_64BIT_HACK + if (!EnvironmentUtility.Is64BitAssembly(assemblyPath)) + { + errorReasonPhrase = "it needs to be updated for 64-bit mode."; + failReason = ModFailReason.LoadFailed; + return false; + } +#endif // load mod - string assemblyPath = manifest?.EntryDll != null - ? Path.Combine(mod.DirectoryPath, manifest.EntryDll) - : null; Assembly modAssembly; try { diff --git a/src/SMAPI/Framework/Translator.cs b/src/SMAPI/Framework/Translator.cs index 11ec983b..4492b17f 100644 --- a/src/SMAPI/Framework/Translator.cs +++ b/src/SMAPI/Framework/Translator.cs @@ -46,18 +46,10 @@ namespace StardewModdingAPI.Framework this.LocaleEnum = localeEnum; this.ForLocale = new Dictionary<string, Translation>(StringComparer.OrdinalIgnoreCase); - foreach (string next in this.GetRelevantLocales(this.Locale)) + foreach (string key in this.GetAllKeysRaw()) { - // skip if locale not defined - if (!this.All.TryGetValue(next, out IDictionary<string, string> translations)) - continue; - - // add missing translations - foreach (var pair in translations) - { - if (!this.ForLocale.ContainsKey(pair.Key)) - this.ForLocale.Add(pair.Key, new Translation(this.Locale, pair.Key, pair.Value)); - } + string text = this.GetRaw(key, locale, withFallback: true); + this.ForLocale.Add(key, new Translation(this.Locale, key, text)); } } @@ -83,6 +75,25 @@ namespace StardewModdingAPI.Framework return this.Get(key).Tokens(tokens); } + /// <summary>Get a translation in every locale for which it's defined.</summary> + /// <param name="key">The translation key.</param> + /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param> + public IDictionary<string, Translation> GetInAllLocales(string key, bool withFallback) + { + IDictionary<string, Translation> translations = new Dictionary<string, Translation>(); + + foreach (var localeSet in this.All) + { + string locale = localeSet.Key; + string text = this.GetRaw(key, locale, withFallback); + + if (text != null) + translations[locale] = new Translation(locale, key, text); + } + + return translations; + } + /// <summary>Set the translations to use.</summary> /// <param name="translations">The translations to use.</param> internal Translator SetTranslations(IDictionary<string, IDictionary<string, string>> translations) @@ -102,6 +113,38 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// <summary>Get all translation keys in the underlying translation data, ignoring the <see cref="ForLocale"/> cache.</summary> + private IEnumerable<string> GetAllKeysRaw() + { + return new HashSet<string>( + this.All.SelectMany(p => p.Value.Keys), + StringComparer.OrdinalIgnoreCase + ); + } + + /// <summary>Get a translation from the underlying translation data, ignoring the <see cref="ForLocale"/> cache.</summary> + /// <param name="key">The translation key.</param> + /// <param name="locale">The locale to get.</param> + /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param> + private string GetRaw(string key, string locale, bool withFallback) + { + foreach (string next in this.GetRelevantLocales(locale)) + { + string translation = null; + bool hasTranslation = + this.All.TryGetValue(next, out IDictionary<string, string> translations) + && translations.TryGetValue(key, out translation); + + if (hasTranslation) + return translation; + + if (!withFallback) + break; + } + + return null; + } + /// <summary>Get the locales which can provide translations for the given locale, in precedence order.</summary> /// <param name="locale">The locale for which to find valid locales.</param> private IEnumerable<string> GetRelevantLocales(string locale) diff --git a/src/SMAPI/IDataHelper.cs b/src/SMAPI/IDataHelper.cs index 252030bd..901266d7 100644 --- a/src/SMAPI/IDataHelper.cs +++ b/src/SMAPI/IDataHelper.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI /// <summary>Save data to a JSON file in the mod'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="data">The arbitrary data to save.</param> + /// <param name="data">The arbitrary data to save, or <c>null</c> to delete the file.</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; @@ -38,7 +38,7 @@ namespace StardewModdingAPI /// <summary>Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day.</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="key">The unique key identifying the data.</param> - /// <param name="data">The arbitrary data to save.</param> + /// <param name="data">The arbitrary data to save, or <c>null</c> to remove the entry.</param> /// <exception cref="InvalidOperationException">The player hasn't loaded a save file yet or isn't the main player.</exception> void WriteSaveData<TModel>(string key, TModel data) where TModel : class; @@ -55,7 +55,7 @@ namespace StardewModdingAPI /// <summary>Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable.</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="key">The unique key identifying the data.</param> - /// <param name="data">The arbitrary data to save.</param> + /// <param name="data">The arbitrary data to save, or <c>null</c> to delete the file.</param> void WriteGlobalData<TModel>(string key, TModel data) where TModel : class; } } diff --git a/src/SMAPI/ITranslationHelper.cs b/src/SMAPI/ITranslationHelper.cs index c4b72444..b30d9b14 100644 --- a/src/SMAPI/ITranslationHelper.cs +++ b/src/SMAPI/ITranslationHelper.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using StardewValley; namespace StardewModdingAPI @@ -30,5 +30,10 @@ namespace StardewModdingAPI /// <param name="key">The translation key.</param> /// <param name="tokens">An object containing token key/value pairs. This can be an anonymous object (like <c>new { value = 42, name = "Cranberries" }</c>), a dictionary, or a class instance.</param> Translation Get(string key, object tokens); + + /// <summary>Get a translation in every locale for which it's defined.</summary> + /// <param name="key">The translation key.</param> + /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param> + IDictionary<string, Translation> GetInAllLocales(string key, bool withFallback = false); } } |