summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/CommandManager.cs13
-rw-r--r--src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs168
-rw-r--r--src/SMAPI/Framework/Commands/HelpCommand.cs64
-rw-r--r--src/SMAPI/Framework/Commands/IInternalCommand.cs24
-rw-r--r--src/SMAPI/Framework/Commands/ReloadI18nCommand.cs44
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs5
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs6
-rw-r--r--src/SMAPI/Framework/Events/ModMultiplayerEvents.cs9
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs6
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs17
-rw-r--r--src/SMAPI/Framework/Input/MouseStateBuilder.cs23
-rw-r--r--src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs5
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs114
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs40
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs53
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs40
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs40
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs48
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs49
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs51
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs120
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs77
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs309
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs (renamed from src/SMAPI/Framework/ModLoading/RewriteHelper.cs)102
-rw-r--r--src/SMAPI/Framework/ModLoading/IInstructionHandler.cs32
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs11
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs42
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs82
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs45
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs (renamed from src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs)16
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs37
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs42
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs127
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs71
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs35
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs136
-rw-r--r--src/SMAPI/Framework/Networking/SGalaxyNetServer.cs29
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenServer.cs31
-rw-r--r--src/SMAPI/Framework/Patching/GamePatcher.cs4
-rw-r--r--src/SMAPI/Framework/Patching/IHarmonyPatch.cs4
-rw-r--r--src/SMAPI/Framework/Patching/PatchHelper.cs34
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs2
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs2
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs2
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs3
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs2
-rw-r--r--src/SMAPI/Framework/Rendering/SDisplayDevice.cs13
-rw-r--r--src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs2
-rw-r--r--src/SMAPI/Framework/SCore.cs185
-rw-r--r--src/SMAPI/Framework/SGame.cs2
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs4
-rw-r--r--src/SMAPI/Framework/Serialization/ColorConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/PointConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/RectangleConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/Vector2Converter.cs2
55 files changed, 1648 insertions, 782 deletions
diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs
index ceeb6f93..eaa91c86 100644
--- a/src/SMAPI/Framework/CommandManager.cs
+++ b/src/SMAPI/Framework/CommandManager.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
+using StardewModdingAPI.Framework.Commands;
namespace StardewModdingAPI.Framework
{
@@ -27,7 +28,7 @@ namespace StardewModdingAPI.Framework
/// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception>
/// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
/// <exception cref="ArgumentException">There's already a command with that name.</exception>
- public void Add(IModMetadata mod, string name, string documentation, Action<string, string[]> callback, bool allowNullCallback = false)
+ public CommandManager Add(IModMetadata mod, string name, string documentation, Action<string, string[]> callback, bool allowNullCallback = false)
{
name = this.GetNormalizedName(name);
@@ -45,6 +46,16 @@ namespace StardewModdingAPI.Framework
// add command
this.Commands.Add(name, new Command(mod, name, documentation, callback));
+ return this;
+ }
+
+ /// <summary>Add a console command.</summary>
+ /// <param name="command">the SMAPI console command to add.</param>
+ /// <param name="monitor">Writes messages to the console.</param>
+ /// <exception cref="ArgumentException">There's already a command with that name.</exception>
+ public CommandManager Add(IInternalCommand command, IMonitor monitor)
+ {
+ return this.Add(null, command.Name, command.Description, (name, args) => command.HandleCommand(args, monitor));
}
/// <summary>Get a command by its unique name.</summary>
diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
new file mode 100644
index 00000000..08233feb
--- /dev/null
+++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using HarmonyLib;
+
+namespace StardewModdingAPI.Framework.Commands
+{
+ /// <summary>The 'harmony_summary' SMAPI console command.</summary>
+ internal class HarmonySummaryCommand : IInternalCommand
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The command name, which the user must type to trigger it.</summary>
+ public string Name { get; } = "harmony_summary";
+
+ /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary>
+ public string Description { get; } = "Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches.\n\nUsage: harmony_summary\nList all Harmony patches.\n\nUsage: harmony_summary <search>\n- search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method.";
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Handle the console command when it's entered by the user.</summary>
+ /// <param name="args">The command arguments.</param>
+ /// <param name="monitor">Writes messages to the console.</param>
+ public void HandleCommand(string[] args, IMonitor monitor)
+ {
+ SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.MethodName).ToArray();
+
+ StringBuilder result = new StringBuilder();
+
+ if (!matches.Any())
+ result.AppendLine("No current patches match your search.");
+ else
+ {
+ result.AppendLine(args.Any() ? "Harmony patches which match your search terms:" : "Current Harmony patches:");
+ result.AppendLine();
+ foreach (var match in matches)
+ {
+ result.AppendLine($" {match.MethodName}");
+ foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key))
+ {
+ var sortedTypes = ownerGroup.Value
+ .OrderBy(p => p switch { PatchType.Prefix => 0, PatchType.Postfix => 1, PatchType.Finalizer => 2, PatchType.Transpiler => 3, _ => 4 });
+
+ result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})");
+ }
+ result.AppendLine();
+ }
+ }
+
+ monitor.Log(result.ToString().TrimEnd(), LogLevel.Info);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get all current Harmony patches matching any of the given search terms.</summary>
+ /// <param name="searchTerms">The search terms to match.</param>
+ private IEnumerable<SearchResult> FilterPatches(string[] searchTerms)
+ {
+ bool hasSearch = searchTerms.Any();
+ bool IsMatch(string target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1);
+
+ foreach (var patch in this.GetAllPatches())
+ {
+ // matches entire patch
+ if (IsMatch(patch.MethodDescription))
+ {
+ yield return patch;
+ continue;
+ }
+
+ // matches individual patchers
+ foreach (var pair in patch.PatchTypesByOwner.ToArray())
+ {
+ if (!IsMatch(pair.Key) && !pair.Value.Any(type => IsMatch(type.ToString())))
+ patch.PatchTypesByOwner.Remove(pair.Key);
+ }
+
+ if (patch.PatchTypesByOwner.Any())
+ yield return patch;
+ }
+ }
+
+ /// <summary>Get all current Harmony patches.</summary>
+ private IEnumerable<SearchResult> GetAllPatches()
+ {
+ foreach (MethodBase method in Harmony.GetAllPatchedMethods())
+ {
+ // get metadata for method
+ HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method);
+ IDictionary<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>>
+ {
+ [PatchType.Prefix] = patchInfo.Prefixes,
+ [PatchType.Postfix] = patchInfo.Postfixes,
+ [PatchType.Finalizer] = patchInfo.Finalizers,
+ [PatchType.Transpiler] = patchInfo.Transpilers
+ };
+
+ // get patch types by owner
+ var typesByOwner = new Dictionary<string, ISet<PatchType>>();
+ foreach (var group in patchGroups)
+ {
+ foreach (var patch in group.Value)
+ {
+ if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType> patchTypes))
+ typesByOwner[patch.owner] = patchTypes = new HashSet<PatchType>();
+ patchTypes.Add(group.Key);
+ }
+ }
+
+ // create search result
+ yield return new SearchResult(method, typesByOwner);
+ }
+ }
+
+ /// <summary>A Harmony patch type.</summary>
+ private enum PatchType
+ {
+ /// <summary>A prefix patch.</summary>
+ Prefix,
+
+ /// <summary>A postfix patch.</summary>
+ Postfix,
+
+ /// <summary>A finalizer patch.</summary>
+ Finalizer,
+
+ /// <summary>A transpiler patch.</summary>
+ Transpiler
+ }
+
+ /// <summary>A patch search result for a method.</summary>
+ private class SearchResult
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A simple human-readable name for the patched method.</summary>
+ public string MethodName { get; }
+
+ /// <summary>A detailed description for the patched method.</summary>
+ public string MethodDescription { get; }
+
+ /// <summary>The patch types by the Harmony instance ID that added them.</summary>
+ public IDictionary<string, ISet<PatchType>> PatchTypesByOwner { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="method">The patched method.</param>
+ /// <param name="patchTypesByOwner">The patch types by the Harmony instance ID that added them.</param>
+ public SearchResult(MethodBase method, IDictionary<string, ISet<PatchType>> patchTypesByOwner)
+ {
+ this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}";
+ this.MethodDescription = method.FullDescription();
+ this.PatchTypesByOwner = patchTypesByOwner;
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Commands/HelpCommand.cs b/src/SMAPI/Framework/Commands/HelpCommand.cs
new file mode 100644
index 00000000..b8730a00
--- /dev/null
+++ b/src/SMAPI/Framework/Commands/HelpCommand.cs
@@ -0,0 +1,64 @@
+using System.Linq;
+
+namespace StardewModdingAPI.Framework.Commands
+{
+ /// <summary>The 'help' SMAPI console command.</summary>
+ internal class HelpCommand : IInternalCommand
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Manages console commands.</summary>
+ private readonly CommandManager CommandManager;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The command name, which the user must type to trigger it.</summary>
+ public string Name { get; } = "help";
+
+ /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary>
+ public string Description { get; } = "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help <cmd>\n- cmd: The name of a command whose documentation to display.";
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="commandManager">Manages console commands.</param>
+ public HelpCommand(CommandManager commandManager)
+ {
+ this.CommandManager = commandManager;
+ }
+
+ /// <summary>Handle the console command when it's entered by the user.</summary>
+ /// <param name="args">The command arguments.</param>
+ /// <param name="monitor">Writes messages to the console.</param>
+ public void HandleCommand(string[] args, IMonitor monitor)
+ {
+ if (args.Any())
+ {
+ Command result = this.CommandManager.Get(args[0]);
+ if (result == null)
+ monitor.Log("There's no command with that name.", LogLevel.Error);
+ else
+ monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info);
+ }
+ else
+ {
+ string message = "The following commands are registered:\n";
+ IGrouping<string, string>[] groups = (from command in this.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray();
+ foreach (var group in groups)
+ {
+ string modName = group.Key ?? "SMAPI";
+ string[] commandNames = group.ToArray();
+ message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n";
+ }
+ message += "For more information about a command, type 'help command_name'.";
+
+ monitor.Log(message, LogLevel.Info);
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Commands/IInternalCommand.cs b/src/SMAPI/Framework/Commands/IInternalCommand.cs
new file mode 100644
index 00000000..abf105b6
--- /dev/null
+++ b/src/SMAPI/Framework/Commands/IInternalCommand.cs
@@ -0,0 +1,24 @@
+namespace StardewModdingAPI.Framework.Commands
+{
+ /// <summary>A core SMAPI console command.</summary>
+ interface IInternalCommand
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The command name, which the user must type to trigger it.</summary>
+ string Name { get; }
+
+ /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary>
+ string Description { get; }
+
+
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Handle the console command when it's entered by the user.</summary>
+ /// <param name="args">The command arguments.</param>
+ /// <param name="monitor">Writes messages to the console.</param>
+ void HandleCommand(string[] args, IMonitor monitor);
+ }
+}
diff --git a/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs
new file mode 100644
index 00000000..12328bb6
--- /dev/null
+++ b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace StardewModdingAPI.Framework.Commands
+{
+ /// <summary>The 'reload_i18n' SMAPI console command.</summary>
+ internal class ReloadI18nCommand : IInternalCommand
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Reload translations for all mods.</summary>
+ private readonly Action ReloadTranslations;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The command name, which the user must type to trigger it.</summary>
+ public string Name { get; } = "reload_i18n";
+
+ /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary>
+ public string Description { get; } = "Reloads translation files for all mods.\n\nUsage: reload_i18n";
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="reloadTranslations">Reload translations for all mods..</param>
+ public ReloadI18nCommand(Action reloadTranslations)
+ {
+ this.ReloadTranslations = reloadTranslations;
+ }
+
+ /// <summary>Handle the console command when it's entered by the user.</summary>
+ /// <param name="args">The command arguments.</param>
+ /// <param name="monitor">Writes messages to the console.</param>
+ public void HandleCommand(string[] args, IMonitor monitor)
+ {
+ this.ReloadTranslations();
+ monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index fda80a83..cfda55b9 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -246,10 +246,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
texture.GetData(data);
for (int i = 0; i < data.Length; i++)
{
- if (data[i].A == byte.MinValue || data[i].A == byte.MaxValue)
+ var pixel = data[i];
+ if (pixel.A == byte.MinValue || pixel.A == byte.MaxValue)
continue; // no need to change fully transparent/opaque pixels
- data[i] = Color.FromNonPremultiplied(data[i].ToVector4());
+ data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4())
}
texture.SetData(data);
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index 538fde59..b5a12a6e 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -109,9 +109,12 @@ namespace StardewModdingAPI.Framework.Events
/****
** Multiplayer
****/
- /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary>
+ /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection (<see cref="IMultiplayerEvents.PeerConnected"/>), so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary>
public readonly ManagedEvent<PeerContextReceivedEventArgs> PeerContextReceived;
+ /// <summary>Raised after a peer connection is approved by the game.</summary>
+ public readonly ManagedEvent<PeerConnectedEventArgs> PeerConnected;
+
/// <summary>Raised after a mod message is received over the network.</summary>
public readonly ManagedEvent<ModMessageReceivedEventArgs> ModMessageReceived;
@@ -217,6 +220,7 @@ namespace StardewModdingAPI.Framework.Events
this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled));
this.PeerContextReceived = ManageEventOf<PeerContextReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived));
+ this.PeerConnected = ManageEventOf<PeerConnectedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerConnected));
this.ModMessageReceived = ManageEventOf<ModMessageReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived));
this.PeerDisconnected = ManageEventOf<PeerDisconnectedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected));
diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
index 2006b2b5..64cf0355 100644
--- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
+++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
@@ -9,13 +9,20 @@ namespace StardewModdingAPI.Framework.Events
/*********
** Accessors
*********/
- /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary>
+ /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection (<see cref="IMultiplayerEvents.PeerConnected"/>), so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary>
public event EventHandler<PeerContextReceivedEventArgs> PeerContextReceived
{
add =>