Jump to content

Continue() is very dangerous


King Diamond

Recommended Posts

The use of Continue() just bit me in the buttocks playtesting the quest of my mod.

 

Darn it, at the time I was reading KD's post, back in October, it didn't make any sense to me. Now I find it, when I could have used it yesterday.

 

*sigh* Better late than never.

Link to comment

Sure, if you want a chance to laugh at my ineptitude. I don't mind, and I'm likely to learn something. The following example was never designed to be incorporated into my mod. It's not working code. I did it to basically wipe out the 2-person party for testing purposes, and it failed to actuate, because the variable that started combat was never set.

 

IF
 See([ENEMY])
 OR(2)
Class(LastSeenBy(),MAGE_ALL)
Class(LastSeenBy(),CLERIC_ALL)
 !Race(LastSeenBy(),SLIME)
 !Race(LastSeenBy(),WYVERN)
 Global("B!DeadEnemy","LOCALS",0)
THEN
 RESPONSE #100
ForceSpellPoint([400.350],WIZARD_ANIMATE_DEAD)
ForceSpellPoint([400.300],WIZARD_ANIMATE_DEAD)
ForceSpell(LastSeenBy(),WIZARD_MAGIC_MISSILE)
ForceSpell(LastSeenBy(),WIZARD_MAGIC_MISSILE)
ForceSpell(LastSeenBy(),WIZARD_LARLOCH_MINOR_DRAIN)
ForceSpell(NearestEnemyOf(),WIZARD_LARLOCH_MINOR_DRAIN)
ForceSpell(NearestEnemyOf(),WIZARD_MAGIC_MISSILE)
ForceSpell(NearestEnemyOf(),WIZARD_MELF_ACID_ARROW)
ForceSpell(SecondNearestEnemyOf(),WIZARD_MELF_ACID_ARROW)
ForceSpell(NearestEnemyOf(),WIZARD_AGANNAZAR_SCORCHER)
ForceSpell(SecondNearestEnemyOf(),WIZARD_AGANNAZAR_SCORCHER)
SetGlobal("B!DeadEnemy","LOCALS",1)
Continue()
END

IF
 See([ENEMY]) 
 Global("B!DeadEnemy","LOCALS",1)	  
THEN
 RESPONSE #100
Attack(NearestEnemyOf())
SetGlobal("B!DeadEnemy","LOCALS",2)
Continue()
END

IF
 HPPercentLT(Myself,50)
 HasItem("potn08",Myself)
THEN
 RESPONSE #100
DisplayStringHead(Myself,~quaffs a potion~)
UseItem("potn08",Myself)
Continue()
END

Link to comment

Where exactly did it fail?

 

Running actions after Attack() or AttackReevaluate() in the same action block might not ever do what you want.

 

The first block might be a bit much for Continue(), but the last block shouldn't ever break. (Since B!DeadEnemy will never again be 0, the Continue() in the first block wouldn't serve any purpose anyway.)

 

What happens if you set "GLOBAL" instead of "LOCALS"?

Link to comment

Oddly enough, the last block *never* triggered, despite the fact that the NPC (a 10th level mage) was *very slowly* reduced to 0 hp by a 1st level party of 2. The whole block never executed.

 

I'm going to scrap the whole thing and break it down into smaller chunks, to see where it breaks.

 

Also, I need to eventually get away from forcing spells and utilize memorized spells, but all this is a project for another day.

 

And when I do run it, I'm going to be darned sure that I'm checking that B!DeadEnemies variable to see where it stands. I've been reluctant to use globals to avoid overpopulating the list of them, but that might be silly.

 

Did I say that I'm new to this?

Link to comment

It's likely you were getting stuck on Attack(). AttackReevaluate() holds the queue; when the script is reevaluated, if any other block is true, the engine flushes the queue and immediately switches to that block (meaning that actions after AttackReevaluate() in the same block shouldn't really ever get executed).

 

I don't know the specifics of Attack(), but it's possible that it just similarly flushes the queue until another block runs. In your case, if you're getting hit and dying, the engine is probably just faithfully running the Attack(); if you ever did kill all the enemies, the block would be false, the Attack() action would be invalid, and it's possible the SetGlobal() wouldn't ever run (if the queue is dropped once the enemies being Attack()ed are dead).

 

But it should switch to the last block at that point regardless; if you CTRL+Y all the enemies, the last block should run just fine (assuming the character has low HPs and the right potion).

Link to comment

The script was *for* an enemy, so I'd never find out about whether or not the last block ran if I zapped everybody, including the pc.

 

I think my best bet is to look at existing script (imagine that :p ) and tweak it.

 

I really do appreciate the advice, by the way. Thanks!

Link to comment

Keep in mind that See([ENEMY]) is an allegiance check. It's looking for a creature who has 255 set in their enemy/ally field (or ran Enemy() or ChangeEnemyAlly()), so your enemies were just looking at each other when deciding what to do.

Link to comment

See, I knew that I would learn something.

 

