Expected: a tower should fire on a monster its owner is attacking and stay on it until the owner
attacks something else, the monster dies, or it leaves range. Only the owner-driven half has a source: the
GamesCampus beginner guide, archived 2012-09-26 at
https://web.archive.org/web/20120926004401/http://rfonline.gamescampus.com/guide/beginner%20guide/guard%20tower,
says under "03 ASSEMBLING COMPLETE": "The Guard Tower has a given firing range and it will only attack
incoming enemies or enemies that the user is attacking." Staying on the target is in no source we found, so
that half is our request. We are not asking for the sentry behaviour the same page's characteristic (1)
describes ("They will automatically open fire at any monsters or other race (acknowledged as enemies) that
are within their fire range"). The page never says which of its two statements wins, and neither finding
needs that settled.
Which monster a tower may shoot is decided by one test, (*Server).towerDefendsAgainst 0x140a2d040, which
asks whose aggro the monster is on: for a player's tower, true when monster+0x238 holds the owner's serial,
and false for any tower when it is 0. Two things fall out of that.
1. A base tower's own hit clears the monster's target
A base tower's owner is 0: (*Server).standSystemTower 0x140a1a320 writes mov dword ptr [rax+8], 0 at
0x140a1a3ed, and placement_system_towers holds 134 such rows. (*Server).towerHitMonster 0x140a2d5e0
loads that owner from +8 and passes it to (*Server).engageMonsterFrom at 0x140a2d914, which has no zero
guard, so (*monster).setAggro 0x140977ee0 writes 0 into monster+0x238 at 0x140978003.
So the monster forgets what it was fighting, (*Server).stepMonsterAIIn re-acquires through
searchNearPlayerIn, and the gate refuses that monster to every tower until something engages it again.
This one is a plain bug in our reading: a tower's shot should not erase what its victim was fighting.
Fix: skip the engageMonsterFrom call at 0x140a2d914 when the owner serial is 0, or have setAggro
refuse serial 0.
2. No tower holds the target it is firing at
(*Server).towerFire 0x140a2cb60 chooses a target afresh on every shot, calling towerNearestMonster and
towerNearestEnemyPlayer once each and keeping the nearer. It records nothing: every store in it, in
(*Server).stepTower and in (*Server).towerHitMonster is to that function's own stack frame, as is every
struct copy and clear the three of them make. And towerNearestMonster re-runs the gate for every candidate
before every shot (0x140a2cee6), so even a remembered target would be re-tested.
What a tower must win each time is the monster's single aggro slot, and that slot belongs to whoever hit
last: setAggro overwrites it for any non-zero serial, and (*Server).handleAttack 0x140877600 passes the
attacker's own serial to engageMonsterFrom at 0x140877aa0. So two characters on one monster is enough.
While the other one's hit is the more recent, the tower is refused the monster it was firing at, and is let
back in only when the owner hits it again. It costs shots rather than the fight: a refused tower arms nothing
(stepTower arms only on a true return from towerFire) and is looked at again on the 200 ms towerLoop
tick.
Fix: give liveTower a current target, set on the shot that first passes the gate, kept while that
monster is alive and in range without re-running the gate, and dropped when it dies, leaves range, or the
owner attacks something else. Nothing in the firing path keeps one today, so this is new state rather than a
changed condition.
How to see it
- Attack a monster inside your own tower's range and let the tower fire, then have a second character hit the same monster, both of you keeping at it. The tower should go quiet whenever the other character's hit is the more recent one, and fire again after each of your own.
- For finding 1, let a monster engage you inside a base ring and count the shots that base tower lands on it.