Skip to content
Open
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
18 changes: 18 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@
- [Ship Types](./basics/ships.md)
- [Ship Artillery](./basics/ship-artillery.md)
- [Operations](./operations.md)
- [Rename Ship](./operations/002d-rename-ship.md)
- [Join Guild](./operations/0037-join-guild.md)
- [Bath House Bribe Success](./operations/0042-bath-house-bribe-success.md)
- [Bath House Bribe Failure](./operations/0043-bath-house-bribe-failure.md)
- [Make Town Hall Offer](./operations/0048-make-town-hall-offer.md)
- [Tavern Interaction](./operations/0052-tavern-interaction.md)
- [Office Autotrade Setting Change](./operations/005b-office-autotrade-setting-change.md)
- [Office Autotrade Lock Change](./operations/0066-office-autotrade-lock-change.md)
- [Set Trade Route Active](./operations/0068-set-trade-route-active.md)
- [Route Stop Setting Change](./operations/0069-route-stop-setting-change.md)
- [Trade Route Stop Town Change](./operations/006a-trade-route-stop-town-change.md)
- [Start Criminal Investigation](./operations/0081-start-criminal-investigation.md)
- [Scheduled Tasks](./scheduled-tasks/0000.md)
- [Criminal Investigation](./scheduled-tasks/0005-criminal-investigation.md)
Expand Down Expand Up @@ -59,7 +65,17 @@
- [Impact](./ships/sea-battles/projectiles/impact.md)
- [Reefs](./ships/sea-battles/reefs.md)
- [Auto Traders](./auto-traders.md)
- [UI](./ui.md)
- [Trading Office Window](./ui/trading-office-window.md)
- [Auto Trade Goods Dialog](./ui/auto-trade-goods-dialog.md)
- [Trade Route Panel](./ui/trade-route-panel.md)
- [Personal Letters Window](./ui/personal-letters-window.md)
- [Notification Tickers](./ui/notification-tickers.md)
- [Name Banks](./ui/name-banks.md)
- [Letters](./letters.md)
- [Scripted Letters](./letters/scripted-letters.md)
- [Mission Scripts](./letters/mission-scripts.md)
- [Tavern Missions](./letters/71-tavern-missions.md)
- [Charge](./letters/0a-charge.md)
- [Indictment](./letters/19-indictment.md)
- [Multiplayer](./multiplayer.md)
Expand All @@ -75,6 +91,8 @@
- [Bath House Bribes Blunders](./bugs/bath-house-bribes-blunders.md)
- [Multiplayer Locks](./bugs/multiplayer-locks.md)
- [Uncompressed Trade Route Loading](./bugs/uncompressed-trade-route-loading.md)
- [Patrol Letter Crash](./bugs/patrol-letter-crash.md)
- [Tavern Mission Lock Leak](./bugs/tavern-mission-lock-leak.md)
- [Patches](./patches.md)
- [High Res](./patches/high-res.md)
- [Increase Alderman "Found Settlement" Mission Limit](./patches/increase-alderman-found-settlement-limit.md)
Expand Down
99 changes: 98 additions & 1 deletion src/auto-traders.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Auto Traders
Both captains and administrators are represented by the same struct.
Captains, administrators and pirate captains are represented by the same struct. The array
lives behind the ships container at `0x006DD7A0` (array pointer at `+0x0`, count
word at `+0xF2` = `0x006DD892`, stride `0x10`). Each town chains its auto traders
through `field_0_next_auto_trader_index`, headed by the town's
`field_82E_auto_trader_chain_head` (see [Towns](./towns.md)). The chain mixes two
record kinds, discriminated by `field_8` (check `0x004FE150`, true for
`field_8 > 0x20`). The record initializer (`0x004FDF50`) fills `field_8` with a
random byte and reduces it `% 11` for captains (0..10), so the range encodes the
kind: captains always pass `<= 0x20`, the pirate captains - one record per town,
maintained by the spawn task - fail it. (The pirates are the tavern characters a
ship can be handed to; identified by matching the records' name ids against the
pirate captains in-game.) For pirates `field_8` doubles as the greed byte: the
loot share a tavern pirate demands is `25 + 5 * ceil(field_8 / 32)`, i.e.
35%..65% - verified against seven live pirates. The initializer also rolls
`field_2`/`field_3` as first/last
name ids (modulo the name-registry counts `0x006DDB70`/`0x006DDB74`), splits a
600-point budget randomly across the three skills, and stamps `field_4` from the
current date serial. The two kinds also differ in their wage formula: captain
records derive it from the trade skill alone (`0x004FE160`), pirate records from
the sum of all three skills plus a base. A town's tavern offers a captain
for hire while the chain contains a captain record no merchant employs
(`field_F_merchant_index` = `0xFF`): the captain resolver `0x005269A0`(town,
merchant) walks the chain applying exactly that, preferring a captain the asking
merchant already employs; the sibling resolver `0x005261D0` does the same for
the pirate captains. Verified against a live save: exactly the towns whose
taverns showed captains had a matching chain record.
The following fields have been identified:
```c
struct auto_trader
Expand All @@ -16,3 +41,75 @@ struct auto_trader
unsigned __int8 field_F_merchant_index;
};
```

