|
| 1 | +using Microsoft.Xna.Framework; |
| 2 | +using Mono.Cecil.Cil; |
| 3 | +using Monocle; |
| 4 | +using MonoMod.Cil; |
| 5 | +using System; |
| 6 | +using System.Reflection; |
| 7 | +using System.Linq; |
| 8 | +using MonoMod.RuntimeDetour; |
| 9 | +using System.Collections.Generic; |
| 10 | +using Mono.Cecil; |
| 11 | +using MonoMod.Utils; |
| 12 | + |
| 13 | +namespace Celeste.Mod.SpringCollab2020 { |
| 14 | + // The new Grandmaster Heart Side is called ZZ-NewHeartSide and the old one is hidden at ZZ-HeartSide. |
| 15 | + // This requires some specific handling from the Collab Utils: |
| 16 | + // - hiding old GMHS from the journals, and not making it behave like a heart side |
| 17 | + // - making new GMHS behave like a heart side |
| 18 | + // We also want the ending heart of old GMHS to send back to the new GMHS, instead of displaying an endscreen. |
| 19 | + static class GrandmasterHeartSideHelper { |
| 20 | + private static Hook hookIsHeartSide; |
| 21 | + private static ILHook hookLobbyJournal; |
| 22 | + private static ILHook hookOverworldJournal; |
| 23 | + private static Hook hookLevelExitToLobby; |
| 24 | + |
| 25 | + public static void Load() { |
| 26 | + Assembly collabUtils = typeof(CollabUtils2.CollabModule).Assembly; |
| 27 | + |
| 28 | + hookIsHeartSide = new Hook( |
| 29 | + collabUtils.GetType("Celeste.Mod.CollabUtils2.LobbyHelper").GetMethod("IsHeartSide"), |
| 30 | + typeof(GrandmasterHeartSideHelper).GetMethod("modIsHeartSide", BindingFlags.NonPublic | BindingFlags.Static)); |
| 31 | + |
| 32 | + hookOverworldJournal = new ILHook( |
| 33 | + collabUtils.GetType("Celeste.Mod.CollabUtils2.UI.OuiJournalCollabProgressInOverworld").GetConstructor(new Type[] { typeof(OuiJournal) }), |
| 34 | + modOverworldJournal); |
| 35 | + |
| 36 | + hookLobbyJournal = new ILHook( |
| 37 | + collabUtils.GetType("Celeste.Mod.CollabUtils2.UI.OuiJournalCollabProgressInLobby").GetMethod("GeneratePages"), |
| 38 | + modLobbyJournal); |
| 39 | + |
| 40 | + IL.Celeste.Level.CompleteArea_bool_bool_bool += modLevelComplete; |
| 41 | + IL.Celeste.OuiChapterPanel.Render += renderOldGMHSCompletionStamp; |
| 42 | + } |
| 43 | + |
| 44 | + public static void Unload() { |
| 45 | + hookIsHeartSide?.Dispose(); |
| 46 | + hookIsHeartSide = null; |
| 47 | + |
| 48 | + hookLobbyJournal?.Dispose(); |
| 49 | + hookLobbyJournal = null; |
| 50 | + |
| 51 | + hookOverworldJournal?.Dispose(); |
| 52 | + hookOverworldJournal = null; |
| 53 | + |
| 54 | + IL.Celeste.Level.CompleteArea_bool_bool_bool -= modLevelComplete; |
| 55 | + IL.Celeste.OuiChapterPanel.Render -= renderOldGMHSCompletionStamp; |
| 56 | + } |
| 57 | + |
| 58 | + private static bool modIsHeartSide(Func<string, bool> orig, string sid) { |
| 59 | + if (sid == "SpringCollab2020/5-Grandmaster/ZZ-HeartSide") { |
| 60 | + return false; // old GMHS |
| 61 | + } else if (sid == "SpringCollab2020/5-Grandmaster/ZZ-NewHeartSide") { |
| 62 | + return true; // new GMHS |
| 63 | + } |
| 64 | + |
| 65 | + return orig(sid); // ... not GMHS |
| 66 | + } |
| 67 | + |
| 68 | + private static void modLobbyJournal(ILContext il) { |
| 69 | + ILCursor cursor = new ILCursor(il); |
| 70 | + |
| 71 | + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt<SaveData>("get_Areas_Safe"))) { |
| 72 | + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Filtering out old GMHS from journal at {cursor.Index} in IL for OuiJournalProgressInLobby.GeneratePages"); |
| 73 | + cursor.EmitDelegate<Func<List<AreaStats>, List<AreaStats>>>(orig => orig.Where(area => area.SID != "SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToList()); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + private static void modOverworldJournal(ILContext il) { |
| 78 | + ILCursor cursor = new ILCursor(il); |
| 79 | + |
| 80 | + while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdfld<LevelSetStats>("Areas"))) { |
| 81 | + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Filtering out old GMHS from journal at {cursor.Index} in IL for OuiJournalCollabProgressInOverworld ctor"); |
| 82 | + cursor.EmitDelegate<Func<List<AreaStats>, List<AreaStats>>>(orig => orig.Where(area => area.SID != "SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToList()); |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + |
| 87 | + private static void modLevelComplete(ILContext il) { |
| 88 | + ILCursor cursor = new ILCursor(il); |
| 89 | + |
| 90 | + while (cursor.TryGotoNext(MoveType.After, |
| 91 | + instr => instr.OpCode == OpCodes.Ldftn && ((instr.Operand as MethodReference)?.Name.StartsWith("<CompleteArea>") ?? false), |
| 92 | + instr => instr.MatchNewobj<Action>())) { |
| 93 | + |
| 94 | + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Redirecting old GMHS ending to new GMHS at {cursor.Index} in IL for Level.CompleteArea"); |
| 95 | + |
| 96 | + cursor.Emit(OpCodes.Ldarg_0); |
| 97 | + cursor.EmitDelegate<Func<Action, Level, Action>>((orig, self) => { |
| 98 | + if (self.Session.Area.GetSID() == "SpringCollab2020/5-Grandmaster/ZZ-HeartSide") { |
| 99 | + // "return to lobby" but it actually returns to the new GM heart side. |
| 100 | + return () => { |
| 101 | + hookLevelExitToLobby = new Hook( |
| 102 | + typeof(CollabUtils2.UI.LevelExitToLobby).GetMethod("Begin"), |
| 103 | + typeof(GrandmasterHeartSideHelper).GetMethod("modLevelExitToLobby", BindingFlags.NonPublic | BindingFlags.Static)); |
| 104 | + On.Celeste.LevelEnter.Go += modLevelEnter; |
| 105 | + |
| 106 | + Engine.Scene = new CollabUtils2.UI.LevelExitToLobby(LevelExit.Mode.Completed, self.Session); |
| 107 | + }; |
| 108 | + } else { |
| 109 | + // do the usual. |
| 110 | + return orig; |
| 111 | + } |
| 112 | + }); |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + private static void modLevelExitToLobby(Action<CollabUtils2.UI.LevelExitToLobby> orig, CollabUtils2.UI.LevelExitToLobby self) { |
| 117 | + // back up return location. |
| 118 | + string bakSID = CollabUtils2.CollabModule.Instance.Session.LobbySID; |
| 119 | + string bakRoom = CollabUtils2.CollabModule.Instance.Session.LobbyRoom; |
| 120 | + float bakX = CollabUtils2.CollabModule.Instance.Session.LobbySpawnPointX; |
| 121 | + float bakY = CollabUtils2.CollabModule.Instance.Session.LobbySpawnPointY; |
| 122 | + |
| 123 | + // modify it to the new gmhs. |
| 124 | + CollabUtils2.CollabModule.Instance.Session.LobbySID = "SpringCollab2020/5-Grandmaster/ZZ-NewHeartSide"; |
| 125 | + CollabUtils2.CollabModule.Instance.Session.LobbyRoom = null; |
| 126 | + CollabUtils2.CollabModule.Instance.Session.LobbySpawnPointX = 0; |
| 127 | + CollabUtils2.CollabModule.Instance.Session.LobbySpawnPointY = 0; |
| 128 | + |
| 129 | + // run collab utils code. |
| 130 | + orig(self); |
| 131 | + |
| 132 | + // restore old values. |
| 133 | + CollabUtils2.CollabModule.Instance.Session.LobbySID = bakSID; |
| 134 | + CollabUtils2.CollabModule.Instance.Session.LobbyRoom = bakRoom; |
| 135 | + CollabUtils2.CollabModule.Instance.Session.LobbySpawnPointX = bakX; |
| 136 | + CollabUtils2.CollabModule.Instance.Session.LobbySpawnPointY = bakY; |
| 137 | + |
| 138 | + // undo the hook so that future returns to lobby behave normally. |
| 139 | + hookLevelExitToLobby?.Dispose(); |
| 140 | + hookLevelExitToLobby = null; |
| 141 | + } |
| 142 | + |
| 143 | + private static void modLevelEnter(On.Celeste.LevelEnter.orig_Go orig, Session session, bool fromSaveData) { |
| 144 | + // This hook is only applied when returning from the old GMHS. |
| 145 | + // We know we are returning to the start of new GMHS, so set up the session properly for that. |
| 146 | + session.FirstLevel = true; |
| 147 | + session.StartedFromBeginning = true; |
| 148 | + new DynData<Session>(session)["pauseTimerUntilAction"] = false; |
| 149 | + |
| 150 | + orig(session, fromSaveData); |
| 151 | + |
| 152 | + // and undo the hook. |
| 153 | + On.Celeste.LevelEnter.Go -= modLevelEnter; |
| 154 | + } |
| 155 | + |
| 156 | + private static void renderOldGMHSCompletionStamp(ILContext il) { |
| 157 | + ILCursor cursor = new ILCursor(il); |
| 158 | + |
| 159 | + // draw the stamp just after the chapter card. |
| 160 | + if (cursor.TryGotoNext(instr => instr.MatchStfld<OuiChapterPanel>("card")) |
| 161 | + && cursor.TryGotoNext(MoveType.After, instr => instr.MatchCallvirt<MTexture>("Draw"))) { |
| 162 | + |
| 163 | + Logger.Log("SpringCollab2020/GrandmasterHeartSideHelper", $"Injecting GMHS stamp rendering at {cursor.Index} in IL for OuiChapterPanel.Render"); |
| 164 | + |
| 165 | + cursor.Emit(OpCodes.Ldarg_0); |
| 166 | + cursor.EmitDelegate<Action<OuiChapterPanel>>(self => { |
| 167 | + // draw it only if the player beat old grandmaster heart side, and we're actually looking at it. |
| 168 | + if (self.Area.GetSID() == "SpringCollab2020/5-Grandmaster/ZZ-NewHeartSide" |
| 169 | + && (SaveData.Instance.GetAreaStatsFor(AreaData.Get("SpringCollab2020/5-Grandmaster/ZZ-HeartSide").ToKey())?.Modes[0].Completed ?? false)) { |
| 170 | + |
| 171 | + MTN.FileSelect["heart"].Draw(self.Position + new Vector2(-580f, 130f)); |
| 172 | + } |
| 173 | + }); |
| 174 | + } |
| 175 | + } |
| 176 | + } |
| 177 | +} |
0 commit comments