Jump to content

MoveToPoint, MoveToObject and more, oh my


fuzzie

Recommended Posts

GemRB's implementation of these is buggy as heck; we're probably doing various things completely incorrectly. It's a bit frustrating because I finally got the pathfinder back to something resembling insanity and now I can't work out how we're meant to be using it. Help! I looked through the forums and the web and various documentation and I think I've just ended up with more questions. So I shall put them here in the hope that someone can clarify.

 

GemRB currently doesn't execute *any* new actions while moving, and doesn't reset the path/stop moving when it gets a new action (although it does empty the pending actions queue). So this is almost definitely wrong wrong, but I'm not sure what exactly is able to interrupt a move - any action? The BG1 script compiler guide tries to clarify this by saying that there's no repathing if the new action moves to the same point, and that NoAction doesn't interrupt moves - does this mean that all other actions interrupt moves? What about if something like Attack is running, does that work like the NoInterrupt variant?

 

I guess that SetInterrupt(FALSE) is going to make sure nothing ever interrupts? When does that setting end, when it runs out of actions, or does it last forever? How does it work, just by not evaluating any triggers until it's interruptable again? I assume ClearActions() does the obvious thing, too.. I see that IESDP documents AttackReevaluate() as only evaluating triggers again once the timing has expired, which is perhaps a good hint as to how it works .. same for RunAwayFromNoInterrupt() / MoveToPointNoInterrupt() / etc, documented as "Conditions are not checked until the time has expired". But then I've learnt not to trust IESDP!

 

How does MoveToObject interact with this all? Right now our implementation of that just adds an internal MoveToPoint (to the current object location) and doesn't remove itself from the queue, but that is a complete nightmare because you end up chasing moving actors all over the place (or more generally, actors chasing each other all around the place) - since our internal implementations of Attack and Dialogue and etc do the same. Should we be repeatedly building new walks to the destinations if the target moved?

 

