Pim_gd / SDTDialogueActions / source / src / Main.as — Bitbucket
Here's what DialogueActions does - it calls g.dialogueControl.nextChar whilst the line isn't finished yet.
var queuedBuild:Number = g.dialogueControl.states["queued"]._buildLevel;
while (g.dialogueControl.sayingWord < wordsLength) {
g.dialogueControl.nextChar();
}
if (queuedBuild < g.dialogueControl.states["queued"]._buildLevel || queuedBuild == g.dialogueControl.states["queued"]._maxBuild) {
playQueuedLine();
}
This is g.dialogueControl.nextChar, courtesy of JPEXS free flash decompiler 10.0:
public function nextChar_l() : String
{
var _loc1_:String = null;
if(this.speaking)
{
if(this.sayingWord >= this.words.length)
{
this.stopSpeaking();
return "";
}
_loc1_ = this.words[this.sayingWord].letter(this.sayingChar);
if(this.sayingChar == 0 && this.words[this.sayingWord].actionWord)
{
this.checkWordAction();
return "";
}
this.sayingChar++;
if(this.sayingChar >= this.words[this.sayingWord].length && this.sayingWord < this.words.length - 1)
{
this.sayingSpace = true;
}
if(this.sayingChar >= this.words[this.sayingWord].length)
{
this.nextWord();
}
return _loc1_;
}
return "";
}
Basically, at the end of the word, "this.nextWord()" is called. If the word is a trigger, then at the start of the word, "this.checkWordAction()" is called.
checkWordAction is a long section of code dealing with the various built-in triggers...
public function checkWordAction_l() : void
{
var /*UnknownSlot*/:* = null;
/*UnknownSlot*/ = null;
/*UnknownSlot*/ = uint(0);
switch(this.words[this.sayingWord].action)
{
case "SWALLOW":
/*UnknownSlot*/ = function():*
{
g.her.swallow(0,false);
};
break;
case "DROOL":
/*UnknownSlot*/ = function():*
{
g.her.startDroolingCum(true);
};
break;
case "COUGH":
/*UnknownSlot*/ = g.her.cough;
//.....
if(/*UnknownSlot*/ != null)
{
this.pauseSpeakingForAction(/*UnknownSlot*/);
this.nextWord();
/*UnknownSlot*/();
}
else
{
/*UnknownSlot*/ = this.words[this.sayingWord].action;
this.queuedPhrase = /*UnknownSlot*/;
this.states[QUEUED].maxBuild();
this.nextWord();
}
}
But the point is, look at the ending. ... the if statement is weird though - but some looking at the byte code reveals that
case "DROOL":
/*UnknownSlot*/ = function():*
{
g.her.startDroolingCum(true);
};
That "/*UnknownSlot*/" and the one if the if-statement below AND the one in
are all the same (the decompiler should have seen this as _loc1_).
More to the point: both the true case of the if statement and the false case contain "this.nextWord();".
What this means is that since DialogueActions calls nextChar repeatedly, and the line consists of only triggers because DialogueActions validates this before instantly executing the line, DialogueActions is in effect calling both checkWordAction, which reads the trigger and executes it if it has a meaning (and else, sets the next line attribute - the next line to play, that's how stuff like [intro2] works - the "this.queuedPhrase" bit), and, through checkWordAction, calls nextWord.
So what does nextWord do?
public function nextWord_l() : void
{
this.sayingChar = 0;
this.sayingWord++;
}
Hmm.
Relevant is this section from nextChar:
if(this.sayingWord >= this.words.length)
{
this.stopSpeaking();
return "";
}
_loc1_ = this.words[this.sayingWord].letter(this.sayingChar);
if(this.sayingChar == 0 && this.words[this.sayingWord].actionWord)
{
this.checkWordAction();
return "";
}
"this.sayingWord" is used to check whether the line has ended, and "this.sayingChar" is used to determine whether the current word needs to be checked for trigger - it's also used to print characters of course but that's not very interesting atm.
When you go through "startSpeakingPhrase", which is what SDT's "playLine" function is, it sets sayingWord and sayingChar back to 0. It then waits a single frame, and then calls "nextChar" to start saying the line.
What this means is that if Animtools now works by proxying the nextWord function, then it shouldn't work if the trigger for animtools is at the start of the line.
This because nextWord would trigger after the first word has been said, at which point, with a post proxy, the "sayingWord" would be at 1 and this wouldn't be the word that contained the Animtools trigger.
In the dialogue line 'intro:"[ANIMTOOLS_banana]Testing testing one two three"', the words array works like this:
"[ANIMTOOLS_banana]" is word 0
"Testing" is word 1
"testing" is word 2
"one" is word 3
"two" is word 4
"three" is word 5.
So when you have a post proxy that listens for nextWord, you can check if the next word that is going to be played is an animtools trigger. Except you can't check the first word that way ( word 0) because nextWord doesn't get called.
What this boils down to is that you should just look at the way DialogueActions proxies triggers:
Pim_gd / SDTDialogueActions / source / src / Main.as — Bitbucket
var onTriggerProxy:Object = lProxy.createProxy(g.dialogueControl, "checkWordAction");
onTriggerProxy.addPre(onTrigger, persist);
We proxy checkWordAction with a pre trigger - this is what allows intercepting triggers to be played.
Pim_gd / SDTDialogueActions / source / src / Main.as — Bitbucket
public function onTrigger():* {
var x:Boolean = triggerManager.trigger();
if (x) {
//displayMessageGreen("onTrigger - Success");
return x;
} else {
//displayMessageWhite("onTrigger - Non DA trigger");
}
}
Some commented out debug code here but basically, if the triggerManager says the trigger was recognized, return a value to stop the proxy from calling the base SDT function. Otherwise, don't return a value and let lProxy continue calling the SDT function, which will check the standard triggers, and, failing any matches there, eventually set the next line to be played.
(By the way, "x" is a terrible name for the variable here, boo Pim)
Also important:
Pim_gd / SDTDialogueActions / source / src / TriggerManager.as — Bitbucket
public function triggerFromString(triggerName:String):Boolean {
//dialogueLog("Trigger found: = " + triggerName);
triggerName = triggerName.substring(1, triggerName.length - 1);
var triggerWithArguments:Array = triggerName.split("_");
var result:Boolean = triggerWithArray(triggerWithArguments);
if (result) {
g.dialogueControl.nextWord();
}
return result;
}
if you find your trigger, you'll have to call g.dialogueControl.nextWord by yourself, because that's what checkWordAction used to do - identify the trigger and play the nextWord.
Now for the other reason you shouldn't proxy nextWord with something that may call nextWord.
The more local variables you use, the smaller your callstack may be. If you go past this limit, ActionScript will block execution and thus crash the dialogue (and SDT, but it might get silently swallowed).
See this stackoverflow question for an approximation as to how large your callstack is allowed to get.
Is the stack limit of 5287 in AS3 variable or predefined?
If we thus imagine a long line with about 200 position changes in a row, this might be enough to crash the dialogue.
This is one of the reasons why DialogueActions's _INSTANT line playing system is not actually instant. Let's go all the way back to that snippet:
Pim_gd / SDTDialogueActions / source / src / Main.as — Bitbucket
} else if (StringFunctions.stringEndsWith(currentLine, "_INSTANT")) {
var allTriggers:Boolean = true;
for (var i:uint = 0; i < wordsLength && allTriggers; i++) {
allTriggers = words[i].actionWord;
}
if (allTriggers) {
var queuedBuild:Number = g.dialogueControl.states["queued"]._buildLevel;
while (g.dialogueControl.sayingWord < wordsLength) {
g.dialogueControl.nextChar();
}
if (queuedBuild < g.dialogueControl.states["queued"]._buildLevel || queuedBuild == g.dialogueControl.states["queued"]._maxBuild) {
playQueuedLine();
}
}
}
DialogueActions loops through the words in a line, checks if they're all triggers, and if so, constantly calls nextChar so that all the triggers may resolve. Then, once it's done, it checks if there's a new line queued up. If so, it "plays" this queued line:
Pim_gd / SDTDialogueActions / source / src / Main.as — Bitbucket
private function playQueuedLine():void {
g.dialogueControl.states["queued"].clearBuild();
g.dialogueControl.stopSpeaking();
g.dialogueControl.sayRandomPhrase(g.dialogueControl.queuedPhrase);
}
But the line isn't actually played directly. g.dialogueControl.sayRandomPhrase will look up a random phrase, and then pass it to startSpeakingPhrase, which will set sayingWord to 0 and sayingChar to 0 (and a whole bunch of other things) - but it DOESN'T play the actual line. That happens 1 frame later.
This one frame is important.
It's what keeps the game from choking on an infinite loop. You can build an infinite loop with _INSTANT lines and the only thing you'll break is dialogue. It's also what keeps the callstack small, and thus prevents the game from getting cut off by ActionScript's call stack limits.
Anyway, that's why I'd recommend to proxy checkWordAction instead.
I hope you find my explanation useful.