## Array Layout
Observed live (128-record array): indices 0..47 hold one world-generation pair
per town - the founding captain of an AI merchant's starting ship (elite skills,
exceeding the 600-point budget of the normal roller; verified by matching the
records against the AI ships' `field_42_captain_index` and owners) and the
town's pirate. The dynamic range
above holds spawned tavern captains and employed records; the tail is free
capacity, `memset` to `0xFF` and freelist-linked through `field_0` (the
allocator at `0x005097C0` grows the array by 64 records). Dismissing an office
administrator frees his record back to the freelist; re-employing allocates a
fresh one (new name and skills, wage 10, trade skill 0) - whatever the office
remembers about a previous administrator is stored on the office, not in this
array.

A record's situation is encoded by chain membership, verified across live save
states: chained to a town = sitting in that town's tavern; unchained = serving
on a ship (`field_42_captain_index`). The record's merchant byte is only
maintained for the player - AI merchants' hires keep `0xFF` - so ship ownership
comes from the ship's `field_0_merchant_index` (AI merchants hold low indices
with two starting ships each; pirate ships and empty ship slots hold `0xFF`).
Records never expire; the population circulates between taverns and decks.

## Captain and Pirate Spawning
A periodic task (`0x004E2634`, rescheduling itself in date-serial ticks, ~345
per day) maintains both populations. Per town it records two flags: whether the
captain resolver finds an unemployed captain (called **with the merchant count
as the asking merchant** - a value no real merchant has, so only records with
`field_F_merchant_index` = `0xFF` count), and whether the pirate resolver finds
a pirate.

- **Pirates**: when fewer than 3 towns have a pirate, one is spawned into a
pirate-less town (`0x00526A50(town, 1)` - the pirate initializer path).
- **Captains**: at 8 or more unemployed captains the task just reschedules far
out (`+0x700` ticks, ~5 days). Below that it compares the count against a
demand target derived from fleet statistics (`0x00509930` on the ships
container) and spawns a captain into a captain-less town when the count is at
or below the target, or below 2 (`0x00526A50(town, 0)`), rescheduling `+0x200`
ticks (~1.5 days) after a spawn and `+0x400` otherwise.

No expiry logic exists in the task, and `field_4` is never compared against the
current date: an unhired captain stays until somebody hires him - including AI
ships, which fill their `field_42_captain_index` through the same resolver and
unlink the captain from the town (`0x0051A1B9`). A captain "disappearing" from a
tavern is somebody else's hire, not a timeout.

Dismissing a captain re-links him into the town's chain still carrying the
dismissing merchant's index; it only flips to `0xFF` (generally hireable) when
that town's tavern is next opened (observed in-game). Until then the spawn task
does not count him - so dismissing captains without revisiting their taverns
makes the game under-count and spawn extras, pushing the world above the usual
two hireable captains (four observed live).

## Buying Discount
Auto traders buy cheaper as their trade skill grows. The captain (`0x004D5347`) and
administrator (`0x004FF7E8`) buying routines both compute the percentage of the
transaction price to pay from the auto trader's `field_A_trade_skill`:

```
percent_paid = 2 * (50 - trade_skill / 43)
```

`trade_skill / 43` is the displayed 0-5 skill level, so each level is worth 2%, up to
a 10% discount at level 5 (skill byte 215). The administrator routine applies it right
after `get_buy_price` (`0x004FF944`: `price * percent / 100`, with the operand order
flipped above `0x1000000` to avoid overflowing); its sell orders are settled through
`get_sell_price` without any skill adjustment, so the discount is buying-only. An
office whose administrator index (`office+0x2F2`) is invalid pays 100%.

Office administrators do gain skill like captains do (verified in-game: a long-running
save showed administrator trade levels 1-5), even though the game never displays it -
a level 5 administrator quietly buys everything 10% cheaper.
46 changes: 46 additions & 0 deletions src/bugs/patrol-letter-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Patrol Letter Crash