In other words, the reason the creature never attacked was that it was an enemy all right, but it was all by its lonesome, with none of its friends around too see.

 

So, remove See([ENEMY]) checks from NPC scripts where the NPC is *supposed* to be hostile, or they will stop fighting when they are by themselves. Got it.

Link to comment

Correct. You'd want See(NearestEnemyOf()) or similar. This will instruct the engine to appropriately look for a creature that can be seen that is an enemy of the creature running the script (if an enemy is running the script, it would look for the nearest GOODCUTOFF character).

 

Note that See(NearestEnemyOf()) resolves to See(NearestEnemyOf(Myself)).

Link to comment

I had a chance to play with this a little; it looks like the engine may be holding the action queue until the first block that doesn't Continue() runs. In a script like this:

IF
Global("Continue","LOCALS",0)
THEN
RESPONSE #100
DisplayStringHead(Myself,1)
SetGlobal("Continue","LOCALS",1)
Continue()
END

IF
Global("Continue","LOCALS",1)
THEN
RESPONSE #100
DisplayStringHead(Myself,2)
SetGlobal("Continue","LOCALS",2)
Continue()
END

IF
!Global("Continue","LOCALS",2)
THEN
RESPONSE #100
DisplayStringHead(Myself,3)
SetGlobal("Continue","LOCALS",2)
Continue()
END

The game evaluates the first block (returns true). Since the action queue is locked once it hits the Continue(), the second block returns false ("Continue" is still 0), and the final block returns true ("Continue" is still 0, which isn't equal to 2). Since the third block is the final block, the Continue() doesn't have any effect (this is using the default script slot), and the action queue finally runs (strings 1 and 3 run over the character, and the value of "Continue" is 2).

 

This can have interesting consequences with LastSeenBy(): since the queue is locked while the triggers are evaluated, LastSeenBy() will be the object last See()n or Detect()ed. In the following example:

IF
See(Player2)
THEN
RESPONSE #100
DisplayStringHead(LastSeenBy(),1)
Continue()
END

IF
See(Player3)
THEN
RESPONSE #100
DisplayStringHead(LastSeenBy(),2)
Continue()
END

IF
See(Player4)
THEN
RESPONSE #100
DisplayStringHead(LastSeenBy(),3)
Continue()
END

Strings 1, 2, & 3 will all run over Player4's head (since the See() triggers were evaluated before the queue was unlocked, LastSeenBy() is already Player4 when the actions are performed).

 

No amount of the SmallWait()s or Wait()s that some scripts try to use before running Continue() seems to make any difference (the actions don't run until the engine finds a non-Continue() block to execute, so they're entirely useless).

Link to comment

Well, i think GemRB already works this way. In fact, even balrog intended to code it like this.

There is an evaluation phase (this fills an action queue) and an execution phase (this consumes the action queue).

 

I have some trouble with instants and non-instants (got a better name for these?).

Non instants break the action queue execution phase, i think this allows for a new evaluation phase.

I just don't know when to clear the previous action queue. Or when to reaccept a response block into the queue...

There seems to be a very complex mechanism.

Link to comment

I still have little clue as to when or under what circumstances the engine holds, fills, executes, or clears the action queue (I don't think there are any really apparent quirks to be able to test any of the subtleties in handling this crap). :(

 

I'll detail objects to make up for it (unless I already wrote it somewhere and forgot): the engine has 3 major classes of AI objects, which I name dynamic, persistent, and static objects.

 

Dynamic objects consist mainly of the "Nearest" objects and object [] specs. These objects are dynamically assigned based on their relation to the calling object; in order for an object to be marked, it must be within visual range of the caller, be visible to the caller, and be alive. If an object is outside visual range, is invisible to the caller, or is dead, they will never be marked in this fashion. Invisibility detection (193) removes the visibility requirement, and non-actor objects don't respect visibility. When object specs are used, the nearest living object of optional spec that is visible to the caller is used; referring to an object by spec only precludes the use of the spec triggers (they can't be assumed to work in a reliable fashion). Object specs cannot execute ActionOverride() commands. (I think the crappy BG1 party-only objects are also dynamic, but I never wanted to test.) EDIT: they're not, although they do behave dynamically. I'm oversimplifying the handling here (Nearest* truly is dynamic and evaluated as-needed; the specs aren't really dynamic objects in this sense, but they're handled in a similar fashion); laziness.

 

Persistent objects are marked by certain triggers and actions; these are usually the "Last" objects. When these objects are assigned, they remain valid for the session; beyond the assignment condition, there's no additional requirement (they can be inside or outside visual range, visible or invisible, alive or dead). There's a subset of these objects which require that the object be alive; I'd probably dub them automatic or instance objects if they were worthy of their own category. Objects such as LastAttackerOf() and LastSummonerOf() are automatically assigned by the engine after certain events (in this case, suffering a physical attack or being summoned by an object), and remain valid as long as the marked object exists and is alive.

 

Static objects are mainly the "Player" objects, Myself, and script names. They will always refer to the same object with no requirements.

Link to comment

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...