JilloSlug/src/story/Iterators.cs

328 lines
16 KiB
C#

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<string[]> PebblesLines = PlayerStrings("jillo/pebbles_lines");
public static readonly PlayerFeature<string[]> PebblesLines2 = PlayerStrings("jillo/pebbles_lines_2");
public static readonly PlayerFeature<string[]> 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, "<PLAYERNAME>", NameForPlayer(self, capitalized: false));
s = Regex.Replace(s, "<CAPPLAYERNAME>", NameForPlayer(self, capitalized: true));
s = Regex.Replace(s, "<PlayerName>", NameForPlayer(self, capitalized: false));
s = Regex.Replace(s, "<CapPlayerName>", 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<OracleBehavior>("oracle"),
i => i.MatchLdfld<Oracle>("ID"),
i => i.MatchLdsfld<MoreSlugcatsEnums.OracleID>("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<SSAction>("MeetRed_Init"),
i => i.MatchCall<SSOracleBehavior>("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<Func<SSOracleBehavior, bool>>(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<SSOracleBehavior>("currSubBehavior"),
i => i.MatchLdarg(0),
i => i.MatchLdfld<SSOracleBehavior>("action"),
i => i.MatchLdarg(1),
i => i.MatchCallOrCallvirt<SSOracleBehavior.SubBehavior>("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<Func<SSAction, SubBehavID, SubBehavID>>((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<SSOracleBehavior.SSOracleMeetWhite>(),
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<Func<SubBehavID, SSOracleBehavior, SSOracleBehavior.SubBehavior>>((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!
}
}