## Summary
Opening the personal letters list sometimes crashes the game to desktop while
certain scripted letters are present - most prominently the escort/patrol
mission's "Patrol destination" letters. The crash is long known in the community
as the "patrol mission crash" and looks random: the same letter may crash the
game, show a wrong town in the list, or show no town at all.

## Details
Every [message](../letters.md) carries a town byte that the letters list draws as
its town column, by indexing the 40-slot town-name
[name bank](../ui/name-banks.md) without a bounds check
(`0x0047D928: mov eax, [edx*4+0x6DDA00]`). The resulting pointer goes straight to
the render DLL's text draw, which dereferences it without any guard
(`ddraw_Dll+0xF100`).

The [letter script](../letters/scripted-letters.md) creation command stores the
low byte of a script variable as the town byte, unvalidated (`0x004ED4E4`), and
the patrol script asks it for a variable that does not exist: command 37 of
`patrouille.p2m` - the "Patrol destination" letter - names **variable 131** in a
script that declares 25 variables (see
[Mission Scripts](../letters/mission-scripts.md)). The handler indexes the
variable array with that byte regardless, reading 424 bytes past its end, so the
town byte is whatever heap data follows the array - observed bytes include 40, 95,
228 and 255. It is the only out-of-range letter town variable in any of the game's
94 script files. Drawing such a row reads past the name bank into unrelated
globals, and the outcome depends on the value it hits:

- ids 40..~81 land in the adjacent full town-name table, producing a genuine but
wrong town name (typically the first town, "Edinburgh");
- a value that points at readable memory usually starts with a zero byte and
draws as an empty town column;
- anything else - colors, coordinates, small integers - crashes the game the
moment the list is drawn.

Which globals hold what depends on resolution, loaded mods and session history,
which is why the crash appears intermittent. Only the list is affected: the
letter body and header are formatted at creation through the bounded town-name
helper, so reading a letter is always safe.

## Fix
[mod-fix-patrol-letter-crash](https://github.com/P3Modding/p3-lib/tree/master/mod-fix-patrol-letter-crash)
detours the lookup at `0x0047D928`: town bytes below 40 read the bank as before,
anything else draws an empty string - the same blank town column the unpatched
game shows whenever the wild read happens to survive.
75 changes: 75 additions & 0 deletions src/bugs/tavern-mission-lock-leak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Tavern Mission Lock Leak

## Summary
Looking at a mission in a tavern's side room locks the offer to the viewing merchant, so
that nobody else can take it. Leaving the side room by switching to another tavern page
releases the lock; closing the tavern window outright - a right click - does not. The
offer stays locked until it is re-issued, and a locked offer is invisible to every other
merchant.

Single player never notices, because the side room accepts an offer locked to the asking
merchant himself. In multiplayer the leak denies the mission to the other players, and one
player can leak a lock in every town by opening each side room and right-clicking out.

## The Lock
A side room offer is a [tavern mission](../letters/71-tavern-missions.md) letter whose
scheduled task holds the mission's script variables; the variable named by the letter's
`descriptor+0xC` is the lock, holding a merchant index while locked and `0xFFFFFFFF` (or
`0xFFFF`) while free.

The lock is taken and released by the
[tavern interaction](../operations/0052-tavern-interaction.md) operation. Two of its types
matter here, and they work in completely different ways:

- **Type 9, the side room** (handler `0x0053C7A3`) carries the task index and the variable
slot in the operation itself, at `+0x4` and `+0x6`. With a valid merchant it writes that
merchant into the variable if it is still free (`0x0053C808`); with an invalid merchant
index it writes `0xFFFFFFFF` back (`0x0053C7F0`). The panel sends the valid-merchant form
when a page is opened and the invalid-merchant form when it is left, so the lock is taken
and released as the player navigates.
- **Type 10, "Leave"** (handler `0x0053C619`) has no task index to work from and instead
walks the merchant's letter chain with `0x004D7900` to find his offers in that town. This
is the path a closing tavern window relies on, and it is broken.

## The Defect
Both the entry and the continuation of that search compare the **letter index** against
the **merchant count** at `0x006DE4AA`, where the letter pool size at `0x006DD736` is
meant:

```
0053C6ED and eax, 0xffff ; letter index that 0x004D7900 found
0053C6F4 mov cx, [0x006DE4AA] ; merchant count (should be [0x006DD736])
0053C6FB cmp ecx, eax
0053C6FD jbe 0x0053C80A ; bail when index >= merchant count
```

```
0053C783 call 0x004D7900 ; next offer in the chain
0053C78F mov cx, [0x006DE4AA] ; merchant count (should be [0x006DD736])
0053C796 cmp ecx, eax
0053C798 ja 0x0053C706 ; loop only while index < merchant count
```

A game has a few dozen merchants and a letter pool of hundreds of entries (400 in the save
below), so any offer sitting past the first few dozen pool slots fails the test and the
release never runs. The rest of the handler - the lock bytes `town+0x83C`..`+0x83F` and the
tavern's captains and pirates through the auto-trader chain - is reached before this search
and works, which is why captains do not leak the same way.

## Observed
One save, Reval's tavern, reading the lock variable of the "Fugitive" offer (letter 264, a
pool index far above the 37 merchants) before and after each operation:

