diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs index f46d9dea4..c7eb5edbb 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs @@ -2,6 +2,7 @@ using Celeste.Mod.Core; using Celeste.Mod.Entities; using Celeste.Mod.Helpers; +using Celeste.Mod.Registry; using MAB.DotIgnore; using Microsoft.Xna.Framework; using Monocle; @@ -609,36 +610,68 @@ internal static void ProcessAssembly(EverestModuleMetadata meta, Assembly asm, T patch_Level.EntityLoader loader = null; - ConstructorInfo ctor; + ConstructorInfo ctor = null; MethodInfo gen; gen = type.GetMethod(genName, new Type[] { typeof(Level), typeof(LevelData), typeof(Vector2), typeof(EntityData) }); if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Entity))) { - loader = (level, levelData, offset, entityData) => (Entity) gen.Invoke(null, new object[] { level, levelData, offset, entityData }); + loader = (level, levelData, offset, entityData) => { + var entityId = ((patch_Level)level).CreateEntityId(levelData, entityData); + var entity = (patch_Entity) gen.Invoke(null, new object[] { level, levelData, offset, entityData }); + if (entity != null) { + entity.SourceData = entityData; + entity.SourceId = entityId; + } + + return entity; + }; goto RegisterEntityLoader; } ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2), typeof(EntityID) }); if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset, new EntityID(levelData.Name, entityData.ID + (patch_Level._isLoadingTriggers ? 10000000 : 0)) }); + loader = (level, levelData, offset, entityData) => { + var entityId = ((patch_Level)level).CreateEntityId(levelData, entityData); + var entity = (patch_Entity) ctor.Invoke(new object[] { entityData, offset, entityId }); + entity.SourceData = entityData; + entity.SourceId = entityId; + + return entity; + }; goto RegisterEntityLoader; } ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2) }); if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset }); + loader = (level, levelData, offset, entityData) => { + var entity = (patch_Entity)ctor.Invoke(new object[] { entityData, offset }); + entity.SourceData = entityData; + entity.SourceId = ((patch_Level)level).CreateEntityId(levelData, entityData); + + return entity; + }; goto RegisterEntityLoader; } ctor = type.GetConstructor(new Type[] { typeof(Vector2) }); if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { offset }); + loader = (level, levelData, offset, entityData) => { + var entity = (patch_Entity)ctor.Invoke(new object[] { offset }); + entity.SourceData = entityData; + entity.SourceId = ((patch_Level)level).CreateEntityId(levelData, entityData); + return entity; + }; goto RegisterEntityLoader; } ctor = type.GetConstructor(Type.EmptyTypes); if (ctor != null) { - loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(null); + loader = (level, levelData, offset, entityData) => { + var entity = (patch_Entity)ctor.Invoke(null); + entity.SourceData = entityData; + entity.SourceId = ((patch_Level)level).CreateEntityId(levelData, entityData); + return entity; + }; goto RegisterEntityLoader; } @@ -647,6 +680,13 @@ internal static void ProcessAssembly(EverestModuleMetadata meta, Assembly asm, T Logger.Warn("core", $"Found custom entity without suitable constructor / {genName}(Level, LevelData, Vector2, EntityData): {id} ({type.FullName})"); continue; } + + // Immediately register the connection when we're calling the ctor, + // since we know the return type upfront. + if (ctor != null) { + EntityRegistry.RegisterSidToTypeConnection(id, ctor.DeclaringType); + } + patch_Level.EntityLoaders[id] = loader; } } diff --git a/Celeste.Mod.mm/Mod/Everest/Everest.cs b/Celeste.Mod.mm/Mod/Everest/Everest.cs index fcdec2adf..6f233a7e0 100644 --- a/Celeste.Mod.mm/Mod/Everest/Everest.cs +++ b/Celeste.Mod.mm/Mod/Everest/Everest.cs @@ -1,6 +1,7 @@ using Celeste.Mod.Core; using Celeste.Mod.Helpers; using Celeste.Mod.Helpers.LegacyMonoMod; +using Celeste.Mod.Registry; using Celeste.Mod.UI; using Microsoft.Xna.Framework; using Monocle; @@ -792,6 +793,9 @@ internal static void Unregister(this EverestModule module) { ((Monocle.patch_Commands) Engine.Commands).ReloadCommandsList(); } + if (module is not NullModule) + EntityRegistry.OnModAssemblyUnload(module.GetType().Assembly); + InvalidateInstallationHash(); module.LogUnregistration(); diff --git a/Celeste.Mod.mm/Mod/Registry/EntityRegistry.cs b/Celeste.Mod.mm/Mod/Registry/EntityRegistry.cs new file mode 100644 index 000000000..9a3737687 --- /dev/null +++ b/Celeste.Mod.mm/Mod/Registry/EntityRegistry.cs @@ -0,0 +1,273 @@ +using Celeste.Mod.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Celeste.Mod.Registry; + +public static class EntityRegistry { + private static readonly Dictionary> SidToTypes = new(); + private static readonly Dictionary> TypeToSids = new(); + + private static readonly HashSet EmptyTypeSet = new(); + private static readonly HashSet EmptyStringSet = new(); + + /// + /// Gets a set of all known C# types associated with the given entity sid. + /// Might not necessarily be exhaustive, for example if entities from that sid have not been instantiated yet. + /// + public static IReadOnlySet GetKnownTypesFromSid(string sid) => SidToTypes.GetValueOrDefault(sid) ?? EmptyTypeSet; + + /// + /// Gets a set of all known sids associated with the given C# type. + /// Might not necessarily be exhaustive, for example if entities from that type have not been instantiated yet. + /// + public static IReadOnlySet GetKnownSidsFromType(Type type) => TypeToSids.GetValueOrDefault(type) ?? EmptyStringSet; + + internal static void RegisterSidToTypeConnection(string sid, Type type) { + ref var sidToTypeEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(SidToTypes, sid, out _); + sidToTypeEntry ??= new(1); + sidToTypeEntry.Add(type); + + ref var typeToSidsEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(TypeToSids, type, out _); + typeToSidsEntry ??= new(1); + typeToSidsEntry.Add(sid); + } + + internal static void OnModAssemblyUnload(Assembly asm) { + var types = asm.GetTypesSafe().ToHashSet(); + foreach (var t in types) + TypeToSids.Remove(t); + foreach (var (_, set) in SidToTypes) + set.RemoveWhere(x => types.Contains(x)); + } + + static EntityRegistry() { + // Register vanilla entities, which do not use the [CustomEntity] attribute. + // While the same mechanism as the one used for Everest.Events.Level.LoadEntity works for figuring out these relations, + // this way allows us to know about these relations ahead of time + RegisterSidToTypeConnection("checkpoint", typeof(Checkpoint)); + RegisterSidToTypeConnection("jumpThru", typeof(JumpthruPlatform)); + RegisterSidToTypeConnection("refill", typeof(Refill)); + RegisterSidToTypeConnection("infiniteStar", typeof(FlyFeather)); + RegisterSidToTypeConnection("strawberry", typeof(Strawberry)); + RegisterSidToTypeConnection("memorialTextController", typeof(Strawberry)); + RegisterSidToTypeConnection("goldenBerry", typeof(Strawberry)); + RegisterSidToTypeConnection("summitgem", typeof(SummitGem)); + RegisterSidToTypeConnection("blackGem", typeof(HeartGem)); + RegisterSidToTypeConnection("dreamHeartGem", typeof(DreamHeartGem)); + RegisterSidToTypeConnection("spring", typeof(Spring)); + RegisterSidToTypeConnection("wallSpringLeft", typeof(Spring)); + RegisterSidToTypeConnection("wallSpringRight", typeof(Spring)); + RegisterSidToTypeConnection("fallingBlock", typeof(FallingBlock)); + RegisterSidToTypeConnection("zipMover", typeof(ZipMover)); + RegisterSidToTypeConnection("crumbleBlock", typeof(CrumblePlatform)); + RegisterSidToTypeConnection("dreamBlock", typeof(DreamBlock)); + RegisterSidToTypeConnection("touchSwitch", typeof(TouchSwitch)); + RegisterSidToTypeConnection("switchGate", typeof(SwitchGate)); + RegisterSidToTypeConnection("negaBlock", typeof(NegaBlock)); + RegisterSidToTypeConnection("key", typeof(Key)); + RegisterSidToTypeConnection("lockBlock", typeof(LockBlock)); + RegisterSidToTypeConnection("movingPlatform", typeof(MovingPlatform)); + RegisterSidToTypeConnection("rotatingPlatforms", typeof(RotatingPlatform)); + RegisterSidToTypeConnection("blockField", typeof(BlockField)); + RegisterSidToTypeConnection("cloud", typeof(Cloud)); + RegisterSidToTypeConnection("booster", typeof(Booster)); + RegisterSidToTypeConnection("moveBlock", typeof(MoveBlock)); + RegisterSidToTypeConnection("light", typeof(PropLight)); + RegisterSidToTypeConnection("switchBlock", typeof(SwapBlock)); + RegisterSidToTypeConnection("swapBlock", typeof(SwapBlock)); + RegisterSidToTypeConnection("dashSwitchH", typeof(DashSwitch)); + RegisterSidToTypeConnection("dashSwitchV", typeof(DashSwitch)); + RegisterSidToTypeConnection("templeGate", typeof(TempleGate)); + RegisterSidToTypeConnection("torch", typeof(Torch)); + RegisterSidToTypeConnection("templeCrackedBlock", typeof(TempleCrackedBlock)); + RegisterSidToTypeConnection("seekerBarrier", typeof(SeekerBarrier)); + RegisterSidToTypeConnection("theoCrystal", typeof(TheoCrystal)); + RegisterSidToTypeConnection("glider", typeof(Glider)); + RegisterSidToTypeConnection("theoCrystalPedestal", typeof(TheoCrystalPedestal)); + RegisterSidToTypeConnection("badelineBoost", typeof(BadelineBoost)); + RegisterSidToTypeConnection("cassette", typeof(Cassette)); + RegisterSidToTypeConnection("cassetteBlock", typeof(CassetteBlock)); + RegisterSidToTypeConnection("wallBooster", typeof(WallBooster)); + RegisterSidToTypeConnection("bounceBlock", typeof(BounceBlock)); + RegisterSidToTypeConnection("coreModeToggle", typeof(CoreModeToggle)); + RegisterSidToTypeConnection("iceBlock", typeof(IceBlock)); + RegisterSidToTypeConnection("fireBarrier", typeof(FireBarrier)); + RegisterSidToTypeConnection("eyebomb", typeof(Puffer)); + RegisterSidToTypeConnection("flingBird", typeof(FlingBird)); + RegisterSidToTypeConnection("flingBirdIntro", typeof(FlingBirdIntro)); + RegisterSidToTypeConnection("birdPath", typeof(BirdPath)); + RegisterSidToTypeConnection("lightningBlock", typeof(LightningBreakerBox)); + RegisterSidToTypeConnection("spikesUp", typeof(Spikes)); + RegisterSidToTypeConnection("spikesDown", typeof(Spikes)); + RegisterSidToTypeConnection("spikesLeft", typeof(Spikes)); + RegisterSidToTypeConnection("spikesRight", typeof(Spikes)); + RegisterSidToTypeConnection("triggerSpikesUp", typeof(TriggerSpikes)); + RegisterSidToTypeConnection("triggerSpikesDown", typeof(TriggerSpikes)); + RegisterSidToTypeConnection("triggerSpikesRight", typeof(TriggerSpikes)); + RegisterSidToTypeConnection("triggerSpikesLeft", typeof(TriggerSpikes)); + RegisterSidToTypeConnection("darkChaser", typeof(BadelineOldsite)); + RegisterSidToTypeConnection("rotateSpinner", typeof(BladeRotateSpinner)); + RegisterSidToTypeConnection("rotateSpinner", typeof(DustRotateSpinner)); + RegisterSidToTypeConnection("rotateSpinner", typeof(StarRotateSpinner)); + RegisterSidToTypeConnection("trackSpinner", typeof(BladeTrackSpinner)); + RegisterSidToTypeConnection("trackSpinner", typeof(StarTrackSpinner)); + RegisterSidToTypeConnection("trackSpinner", typeof(DustTrackSpinner)); + RegisterSidToTypeConnection("spinner", typeof(CrystalStaticSpinner)); + RegisterSidToTypeConnection("sinkingPlatform", typeof(SinkingPlatform)); + RegisterSidToTypeConnection("friendlyGhost", typeof(AngryOshiro)); + RegisterSidToTypeConnection("seeker", typeof(Seeker)); + RegisterSidToTypeConnection("seekerStatue", typeof(SeekerStatue)); + RegisterSidToTypeConnection("slider", typeof(Slider)); + RegisterSidToTypeConnection("templeBigEyeball", typeof(TempleBigEyeball)); + RegisterSidToTypeConnection("crushBlock", typeof(CrushBlock)); + RegisterSidToTypeConnection("bigSpinner", typeof(Bumper)); + RegisterSidToTypeConnection("starJumpBlock", typeof(StarJumpBlock)); + RegisterSidToTypeConnection("floatySpaceBlock", typeof(FloatySpaceBlock)); + RegisterSidToTypeConnection("glassBlock", typeof(GlassBlock)); + RegisterSidToTypeConnection("goldenBlock", typeof(GoldenBlock)); + RegisterSidToTypeConnection("fireBall", typeof(FireBall)); + RegisterSidToTypeConnection("risingLava", typeof(RisingLava)); + RegisterSidToTypeConnection("sandwichLava", typeof(SandwichLava)); + RegisterSidToTypeConnection("killbox", typeof(Killbox)); + RegisterSidToTypeConnection("fakeHeart", typeof(FakeHeart)); + RegisterSidToTypeConnection("lightning", typeof(Lightning)); + RegisterSidToTypeConnection("finalBoss", typeof(FinalBoss)); + RegisterSidToTypeConnection("finalBossFallingBlock", typeof(FallingBlock)); + RegisterSidToTypeConnection("finalBossMovingBlock", typeof(FinalBossMovingBlock)); + RegisterSidToTypeConnection("fakeWall", typeof(FakeWall)); + RegisterSidToTypeConnection("fakeBlock", typeof(FakeWall)); + RegisterSidToTypeConnection("dashBlock", typeof(DashBlock)); + RegisterSidToTypeConnection("invisibleBarrier", typeof(InvisibleBarrier)); + RegisterSidToTypeConnection("exitBlock", typeof(ExitBlock)); + RegisterSidToTypeConnection("conditionBlock", typeof(ExitBlock)); + RegisterSidToTypeConnection("coverupWall", typeof(CoverupWall)); + RegisterSidToTypeConnection("crumbleWallOnRumble", typeof(CrumbleWallOnRumble)); + RegisterSidToTypeConnection("ridgeGate", typeof(RidgeGate)); + RegisterSidToTypeConnection("tentacles", typeof(Tentacles)); + RegisterSidToTypeConnection("starClimbController", typeof(StarClimbGraphicsController)); + RegisterSidToTypeConnection("playerSeeker", typeof(PlayerSeeker)); + RegisterSidToTypeConnection("chaserBarrier", typeof(ChaserBarrier)); + RegisterSidToTypeConnection("introCrusher", typeof(IntroCrusher)); + RegisterSidToTypeConnection("bridge", typeof(Bridge)); + RegisterSidToTypeConnection("bridgeFixed", typeof(BridgeFixed)); + RegisterSidToTypeConnection("bird", typeof(BirdNPC)); + RegisterSidToTypeConnection("introCar", typeof(IntroCar)); + RegisterSidToTypeConnection("memorial", typeof(Memorial)); + RegisterSidToTypeConnection("wire", typeof(Wire)); + RegisterSidToTypeConnection("cobweb", typeof(Cobweb)); + RegisterSidToTypeConnection("lamp", typeof(Lamp)); + RegisterSidToTypeConnection("hanginglamp", typeof(HangingLamp)); + RegisterSidToTypeConnection("hahaha", typeof(Hahaha)); + RegisterSidToTypeConnection("bonfire", typeof(Bonfire)); + RegisterSidToTypeConnection("payphone", typeof(Payphone)); + RegisterSidToTypeConnection("colorSwitch", typeof(ClutterSwitch)); + RegisterSidToTypeConnection("clutterDoor", typeof(ClutterDoor)); + RegisterSidToTypeConnection("dreammirror", typeof(DreamMirror)); + RegisterSidToTypeConnection("resortmirror", typeof(ResortMirror)); + RegisterSidToTypeConnection("towerviewer", typeof(Lookout)); + RegisterSidToTypeConnection("picoconsole", typeof(PicoConsole)); + RegisterSidToTypeConnection("wavedashmachine", typeof(WaveDashTutorialMachine)); + RegisterSidToTypeConnection("yellowBlocks", typeof(ClutterBlockBase)); + RegisterSidToTypeConnection("redBlocks", typeof(ClutterBlockBase)); + RegisterSidToTypeConnection("greenBlocks", typeof(ClutterBlockBase)); + RegisterSidToTypeConnection("oshirodoor", typeof(MrOshiroDoor)); + RegisterSidToTypeConnection("templeMirrorPortal", typeof(TempleMirrorPortal)); + RegisterSidToTypeConnection("reflectionHeartStatue", typeof(ReflectionHeartStatue)); + RegisterSidToTypeConnection("resortRoofEnding", typeof(ResortRoofEnding)); + RegisterSidToTypeConnection("gondola", typeof(Gondola)); + RegisterSidToTypeConnection("birdForsakenCityGem", typeof(ForsakenCitySatellite)); + RegisterSidToTypeConnection("whiteblock", typeof(WhiteBlock)); + RegisterSidToTypeConnection("plateau", typeof(Plateau)); + RegisterSidToTypeConnection("soundSource", typeof(SoundSourceEntity)); + RegisterSidToTypeConnection("templeMirror", typeof(TempleMirror)); + RegisterSidToTypeConnection("templeEye", typeof(TempleEye)); + RegisterSidToTypeConnection("clutterCabinet", typeof(ClutterCabinet)); + RegisterSidToTypeConnection("floatingDebris", typeof(FloatingDebris)); + RegisterSidToTypeConnection("foregroundDebris", typeof(ForegroundDebris)); + RegisterSidToTypeConnection("moonCreature", typeof(MoonCreature)); + RegisterSidToTypeConnection("lightbeam", typeof(LightBeam)); + RegisterSidToTypeConnection("door", typeof(Door)); + RegisterSidToTypeConnection("trapdoor", typeof(Trapdoor)); + RegisterSidToTypeConnection("resortLantern", typeof(ResortLantern)); + RegisterSidToTypeConnection("water", typeof(Water)); + RegisterSidToTypeConnection("waterfall", typeof(WaterFall)); + RegisterSidToTypeConnection("bigWaterfall", typeof(BigWaterfall)); + RegisterSidToTypeConnection("clothesline", typeof(Clothesline)); + RegisterSidToTypeConnection("cliffflag", typeof(CliffFlags)); + RegisterSidToTypeConnection("cliffside_flag", typeof(CliffsideWindFlag)); + RegisterSidToTypeConnection("flutterbird", typeof(FlutterBird)); + RegisterSidToTypeConnection("SoundTest3d", typeof(_3dSoundTest)); + RegisterSidToTypeConnection("SummitBackgroundManager", typeof(AscendManager)); + RegisterSidToTypeConnection("summitGemManager", typeof(SummitGem)); + RegisterSidToTypeConnection("heartGemDoor", typeof(HeartGemDoor)); + RegisterSidToTypeConnection("summitcheckpoint", typeof(SummitCheckpoint)); + RegisterSidToTypeConnection("summitcloud", typeof(SummitCloud)); + RegisterSidToTypeConnection("coreMessage", typeof(CoreMessage)); + RegisterSidToTypeConnection("playbackTutorial", typeof(PlayerPlayback)); + RegisterSidToTypeConnection("playbackBillboard", typeof(PlaybackBillboard)); + RegisterSidToTypeConnection("cutsceneNode", typeof(CutsceneNode)); + RegisterSidToTypeConnection("kevins_pc", typeof(KevinsPC)); + RegisterSidToTypeConnection("powerSourceNumber", typeof(PowerSourceNumber)); + RegisterSidToTypeConnection("npc", typeof(NPC00_Granny)); + RegisterSidToTypeConnection("npc", typeof(NPC01_Theo)); + RegisterSidToTypeConnection("npc", typeof(NPC02_Theo)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Oshiro_Cluttter)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Oshiro_Breakdown)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Oshiro_Hallway2)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Oshiro_Hallway1)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Oshiro_Lobby)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Oshiro_Rooftop)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Oshiro_Suite)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Theo_Escaping)); + RegisterSidToTypeConnection("npc", typeof(NPC03_Theo_Vents)); + RegisterSidToTypeConnection("npc", typeof(NPC04_Theo)); + RegisterSidToTypeConnection("npc", typeof(NPC04_Granny)); + RegisterSidToTypeConnection("npc", typeof(NPC05_Badeline)); + RegisterSidToTypeConnection("npc", typeof(NPC05_Theo_Entrance)); + RegisterSidToTypeConnection("npc", typeof(NPC05_Theo_Mirror)); + RegisterSidToTypeConnection("npc", typeof(NPC06_Granny)); + RegisterSidToTypeConnection("npc", typeof(NPC06_Badeline_Crying)); + RegisterSidToTypeConnection("npc", typeof(NPC06_Granny_Ending)); + RegisterSidToTypeConnection("npc", typeof(NPC06_Theo_Ending)); + RegisterSidToTypeConnection("npc", typeof(NPC06_Theo_Plateau)); + RegisterSidToTypeConnection("npc", typeof(NPC07X_Granny_Ending)); + RegisterSidToTypeConnection("npc", typeof(NPC08_Theo)); + RegisterSidToTypeConnection("npc", typeof(NPC08_Granny)); + RegisterSidToTypeConnection("npc", typeof(NPC09_Granny_Outside)); + RegisterSidToTypeConnection("npc", typeof(NPC09_Granny_Inside)); + RegisterSidToTypeConnection("npc", typeof(NPC10_Gravestone)); + RegisterSidToTypeConnection("eventTrigger", typeof(EventTrigger)); + RegisterSidToTypeConnection("musicFadeTrigger", typeof(MusicFadeTrigger)); + RegisterSidToTypeConnection("musicTrigger", typeof(MusicTrigger)); + RegisterSidToTypeConnection("altMusicTrigger", typeof(AltMusicTrigger)); + RegisterSidToTypeConnection("cameraOffsetTrigger", typeof(CameraOffsetTrigger)); + RegisterSidToTypeConnection("lightFadeTrigger", typeof(LightFadeTrigger)); + RegisterSidToTypeConnection("bloomFadeTrigger", typeof(BloomFadeTrigger)); + RegisterSidToTypeConnection("cameraTargetTrigger", typeof(CameraTargetTrigger)); + RegisterSidToTypeConnection("cameraAdvanceTargetTrigger", typeof(CameraAdvanceTargetTrigger)); + RegisterSidToTypeConnection("respawnTargetTrigger", typeof(RespawnTargetTrigger)); + RegisterSidToTypeConnection("changeRespawnTrigger", typeof(ChangeRespawnTrigger)); + RegisterSidToTypeConnection("windTrigger", typeof(WindTrigger)); + RegisterSidToTypeConnection("windAttackTrigger", typeof(WindAttackTrigger)); + RegisterSidToTypeConnection("minitextboxTrigger", typeof(MiniTextboxTrigger)); + RegisterSidToTypeConnection("oshiroTrigger", typeof(OshiroTrigger)); + RegisterSidToTypeConnection("interactTrigger", typeof(InteractTrigger)); + RegisterSidToTypeConnection("checkpointBlockerTrigger", typeof(CheckpointBlockerTrigger)); + RegisterSidToTypeConnection("lookoutBlocker", typeof(LookoutBlocker)); + RegisterSidToTypeConnection("stopBoostTrigger", typeof(StopBoostTrigger)); + RegisterSidToTypeConnection("noRefillTrigger", typeof(NoRefillTrigger)); + RegisterSidToTypeConnection("ambienceParamTrigger", typeof(AmbienceParamTrigger)); + RegisterSidToTypeConnection("creditsTrigger", typeof(CreditsTrigger)); + RegisterSidToTypeConnection("goldenBerryCollectTrigger", typeof(GoldBerryCollectTrigger)); + RegisterSidToTypeConnection("moonGlitchBackgroundTrigger", typeof(MoonGlitchBackgroundTrigger)); + RegisterSidToTypeConnection("blackholeStrength", typeof(BlackholeStrengthTrigger)); + RegisterSidToTypeConnection("rumbleTrigger", typeof(RumbleTrigger)); + RegisterSidToTypeConnection("birdPathTrigger", typeof(BirdPathTrigger)); + RegisterSidToTypeConnection("spawnFacingTrigger", typeof(SpawnFacingTrigger)); + RegisterSidToTypeConnection("detachFollowersTrigger", typeof(DetachStrawberryTrigger)); + } +} \ No newline at end of file diff --git a/Celeste.Mod.mm/Patches/Level.cs b/Celeste.Mod.mm/Patches/Level.cs index 868bdb2d8..572fc1bef 100644 --- a/Celeste.Mod.mm/Patches/Level.cs +++ b/Celeste.Mod.mm/Patches/Level.cs @@ -29,12 +29,12 @@ class patch_Level : Level { // We're effectively in GameLoader, but still need to "expose" private fields to our mod. private float flash; - private Color flashColor = Color.White; - + private Color flashColor = Color.White; + private bool doFlash; - private bool flashDrawPlayer; - + private bool flashDrawPlayer; + private static EventInstance PauseSnapshot; public static EventInstance _PauseSnapshot => PauseSnapshot; @@ -102,19 +102,19 @@ public static void RegisterLoadOverride(Level level, LoadOverride loadOverride) [PatchLevelUpdate] // ... except for manually manipulating the method via MonoModRules public extern new void Update(); - /// - /// Flash the screen a solid color. Respects the user's advanced photosensitivity settings. - /// - /// + /// + /// Flash the screen a solid color. Respects the user's advanced photosensitivity settings. + /// + /// /// Whether the player should render over the flash. [MonoModReplace] // We copy the entirety of this method instead of doing an IL patch because it's 5 lines of code. - public new void Flash(Color color, bool drawPlayerOver = false) { - if (CoreModule.Settings.AllowScreenFlash) { - doFlash = true; - flashDrawPlayer = drawPlayerOver; - flash = 1f; - flashColor = color; - } + public new void Flash(Color color, bool drawPlayerOver = false) { + if (CoreModule.Settings.AllowScreenFlash) { + doFlash = true; + flashDrawPlayer = drawPlayerOver; + flash = 1f; + flashColor = color; + } } [MonoModReplace] @@ -200,19 +200,19 @@ void Unpause() { } Everest.Events.Level.Pause(this, startIndex, minimal, quickReset); - } - + } + /// /// Forcefully close the pause menu; resume from paused. - /// + /// public void Unpause() { - if (Paused) { - PauseMainMenuOpen = false; - if (Entities.FindFirst() is patch_TextMenu menu) - menu.CloseAndRun(Everest.SaveSettings(), null); - Paused = false; - Audio.Play("event:/ui/game/unpause"); - unpauseTimer = 0.15f; + if (Paused) { + PauseMainMenuOpen = false; + if (Entities.FindFirst() is patch_TextMenu menu) + menu.CloseAndRun(Everest.SaveSettings(), null); + Paused = false; + Audio.Play("event:/ui/game/unpause"); + unpauseTimer = 0.15f; } } @@ -401,6 +401,9 @@ private static Player LoadNewPlayerForLevel(Vector2 position, PlayerSpriteMode s #pragma warning restore 0618 } + internal EntityID CreateEntityId(LevelData levelData, EntityData entityData) + => new EntityID(levelData.Name, entityData.ID + (_isLoadingTriggers ? 10000000 : 0)); + /// /// Search for a custom entity that matches the .
/// To register a custom entity, use or .
@@ -413,143 +416,147 @@ public static bool LoadCustomEntity(EntityData entityData, Level level) { LevelData levelData = level.Session.LevelData; Vector2 offset = new Vector2(levelData.Bounds.Left, levelData.Bounds.Top); + var prevStoredData = _currentEntityData; + + // We don't get access to the entity if it got created by this event, + // we'll let EntityList.Add set the entity data on the created entity. + _currentEntityData = entityData; if (Everest.Events.Level.LoadEntity(level, levelData, offset, entityData)) return true; + + // Now let's set this to null, as we have direct access to the entity and can set the entity data directly, + // avoiding the possibility of unrelated entities getting their EntityData set due to being added in a ctor + _currentEntityData = null; - if (EntityLoaders.TryGetValue(entityData.Name, out EntityLoader loader)) { - Entity loaded = loader(level, levelData, offset, entityData); - if (loaded != null) { - level.Add(loaded); - return true; - } - } + Entity loaded = null; - if (entityData.Name == "everest/spaceController") { - level.Add(new SpaceController()); - return true; + if (EntityLoaders.TryGetValue(entityData.Name, out EntityLoader loader)) { + loaded = loader(level, levelData, offset, entityData); } - // The following entities have hardcoded "attributes." - // Everest allows custom maps to set them. - - if (entityData.Name == "spinner") { - if (level.Session.Area.ID == 3 || - (level.Session.Area.ID == 7 && level.Session.Level.StartsWith("d-")) || - entityData.Bool("dust")) { - level.Add(new DustStaticSpinner(entityData, offset)); - return true; - } + if (loaded == null) { + // The following entities have hardcoded "attributes." + // Everest allows custom maps to set them. + switch (entityData.Name) { + case "everest/spaceController": + loaded = new SpaceController(); + break; + case "spinner": + if (level.Session.Area.ID == 3 || (level.Session.Area.ID == 7 && level.Session.Level.StartsWith("d-")) || entityData.Bool("dust")) { + loaded = new DustStaticSpinner(entityData, offset); + break; + } - CrystalColor color = CrystalColor.Blue; - if (level.Session.Area.ID == 5) - color = CrystalColor.Red; - else if (level.Session.Area.ID == 6) - color = CrystalColor.Purple; - else if (level.Session.Area.ID == 10) - color = CrystalColor.Rainbow; - else if ("core".Equals(entityData.Attr("color"), StringComparison.InvariantCultureIgnoreCase)) - color = (CrystalColor) (-1); - else if (!Enum.TryParse(entityData.Attr("color"), true, out color)) - color = CrystalColor.Blue; - - level.Add(new CrystalStaticSpinner(entityData, offset, color)); - return true; - } + CrystalColor color; + switch (level.Session.Area.ID) { + case 5: + color = CrystalColor.Red; + break; + case 6: + color = CrystalColor.Purple; + break; + case 10: + color = CrystalColor.Rainbow; + break; + default: { + if ("core".Equals(entityData.Attr("color"), StringComparison.InvariantCultureIgnoreCase)) + color = (CrystalColor) (-1); + else if (!Enum.TryParse(entityData.Attr("color"), true, out color)) + color = CrystalColor.Blue; + break; + } + } - if (entityData.Name == "trackSpinner") { - if (level.Session.Area.ID == 10 || - entityData.Bool("star")) { - level.Add(new StarTrackSpinner(entityData, offset)); - return true; - } else if (level.Session.Area.ID == 3 || - (level.Session.Area.ID == 7 && level.Session.Level.StartsWith("d-")) || - entityData.Bool("dust")) { - level.Add(new DustTrackSpinner(entityData, offset)); - return true; - } + loaded = new CrystalStaticSpinner(entityData, offset, color); + break; + case "trackSpinner": + if (level.Session.Area.ID == 10 || entityData.Bool("star")) { + loaded = new StarTrackSpinner(entityData, offset); + break; + } + if (level.Session.Area.ID == 3 || (level.Session.Area.ID == 7 && level.Session.Level.StartsWith("d-")) || entityData.Bool("dust")) { + loaded = new DustTrackSpinner(entityData, offset); + break; + } - level.Add(new BladeTrackSpinner(entityData, offset)); - return true; - } + loaded = new BladeTrackSpinner(entityData, offset); + break; + case "rotateSpinner": + if (level.Session.Area.ID == 10 || entityData.Bool("star")) { + loaded = new StarRotateSpinner(entityData, offset); + break; + } + if (level.Session.Area.ID == 3 || (level.Session.Area.ID == 7 && level.Session.Level.StartsWith("d-")) || entityData.Bool("dust")) { + loaded = new DustRotateSpinner(entityData, offset); + break; + } - if (entityData.Name == "rotateSpinner") { - if (level.Session.Area.ID == 10 || - entityData.Bool("star")) { - level.Add(new StarRotateSpinner(entityData, offset)); - return true; - } else if (level.Session.Area.ID == 3 || - (level.Session.Area.ID == 7 && level.Session.Level.StartsWith("d-")) || - entityData.Bool("dust")) { - level.Add(new DustRotateSpinner(entityData, offset)); - return true; + loaded = new BladeRotateSpinner(entityData, offset); + break; + case "checkpoint": + if (entityData.Position == Vector2.Zero && !entityData.Bool("allowOrigin")) { + // Workaround for mod levels with old versions of Ahorn containing a checkpoint at (0, 0): + // Create the checkpoint and avoid the start position update in orig_Load. + loaded = new Checkpoint(entityData, offset); + } + break; + case "cloud": { + patch_Cloud cloud = new Cloud(entityData, offset) as patch_Cloud; + if (entityData.Has("small")) + cloud.Small = entityData.Bool("small"); + loaded = cloud; + break; + } + case "cobweb": { + patch_Cobweb cobweb = new Cobweb(entityData, offset) as patch_Cobweb; + if (entityData.Has("color")) + cobweb.OverrideColors = entityData.Attr("color")?.Split(',').Select(s => Calc.HexToColor(s)).ToArray(); + loaded = cobweb; + break; + } + case "movingPlatform": { + patch_MovingPlatform platform = new MovingPlatform(entityData, offset) as patch_MovingPlatform; + if (entityData.Has("texture")) + platform.OverrideTexture = entityData.Attr("texture"); + loaded = platform; + break; + } + case "sinkingPlatform": { + patch_SinkingPlatform platform = new SinkingPlatform(entityData, offset) as patch_SinkingPlatform; + if (entityData.Has("texture")) + platform.OverrideTexture = entityData.Attr("texture"); + loaded = platform; + break; + } + case "crumbleBlock": { + patch_CrumblePlatform platform = new CrumblePlatform(entityData, offset) as patch_CrumblePlatform; + if (entityData.Has("texture")) + platform.OverrideTexture = entityData.Attr("texture"); + loaded = platform; + break; + } + case "wire": { + Wire wire = new Wire(entityData, offset); + if (entityData.Has("color")) + wire.Color = entityData.HexColor("color"); + loaded = wire; + break; + } } - - level.Add(new BladeRotateSpinner(entityData, offset)); - return true; - } - - if (entityData.Name == "checkpoint" && - entityData.Position == Vector2.Zero && - !entityData.Bool("allowOrigin")) { - // Workaround for mod levels with old versions of Ahorn containing a checkpoint at (0, 0): - // Create the checkpoint and avoid the start position update in orig_Load. - level.Add(new Checkpoint(entityData, offset)); - return true; - } - - if (entityData.Name == "cloud") { - patch_Cloud cloud = new Cloud(entityData, offset) as patch_Cloud; - if (entityData.Has("small")) - cloud.Small = entityData.Bool("small"); - level.Add(cloud); - return true; - } - - if (entityData.Name == "cobweb") { - patch_Cobweb cobweb = new Cobweb(entityData, offset) as patch_Cobweb; - if (entityData.Has("color")) - cobweb.OverrideColors = entityData.Attr("color")?.Split(',').Select(s => Calc.HexToColor(s)).ToArray(); - level.Add(cobweb); - return true; - } - - if (entityData.Name == "movingPlatform") { - patch_MovingPlatform platform = new MovingPlatform(entityData, offset) as patch_MovingPlatform; - if (entityData.Has("texture")) - platform.OverrideTexture = entityData.Attr("texture"); - level.Add(platform); - return true; - } - - if (entityData.Name == "sinkingPlatform") { - patch_SinkingPlatform platform = new SinkingPlatform(entityData, offset) as patch_SinkingPlatform; - if (entityData.Has("texture")) - platform.OverrideTexture = entityData.Attr("texture"); - level.Add(platform); - return true; - } - - if (entityData.Name == "crumbleBlock") { - patch_CrumblePlatform platform = new CrumblePlatform(entityData, offset) as patch_CrumblePlatform; - if (entityData.Has("texture")) - platform.OverrideTexture = entityData.Attr("texture"); - level.Add(platform); - return true; - } - - if (entityData.Name == "wire") { - Wire wire = new Wire(entityData, offset); - if (entityData.Has("color")) - wire.Color = entityData.HexColor("color"); - level.Add(wire); - return true; } - - if (!_LoadStrings.Contains(entityData.Name)) { + + if (loaded != null) { + ((patch_Entity)loaded).SourceData ??= entityData; + if (((patch_Entity)loaded).SourceId.Level is null) + ((patch_Entity)loaded).SourceId = ((patch_Level) level).CreateEntityId(levelData, entityData); + + level.Add(loaded); + } else if (!_LoadStrings.Contains(entityData.Name)) { Logger.Warn("LoadLevel", $"Failed loading entity {entityData.Name}. Room: {entityData.Level.Name} Position: {entityData.Position}"); } - - return false; + + _currentEntityData = prevStoredData; + return loaded != null; } private static object _GCCollectLock = Tuple.Create(new object(), "Level Transition GC.Collect"); @@ -655,6 +662,12 @@ private bool CheckForErrors() { [ThreadStatic] internal static bool _isLoadingTriggers; + + [ThreadStatic] + internal static EntityData _currentEntityData; + + [ThreadStatic] + internal static EntityID _currentEntityId; } public static class LevelExt { @@ -727,6 +740,8 @@ public static void PatchLevelLoader(ILContext context, CustomAttribute attrib) { m_LoadStrings_ctor.DeclaringType = t_LoadStrings; FieldReference f_isLoadingTriggers = context.Method.DeclaringType.FindField("_isLoadingTriggers")!; + FieldReference f_currentEntityData = context.Method.DeclaringType.FindField("_currentEntityData")!; + FieldReference f_currentEntityId = context.Method.DeclaringType.FindField("_currentEntityId")!; MethodReference m_IsInDoNotLoadIncreased = context.Method.DeclaringType.FindMethod("_IsInDoNotLoadIncreased")!; ILCursor cursor = new ILCursor(context); @@ -736,11 +751,27 @@ public static void PatchLevelLoader(ILContext context, CustomAttribute attrib) { // After: string name = (!Level.LoadCustomEntity(entityData2, this)) ? entityData2.Name : ""; int nameLoc = -1; for (int i = 0; i < 2; i++) { + int idLoc = -1; + + // Store the local used for the entityId for later use + cursor.GotoNext( + instr => instr.MatchLdloc(out idLoc), + instr => instr.OpCode == OpCodes.Callvirt && (instr.Operand as MethodReference).GetID() + .Contains("HashSet`1::Contains")); + cursor.GotoNext( instr => instr.MatchLdfld("Celeste.EntityData", "Name"), // cursor.Next (get entity name) instr => instr.MatchStloc(out nameLoc), // cursor.Next.Next (save entity name) instr => instr.MatchLdloc(out _), instr => instr.MatchCall("", "System.UInt32 ComputeStringHash(System.String)")); + + // + _currentEntityData = entityData; + // + _currentEntityId = entityId; + cursor.Emit(OpCodes.Dup); + cursor.EmitStsfld(f_currentEntityData); + cursor.EmitLdloc(idLoc); + cursor.EmitStsfld(f_currentEntityId); + cursor.Emit(OpCodes.Dup); cursor.Emit(OpCodes.Ldarg_0); cursor.Emit(OpCodes.Call, m_LoadCustomEntity); @@ -749,10 +780,28 @@ public static void PatchLevelLoader(ILContext context, CustomAttribute attrib) { cursor.Emit(OpCodes.Ldstr, ""); cursor.Emit(OpCodes.Br_S, cursor.Next.Next); // True -> custom entity loaded, so skip the vanilla handler by saving "" as the entity name cursor.Index++; + + if (i == 0) { + // Go to trigger processing next iteration + cursor.GotoNext(MoveType.After, instr => instr.MatchLdfld("Celeste.LevelData", "Triggers")); + } } // Reset to apply trigger loading patches cursor.Index = 0; + + // + _currentEntityData = null; + // Celeste.ClutterBlockGenerator.Generate(); + cursor.GotoNext(MoveType.AfterLabel, instr => instr.MatchCallOrCallvirt("Celeste.ClutterBlockGenerator", "Generate")); + Instruction oldFinallyEnd = cursor.Next; + cursor.Emit(OpCodes.Ldnull); + Instruction newFinallyEnd = cursor.Prev; + cursor.EmitStsfld(f_currentEntityData); + foreach (ExceptionHandler handler in context.Body.ExceptionHandlers.Where(handler => handler.HandlerEnd == oldFinallyEnd)) { + handler.HandlerEnd = newFinallyEnd; + break; + } + int v_levelData = -1; cursor.GotoNext(MoveType.Before, instr => instr.MatchLdloc(out v_levelData), instr => instr.MatchLdfld("Celeste.LevelData", "Triggers")); // set global flag _isLoadingTriggers to true @@ -771,10 +820,14 @@ public static void PatchLevelLoader(ILContext context, CustomAttribute attrib) { cursor.EmitCall(m_IsInDoNotLoadIncreased); cursor.EmitBrtrue(continueLabel); cursor.GotoNext(MoveType.AfterLabel, instr => instr.MatchLdloc(out _), instr => instr.MatchLdfld("Celeste.LevelData", "FgDecals")); - Instruction oldFinallyEnd = cursor.Next; + oldFinallyEnd = cursor.Next; + + // Clear _currentEntityData + cursor.Emit(OpCodes.Ldnull); + newFinallyEnd = cursor.Prev; + cursor.EmitStsfld(f_currentEntityData); // set _isLoadingTriggers to false cursor.EmitLdcI4(0); - Instruction newFinallyEnd = cursor.Prev; cursor.EmitStsfld(f_isLoadingTriggers); // fix end of finally block foreach (ExceptionHandler handler in context.Body.ExceptionHandlers.Where(handler => handler.HandlerEnd == oldFinallyEnd)) { diff --git a/Celeste.Mod.mm/Patches/Monocle/Entity.cs b/Celeste.Mod.mm/Patches/Monocle/Entity.cs index 5ad2d2eaa..c6e3fbc9c 100755 --- a/Celeste.Mod.mm/Patches/Monocle/Entity.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Entity.cs @@ -1,6 +1,8 @@ -using System; +using Celeste; +using System; using MonoMod; -using Celeste.Mod.Entities; +using Celeste.Mod.Registry; +using System.ComponentModel; namespace Monocle { class patch_Entity : Entity { @@ -11,6 +13,29 @@ class patch_Entity : Entity { private set; } + [EditorBrowsable(EditorBrowsableState.Never)] + private EntityData _sourceData; + + /// + /// EntityData instance used to construct this entity, might be null. + /// + public EntityData SourceData { + get => _sourceData; + set { + // Notify the entity registry about our new data, + // so it can properly track which C# types can be instantiated by a sid. + if (value != null && value != _sourceData) + EntityRegistry.RegisterSidToTypeConnection(value.Name, GetType()); + + _sourceData = value; + } + } + + /// + /// EntityID used to construct this entity, might be default(EntityID). + /// + public EntityID SourceId { get; set; } + public event Action PreUpdate; public event Action PostUpdate; diff --git a/Celeste.Mod.mm/Patches/Monocle/EntityList.cs b/Celeste.Mod.mm/Patches/Monocle/EntityList.cs index ed5406084..dc8c98a97 100644 --- a/Celeste.Mod.mm/Patches/Monocle/EntityList.cs +++ b/Celeste.Mod.mm/Patches/Monocle/EntityList.cs @@ -1,5 +1,7 @@ #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value +using Celeste; +using Celeste.Mod.Registry; using Mono.Cecil; using Mono.Cecil.Cil; using MonoMod; @@ -8,6 +10,7 @@ using MonoMod.Utils; using System; using System.Collections.Generic; +using System.Linq; namespace Monocle { // No public constructors. @@ -37,6 +40,19 @@ internal void ClearEntities() { [MonoModIgnore] [PatchEntityListAdd] public extern void Add(Entity entity); + + private void _HandleEntityData(Entity entity) { + if (patch_Level._currentEntityData is { } data && ((patch_Entity)entity).SourceData is null) { + ((patch_Entity)entity).SourceData = data; + ((patch_Entity)entity).SourceId = patch_Level._currentEntityId; + // We don't reset '_currentEntityData' here, in case an entity Adds entities in the ctor, + // in which case we'll see that other entity before the actual entity, + // meaning the real entity wouldn't get its SourceData set at all, + // which is worse than setting it for an unrelated entity. + // This can only happen with entities added via the legacy Everest.Events.Level.LoadEntity event anyway, + // so this should be very rare in practice. + } + } } public static class EntityListExt { @@ -140,6 +156,14 @@ public static void PatchEntityListAdd(ILContext context, CustomAttribute attrib) .Emit(OpCodes.Newobj, ctor_ArgumentNullException) .Emit(OpCodes.Throw) .MarkLabel(label); + + // Add call to _HandleEntityData to track EntityData -> Entity relations + TypeDefinition t_EntityList = MonoModRule.Modder.FindType("Monocle.EntityList").Resolve(); + MethodDefinition m_EntityList_HandleEntityData = t_EntityList.FindMethod("_HandleEntityData"); + cursor.GotoNext(MoveType.Before, instr => instr.OpCode == OpCodes.Callvirt && (instr.Operand as MethodReference).GetID().Contains("HashSet`1::Add")); + cursor.EmitLdarg0(); + cursor.EmitLdarg1(); + cursor.EmitCall(m_EntityList_HandleEntityData); } } diff --git a/Celeste.Mod.mm/Patches/Monocle/Scene.cs b/Celeste.Mod.mm/Patches/Monocle/Scene.cs index 357e2badb..f9a020d4e 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Scene.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Scene.cs @@ -1,5 +1,8 @@ -using MonoMod; +using Celeste.Mod.Registry; +using MonoMod; using System; +using System.Collections.Generic; +using System.Linq; namespace Monocle { class patch_Scene : Scene { @@ -19,6 +22,23 @@ class patch_Scene : Scene { return Math.Floor(((double) TimeActive - offset - Engine.DeltaTime) / interval) < Math.Floor(((double) TimeActive - offset) / interval); } + + /// + /// Finds all entities created from an EntityData with the specified SID, using the Tracker if possible. + /// + public IEnumerable FindEntitiesWithSid(string sid) { + var types = EntityRegistry.GetKnownTypesFromSid(sid); + switch (types.Count) + { + case 0: + return Enumerable.Empty(); + case 1: + return ((patch_Tracker)Tracker).GetEntitiesTrackIfNeeded(types.First()); + default: + return types.SelectMany(x => ((patch_Tracker)Tracker).GetEntitiesTrackIfNeeded(x)); + } + } + internal void ClearOnEndOfFrame() => OnEndOfFrame = null; } diff --git a/Celeste.Mod.mm/Patches/Monocle/Tracker.cs b/Celeste.Mod.mm/Patches/Monocle/Tracker.cs index 939a89e70..3a968e4dd 100644 --- a/Celeste.Mod.mm/Patches/Monocle/Tracker.cs +++ b/Celeste.Mod.mm/Patches/Monocle/Tracker.cs @@ -5,7 +5,9 @@ using MonoMod; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; namespace Monocle { /// @@ -143,11 +145,18 @@ private static bool AddSpecificType(Type type, Type trackedAsType, Dictionary list) ? list : new List()); List result = tracked[type] = value.Distinct().ToList(); return cnt != result.Count; } + [MonoModIgnore] + internal extern void EntityAdded(Entity entity); + + [MonoModIgnore] + internal extern void ComponentAdded(Component component); + /// /// Ensures the 's tracker contains all entities of all tracked Types from the . /// Must be called if a type is added to the tracker manually and if the 's Tracker isn't refreshed. @@ -158,40 +167,87 @@ private static bool AddSpecificType(Type type, Type trackedAsType, Dictionary public static void Refresh(Scene scene = null, bool force = false) { Scene sceneUpdate = scene ?? Engine.Scene; - if ((sceneUpdate.Tracker as patch_Tracker).currentVersion >= TrackedTypeVersion && !force) { + + var tracker = (sceneUpdate.Tracker as patch_Tracker)!; + if (tracker.currentVersion >= TrackedTypeVersion && !force) { return; } - (sceneUpdate.Tracker as patch_Tracker).currentVersion = TrackedTypeVersion; + tracker.currentVersion = TrackedTypeVersion; foreach (Type entityType in StoredEntityTypes) { - if (!sceneUpdate.Tracker.Entities.ContainsKey(entityType)) { - sceneUpdate.Tracker.Entities.Add(entityType, new List()); - } + ref var list = ref CollectionsMarshal.GetValueRefOrAddDefault(tracker.Entities, entityType, out _); + list ??= new(); + list.Clear(); } foreach (Type componentType in StoredComponentTypes) { - if (!sceneUpdate.Tracker.Components.ContainsKey(componentType)) { - sceneUpdate.Tracker.Components.Add(componentType, new List()); - } + ref var list = ref CollectionsMarshal.GetValueRefOrAddDefault(tracker.Components, componentType, out _); + list ??= new(); + list.Clear(); } + foreach (Entity entity in sceneUpdate.Entities) { foreach (Component component in entity.Components) { - Type componentType = component.GetType(); - if (!TrackedComponentTypes.TryGetValue(componentType, out List componentTypes) - || sceneUpdate.Tracker.Components[componentType].Contains(component)) { - continue; - } - foreach (Type trackedType in componentTypes) { - sceneUpdate.Tracker.Components[trackedType].Add(component); - } - } - Type entityType = entity.GetType(); - if (!TrackedEntityTypes.TryGetValue(entityType, out List entityTypes) - || sceneUpdate.Tracker.Entities[entityType].Contains(entity)) { - continue; - } - foreach (Type trackedType in entityTypes) { - sceneUpdate.Tracker.Entities[trackedType].Add(entity); + tracker.ComponentAdded(component); } + tracker.EntityAdded(entity); } } + + /// + /// Gets all entities of the given type, adding that type to the tracker if needed. + /// + public List GetEntitiesTrackIfNeeded() where T : Entity + => GetEntitiesTrackIfNeeded(typeof(T)); + + /// + /// Gets all components of the given type, adding that type to the tracker if needed. + /// + public List GetComponentsTrackIfNeeded() where T : Component + => GetComponentsTrackIfNeeded(typeof(T)); + + /// + /// Gets all entities of the given type, adding that type to the tracker if needed. + /// Will throw an exception when the type does not derive from Entity. + /// + public List GetEntitiesTrackIfNeeded(Type type) { + if (Entities.TryGetValue(type, out var tracked)) + return tracked; + + // We only validate the type on the cold path + // - if the type does not derive from Entity, it couldn't have been added to Entities in the first place. + if (!typeof(Entity).IsAssignableFrom(type)) + throw new Exception($"Type '{type}' does not derive from Entity."); + + AddTypeToTracker(type); + Refresh(); + + if (Entities.TryGetValue(type, out tracked)) + return tracked; + + // Should never happen + throw new UnreachableException($"Tracking type '{type}' failed for an unknown reason!"); + } + + /// + /// Gets all components of the given type, adding that type to the tracker if needed. + /// Will throw an exception when the type does not derive from Component. + /// + public List GetComponentsTrackIfNeeded(Type type) { + if (Components.TryGetValue(type, out var tracked)) + return tracked; + + // We only validate the type on the cold path + // - if the type does not derive from Component, it couldn't have been added to Components in the first place. + if (!typeof(Component).IsAssignableFrom(type)) + throw new Exception($"Type '{type}' does not derive from Component."); + + AddTypeToTracker(type); + Refresh(); + + if (Components.TryGetValue(type, out tracked)) + return tracked; + + // Should never happen + throw new UnreachableException($"Tracking type '{type}' failed for an unknown reason!"); + } } }