Skip to content

Commit a6f11bf

Browse files
committed
Fix mods being reloaded twice
1 parent 847ad71 commit a6f11bf

5 files changed

Lines changed: 147 additions & 27 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
// <auto-generated/>
22
#pragma warning disable
3+
#nullable enable annotations
34

4-
[assembly: global::System.Runtime.CompilerServices.TypeForwardedTo(typeof(global::System.Runtime.CompilerServices.IsExternalInit))]
5+
// Licensed to the .NET Foundation under one or more agreements.
6+
// The .NET Foundation licenses this file to you under the MIT license.
7+
8+
namespace System.Runtime.CompilerServices
9+
{
10+
/// <summary>
11+
/// Reserved to be used by the compiler for tracking metadata.
12+
/// This class should not be used by developers in source code.
13+
/// </summary>
14+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
15+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
16+
internal static class IsExternalInit
17+
{
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
// <auto-generated/>
22
#pragma warning disable
3+
#nullable enable annotations
34

4-
[assembly: global::System.Runtime.CompilerServices.TypeForwardedTo(typeof(global::System.Runtime.CompilerServices.RequiresLocationAttribute))]
5+
// Licensed to the .NET Foundation under one or more agreements.
6+
// The .NET Foundation licenses this file to you under the MIT license.
7+
8+
namespace System.Runtime.CompilerServices
9+
{
10+
/// <summary>
11+
/// Reserved for use by a compiler for tracking metadata.
12+
/// This attribute should not be used by developers in source code.
13+
/// </summary>
14+
[global::System.AttributeUsage(global::System.AttributeTargets.Parameter, Inherited = false)]
15+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
16+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
17+
internal sealed class RequiresLocationAttribute : global::System.Attribute
18+
{
19+
}
20+
}

MonkeyLoader/Meta/ModLoadingLocation.cs

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Newtonsoft.Json;
33
using System;
44
using System.Collections.Generic;
5+
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
67
using System.IO;
78
using System.Linq;
@@ -24,8 +25,16 @@ namespace MonkeyLoader.Meta
2425
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
2526
public sealed class ModLoadingLocation : IDisposable
2627
{
28+
/// <summary>
29+
/// Hot (Re)Load tasks by file.<br/>
30+
/// Ignores case, which may be technically wrong for some file systems, but makes sense for mod files.
31+
/// </summary>
32+
private static readonly Dictionary<string, HotReloadTask> _hotReloadTasksByFile = new(StringComparer.OrdinalIgnoreCase);
33+
2734
private bool _disposedValue;
35+
2836
private Regex[] _ignorePatterns;
37+
2938
private FileSystemWatcher? _watcher;
3039

3140
/// <summary>
@@ -97,7 +106,7 @@ internal bool ShouldWatcherBeActive
97106
{
98107
EnableRaisingEvents = true,
99108
IncludeSubdirectories = Recursive,
100-
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
109+
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName
101110
};
102111

103112
_watcher.Created += OnLoadMod;
@@ -161,7 +170,7 @@ public bool PassesIgnorePatterns(string path)
161170
/// <returns>The full names (including paths) of all files that satisfy the specifications.</returns>
162171
public IEnumerable<string> Search()
163172
=> Directory.EnumerateFiles(Path, NuGetPackageMod.SearchPattern, Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
164-
.Where(PassesIgnorePatterns);
173+
.Where(PassesIgnorePatterns);
165174

166175
/// <inheritdoc/>
167176
public override string ToString()
@@ -185,20 +194,50 @@ private void Dispose(bool disposing)
185194
}
186195
}
187196

197+
private HotReloadTask GetOrCreateHotLoadTask(string fullPath)
198+
{
199+
lock (_hotReloadTasksByFile)
200+
{
201+
if (!_hotReloadTasksByFile.TryGetValue(fullPath, out var hotLoadTask) || hotLoadTask.Task.IsCompleted)
202+
{
203+
hotLoadTask = new(this, fullPath);
204+
_hotReloadTasksByFile[fullPath] = hotLoadTask;
205+
}
206+
207+
return hotLoadTask;
208+
}
209+
}
210+
188211
private void OnLoadMod(object sender, FileSystemEventArgs e)
189212
{
190-
if (PassesIgnorePatterns(e.FullPath))
191-
LoadMod?.Invoke(this, e.FullPath);
213+
// e.FullPath is not actually a full path D:
214+
var fullPath = System.IO.Path.GetFullPath(e.FullPath);
215+
216+
if (!PassesIgnorePatterns(fullPath))
217+
return;
218+
219+
var hotLoadTask = GetOrCreateHotLoadTask(fullPath);
220+
hotLoadTask.ShouldLoad |= true;
192221
}
193222

194223
private void OnReloadMod(object sender, FileSystemEventArgs e)
195224
{
196-
OnUnloadMod(sender, e);
197-
OnLoadMod(sender, e);
225+
// e.FullPath is not actually a full path D:
226+
var fullPath = System.IO.Path.GetFullPath(e.FullPath);
227+
228+
var hotLoadTask = GetOrCreateHotLoadTask(fullPath);
229+
hotLoadTask.ShouldUnload |= true;
230+
hotLoadTask.ShouldLoad |= PassesIgnorePatterns(fullPath);
198231
}
199232

200233
private void OnUnloadMod(object sender, FileSystemEventArgs e)
201-
=> UnloadMod?.Invoke(this, e.FullPath);
234+
{
235+
// e.FullPath is not actually a full path D:
236+
var fullPath = System.IO.Path.GetFullPath(e.FullPath);
237+
238+
var hotLoadTask = GetOrCreateHotLoadTask(fullPath);
239+
hotLoadTask.ShouldUnload |= true;
240+
}
202241

203242
/// <summary>
204243
/// Called when a mod should be loaded because its got added or changed.
@@ -210,6 +249,45 @@ private void OnUnloadMod(object sender, FileSystemEventArgs e)
210249
/// </summary>
211250
public event HotReloadModEventHandler? UnloadMod;
212251

252+
private sealed class HotReloadTask
253+
{
254+
public string FullPath { get; }
255+
public ModLoadingLocation Location { get; }
256+
public bool ShouldLoad { get; set; }
257+
public bool ShouldUnload { get; set; }
258+
public Task Task { get; }
259+
260+
public HotReloadTask(ModLoadingLocation location, string fullPath)
261+
{
262+
Location = location;
263+
FullPath = fullPath;
264+
Task = Task.Run(DelayAndExecuteAsync);
265+
}
266+
267+
private async Task DelayAndExecuteAsync()
268+
{
269+
await Task.Delay(TimeSpan.FromSeconds(5));
270+
271+
if (ShouldUnload)
272+
{
273+
try
274+
{
275+
Location.UnloadMod?.TryInvokeAll(Location, FullPath);
276+
}
277+
catch { }
278+
}
279+
280+
if (ShouldLoad)
281+
{
282+
try
283+
{
284+
Location.LoadMod?.TryInvokeAll(Location, FullPath);
285+
}
286+
catch { }
287+
}
288+
}
289+
}
290+
213291
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
214292
// ~ModLoadingLocation()
215293
// {

MonkeyLoader/MonkeyLoader.cs

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,16 @@ private set
193193
internal AssemblyPool GameAssemblyPool { get; }
194194
internal AssemblyPool PatcherAssemblyPool { get; }
195195
internal AssemblyPool RuntimeAssemblyPool { get; }
196-
196+
197197
public IAssemblyLoadStrategy AssemblyLoadStrategy { get; }
198198

199199
public Assembly? ResolveAssemblyFromPoolsAndMods(System.Reflection.AssemblyName assemblyName)
200200
{
201201
var mlAssemblyName = new AssemblyName(assemblyName.FullName);
202-
202+
203203
if (PatcherAssemblyPool.TryResolveAssembly(mlAssemblyName, out var assembly))
204204
return assembly;
205-
205+
206206
if (GameAssemblyPool.TryResolveAssembly(mlAssemblyName, out assembly))
207207
return assembly;
208208

@@ -223,14 +223,14 @@ static MonkeyLoader()
223223

224224
// Assume Unity structure
225225
var gameName = Path.GetFileNameWithoutExtension(executablePath);
226-
var gameAssemblyPath = Path.Combine(Path.GetDirectoryName(executablePath), $"{GameName}_Data", "Managed");
226+
var gameAssemblyPath = Path.Combine(Path.GetDirectoryName(executablePath)!, $"{GameName}_Data", "Managed");
227227

228228
if (!Directory.Exists(gameAssemblyPath))
229229
{
230230
// If Unity directory doesn't exist, assume plain .NET application
231231
DirectoryInfo executablePathInfo = new(executablePath);
232232

233-
gameName = executablePathInfo.Parent.Name;
233+
gameName = executablePathInfo.Parent!.Name;
234234
gameAssemblyPath = executablePathInfo.Parent.FullName;
235235
}
236236

@@ -268,7 +268,7 @@ public MonkeyLoader(LoggingController loggingController, string configPath = Def
268268
#if NET5_0_OR_GREATER
269269
AssemblyLoadStrategy = new AssemblyLoadContextLoadStrategy();
270270
#endif
271-
271+
272272
ConfigPath = configPath;
273273
Id = GetId(configPath);
274274

@@ -284,25 +284,33 @@ public MonkeyLoader(LoggingController loggingController, string configPath = Def
284284

285285
foreach (var modLocation in Locations.Mods)
286286
{
287-
modLocation.LoadMod += (mL, path) => TryLoadAndRunMod(path, out _);
287+
modLocation.LoadMod += (mL, path) =>
288+
{
289+
Logger.Info(() => $"Trying to hot-load mod from: {path}");
290+
291+
TryLoadAndRunMod(path, out _);
292+
};
293+
288294
modLocation.UnloadMod += (mL, path) =>
289295
{
296+
Logger.Info(() => $"Trying to unload mod from: {path}");
297+
290298
if (TryFindModByLocation(path, out var mod))
291299
ShutdownMod(mod);
292300
};
293301
}
294302

295303
// TODO: do this properly - scan all loaded assemblies?
296304
NuGet = new NuGetManager(this);
297-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("MonkeyLoader", new NuGetVersion(Assembly.GetExecutingAssembly().GetName().Version)), NuGetHelper.Framework));
298-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Newtonsoft.Json", new NuGetVersion(13, 0, 3)), NuGetHelper.Framework));
299-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("NuGet.Packaging", new NuGetVersion(6, 10, 0)), NuGetHelper.Framework));
300-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("NuGet.Protocol", new NuGetVersion(6, 10, 0)), NuGetHelper.Framework));
301-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Mono.Cecil", new NuGetVersion(0, 11, 5)), NuGetHelper.Framework));
302-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Harmony", new NuGetVersion(2, 3, 3)), NuGetHelper.Framework));
303-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Lib.Harmony", new NuGetVersion(2, 3, 3)), NuGetHelper.Framework));
304-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Lib.Harmony.Thin", new NuGetVersion(2, 3, 3)), NuGetHelper.Framework));
305-
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Zio", new NuGetVersion(0, 18, 0)), NuGetHelper.Framework));
305+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("MonkeyLoader", new NuGetVersion(Assembly.GetExecutingAssembly().GetName().Version!)), NuGetHelper.Framework));
306+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Newtonsoft.Json", new NuGetVersion(13, 0, 4)), NuGetHelper.Framework));
307+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("NuGet.Packaging", new NuGetVersion(6, 14, 0)), NuGetHelper.Framework));
308+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("NuGet.Protocol", new NuGetVersion(6, 14, 0)), NuGetHelper.Framework));
309+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Mono.Cecil", new NuGetVersion(0, 11, 6)), NuGetHelper.Framework));
310+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Harmony", new NuGetVersion(2, 4, 2)), NuGetHelper.Framework));
311+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Lib.Harmony", new NuGetVersion(2, 4, 2)), NuGetHelper.Framework));
312+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Lib.Harmony.Thin", new NuGetVersion(2, 4, 2)), NuGetHelper.Framework));
313+
NuGet.Add(new LoadedNuGetPackage(new PackageIdentity("Zio", new NuGetVersion(0, 22, 1)), NuGetHelper.Framework));
306314

