diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs index a420ae955..d8d9252de 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs @@ -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"); + DataComponentRegistry.Optimize(); + try { Watcher = new FileSystemWatcher { Path = PathMods, diff --git a/Celeste.Mod.mm/Mod/Registry/DataComponentRegistry.cs b/Celeste.Mod.mm/Mod/Registry/DataComponentRegistry.cs new file mode 100644 index 000000000..3e7245a0f --- /dev/null +++ b/Celeste.Mod.mm/Mod/Registry/DataComponentRegistry.cs @@ -0,0 +1,287 @@ +using Monocle; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#nullable enable + +namespace Celeste.Mod.Registry { + public class DataComponentInfo { + public string? ModName; + public string? Description; + } + internal struct DebugModeDataComponentInfo { + internal DataComponentInfo? info; + internal nint registryId; + internal bool unloaded; + internal int knownUnloaded; + } + internal struct DebugModeDataComponent { + internal object? content; + internal nint registryId; + } + + /// + /// getter and setter for your registration. + /// + /// Target entity type. + /// Attached data type. + public abstract class DataComponentAccessor where T : patch_Entity where TRet : class? { + public TRet? GetValue(T entity) { + return GetValueRefUnsafe(entity); + } + + public void SetValue(T entity, TRet? value) { + GetValueRefUnsafe(entity) = value; + } + /// + /// faster getter and setter. + /// + /// + /// some operations *may* invalidate all existing references. + /// they are: + /// in debug mode, register new field and then access it; + /// in release mode, access child class registration. + ///
+ /// this can secretly happen in hook chain. + /// be careful with this one, + /// or switch to simple getter and setter. + ///
+ public abstract ref TRet? GetValueRefUnsafe(T entity); + }; + + public static class DataComponentRegistry { + internal interface SlotHolderBase { + internal int slot { set; } + internal int knownCount { set; } + internal abstract Type declaringType { get; } + internal abstract Type fieldType { get; } + } + + internal static int getHierarchyDepth(Type? self) { + int depth = 0; + while (self is not null) { + self = self.BaseType; + depth++; + } + return depth - 1; + } + + internal static nint registryId = 1; + internal static readonly Dictionary> debugInfos = new(); + internal static readonly Dictionary knownUnloadedIndex = new(); + + internal static readonly Dictionary> infos = new(); + internal static Dictionary>? toOptimize = new(); + + internal static void Optimize() { + if (toOptimize is not { } toop) { + throw new InvalidOperationException("what did it mean"); + } + toOptimize = null; + Dictionary types = new(); + + foreach ((Type k, List o) in infos) { + static int GetOrSet(Dictionary 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? 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; + } + } + } + + internal static void ThrowNotPrepared() { + throw new InvalidOperationException("It's not prepared."); + } + internal sealed class SlotHolder : DataComponentAccessor, SlotHolderBase + where T : patch_Entity where TRet : class? { + public override ref TRet? GetValueRefUnsafe(T self) { + if (slot < 0) { + ThrowNotPrepared(); + } + 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(ref slots[slot]); + } + + public Type declaringType => typeof(T); + public Type fieldType => typeof(TRet); + public int slot { get; set; } = -1; + public int knownCount { get; set; } + } + + internal static void ThrowUnregistered() { + throw new ObjectDisposedException("It's already unregistered."); + } + internal sealed class DebugModeSlotHolder : DataComponentAccessor where T : patch_Entity where TRet : class? { + internal required int depth; + internal required int slot; + internal required nint registryId; + internal required List registered; + public override ref TRet? GetValueRefUnsafe(T self) { + if (slot < 0) { + ThrowUnregistered(); + } + + self.debugSlots ??= new DebugModeDataComponent[getHierarchyDepth(self.GetType())][]; + ref DebugModeDataComponent[]? curDepth = ref self.debugSlots[depth]; + + if (curDepth is not { }) { + curDepth = new DebugModeDataComponent[registered.Count]; + } else if (curDepth.Length <= slot) { + Array.Resize(ref curDepth, registered.Count); + } + + ref DebugModeDataComponent _got = ref curDepth[slot]; + ref object? got = ref _got.content; + + if (registryId != _got.registryId) { + if (registryId == registered[slot].registryId) { + got = null; + } else { + if (_got.registryId != 0) { + throw new InvalidOperationException("Unknown Error."); + } + _got.registryId = registryId; + } + } + + if (got is { } && got is not TRet) { + throw new InvalidCastException(); + } + return ref Unsafe.As(ref got); + } + } + + internal static DataComponentAccessor RegisterForDebug(DataComponentInfo? info) where T : patch_Entity where TRet : class? { + if (!debugInfos.TryGetValue(typeof(T), out List? regList)) { + regList = new(); + debugInfos.Add(typeof(T), regList); + } + nint id = registryId++; + DebugModeDataComponentInfo debugInfo = new() { registryId = id, info = info, knownUnloaded = -1, unloaded = false }; + + Span reg = CollectionsMarshal.AsSpan(regList); + if (knownUnloadedIndex.TryGetValue(typeof(T), out int i)) { + int t = reg[i].knownUnloaded; + if (t != -1) { + knownUnloadedIndex[typeof(T)] = t; + } else { + knownUnloadedIndex.Remove(typeof(T)); + } + reg[i] = debugInfo; + } else { + i = regList.Count; + regList.Add(debugInfo); + } + + return new DebugModeSlotHolder() { registryId = id, slot = i, registered = regList, depth = getHierarchyDepth(typeof(T)) - 1, }; + } + + internal static DataComponentAccessor RegisterFor(DataComponentInfo? info) where T : patch_Entity where TRet : class? { + if (toOptimize is not { } toop) { + throw new InvalidOperationException("Slots have been frozen."); + } + if (!toop.TryGetValue(typeof(T), out List? holderList)) { + holderList = new(); + toop.Add(typeof(T), holderList); + } + if (!infos.TryGetValue(typeof(T), out List? regList)) { + regList = new(); + infos.Add(typeof(T), regList); + } + var slot = new SlotHolder(); + regList.Add(info); + holderList.Add(slot); + return slot; + } + + /// + /// A performant data holder implementation. + /// Allows you to attach any data to a type of entity. + /// + /// + /// it's mainly for external tools, and not actually used anywhere. + /// type your modname and comment here. + /// + /// + /// debug mode will enable type check, dynamic register and unregister. + /// it's not free, so please disable them when publishing. + ///
+ /// a good idea is declare your own wrapper method like this: + /// + /// public static Accessor<T, TRet> RegisterFor<T, TRet>(RegistryInfo? info) where T : Entity where TRet : class? { + /// #if DEBUG + /// bool debug = true; + /// #else + /// bool debug = false; + /// #endif + /// DataComponentRegistry.RegisterFor<T, TRet>(info, debug); + /// } + /// + /// + /// The field accessor. note that your field can be null if it's not initialized. + public static DataComponentAccessor RegisterFor(DataComponentInfo? info, bool debug) where T : patch_Entity where TRet : class? { + if (debug) { + return RegisterForDebug(info); + } else { + return RegisterFor(info); + } + } + + /// + /// You can only unload slots which is registered under debug mode. + /// + /// + /// a good idea is declare your own wrapper method like this: + /// + /// public static void Unregister<T, TRet>(Accessor<T, TRet> accessor) where T : Entity where TRet : class? { + /// #if DEBUG + /// DataComponentRegistry.Unregister(accessor); + /// #endif + /// } + /// + /// + public static void Unregister(DataComponentAccessor accessor) where T : patch_Entity where TRet : class? { + if (accessor is not DebugModeSlotHolder holder) { + if (accessor is SlotHolder) { + throw new ArgumentException("Can't unregister release slot."); + } + throw new ArgumentException("Where did you get this accessor?"); + } + Span reg = CollectionsMarshal.AsSpan(holder.registered); + int target = holder.slot; + if (target < 0 || reg[target].unloaded) { + return; + } + reg[target].unloaded = true; + + if (knownUnloadedIndex.TryGetValue(typeof(T), out int i)) { + reg[target].knownUnloaded = i; + } + + knownUnloadedIndex[typeof(T)] = target; + holder.slot = -1; + } + } +} diff --git a/Celeste.Mod.mm/Patches/Monocle/Entity.cs b/Celeste.Mod.mm/Patches/Monocle/Entity.cs index c6e3fbc9c..453ebe56e 100755 --- a/Celeste.Mod.mm/Patches/Monocle/Entity.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Entity.cs @@ -5,7 +5,10 @@ using System.ComponentModel; namespace Monocle { - class patch_Entity : Entity { + public class patch_Entity : Entity { + internal object[] slots = null; + internal DebugModeDataComponent[][] debugSlots = null; + public new Scene Scene { [MonoModIgnore] get;