Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ set(TEST_FILES
src/rwe/sim/patrol.test.cpp
src/rwe/sim/capture.test.cpp
src/rwe/sim/worksounds.test.cpp
src/rwe/sim/unitnotifications.test.cpp
src/rwe/sim/burning.test.cpp
src/rwe/sim/waterbuilding.test.cpp
src/rwe/setup/TaInstall.test.cpp
Expand Down
100 changes: 100 additions & 0 deletions docs/TOTALA-EXE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9998,6 +9998,106 @@ The feature's `seqnamereclamate` swirl was already implemented (it plays on
> the *when* -- in particular that a builder sent to a wreck it must walk to
> announces nothing until it arrives.

### `underattack`, `repair` and `cant`: the three voices that were never played

> **Ported, 2026-09-11.** `UnitDamagedEvent` carries the cause (its
> `paralyzer` flag) and the scene plays slot 2 off it under the gates below;
> `UnitRepairedEvent` is raised where a repair job lands, on both sides of
> it, and plays slot 10; `UnitCannotComplyEvent` carries the caption a
> refusing site passes and plays slot 7. Four of the forty-five `cant`
> sites are wired, the ones whose refusals RWE already makes: the nanoframe
> capture, the Commander reclaim, the pad that is taken, and the corpse that
> has gone. The rest are in the table for when their refusals exist.
> `src/rwe/sim/unitnotifications.test.cpp`.

The rest of the slot table at `0x5086F0`, read out in full this time:

| Id | Key | Caption | | Id | Key | Caption |
|---|---|---|---|---|---|---|
| 1 | `select` | | | 12 | `load` | |
| 2 | `underattack` | Under Attack | | 13 | `unload` | |
| 3 | `activate` | | | 14 | `cloak` | Cloaked |
| 4 | `deactivate` | | | 15 | `uncloak` | Visible |
| 5 | `ok` | | | 16 | `capture` | |
| 6 | `arrived` | Arrived | | 17-22 | `count5`..`count0` | five..zero |
| 7 | `cant` | Cannot Comply | | 23 | `canceldestruct` | Self destruct terminated |
| 8 | `unitcomplete` | Nanolathe Complete | | | | |
| 9 | `build` | | | | | |
| 10 | `repair` | | | | | |
| 11 | `working` | | | | | |

**Three entry points, one player.** `0x47F780(unit, id, caption)` is the one
the eighty-two direct call sites use. It refuses a unit that is not the local
player's, or not alive (`unit+0x110` bit 28 clear or bit 14 set); takes the
caption passed in, or the table's if none; prints it (`0x4C5740`) and hands
`(unit, id, text)` to the player `0x47FAD0`. Two siblings differ only in one
extra gate, `0x48BCB0(unit)`, which walks the frame's draw list (the "HOT
UNITS" list at `world+0x1435F`, §18): `0x47F7E0` plays only for a unit *in*
the list and is never called; `0x47F850` plays only for a unit *not* in it,
and has one caller.

**The player keeps a queue.** `0x47FAD0` holds up to eight pending voices of
17 bytes each. A slot id already waiting is not queued again (`0x47FB0A`), a
full queue drops its head, and the entries are kept in priority order from
the table's third dword (`underattack` 0x14, `cant` 1, `select` 0). So a unit
under sustained fire says "Under Attack" once per playback, not once per hit.
RWE has one reserved voice channel and drops a voice while it is busy, which
comes to nearly the same thing.

**`underattack`, slot 2, one site.** `0x4071D8`, the tail of the return-fire
routine `0x406F80(attacker, victim, damage)` (§"Firing modes"). Every early
exit in that routine jumps to the tail rather than returning, so the voice is
independent of the fire mode and of whether the unit shot back. Its own gates:

- `0x438BE0(victim)` returns the victim's current mission flags word
(`mission+0x42`); bit 7 set skips the voice. No mission handler sets that
bit directly in this binary; it is treated as never set.
- `[victim+0xF4]` (the owner of the last unit to damage it) differs from the
victim's owner: play. Otherwise play only if `[victim+0xF5]`, the cause
byte, is 1, a weapon hit -- so a paralyser from your own side is silent.
- Through `0x47F850`: only for a unit that is not being drawn this frame. It
is the warning for what you cannot see.

**`repair`, slot 10, five sites, all with `0x5012CC` "Unit repaired".**
`0x402491` in `SELFREPAIR` (`0x402430`, the aircraft on a pad, once its hit
points meet the maximum), `0x415298` in `VTOL_GetRepaired` (`0x415250`, the
same from the air side), `0x415219` in `VTOL_RepairUnit` (`0x414E70`, the
repairer), and `0x4056FE`/`0x4057A1` in the ground repair body shared below
`0x4056A5` (the repairer again). Both ends of a repair say it.

**`cant`, slot 7, 45 sites.** Not one passes a null caption; every site
overrides "Cannot Comply" with a message of its own, which is why the table's
caption is never seen. The sites, by caption:

| Sites | Caption |
|---|---|
| `4046fe` | Capture failed |
| `402736` | Construction stopped |
| `403a3c, 403fb0, 413dba` | Construction terminated |
| `41473c` | Construction terminated by hostile action |
| `40407a` | I can't get there |
| `403c45` | I can't reach the construction site |
| `41190b` | Landing aborted |
| `411d36` | Landing aborted: all pads are occupied |
| `411e01` | Landing aborted: no pads available |
| `411c3a` | Landing failed |
| `4047a6, 4047e6, 404b00, 41479f, 414d69` | Reclamation failed |
| `40244e, 415268` | Repair aborted. |
| `414f8e` | Repair mission failed |
| `40531e, 4053aa, 40575e, 414e8e, 414f18` | Repairs unsuccessful. |
| `404f7e` | Ressurection failed |
| `403d10, 414055` | Target area was blocked |
| `4042ff` | That unit cannot be captured |
| `404799, 414d91` | That unit cannot be reclaimed |
| `40432e` | That unit is a cloud of vapor and cannot be captured |
| `4068d7, 411207, 411526` | Transport mission failed |
| `402907, 403d78, 405121, 4140b8` | Unable to create any more units |
| `411771` | Unable to unload unit |
| `411275` | Unit is too heavy to transport |
| `4067ea` | Unit is too large to transport |
| `406916` | Unloading process is proceeding non-optimally |
| `403cdf, 414020` | Waiting for target area to clear |

---

## 98. Resurrect: a real mission, a crude corpse mapping, and nothing that can use it
Expand Down
48 changes: 48 additions & 0 deletions src/rwe/game/GameScene_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1331,9 +1331,57 @@ namespace rwe
if (auto victim = tryGetUnit(e.unitId))
{
lastAttackPosition = victim->get().position;

// Sound slot 2, `underattack`, "Under Attack".
// Its one call site (0x4071D8) is the tail of
// the return-fire routine, reached on every hit
// whatever the fire mode. It plays for damage
// from another side of any cause, and for the
// unit's own side only on a weapon hit; and it
// goes through the entry point that refuses a
// unit in the frame's draw list (0x47F850), so
// it is the warning for what you cannot see.
// The original also drops a repeat while one is
// waiting in its voice queue; here the reserved
// voice channel does the same by being busy.
// TOTALA-EXE.md §97.
auto otherSide = !e.attackerOwner || *e.attackerOwner != e.victimOwner;
if ((otherSide || !e.paralyzer) && !victim->get().isDead())
{
auto viewProjectionMatrix = computeViewProjectionMatrix(worldCameraState, worldViewport.width(), worldViewport.height());
auto viewCull = makeViewCullTest(viewProjectionMatrix);
if (!viewCull.couldBeVisible(simVectorToFloat(victim->get().position), ViewCullModelRadius))
{
const auto& unitDefinition = simulation.unitDefinitions.at(victim->get().unitType);
printConsole(unitDefinition.unitName + ": Under Attack");
playUnitNotificationSound(localPlayerId, victim->get().unitType, UnitSoundType::UnderAttack);
}
}
}
}
},
[&](const UnitRepairedEvent& e) {
// Sound slot 10, `repair`, with its caption "Unit
// repaired" (0x5012CC), from the repairer and from the
// aircraft on a pad alike. TOTALA-EXE.md §97.
if (auto unit = tryGetUnit(e.unitId); unit && unit->get().isOwnedBy(localPlayerId))
{
const auto& unitDefinition = simulation.unitDefinitions.at(unit->get().unitType);
printConsole(unitDefinition.unitName + ": Unit repaired");
playUnitNotificationSound(localPlayerId, unit->get().unitType, UnitSoundType::Repair);
}
},
[&](const UnitCannotComplyEvent& e) {
// Sound slot 7, `cant`, with the caption the refusing
// site passes in place of the table's "Cannot Comply".
// TOTALA-EXE.md §97.
if (auto unit = tryGetUnit(e.unitId); unit && unit->get().isOwnedBy(localPlayerId))
{
const auto& unitDefinition = simulation.unitDefinitions.at(unit->get().unitType);
printConsole(unitDefinition.unitName + ": " + e.message);
playUnitNotificationSound(localPlayerId, unit->get().unitType, UnitSoundType::Cant1);
}
},
[&](const UnitDiedEvent& e) {
const auto& unitDefinition = simulation.unitDefinitions.at(e.unitType);

Expand Down
2 changes: 1 addition & 1 deletion src/rwe/sim/GameSimulation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3499,7 +3499,7 @@ namespace rwe
attackerOwner = attackerUnit->get().owner;
}
}
events.push_back(UnitDamagedEvent{unitId, getUnitState(unitId).owner, attackerOwner});
events.push_back(UnitDamagedEvent{unitId, getUnitState(unitId).owner, attackerOwner, paralyzer});
}

