From 25a9f54ecfdaf6c4ad67dfb46c3f8c556d15d949 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Jan 2022 20:43:45 -0500 Subject: fix manifest JSON schema's update key pattern --- src/SMAPI.Web/wwwroot/schemas/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index b6722347..7457b993 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -103,7 +103,7 @@ "type": "array", "items": { "type": "string", - "pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)(?: *@ *[a-zA-Z0-9_]+ *)$", + "pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)(?: *@ *[a-zA-Z0-9_]+ *)?$", "@errorMessages": { "pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info." } -- cgit From 8429485e696cd4eb9f1f1a6ab50fe2629a1836a0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 27 Feb 2022 20:40:49 -0500 Subject: update schema for Content Patcher 1.25.0 --- docs/release-notes.md | 1 + src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 42 ++++++++++------------ 2 files changed, 20 insertions(+), 23 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/docs/release-notes.md b/docs/release-notes.md index 29aa24c9..d92ef63d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -26,6 +26,7 @@ `IAssetInfo.AssetNameEquals`
`IAssetData.AssetNameEquals` | Use `Name.IsEquivalentTo` instead. * For the web UI: + * Updated the JSON validator/schema for Content Patcher 1.25.0. * Added `data-*` attributes to log parser page for external tools. * Fixed JSON validator warning shown for update keys without a subkey. diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 6b80f260..4975a973 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -14,9 +14,9 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.", "type": "string", - "const": "1.24.0", + "const": "1.25.0", "@errorMessages": { - "const": "Incorrect value '@value'. You should always use the latest format version (currently 1.24.0) to enable the latest features, avoid obsolete behavior, and reduce load times." + "const": "Incorrect value '@value'. You should always use the latest format version (currently 1.25.0) to enable the latest features, avoid obsolete behavior, and reduce load times." } }, "ConfigSchema": { @@ -159,6 +159,14 @@ "additionalProperties": false } }, + "AliasTokenNames": { + "title": "Alias token names", + "description": "Defines optional alternate name for existing tokens. This only affects your content pack, and you can use both the alias name and the original token name. This is mostly useful for custom tokens provided by other mods, which often have longer names. Each entry key is the alias name, and the value is the original token name.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "Changes": { "title": "Changes", "description": "The changes you want to make. Each entry is called a patch, and describes a specific action to perform: replace this file, copy this image into the file, etc. You can list any number of patches.", @@ -187,22 +195,6 @@ "description": "A name for this patch shown in log messages. This is very useful for understanding errors; if not specified, will default to a name like 'entry #14 (EditImage Animals/Dinosaurs)'.", "type": "string" }, - "Enabled": { - "title": "Enabled", - "description": "Whether to apply this patch. Default true. This field does not allow tokens.", - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "enum": [ "true", "false" ] - } - ], - "@errorMessages": { - "anyOf": "Invalid value; must be true, false, or a single token which evaluates to true or false." - } - }, "Update": { "title": "Update", "description": "When the patch should update if it changed. The possible values are 'OnDayStart', 'OnLocationChange', or 'OnTimeChange' (defaults to OnDayStart).", @@ -241,6 +233,14 @@ "description": "The part of the target image to replace. Defaults to the FromArea size starting from the top-left corner.", "$ref": "#/definitions/Rectangle" }, + "TargetField": { + "title": "Target field", + "description": "The path to the field within the value to set as the root scope. See 'target field' in the EditData documentation for more info. This field supports tokens.", + "type": "array", + "items": { + "type": "string" + } + }, "Fields": { "title": "Fields", "description": "The individual fields you want to change for existing entries. This field supports tokens in field keys and values. The key for each field is the field index (starting at zero) for a slash-delimited string, or the field name for an object.", @@ -408,7 +408,6 @@ "propertyNames": { "enum": [ "Action", - "Enabled", "FromFile", "LogName", "Target", @@ -438,7 +437,6 @@ "propertyNames": { "enum": [ "Action", - "Enabled", "FromFile", "LogName", "Target", @@ -462,12 +460,12 @@ "propertyNames": { "enum": [ "Action", - "Enabled", "LogName", "Target", "Update", "When", + "TargetField", "Entries", "Fields", "MoveEntries", @@ -504,7 +502,6 @@ "propertyNames": { "enum": [ "Action", - "Enabled", "FromFile", "LogName", "Target", @@ -533,7 +530,6 @@ "propertyNames": { "enum": [ "Action", - "Enabled", "FromFile", "LogName", "Update", -- cgit From 077d8e4f401ad1806c6af0540f432366314a2300 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 18:25:00 -0400 Subject: remove some unused/redundant code --- src/SMAPI.Installer/InteractiveInstaller.cs | 2 +- src/SMAPI.Internal/ExceptionHelper.cs | 11 +------ .../Framework/DiagnosticVerifier.cs | 2 +- .../NetFieldAnalyzer.cs | 3 -- .../ObsoleteFieldAnalyzer.cs | 2 +- .../Framework/Commands/ConsoleCommand.cs | 2 +- .../Commands/Other/ApplySaveFixCommand.cs | 2 ++ .../Framework/Commands/Other/DebugCommand.cs | 4 ++- .../Framework/Commands/Other/RegenerateBundles.cs | 2 ++ .../Commands/Other/ShowDataFilesCommand.cs | 4 ++- .../Commands/Other/ShowGameFilesCommand.cs | 2 ++ .../Framework/Commands/Other/TestInputCommand.cs | 6 ++-- .../Commands/Player/ListItemTypesCommand.cs | 4 ++- .../Framework/Commands/Player/ListItemsCommand.cs | 2 ++ .../Framework/Commands/Player/SetColorCommand.cs | 2 ++ .../Commands/Player/SetFarmTypeCommand.cs | 2 ++ .../Framework/Commands/Player/SetHealthCommand.cs | 2 ++ .../Commands/Player/SetImmunityCommand.cs | 4 ++- .../Commands/Player/SetMaxHealthCommand.cs | 4 ++- .../Commands/Player/SetMaxStaminaCommand.cs | 4 ++- .../Framework/Commands/Player/SetMoneyCommand.cs | 2 ++ .../Framework/Commands/Player/SetNameCommand.cs | 2 ++ .../Framework/Commands/Player/SetStaminaCommand.cs | 2 ++ .../Framework/Commands/Player/SetStyleCommand.cs | 2 ++ .../Framework/Commands/World/ClearCommand.cs | 12 ++++--- .../Commands/World/DownMineLevelCommand.cs | 2 ++ .../Framework/Commands/World/HurryAllCommand.cs | 2 ++ .../Framework/Commands/World/SetDayCommand.cs | 4 ++- .../Commands/World/SetMineLevelCommand.cs | 2 ++ .../Framework/Commands/World/SetSeasonCommand.cs | 2 ++ .../Framework/Commands/World/SetTimeCommand.cs | 2 ++ .../Framework/Commands/World/SetYearCommand.cs | 4 ++- .../Framework/ItemData/SearchableItem.cs | 10 ------ .../Framework/ItemRepository.cs | 2 +- .../Patches/SaveGamePatcher.cs | 2 +- src/SMAPI.Mods.SaveBackup/ModEntry.cs | 6 ++-- src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs | 4 +-- src/SMAPI.Tests/Core/AssetNameTests.cs | 4 +-- src/SMAPI.Tests/Core/ModResolverTests.cs | 14 ++++---- .../WikiClient/ChangeDescriptorTests.cs | 1 - .../Framework/Clients/Wiki/WikiClient.cs | 2 +- src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs | 2 +- .../Controllers/JsonValidatorController.cs | 11 +++---- .../Framework/Caching/Mods/IModCacheRepository.cs | 1 - .../Caching/Mods/ModCacheMemoryRepository.cs | 1 - src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 2 +- src/SMAPI.Web/Framework/ModInfoModel.cs | 2 -- .../RedirectRules/RedirectHostsToUrlsRule.cs | 2 -- .../Framework/RedirectRules/RedirectToHttpsRule.cs | 2 +- src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 ++-- src/SMAPI.Web/wwwroot/Content/css/file-upload.css | 2 +- src/SMAPI/Constants.cs | 3 -- src/SMAPI/Framework/CommandManager.cs | 2 +- src/SMAPI/Framework/Content/AssetDataForMap.cs | 2 +- src/SMAPI/Framework/Content/AssetName.cs | 2 +- src/SMAPI/Framework/Content/TilesheetReference.cs | 1 - .../ContentManagers/BaseContentManager.cs | 2 +- .../Framework/ContentManagers/ModContentManager.cs | 2 +- src/SMAPI/Framework/Input/SInputState.cs | 2 +- src/SMAPI/Framework/InternalExtensions.cs | 1 - .../Framework/ModHelpers/ModRegistryHelper.cs | 2 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 4 +-- src/SMAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/SMAPI/Framework/ModRegistry.cs | 2 -- src/SMAPI/Framework/Monitor.cs | 2 +- src/SMAPI/Framework/SCore.cs | 16 ++++----- src/SMAPI/Framework/SMultiplayer.cs | 38 +++++++++++----------- .../Framework/Serialization/KeybindConverter.cs | 2 +- .../StateTracking/Snapshots/PlayerSnapshot.cs | 2 +- .../Framework/TemporaryHacks/MiniMonoModHotfix.cs | 2 +- src/SMAPI/Program.cs | 2 +- src/SMAPI/Translation.cs | 2 +- src/SMAPI/Utilities/PerScreen.cs | 2 +- 74 files changed, 143 insertions(+), 134 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 49ca80a5..09183b5d 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -562,7 +562,7 @@ namespace StardewModdingApi.Installer { try { - FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); + FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : new FileInfo(path)); break; } catch (Exception ex) diff --git a/src/SMAPI.Internal/ExceptionHelper.cs b/src/SMAPI.Internal/ExceptionHelper.cs index 03d48911..6bd1d579 100644 --- a/src/SMAPI.Internal/ExceptionHelper.cs +++ b/src/SMAPI.Internal/ExceptionHelper.cs @@ -25,7 +25,7 @@ namespace StardewModdingAPI.Internal case ReflectionTypeLoadException ex: string summary = ex.ToString(); - foreach (Exception childEx in ex.LoaderExceptions ?? Array.Empty()) + foreach (Exception childEx in ex.LoaderExceptions) summary += $"\n\n{childEx?.GetLogSummary()}"; message = summary; break; @@ -43,15 +43,6 @@ namespace StardewModdingAPI.Internal } } - /// Get the lowest exception in an exception stack. - /// The exception from which to search. - public static Exception GetInnermostException(this Exception exception) - { - while (exception.InnerException != null) - exception = exception.InnerException; - return exception; - } - /// Simplify common patterns in exception log messages that don't convey useful info. /// The log message to simplify. public static string SimplifyExtensionMessage(string message) diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs index edaaabd4..49697dfa 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs @@ -201,7 +201,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework var builder = new StringBuilder(); for (int i = 0; i < diagnostics.Length; ++i) { - builder.AppendLine("// " + diagnostics[i].ToString()); + builder.AppendLine("// " + diagnostics[i]); var analyzerType = analyzer.GetType(); var rules = analyzer.SupportedDiagnostics; diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 8478dc54..553aae99 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -227,10 +227,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer // warn: implicit conversion if (this.IsInvalidConversion(memberType.Type, memberType.ConvertedType)) - { context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType)); - return; - } }); } diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index 3184147a..ba089513 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -77,7 +77,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer try { // get reference info - if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) + if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out _, out string memberName)) return; // suggest replacement diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs index 01cab92e..44b7824e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs @@ -100,7 +100,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands List lines = new List(rows.Length + 2) { header, - header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() + header.Select((_, i) => "".PadRight(widths[i], '-')).ToArray() }; lines.AddRange(rows); diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs index 957b0e75..b02ba4b1 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which runs one of the game's save migrations. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class ApplySaveFixCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs index 1955c14e..cf1dcbce 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs @@ -1,8 +1,10 @@ -using StardewValley; +using System.Diagnostics.CodeAnalysis; +using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which sends a debug command to the game. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class DebugCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs index 9beedb96..159d7c4a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Netcode; @@ -9,6 +10,7 @@ using StardewValley.Network; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which regenerates the game's bundles. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class RegenerateBundlesCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs index 27f6ce53..a233d588 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs @@ -1,8 +1,10 @@ -using System.Diagnostics; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which shows the data files. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class ShowDataFilesCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs index b97cb3e6..745b821b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs @@ -1,8 +1,10 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which shows the game files. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class ShowGameFilesCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs index 46583dc3..8bf9f5db 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { /// A command which logs the keys being pressed for 30 seconds once enabled. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class TestInputCommand : ConsoleCommand { /********* @@ -37,9 +39,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other public override void OnUpdated(IMonitor monitor) { // handle expiry - if (this.ExpiryTicks == null) - return; - if (this.ExpiryTicks <= DateTime.UtcNow.Ticks) + if (this.ExpiryTicks != null && this.ExpiryTicks <= DateTime.UtcNow.Ticks) { monitor.Log("No longer logging input.", LogLevel.Info); this.ExpiryTicks = null; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs index af362bcd..ef35ad19 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which list item types. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class ListItemTypesCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index 46fc1d9d..5cc464fe 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which list items available to spawn. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class ListItemsCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index 7b7cbf83..af7f2d18 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the color of a player feature. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetColorCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs index 6fb399ae..2809df9c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text; @@ -10,6 +11,7 @@ using StardewValley.GameData; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which changes the player's farm type. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetFarmTypeCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index f27b336f..f169159f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current health. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetHealthCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs index df90adf2..1065bd21 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current immunity. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetImmunityCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs index a5f7f444..c2c4931d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's maximum health. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetMaxHealthCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs index e3c2f011..8c794e75 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's maximum stamina. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetMaxStaminaCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index 787ce920..3afcc62b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current money. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetMoneyCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs index 4911ad1c..12d6b6e8 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's name. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetNameCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index c78378ef..24718ace 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits the player's current stamina. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetStaminaCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs index 98f6c330..558c327d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which edits a player style. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetStyleCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs index 7935b05f..eeb95553 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewValley; using StardewValley.Locations; @@ -9,6 +10,7 @@ using SObject = StardewValley.Object; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which clears in-game objects. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class ClearCommand : ConsoleCommand { /********* @@ -113,7 +115,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World case "furniture": { - int removed = this.RemoveFurniture(location, furniture => true); + int removed = this.RemoveFurniture(location, _ => true); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } @@ -137,11 +139,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { bool everything = type == "everything"; int removed = - this.RemoveFurniture(location, p => true) - + this.RemoveObjects(location, p => true) - + this.RemoveTerrainFeatures(location, p => true) + this.RemoveFurniture(location, _ => true) + + this.RemoveObjects(location, _ => true) + + this.RemoveTerrainFeatures(location, _ => true) + this.RemoveLargeTerrainFeatures(location, p => everything || p is not Bush bush || bush.isDestroyable(location, p.currentTileLocation)) - + this.RemoveResourceClumps(location, p => true); + + this.RemoveResourceClumps(location, _ => true); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs index 0aa9c9c3..5b1a4a13 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using StardewValley; using StardewValley.Locations; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which moves the player to the next mine level. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class DownMineLevelCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs index 2deac5f8..09531720 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs @@ -1,9 +1,11 @@ using System; +using System.Diagnostics.CodeAnalysis; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which immediately warps all NPCs to their scheduled positions. To hurry a single NPC, see debug hurry npc-name instead. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class HurryAllCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs index 4028b3dc..399fd934 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs @@ -1,10 +1,12 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current day. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetDayCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs index 40f4b19f..f977fce3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs @@ -1,9 +1,11 @@ using System; +using System.Diagnostics.CodeAnalysis; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which moves the player to the given mine level. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetMineLevelCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs index a4cb35bb..d839c037 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Utilities; using StardewValley; @@ -5,6 +6,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current season. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetSeasonCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs index 2d4b4565..8c4458dd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Xna.Framework; using StardewValley; @@ -5,6 +6,7 @@ using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current time. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetTimeCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs index 95401962..a666a634 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs @@ -1,10 +1,12 @@ -using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which sets the current year. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class SetYearCommand : ConsoleCommand { /********* diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs index 72d01eb7..3675a963 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs @@ -43,16 +43,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData this.Item = createItem(this); } - /// Construct an instance. - /// The item metadata to copy. - public SearchableItem(SearchableItem item) - { - this.Type = item.Type; - this.ID = item.ID; - this.CreateItem = item.CreateItem; - this.Item = item.Item; - } - /// Get whether the item name contains a case-insensitive substring. /// The substring to find. public bool NameContains(string substring) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index ca313f41..3915db9a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework foreach (int id in this.TryLoad("Data\\weapons").Keys) { yield return this.TryCreate(ItemType.Weapon, id, p => p.ID is >= 32 and <= 34 - ? (Item)new Slingshot(p.ID) + ? new Slingshot(p.ID) : new MeleeWeapon(p.ID) ); } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs index 5f6dbcb3..01bfb888 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs @@ -77,7 +77,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches 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 _)) + 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); diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index f6925707..273b1434 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Mods.SaveBackup // back up & prune saves Task .Run(() => this.CreateBackup(backupFolder)) - .ContinueWith(backupTask => this.PruneBackups(backupFolder, this.BackupsToKeep)); + .ContinueWith(_ => this.PruneBackups(backupFolder, this.BackupsToKeep)); } catch (Exception ex) { @@ -170,8 +170,8 @@ namespace StardewModdingAPI.Mods.SaveBackup try { // create compressed backup - Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly."); - Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly."); + Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); Type compressionLevelType = coreAssembly.GetType("System.IO.Compression.CompressionLevel") ?? throw new InvalidOperationException("Can't load CompressionLevel type."); Type zipFileType = fsAssembly.GetType("System.IO.Compression.ZipFile") ?? throw new InvalidOperationException("Can't load ZipFile type."); createFromDirectory = zipFileType.GetMethod("CreateFromDirectory", new[] { typeof(string), typeof(string), compressionLevelType, typeof(bool) }) ?? throw new InvalidOperationException("Can't load ZipFile.CreateFromDirectory method."); diff --git a/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs b/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs index 2c7f9952..ac7bd338 100644 --- a/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs +++ b/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs @@ -17,7 +17,7 @@ namespace SMAPI.Tests.ModApiConsumer // act int calls = 0; int lastValue = -1; - api.OnEventRaised += (sender, value) => + api.OnEventRaised += (_, value) => { calls++; lastValue = value; @@ -34,7 +34,7 @@ namespace SMAPI.Tests.ModApiConsumer // act int calls = 0; int lastValue = -1; - api.OnEventRaisedProperty += (sender, value) => + api.OnEventRaisedProperty += (_, value) => { calls++; lastValue = value; diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs index 8785aab8..7b817d13 100644 --- a/src/SMAPI.Tests/Core/AssetNameTests.cs +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -33,7 +33,7 @@ namespace SMAPI.Tests.Core // act string calledWithLocale = null; - IAssetName assetName = AssetName.Parse(name, parseLocale: locale => expectedLanguageCode); + IAssetName assetName = AssetName.Parse(name, parseLocale: _ => expectedLanguageCode); // assert assetName.Name.Should() @@ -161,7 +161,7 @@ namespace SMAPI.Tests.Core AssetName name = AssetName.Parse(mainAssetName, _ => null); // assert value is the same for any combination of options - bool result = name.StartsWith(prefix, true, true); + bool result = name.StartsWith(prefix); foreach (bool allowPartialWord in new[] { true, false }) { foreach (bool allowSubfolder in new[] { true, true }) diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 1755f644..86c50606 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -123,7 +123,7 @@ namespace SMAPI.Tests.Core [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] public void ValidateManifests_NoMods_DoesNothing() { - new ModResolver().ValidateManifests(Array.Empty(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(Array.Empty(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] @@ -134,7 +134,7 @@ namespace SMAPI.Tests.Core mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); @@ -151,7 +151,7 @@ namespace SMAPI.Tests.Core }); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -166,7 +166,7 @@ namespace SMAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -180,7 +180,7 @@ namespace SMAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -197,7 +197,7 @@ namespace SMAPI.Tests.Core this.SetupMetadataForValidation(mod); // act - new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); @@ -223,7 +223,7 @@ namespace SMAPI.Tests.Core mock.Setup(p => p.DirectoryPath).Returns(modFolder); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. diff --git a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs index b896b09c..84cae8df 100644 --- a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs +++ b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using StardewModdingAPI; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index abbcdc81..c936bb3e 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki foreach (var entry in this.ParseOverrideEntries(modNodes)) { - if (entry.Ids?.Any() != true || !entry.HasChanges) + if (entry.Ids.Any() != true || !entry.HasChanges) continue; foreach (string id in entry.Ids) diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs index 5b7e2a02..9bb3f558 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData *********/ /// Construct an empty instance. public ModDatabase() - : this(Array.Empty(), key => null) { } + : this(Array.Empty(), _ => null) { } /// Construct an instance. /// The underlying mod data records indexed by default display name. diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index bcd4097a..985f91ae 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -161,7 +161,7 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", this.GetModel(result.ID, schemaName, isEditView: true).SetContent(input, null).SetUploadError(result.UploadError)); // redirect to view - return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID })); + return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName, id = result.ID })); } @@ -317,13 +317,10 @@ namespace StardewModdingAPI.Web.Controllers /// The case-insensitive field key. private T GetExtensionField(JSchema schema, string key) { - if (schema.ExtensionData != null) + foreach ((string curKey, JToken value) in schema.ExtensionData) { - foreach ((string curKey, JToken value) in schema.ExtensionData) - { - if (curKey.Equals(key, StringComparison.OrdinalIgnoreCase)) - return value.ToObject(); - } + if (curKey.Equals(key, StringComparison.OrdinalIgnoreCase)) + return value.ToObject(); } return default; diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index 0d912c7b..a16e6b73 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -1,6 +1,5 @@ using System; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.Clients; namespace StardewModdingAPI.Web.Framework.Caching.Mods { diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs index 9769793c..f871a9da 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.Clients; namespace StardewModdingAPI.Web.Framework.Caching.Mods { diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 6a3ea222..864caef1 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -211,7 +211,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing { Match match = this.ModPathPattern.Match(message.Text); log.ModPath = match.Groups["path"].Value; - int lastDelimiterPos = log.ModPath.LastIndexOfAny(new char[] { '/', '\\' }); + int lastDelimiterPos = log.ModPath.LastIndexOfAny(new[] { '/', '\\' }); log.GamePath = lastDelimiterPos >= 0 ? log.ModPath.Substring(0, lastDelimiterPos) : log.ModPath; diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs index 7845b8c5..86f70788 100644 --- a/src/SMAPI.Web/Framework/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -1,5 +1,3 @@ -using StardewModdingAPI.Web.Framework.Clients; - namespace StardewModdingAPI.Web.Framework { /// Generic metadata about a mod. diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs index d75ee791..d67b5156 100644 --- a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs @@ -37,8 +37,6 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules { // get requested host string host = context.HttpContext.Request.Host.Host; - if (host == null) - return null; // get new host host = this.Map(host); diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs index 2a503ae3..265a605f 100644 --- a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// Matches requests which should be ignored. public RedirectToHttpsRule(Func except = null) { - this.Except = except ?? (req => false); + this.Except = except ?? (_ => false); this.StatusCode = HttpStatusCode.RedirectKeepVerb; } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index d8561172..6d9591ee 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -81,7 +81,7 @@ namespace StardewModdingAPI.Web // init Hangfire services - .AddHangfire((serv, config) => + .AddHangfire((_, config) => { config .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index b54867b1..3ddc6303 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -40,8 +40,8 @@ smapi.logParser({ logStarted: new Date(@this.ForJson(log?.Timestamp)), showPopup: @this.ForJson(log == null), - showMods: @this.ForJson(log?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true)), - showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false)), + showMods: @this.ForJson(log?.Mods?.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), screenIds: @this.ForJson(screenIds) @@ -207,7 +207,7 @@ else if (log?.IsValid == true) @if (mod.HasUpdate) { - @(mod.Version == null ? @mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}") + @(mod.Version == null ? mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}") } else diff --git a/src/SMAPI.Web/wwwroot/Content/css/file-upload.css b/src/SMAPI.Web/wwwroot/Content/css/file-upload.css index ff170691..f29d46aa 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/file-upload.css +++ b/src/SMAPI.Web/wwwroot/Content/css/file-upload.css @@ -11,7 +11,7 @@ border-radius: 5px; border: 1px solid #000088; outline: none; - box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); + box-shadow: inset 0 0 1px 1px rgba(0, 0, 192, .2); } #submit { diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 76f4ef87..6b9e9b05 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -170,9 +170,6 @@ namespace StardewModdingAPI /// The target game platform as a SMAPI toolkit constant. internal static Platform Platform { get; } = (Platform)Constants.TargetPlatform; - /// The language code for non-translated mod assets. - internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en; - /********* ** Internal methods diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index a7b64cb7..0c0f6685 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Framework /// There's already a command with that name. public CommandManager Add(IInternalCommand command, IMonitor monitor) { - return this.Add(null, command.Name, command.Description, (name, args) => command.HandleCommand(args, monitor)); + return this.Add(null, command.Name, command.Description, (_, args) => command.HandleCommand(args, monitor)); } /// Get a command by its unique name. diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs index 0458f80a..5986e797 100644 --- a/src/SMAPI/Framework/Content/AssetDataForMap.cs +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -147,7 +147,7 @@ namespace StardewModdingAPI.Framework.Content { switch (sourceTile) { - case StaticTile _: + case StaticTile: return new StaticTile(targetLayer, targetSheet, sourceTile.BlendMode, sourceTile.TileIndex); case AnimatedTile animatedTile: diff --git a/src/SMAPI/Framework/Content/AssetName.cs b/src/SMAPI/Framework/Content/AssetName.cs index a1d37b0b..4973b444 100644 --- a/src/SMAPI/Framework/Content/AssetName.cs +++ b/src/SMAPI/Framework/Content/AssetName.cs @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Framework.Content return this.BaseName.Equals(assetName?.BaseName, StringComparison.OrdinalIgnoreCase); if (assetName is AssetName impl) - return this.ComparableName == impl?.ComparableName; + return this.ComparableName == impl.ComparableName; return this.Name.Equals(assetName?.Name, StringComparison.OrdinalIgnoreCase); } diff --git a/src/SMAPI/Framework/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs index 0919bb44..0339b802 100644 --- a/src/SMAPI/Framework/Content/TilesheetReference.cs +++ b/src/SMAPI/Framework/Content/TilesheetReference.cs @@ -1,4 +1,3 @@ -using System.Numerics; using xTile.Dimensions; namespace StardewModdingAPI.Framework.Content diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 31199b3a..c803905a 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -308,7 +308,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { return useCache ? base.LoadBase(assetName.Name) - : base.ReadAsset(assetName.Name, disposable => this.Disposables.Add(new WeakReference(disposable))); + : this.ReadAsset(assetName.Name, disposable => this.Disposables.Add(new WeakReference(disposable))); } /// Add tracking data to an asset and add it to the cache. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 63070d85..e0c85265 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -124,7 +124,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // track & return asset - this.TrackAsset(assetName, asset, useCache); + this.TrackAsset(assetName, asset, useCache: false); return asset; } diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index d5feaf94..72adca02 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Framework.Input var keyboard = new KeyboardStateBuilder(base.GetKeyboardState()); var mouse = new MouseStateBuilder(base.GetMouseState()); Vector2 cursorAbsolutePos = new((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y); - Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; + Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : null; HashSet reallyDown = new HashSet(this.GetPressedButtons(keyboard, mouse, controller)); // apply overrides diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index fe10b045..54aeffd7 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Events; diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 95eb03f3..09a392a6 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get raw API IModMetadata mod = this.Registry.Get(uniqueID); if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID)) - this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace); + this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}."); return mod?.Api; } diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 4480f4e8..24214c96 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -138,7 +138,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (changed) { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace); + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)..."); // load assembly using MemoryStream outAssemblyStream = new(); @@ -150,7 +150,7 @@ namespace StardewModdingAPI.Framework.ModLoading else { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + this.Monitor.Log($" Loading {assembly.File.Name}..."); lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 21366bb4..d52cdbb4 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -237,7 +237,7 @@ namespace StardewModdingAPI.Framework.ModLoading // initialize metadata mods = mods.ToArray(); var sortedMods = new Stack(); - var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued); + var states = mods.ToDictionary(mod => mod, _ => ModDependencyStatus.Queued); // handle failed mods foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed)) diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 99548cf8..c0f8d537 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -96,8 +96,6 @@ namespace StardewModdingAPI.Framework // get stack frames StackTrace stack = new(); StackFrame[] frames = stack.GetFrames(); - if (frames == null) - return null; // search stack for a source assembly foreach (StackFrame frame in frames) diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index ab76e7c0..6b53daff 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -92,7 +92,7 @@ namespace StardewModdingAPI.Framework public void VerboseLog(string message) { if (this.IsVerbose) - this.Log(message, LogLevel.Trace); + this.Log(message); } /// Write a newline to the console and log file. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c8e0842e..cc531de9 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -232,13 +232,13 @@ namespace StardewModdingAPI.Framework this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); // add error handlers - AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + AppDomain.CurrentDomain.UnhandledException += (_, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // add more lenient assembly resolver - AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); + AppDomain.CurrentDomain.AssemblyResolve += (_, e) => AssemblyLoader.ResolveAssembly(e.Name); // hook locale event - LocalizedContentManager.OnLanguageChange += locale => this.OnLocaleChanged(); + LocalizedContentManager.OnLanguageChange += _ => this.OnLocaleChanged(); // override game this.Multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.Reflection, this.OnModMessageReceived, this.Settings.LogNetworkTraffic); @@ -1453,14 +1453,12 @@ namespace StardewModdingAPI.Framework // check Stardew64Installer version if (Constants.IsPatchedByStardew64Installer(out ISemanticVersion patchedByVersion)) { - ISemanticVersion updateFound = null; - string updateUrl = null; try { // fetch update check ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Steviegt6.Stardew64Installer", patchedByVersion, new[] { $"GitHub:{this.Settings.Stardew64InstallerGitHubProjectName}" }) }, apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform).Single().Value; - updateFound = response.SuggestedUpdate?.Version; - updateUrl = response.SuggestedUpdate?.Url ?? Constants.HomePageUrl; + ISemanticVersion updateFound = response.SuggestedUpdate?.Version; + string updateUrl = response.SuggestedUpdate?.Url ?? Constants.HomePageUrl; // log message if (updateFound != null) @@ -1658,8 +1656,8 @@ namespace StardewModdingAPI.Framework // ReSharper restore SuspiciousTypeConversion.Global ContentHelper content = helper.GetLegacyContentHelper(); - content.ObservableAssetEditors.CollectionChanged += (sender, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); - content.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders); + content.ObservableAssetEditors.CollectionChanged += (_, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); + content.ObservableAssetLoaders.CollectionChanged += (_, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders); } // call entry method diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 3981e4e5..bcf97006 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -111,20 +111,20 @@ namespace StardewModdingAPI.Framework { switch (client) { - case LidgrenClient _: + case LidgrenClient: { string address = this.Reflection.GetField(client, "address").GetValue(); return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); } - case GalaxyNetClient _: + case GalaxyNetClient: { GalaxyID address = this.Reflection.GetField(client, "lobbyId").GetValue(); return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); } default: - this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}", LogLevel.Trace); + this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}"); return client; } } @@ -135,20 +135,20 @@ namespace StardewModdingAPI.Framework { switch (server) { - case LidgrenServer _: + case LidgrenServer: { IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage); } - case GalaxyNetServer _: + case GalaxyNetServer: { IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage); } default: - this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}", LogLevel.Trace); + this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}"); return server; } } @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Framework protected void OnClientSendingMessage(OutgoingMessage message, Action sendMessage, Action resume) { if (this.LogNetworkTraffic) - this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}"); switch (message.MessageType) { @@ -184,7 +184,7 @@ namespace StardewModdingAPI.Framework public void OnServerProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) { if (this.LogNetworkTraffic) - this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}"); switch (message.MessageType) { @@ -193,7 +193,7 @@ namespace StardewModdingAPI.Framework { // parse message RemoteContextModel model = this.ReadContext(message.Reader); - this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); + this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}."); // store peer MultiplayerPeer newPeer = new( @@ -243,7 +243,7 @@ namespace StardewModdingAPI.Framework // store peer if new if (!this.Peers.ContainsKey(message.FarmerID)) { - this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}."); MultiplayerPeer peer = new( playerID: message.FarmerID, screenID: this.GetScreenId(message.FarmerID), @@ -280,7 +280,7 @@ namespace StardewModdingAPI.Framework public void OnClientProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) { if (this.LogNetworkTraffic) - this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}"); switch (message.MessageType) { @@ -289,7 +289,7 @@ namespace StardewModdingAPI.Framework { // parse message RemoteContextModel model = this.ReadContext(message.Reader); - this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace); + this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}."); // store peer MultiplayerPeer peer = new( @@ -314,7 +314,7 @@ namespace StardewModdingAPI.Framework // store peer if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) { - this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); + this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}."); var peer = new MultiplayerPeer( playerID: message.FarmerID, screenID: this.GetScreenId(message.FarmerID), @@ -341,7 +341,7 @@ namespace StardewModdingAPI.Framework sendMessage: sendMessage, isHost: this.HostPeer == null ); - this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); + this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}."); this.AddPeer(peer, canBeHost: true); } @@ -367,7 +367,7 @@ namespace StardewModdingAPI.Framework { if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) { - this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); + this.Monitor.Log($"Player quit: {playerID}"); this.Peers.Remove(playerID); this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer)); } @@ -434,7 +434,7 @@ namespace StardewModdingAPI.Framework if (sendToSelf) { if (this.LogNetworkTraffic) - this.Monitor.Log($"Broadcasting '{messageType}' message to self: {data}.", LogLevel.Trace); + this.Monitor.Log($"Broadcasting '{messageType}' message to self: {data}."); this.OnModMessageReceived(model); } @@ -447,7 +447,7 @@ namespace StardewModdingAPI.Framework foreach (MultiplayerPeer peer in sendToPeers) { if (this.LogNetworkTraffic) - this.Monitor.Log($"Broadcasting '{messageType}' message to farmhand {peer.PlayerID}: {data}.", LogLevel.Trace); + this.Monitor.Log($"Broadcasting '{messageType}' message to farmhand {peer.PlayerID}: {data}."); peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data)); } @@ -455,7 +455,7 @@ namespace StardewModdingAPI.Framework else if (this.HostPeer?.HasSmapi == true) { if (this.LogNetworkTraffic) - this.Monitor.Log($"Broadcasting '{messageType}' message to host {this.HostPeer.PlayerID}: {data}.", LogLevel.Trace); + this.Monitor.Log($"Broadcasting '{messageType}' message to host {this.HostPeer.PlayerID}: {data}."); this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data)); } @@ -504,7 +504,7 @@ namespace StardewModdingAPI.Framework ModMessageModel model = this.JsonHelper.Deserialize(json); HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); if (this.LogNetworkTraffic) - this.Monitor.Log($"Received message: {json}.", LogLevel.Trace); + this.Monitor.Log($"Received message: {json}."); // notify local mods if (playerIDs.Contains(Game1.player.UniqueMultiplayerID)) diff --git a/src/SMAPI/Framework/Serialization/KeybindConverter.cs b/src/SMAPI/Framework/Serialization/KeybindConverter.cs index 93a274a8..7c5db3ad 100644 --- a/src/SMAPI/Framework/Serialization/KeybindConverter.cs +++ b/src/SMAPI/Framework/Serialization/KeybindConverter.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.Serialization { case JsonToken.Null: return objectType == typeof(Keybind) - ? (object)new Keybind() + ? new Keybind() : new KeybindList(); case JsonToken.String: diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs index f3e42948..e113d27c 100644 --- a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs +++ b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots Enum .GetValues(typeof(SkillType)) .Cast() - .ToDictionary(skill => skill, skill => new SnapshotDiff()); + .ToDictionary(skill => skill, _ => new SnapshotDiff()); /// Get a list of inventory changes. public SnapshotItemListDiff Inventory { get; private set; } diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs index cab1a94c..c0f119f1 100644 --- a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs +++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs @@ -126,7 +126,7 @@ namespace MonoMod.Utils } public static Type GetRealDeclaringType(this MemberInfo member) - => member.DeclaringType ?? member.Module?.GetModuleType(); + => member.DeclaringType ?? member.Module.GetModuleType(); public static void FixReflectionCache(this Type type) { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 29f4be1b..1039cc9a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -78,7 +78,7 @@ namespace StardewModdingAPI } catch { - continue; + // ignore invalid DLL } } } diff --git a/src/SMAPI/Translation.cs b/src/SMAPI/Translation.cs index 149f6728..5ab432f0 100644 --- a/src/SMAPI/Translation.cs +++ b/src/SMAPI/Translation.cs @@ -74,7 +74,7 @@ namespace StardewModdingAPI { foreach (DictionaryEntry entry in inputLookup) { - string key = entry.Key?.ToString().Trim(); + string key = entry.Key.ToString()?.Trim(); if (key != null) tokenLookup[key] = entry.Value?.ToString(); } diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 20b8fbce..6b7153ac 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -76,7 +76,7 @@ namespace StardewModdingAPI.Utilities /// Remove all active values. public void ResetAllScreens() { - this.RemoveScreens(p => true); + this.RemoveScreens(_ => true); } -- cgit From 0beff189d19416dfcbb64bce800af41de37ccd08 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Fri, 8 Apr 2022 14:59:52 -0400 Subject: Implement client-side log rendering, better filtering, and pagination to improve performance and enhance the user experience with using the log parser. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 143 +++-- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 109 +++- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 632 ++++++++++++++++++++++- 3 files changed, 827 insertions(+), 57 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index c26ec230..39a2da0f 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -20,6 +20,16 @@ .Cast() .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); + IDictionary logLevels = Enum + .GetValues(typeof(LogLevel)) + .Cast() + .ToDictionary(level => (int)level, level => level.ToString().ToLower()); + + IDictionary logSections = Enum + .GetValues(typeof(LogSection)) + .Cast() + .ToDictionary(section => (int)section, section => section.ToString()); + string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true); ISet screenIds = new HashSet(log?.Messages?.Select(p => p.ScreenId) ?? Array.Empty()); @@ -34,8 +44,15 @@ + @if (!Model.ShowRaw) + { + + + + + } - + @@ -275,29 +292,35 @@ else if (log?.IsValid == true) click any mod to filter show all hide all + toggle content packs } @foreach (var mod in log.Mods.Where(p => p.Loaded && !p.IsContentPack)) { + LogModInfo[]? contentPackList; + if (contentPacks == null || !contentPacks.TryGetValue(mod.Name, out contentPackList)) + contentPackList = null; + - - @mod.Name @mod.Version - @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) + + @mod.Name @mod.Version + @if (contentPackList != null) { -
+
@foreach (var contentPack in contentPackList) { + @contentPack.Name @contentPack.Version
}
+ (+ @contentPackList.Length Content Packs) } - + @mod.Author - @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList)) + @if (contentPackList != null) { -
+
@foreach (var contentPack in contentPackList) { + @contentPack.Author
@@ -323,57 +346,67 @@ else if (log?.IsValid == true) @if (!Model.ShowRaw) { +
- Filter messages: - TRACE | - DEBUG | - INFO | - ALERT | - WARN | - ERROR +
+
+ Filter messages: +
+
+ TRACE | + DEBUG | + INFO | + ALERT | + WARN | + ERROR +
+ + .* + aA + Ab + HL +
+ +
+
+
- - @foreach (var message in log.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 + - v-on:click="toggleSection('@message.Section')" } - v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr') @sectionFilter"> - - @if (screenIds.Count > 1) - { - - } - - - - - if (message.Repeated > 0) - { - - - - - } - } -
@message.Timescreen_@message.ScreenId@message.Level.ToString().ToUpper()@message.Mod - @message.Text - @if (message.IsStartOfSection) - { - - - - - } -
repeats [@message.Repeated] times.
+ + + } else { diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 8c3acceb..94bc049b 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -148,6 +148,10 @@ table caption { font-style: italic; } +.content-packs--short { + opacity: 0.75; +} + #metadata td:first-child { padding-right: 5px; } @@ -158,8 +162,58 @@ table caption { #filters { margin: 1em 0 0 0; - padding: 0; + padding: 0 0 0.5em; + display: flex; + justify-content: space-between; + width: calc(100vw - 16em); +} + +#filters > div { + align-self: center; +} + +#filters .toggles { + display: flex; +} + +#filters .toggles > div:first-child { font-weight: bold; + padding: 0.2em 1em 0 0; +} + +#filters .filter-text { + margin-top: 0.5em; +} + +#filters .stats { + margin-top: 0.5em; + font-size: 0.75em; +} + +#filters.sticky { + position: fixed; + top: 0; + left: 0em; + background: #fff; + margin: 0; + padding: 0.5em; + width: calc(100% - 1em); +} + +@media (min-width: 1020px) and (max-width: 1199px) { + #filters:not(.sticky) { + width: calc(100vw - 13em); + } +} + +@media (max-width: 1019px) { + #filters:not(.sticky) { + width: calc(100vw - 3em); + } + + #filters { + display: block; + } } #filters span { @@ -173,6 +227,17 @@ table caption { color: #000; border-color: #880000; background-color: #fcc; + + user-select: none; +} + +#filters .filter-text span { + padding: 3px 0.5em; +} + +#filters .whole-word i { + padding: 0 1px; + border: 1px dashed; } #filters span:hover { @@ -188,11 +253,48 @@ table caption { background: #efe; } +#filters .pager { + margin-top: 0.5em; + text-align: right; +} + +#filters .pager div { + margin-top: 0.5em; +} + +#filters .pager div span { + padding: 0 0.5em; + margin: 0 1px; +} + +#filters .pager span { + background-color: #eee; + border-color: #888; +} + +#filters .pager span.active { + font-weight: bold; + border-color: transparent; + background: transparent; + cursor: unset; +} + +#filters .pager span.disabled { + opacity: 0.3; + cursor: unset; +} + +#filters .pager span:not(.disabled):hover { + background-color: #fff; +} + + /********* ** Log *********/ #log .mod-repeat { font-size: 0.85em; + font-style: italic; } #log .trace { @@ -237,6 +339,11 @@ table caption { white-space: pre-wrap; } +#log .log-message-text strong { + background-color: yellow; + font-weight: normal; +} + #log { border-spacing: 0; } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 90715375..c16b237a 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -2,12 +2,98 @@ var smapi = smapi || {}; var app; +var messages; + + +// Necessary helper method for updating our text filter in a performant way. +// Wouldn't want to update it for every individually typed character. +function debounce(fn, delay) { + var timeoutID = null + return function () { + clearTimeout(timeoutID) + var args = arguments + var that = this + timeoutID = setTimeout(function () { + fn.apply(that, args) + }, delay) + } +} + +// Case insensitive text searching and match word searching is best done in +// regex, so if the user isn't trying to use regex, escape their input. +function escapeRegex(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Use a scroll event to apply a sticky effect to the filters / pagination +// bar. We can't just use "position: sticky" due to how the page is structured +// but this works well enough. +$(function () { + let sticking = false; + + document.addEventListener('scroll', function (event) { + const filters = document.getElementById('filters'); + const holder = document.getElementById('filterHolder'); + if (!filters || !holder) + return; + + const offset = holder.offsetTop; + const should_stick = window.pageYOffset > offset; + if (should_stick === sticking) + return; + + sticking = should_stick; + if (sticking) { + holder.style.marginBottom = `calc(1em + ${filters.offsetHeight}px)`; + filters.classList.add('sticky'); + } else { + filters.classList.remove('sticky'); + holder.style.marginBottom = ''; + } + }); +}); + +// This method is called when we click a log line to toggle the visibility +// of a section. Binding methods is problematic with functional components +// so we just use the `data-section` parameter and our global reference +// to the app. +smapi.clickLogLine = function (event) { + app.toggleSection(event.currentTarget.dataset.section); + event.preventDefault(); + return false; +} + +// And these methods are called when doing pagination. Just makes things +// easier, so may as well use helpers. +smapi.prevPage = function () { + app.prevPage(); +} + +smapi.nextPage = function () { + app.nextPage(); +} + +smapi.changePage = function (event) { + if (typeof event === 'number') + app.changePage(event); + else if (event) { + const page = parseInt(event.currentTarget.dataset.page); + if (!isNaN(page) && isFinite(page)) + app.changePage(page); + } +} + + smapi.logParser = function (data, sectionUrl) { + if (!data) + data = {}; + // internal filter counts var stats = data.stats = { modsShown: 0, modsHidden: 0 }; + function updateModFilters() { // counts stats.modsShown = 0; @@ -22,10 +108,357 @@ smapi.logParser = function (data, sectionUrl) { } } + // load our data + + // Rather than pre-rendering the list elements into the document, we read + // a lot of JSON and use Vue to build the list. This is a lot more + // performant and easier on memory.Our JSON is stored in special script + // tags, that we later remove to let the browser clean up even more memory. + let nodeParsedMessages = document.querySelector('script#parsedMessages'); + if (nodeParsedMessages) { + messages = JSON.parse(nodeParsedMessages.textContent) || []; + const logLevels = JSON.parse(document.querySelector('script#logLevels').textContent) || {}; + const logSections = JSON.parse(document.querySelector('script#logSections').textContent) || {}; + const modSlugs = JSON.parse(document.querySelector('script#modSlugs').textContent) || {}; + + // Remove all references to the script tags and remove them from the + // DOM so that the browser can clean them up. + nodeParsedMessages.remove(); + document.querySelector('script#logLevels').remove(); + document.querySelector('script#logSections').remove(); + document.querySelector('script#modSlugs').remove(); + nodeParsedMessages = null; + + // Pre-process the messages since they aren't quite serialized in + // the format we want. We also want to freeze every last message + // so that Vue won't install its change listening behavior. + for (let i = 0, length = messages.length; i < length; i++) { + const msg = messages[i]; + msg.id = i; + msg.LevelName = logLevels && logLevels[msg.Level]; + msg.SectionName = logSections && logSections[msg.Section]; + msg.ModSlug = modSlugs && modSlugs[msg.Mod] || msg.Mod; + + // For repeated messages, since our component + // can't return two rows, just insert a second message + // which will display as the message repeated notice. + if (msg.Repeated > 0 && ! msg.isRepeated) { + const second = { + id: i + 1, + Level: msg.Level, + Section: msg.Section, + Mod: msg.Mod, + Repeated: msg.Repeated, + isRepeated: true, + }; + + messages.splice(i + 1, 0, second); + length++; + } + + Object.freeze(msg); + } + + Object.freeze(messages); + + } else + messages = []; + // set local time started - if (data) + if (data.logStarted) data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); + // Add some properties to the data we're passing to Vue. + data.totalMessages = messages.length; + + data.filterText = ''; + data.filterRegex = ''; + + data.showContentPacks = true; + data.useHighlight = true; + data.useRegex = false; + data.useInsensitive = true; + data.useWord = false; + + data.perPage = 1000; + data.page = 1; + + // Now load these values. + if (localStorage.settings) { + try { + const saved = JSON.parse(localStorage.settings); + if (saved.hasOwnProperty('showContentPacks')) + data.showContentPacks = saved.showContentPacks; + if (saved.hasOwnProperty('useHighlight')) + dat.useHighlight = saved.useHighlight; + if (saved.hasOwnProperty('useRegex')) + data.useRegex = saved.useRegex; + if (saved.hasOwnProperty('useInsensitive')) + data.useInsensitive = saved.useInsensitive; + if (saved.hasOwnProperty('useWord')) + data.useWord = saved.useWord; + } catch { /* ignore errors */ } + } + + // This would be easier if we could just use JSX but this project doesn't + // have a proper JavaScript build environment and I really don't feel + // like setting one up. + + // Add a number formatter so that our numbers look nicer. + const fmt = window.Intl && Intl.NumberFormat && new Intl.NumberFormat(); + function formatNumber(value) { + if (!fmt || !fmt.format) return `${value}`; + return fmt.format(value); + } + Vue.filter('number', formatNumber); + + // Strictly speaking, we don't need this. However, due to the way our + // Vue template is living in-page the browser is "helpful" and moves + // our s outside of a basic since obviously they + // aren't table rows and don't belong inside a table. By using another + // Vue component, we avoid that. + Vue.component('log-table', { + functional: true, + render: function (createElement, context) { + return createElement('table', { + attrs: { + id: 'log' + } + }, context.children); + } + }); + + // The component draws a nice message under the filters + // telling a user how many messages match their filters, and also expands + // on how many of them they're seeing because of pagination. + Vue.component('filter-stats', { + functional: true, + render: function (createElement, context) { + const props = context.props; + if (props.pages > 1) + return createElement('div', { + class: 'stats' + }, [ + 'showing ', + createElement('strong', formatNumber(props.start + 1)), + ' to ', + createElement('strong', formatNumber(props.end)), + ' of ', + createElement('strong', formatNumber(props.filtered)), + ' (total: ', + createElement('strong', formatNumber(props.total)), + ')' + ]); + + return createElement('div', { + class: 'stats' + }, [ + 'showing ', + createElement('strong', formatNumber(props.filtered)), + ' out of ', + createElement('strong', formatNumber(props.total)) + ]); + } + }); + + // Next up we have which renders the pagination list. This has a + // helper method to make building the list of links easier. + function addPageLink(page, links, visited, createElement, currentPage) { + if (visited.has(page)) + return; + + if (page > 1 && !visited.has(page - 1)) + links.push(' … '); + + visited.add(page); + links.push(createElement('span', { + class: page == currentPage ? 'active' : null, + attrs: { + 'data-page': page + }, + on: { + click: smapi.changePage + } + }, formatNumber(page))); + } + + Vue.component('pager', { + functional: true, + render: function (createElement, context) { + const props = context.props; + if (props.pages <= 1) + return null; + + const visited = new Set; + const pageLinks = []; + + for (let i = 1; i <= 2; i++) + addPageLink(i, pageLinks, visited, createElement, props.page); + + for (let i = props.page - 2; i <= props.page + 2; i++) { + if (i < 1 || i > props.pages) + continue; + + addPageLink(i, pageLinks, visited, createElement, props.page); + } + + for (let i = props.pages - 2; i <= props.pages; i++) { + if (i < 1) + continue; + + addPageLink(i, pageLinks, visited, createElement, props.page); + } + + return createElement('div', { + class: 'pager' + }, [ + createElement('span', { + class: props.page <= 1 ? 'disabled' : null, + on: { + click: smapi.prevPage + } + }, 'Prev'), + ' ', + 'Page ', + formatNumber(props.page), + ' of ', + formatNumber(props.pages), + ' ', + createElement('span', { + class: props.page >= props.pages ? 'disabled' : null, + on: { + click: smapi.nextPage + } + }, 'Next'), + createElement('div', {}, pageLinks) + ]); + } + }); + + // Our functional component draws each log line. + Vue.component('log-line', { + functional: true, + props: { + showScreenId: { + type: Boolean, + required: true + }, + message: { + type: Object, + required: true + }, + sectionExpanded: { + type: Boolean, + required: false + }, + highlight: { + type: Boolean, + required: false + } + }, + render: function (createElement, context) { + const msg = context.props.message; + const level = msg.LevelName; + + if (msg.isRepeated) + return createElement('tr', { + class: [ + "mod", + level, + "mod-repeat" + ] + }, [ + createElement('td', { + attrs: { + colspan: context.props.showScreenId ? 4 : 3 + } + }, ''), + createElement('td', `repeats ${msg.Repeated} times`) + ]); + + const events = {}; + let toggleMessage; + if (msg.IsStartOfSection) { + const visible = context.props.sectionExpanded; + events.click = smapi.clickLogLine; + toggleMessage = visible ? + 'This section is shown. Click here to hide it.' : + 'This section is hidden. Click here to show it.'; + } + + let text = msg.Text; + const filter = window.app && app.filterRegex; + if (text && filter && context.props.highlight) { + text = []; + let match, consumed = 0, idx = 0; + filter.lastIndex = -1; + + // Our logic to highlight the text is a bit funky because we + // want to group consecutive matches to avoid a situation + // where a ton of single characters are in their own elements + // if the user gives us bad input. + + while (match = filter.exec(msg.Text)) { + // Do we have an area of non-matching text? This + // happens if the new match's index is further + // along than the last index. + if (match.index > idx) { + // Alright, do we have a previous match? If + // we do, we need to consume some text. + if (consumed < idx) + text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + + text.push(msg.Text.slice(idx, match.index)); + consumed = match.index; + } + + idx = match.index + match[0].length; + } + + // Add any trailing text after the last match was found. + if (consumed < msg.Text.length) { + if (consumed < idx) + text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + + if (idx < msg.Text.length) + text.push(msg.Text.slice(idx)); + } + } + + return createElement('tr', { + class: [ + "mod", + level, + msg.IsStartOfSection ? "section-start" : null + ], + attrs: { + 'data-section': msg.SectionName + }, + on: events + }, [ + createElement('td', msg.Time), + context.props.showScreenId ? createElement('td', msg.ScreenId) : null, + createElement('td', level.toUpperCase()), + createElement('td', { + attrs: { + 'data-title': msg.Mod + } + }, msg.Mod), + createElement('td', [ + createElement('span', { + class: 'log-message-text' + }, text), + msg.IsStartOfSection ? createElement('span', { + class: 'section-toggle-message' + }, [ + ' ', + toggleMessage + ]) : null + ]) + ]); + } + }); + // init app app = new Vue({ el: '#output', @@ -36,9 +469,114 @@ smapi.logParser = function (data, sectionUrl) { }, anyModsShown: function () { return stats.modsShown > 0; + }, + showScreenId: function () { + return this.screenIds.length > 1; + }, + + // Maybe not strictly necessary, but the Vue template is being + // weird about accessing data entries on the app rather than + // computed properties. + visibleSections: function () { + const ret = []; + for (const [k, v] of Object.entries(this.showSections)) + if (v !== false) + ret.push(k); + return ret; + }, + hideContentPacks: function () { + return !data.showContentPacks; + }, + + // Filter messages for visibility. + filterUseRegex: function () { return data.useRegex; }, + filterInsensitive: function () { return data.useInsensitive; }, + filterUseWord: function () { return data.useWord; }, + shouldHighlight: function () { return data.useHighlight; }, + + filteredMessages: function () { + if (!messages) + return []; + + const start = performance.now(); + const ret = []; + + // This is slightly faster than messages.filter(), which is + // important when working with absolutely huge logs. + for (let i = 0, length = messages.length; i < length; i++) { + const msg = messages[i]; + if (msg.SectionName && !msg.IsStartOfSection && !this.sectionsAllow(msg.SectionName)) + continue; + + if (!this.filtersAllow(msg.ModSlug, msg.LevelName)) + continue; + + let text = msg.Text || (i > 0 ? messages[i - 1].Text : null); + + if (this.filterRegex) { + this.filterRegex.lastIndex = -1; + if (!text || !this.filterRegex.test(text)) + continue; + } else if (this.filterText && (!text || text.indexOf(this.filterText) == -1)) + continue; + + ret.push(msg); + } + + const end = performance.now(); + console.log(`filter took ${end - start}ms`); + + return ret; + }, + + // And the rest are about pagination. + start: function () { + return (this.page - 1) * data.perPage; + }, + end: function () { + return this.start + this.visibleMessages.length; + }, + totalPages: function () { + return Math.ceil(this.filteredMessages.length / data.perPage); + }, + // + visibleMessages: function () { + if (this.totalPages <= 1) + return this.filteredMessages; + + const start = this.start; + const end = start + data.perPage; + + return this.filteredMessages.slice(start, end); } }, + created: function () { + this.loadFromUrl = this.loadFromUrl.bind(this); + window.addEventListener('popstate', this.loadFromUrl); + this.loadFromUrl(); + }, methods: { + // Mostly I wanted people to know they can override the PerPage + // message count with a URL parameter, but we can read Page too. + // In the future maybe we should read *all* filter state so a + // user can link to their exact page state for someone else? + loadFromUrl: function () { + const params = new URL(location).searchParams; + if (params.has('PerPage')) + try { + const perPage = parseInt(params.get('PerPage')); + if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) + data.perPage = perPage; + } catch { /* ignore errors */ } + + if (params.has('Page')) + try { + const page = parseInt(params.get('Page')); + if (!isNaN(page) && isFinite(page) && page > 0) + this.page = page; + } catch { /* ignore errors */ } + }, + toggleLevel: function (id) { if (!data.enableFilters) return; @@ -46,6 +584,98 @@ smapi.logParser = function (data, sectionUrl) { this.showLevels[id] = !this.showLevels[id]; }, + toggleContentPacks: function () { + data.showContentPacks = !data.showContentPacks; + this.saveSettings(); + }, + + toggleFilterUseRegex: function () { + data.useRegex = !data.useRegex; + this.saveSettings(); + this.updateFilterText(); + }, + + toggleFilterInsensitive: function () { + data.useInsensitive = !data.useInsensitive; + this.saveSettings(); + this.updateFilterText(); + }, + + toggleFilterWord: function () { + data.useWord = !data.useWord; + this.saveSettings(); + this.updateFilterText(); + }, + + toggleHighlight: function () { + data.useHighlight = !data.useHighlight; + this.saveSettings(); + }, + + prevPage: function () { + if (this.page <= 1) + return; + this.page--; + this.updateUrl(); + }, + + nextPage: function () { + if (this.page >= this.totalPages) + return; + this.page++; + this.updateUrl(); + }, + + changePage: function (page) { + if (page < 1 || page > this.totalPages) + return; + this.page = page; + this.updateUrl(); + }, + + // Persist settings into localStorage for use the next time + // the user opens a log. + saveSettings: function () { + localStorage.settings = JSON.stringify({ + showContentPacks: data.showContentPacks, + useRegex: data.useRegex, + useInsensitive: data.useInsensitive, + useWord: data.useWord, + useHighlight: data.useHighlight + }); + }, + + // Whenever the page is changed, replace the current page URL. Using + // replaceState rather than pushState to avoid filling the tab history + // with tons of useless history steps the user probably doesn't + // really care about. + updateUrl: function () { + const url = new URL(location); + url.searchParams.set('Page', data.page); + url.searchParams.set('PerPage', data.perPage); + + window.history.replaceState(null, document.title, url.toString()); + }, + + // We don't want to update the filter text often, so use a debounce with + // a quarter second delay. We basically always build a regular expression + // since we use it for highlighting, and it also make case insensitivity + // much easier. + updateFilterText: debounce(function () { + let text = this.filterText = document.querySelector('input[type=text]').value; + if (!text || !text.length) { + this.filterText = ''; + this.filterRegex = null; + } else { + if (!data.useRegex) + text = escapeRegex(text); + this.filterRegex = new RegExp( + data.useWord ? `\\b${text}\\b` : text, + data.useInsensitive ? 'ig' : 'g' + ); + } + }, 250), + toggleMod: function (id) { if (!data.enableFilters) return; -- cgit From 631d0375c3868cb68d1487662955db4ea1b7dd77 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Fri, 8 Apr 2022 15:26:35 -0400 Subject: Simplify visible section checking by abusing Vue behavior, since the proper way is being buggy. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 1 - src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 13 +------------ 2 files changed, 1 insertion(+), 13 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 39a2da0f..8f44b4a2 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -404,7 +404,6 @@ else if (log?.IsValid == true) v-bind:showScreenId="showScreenId" v-bind:message="msg" v-bind:highlight="shouldHighlight" - v-bind:sectionExpanded="msg.SectionName && visibleSections.includes(msg.SectionName)" /> } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index c16b237a..1984d58f 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -347,10 +347,6 @@ smapi.logParser = function (data, sectionUrl) { type: Object, required: true }, - sectionExpanded: { - type: Boolean, - required: false - }, highlight: { type: Boolean, required: false @@ -379,7 +375,7 @@ smapi.logParser = function (data, sectionUrl) { const events = {}; let toggleMessage; if (msg.IsStartOfSection) { - const visible = context.props.sectionExpanded; + const visible = msg.SectionName && window.app && app.sectionsAllow(msg.SectionName); events.click = smapi.clickLogLine; toggleMessage = visible ? 'This section is shown. Click here to hide it.' : @@ -477,13 +473,6 @@ smapi.logParser = function (data, sectionUrl) { // Maybe not strictly necessary, but the Vue template is being // weird about accessing data entries on the app rather than // computed properties. - visibleSections: function () { - const ret = []; - for (const [k, v] of Object.entries(this.showSections)) - if (v !== false) - ret.push(k); - return ret; - }, hideContentPacks: function () { return !data.showContentPacks; }, -- cgit From b3519f3cc161f460e56cfc0a0662ec3b5bfb841b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Apr 2022 12:59:21 -0400 Subject: rename 'data' to 'state' for upcoming changes --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 106 ++++++++++++------------- 1 file changed, 53 insertions(+), 53 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 1984d58f..51d6b53e 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -84,12 +84,12 @@ smapi.changePage = function (event) { } -smapi.logParser = function (data, sectionUrl) { - if (!data) - data = {}; +smapi.logParser = function (state, sectionUrl) { + if (!state) + state = {}; // internal filter counts - var stats = data.stats = { + var stats = state.stats = { modsShown: 0, modsHidden: 0 }; @@ -98,9 +98,9 @@ smapi.logParser = function (data, sectionUrl) { // counts stats.modsShown = 0; stats.modsHidden = 0; - for (var key in data.showMods) { - if (data.showMods.hasOwnProperty(key)) { - if (data.showMods[key]) + for (var key in state.showMods) { + if (state.showMods.hasOwnProperty(key)) { + if (state.showMods[key]) stats.modsShown++; else stats.modsHidden++; @@ -165,38 +165,38 @@ smapi.logParser = function (data, sectionUrl) { messages = []; // set local time started - if (data.logStarted) - data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); + if (state.logStarted) + state.localTimeStarted = ("0" + state.logStarted.getHours()).slice(-2) + ":" + ("0" + state.logStarted.getMinutes()).slice(-2); // Add some properties to the data we're passing to Vue. - data.totalMessages = messages.length; + state.totalMessages = messages.length; - data.filterText = ''; - data.filterRegex = ''; + state.filterText = ''; + state.filterRegex = ''; - data.showContentPacks = true; - data.useHighlight = true; - data.useRegex = false; - data.useInsensitive = true; - data.useWord = false; + state.showContentPacks = true; + state.useHighlight = true; + state.useRegex = false; + state.useInsensitive = true; + state.useWord = false; - data.perPage = 1000; - data.page = 1; + state.perPage = 1000; + state.page = 1; // Now load these values. if (localStorage.settings) { try { const saved = JSON.parse(localStorage.settings); if (saved.hasOwnProperty('showContentPacks')) - data.showContentPacks = saved.showContentPacks; + state.showContentPacks = saved.showContentPacks; if (saved.hasOwnProperty('useHighlight')) dat.useHighlight = saved.useHighlight; if (saved.hasOwnProperty('useRegex')) - data.useRegex = saved.useRegex; + state.useRegex = saved.useRegex; if (saved.hasOwnProperty('useInsensitive')) - data.useInsensitive = saved.useInsensitive; + state.useInsensitive = saved.useInsensitive; if (saved.hasOwnProperty('useWord')) - data.useWord = saved.useWord; + state.useWord = saved.useWord; } catch { /* ignore errors */ } } @@ -458,7 +458,7 @@ smapi.logParser = function (data, sectionUrl) { // init app app = new Vue({ el: '#output', - data: data, + data: state, computed: { anyModsHidden: function () { return stats.modsHidden > 0; @@ -474,14 +474,14 @@ smapi.logParser = function (data, sectionUrl) { // weird about accessing data entries on the app rather than // computed properties. hideContentPacks: function () { - return !data.showContentPacks; + return !state.showContentPacks; }, // Filter messages for visibility. - filterUseRegex: function () { return data.useRegex; }, - filterInsensitive: function () { return data.useInsensitive; }, - filterUseWord: function () { return data.useWord; }, - shouldHighlight: function () { return data.useHighlight; }, + filterUseRegex: function () { return state.useRegex; }, + filterInsensitive: function () { return state.useInsensitive; }, + filterUseWord: function () { return state.useWord; }, + shouldHighlight: function () { return state.useHighlight; }, filteredMessages: function () { if (!messages) @@ -520,13 +520,13 @@ smapi.logParser = function (data, sectionUrl) { // And the rest are about pagination. start: function () { - return (this.page - 1) * data.perPage; + return (this.page - 1) * state.perPage; }, end: function () { return this.start + this.visibleMessages.length; }, totalPages: function () { - return Math.ceil(this.filteredMessages.length / data.perPage); + return Math.ceil(this.filteredMessages.length / state.perPage); }, // visibleMessages: function () { @@ -534,7 +534,7 @@ smapi.logParser = function (data, sectionUrl) { return this.filteredMessages; const start = this.start; - const end = start + data.perPage; + const end = start + state.perPage; return this.filteredMessages.slice(start, end); } @@ -555,7 +555,7 @@ smapi.logParser = function (data, sectionUrl) { try { const perPage = parseInt(params.get('PerPage')); if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) - data.perPage = perPage; + state.perPage = perPage; } catch { /* ignore errors */ } if (params.has('Page')) @@ -567,37 +567,37 @@ smapi.logParser = function (data, sectionUrl) { }, toggleLevel: function (id) { - if (!data.enableFilters) + if (!state.enableFilters) return; this.showLevels[id] = !this.showLevels[id]; }, toggleContentPacks: function () { - data.showContentPacks = !data.showContentPacks; + state.showContentPacks = !state.showContentPacks; this.saveSettings(); }, toggleFilterUseRegex: function () { - data.useRegex = !data.useRegex; + state.useRegex = !state.useRegex; this.saveSettings(); this.updateFilterText(); }, toggleFilterInsensitive: function () { - data.useInsensitive = !data.useInsensitive; + state.useInsensitive = !state.useInsensitive; this.saveSettings(); this.updateFilterText(); }, toggleFilterWord: function () { - data.useWord = !data.useWord; + state.useWord = !state.useWord; this.saveSettings(); this.updateFilterText(); }, toggleHighlight: function () { - data.useHighlight = !data.useHighlight; + state.useHighlight = !state.useHighlight; this.saveSettings(); }, @@ -626,11 +626,11 @@ smapi.logParser = function (data, sectionUrl) { // the user opens a log. saveSettings: function () { localStorage.settings = JSON.stringify({ - showContentPacks: data.showContentPacks, - useRegex: data.useRegex, - useInsensitive: data.useInsensitive, - useWord: data.useWord, - useHighlight: data.useHighlight + showContentPacks: state.showContentPacks, + useRegex: state.useRegex, + useInsensitive: state.useInsensitive, + useWord: state.useWord, + useHighlight: state.useHighlight }); }, @@ -640,8 +640,8 @@ smapi.logParser = function (data, sectionUrl) { // really care about. updateUrl: function () { const url = new URL(location); - url.searchParams.set('Page', data.page); - url.searchParams.set('PerPage', data.perPage); + url.searchParams.set('Page', state.page); + url.searchParams.set('PerPage', state.perPage); window.history.replaceState(null, document.title, url.toString()); }, @@ -656,17 +656,17 @@ smapi.logParser = function (data, sectionUrl) { this.filterText = ''; this.filterRegex = null; } else { - if (!data.useRegex) + if (!state.useRegex) text = escapeRegex(text); this.filterRegex = new RegExp( - data.useWord ? `\\b${text}\\b` : text, - data.useInsensitive ? 'ig' : 'g' + state.useWord ? `\\b${text}\\b` : text, + state.useInsensitive ? 'ig' : 'g' ); } }, 250), toggleMod: function (id) { - if (!data.enableFilters) + if (!state.enableFilters) return; var curShown = this.showMods[id]; @@ -689,14 +689,14 @@ smapi.logParser = function (data, sectionUrl) { }, toggleSection: function (name) { - if (!data.enableFilters) + if (!state.enableFilters) return; this.showSections[name] = !this.showSections[name]; }, showAllMods: function () { - if (!data.enableFilters) + if (!state.enableFilters) return; for (var key in this.showMods) { @@ -708,7 +708,7 @@ smapi.logParser = function (data, sectionUrl) { }, hideAllMods: function () { - if (!data.enableFilters) + if (!state.enableFilters) return; for (var key in this.showMods) { -- cgit From 260dbbf205bfa86c76a999ccd00e01941e9ce469 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Apr 2022 13:02:25 -0400 Subject: standardize quote style --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 192 ++++++++++++------------- 1 file changed, 96 insertions(+), 96 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 51d6b53e..9684834c 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -22,7 +22,7 @@ function debounce(fn, delay) { // Case insensitive text searching and match word searching is best done in // regex, so if the user isn't trying to use regex, escape their input. function escapeRegex(text) { - return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // Use a scroll event to apply a sticky effect to the filters / pagination @@ -31,9 +31,9 @@ function escapeRegex(text) { $(function () { let sticking = false; - document.addEventListener('scroll', function (event) { - const filters = document.getElementById('filters'); - const holder = document.getElementById('filterHolder'); + document.addEventListener("scroll", function (event) { + const filters = document.getElementById("filters"); + const holder = document.getElementById("filterHolder"); if (!filters || !holder) return; @@ -45,10 +45,10 @@ $(function () { sticking = should_stick; if (sticking) { holder.style.marginBottom = `calc(1em + ${filters.offsetHeight}px)`; - filters.classList.add('sticky'); + filters.classList.add("sticky"); } else { - filters.classList.remove('sticky'); - holder.style.marginBottom = ''; + filters.classList.remove("sticky"); + holder.style.marginBottom = ""; } }); }); @@ -74,7 +74,7 @@ smapi.nextPage = function () { } smapi.changePage = function (event) { - if (typeof event === 'number') + if (typeof event === "number") app.changePage(event); else if (event) { const page = parseInt(event.currentTarget.dataset.page); @@ -114,19 +114,19 @@ smapi.logParser = function (state, sectionUrl) { // a lot of JSON and use Vue to build the list. This is a lot more // performant and easier on memory.Our JSON is stored in special script // tags, that we later remove to let the browser clean up even more memory. - let nodeParsedMessages = document.querySelector('script#parsedMessages'); + let nodeParsedMessages = document.querySelector("script#parsedMessages"); if (nodeParsedMessages) { messages = JSON.parse(nodeParsedMessages.textContent) || []; - const logLevels = JSON.parse(document.querySelector('script#logLevels').textContent) || {}; - const logSections = JSON.parse(document.querySelector('script#logSections').textContent) || {}; - const modSlugs = JSON.parse(document.querySelector('script#modSlugs').textContent) || {}; + const logLevels = JSON.parse(document.querySelector("script#logLevels").textContent) || {}; + const logSections = JSON.parse(document.querySelector("script#logSections").textContent) || {}; + const modSlugs = JSON.parse(document.querySelector("script#modSlugs").textContent) || {}; // Remove all references to the script tags and remove them from the // DOM so that the browser can clean them up. nodeParsedMessages.remove(); - document.querySelector('script#logLevels').remove(); - document.querySelector('script#logSections').remove(); - document.querySelector('script#modSlugs').remove(); + document.querySelector("script#logLevels").remove(); + document.querySelector("script#logSections").remove(); + document.querySelector("script#modSlugs").remove(); nodeParsedMessages = null; // Pre-process the messages since they aren't quite serialized in @@ -171,8 +171,8 @@ smapi.logParser = function (state, sectionUrl) { // Add some properties to the data we're passing to Vue. state.totalMessages = messages.length; - state.filterText = ''; - state.filterRegex = ''; + state.filterText = ""; + state.filterRegex = ""; state.showContentPacks = true; state.useHighlight = true; @@ -187,15 +187,15 @@ smapi.logParser = function (state, sectionUrl) { if (localStorage.settings) { try { const saved = JSON.parse(localStorage.settings); - if (saved.hasOwnProperty('showContentPacks')) + if (saved.hasOwnProperty("showContentPacks")) state.showContentPacks = saved.showContentPacks; - if (saved.hasOwnProperty('useHighlight')) + if (saved.hasOwnProperty("useHighlight")) dat.useHighlight = saved.useHighlight; - if (saved.hasOwnProperty('useRegex')) + if (saved.hasOwnProperty("useRegex")) state.useRegex = saved.useRegex; - if (saved.hasOwnProperty('useInsensitive')) + if (saved.hasOwnProperty("useInsensitive")) state.useInsensitive = saved.useInsensitive; - if (saved.hasOwnProperty('useWord')) + if (saved.hasOwnProperty("useWord")) state.useWord = saved.useWord; } catch { /* ignore errors */ } } @@ -210,19 +210,19 @@ smapi.logParser = function (state, sectionUrl) { if (!fmt || !fmt.format) return `${value}`; return fmt.format(value); } - Vue.filter('number', formatNumber); + Vue.filter("number", formatNumber); // Strictly speaking, we don't need this. However, due to the way our // Vue template is living in-page the browser is "helpful" and moves // our s outside of a basic
since obviously they // aren't table rows and don't belong inside a table. By using another // Vue component, we avoid that. - Vue.component('log-table', { + Vue.component("log-table", { functional: true, render: function (createElement, context) { - return createElement('table', { + return createElement("table", { attrs: { - id: 'log' + id: "log" } }, context.children); } @@ -231,32 +231,32 @@ smapi.logParser = function (state, sectionUrl) { // The component draws a nice message under the filters // telling a user how many messages match their filters, and also expands // on how many of them they're seeing because of pagination. - Vue.component('filter-stats', { + Vue.component("filter-stats", { functional: true, render: function (createElement, context) { const props = context.props; if (props.pages > 1) - return createElement('div', { - class: 'stats' + return createElement("div", { + class: "stats" }, [ - 'showing ', - createElement('strong', formatNumber(props.start + 1)), - ' to ', - createElement('strong', formatNumber(props.end)), - ' of ', - createElement('strong', formatNumber(props.filtered)), - ' (total: ', - createElement('strong', formatNumber(props.total)), - ')' + "showing ", + createElement("strong", formatNumber(props.start + 1)), + " to ", + createElement("strong", formatNumber(props.end)), + " of ", + createElement("strong", formatNumber(props.filtered)), + " (total: ", + createElement("strong", formatNumber(props.total)), + ")" ]); - return createElement('div', { - class: 'stats' + return createElement("div", { + class: "stats" }, [ - 'showing ', - createElement('strong', formatNumber(props.filtered)), - ' out of ', - createElement('strong', formatNumber(props.total)) + "showing ", + createElement("strong", formatNumber(props.filtered)), + " out of ", + createElement("strong", formatNumber(props.total)) ]); } }); @@ -268,13 +268,13 @@ smapi.logParser = function (state, sectionUrl) { return; if (page > 1 && !visited.has(page - 1)) - links.push(' … '); + links.push(" … "); visited.add(page); - links.push(createElement('span', { - class: page == currentPage ? 'active' : null, + links.push(createElement("span", { + class: page == currentPage ? "active" : null, attrs: { - 'data-page': page + "data-page": page }, on: { click: smapi.changePage @@ -282,7 +282,7 @@ smapi.logParser = function (state, sectionUrl) { }, formatNumber(page))); } - Vue.component('pager', { + Vue.component("pager", { functional: true, render: function (createElement, context) { const props = context.props; @@ -309,34 +309,34 @@ smapi.logParser = function (state, sectionUrl) { addPageLink(i, pageLinks, visited, createElement, props.page); } - return createElement('div', { - class: 'pager' + return createElement("div", { + class: "pager" }, [ - createElement('span', { - class: props.page <= 1 ? 'disabled' : null, + createElement("span", { + class: props.page <= 1 ? "disabled" : null, on: { click: smapi.prevPage } - }, 'Prev'), - ' ', - 'Page ', + }, "Prev"), + " ", + "Page ", formatNumber(props.page), - ' of ', + " of ", formatNumber(props.pages), - ' ', - createElement('span', { - class: props.page >= props.pages ? 'disabled' : null, + " ", + createElement("span", { + class: props.page >= props.pages ? "disabled" : null, on: { click: smapi.nextPage } - }, 'Next'), - createElement('div', {}, pageLinks) + }, "Next"), + createElement("div", {}, pageLinks) ]); } }); // Our functional component draws each log line. - Vue.component('log-line', { + Vue.component("log-line", { functional: true, props: { showScreenId: { @@ -357,19 +357,19 @@ smapi.logParser = function (state, sectionUrl) { const level = msg.LevelName; if (msg.isRepeated) - return createElement('tr', { + return createElement("tr", { class: [ "mod", level, "mod-repeat" ] }, [ - createElement('td', { + createElement("td", { attrs: { colspan: context.props.showScreenId ? 4 : 3 } - }, ''), - createElement('td', `repeats ${msg.Repeated} times`) + }, ""), + createElement("td", `repeats ${msg.Repeated} times`) ]); const events = {}; @@ -377,9 +377,9 @@ smapi.logParser = function (state, sectionUrl) { if (msg.IsStartOfSection) { const visible = msg.SectionName && window.app && app.sectionsAllow(msg.SectionName); events.click = smapi.clickLogLine; - toggleMessage = visible ? - 'This section is shown. Click here to hide it.' : - 'This section is hidden. Click here to show it.'; + toggleMessage = visible + ? "This section is shown. Click here to hide it." + : "This section is hidden. Click here to show it."; } let text = msg.Text; @@ -402,7 +402,7 @@ smapi.logParser = function (state, sectionUrl) { // Alright, do we have a previous match? If // we do, we need to consume some text. if (consumed < idx) - text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + text.push(createElement("strong", {}, msg.Text.slice(consumed, idx))); text.push(msg.Text.slice(idx, match.index)); consumed = match.index; @@ -414,40 +414,40 @@ smapi.logParser = function (state, sectionUrl) { // Add any trailing text after the last match was found. if (consumed < msg.Text.length) { if (consumed < idx) - text.push(createElement('strong', {}, msg.Text.slice(consumed, idx))); + text.push(createElement("strong", {}, msg.Text.slice(consumed, idx))); if (idx < msg.Text.length) text.push(msg.Text.slice(idx)); } } - return createElement('tr', { + return createElement("tr", { class: [ "mod", level, msg.IsStartOfSection ? "section-start" : null ], attrs: { - 'data-section': msg.SectionName + "data-section": msg.SectionName }, on: events }, [ - createElement('td', msg.Time), - context.props.showScreenId ? createElement('td', msg.ScreenId) : null, - createElement('td', level.toUpperCase()), - createElement('td', { + createElement("td", msg.Time), + context.props.showScreenId ? createElement("td", msg.ScreenId) : null, + createElement("td", level.toUpperCase()), + createElement("td", { attrs: { - 'data-title': msg.Mod + "data-title": msg.Mod } }, msg.Mod), - createElement('td', [ - createElement('span', { - class: 'log-message-text' + createElement("td", [ + createElement("span", { + class: "log-message-text" }, text), - msg.IsStartOfSection ? createElement('span', { - class: 'section-toggle-message' + msg.IsStartOfSection ? createElement("span", { + class: "section-toggle-message" }, [ - ' ', + " ", toggleMessage ]) : null ]) @@ -457,7 +457,7 @@ smapi.logParser = function (state, sectionUrl) { // init app app = new Vue({ - el: '#output', + el: "#output", data: state, computed: { anyModsHidden: function () { @@ -541,7 +541,7 @@ smapi.logParser = function (state, sectionUrl) { }, created: function () { this.loadFromUrl = this.loadFromUrl.bind(this); - window.addEventListener('popstate', this.loadFromUrl); + window.addEventListener("popstate", this.loadFromUrl); this.loadFromUrl(); }, methods: { @@ -551,16 +551,16 @@ smapi.logParser = function (state, sectionUrl) { // user can link to their exact page state for someone else? loadFromUrl: function () { const params = new URL(location).searchParams; - if (params.has('PerPage')) + if (params.has("PerPage")) try { - const perPage = parseInt(params.get('PerPage')); + const perPage = parseInt(params.get("PerPage")); if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) state.perPage = perPage; } catch { /* ignore errors */ } - if (params.has('Page')) + if (params.has("Page")) try { - const page = parseInt(params.get('Page')); + const page = parseInt(params.get("Page")); if (!isNaN(page) && isFinite(page) && page > 0) this.page = page; } catch { /* ignore errors */ } @@ -640,8 +640,8 @@ smapi.logParser = function (state, sectionUrl) { // really care about. updateUrl: function () { const url = new URL(location); - url.searchParams.set('Page', state.page); - url.searchParams.set('PerPage', state.perPage); + url.searchParams.set("Page", state.page); + url.searchParams.set("PerPage", state.perPage); window.history.replaceState(null, document.title, url.toString()); }, @@ -651,16 +651,16 @@ smapi.logParser = function (state, sectionUrl) { // since we use it for highlighting, and it also make case insensitivity // much easier. updateFilterText: debounce(function () { - let text = this.filterText = document.querySelector('input[type=text]').value; + let text = this.filterText = document.querySelector("input[type=text]").value; if (!text || !text.length) { - this.filterText = ''; + this.filterText = ""; this.filterRegex = null; } else { if (!state.useRegex) text = escapeRegex(text); this.filterRegex = new RegExp( state.useWord ? `\\b${text}\\b` : text, - state.useInsensitive ? 'ig' : 'g' + state.useInsensitive ? "ig" : "g" ); } }, 250), -- cgit From ccf760452d64e1965c92c5cf8af399a5e80d5a3a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Apr 2022 13:08:38 -0400 Subject: pass data directly to script instead of serializing & deserializing it --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 43 ++++++---- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 113 +++++++++++-------------- 2 files changed, 76 insertions(+), 80 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 8f44b4a2..ff8aa003 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -44,13 +44,6 @@ - @if (!Model.ShowRaw) - { - - - - - } @@ -58,15 +51,33 @@ + + + } @@ -378,6 +383,7 @@ else if (log?.IsValid == true) @@ -390,7 +396,7 @@ else if (log?.IsValid == true) v-bind:start="start" v-bind:end="end" v-bind:pages="totalPages" - v-bind:filtered="filteredMessages.length" + v-bind:filtered="filteredMessages.total" v-bind:total="totalMessages" /> diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 59c6026c..69f0a46d 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -94,7 +94,93 @@ smapi.logParser = function (state) { return formatter && formatter.format ? formatter.format(value) : `${value}`; + }, + + /** + * Convert an array of boolean values into a bitmap. + * @param {Boolean[]} value An array of boolean values + * @returns {BigInt} + */ + toBitmap(value) { + let result = BigInt(0); + if (!Array.isArray(value)) + return value ? BigInt(1) : BigInt(0); + + for (let i = 0; i < value.length; i++) { + if (value[i]) + result += BigInt(2) ** BigInt(value.length - i - 1); + } + + return result; + }, + + /** + * Convert a bitmap into an array of boolean values. + * @param {BigInt} value The bitmap + * @param {Number} length The expected length of the result + * @returns {Boolean[]} + */ + fromBitmap(value, length = -1) { + if (typeof value != "bigint") + value = ""; + else + value = value.toString(2); + + const result = []; + while (length > value.length) { + result.push(false); + length--; + } + + for (let i = 0; i < value.length; i++) { + result.push(value[i] === "1" ? true : false); + } + + return result; + }, + + b64ToBigInt(value) { + const bin = atob(value); + const hex = []; + + for (let i = 0; i < bin.length; i++) { + let h = bin.charCodeAt(i).toString(16); + if (h.length % 2) h = `0${h}`; + hex.push(h); + } + + return BigInt(`0x${hex.join('')}`); + }, + + bigIntTo64(value) { + let hex = value.toString(16); + if (hex.length % 2) hex = `0${hex}`; + + const result = []; + for (let i = 0; i < hex.length; i += 2) { + const val = parseInt(hex.slice(i, i + 2), 16); + result.push(String.fromCharCode(val)); + } + + return btoa(result.join('')); + }, + + b64ToUrl(value) { + return value.replace(/\//g, '_').replace(/=/g, '-').replace(/\+/g, '.'); + }, + + urlTob64(value) { + return value.replace(/_/g, '/').replace(/-/g, '=').replace(/\./g, '+'); + }, + + toUrlBitmap(value) { + return helpers.b64ToUrl(helpers.bigIntTo64(helpers.toBitmap(value))); + }, + + fromUrlBitmap(value, length = -1) { + return helpers.fromBitmap(helpers.b64ToBigInt(helpers.urlTob64(value)), length); } + }; // internal event handlers @@ -272,32 +358,19 @@ smapi.logParser = function (state) { functional: true, render: function (createElement, context) { const props = context.props; - if (props.pages > 1) { - return createElement( - "div", - { class: "stats" }, - [ - "showing ", - createElement("strong", helpers.formatNumber(props.start + 1)), - " to ", - createElement("strong", helpers.formatNumber(props.end)), - " of ", - createElement("strong", helpers.formatNumber(props.filtered)), - " (total: ", - createElement("strong", helpers.formatNumber(props.total)), - ")" - ] - ); - } - return createElement( "div", { class: "stats" }, [ "showing ", + createElement("strong", helpers.formatNumber(props.start + 1)), + " to ", + createElement("strong", helpers.formatNumber(props.end)), + " of ", createElement("strong", helpers.formatNumber(props.filtered)), - " out of ", - createElement("strong", helpers.formatNumber(props.total)) + " (total: ", + createElement("strong", helpers.formatNumber(props.total)), + ")" ] ); } @@ -578,34 +651,39 @@ smapi.logParser = function (state) { if (!state.messages) return []; - const start = performance.now(); + //const start = performance.now(); const filtered = []; + let total = 0; + // This is slightly faster than messages.filter(), which is // important when working with absolutely huge logs. for (let i = 0, length = state.messages.length; i < length; i++) { const msg = state.messages[i]; - if (msg.SectionName && !msg.IsStartOfSection && !this.sectionsAllow(msg.SectionName)) - continue; - if (!this.filtersAllow(msg.ModSlug, msg.LevelName)) continue; - const text = msg.Text || (i > 0 ? state.messages[i - 1].Text : null); - if (this.filterRegex) { + const text = msg.Text || (i > 0 ? state.messages[i - 1].Text : null); this.filterRegex.lastIndex = -1; if (!text || !this.filterRegex.test(text)) continue; } - else if (this.filterText && (!text || text.indexOf(this.filterText) === -1)) + + total++; + + if (msg.SectionName && !msg.IsStartOfSection && !this.sectionsAllow(msg.SectionName)) continue; filtered.push(msg); } - const end = performance.now(); - //console.log(`applied ${(this.filterRegex ? "regex" : "text")} filter '${this.filterRegex || this.filterText}' in ${end - start}ms`); + filtered.total = total; + + Object.freeze(filtered); + + //const end = performance.now(); + //console.log(`applied ${(this.useRegex ? "regex" : "text")} filter '${this.filterRegex}' in ${end - start}ms`); return filtered; }, @@ -636,10 +714,6 @@ smapi.logParser = function (state) { this.loadFromUrl(); }, methods: { - // Mostly I wanted people to know they can override the PerPage - // message count with a URL parameter, but we can read Page too. - // In the future maybe we should read *all* filter state so a - // user can link to their exact page state for someone else? loadFromUrl: function () { const params = new URL(location).searchParams; if (params.has("PerPage")) { @@ -653,6 +727,78 @@ smapi.logParser = function (state) { if (!isNaN(page) && isFinite(page) && page > 0) this.page = page; } + + let updateFilter = false; + + if (params.has("Filter")) { + state.filterText = params.get("Filter"); + updateFilter = true; + } + + if (params.has("FilterMode")) { + const values = helpers.fromUrlBitmap(params.get("FilterMode"), 3); + state.useRegex = values[0]; + state.useInsensitive = values[1]; + state.useWord = values[2]; + updateFilter = true; + } + + if (params.has("Mods")) { + const keys = Object.keys(this.showMods); + const value = params.get("Mods"); + const values = value === "all" ? true : value === "none" ? false : helpers.fromUrlBitmap(value, keys.length); + + for (let i = 0; i < keys.length; i++) { + this.showMods[keys[i]] = Array.isArray(values) ? values[i] : values; + } + + updateModFilters(); + } + + if (params.has("Levels")) { + const keys = Object.keys(this.showLevels); + const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + + for (let i = 0; i < keys.length; i++) { + this.showLevels[keys[i]] = values[i]; + } + } + + if (params.has("Sections")) { + const keys = Object.keys(this.showSections); + const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + + for (let i = 0; i < keys.length; i++) { + this.showSections[keys[i]] = values[i]; + } + } + + if (updateFilter) + this.updateFilterText(); + }, + + // Whenever the page state changed, replace the current page URL. Using + // replaceState rather than pushState to avoid filling the tab history + // with tons of useless history steps the user probably doesn't + // really care about. + updateUrl: function () { + const url = new URL(location); + url.searchParams.set("Page", state.page); + url.searchParams.set("PerPage", state.perPage); + + url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); + url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); + url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + + if (state.filterText && state.filterText.length) { + url.searchParams.set("Filter", state.filterText); + url.searchParams.set("FilterMode", helpers.toUrlBitmap([state.useRegex, state.useInsensitive, state.useWord])); + } else { + url.searchParams.delete("Filter"); + url.searchParams.delete("FilterMode"); + } + + window.history.replaceState(null, document.title, url.toString()); }, toggleLevel: function (id) { @@ -660,6 +806,7 @@ smapi.logParser = function (state) { return; this.showLevels[id] = !this.showLevels[id]; + this.updateUrl(); }, toggleContentPacks: function () { @@ -723,25 +870,13 @@ smapi.logParser = function (state) { }); }, - // Whenever the page is changed, replace the current page URL. Using - // replaceState rather than pushState to avoid filling the tab history - // with tons of useless history steps the user probably doesn't - // really care about. - updateUrl: function () { - const url = new URL(location); - url.searchParams.set("Page", state.page); - url.searchParams.set("PerPage", state.perPage); - - window.history.replaceState(null, document.title, url.toString()); - }, - // We don't want to update the filter text often, so use a debounce with // a quarter second delay. We basically always build a regular expression // since we use it for highlighting, and it also make case insensitivity // much easier. updateFilterText: helpers.getDebouncedHandler( function () { - let text = this.filterText = document.querySelector("input[type=text]").value; + let text = state.filterText; if (!text || !text.length) { this.filterText = ""; this.filterRegex = null; @@ -754,6 +889,8 @@ smapi.logParser = function (state) { state.useInsensitive ? "ig" : "g" ); } + + this.updateUrl(); }, 250 ), @@ -779,6 +916,7 @@ smapi.logParser = function (state) { this.showMods[id] = !this.showMods[id]; updateModFilters(); + this.updateUrl(); }, toggleSection: function (name) { @@ -786,6 +924,7 @@ smapi.logParser = function (state) { return; this.showSections[name] = !this.showSections[name]; + this.updateUrl(); }, showAllMods: function () { @@ -798,6 +937,7 @@ smapi.logParser = function (state) { } } updateModFilters(); + this.updateUrl(); }, hideAllMods: function () { @@ -810,6 +950,7 @@ smapi.logParser = function (state) { } } updateModFilters(); + this.updateUrl(); }, filtersAllow: function (modId, level) { -- cgit From 94b8507a4763020c578e98ecf5af645fe6583cee Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Mon, 11 Apr 2022 15:01:59 -0400 Subject: Add more documentation strings. Use shallow equality checking to decide whether to include a filter in the URL or not to avoid unnecessarily large URLs. --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 90 ++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 69f0a46d..8538423f 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -139,6 +139,11 @@ smapi.logParser = function (state) { return result; }, + /** + * Convert a base-64 string to a BigInt. + * @param {string} value + * @returns {BigInt} + */ b64ToBigInt(value) { const bin = atob(value); const hex = []; @@ -152,6 +157,11 @@ smapi.logParser = function (state) { return BigInt(`0x${hex.join('')}`); }, + /** + * Convert a BigInt to a base-64 string. + * @param {BigInt} value + * @returns {string} + */ bigIntTo64(value) { let hex = value.toString(16); if (hex.length % 2) hex = `0${hex}`; @@ -165,22 +175,79 @@ smapi.logParser = function (state) { return btoa(result.join('')); }, + /** + * Make a base-64 string URL safe. + * @param {string} value + * @returns {string} + */ b64ToUrl(value) { return value.replace(/\//g, '_').replace(/=/g, '-').replace(/\+/g, '.'); }, + /** + * Convert a URL safe base-64 string back to normal. + * @param {string} value + * @returns {string} + */ urlTob64(value) { return value.replace(/_/g, '/').replace(/-/g, '=').replace(/\./g, '+'); }, + /** + * Convert an array of booleans to a BigInt bitmap, then convert that + * to a base-64 string, then make it URL safe. + * @param {Boolean[]} value + * @returns {string} + */ toUrlBitmap(value) { return helpers.b64ToUrl(helpers.bigIntTo64(helpers.toBitmap(value))); }, + /** + * Convert a URL safe base-64 string to a normal base-64 string, convert + * that to a BigInt, and then parse a bitmap from the BigInt. + * @param {string} value + * @param {Number} length The expected length of the bitmap. + */ fromUrlBitmap(value, length = -1) { return helpers.fromBitmap(helpers.b64ToBigInt(helpers.urlTob64(value)), length); - } + }, + /** + * Check the shallow equality of two objects. + * @param {Array} first + * @param {Array} second + * @returns {Boolean} + */ + shallowEquals(first, second) { + if (typeof first !== "object" || typeof second !== "object") + return first === second; + + if (first == null || second == null) + return first == null && second == null; + + const f_array = Array.isArray(first); + const s_array = Array.isArray(second); + + if (f_array !== s_array) + return false; + + const f_keys = Object.keys(first); + const s_keys = Object.keys(second); + + if (f_keys.length != s_keys.length) + return false; + + for (const key of f_keys) { + if (!s_keys.includes(key)) + return false; + + if (first[key] !== second[key]) + return false; + } + + return true; + } }; // internal event handlers @@ -312,6 +379,10 @@ smapi.logParser = function (state) { state.perPage = 1000; state.page = 1; + state.defaultMods = JSON.parse(JSON.stringify(state.showMods)); + state.defaultSections = JSON.parse(JSON.stringify(state.showSections)); + state.defaultLevels = JSON.parse(JSON.stringify(state.showLevels)); + // load saved values, if any if (localStorage.settings) { try { @@ -786,9 +857,20 @@ smapi.logParser = function (state) { url.searchParams.set("Page", state.page); url.searchParams.set("PerPage", state.perPage); - url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); - url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); - url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + if (!helpers.shallowEquals(this.showMods, state.defaultMods)) + url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); + else + url.searchParams.delete("Mods"); + + if (!helpers.shallowEquals(this.showLevels, state.defaultLevels)) + url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); + else + url.searchParams.delete("Levels"); + + if (!helpers.shallowEquals(this.showSections, state.defaultSections)) + url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + else + url.searchParams.delete("Sections"); if (state.filterText && state.filterText.length) { url.searchParams.set("Filter", state.filterText); -- cgit From a21d24f4b7d14701205a6805422de31da84da6ca Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Tue, 12 Apr 2022 02:07:21 -0400 Subject: Replace bitfields for state and just use comma-separated strings. Add a note that numbers may be inaccurate if filtering is used when sections are collapsed. Add quick navigation links. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 13 ++ src/SMAPI.Web/wwwroot/Content/css/main.css | 32 ++++ src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 235 +++++++++---------------- 3 files changed, 124 insertions(+), 156 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 7aa0fd6b..d95499b7 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -94,6 +94,19 @@ } +@* quick navigation links *@ +@if (log != null) +{ + +} + @* upload result banner *@ @if (Model.UploadError != null) { diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index dcc7a798..52b304d0 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -72,6 +72,7 @@ a { color: #666; } +#quickNav h4, #sidebar h4 { margin: 1.5em 0 0.2em 0; width: 10em; @@ -80,11 +81,13 @@ a { font-weight: normal; } +#quickNav a, #sidebar a { color: #77B; border: 0; } +#quickNav ul, #quickNav li, #sidebar ul, #sidebar li { margin: 0; padding: 0; @@ -93,10 +96,29 @@ a { color: #888; } +#quickNav li, #sidebar li { margin-left: 1em; } +/* quick navigation */ + +#quickNav { + position: fixed; + left: 8px; + bottom: 3em; + width: 12em; + color: #666; +} + +@media (max-height: 400px) { + #quickNav { + position: unset; + width: auto; + } +} + + /* footer */ #footer { margin: 1em; @@ -111,11 +133,16 @@ a { /* mobile fixes */ @media (min-width: 1020px) and (max-width: 1199px) { + #quickNav, #sidebar { width: 7em; background: none; } + #quickNav h4 { + width: unset; + } + #content-column { left: 7em; } @@ -138,4 +165,9 @@ a { top: inherit; left: inherit; } + + #quickNav { + position: unset; + width: auto; + } } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 8538423f..37c57082 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -97,120 +97,23 @@ smapi.logParser = function (state) { }, /** - * Convert an array of boolean values into a bitmap. - * @param {Boolean[]} value An array of boolean values - * @returns {BigInt} + * Try parsing the value as an integer, in base 10. Return the number + * if it's valid, or return the default value otherwise. + * @param {String} value The value to parse. + * @param {Number} defaultValue The value to return if parsing fails. + * @param {Function} critera An optional criteria to check the number with. */ - toBitmap(value) { - let result = BigInt(0); - if (!Array.isArray(value)) - return value ? BigInt(1) : BigInt(0); - - for (let i = 0; i < value.length; i++) { - if (value[i]) - result += BigInt(2) ** BigInt(value.length - i - 1); + tryNumber(value, defaultValue, critera = null) { + try { + value = parseInt(value, 10); + } catch { + return defaultValue; } - return result; - }, - - /** - * Convert a bitmap into an array of boolean values. - * @param {BigInt} value The bitmap - * @param {Number} length The expected length of the result - * @returns {Boolean[]} - */ - fromBitmap(value, length = -1) { - if (typeof value != "bigint") - value = ""; - else - value = value.toString(2); - - const result = []; - while (length > value.length) { - result.push(false); - length--; - } - - for (let i = 0; i < value.length; i++) { - result.push(value[i] === "1" ? true : false); - } - - return result; - }, - - /** - * Convert a base-64 string to a BigInt. - * @param {string} value - * @returns {BigInt} - */ - b64ToBigInt(value) { - const bin = atob(value); - const hex = []; - - for (let i = 0; i < bin.length; i++) { - let h = bin.charCodeAt(i).toString(16); - if (h.length % 2) h = `0${h}`; - hex.push(h); - } - - return BigInt(`0x${hex.join('')}`); - }, - - /** - * Convert a BigInt to a base-64 string. - * @param {BigInt} value - * @returns {string} - */ - bigIntTo64(value) { - let hex = value.toString(16); - if (hex.length % 2) hex = `0${hex}`; - - const result = []; - for (let i = 0; i < hex.length; i += 2) { - const val = parseInt(hex.slice(i, i + 2), 16); - result.push(String.fromCharCode(val)); - } - - return btoa(result.join('')); - }, - - /** - * Make a base-64 string URL safe. - * @param {string} value - * @returns {string} - */ - b64ToUrl(value) { - return value.replace(/\//g, '_').replace(/=/g, '-').replace(/\+/g, '.'); - }, - - /** - * Convert a URL safe base-64 string back to normal. - * @param {string} value - * @returns {string} - */ - urlTob64(value) { - return value.replace(/_/g, '/').replace(/-/g, '=').replace(/\./g, '+'); - }, + if (isNaN(value) || !isFinite(value) || (critera && !critera(value))) + return defaultValue; - /** - * Convert an array of booleans to a BigInt bitmap, then convert that - * to a base-64 string, then make it URL safe. - * @param {Boolean[]} value - * @returns {string} - */ - toUrlBitmap(value) { - return helpers.b64ToUrl(helpers.bigIntTo64(helpers.toBitmap(value))); - }, - - /** - * Convert a URL safe base-64 string to a normal base-64 string, convert - * that to a BigInt, and then parse a bitmap from the BigInt. - * @param {string} value - * @param {Number} length The expected length of the bitmap. - */ - fromUrlBitmap(value, length = -1) { - return helpers.fromBitmap(helpers.b64ToBigInt(helpers.urlTob64(value)), length); + return value; }, /** @@ -433,12 +336,18 @@ smapi.logParser = function (state) { "div", { class: "stats" }, [ - "showing ", - createElement("strong", helpers.formatNumber(props.start + 1)), - " to ", - createElement("strong", helpers.formatNumber(props.end)), - " of ", - createElement("strong", helpers.formatNumber(props.filtered)), + createElement('abbr', { + attrs: { + title: "These numbers may be inaccurate when using filtering with sections collapsed." + } + }, [ + "showing ", + createElement("strong", helpers.formatNumber(props.start + 1)), + " to ", + createElement("strong", helpers.formatNumber(props.end)), + " of ", + createElement("strong", helpers.formatNumber(props.filtered)) + ]), " (total: ", createElement("strong", helpers.formatNumber(props.total)), ")" @@ -788,64 +697,66 @@ smapi.logParser = function (state) { loadFromUrl: function () { const params = new URL(location).searchParams; if (params.has("PerPage")) { - const perPage = parseInt(params.get("PerPage")); - if (!isNaN(perPage) && isFinite(perPage) && perPage > 0) - state.perPage = perPage; + state.perPage = helpers.tryNumber(params.get("PerPage"), 1000, n => n > 0); + } else { + state.perPage = 1000; } if (params.has("Page")) { - const page = parseInt(params.get("Page")); - if (!isNaN(page) && isFinite(page) && page > 0) - this.page = page; + this.page = helpers.tryNumber(params.get("Page"), 1, n => n > 0); + } else { + this.page = 1; } - let updateFilter = false; - - if (params.has("Filter")) { + if (params.has("Filter")) state.filterText = params.get("Filter"); - updateFilter = true; - } + else + state.filterText = ""; if (params.has("FilterMode")) { - const values = helpers.fromUrlBitmap(params.get("FilterMode"), 3); - state.useRegex = values[0]; - state.useInsensitive = values[1]; - state.useWord = values[2]; - updateFilter = true; + const values = params.get("FilterMode").split("~"); + state.useRegex = values.includes('Regex'); + state.useInsensitive = !values.includes('Sensitive'); + state.useWord = values.includes('Word'); + } else { + state.useRegex = false; + state.useInsensitive = true; + state.useWord = false; } if (params.has("Mods")) { - const keys = Object.keys(this.showMods); - const value = params.get("Mods"); - const values = value === "all" ? true : value === "none" ? false : helpers.fromUrlBitmap(value, keys.length); - - for (let i = 0; i < keys.length; i++) { - this.showMods[keys[i]] = Array.isArray(values) ? values[i] : values; - } + const value = params.get("Mods").split("~"); + for (const key of Object.keys(this.showMods)) + this.showMods[key] = value.includes(key); - updateModFilters(); + } else { + for (const key of Object.keys(this.showMods)) + this.showMods[key] = state.defaultMods[key]; } if (params.has("Levels")) { - const keys = Object.keys(this.showLevels); - const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + const values = params.get("Levels").split("~"); + for (const key of Object.keys(this.showLevels)) + this.showLevels[key] = values.includes(key); - for (let i = 0; i < keys.length; i++) { - this.showLevels[keys[i]] = values[i]; - } + } else { + const keys = Object.keys(this.showLevels); + for (const key of Object.keys(this.showLevels)) + this.showLevels[key] = state.defaultLevels[key]; } if (params.has("Sections")) { - const keys = Object.keys(this.showSections); - const values = helpers.fromUrlBitmap(params.get("Levels"), keys.length); + const values = params.get("Sections").split("~"); + for (const key of Object.keys(this.showSections)) + this.showSections[key] = values.includes(key); - for (let i = 0; i < keys.length; i++) { - this.showSections[keys[i]] = values[i]; - } + } else { + for (const key of Object.keys(this.showSections)) + this.showSections[key] = state.defaultSections[key]; } - if (updateFilter) - this.updateFilterText(); + updateModFilters(); + this.updateFilterText(); }, // Whenever the page state changed, replace the current page URL. Using @@ -858,23 +769,35 @@ smapi.logParser = function (state) { url.searchParams.set("PerPage", state.perPage); if (!helpers.shallowEquals(this.showMods, state.defaultMods)) - url.searchParams.set("Mods", stats.modsHidden == 0 ? "all" : stats.modsShown == 0 ? "none" : helpers.toUrlBitmap(Object.values(this.showMods))); + url.searchParams.set("Mods", Object.entries(this.showMods).filter(x => x[1]).map(x => x[0]).join("~")); else url.searchParams.delete("Mods"); if (!helpers.shallowEquals(this.showLevels, state.defaultLevels)) - url.searchParams.set("Levels", helpers.toUrlBitmap(Object.values(this.showLevels))); + url.searchParams.set("Levels", Object.entries(this.showLevels).filter(x => x[1]).map(x => x[0]).join("~")); else url.searchParams.delete("Levels"); if (!helpers.shallowEquals(this.showSections, state.defaultSections)) - url.searchParams.set("Sections", helpers.toUrlBitmap(Object.values(this.showSections))); + url.searchParams.set("Sections", Object.entries(this.showSections).filter(x => x[1]).map(x => x[0]).join("~")); else url.searchParams.delete("Sections"); if (state.filterText && state.filterText.length) { url.searchParams.set("Filter", state.filterText); - url.searchParams.set("FilterMode", helpers.toUrlBitmap([state.useRegex, state.useInsensitive, state.useWord])); + const modes = []; + if (state.useRegex) + modes.push("Regex"); + if (!state.useInsensitive) + modes.push("Sensitive"); + if (state.useWord) + modes.push("Word"); + + if (modes.length) + url.searchParams.set("FilterMode", modes.join("~")); + else + url.searchParams.delete("FilterMode"); + } else { url.searchParams.delete("Filter"); url.searchParams.delete("FilterMode"); -- cgit From 0b9227564979b3e6e71dbd48ced2a9b7407fd640 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Tue, 12 Apr 2022 02:18:51 -0400 Subject: Make horizontal scrolling with the quick navigation links less bad. Probably need to move them into the actual sidebar element though for proper sorting. --- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 41b54e11..e47a938d 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -13,6 +13,9 @@ caption { #output { padding: 10px; overflow: auto; + z-index: 1; + background: #fff; + position: relative; } #output h2 { -- cgit From 4f54f517ce3d6fd6e87cfee6b0ce61346d62c3e3 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Tue, 12 Apr 2022 13:50:51 -0400 Subject: Use an optional section for rendering quick navigation links on the mod viewer, containing them within the #sidebar element. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 22 ++++++++++++---------- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 2 ++ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 3 --- src/SMAPI.Web/wwwroot/Content/css/main.css | 6 ------ 4 files changed, 14 insertions(+), 19 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d95499b7..d55bfd4d 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -95,16 +95,18 @@ } @* quick navigation links *@ -@if (log != null) -{ - +@section SidebarExtra { + @if (log != null) + { + + } } @* upload result banner *@ diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 7c86a68c..248cc7ef 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -30,6 +30,8 @@
  • Log parser
  • JSON validator
  • + + @RenderSection("SidebarExtra", required: false)
    diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index e47a938d..41b54e11 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -13,9 +13,6 @@ caption { #output { padding: 10px; overflow: auto; - z-index: 1; - background: #fff; - position: relative; } #output h2 { diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index 52b304d0..a0a407d8 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -72,7 +72,6 @@ a { color: #666; } -#quickNav h4, #sidebar h4 { margin: 1.5em 0 0.2em 0; width: 10em; @@ -81,13 +80,11 @@ a { font-weight: normal; } -#quickNav a, #sidebar a { color: #77B; border: 0; } -#quickNav ul, #quickNav li, #sidebar ul, #sidebar li { margin: 0; padding: 0; @@ -96,7 +93,6 @@ a { color: #888; } -#quickNav li, #sidebar li { margin-left: 1em; } @@ -105,10 +101,8 @@ a { #quickNav { position: fixed; - left: 8px; bottom: 3em; width: 12em; - color: #666; } @media (max-height: 400px) { -- cgit From 64b9da560fcde5696b27126adbc45e8331f1cc70 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Apr 2022 12:56:38 -0400 Subject: minor refactoring & code style --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 8 +- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 192 ++++++++++++------------- 2 files changed, 93 insertions(+), 107 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d55bfd4d..dbaa14e0 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -98,14 +98,14 @@ @section SidebarExtra { @if (log != null) { - + } } diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 37c57082..c730309b 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -97,55 +97,43 @@ smapi.logParser = function (state) { }, /** - * Try parsing the value as an integer, in base 10. Return the number - * if it's valid, or return the default value otherwise. - * @param {String} value The value to parse. - * @param {Number} defaultValue The value to return if parsing fails. - * @param {Function} critera An optional criteria to check the number with. + * Try parsing the value as a base-10 integer. + * @param {string} value The value to parse. + * @param {number} defaultValue The value to return if parsing fails. + * @param {() => boolean} criteria An optional callback to check whether a parsed number is valid. + * @returns {number} The parsed number if it's valid, else the default value. */ - tryNumber(value, defaultValue, critera = null) { - try { - value = parseInt(value, 10); - } catch { - return defaultValue; - } - - if (isNaN(value) || !isFinite(value) || (critera && !critera(value))) - return defaultValue; - - return value; + tryParseNumber(value, defaultValue, criteria = null) { + value = parseInt(value, 10); + return !isNaN(value) && isFinite(value) && (!criteria || criteria(value)) + ? value + : defaultValue; }, /** - * Check the shallow equality of two objects. - * @param {Array} first - * @param {Array} second + * Get whether two objects are equivalent based on their top-level properties. + * @param {Object} left The first value to compare. + * @param {Object} right The second value to compare. * @returns {Boolean} */ - shallowEquals(first, second) { - if (typeof first !== "object" || typeof second !== "object") - return first === second; + shallowEquals(left, right) { + if (typeof left !== "object" || typeof right !== "object") + return left === right; - if (first == null || second == null) - return first == null && second == null; + if (left == null || right == null) + return left == null && right == null; - const f_array = Array.isArray(first); - const s_array = Array.isArray(second); - - if (f_array !== s_array) + if (Array.isArray(left) !== Array.isArray(right)) return false; - const f_keys = Object.keys(first); - const s_keys = Object.keys(second); + const leftKeys = Object.keys(left); + const rightKeys = Object.keys(right); - if (f_keys.length != s_keys.length) + if (leftKeys.length != rightKeys.length) return false; - for (const key of f_keys) { - if (!s_keys.includes(key)) - return false; - - if (first[key] !== second[key]) + for (const key of leftKeys) { + if (!rightKeys.includes(key) || left[key] !== right[key]) return false; } @@ -204,20 +192,6 @@ smapi.logParser = function (state) { modsHidden: 0 }; - function updateModFilters() { - // counts - stats.modsShown = 0; - stats.modsHidden = 0; - for (let key in state.showMods) { - if (state.showMods.hasOwnProperty(key)) { - if (state.showMods[key]) - stats.modsShown++; - else - stats.modsHidden++; - } - } - } - // load raw log data { const dataElement = document.querySelector(state.dataElement); @@ -282,9 +256,9 @@ smapi.logParser = function (state) { state.perPage = 1000; state.page = 1; - state.defaultMods = JSON.parse(JSON.stringify(state.showMods)); - state.defaultSections = JSON.parse(JSON.stringify(state.showSections)); - state.defaultLevels = JSON.parse(JSON.stringify(state.showLevels)); + state.defaultMods = { ...state.showMods }; + state.defaultSections = { ...state.showSections }; + state.defaultLevels = { ...state.showLevels }; // load saved values, if any if (localStorage.settings) { @@ -336,18 +310,22 @@ smapi.logParser = function (state) { "div", { class: "stats" }, [ - createElement('abbr', { - attrs: { - title: "These numbers may be inaccurate when using filtering with sections collapsed." - } - }, [ - "showing ", - createElement("strong", helpers.formatNumber(props.start + 1)), - " to ", - createElement("strong", helpers.formatNumber(props.end)), - " of ", - createElement("strong", helpers.formatNumber(props.filtered)) - ]), + createElement( + "abbr", + { + attrs: { + title: "These numbers may be inaccurate when using filtering with sections collapsed." + } + }, + [ + "showing ", + createElement("strong", helpers.formatNumber(props.start + 1)), + " to ", + createElement("strong", helpers.formatNumber(props.end)), + " of ", + createElement("strong", helpers.formatNumber(props.filtered)) + ] + ), " (total: ", createElement("strong", helpers.formatNumber(props.total)), ")" @@ -696,29 +674,18 @@ smapi.logParser = function (state) { methods: { loadFromUrl: function () { const params = new URL(location).searchParams; - if (params.has("PerPage")) { - state.perPage = helpers.tryNumber(params.get("PerPage"), 1000, n => n > 0); - } else { - state.perPage = 1000; - } - - if (params.has("Page")) { - this.page = helpers.tryNumber(params.get("Page"), 1, n => n > 0); - } else { - this.page = 1; - } - if (params.has("Filter")) - state.filterText = params.get("Filter"); - else - state.filterText = ""; + state.perPage = helpers.tryParseNumber(params.get("PerPage"), 1000, n => n > 0); + this.page = helpers.tryParseNumber(params.get("Page"), 1, n => n > 0); + state.filterText = params.get("Filter") || ""; if (params.has("FilterMode")) { const values = params.get("FilterMode").split("~"); - state.useRegex = values.includes('Regex'); - state.useInsensitive = !values.includes('Sensitive'); - state.useWord = values.includes('Word'); - } else { + state.useRegex = values.includes("Regex"); + state.useInsensitive = !values.includes("Sensitive"); + state.useWord = values.includes("Word"); + } + else { state.useRegex = false; state.useInsensitive = true; state.useWord = false; @@ -729,7 +696,8 @@ smapi.logParser = function (state) { for (const key of Object.keys(this.showMods)) this.showMods[key] = value.includes(key); - } else { + } + else { for (const key of Object.keys(this.showMods)) this.showMods[key] = state.defaultMods[key]; } @@ -739,7 +707,8 @@ smapi.logParser = function (state) { for (const key of Object.keys(this.showLevels)) this.showLevels[key] = values.includes(key); - } else { + } + else { const keys = Object.keys(this.showLevels); for (const key of Object.keys(this.showLevels)) this.showLevels[key] = state.defaultLevels[key]; @@ -750,41 +719,42 @@ smapi.logParser = function (state) { for (const key of Object.keys(this.showSections)) this.showSections[key] = values.includes(key); - } else { + } + else { for (const key of Object.keys(this.showSections)) this.showSections[key] = state.defaultSections[key]; } - updateModFilters(); + this.updateModFilters(); this.updateFilterText(); }, - // Whenever the page state changed, replace the current page URL. Using - // replaceState rather than pushState to avoid filling the tab history - // with tons of useless history steps the user probably doesn't - // really care about. + /** + * Update the page URL to track non-default filter values. + */ updateUrl: function () { const url = new URL(location); url.searchParams.set("Page", state.page); url.searchParams.set("PerPage", state.perPage); if (!helpers.shallowEquals(this.showMods, state.defaultMods)) - url.searchParams.set("Mods", Object.entries(this.showMods).filter(x => x[1]).map(x => x[0]).join("~")); + url.searchParams.set("Mods", Object.entries(this.showMods).filter(p => p[1]).map(p => p[0]).join("~")); else url.searchParams.delete("Mods"); if (!helpers.shallowEquals(this.showLevels, state.defaultLevels)) - url.searchParams.set("Levels", Object.entries(this.showLevels).filter(x => x[1]).map(x => x[0]).join("~")); + url.searchParams.set("Levels", Object.entries(this.showLevels).filter(p => p[1]).map(p => p[0]).join("~")); else url.searchParams.delete("Levels"); if (!helpers.shallowEquals(this.showSections, state.defaultSections)) - url.searchParams.set("Sections", Object.entries(this.showSections).filter(x => x[1]).map(x => x[0]).join("~")); + url.searchParams.set("Sections", Object.entries(this.showSections).filter(p => p[1]).map(p => p[0]).join("~")); else url.searchParams.delete("Sections"); - if (state.filterText && state.filterText.length) { + if (state.filterText?.length) { url.searchParams.set("Filter", state.filterText); + const modes = []; if (state.useRegex) modes.push("Regex"); @@ -798,12 +768,13 @@ smapi.logParser = function (state) { else url.searchParams.delete("FilterMode"); - } else { + } + else { url.searchParams.delete("Filter"); url.searchParams.delete("FilterMode"); } - window.history.replaceState(null, document.title, url.toString()); + window.history.replaceState(null, document.title, url.toString()); // use replaceState instead of pushState to avoid filling the tab history with history steps the user probably doesn't care about }, toggleLevel: function (id) { @@ -863,8 +834,9 @@ smapi.logParser = function (state) { this.updateUrl(); }, - // Persist settings into localStorage for use the next time - // the user opens a log. + /** + * Persist settings into localStorage for use the next time the user opens a log. + */ saveSettings: function () { localStorage.settings = JSON.stringify({ showContentPacks: state.showContentPacks, @@ -900,6 +872,20 @@ smapi.logParser = function (state) { 250 ), + updateModFilters: function () { + // counts + stats.modsShown = 0; + stats.modsHidden = 0; + for (let key in state.showMods) { + if (state.showMods.hasOwnProperty(key)) { + if (state.showMods[key]) + stats.modsShown++; + else + stats.modsHidden++; + } + } + }, + toggleMod: function (id) { if (!state.enableFilters) return; @@ -920,7 +906,7 @@ smapi.logParser = function (state) { else this.showMods[id] = !this.showMods[id]; - updateModFilters(); + this.updateModFilters(); this.updateUrl(); }, @@ -941,7 +927,7 @@ smapi.logParser = function (state) { this.showMods[key] = true; } } - updateModFilters(); + this.updateModFilters(); this.updateUrl(); }, @@ -954,7 +940,7 @@ smapi.logParser = function (state) { this.showMods[key] = false; } } - updateModFilters(); + this.updateModFilters(); this.updateUrl(); }, -- cgit From 446205c7bd342422f4b4f14d6c8f36dc0b6cf468 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Sat, 16 Apr 2022 13:55:54 -0400 Subject: Add regex error checking, and display a message to the user when their regular expression has a syntax error. Additionally, use a non-capturing group to surround the user input when `Match whole word` is enabled in case alternates are being used. Finally, add a safety check to highlighting to avoid an infinite loop when zero-length matches happen. --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 +++++ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 7 ++++- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 33 ++++++++++++++++++++---- 3 files changed, 40 insertions(+), 6 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index dfb603f2..5e55906d 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -406,6 +406,12 @@ else if (log?.IsValid == true) aA “ ” HL +
    + {{ filterError }} +
    Date: Sat, 16 Apr 2022 21:07:34 -0400 Subject: tweak code style --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 27 +++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index e19e3301..8886715e 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -860,34 +860,29 @@ smapi.logParser = function (state) { // much easier. updateFilterText: helpers.getDebouncedHandler( function () { + // reset + this.filterError = null; + this.filterRegex = null; + + // apply search let text = state.filterText; - if (!text || !text.length) { + if (!text) this.filterText = ""; - this.filterRegex = null; - this.filterError = null; - } else { if (!state.useRegex) text = helpers.escapeRegex(text); const flags = state.useInsensitive ? "ig" : "g"; - this.filterError = null; - let regex; - try { - regex = new RegExp(text, flags); - } catch (err) { - regex = null; + this.filterRegex = new RegExp(text, flags); + } + catch (err) { this.filterError = err.message; } - if (regex) - this.filterRegex = state.useWord ? - new RegExp(`\\b(?:${text})\\b`, flags) : - regex; - else - this.filterRegex = null; + if (this.filterRegex && state.useWord) + this.filterRegex = new RegExp(`\\b(?:${text})\\b`, flags); } this.updateUrl(); -- cgit From d4d378bff3a8079e6d665b8c99d9a80937f588cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Apr 2022 19:25:10 -0400 Subject: don't add default pagination values to log URL --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 8886715e..3fb5fd6c 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -245,6 +245,7 @@ smapi.logParser = function (state) { state.localTimeStarted = ("0" + state.logStarted.getHours()).slice(-2) + ":" + ("0" + state.logStarted.getMinutes()).slice(-2); // add the properties we're passing to Vue + const defaultPerPage = 1000; state.totalMessages = state.messages.length; state.filterText = ""; state.filterRegex = null; @@ -254,7 +255,7 @@ smapi.logParser = function (state) { state.useRegex = false; state.useInsensitive = true; state.useWord = false; - state.perPage = 1000; + state.perPage = defaultPerPage; state.page = 1; state.defaultMods = { ...state.showMods }; @@ -682,7 +683,7 @@ smapi.logParser = function (state) { loadFromUrl: function () { const params = new URL(location).searchParams; - state.perPage = helpers.tryParseNumber(params.get("PerPage"), 1000, n => n > 0); + state.perPage = helpers.tryParseNumber(params.get("PerPage"), defaultPerPage, n => n > 0); this.page = helpers.tryParseNumber(params.get("Page"), 1, n => n > 0); state.filterText = params.get("Filter") || ""; @@ -741,8 +742,12 @@ smapi.logParser = function (state) { */ updateUrl: function () { const url = new URL(location); - url.searchParams.set("Page", state.page); - url.searchParams.set("PerPage", state.perPage); + + if (state.page != 1) + url.searchParams.set("Page", state.page); + + if (state.perPage != defaultPerPage) + url.searchParams.set("PerPage", state.perPage); if (!helpers.shallowEquals(this.showMods, state.defaultMods)) url.searchParams.set("Mods", Object.entries(this.showMods).filter(p => p[1]).map(p => p[0]).join("~")); -- cgit From 4fa414c2bd5ddb452c0560d00e0d4f1d383c1d8b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Apr 2022 20:59:35 -0400 Subject: set page/perPage URL args together Since there's no UI to set the page size, this makes the argument more discoverable. --- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src/SMAPI.Web/wwwroot') diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 3fb5fd6c..fccd00be 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -743,11 +743,14 @@ smapi.logParser = function (state) { updateUrl: function () { const url = new URL(location); - if (state.page != 1) + if (state.page != 1 || state.perPage != defaultPerPage) { url.searchParams.set("Page", state.page); - - if (state.perPage != defaultPerPage) url.searchParams.set("PerPage", state.perPage); + } + else { + url.searchParams.delete("Page"); + url.searchParams.delete("PerPage"); + } if (!helpers.shallowEquals(this.showMods, state.defaultMods)) url.searchParams.set("Mods", Object.entries(this.showMods).filter(p => p[1]).map(p => p[0]).join("~")); -- cgit