@using Humanizer @using Newtonsoft.Json @using StardewModdingAPI.Toolkit.Utilities @using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.LogParsing.Models @model StardewModdingAPI.Web.ViewModels.LogParserModel @{ ViewData["Title"] = "SMAPI log parser"; IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod(); IDictionary<string, bool> defaultFilters = Enum .GetValues(typeof(LogLevel)) .Cast<LogLevel>() .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); JsonSerializerSettings noFormatting = new JsonSerializerSettings { Formatting = Formatting.None }; } @section Head { @if (Model.PasteID != null) { <meta name="robots" content="noindex" /> } <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20191127" /> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script> <script src="~/Content/js/log-parser.js?r=20190515"></script> <script> $(function() { smapi.logParser({ logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)), showPopup: @Json.Serialize(Model.ParsedLog == null), showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting), showSections: @Json.Serialize(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false), noFormatting), showLevels: @Json.Serialize(defaultFilters, noFormatting), enableFilters: @Json.Serialize(!Model.ShowRaw) }, '@this.Url.PlainAction("Index", "LogParser", values: null)'); }); </script> } @* 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>https://@this.Context.Request.Host.ToUriComponent()@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }))</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 (Model.ParsedLog?.IsValid == true) { <div class="banner success" v-pre> <strong>Share this link to let someone else see the log:</strong> <code>https://@this.Context.Request.Host.ToUriComponent()@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })</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) { <div class="save-metadata" v-pre> @if (Model.Expiry != null) { <text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()). </text> } <!--@Model.UploadWarning--> </div> } @* upload new log *@ @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 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 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> <h2>How do I share my log?</h2> <form action="@this.Url.PlainAction("PostAsync", "LogParser")" method="post"> <ol> <li> Drag the file onto this textbox (or paste the text in):<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 (Model.ParsedLog?.IsValid == true) { <div id="output"> @if (Model.ParsedLog.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 Model.ParsedLog.Mods.Where(mod => (mod.HasUpdate && mod.ContentPackFor == null) || (contentPacks != null && 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"> <caption>Game info:</caption> <tr> <th>Stardew Valley:</th> <td v-pre>@Model.ParsedLog.GameVersion on @Model.ParsedLog.OperatingSystem</td> </tr> <tr> <th>SMAPI:</th> <td v-pre>@Model.ParsedLog.ApiVersion</td> </tr> <tr> <th>Folder:</th> <td v-pre>@Model.ParsedLog.GamePath</td> </tr> <tr> <th>Log started:</th> <td>@Model.ParsedLog.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> } </caption> @foreach (var mod in Model.ParsedLog.Mods.Where(p => p.Loaded && p.ContentPackFor == 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 v-pre> <strong>@mod.Name</strong> @mod.Version @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) { <div class="content-packs"> @foreach (var contentPack in contentPackList) { <text>+ @contentPack.Name @contentPack.Version</text><br /> } </div> } </td> <td v-pre> @mod.Author @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList)) { <div 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="filters"> Filter messages: <span v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> | <span v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> | <span v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> | <span v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> | <span v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> | <span v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span> </div> <table id="log"> @foreach (var message in Model.ParsedLog.Messages) { string levelStr = message.Level.ToString().ToLower(); string sectionStartClass = message.IsStartOfSection ? "section-start" : null; string sectionFilter = message.Section != null && !message.IsStartOfSection ? $"&& sectionsAllow('{message.Section}')" : null; // filter the message by section if applicable <tr class="mod @levelStr @sectionStartClass" @if (message.IsStartOfSection) { <text>v-on:click="toggleSection('@message.Section')"</text> } v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter"> <td v-pre>@message.Time</td> <td v-pre>@message.Level.ToString().ToUpper()</td> <td v-pre data-title="@message.Mod">@message.Mod</td> <td> <span v-pre class="log-message-text">@message.Text</span> @if (message.IsStartOfSection) { <span class="section-toggle-message"> <template v-if="sectionsAllow('@message.Section')">This section is shown. Click here to hide it.</template> <template v-else>This section is hidden. Click here to show it.</template> </span> } </td> </tr> if (message.Repeated > 0) { <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter"> <td colspan="3"></td> <td v-pre><i>repeats [@message.Repeated] times.</i></td> </tr> } } </table> <small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, raw = true })">view raw log</a></small> } else { <pre v-pre>@Model.ParsedLog.RawText</pre> <small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })">view parsed log</a></small> } </div> } else if (Model.ParsedLog?.IsValid == false) { <h3>Raw log</h3> <pre v-pre>@Model.ParsedLog.RawText</pre> }