iterators done!

This commit is contained in:
Jill 2023-03-07 22:29:11 +03:00
parent 5f63d0bb46
commit 3a8694ab6f
Signed by: oat
GPG Key ID: 33489AA58A955108
5 changed files with 356 additions and 37 deletions

View File

@ -41,19 +41,29 @@
"jillo/has_mark": true,
"jillo/pebbles_lines": [
"!wait 10",
"A little creature, on the floor of my chamber.",
"!wait 8",
"You have an... interesting appearance. A translucent purposed organism is not something I recognize; and with the mark no less.",
"You don't appear to be a messenger, and I can't give you anything you don't already have. What is it that you want? What is your purpose?",
"The little beasts that tend to visit my chambers seem to seek a way out of the cycle, but I'm hesitant to give it to you.<LINE>I don't want to meddle with other iterators' endeavours more than I already have.",
"...",
"!wait 8",
"I shouldn't be spending my time thinking about this. Go. I have more important work to attend to. Maybe you'll be more of use to another iterator,<LINE>but you have no purpose here.",
"!wait 10",
"!wait 12",
"I shouldn't be spending my time thinking about this. Go. I have more important work to attend to. Maybe you'll be more of use to another iterator,<LINE>but you have no purpose here."
],
"jillo/pebbles_lines_2": [
"..although perhaps it was foolish of me to mention escaping the cycle. If you don't wish to serve your purpose,<LINE>there is another way for you.",
"The old path. Go to the west past the Farm Arrays, and then down into the earth where the land fissures, as deep as you can reach,<LINE>where the ancients built their temples and danced their silly rituals.",
"Now leave, creature. You have no reason to be here anymore, and I'm not willing to<LINE>entertain your existance anymore."
],
"jillo/moon_lines": [
"Hello, <PlayerName>. Just what are you exactly? I don't recognize your blueprints, yet you've been given the mark by an iterator..<LINE>What brings you here? You clearly have a purpose, I'm just not sure what it is...",
"How come you're so jelly? Is this an adaptation for your goal, or a side effect of your creation's circumstances?",
"!wait 10",
"...",
"I'll stop asking so many questions you won't be able to answer, <PlayerName>. You probably don't know your purpose either. Poor thing...",
"!wait 20",
"You're welcome to stay for as long as you need, but I'm not sure what I can offer you besides company."
]
}
}

View File

@ -11,7 +11,6 @@ class Plugin : BaseUnityPlugin {
private const string MOD_ID = "zone.oat.jilloslug";
internal static ManualLogSource Log;
// Add hooks
public void OnEnable() {
Plugin.Log = base.Logger;
@ -22,7 +21,7 @@ class Plugin : BaseUnityPlugin {
ImmuneToDartMaggotsFeature.AddHooks();
MarkFeature.AddHooks();
Pebbles.AddHooks();
Iterators.AddHooks();
} catch (Exception err) {
Logger.LogError($"error initializing: {err}");
}

327
src/story/Iterators.cs Normal file
View File

@ -0,0 +1,327 @@
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!
}
}

View File

@ -1,31 +0,0 @@
using System;
using Mono.Cecil.Cil;
using MonoMod.Cil;
using SlugBase;
using SlugBase.Features;
using static SlugBase.Features.FeatureTypes;
namespace JilloSlug.Story;
internal static class Pebbles {
public static readonly PlayerFeature<string[]> PebblesLines = PlayerStrings("jillo/pebbles_lines");
public static void AddHooks() {
On.SSOracleBehavior.PebblesConversation.AddEvents += PebblesConversation_AddEvents;
}
private static void PebblesConversation_AddEvents(On.SSOracleBehavior.PebblesConversation.orig_AddEvents orig, SSOracleBehavior.PebblesConversation self) {
if (PebblesLines.TryGet(self.convBehav.player, out var 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, self.Translate(line), 0));
}
}
return;
}
orig(self);
}
}

14
todo.txt Normal file
View File

@ -0,0 +1,14 @@
- graphics adjustements
- give jillo a chip
- remove ears
- potentially more
- gameplay
- hold eat to remove food pips for offensive slime mold
- could maybe slow down enemies similar to spit
- still edible
- sleeping transforms all food pips into slime mold (maybe)
- taking damage has a chance to drop slime mold & a pip
- increased lung capacity slightly
- centi changes
- electrocution should only ever stun