Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ internal static void LoadAuto() {
Logger.Verbose("loader", $"ALL MODS LOADED IN {watch.ElapsedMilliseconds}ms");
Logger.Info("loader", $"Loaded {Everest._Modules.Count} modules");

DataComponentRegistryBase.Optimize();

try {
Watcher = new FileSystemWatcher {
Path = PathMods,
Expand Down
260 changes: 260 additions & 0 deletions Celeste.Mod.mm/Mod/Registry/DataComponentRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
using Celeste.Mod.Registry;
using Monocle;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Celeste.Mod.Registry.DataComponentRegistryBase;
#nullable enable

namespace Celeste.Mod.Registry {
public class DataComponentInfo {
public string? ModName;
public string? Description;
}
internal struct DebugModeDataComponentInfo {
internal DataComponentInfo? info;
internal bool unloaded;
internal int knownUnloaded;
}

internal static class DataComponentRegistryBase {
internal abstract class SlotHolderBase {
internal int slot = -1;
internal int knownCount;
internal abstract Type declaringType { get; }
internal abstract Type fieldType { get; }
internal static void Throw() {
throw new InvalidOperationException("It's not prepared.");
}
}
internal static int getHierarchyDepth(Type? self) {
int depth = 0;
while (self is not null) {
self = self.BaseType;
depth++;
}
return depth - 1;
}
internal static readonly Dictionary<Type, List<DebugModeDataComponentInfo>> debugInfos = new();

internal static readonly Dictionary<Type, List<DataComponentInfo?>> infos = new();
internal static Dictionary<Type, List<SlotHolderBase>>? toOptimize = new();

internal static void Optimize() {
if (toOptimize is not { } toop) {
throw new InvalidOperationException("what did it mean");
}
toOptimize = null;
Dictionary<Type, int> types = new();

foreach ((Type k, List<DataComponentInfo?> o) in infos) {
static int GetOrSet(Dictionary<Type, int> types, Type t, int? hint) {
if (types.TryGetValue(t, out int cur)) {
return cur;
}
int baseCnt;
if (t == typeof(Entity)) {
baseCnt = 0;
} else {
baseCnt = GetOrSet(types, t.BaseType!, null);
}
return types[t] = baseCnt + (hint ?? infos.GetValueOrDefault(t)?.Count ?? 0);
}
GetOrSet(types, k, o.Count);
}
foreach ((Type? k, List<SlotHolderBase>? v) in toop) {
int all = types[k];
int bas = all - v.Count;
for (int i = 0; i < v.Count; i++) {
v[i].slot = bas + i;
v[i].knownCount = all;
}
}
}
}

public delegate ref TRet Accessor<T, TRet>(T self) where T : patch_Entity where TRet : class?;

/// <typeparam name="T">Target entity type.</typeparam>
/// <typeparam name="TRet">Attached data type.</typeparam>
public static class DataComponentRegistry<T, TRet> where T : patch_Entity where TRet : class? {
internal class SlotHolder : SlotHolderBase {
internal ref TRet? ReadSlot(T self) {
int slot1 = slot;
if (slot1 < 0) {
Throw();
}
ref object[] slots = ref self.slots;
if (slots is not { }) {
slots = new object[knownCount];
} else if (slots.Length <= slot) {
Array.Resize(ref slots, knownCount);
}
return ref Unsafe.As<object, TRet?>(ref slots[slot1]);
}

internal override Type declaringType => typeof(T);
internal override Type fieldType => typeof(TRet);
}

internal class DebugModeSlotHolder {
internal required int depth;
internal required int slot;
internal required List<DebugModeDataComponentInfo> registered;
internal ref TRet? ReadSlot(T self) {
self.debugSlots ??= new object[getHierarchyDepth(self.GetType())][];
ref object?[]? curDepth = ref self.debugSlots[depth];

if (curDepth is not { }) {
curDepth = new object[registered.Count];
} else if (curDepth.Length <= slot) {
Array.Resize(ref curDepth, registered.Count);
}
// type check is necessary because you can unregister a slot
ref object? got = ref curDepth[slot];
if (got is { } && got is not TRet) {
throw new InvalidCastException();
}
return ref Unsafe.As<object?, TRet?>(ref got);
}
}
internal static Accessor<T, TRet?> RegisterForDebug(DataComponentInfo? info) {
if (!debugInfos.TryGetValue(typeof(T), out List<DebugModeDataComponentInfo>? regList)) {
regList = new();
debugInfos.Add(typeof(T), regList);
}

DebugModeDataComponentInfo debugInfo = new() { info = info, knownUnloaded = -1, unloaded = false };
// TODO: Optimize it to use the linked list which is not implemented yet
if (regList.FindIndex(d => d.unloaded) is { } i and >= 0) {
regList[i] = debugInfo;
} else {
i = regList.Count;
regList.Add(debugInfo);
}

return new DebugModeSlotHolder() { slot = i, registered = regList, depth = getHierarchyDepth(typeof(T)) - 1, }.ReadSlot;
}

internal static Accessor<T, TRet?> RegisterFor(DataComponentInfo? info) {
if (toOptimize is not { } toop) {
throw new InvalidOperationException("Slots have been frozen.");
}
if (!toop.TryGetValue(typeof(T), out List<SlotHolderBase>? holderList)) {
holderList = new();
toop.Add(typeof(T), holderList);
}
if (!infos.TryGetValue(typeof(T), out List<DataComponentInfo?>? regList)) {
regList = new();
infos.Add(typeof(T), regList);
}
var slot = new SlotHolder();
regList.Add(info);
holderList.Add(slot);
return slot.ReadSlot;
}

/// <summary>
/// A performant data holder implementation.
/// Allows you to attach any data to a type of entity.
/// </summary>
/// <remarks>
/// note: in debug mode, register new field and then access it
/// *may* invalidate all existing debug references.
/// this can secretly happens in hook chain.
/// be careful with this one.
/// <br/>
/// nobody except code reloading should be doing this, and it should not happen during Update.
/// </remarks>
/// <param name="info">
/// it's mainly for external tools, and not actually used anywhere.
/// type your modname and comment here.
/// </param>
/// <param name="debug">
/// debug mode will enable type check, dynamic register and unregister.
/// it's not free, so please disable them when publishing.
/// <br/>
/// a good idea is declare your own wrapper method like this:
/// <code>
/// public static Accessor&lt;T, TRet&gt; RegisterFor&lt;T, TRet&gt;(RegistryInfo? info) where T : Entity where TRet : class? {
/// #if DEBUG
/// bool debug = true;
/// #else
/// bool debug = false;
/// #endif
/// DataComponentRegistry.RegisterFor&lt;T, TRet&gt;(info, debug);
/// }
/// </code>
/// </param>
/// <returns>The field accessor. note that your field can be null if it's not initialized.</returns>
public static Accessor<T, TRet?> RegisterFor(DataComponentInfo? info, bool debug) {
if (debug) {
return RegisterForDebug(info);
} else {
return RegisterFor(info);
}
}

internal class GetterSetterWrapper {
internal readonly Accessor<T, TRet?> accessor;

public GetterSetterWrapper(Accessor<T, TRet?> accessor) {
this.accessor = accessor;
}

internal TRet? Getter(T self) => accessor(self);
internal void Setter(T self, TRet? value) => accessor(self) = value;
}

/// <remarks>
/// returns simple getter and setter.
/// </remarks>
/// <inheritdoc cref="RegisterFor(DataComponentInfo?, bool)"/>
public static (Action<T, TRet?> setter, Func<T, TRet?> getter) RegisterForSimple(DataComponentInfo? info, bool debug) {
var reader = new GetterSetterWrapper(RegisterFor(info, debug));
return (reader.Setter, reader.Getter);
}

/// <inheritdoc cref="Unregister(Accessor{T, TRet?})"/>
public static void Unregister(Func<T, TRet?> getter) {
if (getter.Target is not GetterSetterWrapper wrapper) {
throw new ArgumentException("Where did you get this accessor?");
}
Unregister(wrapper.accessor);
}
/// <inheritdoc cref="Unregister(Accessor{T, TRet?})"/>
public static void Unregister(Action<T, TRet?> setter) {
if (setter.Target is not GetterSetterWrapper wrapper) {
throw new ArgumentException("Where did you get this accessor?");
}
Unregister(wrapper.accessor);
}
/// <summary>
/// You can only unload slots which is registered under debug mode.
/// </summary>
/// <remarks>
/// a good idea is declare your own wrapper method like this:
/// <code>
/// public static void Unregister&lt;T, TRet&gt;(Accessor&lt;T, TRet&gt; accessor) where T : Entity where TRet : class? {
/// #if DEBUG
/// DataComponentRegistry&lt;T, TRet&gt;.Unregister(accessor);
/// #endif
/// }
/// </code>
/// </remarks>
public static void Unregister(Accessor<T, TRet?> accessor) {
if (accessor.Target is not DebugModeSlotHolder holder) {
if (accessor.Target is SlotHolderBase) {
throw new ArgumentException("Can't unregister release slot.");
}
throw new ArgumentException("Where did you get this accessor?");
}
Span<DebugModeDataComponentInfo> reg = CollectionsMarshal.AsSpan(holder.registered);
reg[holder.slot].unloaded = true;
// TODO: Update knownUnloaded to make a linked list so it's faster
// if anybody care about the loading performance
}
}
}
5 changes: 4 additions & 1 deletion Celeste.Mod.mm/Patches/Monocle/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
using System.ComponentModel;

namespace Monocle {
class patch_Entity : Entity {
public class patch_Entity : Entity {
internal object[] slots = null;
internal object[][] debugSlots = null;

public new Scene Scene {
[MonoModIgnore]
get;
Expand Down
Loading