Skip to content

Commit

Permalink
🪓 Split mod handling and mod loading
Browse files Browse the repository at this point in the history
  • Loading branch information
tomrijnbeek committed Jan 1, 2024
1 parent db8b33a commit 4b90261
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 172 deletions.
192 changes: 75 additions & 117 deletions src/Bearded.TD/Content/ContentManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Bearded.Graphics;
using Bearded.TD.Content.Mods;
using Bearded.TD.Rendering;
using Bearded.TD.Utilities;
Expand All @@ -14,156 +13,62 @@ namespace Bearded.TD.Content;
sealed class ContentManager
{
private readonly ModLoadingContext loadingContext;
public ModLoadingProfiler LoadingProfiler => loadingContext.Profiler;

private readonly ImmutableArray<ModMetadata> allMods;
private readonly ImmutableDictionary<string, ModMetadata> modsById;
public ImmutableHashSet<ModMetadata> AvailableMods { get; }

private readonly HashSet<ModMetadata> enabledMods = new();
public ImmutableHashSet<ModMetadata> EnabledMods => enabledMods.ToImmutableHashSet();

private readonly Dictionary<ModMetadata, ModForLoading> modsForLoading = new();
private readonly Queue<ModMetadata> modLoadingQueue = new();
private readonly MultiDictionary<ModMetadata, ModLease> leasesByMods = new();

public ModMetadata? CurrentlyLoading { get; private set; }
public bool IsFinishedLoading => CurrentlyLoading is null && modLoadingQueue.Count == 0;
private ModMetadata? currentlyLoading;
private bool isFinishedLoading => currentlyLoading is null && modLoadingQueue.Count == 0;

public IEnumerable<Mod> LoadedEnabledMods
{
get
{
if (!IsFinishedLoading)
{
throw new InvalidOperationException("Cannot access loaded enabled mods before finished loading.");
}
public ModLoadingProfiler LoadingProfiler => loadingContext.Profiler;

return enabledMods.Select(metadata => modsForLoading[metadata].GetLoadedMod()).ToImmutableArray();
}
}
public ImmutableArray<ModMetadata> VisibleMods => allMods.Where(m => m.Visible).ToImmutableArray();

public ContentManager(
Logger logger, IGraphicsLoader graphicsLoader, IReadOnlyCollection<ModMetadata> availableMods)
public ContentManager(Logger logger, IGraphicsLoader graphicsLoader, IReadOnlyCollection<ModMetadata> allMods)
{
loadingContext = new ModLoadingContext(logger, graphicsLoader, new ModLoadingProfiler());

AvailableMods = ImmutableHashSet.CreateRange(availableMods);
modsById = availableMods.ToImmutableDictionary(m => m.Id);
this.allMods = ImmutableArray.CreateRange(allMods);
modsById = allMods.ToImmutableDictionary(m => m.Id);
}

public ModMetadata FindMod(string modId) => modsById[modId];

public void SetEnabledModsById(IEnumerable<string> modIds)
{
setEnabledMods(modIds.Select(FindMod));
}

private void setEnabledMods(IEnumerable<ModMetadata> mods)
{
enabledMods.Clear();
mods.ForEach(enableMod);
}

private void enableMod(ModMetadata mod)
{
if (enabledMods.Contains(mod))
{
return;
}

// Make sure all dependencies are enabled too.
foreach (var dependency in mod.Dependencies)
{
var dependencyMod = modsById[dependency.Id];
if (!enabledMods.Contains(dependencyMod))
{
enableMod(dependencyMod);
}
}

// Enqueue for loading if it hasn't been loaded yet.
if (!modsForLoading.ContainsKey(mod))
{
modsForLoading.Add(mod, new ModForLoading(mod));
modLoadingQueue.Enqueue(mod);
}

enabledMods.Add(mod);
}

public HashSet<ModMetadata> PreviewEnableMod(ModMetadata modToEnable)
{
var enabled = enabledMods.ToHashSet();

enable(modToEnable);

return enabled;

void enable(ModMetadata mod)
{
if (enabled.Contains(mod))
return;

foreach (var dependency in mod.Dependencies)
{
var dependencyMod = modsById[dependency.Id];
enable(dependencyMod);
}

enabled.Add(mod);
}
}

public HashSet<ModMetadata> PreviewDisableMod(ModMetadata modToDisable)
{
var enabled = enabledMods.ToHashSet();

disable(modToDisable);

return enabled;

void disable(ModMetadata mod)
{
if (!enabled.Contains(mod))
return;

var dependents = enabled.Where(m => m.Dependencies.Any(d => d.Id == mod.Id));
dependents.ForEach(disable);

enabled.Remove(mod);
}
}

public void Update(UpdateEventArgs args)
public void Update()
{
pumpLoadingQueue();
}

private void pumpLoadingQueue()
{
if (CurrentlyLoading is { } metadata)
if (currentlyLoading is { } metadata)
{
var modForLoading = modsForLoading[metadata];
if (!modForLoading.IsDone)
{
return;
}
// TODO: deal with errors
CurrentlyLoading = null;
currentlyLoading = null;
}

if (CurrentlyLoading is null)
if (currentlyLoading is null)
{
startLoadingNextMod();
}
}

private void startLoadingNextMod()
{
ModMetadata metadata;
ModMetadata? metadata;
do
{
metadata = modLoadingQueue.Count == 0 ? null : modLoadingQueue.Dequeue();
} while (metadata != null && !enabledMods.Contains(metadata));
} while (metadata != null && (!modsForLoading.ContainsKey(metadata) || !leasesByMods.ContainsKey(metadata)));

if (metadata == null)
{
Expand All @@ -179,28 +84,81 @@ private void startLoadingNextMod()
.AsReadOnly();

modForLoading.StartLoading(loadingContext, loadedDependencies);
CurrentlyLoading = metadata;
currentlyLoading = metadata;
}

public void CleanUp()
public void CleanUpUnused()
{
DebugAssert.State.Satisfies(IsFinishedLoading);
var unusedMods = modsForLoading.Where(m => !enabledMods.Contains(m.Key)).ToList();
var unusedMods = modsForLoading.Where(kvp => !leasesByMods.ContainsKey(kvp.Key)).ToImmutableArray();

foreach (var (metadata, modForLoading) in unusedMods)
{
GraphicsUnloader.CleanUp(modForLoading.GetLoadedMod().Blueprints);
// We have no way to abort loading. Just finish loading it and we'll pick it up in a future clean-up cycle.
if (currentlyLoading == metadata)
{
continue;
}

if (modForLoading.IsDone)
{
GraphicsUnloader.CleanUp(modForLoading.GetLoadedMod().Blueprints);
}
modsForLoading.Remove(metadata);
}
}

public void CleanUpAll()
{
DebugAssert.State.Satisfies(IsFinishedLoading);
DebugAssert.State.Satisfies(isFinishedLoading);
foreach (var (_, modForLoading) in modsForLoading)
{
GraphicsUnloader.CleanUp(modForLoading.GetLoadedMod().Blueprints);
}
enabledMods.Clear();
leasesByMods.Clear();
modsForLoading.Clear();
}

public IModLease LeaseMod(ModMetadata mod)
{
var lease = new ModLease(this, mod, findModForLoading(mod));
leasesByMods.Add(mod, lease);
return lease;
}

private ModForLoading findModForLoading(ModMetadata metadata)
{
if (modsForLoading.TryGetValue(metadata, out var mod))
{
return mod;
}

var m = new ModForLoading(metadata);
modsForLoading.Add(metadata, m);
modLoadingQueue.Enqueue(metadata);
return m;
}

private void release(ModLease lease)
{
leasesByMods.Remove(lease.Metadata, lease);
}

private sealed class ModLease(ContentManager contentManager, ModMetadata metadata, ModForLoading modForLoading)
: IModLease
{
public ModMetadata Metadata => metadata;
public bool IsLoaded => modForLoading.IsDone;
public Mod LoadedMod => modForLoading.GetLoadedMod();

public void Dispose()
{
contentManager.release(this);
}
}
}

