|
| 1 | +using Celeste.Mod.Registry; |
| 2 | +using Monocle; |
| 3 | +using System; |
| 4 | +using System.Collections.Generic; |
| 5 | +using System.Diagnostics.CodeAnalysis; |
| 6 | +using System.Runtime.CompilerServices; |
| 7 | +using System.Runtime.InteropServices; |
| 8 | +using static Celeste.Mod.Registry.DataComponentRegistryBase; |
| 9 | +#nullable enable |
| 10 | + |
| 11 | +namespace Celeste.Mod.Registry { |
| 12 | + public class DataComponentInfo { |
| 13 | + public string? ModName; |
| 14 | + public string? Description; |
| 15 | + } |
| 16 | + internal struct DebugModeDataComponentInfo { |
| 17 | + internal DataComponentInfo? info; |
| 18 | + internal bool unloaded; |
| 19 | + internal int knownUnloaded; |
| 20 | + } |
| 21 | + |
| 22 | + internal static class DataComponentRegistryBase { |
| 23 | + internal abstract class SlotHolderBase { |
| 24 | + internal int slot = -1; |
| 25 | + internal int knownCount; |
| 26 | + internal abstract Type declaringType { get; } |
| 27 | + internal abstract Type fieldType { get; } |
| 28 | + internal static void Throw() { |
| 29 | + throw new InvalidOperationException("It's not prepared."); |
| 30 | + } |
| 31 | + } |
| 32 | + internal static int getHierarchyDepth(Type? self) { |
| 33 | + int depth = 0; |
| 34 | + while (self is not null) { |
| 35 | + self = self.BaseType; |
| 36 | + depth++; |
| 37 | + } |
| 38 | + return depth - 1; |
| 39 | + } |
| 40 | + internal static readonly Dictionary<Type, List<DebugModeDataComponentInfo>> debugInfos = new(); |
| 41 | + |
| 42 | + internal static readonly Dictionary<Type, List<DataComponentInfo?>> infos = new(); |
| 43 | + internal static Dictionary<Type, List<SlotHolderBase>>? toOptimize = new(); |
| 44 | + |
| 45 | + internal static void Optimize() { |
| 46 | + if (toOptimize is not { } toop) { |
| 47 | + throw new InvalidOperationException("what did it mean"); |
| 48 | + } |
| 49 | + toOptimize = null; |
| 50 | + Dictionary<Type, int> types = new(); |
| 51 | + |
| 52 | + foreach ((Type k, List<DataComponentInfo?> o) in infos) { |
| 53 | + static int GetOrSet(Dictionary<Type, int> types, Type t, int? hint) { |
| 54 | + if (types.TryGetValue(t, out int cur)) { |
| 55 | + return cur; |
| 56 | + } |
| 57 | + int baseCnt; |
| 58 | + if (t == typeof(Entity)) { |
| 59 | + baseCnt = 0; |
| 60 | + } else { |
| 61 | + baseCnt = GetOrSet(types, t.BaseType!, null); |
| 62 | + } |
| 63 | + return types[t] = baseCnt + (hint ?? infos.GetValueOrDefault(t)?.Count ?? 0); |
| 64 | + } |
| 65 | + GetOrSet(types, k, o.Count); |
| 66 | + } |
| 67 | + foreach ((Type? k, List<SlotHolderBase>? v) in toop) { |
| 68 | + int all = types[k]; |
| 69 | + int bas = all - v.Count; |
| 70 | + for (int i = 0; i < v.Count; i++) { |
| 71 | + v[i].slot = bas + i; |
| 72 | + v[i].knownCount = all; |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + public delegate ref TRet Accessor<T, TRet>(T self) where T : patch_Entity where TRet : class?; |
| 79 | + |
| 80 | + /// <typeparam name="T">Target entity type.</typeparam> |
| 81 | + /// <typeparam name="TRet">Attached data type.</typeparam> |
| 82 | + public static class DataComponentRegistry<T, TRet> where T : patch_Entity where TRet : class? { |
| 83 | + internal class SlotHolder : SlotHolderBase { |
| 84 | + internal ref TRet? ReadSlot(T self) { |
| 85 | + int slot1 = slot; |
| 86 | + if (slot1 < 0) { |
| 87 | + Throw(); |
| 88 | + } |
| 89 | + ref object[] slots = ref self.slots; |
| 90 | + if (slots is not { }) { |
| 91 | + slots = new object[knownCount]; |
| 92 | + } else if (slots.Length <= slot) { |
| 93 | + Array.Resize(ref slots, knownCount); |
| 94 | + } |
| 95 | + return ref Unsafe.As<object, TRet?>(ref slots[slot1]); |
| 96 | + } |
| 97 | + |
| 98 | + internal override Type declaringType => typeof(T); |
| 99 | + internal override Type fieldType => typeof(TRet); |
| 100 | + } |
| 101 | + |
| 102 | + internal class DebugModeSlotHolder { |
| 103 | + internal required int depth; |
| 104 | + internal required int slot; |
| 105 | + internal required List<DebugModeDataComponentInfo> registered; |
| 106 | + internal ref TRet? ReadSlot(T self) { |
| 107 | + self.debugSlots ??= new object[getHierarchyDepth(self.GetType())][]; |
| 108 | + ref object?[]? curDepth = ref self.debugSlots[depth]; |
| 109 | + |
| 110 | + if (curDepth is not { }) { |
| 111 | + curDepth = new object[registered.Count]; |
| 112 | + } else if (curDepth.Length <= slot) { |
| 113 | + Array.Resize(ref curDepth, registered.Count); |
| 114 | + } |
| 115 | + // type check is necessary because you can unregister a slot |
| 116 | + ref object? got = ref curDepth[slot]; |
| 117 | + if (got is { } && got is not TRet) { |
| 118 | + throw new InvalidCastException(); |
| 119 | + } |
| 120 | + return ref Unsafe.As<object?, TRet?>(ref got); |
| 121 | + } |
| 122 | + } |
| 123 | + internal static Accessor<T, TRet?> RegisterForDebug(DataComponentInfo? info) { |
| 124 | + if (!debugInfos.TryGetValue(typeof(T), out List<DebugModeDataComponentInfo>? regList)) { |
| 125 | + regList = new(); |
| 126 | + debugInfos.Add(typeof(T), regList); |
| 127 | + } |
| 128 | + |
| 129 | + DebugModeDataComponentInfo debugInfo = new() { info = info, knownUnloaded = -1, unloaded = false }; |
| 130 | + // TODO: Optimize it to use the linked list which is not implemented yet |
| 131 | + if (regList.FindIndex(d => d.unloaded) is { } i and >= 0) { |
| 132 | + regList[i] = debugInfo; |
| 133 | + } else { |
| 134 | + i = regList.Count; |
| 135 | + regList.Add(debugInfo); |
| 136 | + } |
| 137 | + |
| 138 | + return new DebugModeSlotHolder() { slot = i, registered = regList, depth = getHierarchyDepth(typeof(T)) - 1, }.ReadSlot; |
| 139 | + } |
| 140 | + |
| 141 | + internal static Accessor<T, TRet?> RegisterFor(DataComponentInfo? info) { |
| 142 | + if (toOptimize is not { } toop) { |
| 143 | + throw new InvalidOperationException("Slots have been frozen."); |
| 144 | + } |
| 145 | + if (!toop.TryGetValue(typeof(T), out List<SlotHolderBase>? holderList)) { |
| 146 | + holderList = new(); |
| 147 | + toop.Add(typeof(T), holderList); |
| 148 | + } |
| 149 | + if (!infos.TryGetValue(typeof(T), out List<DataComponentInfo?>? regList)) { |
| 150 | + regList = new(); |
| 151 | + infos.Add(typeof(T), regList); |
| 152 | + } |
| 153 | + var slot = new SlotHolder(); |
| 154 | + regList.Add(info); |
| 155 | + holderList.Add(slot); |
| 156 | + return slot.ReadSlot; |
| 157 | + } |
| 158 | + |
| 159 | + /// <summary> |
| 160 | + /// A performant data holder implementation. |
| 161 | + /// Allows you to attach any data to a type of entity. |
| 162 | + /// </summary> |
| 163 | + /// <remarks> |
| 164 | + /// note: in debug mode, register new field and then access it |
| 165 | + /// *may* invalidate all existing debug references. |
| 166 | + /// this can secretly happens in hook chain. |
| 167 | + /// be careful with this one. |
| 168 | + /// <br/> |
| 169 | + /// nobody except code reloading should be doing this, and it should not happen during Update. |
| 170 | + /// </remarks> |
| 171 | + /// <param name="info"> |
| 172 | + /// it's mainly for external tools, and not actually used anywhere. |
| 173 | + /// type your modname and comment here. |
| 174 | + /// </param> |
| 175 | + /// <param name="debug"> |
| 176 | + /// debug mode will enable type check, dynamic register and unregister. |
| 177 | + /// it's not free, so please disable them when publishing. |
| 178 | + /// <br/> |
| 179 | + /// a good idea is declare your own wrapper method like this: |
| 180 | + /// <code> |
| 181 | + /// public static Accessor<T, TRet> RegisterFor<T, TRet>(RegistryInfo? info) where T : Entity where TRet : class? { |
| 182 | + /// #if DEBUG |
| 183 | + /// bool debug = true; |
| 184 | + /// #else |
| 185 | + /// bool debug = false; |
| 186 | + /// #endif |
| 187 | + /// DataComponentRegistry.RegisterFor<T, TRet>(info, debug); |
| 188 | + /// } |
| 189 | + /// </code> |
| 190 | + /// </param> |
| 191 | + /// <returns>The field accessor. note that your field can be null if it's not initialized.</returns> |
| 192 | + public static Accessor<T, TRet?> RegisterFor(DataComponentInfo? info, bool debug) { |
| 193 | + if (debug) { |
| 194 | + return RegisterForDebug(info); |
| 195 | + } else { |
| 196 | + return RegisterFor(info); |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + internal class GetterSetterWrapper { |
| 201 | + internal readonly Accessor<T, TRet?> accessor; |
| 202 | + |
| 203 | + public GetterSetterWrapper(Accessor<T, TRet?> accessor) { |
| 204 | + this.accessor = accessor; |
| 205 | + } |
| 206 | + |
| 207 | + internal TRet? Getter(T self) => accessor(self); |
| 208 | + internal void Setter(T self, TRet? value) => accessor(self) = value; |
| 209 | + } |
| 210 | + |
| 211 | + /// <remarks> |
| 212 | + /// returns simple getter and setter. |
| 213 | + /// </remarks> |
| 214 | + /// <inheritdoc cref="RegisterFor(DataComponentInfo?, bool)"/> |
| 215 | + public static (Action<T, TRet?> setter, Func<T, TRet?> getter) RegisterForSimple(DataComponentInfo? info, bool debug) { |
| 216 | + var reader = new GetterSetterWrapper(RegisterFor(info, debug)); |
| 217 | + return (reader.Setter, reader.Getter); |
| 218 | + } |
| 219 | + |
| 220 | + /// <inheritdoc cref="Unregister(Accessor{T, TRet?})"/> |
| 221 | + public static void Unregister(Func<T, TRet?> getter) { |
| 222 | + if (getter.Target is not GetterSetterWrapper wrapper) { |
| 223 | + throw new ArgumentException("Where did you get this accessor?"); |
| 224 | + } |
| 225 | + Unregister(wrapper.accessor); |
| 226 | + } |
| 227 | + /// <inheritdoc cref="Unregister(Accessor{T, TRet?})"/> |
| 228 | + public static void Unregister(Action<T, TRet?> setter) { |
| 229 | + if (setter.Target is not GetterSetterWrapper wrapper) { |
| 230 | + throw new ArgumentException("Where did you get this accessor?"); |
| 231 | + } |
| 232 | + Unregister(wrapper.accessor); |
| 233 | + } |
| 234 | + /// <summary> |
| 235 | + /// You can only unload slots which is registered under debug mode. |
| 236 | + /// </summary> |
| 237 | + /// <remarks> |
| 238 | + /// a good idea is declare your own wrapper method like this: |
| 239 | + /// <code> |
| 240 | + /// public static void Unregister<T, TRet>(Accessor<T, TRet> accessor) where T : Entity where TRet : class? { |
| 241 | + /// #if DEBUG |
| 242 | + /// DataComponentRegistry<T, TRet>.Unregister(accessor); |
| 243 | + /// #endif |
| 244 | + /// } |
| 245 | + /// </code> |
| 246 | + /// </remarks> |
| 247 | + public static void Unregister(Accessor<T, TRet?> accessor) { |
| 248 | + if (accessor.Target is not DebugModeSlotHolder holder) { |
| 249 | + if (accessor.Target is SlotHolderBase) { |
| 250 | + throw new ArgumentException("Can't unregister release slot."); |
| 251 | + } |
| 252 | + throw new ArgumentException("Where did you get this accessor?"); |
| 253 | + } |
| 254 | + Span<DebugModeDataComponentInfo> reg = CollectionsMarshal.AsSpan(holder.registered); |
| 255 | + reg[holder.slot].unloaded = true; |
| 256 | + // TODO: Update knownUnloaded to make a linked list so it's faster |
| 257 | + // if anybody care about the loading performance |
| 258 | + } |
| 259 | + } |
| 260 | +} |
0 commit comments