I note that IESDP talks about MoveToObject not updating the current position in the CRE data, does anyone know how if that's true? It doesn't seem to make much sense! It also talks about not updating the current position for MoveToPoint until the walking is done - is that really true? Does it really save the ARE files like that? It sounds a bit mad. :( I guess it's easily tested.

 

I guess that's more than enough late-night rambling for one post..

Link to comment

Interruptibility and the state of the action queue is a *major* part of script execution and varies wildly between just about every flavor of IE in existence.

 

MoveToPoint() and MoveToObject() are interruptible actions in BG2 (as are just about everything else). The engine will not wait until you reach the destination if it finds something else for you to do. Attack also is interruptible, but this can get weird with the way attacks work and the way the engine runs the script (if the engine gets to the same block with nothing else to do, then it doesn't actually re-initiate any actions--it just keeps running the same one--which would suck for attacking since you'd be fighting the same guy forever: enter AttackReevaluate, which periodically forces a check for a new target even if we're still stuck in this same block).

 

As expected, MoveToPointNoInterrupt() forcibly locks the script queue. No new actions can be performed until completion or cancelation of the current action. RunAwayFromNoInterrupt() is similar, but the entire action quits after the flee period (in ticks).

 

MoveToObject() is basically just MoveToPoint(target.loc), but it can get weird for moving targets. You'll stop moving once you catch up to the object, but I believe there are also instances where the engine will declare you unable to reach the target and just give up. Somebody would have to test in the game to see if the coordinates are really ever updated or if it's just the same action getting called again after returning. MoveToObjectFollow() is more persistent, I think.

 

Actor structures are stored with two locations: a default "home" location and a "dirty" current location. Most forms of dynamic AI movement, like random walking or fleeing or attacking, only update the current location, and the engine will periodically reset all actors to their home locations. Some forms of permanent scripted movement, like JumpToPoint(), don't update the home location, so characters can eventually get reset to weird spots (cue Hendak).

 

There was an issue where MoveToPointNoInterrupt() could send creatures out into the ether if the area was left before they reached their destination, I believe, but I don't think it's much of a problem anymore. In almost all cases, it doesn't actually matter when the coordinates get updated; it's always been possible to completely break stuff if you save prematurely (like saving in the middle of a big block that sets variables early and has a bunch of Wait()s or persistent actions and then reloading).

Link to comment
MoveToObject() is basically just MoveToPoint(target.loc), but it can get weird for moving targets. You'll stop moving once you catch up to the object, but I believe there are also instances where the engine will declare you unable to reach the target and just give up. Somebody would have to test in the game to see if the coordinates are really ever updated or if it's just the same action getting called again after returning. MoveToObjectFollow() is more persistent, I think.

 

Some quick experimenting before breakfast:

 

Using a single-shot ActionOverride on a PC without scripting, I found that MoveToObject() will constantly re-pathfind while moving towards the object, if the destination object moves. It stops when it touches the object. Dialogue() works like this too. MoveToObjectFollow() will re-pathfind forever (until interrupted), and stay at a further distance than touching.

 

If the actor gets stuck (unable to find a path, or ending up on the other side of a closed door) with any of these actions, it will wait forever (twitching) for the door to be opened or for it to find another path, and then continue following.

 

Attack() is constantly re-pathfinding to touch the target until the fight is over, too. AttackReevaluate() seems to do the same (bearing in mind that I'm using ActionOverride without scripts here, so the action doesn't get interrupted by another one).

Link to comment
Using a single-shot ActionOverride on a PC without scripting, I found that MoveToObject() will constantly re-pathfind while moving towards the object, if the destination object moves. It stops when it touches the object. Dialogue() works like this too. MoveToObjectFollow() will re-pathfind forever (until interrupted), and stay at a further distance than touching.

 

If the actor gets stuck (unable to find a path, or ending up on the other side of a closed door) with any of these actions, it will wait forever (twitching) for the door to be opened or for it to find another path, and then continue following.

 

Attack() is constantly re-pathfinding to touch the target until the fight is over, too. AttackReevaluate() seems to do the same (bearing in mind that I'm using ActionOverride without scripts here, so the action doesn't get interrupted by another one).

Yeah, normally, I believe the AI would kick in (since these actions would usually be coming from the object's own script) and allow the action to be restarted (if this were an Attack() block and you moved behind a closed door, the object eventually gives up and finds somebody else to hit IIRC). But it may also have something to do with object matching: something like Player2 is always valid if Player2 is in the party, but something like [PC] is only valid if there's a living EA PC actor that's visible to the acting object--the engine may eventually decide that [PC] doesn't match any object and break the action.

 

Attack() and such are coded to run until the target dies (unless the engine finds something else for the runner to do), and I believe death and removal of the target are sufficient to also stop all of the above.

Link to comment
But it may also have something to do with object matching: something like Player2 is always valid if Player2 is in the party, but something like [PC] is only valid if there's a living EA PC actor that's visible to the acting object--the engine may eventually decide that [PC] doesn't match any object and break the action.

 

I just tested this with [PC] (using MoveToObject([PC]) in an ActionOverride) and indeed the object seems to be re-evaluated constantly - if you jump the nearest PC away (or just make it run away) then it will pick the next nearest PC, and if there are no more PCs present then it will give up and stop.

 

I hope I am wrong because that means we have to re-evaluate blocking actions every frame! It also seems difficult to tell how it would interact with things like LastSeenBy - does LastSeenBy persist forever until changed or do the triggers get re-evaluated? I guess "persist forever" is the right answer, looking at your post http://forums.gibberlings3.net/index.php?s...&st=30& - we are wiping LastSeenBy every frame right now, oops? Still, if it's an interruptable action and one of the triggers does a See(), I wonder if something like MoveToObject(LastSeenBy(Myself)) should be updated to see the new LastSeenBy, or whether LastSeenBy should only *truly* be updated if the block is actually executed?

Link to comment

LastSeenBy() persists forever until you see again. As far as I know, the engine doesn't bother with it at all except in the case that some object "looks" (through See() or Detect() or whatever). If the engine is constantly evaluating See() and picks out a new object that can be seen, then LastSeenBy() is changed, but failure does nothing -- they didn't dub the object LastSeenAndCanStillBeSeenBy, after all! :-)

 

Given the way it works, though, I believe you're into the same problem as Attack() -- even if the LastSeenBy() object changes, the engine knows you're still hitting the block (See(targetToMoveTo)) that's currently running and doesn't actually do anything (you're still MoveToObject(LastSeenBy(Me, but only the first time I looked and started moving)). The object [] specs, however, have always been determined in a more dynamic fashion (this is the reason you can manually get an NPC to talk to a specific party member by moving the other PCs away when you see it approaching for Dialogue() -- it will pick whichever one happens to be closest right up to the moment it reaches the target and starts talking). (Basically, I guess it maybe has to determine what [spec] actually corresponds to every single frame, whereas it just stores LastSeenBy() and checks it only when it absolutely has to.)

 

There's evidence that these Last* objects are funky global in some cases (LastTalkedToBy is just generic "last party member in dialogue mode," but you can still pick out LastTalkedToBy("Gump") or LastTrigger("Bomb") to narrow the search). LastSeenBy, I believe, returns nothing unless you qualify an object doing the looking (with (Myself) or whatever).

 

Like I said in the other thread, there are some actions that update LastTrigger on a truly global basis (spellcasting causes every single objects' LastTrigger association to get set to the caster; or maybe they just invalidate the local associations and it knows to grab the global one). LastTrigger(specificObject) won't help you then.

Link to comment

I now wrote a huge patch to GemRB which re-evaluates and re-executes these 'blocking' actions every turn, which seems to match original engine behaviour far better.

 

My latest confusing discovery: if you use ActionOverride to queue a Wait, a MoveToPoint, another MoveToPoint and then an Attack, the target actor will wait for the specified time, then start moving - with the displayed target recticle in the destination of the second MoveToPoint action! - and then finally do the Attack once it's done all the moving. Either the engine is doing some clever consolidation of the MoveToPoint actions, or else these walking actions are even more complicated than I'd thought. Trying it with MoveToObject instead works as I'd expect - first the recticle appears at the first object, and then only when that is reached does the recticle move onward to the second object.

 

In the process I discovered that ActionOverride seems to interrupt any ongoing actions (wiping the whole list) on the target, but then when called repeatedly it works fine, queueing multiple actions! Can anyone else confirm?

 

I'm also not sure exactly when IDS targetting is allowed to match the 'owner' of the actions - [PC] and [0] don't ever match the owner, at least.

 

(And while I'm asking questions, I don't suppose anyone knows how long a round is in Planescape: Torment? Hours of poking at it and I still have little clue.)

 

Edit for more findings: Attack([PC]) and AttackReevaluate([PC], 90) seem to behave entirely identically: they can both be interrupted at once by another block, and they both keep going forever (they never move on to the next action). It's only things like LastSeenBy which are different with AttackReevaluate - Attack() will keep hitting the initial result of See() forever, while AttackReevaluate() will eventually (after the specified number of AI updates) notice that it changed. This corresponds with what devSin said earlier.

 

I also note that LastSeenBy *is* constantly updated - put a Wait(5) in a See([PC]) trigger, and then an AttackReevaluate(LastSeenBy(Myself)), 90) afterwards, and the attacked creature will be the one closest *after the 5-second wait*, not the one that was closest at the start of block execution. So this isn't a block-based thing, it's just an action-based thing.

 

Happily, AttackOneRound does exactly what it says it does: it attacks for one round, then it stops, moving on to the next action.

Link to comment

That is the way it works, yes. You need consecutive invocations, else you risk wiping the action queue with your new ActionOverride() (the engine will allow you to build up an action queue as long as you keep them together).

 

Specs will never target the caller. Unless you use a script name, static (Player1) object, or Myself, you shouldn't ever be getting yourself in return (MostDamagedOf and such may possibly return the caller, but I hope not). I don't believe it's possible to ever See() (and similar) yourself (in BG2 at least).

 

EA NOTGOOD can target other AI objects than actors IIRC. GemRB shouldn't do this.

 

I also note that LastSeenBy *is* constantly updated - put a Wait(5) in a See([PC]) trigger, and then an AttackReevaluate(LastSeenBy(Myself)), 90) afterwards, and the attacked creature will be the one closest *after the 5-second wait*, not the one that was closest at the start of block execution. So this isn't a block-based thing, it's just an action-based thing.
That's basically what I mean. LastSeenBy will *always* be updated when a See() is performed (if the object is actually seen), but the engine isn't going to be coming back to the value for an in-flight action (you're not suddenly going to change targets mid-MoveToObject(LastSeenBy()) simply because the engine found a new LastSeenBy iff you don't switch blocks (which would interrupt the current action when the engine does the new actions, even if it's only a Continue() action -- which is why BioWare's ToB AI can get pretty flakey BTW).

 

And note in the case of Continue(), the engine considers the "active" block to be the one where it encounters the first Continue() even though it dutifully starts checking subsequent blocks, which can lead to some funky behavior.

Link to comment

Any idea how AttackedBy() works? The following is in various override scripts (eg, GRPSHT01) and Allegiance(Myself,0) seems to always return true - so none of the other blocks ever get much time (and there's a lot of shouting!) if AttackedBy() returns true when attacked or in an attack. This seems to be documented in varied places as "last script round" (so this goes into the trigger clearing), but I have no idea *what* was meant to have happened then, and it doesn't seem to be obvious when testing.

 

IF
 AttackedBy([GOODCUTOFF],DEFAULT)
 OR(3)
Allegiance(Myself,GOODBUTBLUE)
Allegiance(Myself,NEUTRAL)
Allegiance(Myself,0)
THEN
 RESPONSE #100
Shout(151)
Enemy()
END

Link to comment
(MostDamagedOf and such may possibly return the caller, but I hope not)
MostDamagedOf will return the caller (I don't know about the other objects) at least it does in BG, IWD and BG2 where I've tested and have used it...

 

Running the following script block with a party member as the actor will cause the 'caller' to be returned if they are the most damaged party member thus allowing for self healing and healing of others in one block rather than several...

IF
 ActionListEmpty()
 HPPercentLT(MostDamagedOf(),100)
 HaveSpell(CLERIC_CURE_LIGHT_WOUNDS) 
THEN
 RESPONSE #100
Spell(MostDamagedOf(),CLERIC_CURE_LIGHT_WOUNDS)
END

SIDE NOTE: I usually do MostDamagedOf(GroupOf(Myself)) for my own sanity in coding scripts but devSin will tell you that GroupOf does nothing and that MostDamagedOf will only return party members anyway. So MostDamagedOf() and MostDamagedOf(GroupOf(Myself)) are equal and get the same results.

 

Any idea how AttackedBy() works?
My understanding is that it returns true if the specified EA type makes an attack attempt with the given attack type MELEE; RANGED; or DEFAULT (which can be either). The attack does not have to be successful unlike HitBy()...
Link to comment
Any idea how AttackedBy() works?
It's hardcoded to bump when a threat is made against the script runner. In general, you damage them, make an attack move against them, or use a hostile spell or item ability against them. The attack style parameter is total dud; it does nothing.

 

Note that LastAttackerOf is set in hardcode to the last *physical* attacker of the given object. If you want to also pick up damage from a spell in a block that checks AttackedBy(), LastTrigger should always get set to whoever caused the check to return true (as noted earlier above, you only want to use LastTrigger in a block that contains a trigger that checks for an object that would set LastTrigger; otherwise, you run the risk of picking up objects that forced a global set of LastTrigger--this is why random characters would die in the asylum crush trap, usually spellcasters).

 

Passing in a 0 will cause several triggers (Allegiance(), Race(), etc.) to always return true. I believe they wanted GOODBUTRED in that block; the fixpack clears all those issues up, actually.

Link to comment

Archived

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

×
×
  • Create New...