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
29 changes: 29 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,30 @@
- [Facilities](./basics/facilities.md)
- [Time](./basics/time.md)
- [Storage](./basics/storage.md)
- [Trading Office](./basics/office.md)
- [Ship Types](./basics/ships.md)
- [Ship Artillery](./basics/ship-artillery.md)
- [Operations](./operations.md)
- [Hand Ship to Pirate](./operations/000d-hand-ship-to-pirate.md)
- [Auto Trader Skill Gain](./operations/0012-auto-trader-skill-gain.md)
- [Captain Retirement](./operations/0013-captain-retirement.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)
- [Administrator Skill Gain](./operations/0067-administrator-skill-gain.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)
- [Advance Time](./operations/00c4-advance-time.md)
- [Set Game Speed](./operations/00c8-set-game-speed.md)
- [Scheduled Tasks](./scheduled-tasks/0000.md)
- [Ten-Day Update](./scheduled-tasks/0003-ten-day-update.md)
- [Criminal Investigation](./scheduled-tasks/0005-criminal-investigation.md)
- [Update Shipyard Experience](./scheduled-tasks/0006-update-shipyard-experience.md)
- [Celebration](./scheduled-tasks/0007-celebration.md)
Expand Down Expand Up @@ -59,7 +73,19 @@
- [Impact](./ships/sea-battles/projectiles/impact.md)
- [Reefs](./ships/sea-battles/reefs.md)
- [Auto Traders](./auto-traders.md)
- [Pirates](./pirates.md)
- [Graphics Library (SGL)](./graphics.md)
- [UI](./ui.md)
- [Trading Office Window](./ui/trading-office-window.md)
- [Auto Trade Goods Dialog](./ui/auto-trade-goods-dialog.md)
- [Ship Panel](./ui/ship-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 +101,9 @@
- [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)
- [Texture Cache Thrash](./bugs/texture-cache-thrash.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
271 changes: 270 additions & 1 deletion src/auto-traders.md

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions src/basics/office.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Trading Office
A trading office starts with a complete [storage](./storage.md) struct - the same layout
towns use, so an office's stock of a ware is `office + 0x4 + ware*4` - and continues with
office-specific fields after it. The whole record is `0x44C` bytes.

|Offset|Type|Meaning|
|-|-|-|
|`0x000`|storage|the office's own [storage](./storage.md), stock at `+0x4 + ware*4`|
|`0x2C4`|u16|owning merchant index|
|`0x2C6`|u8|town index|
|`0x2C8`|u16|next office of the same merchant|
|`0x2CA`|u16|next office in the same town|
|`0x2D2`|u16|business buildings the merchant owns in this town - one per building, verified across several saves and offices. Counted in a loop over the town's buildings (`0x004FFDD9`, `0x004FFE5A`) and added to the administrator's [wage](../auto-traders.md#wages) wherever the interface shows it|
|`0x2D6`|u16|state flags; bit `0x1` means the office holds administrator orders|
|`0x2F2`|u16|the administrator, as an index into the [auto trader](../auto-traders.md) array; out of range when the office has none|
|`0x2F4`|i32[24]|administrator order price per ware, the sign encoding the direction|
|`0x354`|i32[24]|administrator minimum store quantity per ware, raw units|
|`0x3B4`|u32|"lock min. store quantity" bitmap, one bit per ware|
|`0x3B8`|f32[24]|average purchase price of the stock, per ware|

The order arrays are written by
[operation `0x5B`](../operations/005b-office-autotrade-setting-change.md) and the lock
bitmap by [operation `0x66`](../operations/0066-office-autotrade-lock-change.md). There is
no direction field: a positive price is a sell order's minimum price, a negative one a buy
order's maximum price negated, and `0` means no order. What the administrator then does
with them is in [Auto Traders](../auto-traders.md#the-administrators-trading).

## Average Purchase Price
`0x004FF6F0` (thiscall on the office, arguments `(ware, quantity, price)`) is how goods
enter an office's stock with a price attached. It adds `quantity` raw units to the ware's
stock and folds `price` into the running average at `office + 0x3B8 + ware*4`:

```
average = (average * old_stock + quantity * price) / (old_stock + quantity)
```

A slot that was empty takes the incoming price directly instead. This average is the
purchase price the trading office UI shows for the stock on hand, and it is on the same
per-unit basis as a ship's `field_B4` average prices - which is exactly what the caller
passes when a ship unloads cargo into the office.

## What the Lock Bit Does
`0x00500EC0` (thiscall on the office, argument `(ware)`) answers "how much of this ware
may an auto trader take out of here?":

```
administrator index out of range -> current stock
lock bit for this ware clear -> current stock
minimum store quantity <= 0 -> current stock
otherwise -> max(current stock - minimum store quantity, 0)
```

So the "Lock min. store quantity for auto trade ships" checkbox does exactly what its
name says, and only in that one direction: it fences the administrator's minimum store
quantity off from ships **loading** at the office. It does not restrain the
administrator's own trading, and it does not affect ships unloading into the office.
22 changes: 22 additions & 0 deletions src/basics/ship-artillery.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ ShipWeaponId::Bombard => 2000
ShipWeaponId::Cannon => 1000
```

## Combat Power
Next to the scaling table sits a second, byte-wide table at `0x00672CC8` giving each
weapon a combat power:

```rust
ShipWeaponId::SmallCatapult => 9
ShipWeaponId::SmallBallista => 10
ShipWeaponId::LargeCatapult => 22
ShipWeaponId::LargeBallista => 24
ShipWeaponId::Bombard => 30
ShipWeaponId::Cannon => 18
```

Fitting a weapon (`0x0051A4E0`) adds its scaling factor from `0x00672CB4` to the ship's
`field_11C` - the capacity the guns occupy - and its power from `0x00672CC8` to the
ship's `field_120`, so `field_120` is the ship's total artillery power. Removing a
weapon subtracts both. That total is one of the two halves of the fighting strength the
pirate AI compares before attacking; the other is the crew count in `field_40`.

Two further six-byte tables sit in the same block and are not yet identified:
`0x00672CC0` = 32, 32, 77, 77, 96, 58 and `0x00672CD0` = 60, 80, 60, 80, 90, 90.

## Ship Artillery Slots
A ship's artillery slots are filled with the following enum:
```c
Expand Down
64 changes: 64 additions & 0 deletions src/basics/time.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
# Time
The game time is stored in the static `game_world` struct at offset `0x14` as *ticks*, and is increased by the `advance_time` function at `0x00530E80`.
That function has exactly one caller: the inline handler of the
[Advance Time operation](../operations/00c4-advance-time.md) - game time only ever
advances through the operation queue.

## Ticks
Every ingame day is 256 ticks long, so there are 93440 ticks in a year.
Consequently the least significant byte conveniently encodes the time of day.

## The Calendar
The day, month and year are not counted up as time passes - they are **derived from the
tick counter once a day** by `0x005310D0`, so nothing else has to keep them in step:

|Field|Meaning|
|-|-|
|`game_world+0x0`|day of the month, 1-based|
|`game_world+0x1`|month, 0-based|
|`game_world+0x2`|year (u16), `ticks / 93440`|
|`game_world+0x4`|day of the year (u16), `(ticks >> 8) % 365`, so 0-based|

The routine writes the year and the day of the year straight from the counter, then walks
the month table at `0x00672D78`/`0x00672D7A` - the cumulative day of the year at which each
month starts and ends - to find the month, and subtracts that month's start to get the day
of the month.

The day of the year is read by game logic, not just by the interface: the
[ten-day update](../scheduled-tasks/0003-ten-day-update.md) resets its round counter on any
run that lands in the first ten days of a year.

## Ticking Objects
Different game objects tick at different intervals.
Information about what happens in those ticks can be found in the respective chapters.
Expand Down Expand Up @@ -38,3 +61,44 @@ This results in the following town tick behaviour:

#### Facilities
All facilities tick when their town ticks.

## Game Speed
The tick pacer inside `execute_operations` (`0x00546640`) converts real
milliseconds into [Advance Time operations](../operations/00c4-advance-time.md)
once per frame. How many ticks a batch gets depends on the pacing *mode*
(`operations+0x92C`) and its ms-per-tick divisor:

|Mode|What|ms per tick|Max ticks per batch|
|-|-|-|-|
|0|normal play|`operations+0x8D4` - set by the speed slider|8|
|1|fast forward (the mode with its own window)|`operations+0x8D8` (2 in vanilla)|256 (a day)|
|2|local map (town view, sea battle)|the constant `[0x00673CF8]` = 3375|1|

The six positions of the speed slider set the mode-0 divisor to 3515, 468, 351,
234, 117 and 78 ms per tick (measured in vanilla 1.1); the slider never changes the
mode. Entering the local map switches to mode 2, whose pace is a hard constant -
which is why the speed controls have no effect there. All speed changes travel as
the [Set Game Speed operation](../operations/00c8-set-game-speed.md), and
`operations+0x914` is the master run flag the pacer requires (0 = paused).

The same pacer also enqueues the autosave operation (`0xC2`) whenever the timer at
`operations+0x940` expires (period `operations+0x944`, 180000 ms in vanilla).

## The Frame Clock
Real time reaches the game through one updater (`0x004BD180`), called once per
frame from the main loop's frame function: it reads the OS time, sleeps the
remainder of a 20 ms frame (the game is capped at 50 fps, `0x004BD1A9`), and
computes the frame's elapsed milliseconds into two globals:

|Global|Meaning|
|-|-|
|`0x006DCCF0`|raw OS time of the last frame|
|`0x006DCCF4`|this frame's elapsed ms|
|`0x006DCCF8`|accumulated game clock (ms), the sum of all frame deltas|

The tick pacer measures against `0x006DCCF8`, and so does the local-map simulation
(ship movement, projectiles, battle AI): it is paced neither by game ticks nor by
how often its update runs - calling the update several times per frame moves
nothing - but purely by this clock. Scaling the delta before it is stored (its
computation at `0x004BD1E2` is a detourable 5-byte sequence) therefore speeds up
the local map and the world alike; mod-ui-tweaks' "extra speed" does exactly that.
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.
Loading