From 38ad76301c5098af4ffe34228bb31dc8b7526a1f Mon Sep 17 00:00:00 2001 From: Leviathan <50114410+leviathan400@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:44:59 +0100 Subject: [PATCH 1/6] Fix DoMove to issue a Move command packet The direct GetMapObject()->CmdMove() thunk leaves the unit's move/waypoint state uninitialized; the next ProcessUnits pass calls through a garbage pointer (EXCEPTION_ACCESS_VIOLATION, eip in the heap). Route the order through ProcessCommandPacket (CommandType::Move), the same path DoStandGround/DoAttack already use. --- API/Unit.h | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/API/Unit.h b/API/Unit.h index e1939b3..701c40f 100644 --- a/API/Unit.h +++ b/API/Unit.h @@ -259,7 +259,19 @@ class Unit : public OP2Class { bool GetLights() { return IsLive() && HasFlag(MoFlagVecHeadlights); } ///< Vehicle's headlights are turned on? void DoSetLights(bool on); ///< Turns vehicle's headlights on or off. - void DoMove(Location where) { if (IsLive()) { const auto [x, y] = where.GetPixel(); GetMapObject()->CmdMove(x, y); } } + /// Orders the unit to move to a location. Issued via the command-packet system (CommandType::Move), + /// not the raw GetMapObject()->CmdMove() thunk - that direct call crashes OP2's pathfinder (eip jumps + /// into the heap), whereas the packet path (the same one DoAttack / DoStandGround use) is safe. + void DoMove(Location where) { + if (IsLive()) { + CommandPacket packet = { CommandType::Move, sizeof(MoveCommand) }; + packet.data.move.numUnits = 1; + packet.data.move.unitID[0] = id_; + packet.data.move.numWaypoints = 1; + packet.data.move.waypoint[0] = where.AsWaypoint(); + ProcessCommandPacket(packet); + } + } void DoDock(Unit at) ///< Docks this Unit at a structure. { auto d = at.GetDockLocation(); if (IsLive() && d) { GetMapObject()->CmdDock(d.GetPixelX(), d.GetPixelY()); } } void DoDockAtGarage(Unit garage); ///< Docks this Unit at a Garage. From 31e67a0ee380739631f01d246723db767db490d7 Mon Sep 17 00:00:00 2001 From: Leviathan <50114410+leviathan400@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:49:35 +0100 Subject: [PATCH 2/6] Fix DoDock to issue a Dock command packet Same class of bug as DoMove: the direct GetMapObject()->CmdDock() thunk never docks the unit and leaves command state corrupted. Route the order through ProcessCommandPacket (CommandType::Dock, which uses MoveCommand with the structure's dock tile as the waypoint). --- API/Unit.h | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/API/Unit.h b/API/Unit.h index 701c40f..2295314 100644 --- a/API/Unit.h +++ b/API/Unit.h @@ -272,8 +272,20 @@ class Unit : public OP2Class { ProcessCommandPacket(packet); } } - void DoDock(Unit at) ///< Docks this Unit at a structure. - { auto d = at.GetDockLocation(); if (IsLive() && d) { GetMapObject()->CmdDock(d.GetPixelX(), d.GetPixelY()); } } + /// Docks this Unit at a structure. Issued via the command-packet system (CommandType::Dock, which + /// uses MoveCommand with the structure's dock tile as the waypoint), not the raw CmdDock thunk - that + /// direct call does not dock the unit (same class of bug as DoMove's raw CmdMove call). + void DoDock(Unit at) { + const auto d = at.GetDockLocation(); + if (IsLive() && d) { + CommandPacket packet = { CommandType::Dock, sizeof(MoveCommand) }; + packet.data.move.numUnits = 1; + packet.data.move.unitID[0] = id_; + packet.data.move.numWaypoints = 1; + packet.data.move.waypoint[0] = d.AsWaypoint(); + ProcessCommandPacket(packet); + } + } void DoDockAtGarage(Unit garage); ///< Docks this Unit at a Garage. void DoBuild(Location bottomRight); ///< [ConVec] Build a structure. void DoDeploy(Location center); ///< [Robo-Miner, GeoCon] Deploy building. From f813a668d8cc330e7de8f3ee29aad1698ad367ef Mon Sep 17 00:00:00 2001 From: Leviathan <50114410+leviathan400@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:53:37 +0100 Subject: [PATCH 3/6] Fix DoMiningRoute command packet header numUnits (a uint8 count) was set to the unit id and the id was written to unitID[1], leaving unitID[0] = 0, so the engine matched the route to no unit and Cargo Trucks never moved. Set numUnits = 1; unitID[0] = id_, matching every other Do* command builder. --- API/Unit.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/API/Unit.h b/API/Unit.h index 2295314..5392357 100644 --- a/API/Unit.h +++ b/API/Unit.h @@ -634,8 +634,8 @@ inline void Unit::DoMiningRoute( { if (IsLive() && mine.IsLive() && smelter.IsLive()) { CommandPacket packet = { CommandType::CargoRoute, sizeof(CargoRouteCommand) }; - packet.data.cargoRoute.numUnits = id_; - packet.data.cargoRoute.unitID[1] = id_; + packet.data.cargoRoute.numUnits = 1; + packet.data.cargoRoute.unitID[0] = id_; packet.data.cargoRoute.numWaypoints = 3; packet.data.cargoRoute.waypoint[0] = mine.GetDockLocation().AsWaypoint(); From 80937539734218f0d5632340b7ee3758bdac2417 Mon Sep 17 00:00:00 2001 From: Leviathan <50114410+leviathan400@users.noreply.github.com> Date: Wed, 17 Jun 2026 01:40:10 +0100 Subject: [PATCH 4/6] Fix DoDumpCargo to issue a DumpCargo command packet The direct GetMapObject()->CmdDumpCargo() thunk corrupts command state and the engine faults a tick later (EXCEPTION_ACCESS_VIOLATION). Route through DoSimpleCommand(CommandType::DumpCargo), the same path the sibling DoLoadCargo/DoUnloadCargo already use - DumpCargo is already handled in DoSimpleCommand's switch. --- API/Unit.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/Unit.h b/API/Unit.h index 5392357..84a07cf 100644 --- a/API/Unit.h +++ b/API/Unit.h @@ -297,7 +297,7 @@ class Unit : public OP2Class { void DoDoze(MapRect area); ///< [Robo-Dozer] Bulldoze an area. void DoMiningRoute(Unit mine, Unit smelter); ///< [Cargo Truck] Mine <-> Smelter route. void DoSalvage(MapRect area, Unit gorf); ///< [Cargo Truck] Salvage rubble. - void DoDumpCargo() { if (IsLive()) { GetMapObject()->CmdDumpCargo(); } } ///< [Cargo Truck] Dispose of cargo. + void DoDumpCargo() { DoSimpleCommand(CommandType::DumpCargo); } ///< [Cargo Truck] Dispose of cargo. void DoLoadCargo() { DoSimpleCommand(CommandType::LoadCargo); } ///< [Cargo Truck] Load truck cargo. void DoUnloadCargo() { DoSimpleCommand(CommandType::UnloadCargo); } ///< [Cargo Truck] Unload truck cargo. From 498a9c16b3f54c56479bf0ad911c13c5f24c05b7 Mon Sep 17 00:00:00 2001 From: Leviathan <50114410+leviathan400@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:02:04 +0100 Subject: [PATCH 5/6] Fix DoTransferCargo to issue a TransferCargo command packet The direct GetMapObject()->CmdTransferCargo() thunk corrupts command state and the engine faults a tick later. Route through ProcessCommandPacket (CommandType::TransferCargo, which uses TransferCargoCommand: unitID, bay, unknown=0). --- API/Unit.h | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/API/Unit.h b/API/Unit.h index 84a07cf..3c28ef9 100644 --- a/API/Unit.h +++ b/API/Unit.h @@ -375,8 +375,18 @@ class Unit : public OP2Class { void DoProduce( ///< [Factory] Issue a factory build command. MapID itemType, MapID weaponType = MapID::None, uint16 scGroupIndex = -1, bool recycleIfFull = false); void DoLaunch(Location target = { }, bool forceEnable = false); ///< [Spaceport] Launch the rocket on launch pad. - void DoTransferCargo(int bay) ///< [Factory, Garage] Move cargo to a bay. - { if (IsLive()) { GetMapObject()->CmdTransferCargo(bay); } } + /// Moves cargo to a bay. Issued via the command-packet system (CommandType::TransferCargo), + /// not the raw CmdTransferCargo thunk - that direct call corrupts state and crashes the engine + /// a tick later (same class of bug as DoMove/DoDumpCargo). [Factory, Garage] + void DoTransferCargo(int bay) { + if (IsLive()) { + CommandPacket packet = { CommandType::TransferCargo, sizeof(TransferCargoCommand) }; + packet.data.transferCargo.unitID = id_; + packet.data.transferCargo.bay = bay; + packet.data.transferCargo.unknown = 0; + ProcessCommandPacket(packet); + } + } void DoResearch(int techID, int numScientists); ///< [Lab] Begin researching a technology. void DoTrainScientists(int numToTrain); ///< [University] Begin training new scientists. From a84f1c8e314deae2c73d58701440f7b8ee2bb7c0 Mon Sep 17 00:00:00 2001 From: Leviathan <50114410+leviathan400@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:21:44 +0100 Subject: [PATCH 6/6] Update Changelog with fixes Add 0.8.3 change log entry for the five Do* command-packet fixes --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index aa15dd0..f27b1ca 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,13 @@ The public mission APIs are within the `TethysAPI` namespace, while everything e # Change log +## Version 0.8.3 +* Fix `Unit::DoMove()` to issue a `CommandType::Move` command packet instead of the raw `CmdMove()` thunk. +* Fix `Unit::DoDock()` to issue a `CommandType::Dock` command packet instead of the raw `CmdDock()` thunk. +* Fix `Unit::DoMiningRoute()` command packet header (`numUnits = 1`, unit id in `unitID[0]`). +* Fix `Unit::DoDumpCargo()` to issue a `CommandType::DumpCargo` command packet instead of the raw `CmdDumpCargo()` thunk. +* Fix `Unit::DoTransferCargo()` to issue a `CommandType::TransferCargo` command packet instead of the raw `CmdTransferCargo()` thunk. + ## Version 0.8.2 * Change `Create*Trigger()` functions to take `triggerFunction` as a `std::string_view` instead of a `const char*`. This is a backwards-compatible interface change. * Rename `OnProcessCommandArgs` to `OnGameCommandArgs`