From f2d6863216d4ad78e5287a2137fd33c9e030b5a4 Mon Sep 17 00:00:00 2001 From: Lozen <865466388@qq.com> Date: Fri, 4 Oct 2024 17:51:05 +0800 Subject: [PATCH] v2.0.7 - Auto-Watch now supports CrumbleWallOnRumble - Auto-Watch FallingBlock now works when HonlyHelper exists --- Dialog/English.txt | 1 + Dialog/Simplified Chinese.txt | 1 + Source/Gameplay/AutoWatchEntity/Config.cs | 4 +- Source/Gameplay/AutoWatchEntity/CoreLogic.cs | 10 + .../Entity/CrumbleWallOnRumble.cs | 214 ++++++++++++++++++ .../AutoWatchEntity/Entity/CutsceneEntity.cs | 4 + .../AutoWatchEntity/Entity/FallingBlock.cs | 29 ++- .../Gameplay/AutoWatchEntity/HelperClasses.cs | 25 +- Source/Module/Menu/AutoWatchMenu.cs | 3 + Source/Module/TASHelperSettings.cs | 2 + Source/Module/WhatsNew.cs | 3 +- Source/Utils/Extensions.cs | 2 - Source/Utils/ModUtils.cs | 7 +- everest.yaml | 2 +- 14 files changed, 270 insertions(+), 37 deletions(-) create mode 100644 Source/Gameplay/AutoWatchEntity/Entity/CrumbleWallOnRumble.cs diff --git a/Dialog/English.txt b/Dialog/English.txt index cd0a50b..d77db99 100644 --- a/Dialog/English.txt +++ b/Dialog/English.txt @@ -290,6 +290,7 @@ TAS_HELPER_AUTO_WATCH_BADELINEORB= Badeline Orb TAS_HELPER_AUTO_WATCH_BOOSTER= Booster TAS_HELPER_AUTO_WATCH_BUMPER= Bumper TAS_HELPER_AUTO_WATCH_CLOUD= Cloud +TAS_HELPER_AUTO_WATCH_CRUMBLEWALLONRUMBLE= Crumble Wall On Rumble TAS_HELPER_AUTO_WATCH_FALLINGBLOCK= Falling Block TAS_HELPER_AUTO_WATCH_FLINGBIRD= Fling Bird TAS_HELPER_AUTO_WATCH_JELLY= Jelly diff --git a/Dialog/Simplified Chinese.txt b/Dialog/Simplified Chinese.txt index e2938ec..841c2ed 100644 --- a/Dialog/Simplified Chinese.txt +++ b/Dialog/Simplified Chinese.txt @@ -279,6 +279,7 @@ TAS_HELPER_AUTO_WATCH_BADELINEORB= Badeline Orb TAS_HELPER_AUTO_WATCH_BOOSTER= Booster TAS_HELPER_AUTO_WATCH_BUMPER= Bumper TAS_HELPER_AUTO_WATCH_CLOUD= Cloud +TAS_HELPER_AUTO_WATCH_CRUMBLEWALLONRUMBLE= Crumble Wall On Rumble TAS_HELPER_AUTO_WATCH_FALLINGBLOCK= Falling Block TAS_HELPER_AUTO_WATCH_FLINGBIRD= Fling Bird TAS_HELPER_AUTO_WATCH_JELLY= Jelly diff --git a/Source/Gameplay/AutoWatchEntity/Config.cs b/Source/Gameplay/AutoWatchEntity/Config.cs index 01528fa..99e427b 100644 --- a/Source/Gameplay/AutoWatchEntity/Config.cs +++ b/Source/Gameplay/AutoWatchEntity/Config.cs @@ -20,6 +20,8 @@ internal static class Config { public static Mode Cloud => TasHelperSettings.AutoWatch_Cloud; + public static Mode CrumbleWallOnRumble => TasHelperSettings.AutoWatch_CrumbleWallOnRumble; + public static Mode CrushBlock => TasHelperSettings.AutoWatch_Kevin; public static Mode CutsceneEntity => TasHelperSettings.AutoWatch_Cutscene; @@ -77,8 +79,6 @@ internal static class TODO { public static Mode Triggers = Mode.Always; // take care of compatibility with simplified triggers , camera trigger in particular - public static Mode RumbleTrigger = Mode.Always; // show its RumbleRoutine - public static Mode CrumblePlatform = Mode.Always; // disappear and respawn public static Mode Seeker = Mode.Always; // more info diff --git a/Source/Gameplay/AutoWatchEntity/CoreLogic.cs b/Source/Gameplay/AutoWatchEntity/CoreLogic.cs index 3b46e01..d5645a3 100644 --- a/Source/Gameplay/AutoWatchEntity/CoreLogic.cs +++ b/Source/Gameplay/AutoWatchEntity/CoreLogic.cs @@ -153,6 +153,15 @@ internal class AutoWatchRenderer : Component { public bool PreActive; public bool PostActive; + + public new bool Active { + get { + throw new Exception("Use Pre/PostActive Instead!"); + } + set { + throw new Exception("Use Pre/PostActive Instead!"); + } + } public AutoWatchRenderer(RenderMode mode, bool hasUpdate = true, bool hasPreUpdate = false) : base(false, visible: true) { // the component itself doesn't update (so active = false), but pass its "update" to entity.Pre/PostUpdate (to avoid some OoO issue) this.mode = mode; @@ -193,6 +202,7 @@ public override void Added(Entity entity) { entity.PostUpdate += this.UpdateWrapper; // move it here so we don't need to worry about OoO // updates even if the entity itself is not active + // though this will not be called when level is transitioning, frozen, or paused } if (hasPreUpdate) { entity.PreUpdate += this.PreUpdateWrapper; diff --git a/Source/Gameplay/AutoWatchEntity/Entity/CrumbleWallOnRumble.cs b/Source/Gameplay/AutoWatchEntity/Entity/CrumbleWallOnRumble.cs new file mode 100644 index 0000000..ae70ffc --- /dev/null +++ b/Source/Gameplay/AutoWatchEntity/Entity/CrumbleWallOnRumble.cs @@ -0,0 +1,214 @@ +using Celeste.Mod.TASHelper.Utils; +using Monocle; + +namespace Celeste.Mod.TASHelper.Gameplay.AutoWatchEntity; + + +internal class CrumbleWallOnRumbleRenderer : AutoWatchTextRenderer { + + public CrumbleWallOnRumble crumble; + + public bool Initalized = false; + + public Dictionary rumbleTriggers = new Dictionary(); + + public RumbleTrigger currentActiveTrigger = null; + + public int listCurrentIndex; + + public int listTargetIndex; + + public float localTimer; + public CrumbleWallOnRumbleRenderer(RenderMode mode) : base(mode, active: true) { } + + public override void Added(Entity entity) { + base.Added(entity); + crumble = entity as CrumbleWallOnRumble; + } + + [Initialize] + private static void Initialize() { + LevelExtensions.AddToTracker(typeof(RumbleTrigger)); + } + + private const string expectedIEnumeratorName = "d__13"; + + public void DelayedInitialize() { + // if a (persistent) rumble trigger remove the CrumbleBlock, then we need to do nothing + if (!crumble.Collidable || crumble.Scene is null) { + SleepForever(); // we don't remove it, so when users click on the crumble wall, nothing happens + return; + } + + text.Position = crumble.Center; + rumbleTriggers.Clear(); + foreach (RumbleTrigger trigger in crumble.Scene.Tracker.GetEntities()) { + int index = trigger.crumbles.IndexOf(crumble); + if (index != -1) { + rumbleTriggers.Add(trigger, index); + } + } + if (rumbleTriggers.IsEmpty()) { + SleepForever(); + return; + } + + Initalized = true; + } + + public void SleepForever() { + Visible = PostActive = hasUpdate = false; + if (text is not null) { + HiresLevelRenderer.Remove(text); + } + } + + public override void UpdateImpl() { + if (!Initalized) { + DelayedInitialize(); + if (!Initalized) { + return; + } + } + if (!crumble.Collidable || crumble.Scene is null) { + SleepForever(); + return; + } + int fastestCurrentIndex = -1; + float fastestWaitTimer = 9999f; + bool triggerChanged = false; + if (rumbleTriggers.Count(x => x.Key.started) == 1) { + if (currentActiveTrigger is null || !currentActiveTrigger.started) { // how can it be not started + currentActiveTrigger = rumbleTriggers.First(x => x.Key.started).Key; + foreach (Component c in currentActiveTrigger.Components) { + if (c is not Coroutine cor) { + continue; + } + if (cor.Current?.GetType()?.Name?.Equals(expectedIEnumeratorName) ?? false) { + // when communal helper exists (due to SwapImmediately), this won't be shown in the first frame + // when vanilla, for some reason we also don't render its first frame + int state = cor.Current.GetFieldValue("<>1__state"); + switch (state) { + case 1: { + fastestCurrentIndex = -1; + break; + } + case 2: { + List.Enumerator enumerator = cor.Current.GetFieldValue.Enumerator>("<>7__wrap1"); + fastestCurrentIndex = currentActiveTrigger.crumbles.IndexOf(enumerator.Current); + break; + } + default: { + continue; + } + } + fastestWaitTimer = cor.waitTimer; + triggerChanged = true; + } + } + if (!triggerChanged) { + currentActiveTrigger = null; + } + } + } + else { + List slowerTriggers = new List(); + RumbleTrigger lastFastestTrigger = null; + int fastestRecord = int.MaxValue; + foreach (KeyValuePair pair in rumbleTriggers) { + if (pair.Key is RumbleTrigger trigger && trigger.started) { + foreach (Component c in trigger.Components) { + if (c is not Coroutine cor) { + continue; + } + if (cor.Current?.GetType()?.Name?.Equals(expectedIEnumeratorName) ?? false) { + // when communal helper exists (due to SwapImmediately), this won't be shown in the first frame + // when vanilla, for some reason we also don't render its first frame + int state = cor.Current.GetFieldValue("<>1__state"); + int currentBreakingBlock = -1; + switch (state) { + case 1: { + currentBreakingBlock = -1; + break; + } + case 2: { + List.Enumerator enumerator = cor.Current.GetFieldValue.Enumerator>("<>7__wrap1"); + currentBreakingBlock = trigger.crumbles.IndexOf(enumerator.Current); + break; + } + default: { + continue; + } + } + int predictTime = PredictTime(currentBreakingBlock, pair.Value, cor.waitTimer); + if (lastFastestTrigger is null) { + lastFastestTrigger = trigger; + fastestRecord = predictTime; + fastestCurrentIndex = currentBreakingBlock; + fastestWaitTimer = cor.waitTimer; + } + else if (predictTime >= fastestRecord) { + slowerTriggers.Add(trigger); + } + else { + slowerTriggers.Add(lastFastestTrigger); + lastFastestTrigger = trigger; + fastestRecord = predictTime; + fastestCurrentIndex = currentBreakingBlock; + fastestWaitTimer = cor.waitTimer; + } + break; + } + } + } + } + if (currentActiveTrigger != lastFastestTrigger && lastFastestTrigger is not null) { + currentActiveTrigger = lastFastestTrigger; + triggerChanged = true; + } + foreach (RumbleTrigger trigger in slowerTriggers) { + rumbleTriggers.Remove(trigger); + } + } + if (currentActiveTrigger is null) { + Visible = false; + return; + } + if (triggerChanged) { + localTimer = fastestWaitTimer; + listCurrentIndex = fastestCurrentIndex; + listTargetIndex = rumbleTriggers[currentActiveTrigger]; + } + else { + if (localTimer > 0f) { + localTimer -= Engine.DeltaTime; + } + else { + listCurrentIndex++; + localTimer = 0.05f; + } + } + text.content = PredictTime(listCurrentIndex, listTargetIndex, localTimer).ToFrame(); + Visible = true; + } + + public static int PredictTime(int currentIndex, int targetIndex, float waitTimer) { + // if there are multiple different manual triggered RumbleTrigger with different delay, this can be a bit wrong when TimeRate changes + return (targetIndex - currentIndex - 1) * ((0.05f).ToFrameData() + 1) + waitTimer.ToFrameData(); + } +} + +internal class CrumbleWallOnRumbleFactory : IRendererFactory { + public Type GetTargetType() => typeof(CrumbleWallOnRumble); + + public bool Inherited() => true; + public RenderMode Mode() => Config.CrumbleWallOnRumble; + public void AddComponent(Entity entity) { + entity.Add(new CrumbleWallOnRumbleRenderer(Mode()).SleepWhenUltraFastforward()); + } +} + + + + + diff --git a/Source/Gameplay/AutoWatchEntity/Entity/CutsceneEntity.cs b/Source/Gameplay/AutoWatchEntity/Entity/CutsceneEntity.cs index f14bf08..df7f4aa 100644 --- a/Source/Gameplay/AutoWatchEntity/Entity/CutsceneEntity.cs +++ b/Source/Gameplay/AutoWatchEntity/Entity/CutsceneEntity.cs @@ -58,6 +58,10 @@ public override void Added(Entity entity) { } public override void UpdateImpl() { + if (!cs.Running) { + Visible = false; + return; + } if (waitingForCoroutine) { // CS06_Campfire adds its coroutine when OnBegin is called bool found = false; foreach (Component c in cs.Components) { diff --git a/Source/Gameplay/AutoWatchEntity/Entity/FallingBlock.cs b/Source/Gameplay/AutoWatchEntity/Entity/FallingBlock.cs index 8c210b5..3e420a5 100644 --- a/Source/Gameplay/AutoWatchEntity/Entity/FallingBlock.cs +++ b/Source/Gameplay/AutoWatchEntity/Entity/FallingBlock.cs @@ -19,26 +19,37 @@ internal class FallingBlockRenderer : AutoWatchTextRenderer { public Vector2 lastPos; public Vector2 pos; + + public bool Initialized = false; public FallingBlockRenderer(RenderMode mode) : base(mode, active: true) { } public override void Added(Entity entity) { base.Added(entity); lastPos = pos = entity.Position; block = entity as FallingBlock; - if (entity.FindCoroutineComponent("Celeste.FallingBlock+d__21", out Tuple tuple)) { - coroutine = tuple.Item1; - sequence = tuple.Item2; - } - else { - coroutine = null; - sequence = null; - } + } public override void UpdateImpl() { text.Position = block.Center; + if (!Initialized) { + Initialized = true; + if (block.FindCoroutineComponent("Celeste.FallingBlock+d__21", out Tuple tuple)) { + coroutine = tuple.Item1; + sequence = tuple.Item2; + } + else if (block.FindCoroutineComponent("Celeste.Mod.HonlyHelper.RisingBlock+d__5", out Tuple tuple2) + && tuple2.Item2.GetFieldValue("5__8") is IEnumerator enumrator) { + coroutine = tuple2.Item1; + sequence = enumrator; + } + else { + coroutine = null; + sequence = null; + } + } if (state == 3) { - text.content = coroutine.waitTimer.ToFrame(); + text.content = coroutine.waitTimer.ToFrameAllowZero(); Visible = true; } else if (state == 4) { diff --git a/Source/Gameplay/AutoWatchEntity/HelperClasses.cs b/Source/Gameplay/AutoWatchEntity/HelperClasses.cs index 6f30035..3386229 100644 --- a/Source/Gameplay/AutoWatchEntity/HelperClasses.cs +++ b/Source/Gameplay/AutoWatchEntity/HelperClasses.cs @@ -1,5 +1,4 @@ -#define Test_Mod_Compatibility -using Celeste.Mod.TASHelper.Entities; +using Celeste.Mod.TASHelper.Entities; using Celeste.Mod.TASHelper.Utils; using Microsoft.Xna.Framework; using Monocle; @@ -340,10 +339,9 @@ internal static class CoroutineFinder { // note that if it's hooked, then the name will change // like Celeste.FallingBlock+d__21 -> HonlyHelper.RisingBlock+d__5 // even if the block itself is a FallingBlock instead of a RisingBlock - public static bool FindCoroutineComponent(this Entity entity, string compiler_generated_class_name, out Tuple pair) { + public static bool FindCoroutineComponent(this Entity entity, string compiler_generated_class_name, out Tuple pair, bool logError = false) { // e.g. compiler_generated_class_name = Celeste.FallingBlock+d__21 -#if Test_Mod_Compatibility foreach (Component c in entity.Components) { if (c is not Coroutine coroutine) { continue; @@ -352,26 +350,13 @@ public static bool FindCoroutineComponent(this Entity entity, string compiler_ge pair = Tuple.Create(coroutine, func); return true; } - //Logger.Log(LogLevel.Debug, "TASHelper", string.Join(" + ", coroutine.enumerators.Select(functionCall => functionCall.GetType().FullName))); } - // throw new Exception($"AutoWatchEntity: can't find {compiler_generated_class_name} of {entity.GetEntityId()}"); - Logger.Log(LogLevel.Error, "TASHelper", $"AutoWatchEntity: can't find {compiler_generated_class_name} of {entity.GetEntityId()}"); - pair = null; - return false; - -#else - foreach (Component c in entity.Components) { - if (c is not Coroutine coroutine) { - continue; - } - if (coroutine.enumerators.FirstOrDefault(functioncall => functioncall.GetType().FullName == compiler_generated_class_name) is System.Collections.IEnumerator func) { - pair = Tuple.Create(coroutine, func); - return true; - } + if (logError) { + Logger.Log(LogLevel.Error, "TASHelper", $"AutoWatchEntity: can't find {compiler_generated_class_name} of {entity.GetEntityId()}"); } pair = null; return false; -#endif + } public static System.Collections.IEnumerator FindIEnumrator(this Coroutine coroutine, string compiler_generated_class_name) { diff --git a/Source/Module/Menu/AutoWatchMenu.cs b/Source/Module/Menu/AutoWatchMenu.cs index 675d3cc..a31ccc2 100644 --- a/Source/Module/Menu/AutoWatchMenu.cs +++ b/Source/Module/Menu/AutoWatchMenu.cs @@ -55,6 +55,9 @@ public static class AutoWatchMenu { page.Add(new EnumerableSliderExt("Auto Watch Cloud".ToDialogText(), CreateOptions(), TasHelperSettings.AutoWatch_Cloud).Change(value => TasHelperSettings.AutoWatch_Cloud = value)); + page.Add(new EnumerableSliderExt("Auto Watch CrumbleWallOnRumble".ToDialogText(), + CreateOptions(), TasHelperSettings.AutoWatch_CrumbleWallOnRumble).Change(value => TasHelperSettings.AutoWatch_CrumbleWallOnRumble = value)); + page.Add(new EnumerableSliderExt("Auto Watch FallingBlock".ToDialogText(), CreateOptions(), TasHelperSettings.AutoWatch_FallingBlock).Change(value => TasHelperSettings.AutoWatch_FallingBlock = value)); diff --git a/Source/Module/TASHelperSettings.cs b/Source/Module/TASHelperSettings.cs index b48545a..8a22441 100644 --- a/Source/Module/TASHelperSettings.cs +++ b/Source/Module/TASHelperSettings.cs @@ -601,6 +601,8 @@ private void AutoWatchInitialize() { public Mode AutoWatch_Cloud = Mode.Always; + public Mode AutoWatch_CrumbleWallOnRumble = Mode.Always; + public Mode AutoWatch_FallingBlock = Mode.Always; public Mode AutoWatch_FlingBird = Mode.Always; diff --git a/Source/Module/WhatsNew.cs b/Source/Module/WhatsNew.cs index be4a0b6..0a2f1b7 100644 --- a/Source/Module/WhatsNew.cs +++ b/Source/Module/WhatsNew.cs @@ -76,7 +76,8 @@ public static void CreateUpdateLog() { AddLog("2.0.3", "Bugfix: resolve incompatibility with SpeedrunTool", "Addition: Add more options to AutoWatch."); AddLog("2.0.4", "Bugfix: If Cassette tempo = 0 and there's freeze frame, then game gets stuck (thanks @trans_alexa)"); AddLog("2.0.5", "Bugfix: Cutscene is auto-watched even if Auto Watch is not enabled. (thanks @socksygen)"); - AddLog("2.0.6", "Feature: Auto-Watch now supports Triggers, and vanilla / Everest ones are handled well."); + AddLog("2.0.6", "Feature: Auto-Watch now supports Triggers, and vanilla / Everest ones are handled well."); + AddLog("2.0.7", "Feature: Auto-Watch now supports CrumbleWallOnRumble."); UpdateLogs.Sort((x, y) => new Version(y.Item1).CompareTo(new Version(x.Item1))); } diff --git a/Source/Utils/Extensions.cs b/Source/Utils/Extensions.cs index 5e1884a..f47f0fb 100644 --- a/Source/Utils/Extensions.cs +++ b/Source/Utils/Extensions.cs @@ -460,8 +460,6 @@ public static void AddImmediately(this Scene scene, Entity entity) { } public static void RemoveImmediately(this Scene scene, Entity entity) { - // ensure entity is added even if the regular engine update loop is interrupted, e.g. TAS stop - // such entities may be added during gameplay instead of when load level toRemove.Add(entity); } diff --git a/Source/Utils/ModUtils.cs b/Source/Utils/ModUtils.cs index a8be081..77da42d 100644 --- a/Source/Utils/ModUtils.cs +++ b/Source/Utils/ModUtils.cs @@ -41,7 +41,9 @@ public static Assembly GetAssembly(string modName) { public static bool FrostHelperInstalled = false; - public static bool VivHelperInstalled = false; + public static bool VivHelperInstalled = false; + + public static bool CommunalHelperInstalled = false; public static bool PandorasBoxInstalled = false; @@ -63,7 +65,8 @@ public static Assembly GetAssembly(string modName) { public static bool UpsideDown => ExtendedVariantsUtils.UpsideDown; public static void InitializeAtFirst() { FrostHelperInstalled = IsInstalled("FrostHelper"); - VivHelperInstalled = IsInstalled("VivHelper"); + VivHelperInstalled = IsInstalled("VivHelper"); + CommunalHelperInstalled = IsInstalled("CommunalHelper"); PandorasBoxInstalled = IsInstalled("PandorasBox"); ExtendedVariantInstalled = IsInstalled("ExtendedVariantMode"); ChronoHelperInstalled = IsInstalled("ChronoHelper"); diff --git a/everest.yaml b/everest.yaml index 3b083f3..e89a064 100644 --- a/everest.yaml +++ b/everest.yaml @@ -1,5 +1,5 @@ - Name: TASHelper - Version: 2.0.6 + Version: 2.0.7 DLL: bin/Release/net7.0/TASHelper.dll Dependencies: - Name: EverestCore