307315
RuntimeAssemblyPool = new AssemblyPool(this, "RuntimeAssemblyPool", () => Locations.PatchedAssemblies);
308316
RuntimeAssemblyPool.AddSearchDirectory(RuntimeAssemblyPath);
@@ -623,6 +631,8 @@ public void LoadGamePackMonkeys()
623631
/// <returns>The loaded mod.</returns>
624632
public NuGetPackageMod LoadMod(string path, bool isGamePack = false)
625633
{
634+
path = Path.GetFullPath(path);
635+
626636
Logger.Debug(() => $"Loading {(isGamePack ? "game pack" : "regular")} mod from: {path}");
627637

628638
var mod = new NuGetPackageMod(this, path, isGamePack);
@@ -1035,7 +1045,8 @@ public bool TryFindModByLocation(string location, [NotNullWhen(true)] out Mod? m
10351045
return false;
10361046
}
10371047

1038-
var mods = _allMods.Where(mod => location.Equals(mod.Location, StringComparison.Ordinal)).ToArray();
1048+
// Ignores case, which may be technically wrong for some file systems, but makes sense for mod files.
1049+
var mods = _allMods.Where(mod => location.Equals(mod.Location, StringComparison.OrdinalIgnoreCase)).ToArray();
10391050

10401051
if (mods.Length == 0)
10411052
return false;

MonkeyLoader/MonkeyLoader.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
<None Include="Configuration\ModLoaderConfiguration.cs" />
5656
</ItemGroup>
5757

58-
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(CopyToLibraries)'=='true' AND '$(TargetFramework)' == 'net9.0'">
58+
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(CopyToLibraries)'=='true' AND '$(TargetFramework)' == 'net10.0'">
5959
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFiles="$(GamePath)/MonkeyLoader/$(AssemblyFileName)" />
6060
<Message Text="Copied $(TargetFileName) to $(GamePath)/MonkeyLoader/$(AssemblyFileName)" Importance="high" />
6161

0 commit comments

Comments
 (0)