|Action|Operation|Lock after|
|-|-|-|
|entering the tavern|type 255, merchant 37 (invalid)|`0xFFFFFFFF` - 255 is past the jump table|
|opening the side room|type 9, merchant 36|**`0x24`** - locked|
|clicking another page|type 9, merchant 37 (invalid)|`0xFFFFFFFF` - released|
|entering that page|type 4, merchant 36|unchanged - type 4 has no handler|
|opening the side room again|type 9, merchant 36|**`0x24`** - locked|
|right-clicking the window closed|type 10, merchant 36|**`0x24`** - not released|

## Fix
Not fixed. The two comparisons above would have to read the letter pool size at
`0x006DD736` instead of the merchant count at `0x006DE4AA` - a four-byte change to each
instruction's operand, leaving the rest of the handler alone.
7 changes: 6 additions & 1 deletion src/ch05-00-merchants.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,9 @@
00000648 int field_648;
0000064C int field_64C;
00000650 };
```
```

`field_19_hometown_index` is the town shown as "Home town" on the Personal screen: the
town holding the merchant's home office. It changes when the player moves the home
office and is distinct from the birth town shown in the same screen's date-of-birth
line.
17 changes: 17 additions & 0 deletions src/ch05-04-sailor-pools.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,20 @@ Dismissing a captain sets the sailor reputation to `0`.
The merchant struct's `sailor_pools` array at offset `0xf0` contains an `u8` for every town (indexed by the town's index), which denotes the size of the sailor pool of the merchant in that town.



## Sailors Available for Hire
The pool byte is not the number a merchant can hire. `0x004F6CA0` (thiscall, one
argument, the town index) computes that:

```
cap = [town + 0x2E4] - 1
if cap < 1 { return 0 }
return min(merchant->sailor_pools[town_index], cap)
```

The tavern's sailors page calls it at `0x005D4CB1` for the player merchant
(`[0x006DFC14]`) and its own town index (`window + 0x1BFC`), then caps what it offers at
`50` - the immediate at `0x005D4CD6` that
[mod-tavern-show-all-sailors](./patches/tavern-show-all-sailors.md) raises to `100`.
The cap the getter itself applies, from the town's `+0x2E4`, is a separate limit and
is not affected by that patch.
31 changes: 31 additions & 0 deletions src/file-formats/rou.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,34 @@ The "direction" of a transaction is encoded in the price and amount:
|Negative|Positive|Town -> Ship|

The "Max" amount is represented by `1_000_000_000` for both barrel and bundle wares.
Amounts are stored in raw units: display units times the ware scaling (bundles 2000, barrels 200).

## Action Byte
The action byte combines the stop's repair flag with a first-stop marker:

|Value|Meaning|
|-|-|
|0x00|repair setting "X"|
|0x01|repair setting "R" (repair at this stop)|
|0x09|repair setting "-"|
|0x04|OR'ed onto the route's logical first stop|

## Applied Routes at Runtime
Loaded routes live in a global pool of the same 220-byte stop records, prefixed by a
2-byte next-stop index in the record's first two ("Unused") bytes:

- `[0x006DD72C]` = pool base, `[0x006DD72A]` (u16) = pool record count.
- A route is a circular chain of records through the next-stop indices; the stop
carrying action bit `0x04` is the logical first stop.
- `ship+0x132` (u16) = the pool index of the ship's current route stop; it advances as
the route runs.

## Loading Path
The game loads a route file through the loader at `0x004D5EE0` (thiscall,
`this = 0x006DD728`): it takes a pointer to an MFC-style string object holding the base
name and forms the path `save\AutoRoute\<name>.rou` itself, returning the decompressed
stop buffer. To attach the route to a ship, `transfer_loaded_traderoute` (`0x005492D0`,
thiscall on the operations struct `0x006DF2F0`) reads the buffer pointer from
`operations+0x930` and the target ship index from `operations+0x934`, validates the
stops, allocates pool records, attaches them to the ship's convoy, and frees the buffer
with the game's own allocator.
Loading