reimplement log parser with serverside parsing and vue.js frontend
This commit is contained in:
parent
68528f7dec
commit
d7696912e0
|
@ -20,8 +20,14 @@
|
|||
* Fixed unhelpful error when a translation file has duplicate keys due to case-insensitivity.
|
||||
* Fixed some JSON field names being case-sensitive.
|
||||
|
||||
* For the [log parser][]:
|
||||
* Significantly reduced download size when viewing files with repeated errors.
|
||||
* Improved parse error handling.
|
||||
* Fixed 'log started' field showing incorrect date.
|
||||
|
||||
* For SMAPI developers:
|
||||
* Overhauled mod DB format to be more concise, reduce the memory footprint, and support versioning/defaulting more fields.
|
||||
* Reimplemented log parser with serverside parsing and vue.js on the frontend.
|
||||
|
||||
## 2.4
|
||||
* For players:
|
||||
|
|
|
@ -8,6 +8,8 @@ using Microsoft.Extensions.Options;
|
|||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.Framework.LogParsing;
|
||||
using StardewModdingAPI.Web.Framework.LogParsing.Models;
|
||||
using StardewModdingAPI.Web.ViewModels;
|
||||
|
||||
namespace StardewModdingAPI.Web.Controllers
|
||||
|
@ -52,25 +54,23 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
[HttpGet]
|
||||
[Route("log")]
|
||||
[Route("log/{id}")]
|
||||
public ViewResult Index(string id = null)
|
||||
public async Task<ViewResult> Index(string id = null)
|
||||
{
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id));
|
||||
// fresh page
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null));
|
||||
|
||||
// log page
|
||||
PasteInfo paste = await this.GetAsync(id);
|
||||
ParsedLog log = paste.Success
|
||||
? new LogParser().Parse(paste.Content)
|
||||
: new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error };
|
||||
return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log));
|
||||
}
|
||||
|
||||
/***
|
||||
** JSON
|
||||
***/
|
||||
/// <summary>Fetch raw text from Pastebin.</summary>
|
||||
/// <param name="id">The Pastebin paste ID.</param>
|
||||
[HttpGet, Produces("application/json")]
|
||||
[Route("log/fetch/{id}")]
|
||||
public async Task<PasteInfo> GetAsync(string id)
|
||||
{
|
||||
PasteInfo response = await this.Pastebin.GetAsync(id);
|
||||
response.Content = this.DecompressString(response.Content);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>Save raw log data.</summary>
|
||||
/// <param name="content">The log content to save.</param>
|
||||
[HttpPost, Produces("application/json"), AllowLargePosts]
|
||||
|
@ -85,6 +85,15 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Fetch raw text from Pastebin.</summary>
|
||||
/// <param name="id">The Pastebin paste ID.</param>
|
||||
private async Task<PasteInfo> GetAsync(string id)
|
||||
{
|
||||
PasteInfo response = await this.Pastebin.GetAsync(id);
|
||||
response.Content = this.DecompressString(response.Content);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>Compress a string.</summary>
|
||||
/// <param name="text">The text to compress.</param>
|
||||
/// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||
{
|
||||
/// <summary>An error while parsing the log file which doesn't require a stack trace to troubleshoot.</summary>
|
||||
internal class LogParseException : Exception
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="message">The user-friendly error message.</param>
|
||||
public LogParseException(string message) : base(message) { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using StardewModdingAPI.Web.Framework.LogParsing.Models;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||
{
|
||||
/// <summary>Parses SMAPI log files.</summary>
|
||||
public class LogParser
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>A regex pattern matching the start of a SMAPI message.</summary>
|
||||
private readonly Regex MessageHeaderPattern = new Regex(@"^\[(?<time>\d\d:\d\d:\d\d) (?<level>[a-z]+) +(?<modName>[^\]]+)\] ", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching SMAPI's initial platform info message.</summary>
|
||||
private readonly Regex InfoLinePattern = new Regex(@"^SMAPI (?<apiVersion>.+) with Stardew Valley (?<gameVersion>.+) on (?<os>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching SMAPI's mod folder path line.</summary>
|
||||
private readonly Regex ModPathPattern = new Regex(@"^Mods go here: (?<path>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching SMAPI's log timestamp line.</summary>
|
||||
private readonly Regex LogStartedAtPattern = new Regex(@"^Log started at (?<timestamp>.+) UTC", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching the start of SMAPI's mod list.</summary>
|
||||
private readonly Regex ModListStartPattern = new Regex(@"^Loaded \d+ mods:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>A regex pattern matching an entry in SMAPI's mod list.</summary>
|
||||
private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>.+) by (?<author>.+) \| (?<description>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Parse SMAPI log text.</summary>
|
||||
/// <param name="logText">The SMAPI log text.</param>
|
||||
public ParsedLog Parse(string logText)
|
||||
{
|
||||
try
|
||||
{
|
||||
// skip if empty
|
||||
if (string.IsNullOrWhiteSpace(logText))
|
||||
{
|
||||
return new ParsedLog
|
||||
{
|
||||
IsValid = false,
|
||||
Error = "The log is empty."
|
||||
};
|
||||
}
|
||||
|
||||
// init log
|
||||
ParsedLog log = new ParsedLog
|
||||
{
|
||||
IsValid = true,
|
||||
Messages = this.CollapseRepeats(this.GetMessages(logText)).ToArray(),
|
||||
};
|
||||
|
||||
// parse log messages
|
||||
LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "" };
|
||||
IDictionary<string, LogModInfo> mods = new Dictionary<string, LogModInfo>();
|
||||
bool inModList = false;
|
||||
foreach (LogMessage message in log.Messages)
|
||||
{
|
||||
// collect stats
|
||||
if (message.Level == LogLevel.Error)
|
||||
{
|
||||
if (message.Mod == "SMAPI")
|
||||
smapiMod.Errors++;
|
||||
else if (mods.ContainsKey(message.Mod))
|
||||
mods[message.Mod].Errors++;
|
||||
}
|
||||
|
||||
// collect SMAPI metadata
|
||||
if (message.Mod == "SMAPI")
|
||||
{
|
||||
// update flags
|
||||
if (inModList && !this.ModListEntryPattern.IsMatch(message.Text))
|
||||
inModList = false;
|
||||
|
||||
// mod list
|
||||
if (!inModList && message.Level == LogLevel.Info && this.ModListStartPattern.IsMatch(message.Text))
|
||||
inModList = true;
|
||||
else if (inModList)
|
||||
{
|
||||
Match match = this.ModListEntryPattern.Match(message.Text);
|
||||
string name = match.Groups["name"].Value;
|
||||
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 };
|
||||
}
|
||||
|
||||
// platform info line
|
||||
else if (message.Level == LogLevel.Info && this.InfoLinePattern.IsMatch(message.Text))
|
||||
{
|
||||
Match match = this.InfoLinePattern.Match(message.Text);
|
||||
log.ApiVersion = match.Groups["apiVersion"].Value;
|
||||
log.GameVersion = match.Groups["gameVersion"].Value;
|
||||
log.OperatingSystem = match.Groups["os"].Value;
|
||||
smapiMod.Version = log.ApiVersion;
|
||||
}
|
||||
|
||||
// mod path line
|
||||
else if (message.Level == LogLevel.Debug && this.ModPathPattern.IsMatch(message.Text))
|
||||
{
|
||||
Match match = this.ModPathPattern.Match(message.Text);
|
||||
log.ModPath = match.Groups["path"].Value;
|
||||
}
|
||||
|
||||
// log UTC timestamp line
|
||||
else if (message.Level == LogLevel.Trace && this.LogStartedAtPattern.IsMatch(message.Text))
|
||||
{
|
||||
Match match = this.LogStartedAtPattern.Match(message.Text);
|
||||
log.Timestamp = DateTime.Parse(match.Groups["timestamp"].Value + "Z");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finalise log
|
||||
log.Mods = new[] { smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray();
|
||||
return log;
|
||||
}
|
||||
catch (LogParseException ex)
|
||||
{
|
||||
return new ParsedLog
|
||||
{
|
||||
IsValid = false,
|
||||
Error = ex.Message,
|
||||
RawTextIfError = logText
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ParsedLog
|
||||
{
|
||||
IsValid = false,
|
||||
Error = $"Parsing the log file failed. Technical details:\n{ex}",
|
||||
RawTextIfError = logText
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Collapse consecutive repeats into the previous message.</summary>
|
||||
/// <param name="messages">The messages to filter.</param>
|
||||
private IEnumerable<LogMessage> CollapseRepeats(IEnumerable<LogMessage> messages)
|
||||
{
|
||||
LogMessage next = null;
|
||||
foreach (LogMessage message in messages)
|
||||
{
|
||||
// new message
|
||||
if (next == null)
|
||||
{
|
||||
next = message;
|
||||
continue;
|
||||
}
|
||||
|
||||
// repeat
|
||||
if (next.Level == message.Level && next.Mod == message.Mod && next.Text == message.Text)
|
||||
{
|
||||
next.Repeated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// non-repeat message
|
||||
yield return next;
|
||||
next = message;
|
||||
}
|
||||
yield return next;
|
||||
}
|
||||
|
||||
/// <summary>Split a SMAPI log into individual log messages.</summary>
|
||||
/// <param name="logText">The SMAPI log text.</param>
|
||||
/// <exception cref="LogParseException">The log text can't be parsed successfully.</exception>
|
||||
private IEnumerable<LogMessage> GetMessages(string logText)
|
||||
{
|
||||
LogMessage message = new LogMessage();
|
||||
using (StringReader reader = new StringReader(logText))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// read data
|
||||
string line = reader.ReadLine();
|
||||
if (line == null)
|
||||
break;
|
||||
Match header = this.MessageHeaderPattern.Match(line);
|
||||
|
||||
// validate
|
||||
if (message.Text == null && !header.Success)
|
||||
throw new LogParseException("Found a log message with no SMAPI metadata. Is this a SMAPI log file?");
|
||||
|
||||
// start or continue message
|
||||
if (header.Success)
|
||||
{
|
||||
if (message.Text != null)
|
||||
yield return message;
|
||||
|
||||
message = new LogMessage
|
||||
{
|
||||
Time = header.Groups["time"].Value,
|
||||
Level = Enum.Parse<LogLevel>(header.Groups["level"].Value, ignoreCase: true),
|
||||
Mod = header.Groups["modName"].Value,
|
||||
Text = line.Substring(header.Length)
|
||||
};
|
||||
}
|
||||
else
|
||||
message.Text += "\n" + line;
|
||||
}
|
||||
|
||||
// end last message
|
||||
if (message.Text != null)
|
||||
yield return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
/// <summary>The log severity levels.</summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>Tracing info intended for developers.</summary>
|
||||
Trace,
|
||||
|
||||
/// <summary>Troubleshooting info that may be relevant to the player.</summary>
|
||||
Debug,
|
||||
|
||||
/// <summary>Info relevant to the player. This should be used judiciously.</summary>
|
||||
Info,
|
||||
|
||||
/// <summary>An issue the player should be aware of. This should be used rarely.</summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>A message indicating something went wrong.</summary>
|
||||
Error,
|
||||
|
||||
/// <summary>Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue.</summary>
|
||||
Alert
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
/// <summary>A parsed log message.</summary>
|
||||
public class LogMessage
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The local time when the log was posted.</summary>
|
||||
public string Time { get; set; }
|
||||
|
||||
/// <summary>The log level.</summary>
|
||||
public LogLevel Level { get; set; }
|
||||
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Mod { get; set; }
|
||||
|
||||
/// <summary>The log text.</summary>
|
||||
public string Text { get; set; }
|
||||
|
||||
/// <summary>The number of times this message was repeated consecutively.</summary>
|
||||
public int Repeated { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
/// <summary>Metadata about a mod or content pack in the log.</summary>
|
||||
public class LogModInfo
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod name.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The mod author.</summary>
|
||||
public string Author { get; set; }
|
||||
|
||||
/// <summary>The mod version.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>The mod description.</summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>The number of errors logged by this mod.</summary>
|
||||
public int Errors { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
{
|
||||
/// <summary>Parsed metadata for a log.</summary>
|
||||
public class ParsedLog
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/****
|
||||
** Metadata
|
||||
****/
|
||||
/// <summary>Whether the log file was successfully parsed.</summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>An error message indicating why the log file is invalid.</summary>
|
||||
public string Error { get; set; }
|
||||
|
||||
/// <summary>The raw text if <see cref="IsValid"/> is false.</summary>
|
||||
public string RawTextIfError { get; set; }
|
||||
|
||||
/****
|
||||
** Log data
|
||||
****/
|
||||
/// <summary>The SMAPI version.</summary>
|
||||
public string ApiVersion { get; set; }
|
||||
|
||||
/// <summary>The game version.</summary>
|
||||
public string GameVersion { get; set; }
|
||||
|
||||
/// <summary>The player's operating system.</summary>
|
||||
public string OperatingSystem { get; set; }
|
||||
|
||||
/// <summary>The mod folder path.</summary>
|
||||
public string ModPath { get; set; }
|
||||
|
||||
/// <summary>The ISO 8601 timestamp when the log was started.</summary>
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
/// <summary>Metadata about installed mods and content packs.</summary>
|
||||
public LogModInfo[] Mods { get; set; } = new LogModInfo[0];
|
||||
|
||||
/// <summary>The log messages.</summary>
|
||||
public LogMessage[] Messages { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
using StardewModdingAPI.Web.Framework.LogParsing.Models;
|
||||
|
||||
namespace StardewModdingAPI.Web.ViewModels
|
||||
{
|
||||
/// <summary>The view model for the log parser page.</summary>
|
||||
|
@ -12,6 +14,9 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/// <summary>The paste ID.</summary>
|
||||
public string PasteID { get; set; }
|
||||
|
||||
/// <summary>The parsed log info.</summary>
|
||||
public ParsedLog ParsedLog { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -22,10 +27,12 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="sectionUrl">The root URL for the log parser controller.</param>
|
||||
/// <param name="pasteID">The paste ID.</param>
|
||||
public LogParserModel(string sectionUrl, string pasteID)
|
||||
/// <param name="parsedLog">The parsed log info.</param>
|
||||
public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog)
|
||||
{
|
||||
this.SectionUrl = sectionUrl;
|
||||
this.PasteID = pasteID;
|
||||
this.ParsedLog = parsedLog;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
@{
|
||||
ViewData["Title"] = "SMAPI log parser";
|
||||
}
|
||||
@using Newtonsoft.Json
|
||||
@using StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
@model StardewModdingAPI.Web.ViewModels.LogParserModel
|
||||
@section Head {
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180101" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/log-parser.js?r=20180101"></script>
|
||||
<style type="text/css" id="modflags"></style>
|
||||
<script>
|
||||
$(function() {
|
||||
smapi.logParser('@Model.SectionUrl', '@Model.PasteID');
|
||||
smapi.logParser({
|
||||
logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)),
|
||||
showPopup: @Json.Serialize(Model.ParsedLog == null),
|
||||
showMods: @Json.Serialize(Model.ParsedLog?.Mods?.ToDictionary(p => p.Name, p => true), new JsonSerializerSettings { Formatting = Formatting.None }),
|
||||
showLevels: {
|
||||
trace: false,
|
||||
debug: false,
|
||||
info: true,
|
||||
alert: true,
|
||||
warn: true,
|
||||
error: true
|
||||
}
|
||||
}, '@Model.SectionUrl');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -20,99 +34,122 @@
|
|||
<p id="blurb">This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.</p>
|
||||
<input type="button" id="upload-button" value="Share a new log" />
|
||||
|
||||
@if (Model.PasteID != null)
|
||||
@if (Model.ParsedLog?.IsValid == true)
|
||||
{
|
||||
<h2>Parsed log</h2>
|
||||
<div id="output">
|
||||
<table id="metadata">
|
||||
<caption>Game info:</caption>
|
||||
<tr>
|
||||
<td>SMAPI version:</td>
|
||||
<td>@Model.ParsedLog.ApiVersion</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game version:</td>
|
||||
<td>@Model.ParsedLog.GameVersion</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platform:</td>
|
||||
<td>@Model.ParsedLog.OperatingSystem</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mods path:</td>
|
||||
<td>@Model.ParsedLog.ModPath</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Log started:</td>
|
||||
<td>@Model.ParsedLog.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time)</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
<table id="mods">
|
||||
<caption>
|
||||
Installed mods:
|
||||
<span class="notice txt"><i>click any mod to filter</i></span>
|
||||
<span class="notice btn txt" v-on:click="showAllMods" v-if="stats.modsHidden > 0">show all</span>
|
||||
<span class="notice btn txt" v-on:click="hideAllMods" v-if="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span>
|
||||
</caption>
|
||||
@foreach (var mod in Model.ParsedLog.Mods)
|
||||
{
|
||||
<tr v-on:click="toggleMod('@mod.Name')" class="mod-entry" v-bind:class="{ hidden: !showMods['@mod.Name'] }">
|
||||
<td><input type="checkbox" v-bind:checked="showMods['@mod.Name']" v-if="anyModsHidden" /></td>
|
||||
<td>@mod.Version</td>
|
||||
<td>@mod.Author</td>
|
||||
@if (mod.Errors == 0)
|
||||
{
|
||||
<td class="color-green">no errors</td>
|
||||
}
|
||||
else if (mod.Errors == 1)
|
||||
{
|
||||
<td class="color-red">@mod.Errors error</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="color-red">@mod.Errors errors</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<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();
|
||||
|
||||
<tr class="@levelStr mod" v-if="showMods['@message.Mod'] && showLevels['@levelStr']">
|
||||
<td>@message.Time</td>
|
||||
<td>@message.Level.ToString().ToUpper()</td>
|
||||
<td data-title="@message.Mod">@message.Mod</td>
|
||||
<td>@message.Text</td>
|
||||
</tr>
|
||||
if (message.Repeated > 0)
|
||||
{
|
||||
<tr class="@levelStr mod mod-repeat" v-if="showMods['@message.Mod'] && showLevels['@levelStr']">
|
||||
<td colspan="3"></td>
|
||||
<td><i>repeats [@message.Repeated] times.</i></td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
<div id="output" class="trace debug">
|
||||
@if (Model.PasteID != null)
|
||||
{
|
||||
<div id="log-data" style="display: none;">
|
||||
<div class="always">
|
||||
<table id="gameinfo">
|
||||
<caption>Game info:</caption>
|
||||
<tr>
|
||||
<td>SMAPI Version</td>
|
||||
<td id="api-version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Game Version</td>
|
||||
<td id="game-version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td id="platform"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mods path</td>
|
||||
<td id="mods-path"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Log started</td>
|
||||
<td id="log-started"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
<table id="modslist">
|
||||
<caption>Installed Mods: <span id="modlink-r" class="notice btn">Remove all mod filters</span><span class="notice txt"><i>Click any mod to filter</i></span> <span id="modlink-a" class="notice btn txt">Select all</span></caption>
|
||||
</table>
|
||||
<div id="filters">
|
||||
Filter messages: <span>TRACE</span> | <span>DEBUG</span> | <span class="active">INFO</span> | <span class="active">ALERT</span> | <span class="active">WARN</span> | <span class="active">ERROR</span>
|
||||
</div>
|
||||
else if (Model.ParsedLog?.IsValid == false)
|
||||
{
|
||||
<h2>Parsed log</h2>
|
||||
<div id="error" class="color-red">
|
||||
<p><strong>We couldn't parse that file, but you can still share the link.</strong></p>
|
||||
<p>Error details: @Model.ParsedLog.Error</p>
|
||||
</div>
|
||||
|
||||
<h3>Raw log</h3>
|
||||
<pre>@Model.ParsedLog.RawTextIfError</pre>
|
||||
}
|
||||
|
||||
<div id="upload-area">
|
||||
<div id="popup-upload" class="popup">
|
||||
<h1>Upload log file</h1>
|
||||
<div class="frame">
|
||||
<ol>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li>
|
||||
<li>Drag the file onto the textbox below (or paste the text in).</li>
|
||||
<li>Click <em>Parse</em>.</li>
|
||||
<li>Share the URL of the new page.</li>
|
||||
</ol>
|
||||
<textarea id="input" placeholder="Paste or drag the log here"></textarea>
|
||||
<div class="buttons">
|
||||
<input type="button" id="submit" value="Parse" />
|
||||
<input type="button" id="cancel" value="Cancel" />
|
||||
</div>
|
||||
<table id="log"></table>
|
||||
</div>
|
||||
}
|
||||
<div id="error" class="color-red"></div>
|
||||
</div>
|
||||
<script class="template" id="template-css" type="text/html">
|
||||
#output.modfilter:not(.mod-{0}) .mod-{0} { display:none; } #output.modfilter.mod-{0} #modslist tr { background:#ffeeee; } #output.modfilter.mod-{0} #modslist tr#modlink-{0} { background:#eeffee; }
|
||||
</script>
|
||||
<script class="template" id="template-modentry" type="text/html">
|
||||
<tr id="modlink-{0}">
|
||||
<td>{1}</td>
|
||||
<td>{2}</td>
|
||||
<td>{3}</td>
|
||||
<td class={4}>{5}</td>
|
||||
</tr>
|
||||
</script>
|
||||
<script class="template" id="template-logentry" type="text/html">
|
||||
<tr class="{0} mod mod-{1}">
|
||||
<td>{2}</td>
|
||||
<td>{3}</td>
|
||||
<td data-title="{4}">{4}</td>
|
||||
<td>{5}</td>
|
||||
</tr>
|
||||
</script>
|
||||
<script class="template" id="template-lognotice" type="text/html">
|
||||
<tr class="{0} mod-repeat mod mod-{1}">
|
||||
<td colspan="3"></td>
|
||||
<td><i>repeats [{2}] times.</i></td>
|
||||
</tr>
|
||||
</script>
|
||||
<div id="popup-upload" class="popup">
|
||||
<h1>Upload log file</h1>
|
||||
<div class="frame">
|
||||
<ol>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li>
|
||||
<li>Drag the file onto the textbox below (or paste the text in).</li>
|
||||
<li>Click <em>Parse</em>.</li>
|
||||
<li>Share the URL of the new page.</li>
|
||||
</ol>
|
||||
<textarea id="input" placeholder="Paste or drag the log here"></textarea>
|
||||
<div class="buttons">
|
||||
<input type="button" id="submit" value="Parse" />
|
||||
<input type="button" id="cancel" value="Cancel" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploader"></div>
|
||||
</div>
|
||||
<div id="popup-raw" class="popup">
|
||||
<h1>Raw log file</h1>
|
||||
<div class="frame">
|
||||
<textarea id="dataraw"></textarea>
|
||||
<div class="buttons">
|
||||
<input type="button" id="closeraw" value="Close" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploader"></div>
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
.mod-repeat {
|
||||
font-size: 8pt;
|
||||
/*********
|
||||
** Main layout
|
||||
*********/
|
||||
input[type="button"] {
|
||||
font-size: 20px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.template {
|
||||
display: none;
|
||||
caption {
|
||||
text-align: left;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.popup, #uploader {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, .33);
|
||||
z-index: 2;
|
||||
display: none;
|
||||
padding: 5px;
|
||||
#output {
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#upload-button {
|
||||
|
@ -27,103 +29,83 @@
|
|||
background: #eef;
|
||||
}
|
||||
|
||||
|
||||
#uploader:after {
|
||||
content: attr(data-text);
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 24px;
|
||||
line-height: 25px;
|
||||
border: 1px solid #000;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -12px -50px 0 0;
|
||||
font-size: 18px;
|
||||
/*********
|
||||
** Log metadata & filters
|
||||
*********/
|
||||
#metadata, #mods, #filters {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-bottom: 1px dashed #888888;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
table#metadata,
|
||||
table#mods {
|
||||
border: 1px solid #000000;
|
||||
background: #ffffff;
|
||||
border-radius: 5px;
|
||||
border-spacing: 1px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
box-shadow: 1px 1px 1px 1px #dddddd;
|
||||
}
|
||||
|
||||
.popup h1 {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
margin-left: -150px;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
border: 1px solid #008;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
font-family: sans-serif;
|
||||
font-size: 40px;
|
||||
margin-top: -25px;
|
||||
z-index: 10;
|
||||
border-bottom: 0;
|
||||
#mods {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.frame {
|
||||
margin: auto;
|
||||
margin-top: 25px;
|
||||
padding: 2em;
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
bottom: 10%;
|
||||
padding-bottom: 30px;
|
||||
background: #FFF;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #008;
|
||||
}
|
||||
|
||||
input[type="button"] {
|
||||
font-size: 20px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 0, .2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#input[type="button"]:hover {
|
||||
background-color: #fee;
|
||||
}
|
||||
|
||||
#cancel, #closeraw {
|
||||
border: 1px solid #880000;
|
||||
background-color: #fcc;
|
||||
}
|
||||
|
||||
#submit {
|
||||
border: 1px solid #008800;
|
||||
background-color: #cfc;
|
||||
}
|
||||
|
||||
#submit:hover {
|
||||
background-color: #efe;
|
||||
}
|
||||
|
||||
#input, #dataraw {
|
||||
width: 100%;
|
||||
height: 30em;
|
||||
max-height: 70%;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #000088;
|
||||
outline: none;
|
||||
box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2);
|
||||
}
|
||||
|
||||
.color-red {
|
||||
#mods .color-red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.color-green {
|
||||
#mods .color-green {
|
||||
color: green;
|
||||
}
|
||||
|
||||
#mods tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#metadata tr,
|
||||
#mods tr {
|
||||
background: #eee
|
||||
}
|
||||
|
||||
#mods span.notice {
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mods span.notice.btn {
|
||||
cursor: pointer;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
padding: 0 2px;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
#mods span.notice.txt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#mods .mod-entry.hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#metadata td:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
#metadata tr:nth-child(even),
|
||||
#mods tr:nth-child(even) {
|
||||
background: #fff
|
||||
}
|
||||
|
||||
#filters {
|
||||
margin: 1em 0 0 0;
|
||||
padding: 0;
|
||||
|
@ -155,59 +137,35 @@ input[type="button"] {
|
|||
background: #efe;
|
||||
}
|
||||
|
||||
#output {
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
/*********
|
||||
** Log
|
||||
*********/
|
||||
#log .mod-repeat {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
#output > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#output.trace .trace,
|
||||
#output.debug .debug,
|
||||
#output.info .info,
|
||||
#output.alert .alert,
|
||||
#output.warn .warn,
|
||||
#output.error .error {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#output .trace {
|
||||
#log .trace {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#output .debug {
|
||||
#log .debug {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
#output .info {
|
||||
color: #000
|
||||
#log .info {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#output .alert {
|
||||
#log .alert {
|
||||
color: #b0b;
|
||||
}
|
||||
|
||||
#output .warn {
|
||||
color: #f80
|
||||
#log .warn {
|
||||
color: #f80;
|
||||
}
|
||||
|
||||
#output .error {
|
||||
color: #f00
|
||||
}
|
||||
|
||||
#output .always {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px dashed #888888;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
caption {
|
||||
text-align: left;
|
||||
padding-top: 2px;
|
||||
#log .error {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
#log {
|
||||
|
@ -224,6 +182,7 @@ caption {
|
|||
border-bottom: 1px dotted #ccc;
|
||||
border-top: 2px solid #fff;
|
||||
vertical-align: top;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#log td:not(:last-child) {
|
||||
|
@ -259,61 +218,99 @@ caption {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
table#gameinfo,
|
||||
table#modslist {
|
||||
border: 1px solid #000000;
|
||||
background: #ffffff;
|
||||
border-radius: 5px;
|
||||
border-spacing: 1px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
box-shadow: 1px 1px 1px 1px #dddddd;
|
||||
#error {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
#modslist {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
#gameinfo td:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
#gameinfo tr,
|
||||
#modslist tr {
|
||||
background: #eee
|
||||
}
|
||||
|
||||
#gameinfo tr:nth-child(even),
|
||||
#modslist tr:nth-child(even) {
|
||||
background: #fff
|
||||
}
|
||||
|
||||
#modslist tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span.notice {
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
/*********
|
||||
** Upload popup
|
||||
*********/
|
||||
#upload-area .popup,
|
||||
#upload-area #uploader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, .33);
|
||||
z-index: 2;
|
||||
display: none;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
span.notice.btn {
|
||||
cursor: pointer;
|
||||
#upload-area #uploader:after {
|
||||
content: attr(data-text);
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 24px;
|
||||
line-height: 25px;
|
||||
border: 1px solid #000;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -12px -50px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
padding: 0 2px;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
#output:not(.modfilter) span.notice.txt {
|
||||
display: inline-block;
|
||||
#upload-area .popup h1 {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
margin-left: -150px;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
border: 1px solid #008;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
font-family: sans-serif;
|
||||
font-size: 40px;
|
||||
margin-top: -25px;
|
||||
z-index: 10;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
#output.modfilter span.notice.btn {
|
||||
display: inline-block;
|
||||
#upload-area .frame {
|
||||
margin: auto;
|
||||
margin-top: 25px;
|
||||
padding: 2em;
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
bottom: 10%;
|
||||
padding-bottom: 30px;
|
||||
background: #FFF;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #008;
|
||||
}
|
||||
|
||||
#upload-area #cancel {
|
||||
border: 1px solid #880000;
|
||||
background-color: #fcc;
|
||||
}
|
||||
|
||||
#upload-area #submit {
|
||||
border: 1px solid #008800;
|
||||
background-color: #cfc;
|
||||
}
|
||||
|
||||
#upload-area #submit:hover {
|
||||
background-color: #efe;
|
||||
}
|
||||
|
||||
#upload-area #input {
|
||||
width: 100%;
|
||||
height: 30em;
|
||||
max-height: 70%;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #000088;
|
||||
outline: none;
|
||||
box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2);
|
||||
}
|
||||
|
|
|
@ -1,48 +1,98 @@
|
|||
/* globals $ */
|
||||
|
||||
var smapi = smapi || {};
|
||||
smapi.logParser = function(sectionUrl, pasteID) {
|
||||
/*********
|
||||
** Initialisation
|
||||
*********/
|
||||
var stage,
|
||||
flags = $("#modflags"),
|
||||
output = $("#output"),
|
||||
error = $("#error"),
|
||||
filters = 0,
|
||||
memory = "",
|
||||
versionInfo,
|
||||
modInfo,
|
||||
modMap,
|
||||
modErrors,
|
||||
logInfo,
|
||||
templateModentry = $("#template-modentry").text(),
|
||||
templateCss = $("#template-css").text(),
|
||||
templateLogentry = $("#template-logentry").text(),
|
||||
templateLognotice = $("#template-lognotice").text(),
|
||||
regexInfo = /\[[\d\:]+ INFO SMAPI] SMAPI (.*?) with Stardew Valley (.*?) on (.*?)\n/g,
|
||||
regexMods = /\[[^\]]+\] Loaded \d+ mods:(?:\n\[[^\]]+\] .+)+/g,
|
||||
regexLog = /\[([\d\:]+) (TRACE|DEBUG|INFO|WARN|ALERT|ERROR) ? ([^\]]+)\] ?((?:\n|.)*?)(?=(?:\[\d\d:|$))/g,
|
||||
regexMod = /\[(?:.*?)\] *(.*?) (\d+\.?(?:.*?))(?: by (.*?))? \|(?:.*?)$/gm,
|
||||
regexDate = /\[\d{2}:\d{2}:\d{2} TRACE SMAPI\] Log started at (.*?) UTC/g,
|
||||
regexPath = /\[\d{2}:\d{2}:\d{2} DEBUG SMAPI\] Mods go here: (.*?)(?:\n|$)/g;
|
||||
var app;
|
||||
smapi.logParser = function (data, sectionUrl) {
|
||||
// internal filter counts
|
||||
var stats = data.stats = {
|
||||
modsShown: 0,
|
||||
modsHidden: 0
|
||||
};
|
||||
function updateModFilters() {
|
||||
// counts
|
||||
stats.modsShown = 0;
|
||||
stats.modsHidden = 0;
|
||||
for (var key in data.showMods) {
|
||||
if (data.showMods.hasOwnProperty(key)) {
|
||||
if (data.showMods[key])
|
||||
stats.modsShown++;
|
||||
else
|
||||
stats.modsHidden++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("#filters span").on("click", function(evt) {
|
||||
var t = $(evt.currentTarget);
|
||||
t.toggleClass("active");
|
||||
output.toggleClass(t.text().toLowerCase());
|
||||
// set local time started
|
||||
if(data)
|
||||
data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2);
|
||||
|
||||
// init app
|
||||
app = new Vue({
|
||||
el: '#output',
|
||||
data: data,
|
||||
computed: {
|
||||
anyModsHidden: function () {
|
||||
return stats.modsHidden > 0;
|
||||
},
|
||||
anyModsShown: function () {
|
||||
return stats.modsShown > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleLevel: function(id) {
|
||||
this.showLevels[id] = !this.showLevels[id];
|
||||
},
|
||||
|
||||
toggleMod: function (id) {
|
||||
var curShown = this.showMods[id];
|
||||
|
||||
// first filter: only show this by default
|
||||
if (stats.modsHidden === 0) {
|
||||
this.hideAllMods();
|
||||
this.showMods[id] = true;
|
||||
}
|
||||
|
||||
// unchecked last filter: reset
|
||||
else if (stats.modsShown === 1 && curShown)
|
||||
this.showAllMods();
|
||||
|
||||
// else toggle
|
||||
else
|
||||
this.showMods[id] = !this.showMods[id];
|
||||
|
||||
updateModFilters();
|
||||
},
|
||||
showAllMods: function () {
|
||||
for (var key in this.showMods) {
|
||||
if (this.showMods.hasOwnProperty(key)) {
|
||||
this.showMods[key] = true;
|
||||
}
|
||||
}
|
||||
updateModFilters();
|
||||
},
|
||||
hideAllMods: function () {
|
||||
for (var key in this.showMods) {
|
||||
if (this.showMods.hasOwnProperty(key)) {
|
||||
this.showMods[key] = false;
|
||||
}
|
||||
}
|
||||
updateModFilters();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**********
|
||||
** Upload form
|
||||
*********/
|
||||
var error = $("#error");
|
||||
|
||||
$("#upload-button").on("click", function() {
|
||||
memory = $("#input").val() || "";
|
||||
$("#input").val("");
|
||||
$("#popup-upload").fadeIn();
|
||||
});
|
||||
|
||||
var closeUploadPopUp = function() {
|
||||
$("#popup-upload").fadeOut(400, function() {
|
||||
$("#input").val(memory);
|
||||
memory = "";
|
||||
});
|
||||
$("#popup-upload").fadeOut(400);
|
||||
};
|
||||
|
||||
$("#popup-upload").on({
|
||||
|
@ -77,7 +127,7 @@ smapi.logParser = function(sectionUrl, pasteID) {
|
|||
$("#popup-upload").fadeOut();
|
||||
var paste = $("#input").val();
|
||||
if (paste) {
|
||||
memory = "";
|
||||
//memory = "";
|
||||
$("#uploader").attr("data-text", "Saving...");
|
||||
$("#uploader").fadeIn();
|
||||
$
|
||||
|
@ -105,210 +155,15 @@ smapi.logParser = function(sectionUrl, pasteID) {
|
|||
});
|
||||
|
||||
$(document).on("keydown", function(e) {
|
||||
if (e.which == 27) {
|
||||
if ($("#popup-upload").css("display") !== "none" && $("#popup-upload").css("opacity") == 1) {
|
||||
if (e.which === 27) {
|
||||
if ($("#popup-upload").css("display") !== "none" && $("#popup-upload").css("opacity") === 1) {
|
||||
closeUploadPopUp();
|
||||
}
|
||||
|
||||
$("#popup-raw").fadeOut(400);
|
||||
}
|
||||
});
|
||||
$("#cancel").on("click", closeUploadPopUp);
|
||||
|
||||
$("#closeraw").on("click", function() {
|
||||
$("#popup-raw").fadeOut(400);
|
||||
});
|
||||
|
||||
$("#popup-raw").on("click", function(e) {
|
||||
if (e.target.id === "popup-raw") {
|
||||
$("#popup-raw").fadeOut(400);
|
||||
}
|
||||
});
|
||||
|
||||
if (pasteID) {
|
||||
getData(pasteID);
|
||||
}
|
||||
else
|
||||
if (data.showPopup)
|
||||
$("#popup-upload").fadeIn();
|
||||
|
||||
|
||||
/*********
|
||||
** Helpers
|
||||
*********/
|
||||
function modClicked(evt) {
|
||||
var id = $(evt.currentTarget).attr("id").split("-")[1],
|
||||
cls = "mod-" + id;
|
||||
if (output.hasClass(cls))
|
||||
filters--;
|
||||
else
|
||||
filters++;
|
||||
output.toggleClass(cls);
|
||||
if (filters === 0) {
|
||||
output.removeClass("modfilter");
|
||||
} else {
|
||||
output.addClass("modfilter");
|
||||
}
|
||||
}
|
||||
|
||||
function removeFilter() {
|
||||
for (var c = 0; c < modInfo.length; c++) {
|
||||
output.removeClass("mod-" + c);
|
||||
}
|
||||
filters = 0;
|
||||
output.removeClass("modfilter");
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
for (var c = 0; c < modInfo.length; c++) {
|
||||
output.addClass("mod-" + c);
|
||||
}
|
||||
filters = modInfo.length;
|
||||
output.addClass("modfilter");
|
||||
}
|
||||
|
||||
function parseData() {
|
||||
stage = "parseData.pre";
|
||||
var data = $("#input").val();
|
||||
if (!data) {
|
||||
stage = "parseData.checkNullData";
|
||||
throw new Error("Field `data` is null");
|
||||
|
||||
}
|
||||
var dataInfo = regexInfo.exec(data) || regexInfo.exec(data) || regexInfo.exec(data),
|
||||
dataMods = regexMods.exec(data) || regexMods.exec(data) || regexMods.exec(data) || [""],
|
||||
dataDate = regexDate.exec(data) || regexDate.exec(data) || regexDate.exec(data),
|
||||
dataPath = regexPath.exec(data) || regexPath.exec(data) || regexPath.exec(data),
|
||||
match;
|
||||
stage = "parseData.doNullCheck";
|
||||
if (!dataInfo)
|
||||
throw new Error("Field `dataInfo` is null");
|
||||
if (!dataMods)
|
||||
throw new Error("Field `dataMods` is null");
|
||||
if (!dataPath)
|
||||
throw new Error("Field `dataPath` is null");
|
||||
dataMods = dataMods[0];
|
||||
stage = "parseData.setupDefaults";
|
||||
modMap = {
|
||||
"SMAPI": 0
|
||||
};
|
||||
modErrors = {
|
||||
"SMAPI": 0,
|
||||
"Console.Out": 0
|
||||
};
|
||||
logInfo = [];
|
||||
modInfo = [
|
||||
["SMAPI", dataInfo[1], "Zoryn, CLxS & Pathoschild"]
|
||||
];
|
||||
stage = "parseData.parseInfo";
|
||||
var date = dataDate ? new Date(dataDate[1] + "Z") : null;
|
||||
versionInfo = {
|
||||
apiVersion: dataInfo[1],
|
||||
gameVersion: dataInfo[2],
|
||||
platform: dataInfo[3],
|
||||
logDate: date ? date.getFullYear() + "-" + ("0" + date.getMonth().toString()).substr(-2) + "-" + ("0" + date.getDay().toString()).substr(-2) + " at " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + " " + date.toLocaleTimeString("en-us", { timeZoneName: "short" }).split(" ")[2] : "No timestamp found",
|
||||
modsPath: dataPath[1]
|
||||
};
|
||||
stage = "parseData.parseMods";
|
||||
while ((match = regexMod.exec(dataMods))) {
|
||||
modErrors[match[1]] = 0;
|
||||
modMap[match[1]] = modInfo.length;
|
||||
modInfo.push([match[1], match[2], match[3] ? ("by " + match[3]) : "Unknown author"]);
|
||||
}
|
||||
stage = "parseData.parseLog";
|
||||
while ((match = regexLog.exec(data))) {
|
||||
if (match[2] === "ERROR")
|
||||
modErrors[match[3]]++;
|
||||
logInfo.push([match[1], match[2], match[3], match[4]]);
|
||||
}
|
||||
stage = "parseData.post";
|
||||
modMap["Console.Out"] = modInfo.length;
|
||||
modInfo.push(["Console.Out", "", ""]);
|
||||
}
|
||||
|
||||
function renderData() {
|
||||
stage = "renderData.pre";
|
||||
|
||||
output.find("#api-version").text(versionInfo.apiVersion);
|
||||
output.find("#game-version").text(versionInfo.gameVersion);
|
||||
output.find("#platform").text(versionInfo.platform);
|
||||
output.find("#log-started").text(versionInfo.logDate);
|
||||
output.find("#mods-path").text(versionInfo.modsPath);
|
||||
|
||||
var modslist = $("#modslist"), log = $("#log"), modCache = [], y = 0;
|
||||
for (; y < modInfo.length; y++) {
|
||||
var errors = modErrors[modInfo[y][0]],
|
||||
err, cls = "color-red";
|
||||
if (errors === 0) {
|
||||
err = "No Errors";
|
||||
cls = "color-green";
|
||||
} else if (errors === 1)
|
||||
err = "1 Error";
|
||||
else
|
||||
err = errors + " Errors";
|
||||
modCache.push(prepare(templateModentry, [y, modInfo[y][0], modInfo[y][1], modInfo[y][2], cls, err]));
|
||||
}
|
||||
modslist.append(modCache.join(""));
|
||||
for (var z = 0; z < modInfo.length; z++)
|
||||
$("#modlink-" + z).on("click", modClicked);
|
||||
var flagCache = [];
|
||||
for (var c = 0; c < modInfo.length; c++)
|
||||
flagCache.push(prepare(templateCss, [c]));
|
||||
flags.html(flagCache.join(""));
|
||||
var logCache = [], dupeCount = 0, dupeMemory = "|||";
|
||||
for (var x = 0; x < logInfo.length; x++) {
|
||||
var dm = logInfo[x][1] + "|" + logInfo[x][2] + "|" + logInfo[x][3];
|
||||
if (dupeMemory !== dm) {
|
||||
if (dupeCount > 0)
|
||||
logCache.push(prepare(templateLognotice, [logInfo[x - 1][1].toLowerCase(), modMap[logInfo[x - 1][2]], dupeCount]));
|
||||
dupeCount = 0;
|
||||
dupeMemory = dm;
|
||||
logCache.push(prepare(templateLogentry, [logInfo[x][1].toLowerCase(), modMap[logInfo[x][2]], logInfo[x][0], logInfo[x][1], logInfo[x][2], logInfo[x][3].split(" ").join("  ").replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br />")]));
|
||||
}
|
||||
else
|
||||
dupeCount++;
|
||||
}
|
||||
log.append(logCache.join(""));
|
||||
$("#modlink-r").on("click", removeFilter);
|
||||
$("#modlink-a").on("click", selectAll);
|
||||
|
||||
$("#log-data").show();
|
||||
}
|
||||
|
||||
function prepare(str, arr) {
|
||||
var regex = /\{(\d)\}/g,
|
||||
match;
|
||||
while ((match = regex.exec(str)))
|
||||
str = str.replace(match[0], arr[match[1]]);
|
||||
return str;
|
||||
}
|
||||
function loadData() {
|
||||
try {
|
||||
stage = "loadData.Pre";
|
||||
parseData();
|
||||
renderData();
|
||||
$("#viewraw").on("click", function() {
|
||||
$("#dataraw").val($("#input").val());
|
||||
$("#popup-raw").fadeIn();
|
||||
});
|
||||
stage = "loadData.Post";
|
||||
}
|
||||
catch (err) {
|
||||
error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br /> <p>Stage: ' + stage + "</p>" + err + '<hr /><pre id="rawlog"></pre>');
|
||||
$("#rawlog").text($("#input").val());
|
||||
}
|
||||
}
|
||||
function getData(pasteID) {
|
||||
$("#uploader").attr("data-text", "Loading...");
|
||||
$("#uploader").fadeIn();
|
||||
$.get(sectionUrl + "/fetch/" + pasteID, function(data) {
|
||||
if (data.success) {
|
||||
$("#input").val(data.content);
|
||||
loadData();
|
||||
} else {
|
||||
error.html('<h1>Fetching the log failed!</h1><p>' + data.error + '</p><pre id="rawlog"></pre>');
|
||||
$("#rawlog").text($("#input").val());
|
||||
}
|
||||
$("#uploader").fadeOut();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2002
|
||||
VisualStudioVersion = 15.0.27130.2036
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\StardewModdingAPI.Mods.ConsoleCommands.csproj", "{28480467-1A48-46A7-99F8-236D95225359}"
|
||||
EndProject
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
/// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary>
|
||||
DetectedDynamic,
|
||||
|
||||
/// <summary>The instruction is compatible, but references <see cref="SpecialisedEvents.InvokeUnvalidatedUpdateTick"/> which may impact stability.</summary>
|
||||
/// <summary>The instruction is compatible, but references <see cref="SpecialisedEvents.UnvalidatedUpdateTick"/> which may impact stability.</summary>
|
||||
DetectedUnvalidatedUpdateTick
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue