using System; using System.Text.RegularExpressions; using Mono.Cecil.Cil; using MonoMod.Cil; using MoreSlugcats; using SlugBase; using SlugBase.Features; using static SlugBase.Features.FeatureTypes; using static SSOracleBehavior; using SSAction = SSOracleBehavior.Action; using SubBehavID = SSOracleBehavior.SubBehavior.SubBehavID; namespace JilloSlug.Story; internal static class Iterators { public static readonly PlayerFeature PebblesLines = PlayerStrings("jillo/pebbles_lines"); public static readonly PlayerFeature PebblesLines2 = PlayerStrings("jillo/pebbles_lines_2"); public static readonly PlayerFeature MoonLines = PlayerStrings("jillo/moon_lines"); public static readonly SSAction MeetJilloSS_Init = new SSAction("MeetJilloSS_Init", register: true); public static readonly SSAction MeetJilloSS_Fakeout = new SSAction("MeetJilloSS_Fakeout", register: true); public static readonly SSAction MeetJilloSS_End = new SSAction("MeetJilloSS_End", register: true); public static readonly SSAction MeetJilloDM_Init = new SSAction("MeetJilloDM_Init", register: true); public static readonly SSAction MeetJilloDM_Done = new SSAction("MeetJilloDM_Done", register: true); public static readonly SubBehavID MeetJilloSS = new SubBehavID("MeetJilloSS", register: true); public static readonly SubBehavID MeetJilloDM = new SubBehavID("MeetJilloDM", register: true); public static readonly Conversation.ID Pebbles_Jillo = new Conversation.ID("Pebbles_Jillo", register: true); public static readonly Conversation.ID Pebbles_Jillo_End = new Conversation.ID("Pebbles_Jillo_End", register: true); public static readonly Conversation.ID Moon_Jillo = new Conversation.ID("Moon_Jillo", register: true); public class SSOracleMeetJillo : ConversationBehavior { public override bool Gravity { get { return base.action != MeetJilloSS_Fakeout; } } public SSOracleMeetJillo(SSOracleBehavior owner) : base(owner, MeetJilloSS, Pebbles_Jillo) { owner.getToWorking = 0f; if (ModManager.MMF && owner.oracle.room.game.IsStorySession && owner.oracle.room.game.GetStorySession.saveState.miscWorldSaveData.memoryArraysFrolicked && base.oracle.room.world.rainCycle.timer > base.oracle.room.world.rainCycle.cycleLength / 4) { base.oracle.room.world.rainCycle.timer = base.oracle.room.world.rainCycle.cycleLength / 4; base.oracle.room.world.rainCycle.dayNightCounter = 0; } } public override void Update() { base.Update(); owner.LockShortcuts(); if (base.action == MeetJilloSS_Init) { base.movementBehavior = MovementBehavior.KeepDistance; if (base.inActionCounter > 180) { owner.NewAction(SSAction.General_MarkTalk); } } else if (base.action == SSAction.General_MarkTalk) { base.movementBehavior = MovementBehavior.Talk; if (base.inActionCounter == 15 && (owner.conversation == null || owner.conversation.id != convoID)) { owner.InitateConversation(convoID, this); } if (owner.conversation != null && owner.conversation.id == convoID && owner.conversation.slatedForDeletion) { owner.conversation = null; owner.NewAction(MeetJilloSS_Fakeout); } } else if (base.action == MeetJilloSS_Fakeout) { base.movementBehavior = MovementBehavior.KeepDistance; if (base.inActionCounter > 240) { owner.NewAction(MeetJilloSS_End); } } else if (base.action == MeetJilloSS_End) { base.movementBehavior = MovementBehavior.Talk; if (base.inActionCounter == 80 && (owner.conversation == null || owner.conversation.id != Pebbles_Jillo_End)) { owner.InitateConversation(Pebbles_Jillo_End, this); } if (owner.conversation != null && owner.conversation.id == Pebbles_Jillo_End && owner.conversation.slatedForDeletion) { owner.conversation = null; owner.NewAction(SSAction.ThrowOut_ThrowOut); } } } } public class DMOracleMeetJillo : ConversationBehavior { public DMOracleMeetJillo(SSOracleBehavior owner) : base(owner, MeetJilloDM, Moon_Jillo) { base.owner.TurnOffSSMusic(abruptEnd: true); owner.getToWorking = 0f; if (base.owner.conversation != null) { base.owner.conversation.Destroy(); base.owner.conversation = null; } } public override void Update() { base.Update(); if (base.action == MeetJilloDM_Init) { owner.LockShortcuts(); base.movementBehavior = MovementBehavior.Investigate; if (base.inActionCounter > 200) { owner.NewAction(SSAction.General_MarkTalk); } } else if (base.action == SSAction.General_MarkTalk) { base.movementBehavior = MovementBehavior.Talk; if (base.inActionCounter == 15 && (owner.conversation == null || owner.conversation.id != convoID)) { owner.InitateConversation(convoID, this); } if (owner.conversation != null && owner.conversation.id == convoID && owner.conversation.slatedForDeletion) { owner.conversation = null; owner.NewAction(MeetJilloDM_Done); } } else if (base.action == MeetJilloDM_Done) { owner.getToWorking = 1f; base.movementBehavior = MovementBehavior.Meditate; owner.UnlockShortcuts(); // TODO: why will moon not read pearls? // we need Some savedata to use to indicate this, and this works Well Enough (owner.oracle.room.world.game.session as StoryGameSession).saveState.miscWorldSaveData.smPearlTagged = true; } } } public static void AddHooks() { // patch to load the correct dialog w/ the Conversation.IDs On.SSOracleBehavior.PebblesConversation.AddEvents += PebblesConversation_AddEvents; // patch to enter the Action as appropriate IL.SSOracleBehavior.SeePlayer += SSOracleBehavior_SeePlayer; // patch the action -> subbehavior conversion IL.SSOracleBehavior.NewAction += SSOracleBehavior_NewAction; } // highly references SLOracleBehaviorHasMark.NameForPlayer private static string NameForPlayer(SSOracleBehavior self, bool capitalized = false) { string titleRaw = "creature"; if (UnityEngine.Random.value < 0.4f) { titleRaw = "friend"; } if (self.rainWorld.inGameTranslator.currentLanguage == InGameTranslator.LanguageID.Portuguese) { string title = self.Translate(titleRaw); if (capitalized && InGameTranslator.LanguageID.UsesCapitals(self.rainWorld.inGameTranslator.currentLanguage)) { title = char.ToUpper(title[0]) + title.Substring(1); } return title; } else { string title = self.Translate(titleRaw); string prefix = self.Translate("little"); if (UnityEngine.Random.value < 0.3f) { prefix = self.Translate("gelatinous"); } if (capitalized && InGameTranslator.LanguageID.UsesCapitals(self.rainWorld.inGameTranslator.currentLanguage)) { prefix = char.ToUpper(prefix[0]) + prefix.Substring(1); } return prefix + " " + title; } } // = SLOracleBehaviorHasMark.ReplaceParts public static string ReplaceParts(SSOracleBehavior self, string s) { s = Regex.Replace(s, "", NameForPlayer(self, capitalized: false)); s = Regex.Replace(s, "", NameForPlayer(self, capitalized: true)); s = Regex.Replace(s, "", NameForPlayer(self, capitalized: false)); s = Regex.Replace(s, "", NameForPlayer(self, capitalized: true)); return s; } private static void AddLines(SSOracleBehavior.PebblesConversation self, string[] lines) { foreach (string line in lines) { if (line.StartsWith("!wait ")) { int wait = int.Parse(line.Split(' ')[1]); self.events.Add(new SSOracleBehavior.PebblesConversation.PauseAndWaitForStillEvent(self, self.convBehav, wait)); } else { self.events.Add(new Conversation.TextEvent(self, 0, ReplaceParts(self.owner, self.Translate(line)), 0)); } } } private static void PebblesConversation_AddEvents(On.SSOracleBehavior.PebblesConversation.orig_AddEvents orig, SSOracleBehavior.PebblesConversation self) { if (self.id == Pebbles_Jillo && PebblesLines.TryGet(self.convBehav.player, out var pebsLines)) { AddLines(self, pebsLines); } else if (self.id == Pebbles_Jillo_End && PebblesLines2.TryGet(self.convBehav.player, out var pebsLines2)) { AddLines(self, pebsLines2); } else if (self.id == Moon_Jillo && MoonLines.TryGet(self.convBehav.player, out var moonLines)) { AddLines(self, moonLines); } else { orig(self); } } private static void SSOracleBehavior_SeePlayer(ILContext il) { ILCursor c = new ILCursor(il); // matching for `oracle.ID == MoreSlugcatsEnums.OracleID.DM` c.GotoNext(MoveType.After, i => i.MatchLdarg(0), i => i.MatchLdfld("oracle"), i => i.MatchLdfld("ID"), i => i.MatchLdsfld("DM"), i => i.Match(OpCodes.Call), // this is a mess of generics; not matching this, but it's the equation call i => i.Match(OpCodes.Brfalse), i => i.MatchLdarg(0) // match the next call's ldarg to not remove the label by accident - this is messy! ); // stuff inside this if will be for checking for different characters; let's override this // first let's grab the label to skip to once we're done ILCursor skipC = c.Clone(); // matching for `NewAction(Action.MeetRed_Init);` skipC.GotoNext(MoveType.After, i => i.MatchLdarg(0), i => i.MatchLdsfld("MeetRed_Init"), i => i.MatchCall("NewAction") ); // the if is exited right after this statement skipC.GotoNext(MoveType.Before, i => i.Match(OpCodes.Br)); // capture the label it goes to ILLabel skipLabel = skipC.Next.Operand as ILLabel; // now that we're done, we can do the usual delegate injection // note the ldarg captured during our first match - we don't need to put one down now //c.Emit(OpCodes.Ldarg_0); // implicit c.EmitDelegate>(self => { // we get thrown in here if: // pebbles: first time meeting // past the first meeting, you'll get the usual throw out behavior. this is Fine // moon: all times // this is fine too; we want to tweak all idle lines of dialog if (self.oracle.ID != MoreSlugcatsEnums.OracleID.DM) { // pebbles if (PebblesLines.TryGet(self.player, out var lines)) { self.NewAction(MeetJilloSS_Init); return false; // skip } } else { // moon if (MoonLines.TryGet(self.player, out var lines)) { if ((self.oracle.room.world.game.session as StoryGameSession).saveState.miscWorldSaveData.smPearlTagged) { self.NewAction(MeetJilloDM_Done); } else { self.NewAction(MeetJilloDM_Init); } return false; // skip } } return true; // proceed as usual }); c.Emit(OpCodes.Brfalse, skipLabel); // clean up after ourselves to account for the ldarg we captured c.Emit(OpCodes.Ldarg_0); } private static void SSOracleBehavior_NewAction(ILContext il) { ILCursor c = new ILCursor(il); // match `currSubBehavior.NewAction(action, nextAction);`; this is the first line after the subbehavior decision c.GotoNext(MoveType.Before, i => i.MatchLdarg(0), i => i.MatchLdfld("currSubBehavior"), i => i.MatchLdarg(0), i => i.MatchLdfld("action"), i => i.MatchLdarg(1), i => i.MatchCallOrCallvirt("NewAction") ); // we want to now set the subbehavior variable to whatever corresponds with the action c.Emit(OpCodes.Ldarg_1); // `nextAction` c.Emit(OpCodes.Ldloc_0); // the subbehavior variable c.EmitDelegate>((action, id) => { if (action == MeetJilloSS_Init || action == MeetJilloSS_Fakeout || action == MeetJilloSS_End) { return MeetJilloSS; } else if (action == MeetJilloDM_Init || action == MeetJilloDM_Done) { return MeetJilloDM; } else { return id; } }); // the returned value is the new id; let's set the variable to it c.Emit(OpCodes.Stloc_0); // unrelatedly, we also need to patch setting the subbehavior field based on the id! // match `subBehavior = new SSOracleMeetWhite(this);` c.GotoNext(MoveType.After, i => i.MatchLdarg(0), i => i.MatchNewobj(), i => i.MatchStloc(1), i => i.Match(OpCodes.Br) ); // capture the label it goes to once it's done ILLabel skipLabel = c.Prev.Operand as ILLabel; // capture an ldloc.0 to preserve labels c.GotoNext(MoveType.After, i => i.MatchLdloc(0)); //c.Emit(OpCodes.Ldloc_0); // implicit; the subbehavior variable c.Emit(OpCodes.Ldarg_0); // this c.EmitDelegate>((id, self) => { if (id == MeetJilloSS) { return new SSOracleMeetJillo(self); } else if (id == MeetJilloDM) { return new DMOracleMeetJillo(self); } else { return null; } }); // we either have null or a subbehavior! // let's set the subbehavior to whatever we got - setting it to null won't do anything, since it's already null by default c.Emit(OpCodes.Stloc_1); // now we skip the rest if it's null // stloc pops the value so we must load it back up (https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.stloc_0?view=net-7.0) c.Emit(OpCodes.Ldloc_1); // skip to our skiplabel if it's no longer null // the code actually does this the same way above at `if (subBehavior == null)` c.Emit(OpCodes.Brtrue, skipLabel); // past this point we know it's null; rebuild the bytecode state back to how the rest of it expects it to be // we put down our previously captured ldloc.0 c.Emit(OpCodes.Ldloc_0); // and now we've cleaned everything up! } }