interface IModLease : IDisposable
{
bool IsLoaded { get; }
Mod LoadedMod { get; }
}
35 changes: 15 additions & 20 deletions src/Bearded.TD/Content/Mods/ModForLoading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,17 @@

namespace Bearded.TD.Content.Mods;

sealed class ModForLoading
sealed class ModForLoading(ModMetadata modMetadata)
{
private readonly ModMetadata modMetadata;
private Mod mod;
private Mod? mod;
private bool isLoading;
private ModLoadingContext context;
private Exception exception;
private ReadOnlyCollection<Mod> loadedDependencies;
private ModLoadingContext? context;
private Exception? exception;
private ReadOnlyCollection<Mod>? loadedDependencies;

public bool IsDone { get; private set; }
public bool DidLoadSuccessfully => IsDone && exception == null;

public ModForLoading(ModMetadata modMetadata)
{
this.modMetadata = modMetadata;
}

public void StartLoading(ModLoadingContext context, ReadOnlyCollection<Mod> loadedDependencies)
{
if (isLoading)
Expand All @@ -34,39 +28,40 @@ public void StartLoading(ModLoadingContext context, ReadOnlyCollection<Mod> load
Task.Run(load);
}

private async Task<Mod> load()
private async Task load()
{
try
{
mod = await ModLoader.Load(context, modMetadata, loadedDependencies);
mod = await ModLoader.Load(context!, modMetadata, loadedDependencies!);
}
catch (Exception e)
{
exception = e;
context.Logger.Error?.Log($"Error loading mod {modMetadata.Id}: {e.Message}");
context!.Logger.Error?.Log($"Error loading mod {modMetadata.Id}: {e.Message}");
}
finally
{
IsDone = true;
}
return mod;
}

public Mod GetLoadedMod()
{
if (!IsDone)
{
throw new InvalidOperationException("Must finish loading mod.");

}
if (exception != null)
{
throw new Exception($"Something went wrong loading mod '{modMetadata.Id}'", exception);
}

return mod;
return mod!;
}

public void Rethrow()
{
DebugAssert.State.Satisfies(exception != null);
// ReSharper disable once PossibleNullReferenceException
throw exception;
throw exception!;
}
}
}
Loading

0 comments on commit 4b90261

Please sign in to comment.