328 lines
16 KiB
C#
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!
|
|
}
|
|
}
|