Paul Stevens wrote:There are two timers active for each monster: Attack Timer
and Move Timer.
I don't think this is the case.
There are definitely two distinct timer 'modes' that the monster AI knows about, which you called A and B, and I'll use those names because I don't really understand all of what they're doing either. Their functionality does not at all seem like they are distinct modes for movement and attacking, though, as I'll explain.
Let's try to unravel this by assuming we're starting at the beginning. When you first enter a level (or load a savegame), ProcessMonstersOnLevel is called. This goes through every monster on the level, and, among other things, destroys every monster group's timer and creates a new B-timer associated with that monster group. ProcessMonstersOnLevel also calls AttachItem16ToMonster which calls NextMonsterUpdateTime and throws away its return value. It does this because a side effect of NextMonsterUpdateTime is to tweak the monster's visual properties, like shift the monster around on the tile and possibly randomly flip it, and it's the
only place in the code where this happens, so this function is called to randomize the monsters' positions on the tile when entering a new level.
At this point, the monster group is "alive" and the only timer that has been created is a B-timer. This in itself doesn't disprove anything, because it could very well be that the monsters don't get an attack timer until they want to attack. So, what happens when these monsters want to attack? Walking up to a monster calls ProcessTimers29to41 with a timer type of TT_M1, which gets turned into TT_31 by InitialChecks. The handler for TT_31 checks if the monster is afraid (StateOfFear5) or already enraged (StateOfFear6) and if not, it deletes all the timers for that monster group and calls MaybeDelTimers_Fear6_TurnIndividuals. This enrages the monster group (sets their "fear state" to 6), then iterates over the entire monster group and possibly turns them to face the party. It then creates an individual timer event for each monster in the group, also calling NextMonsterUpdateTime so they might fidget around or flip at the same time. Depending on the results of some randomness, this might be an A-timer or a B-timer. All the A-timer for a monster who already wants to attack does is call NextMonsterUpdateTime and trigger the B-timer after another short delay returned by that function, so let's just, for simplicity, assume that it's a B-timer.
This means the time delay returned by NextMonsterUpdateTime is used for more than I thought it was, and is not purely cosmetic, because the delay returned is used as a sort of 'reaction time' to determine when the monsters might get to react... which is pretty interesting! However, the important thing here is that, while there's now a timer for each individual monster as opposed to one for the entire group, there's still only
one timer for each monster. What happens when that B-timer fires? That is deep inside ProcessTimers29to41, starting at around line 3753. The first big check is that we're not in a group B-timer; we aren't, since we're firing off events for all monsters in the group individually. The next check is if the monster is afraid, i.e., in StateOfFear5. If it is, it checks if the group has more monsters in it and deletes all their timers if there are, and then calls Try_To_Move, presumably to run away. The next check is whether the monster is attacking, via TestAttacking. This does some interesting stuff but for this test case our monster is not attacking, so, we go to the else, and do a whole bunch of checks I don't really understand to see if the monsters are in the right position and able to attack. At line 3902, it checks whether the xDistance or yDistance is 0, which means the monster can attack. It then does some more stuff I don't understand (this is a bit of a recurring theme) but, at line 3999, we call MonsterAttacks. This is the only place in the code that MonsterAttacks gets called, so, when a monster's going to attack, it happens right here!
However, in the various else blocks for if the monster can't attack, if it's out of position, or, as mentioned above, if it's too frightened to attack, then movement happens. The various timers still in the queue for the individual monsters get deleted via ClearAttacking_DeleteMovementTimers and a single new movement timer gets set. From this, it's clear (well, as clear as any of this can be...) that a single firing of a B-timer event is able to result in either attacks or movement, depending on circumstances. There is not a separate attack timer and movement timer. There is some weirdness between A-timers and B-timers, but that's different, and that's all I can really say about it because there's still a lot I don't understand. In general, A-timers seem like they just change the monster's state a bit, while all the actual movement and attacking happens in B-timers. I'm pretty sure that monsters don't have concurrently running A-timers and B-timers, either, because it seems like timeUntilAlternateUpdate is mainly used by an A-timer to tell when to schedule the next B-timer.
In all this digging around, I think I found a bit more out about what the delay generated by NextMonsterUpdateTime is actually for, too. As mentioned above, it seems to be a sort of reaction time. When MonsterAttacks gets called, its return value is passed to NextMonsterUpdateTime, which stores a delay in i_60. As a side effect, since NextMonsterUpdateTime is where various graphical tweaking happens, the monster's attack animation gets set. This is important, because, while you were correct in your feeling that the small number returned by NextMonsterUpdateTime is used to set a timer after a monster attacks, the monster's attack animation is still set, so the next timer event had better unset the attack animation or things will look pretty stupid, and it had better do it quick.
A small number of ticks later (after whatever delay NextMonsterUpdateTime returns, possibly with some other tweaks I don't really understand) the monster's B-timer fires again. This time, the TestAttacking check is true, because the monster is still in its attacking animation. At that point, NextMonsterUpdateTime is called again, to turn off the monster's attacking animation, storing its result in i_60, while attackTicks07 is added to timer_70's time, and then Set_Monster_Timer gets called. What I believe this ends up doing is scheduling an A-timer some short number of ticks in the future. The A-timer doesn't do much, and the value of attackTicks07 got stored as the timer's UByte8 (i.e., timeUntilAlternateUpdate) so after that, the B-timer goes off and the monster gets another turn. That means that while NextMonsterUpdate's return value is definitely used, it's mostly just to schedule animation stuff and various status updates and whatever else the A-timer does, and it's ultimately still basically moveTicks06 and attackTicks07 that control when the B-timers go off and the monsters get actual turns.
With all the different A-timers and B-timers and such going off at various times, most of them calling NextMonsterUpdateTime, monsters fidget and shift around because doing that is a side effect of NextMonsterUpdateTime. This also means that if a monster moves a little or flips to its mirror image version, but doesn't do anything else, it probably just had an A-timer event. Which is kind of interesting that you can see that, I guess! It almost seems like the A-timer is there solely so the monsters
can animate, and run a few simple checks at the same time, because the events do so much less than the B-timer.
I feel like I understand the DM AI more than I ever have, and I still don't really understand it.
When I started delving into this, it was 8:00. I figured I'd spend an hour or two on it and call it a night. It's now 1 am! I think I'm done.