diff options
Diffstat (limited to 'src')
28 files changed, 425 insertions, 266 deletions
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 1257f12b..b3bba883 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -41,6 +41,7 @@ namespace StardewModdingApi.Installer // current files yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only + yield return GetInstallPath("StardewModdingAPI.deps.json"); yield return GetInstallPath("StardewModdingAPI.dll"); yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.exe.config"); diff --git a/src/SMAPI.Installer/assets/unix-launcher.sh b/src/SMAPI.Installer/assets/unix-launcher.sh index e8b9ae62..47937f95 100644 --- a/src/SMAPI.Installer/assets/unix-launcher.sh +++ b/src/SMAPI.Installer/assets/unix-launcher.sh @@ -6,6 +6,10 @@ # move to script's directory cd "$(dirname "$0")" || exit $? +# change to true to skip opening a terminal +# This isn't recommended since you won't see errors, warnings, and update alerts. +SKIP_TERMINAL=false + ########## ## Open terminal if needed @@ -16,7 +20,6 @@ cd "$(dirname "$0")" || exit $? if [ "$(uname)" == "Darwin" ]; then if [ ! -t 1 ]; then # https://stackoverflow.com/q/911168/262123 # sanity check to make sure we don't have an infinite loop of opening windows - SKIP_TERMINAL=false for argument in "$@"; do if [ "$argument" == "--no-reopen-terminal" ]; then SKIP_TERMINAL=true @@ -28,7 +31,8 @@ if [ "$(uname)" == "Darwin" ]; then # https://stackoverflow.com/a/29511052/262123 if [ "$SKIP_TERMINAL" == "false" ]; then echo "Reopening in the Terminal app..." - echo "\"$0\" $@ --no-reopen-terminal" > /tmp/open-smapi-terminal.sh + echo '#!/bin/sh' > /tmp/open-smapi-terminal.sh + echo "\"$0\" $@ --no-reopen-terminal" >> /tmp/open-smapi-terminal.sh chmod +x /tmp/open-smapi-terminal.sh cat /tmp/open-smapi-terminal.sh open -W -a Terminal /tmp/open-smapi-terminal.sh @@ -63,64 +67,71 @@ else LAUNCH_FILE="./StardewModdingAPI" export LAUNCH_FILE - # select terminal (prefer xterm for best compatibility, then known supported terminals) - for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do - if command -v "$terminal" 2>/dev/null; then - export TERMINAL_NAME=$terminal - break; + # run in terminal + if [ "$SKIP_TERMINAL" == "false" ]; then + # select terminal (prefer xterm for best compatibility, then known supported terminals) + for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do + if command -v "$terminal" 2>/dev/null; then + export TERMINAL_NAME=$terminal + break; + fi + done + + # find the true shell behind x-terminal-emulator + if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then + export TERMINAL_NAME="$(basename "$(readlink -f $(command -v x-terminal-emulator))")" fi - done - # find the true shell behind x-terminal-emulator - if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then - export TERMINAL_NAME="$(basename "$(readlink -f $(command -v x-terminal-emulator))")" - fi + # run in selected terminal and account for quirks + export TERMINAL_PATH="$(command -v $TERMINAL_NAME)" + if [ -x $TERMINAL_PATH ]; then + case $TERMINAL_NAME in + terminal|termite) + # consumes only one argument after -e + # options containing space characters are unsupported + exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@" + ;; + + xterm|konsole|alacritty) + # consumes all arguments after -e + exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@" + ;; + + terminator|xfce4-terminal|mate-terminal) + # consumes all arguments after -x + exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@" + ;; + + gnome-terminal) + # consumes all arguments after -- + exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@" + ;; + + kitty) + # consumes all trailing arguments + exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@" + ;; + + *) + # If we don't know the terminal, just try to run it in the current shell. + # If THAT fails, launch with no output. + env TERM=xterm $LAUNCH_FILE "$@" + if [ $? -eq 127 ]; then + exec $LAUNCH_FILE --no-terminal "$@" + fi + esac + + ## terminal isn't executable; fallback to current shell or no terminal + else + echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell." + env TERM=xterm $LAUNCH_FILE "$@" + if [ $? -eq 127 ]; then + exec $LAUNCH_FILE --no-terminal "$@" + fi + fi - # run in selected terminal and account for quirks - export TERMINAL_PATH="$(command -v $TERMINAL_NAME)" - if [ -x $TERMINAL_PATH ]; then - case $TERMINAL_NAME in - terminal|termite) - # consumes only one argument after -e - # options containing space characters are unsupported - exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@" - ;; - - xterm|konsole|alacritty) - # consumes all arguments after -e - exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@" - ;; - - terminator|xfce4-terminal|mate-terminal) - # consumes all arguments after -x - exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@" - ;; - - gnome-terminal) - # consumes all arguments after -- - exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@" - ;; - - kitty) - # consumes all trailing arguments - exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@" - ;; - - *) - # If we don't know the terminal, just try to run it in the current shell. - # If THAT fails, launch with no output. - env TERM=xterm $LAUNCH_FILE "$@" - if [ $? -eq 127 ]; then - exec $LAUNCH_FILE --no-terminal "$@" - fi - esac - - ## terminal isn't executable; fallback to current shell or no terminal + # explicitly run without terminal else - echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell." - env TERM=xterm $LAUNCH_FILE "$@" - if [ $? -eq 127 ]; then - exec $LAUNCH_FILE --no-terminal "$@" - fi + exec $LAUNCH_FILE --no-terminal "$@" fi fi diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 216a4c32..97e1c243 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.13.2", + "Version": "3.13.3", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.13.2" + "MinimumApiVersion": "3.13.3" } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs index 2a43cb10..0a7ed212 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs @@ -4,11 +4,11 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.Patching; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { @@ -47,6 +47,11 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches original: this.RequireMethod<SaveGame>(nameof(SaveGame.loadDataToLocations)), prefix: this.GetHarmonyMethod(nameof(SaveGamePatcher.Before_LoadDataToLocations)) ); + + harmony.Patch( + original: this.RequireMethod<SaveGame>(nameof(SaveGame.LoadFarmType)), + finalizer: this.GetHarmonyMethod(nameof(SaveGamePatcher.Finalize_LoadFarmType)) + ); } @@ -58,14 +63,35 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /// <returns>Returns whether to execute the original method.</returns> private static bool Before_LoadDataToLocations(List<GameLocation> gamelocations) { + // missing locations/NPCs IDictionary<string, string> npcs = Game1.content.Load<Dictionary<string, string>>("Data\\NPCDispositions"); - if (SaveGamePatcher.RemoveBrokenContent(gamelocations, npcs)) SaveGamePatcher.OnContentRemoved(); return true; } + /// <summary>The method to call after <see cref="SaveGame.LoadFarmType"/> throws an exception.</summary> + /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> + /// <returns>Returns the exception to throw, if any.</returns> + private static Exception Finalize_LoadFarmType(Exception __exception) + { + // missing custom farm type + if (__exception?.Message?.Contains("not a valid farm type") == true && !int.TryParse(SaveGame.loaded.whichFarm, out _)) + { + SaveGamePatcher.Monitor.Log(__exception.GetLogSummary(), LogLevel.Error); + SaveGamePatcher.Monitor.Log($"Removed invalid custom farm type '{SaveGame.loaded.whichFarm}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom farm type mod?)", LogLevel.Warn); + + SaveGame.loaded.whichFarm = Farm.default_layout.ToString(); + SaveGame.LoadFarmType(); + SaveGamePatcher.OnContentRemoved(); + + __exception = null; + } + + return __exception; + } + /// <summary>Remove content which no longer exists in the game data.</summary> /// <param name="locations">The current game locations.</param> /// <param name="npcs">The NPC data.</param> diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/uk.json b/src/SMAPI.Mods.ErrorHandler/i18n/uk.json new file mode 100644 index 00000000..a58102ab --- /dev/null +++ b/src/SMAPI.Mods.ErrorHandler/i18n/uk.json @@ -0,0 +1,4 @@ +{ + // warning messages + "warn.invalid-content-removed": "Недійсний вміст видалено, щоб запобігти аварійному завершенню роботи (Додаткову інформацію див. на консолі SMAPI)." +} diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index beb52020..c4246721 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.13.2", + "Version": "3.13.3", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.13.2" + "MinimumApiVersion": "3.13.3" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 2bd20a63..b1b946c8 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.13.2", + "Version": "3.13.3", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.13.2" + "MinimumApiVersion": "3.13.3" } diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 37e4f263..8d4198de 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -157,7 +157,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); #endif - // default paths + // default GOG/Steam paths foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" }) { yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley"; @@ -165,6 +165,15 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning yield return $@"{programFiles}\GOG Games\Stardew Valley"; yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley"; } + + // default Xbox app paths + // The Xbox app saves the install path to the registry, but we can't use it + // here since it saves the internal readonly path (like C:\Program Files\WindowsApps\Mutable\<package ID>) + // instead of the mods-enabled path(like C:\Program Files\ModifiableWindowsApps\Stardew Valley). + // Fortunately we can cheat a bit: players can customize the install drive, but they can't + // change the install path on the drive. + for (char driveLetter = 'C'; driveLetter <= 'H'; driveLetter++) + yield return $@"{driveLetter}:\Program Files\ModifiableWindowsApps\Stardew Valley"; } break; diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 39de4b5d..db53d942 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using StardewModdingAPI.Toolkit.Utilities; @@ -39,24 +40,42 @@ 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="format">How to render the log view.</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, bool renew = false) + public async Task<ActionResult> Index(string id = null, LogViewFormat format = LogViewFormat.Default, bool renew = false) { // fresh page if (string.IsNullOrWhiteSpace(id)) return this.View("Index", this.GetModel(id)); - // log page + // fetch log StoredFileInfo file = await this.Storage.GetAsync(id, renew); - ParsedLog log = file.Success - ? new LogParser().Parse(file.Content) - : new ParsedLog { IsValid = false, Error = file.Error }; - return this.View("Index", this.GetModel(id, uploadWarning: file.Warning, expiry: file.Expiry).SetResult(log, raw)); + // render view + switch (format) + { + case LogViewFormat.Default: + case LogViewFormat.RawView: + { + ParsedLog log = file.Success + ? new LogParser().Parse(file.Content) + : new ParsedLog { IsValid = false, Error = file.Error }; + + return this.View("Index", this.GetModel(id, uploadWarning: file.Warning, expiry: file.Expiry).SetResult(log, showRaw: format == LogViewFormat.RawView)); + } + + case LogViewFormat.RawDownload: + { + string content = file.Error ?? file.Content; + return this.File(Encoding.UTF8.GetBytes(content), "plain/text", $"SMAPI log ({id}).txt"); + } + + default: + throw new InvalidOperationException($"Unknown log view format '{format}'."); + } } /*** diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 84013ccc..887d0105 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -79,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing // parse log messages LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "", Loaded = true }; LogModInfo gameMod = new LogModInfo { Name = "game", Author = "", Description = "", Loaded = true }; - IDictionary<string, LogModInfo> mods = new Dictionary<string, LogModInfo>(); + IDictionary<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>(); bool inModList = false; bool inContentPackList = false; bool inModUpdateList = false; @@ -99,8 +99,11 @@ namespace StardewModdingAPI.Web.Framework.LogParsing break; default: - if (mods.ContainsKey(message.Mod)) - mods[message.Mod].Errors++; + if (mods.TryGetValue(message.Mod, out var entries)) + { + foreach (var entry in entries) + entry.Errors++; + } break; } } @@ -127,7 +130,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing string version = match.Groups["version"].Value; string author = match.Groups["author"].Value; string description = match.Groups["description"].Value; - mods[name] = new LogModInfo { Name = name, Author = author, Version = version, Description = description, Loaded = true }; + + if (!mods.TryGetValue(name, out List<LogModInfo> entries)) + mods[name] = entries = new List<LogModInfo>(); + entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, Loaded = true }); message.Section = LogSection.ModsList; } @@ -147,7 +153,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing string author = match.Groups["author"].Value; string description = match.Groups["description"].Value; string forMod = match.Groups["for"].Value; - mods[name] = new LogModInfo { Name = name, Author = author, Version = version, Description = description, ContentPackFor = forMod, Loaded = true }; + + if (!mods.TryGetValue(name, out List<LogModInfo> entries)) + mods[name] = entries = new List<LogModInfo>(); + entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, ContentPackFor = forMod, Loaded = true }); message.Section = LogSection.ContentPackList; } @@ -165,14 +174,14 @@ namespace StardewModdingAPI.Web.Framework.LogParsing string name = match.Groups["name"].Value; string version = match.Groups["version"].Value; string link = match.Groups["link"].Value; - if (mods.ContainsKey(name)) - { - mods[name].UpdateLink = link; - mods[name].UpdateVersion = version; - } - else + + if (mods.TryGetValue(name, out var entries)) { - mods[name] = new LogModInfo { Name = name, UpdateVersion = version, UpdateLink = link, Loaded = false }; + foreach (var entry in entries) + { + entry.UpdateLink = link; + entry.UpdateVersion = version; + } } message.Section = LogSection.ModUpdateList; @@ -219,7 +228,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing // finalize log gameMod.Version = log.GameVersion; - log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray(); + log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.SelectMany(p => p).OrderBy(p => p.Name)).ToArray(); return log; } catch (LogParseException ex) diff --git a/src/SMAPI.Web/ViewModels/LogViewFormat.cs b/src/SMAPI.Web/ViewModels/LogViewFormat.cs new file mode 100644 index 00000000..7ef79319 --- /dev/null +++ b/src/SMAPI.Web/ViewModels/LogViewFormat.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.ViewModels +{ + /// <summary>How a log file should be displayed.</summary> + public enum LogViewFormat + { + /// <summary>Render a parsed log and metadata.</summary> + Default, + + /// <summary>Render a raw log with parsed metadata.</summary> + RawView, + + /// <summary>Render directly as a text file.</summary> + RawDownload + } +} diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 06d46c9e..91fc3535 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -2,6 +2,7 @@ @using StardewModdingAPI.Toolkit.Utilities @using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.LogParsing.Models +@using StardewModdingAPI.Web.ViewModels @model StardewModdingAPI.Web.ViewModels.LogParserModel @{ @@ -22,13 +23,15 @@ { <meta name="robots" content="noindex" /> } - <link rel="stylesheet" href="~/Content/css/file-upload.css?r=202002" /> - <link rel="stylesheet" href="~/Content/css/log-parser.css?r=202002" /> + <link rel="stylesheet" href="~/Content/css/file-upload.css" /> + <link rel="stylesheet" href="~/Content/css/log-parser.css" /> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3/dist/css/tabby-ui-vertical.min.css" /> + <script src="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script> - <script src="~/Content/js/file-upload.js?r=202002"></script> - <script src="~/Content/js/log-parser.js?r=202002"></script> + <script src="~/Content/js/file-upload.js"></script> + <script src="~/Content/js/log-parser.js"></script> <script> $(function() { smapi.logParser({ @@ -40,6 +43,8 @@ enableFilters: @this.ForJson(!Model.ShowRaw), screenIds: @this.ForJson(screenIds) }, '@this.Url.PlainAction("Index", "LogParser", values: null)'); + + new Tabby('[data-tabs]'); }); </script> } @@ -90,51 +95,65 @@ else if (Model.ParsedLog?.IsValid == true) @if (Model.ParsedLog == null) { <h2>Where do I find my SMAPI log?</h2> - <div>What system do you use?</div> - <ul id="os-list"> - @foreach (Platform platform in new[] { Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows }) - { - <li> - <input type="radio" name="os" value="@platform" id="os-@platform" checked="@(Model.DetectedPlatform == platform)" /> - <label for="os-@platform">@platform</label> - </li> - } - </ul> - <div data-os="@Platform.Android"> - On Android: - <ol> - <li>Open a file app (like My Files or MT Manager).</li> - <li>Find the <code>StardewValley</code> folder on your internal storage.</li> - <li>Open the <code>ErrorLogs</code> subfolder.</li> - <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> - </ol> - </div> - <div data-os="@Platform.Linux"> - On Linux: - <ol> - <li>Open the Files app.</li> - <li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li> - <li>Choose <em>Enter Location</em>.</li> - <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li> - <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> - </ol> - </div> - <div data-os="@Platform.Mac"> - On macOS: - <ol> - <li>Open the Finder app.</li> - <li>Click <em>Go</em> at the top, then <em>Go to Folder</em>.</li> - <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li> - <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> - </ol> - </div> - <div data-os="@Platform.Windows"> - On Windows: - <ol> - <li>Press the <code>Windows</code> and <code>R</code> buttons at the same time.</li> - <li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li> - <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> - </ol> + <div id="os-instructions"> + <div> + <ul data-tabs> + @foreach (Platform platform in new[] {Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows}) + { + @if (platform == Platform.Windows) + { + <li><a data-tabby-default href="#@(platform)-steamgog">@platform (Steam or GOG)</a></li> + <li><a href="#@(platform)-xbox">@platform (Xbox app)</a></li> + } + else + { + <li><a href="#@platform">@platform</a></li> + } + } + </ul> + </div> + <div> + <div id="@Platform.Android"> + <ol> + <li>Open a file app (like My Files or MT Manager).</li> + <li>Find the <code>StardewValley</code> folder on your internal storage.</li> + <li>Open the <code>ErrorLogs</code> subfolder.</li> + <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> + </ol> + </div> + <div id="@Platform.Linux"> + <ol> + <li>Open the Files app.</li> + <li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li> + <li>Choose <em>Enter Location</em>.</li> + <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li> + <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> + </ol> + </div> + <div id="@Platform.Mac"> + <ol> + <li>Open the Finder app.</li> + <li>Click <em>Go</em> at the top, then <em>Go to Folder</em>.</li> + <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li> + <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> + </ol> + </div> + <div id="@(Platform.Windows)-steamgog"> + <ol> + <li>Press the <kbd>Windows</kbd> and <kbd>R</kbd> buttons at the same time.</li> + <li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li> + <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> + </ol> + </div> + <div id="@(Platform.Windows)-xbox"> + <ol> + <li>Press the <kbd>Windows</kbd> |