if (attacker)
Expand Down
44 changes: 43 additions & 1 deletion src/rwe/sim/GameSimulation.h
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,46 @@ namespace rwe
UnitId unitId;
PlayerId victimOwner;
std::optional<PlayerId> attackerOwner;

/**
* The original's damage cause byte (unit+0xF5): 1 is a weapon hit
* and 2 a paralyser. The under-attack voice, sound slot 2, plays
* for damage from another player whatever its cause, and for damage
* from the unit's own side only when it is a weapon hit
* (0x4071BA-0x4071D1). See TOTALA-EXE.md §97.
*/
bool paralyzer{false};
};

/**
* A repair job has finished and its unit says "Unit repaired": sound
* slot 10, `repair`. The original plays it from both ends of the job --
* the repairer when RepairUnit or VTOL_RepairUnit completes, and the
* aircraft itself when SELFREPAIR (0x402491) or VTOL_GetRepaired
* (0x415298) sees its hit points reach the maximum on a pad. See
* TOTALA-EXE.md §97.
*
* Emitted for the scene, and never hashed.
*/
struct UnitRepairedEvent
{
UnitId unitId;
};

/**
* An order the unit cannot carry out, with the caption the original
* prints for it: sound slot 7, `cant`, whose table caption is "Cannot
* Comply" but which every one of its forty-five call sites overrides
* with a message of its own ("That unit is a cloud of vapor and cannot
* be captured", "Landing aborted: no pads available", ...). See
* TOTALA-EXE.md §97 for the full list.
*
* Emitted for the scene, and never hashed.
*/
struct UnitCannotComplyEvent
{
UnitId unitId;
std::string message;
};

