@using Humanizer @using StardewModdingAPI.Toolkit.Utilities @using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.LogParsing.Models @using StardewModdingAPI.Web.ViewModels @model StardewModdingAPI.Web.ViewModels.LogParserModel @{ ViewData["Title"] = "SMAPI log parser"; ParsedLog? log = Model!.ParsedLog; IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod(); IDictionary<string, bool> defaultFilters = Enum .GetValues<LogLevel>() .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); IDictionary<int, string> logLevels = Enum .GetValues<LogLevel>() .ToDictionary(level => (int)level, level => level.ToString().ToLower()); IDictionary<int, string> logSections = Enum .GetValues<LogSection>() .ToDictionary(section => (int)section, section => section.ToString()); string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true)!; ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>()); } @section Head { @if (Model.PasteID != null) { <meta name="robots" content="noindex" /> } <link rel="stylesheet" href="~/Content/css/file-upload.css" /> <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20220409" /> <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.14" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script> <script src="~/Content/js/file-upload.js"></script> <script src="~/Content/js/log-parser.js?r=20220409"></script> <script id="serializedData" type="application/json"> @if (!Model.ShowRaw) { <text> { "messages": @this.ForJson(log?.Messages), "sections": @this.ForJson(logSections), "logLevels": @this.ForJson(logLevels), "modSlugs": @this.ForJson(log?.Mods.DistinctBy(p => p.Name).Select(p => new {p.Name, Slug = Model.GetSlug(p.Name)}).Where(p => p.Name != p.Slug).ToDictionary(p => p.Name, p => p.Slug)), "screenIds": @this.ForJson(screenIds) } </text> } else { <text> { "messages": [], "sections": {}, "logLevels": {}, "modSlugs": {}, "screenIds": [] } </text> } </script> <script> $(function() { smapi.logParser( { logStarted: new Date(@this.ForJson(log?.Timestamp)), dataElement: "script#serializedData", showPopup: @this.ForJson(log == null), showMods: @this.ForJson(log?.Mods.Where(p => p.Loaded && !p.IsContentPack).Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, _ => true)), showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, _ => false)), showLevels: @this.ForJson(defaultFilters), enableFilters: @this.ForJson(!Model.ShowRaw) } ); @if (log == null) { <text> new Tabby("[data-tabs]"); </text> } }); </script> } @* quick navigation links *@ @section SidebarExtra { @if (log != null) { <nav id="quickNav"> <h4>Scroll to...</h4> <ul> <li><a href="#content">Top</a></li> <li><a href="#filterHolder">Log start</a></li> <li><a href="#footer">Bottom</a></li> </ul> </nav> } } @* upload result banner *@ @if (Model.UploadError != null) { <div class="banner error" v-pre> <strong>Oops, the server ran into trouble saving that file.</strong><br /> <small v-pre>Error details: @Model.UploadError</small> </div> } else if (Model.ParseError != null) { <div class="banner error" v-pre> <strong>Oops, couldn't parse that log. (Make sure you upload the log file, not the console text.)</strong><br /> Share this URL when asking for help: <code>@curPageUrl</code><br /> (Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.)<br /> <br /> <small v-pre>Error details: @Model.ParseError</small> </div> } else if (log?.IsValid == true) { <div class="banner success" v-pre> <strong>Share this link to let someone else see the log:</strong> <code>@curPageUrl</code><br /> (Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.) </div> } @* save warnings *@ @if (Model.UploadWarning != null || Model.Expiry != null) { @if (Model.UploadWarning != null) { <text>⚠️ @Model.UploadWarning<br /></text> } <div class="save-metadata" v-pre> @if (Model.Expiry != null) { <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> } @* upload new log *@ @if (log == null) { <h2>Where do I find my SMAPI log?</h2> <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> and <kbd>R</kbd> buttons at the same time.</li> <li>In the 'run' box that appears, enter this exact text: <pre>%localappdata%\Packages\ConcernedApe.StardewValleyPC_0c8vynj4cqe4e\LocalCache\Roaming\StardewValley\ErrorLogs</pre></li> <li>If you get an error with the title "Location is not available", try the "with Steam or GOG" instructions above.</li> <li>Otherwise the log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li> </ol> </div> </div> </div> <h2>How do I share my log?</h2> <form action="@this.Url.PlainAction("Post", "LogParser")" method="post"> <input id="inputFile" type="file" /> <ol> <li> Drag the file onto this textbox <small>(or <a href="#" id="choose-file-link">choose a file</a>)</small>:<br /> <textarea id="input" name="input" placeholder="paste log here"></textarea> </li> <li> Click this button:<br /> <input type="submit" id="submit" value="save & parse log" /> </li> <li>On the new page, copy the URL and send it to the person helping you.</li> </ol> </form> } @* parsed log *@ @if (log?.IsValid == true) { <div id="output"> @if (log.Mods.Any(mod => mod.HasUpdate)) { <h2>Suggested fixes</h2> <ul id="fix-list"> <li> Consider updating these mods to fix problems: <table id="updates" class="table"> @foreach (LogModInfo mod in log.Mods.Where(mod => (mod.HasUpdate && !mod.IsContentPack) || (contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) && contentPackList.Any(pack => pack.HasUpdate)))) { <tr class="mod-entry"> <td> <strong class=@(!mod.HasUpdate ? "hidden" : "")>@mod.Name</strong> @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList)) { <div class="content-packs"> @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate)) { <text>+ @contentPack.Name</text><br /> } </div> } </td> <td> @if (mod.HasUpdate) { <a href="@mod.UpdateLink" target="_blank"> @(mod.Version == null ? mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}") </a> } else { <text> </text> } @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList)) { <div> @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate)) { <a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br /> } </div> } </td> </tr> } </table> </li> </ul> } <h2>Log info</h2> <table id="metadata" class="table" data-code-mods="@log.Mods.Count(p => !p.IsContentPack)" data-content-packs="@log.Mods.Count(p => p.IsContentPack)" data-os="@log.OperatingSystem" data-game-version="@log.GameVersion" data-game-path="@log.GamePath" data-smapi-version="@log.ApiVersion" data-log-started="@log.Timestamp.UtcDateTime.ToString("O")" > <caption>Game info:</caption> <tr> <th>Stardew Valley:</th> <td v-pre>@log.GameVersion on @log.OperatingSystem</td> </tr> <tr> <th>SMAPI:</th> <td v-pre>@log.ApiVersion</td> </tr> <tr> <th>Folder:</th> <td v-pre>@log.GamePath</td> </tr> <tr> <th>Log started:</th> <td>@log.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time)</td> </tr> </table> <br /> <table id="mods" class="@(Model.ShowRaw ? "filters-disabled" : null) table"> <caption> Installed mods: @if (!Model.ShowRaw) { <span class="notice txt"><i>click any mod to filter</i></span> <span class="notice btn txt" v-on:click="showAllMods" v-bind:class="{ invisible: !anyModsHidden }">show all</span> <span class="notice btn txt" v-on:click="hideAllMods" v-bind:class="{ invisible: !anyModsShown || !anyModsHidden }">hide all</span> <span class="notice btn txt" v-on:click="toggleContentPacks">toggle content packs in list</span> } </caption> @foreach (var mod in log.Mods.Where(p => p.Loaded && !p.IsContentPack)) { if (contentPacks == null || !contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList)) contentPackList = null; <tr v-on:click="toggleMod('@Model.GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@Model.GetSlug(mod.Name)'] }"> <td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-bind:class="{ invisible: !anyModsHidden }" /></td> <td> <strong v-pre>@mod.Name</strong> @mod.Version @if (contentPackList != null) { <div v-if="!hideContentPacks" class="content-packs"> @foreach (var contentPack in contentPackList) { <text>+ @contentPack.Name @contentPack.Version</text><br /> } </div> <span v-else class="content-packs-collapsed"> (+ @contentPackList.Length content packs)</span> } </td> <td> @mod.Author @if (contentPackList != null) { <div v-if="!hideContentPacks" class="content-packs"> @foreach (var contentPack in contentPackList) { <text>+ @contentPack.Author</text><br /> } </div> } </td> @if (mod.Errors == 0) { <td v-pre class="color-green">no errors</td> } else if (mod.Errors == 1) { <td v-pre class="color-red">@mod.Errors error</td> } else { <td v-pre class="color-red">@mod.Errors errors</td> } </tr> } </table> @if (!Model.ShowRaw) { <div id="filterHolder"></div> <div id="filters"> <div class="toggles"> <div> Filter messages: </div> <div> <span role="button" v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> | <span role="button" v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> | <span role="button" v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> | <span role="button" v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> | <span role="button" v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> | <span role="button" v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span> <div class="filter-text"> <input type="text" v-bind:class="{ active: !!filterText }" v-model="filterText" v-on:input="updateFilterText" placeholder="search to filter log..." /> <span role="button" v-bind:class="{ active: filterUseRegex }" v-on:click="toggleFilterUseRegex" title="Use regular expression syntax.">.*</span> <span role="button" v-bind:class="{ active: !filterInsensitive }" v-on:click="toggleFilterInsensitive" title="Match exact capitalization only.">aA</span> <span role="button" v-bind:class="{ active: filterUseWord, 'whole-word': true }" v-on:click="toggleFilterWord" title="Match whole word only."><i>“ ”</i></span> <span role="button" v-bind:class="{ active: shouldHighlight }" v-on:click="toggleHighlight" title="Highlight matches in the log text.">HL</span> <div v-if="filterError" class="filter-error" > {{ filterError }} </div> </div> <filter-stats v-bind:start="start" v-bind:end="end" v-bind:pages="totalPages" v-bind:filtered="filteredMessages.total" v-bind:total="totalMessages" /> </div> </div> <pager v-bind:page="page" v-bind:pages="totalPages" v-bind:prevPage="prevPage" v-bind:nextPage="nextPage" /> </div> <noscript> <div> This website uses JavaScript to display a filterable table. To view this log, please enable JavaScript or <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, format = LogViewFormat.RawView })">view the raw log</a>. </div> <br/> </noscript> <log-table> <log-line v-for="msg in visibleMessages" v-bind:key="msg.id" v-bind:showScreenId="showScreenId" v-bind:message="msg" v-bind:highlight="shouldHighlight" /> </log-table> } else { <pre v-pre>@log.RawText</pre> } <small> @if (Model.ShowRaw) { <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })">view parsed log</a> } else { <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, format = LogViewFormat.RawView })">view raw log</a> } | <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, format = LogViewFormat.RawDownload })" download>download</a> </small> </div> } else if (log?.IsValid == false) { <h3>Raw log</h3> <pre v-pre>@log.RawText</pre> }