struct UnitStartedBuildingEvent
Expand Down Expand Up @@ -422,7 +462,9 @@ namespace rwe
UnitStartedReclaimingEvent,
ProjectileDiedEvent,
ProjectileDetonatedEvent,
UnitCapturedEvent>;
UnitCapturedEvent,
UnitRepairedEvent,
UnitCannotComplyEvent>;


struct UnitInfo
Expand Down
24 changes: 21 additions & 3 deletions src/rwe/sim/UnitBehaviorService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,7 @@ namespace rwe
// Somebody else got there first.
if (airBaseIsClaimedByAnother(*sim, order.target, unitInfo.id))
{
sim->events.push_back(UnitCannotComplyEvent{unitInfo.id, "Landing aborted: no pads available"});
return true;
}

Expand All @@ -1837,7 +1838,14 @@ namespace rwe
// mending is not its work: the pad does that, in the builder
// path, which is why an aircraft on a switched-off pad sits there
// indefinitely instead of taking off again.
return unitInfo.state->hitPoints >= unitInfo.definition->maxHitPoints;
if (unitInfo.state->hitPoints >= unitInfo.definition->maxHitPoints)
{
// The aircraft's own "Unit repaired" (0x402491, and
// VTOL_GetRepaired's at 0x415298).
sim->events.push_back(UnitRepairedEvent{unitInfo.id});
return true;
}
return false;
}

if (navigateTo(unitInfo, pad.position))
Expand Down Expand Up @@ -3576,6 +3584,8 @@ namespace rwe
const auto& targetDefinition = sim->unitDefinitions.at(target->get().unitType);
if (targetDefinition.canCapture)
{
// 0x50164C, from ReclaimUnit (0x4047A6).
sim->events.push_back(UnitCannotComplyEvent{unitInfo.id, "That unit cannot be reclaimed"});
return true;
}
}
Expand All @@ -3595,7 +3605,8 @@ namespace rwe
auto featureRef = sim->tryGetFeature(resurrectOrder.target);
if (!featureRef)
{
// "Ressurection failed" -- the corpse has gone.
// The corpse has gone. 0x501684, spelt as the original spells it.
sim->events.push_back(UnitCannotComplyEvent{unitInfo.id, "Ressurection failed"});
return true;
}
const auto& feature = featureRef->get();
Expand Down Expand Up @@ -3722,7 +3733,9 @@ namespace rwe

if (target.hitPoints >= targetDefinition.maxHitPoints)
{
// Nothing to repair.
// Nothing to repair. The "Unit repaired" voice is raised where
// the mending finishes, in deployRepairArm, so an order for a
// unit that was already whole says nothing.
return true;
}

Expand Down Expand Up @@ -4054,6 +4067,7 @@ namespace rwe
// thinks an unfinished building is.
if (targetRef->get().isBeingBuilt(sim->unitDefinitions.at(targetRef->get().unitType)))
{
sim->events.push_back(UnitCannotComplyEvent{unitInfo.id, "That unit is a cloud of vapor and cannot be captured"});
return true;
}

Expand Down Expand Up @@ -4972,6 +4986,10 @@ namespace rwe

if (targetUnit.hitPoints >= targetUnitDefinition.maxHitPoints)
{
// "Unit repaired", sound slot 10, from the repairer's
// side: RepairUnit and VTOL_RepairUnit (0x415219) say
// it as the job lands.
sim->events.push_back(UnitRepairedEvent{unitInfo.id});
changeState(*unitInfo.state, UnitBehaviorStateIdle());
return true;
}
Expand Down
Loading