Patrician 3 Insights

This book is a compilation of insights gained by reverse engineering of Ascaron's game Patrician 3. It is not a guide that explains strategies and tactics, the only purpose is to explain the game's behavior as accurately as possible.

All names (of functions, structs, enums, classes, static objects) have been guessed during the reverse engineering process. The original names are unknown.

The book is open source. If you spot something wrong or want to contribute, send a pull request or open an issue.

Introduction

Patrician 3 has been distributed by different publishers in different time periods and countries, and has no universally patch naming scheme. This book references to the latest GoG release (1.1 w10), which was released on 2021/04/26. To verify you are on the same version, you can check these md5 sums of the relevant game files:

b19cba7ae0ffd46ec1d4b59ef9ad7c10  Mapeditor.exe
f9ae89baeaa567938e3b267d9bb466ab  P3HardwareSettings.exe
31b0ca57335160f7700e8613fa16b93c  P3Setup.exe
baf7e0f931ce3d35c1ba3f5d81eb990a  Patrician3.exe
809cd5186cedcbc00ccd3c473d23cdcb  binkplay.exe
487ab8252e4b8248db20f00e4f322240  dxcfg.exe
01190d8b6805fd4d2a68750fbd041966  unins000.exe
74c32bab62943da3e9397c57e4afce92  AIM.dll
6050bcc1b23f3df7a1876cbdcbac8232  MSVCP60.dll
b765198036e60c0cfa8b71d3d1dbd5e2  Schnelltransfer.dll
67da997f755ad3c02d626a9ed2aaa2e1  Vto.dll
9dee655ee5a80a3c61c7061530b74b66  WalType.dll
3e6c131e916311a1d80823da8b0fb6d7  archiver.dll
3e1dbf53fb6d3fee8932a1062f5559b0  ddraw.dll
1dc50afed2fadda3cb69c989aea52964  ddraw_Dll.dll
f067b3e660cebed6ac554fe0c9d7d17d  drvmgt.dll
a0ce0247d48fecaac607edb1e2d87fd8  ijl11.dll
253a5cce82e4b2d9a76553790851aff9  mfc42.dll
30873a23430e136247fe3a0f84d9f7b8  mss32.dll
92781a3ddc20089ee165084db0909f63  p2arch0_eng.cpr
9087d5e9c0b1c3314d2c2b6b0a3b4f3c  p2arch1_eng.cpr

Getting Started

Patrician 3 is a 32bit executable with no DRM or anti debugging methods included. Reverse engineering suites such as IDA or Ghidra can decompile most functions. While some classes have vtables, most functions operate on structs, and the call hierarchy can be analyzed statically.

Scripts and Tooling

Sometimes, this book will provide IDC scripts that help debugging and understanding Patrician 3's runtime behavior. A Cheat Engine table can be found in the appendix. There is also a Rust library that provides an API to Patrician 3's memory space.

Abbreviations

AbbreviationMeaning
P3Patrician 3

Basics

This chapter describes ubiquitous functions, enums and structs whose understanding of is a prerequisite of the following chapters.

Enums

Functions

AddressSignatureDescription
0x0064F7B9malloc_wrapper(size_t size)A thin wrapper around malloc that is used for most heap allocations of the game.

Structs and Unions

Ware Types

Wares are represented by the following enum:

#![allow(unused)]
fn main() {
pub enum WareId {
    Grain = 0x00,
    Meat = 0x01,
    Fish = 0x02,
    Beer = 0x03,
    Salt = 0x04,
    Honey = 0x05,
    Spices = 0x06,
    Wine = 0x07,
    Cloth = 0x08,
    Skins = 0x09,
    WhaleOil = 0x0a,
    Timber = 0x0b,
    IronGoods = 0x0c,
    Leather = 0x0d,
    Wool = 0x0e,
    Pitch = 0x0f,
    PigIron = 0x10,
    Hemp = 0x11,
    Pottery = 0x12,
    Bricks = 0x13,
    Sword = 0x14,
    Bow = 0x15,
    Crossbow = 0x16,
    Carbine = 0x17,
}
}

Although the militia weapons are part of the wares enum, the game often uses loops and mapping arrays that exclude them.

Ware Scaling

The amounts P3 displays in-game are not the values the game uses under the hood. Every ware has a scaling factor, through which the game divides the actual values. Wares with a barrel icon have a scaling factor of 200, Wares with a bundle icon have a scaling factor of 2000.

A table that maps every barrel WareId to 1 and every bundle WareId to 0 can be found at 0x00672C14. The scaling of militia weapons can be inferred by transferring one piece and observing the value changes in memory. This reveals the following factors:

#![allow(unused)]
fn main() {
WareId::Grain => 2000
WareId::Meat => 2000
WareId::Fish => 2000
WareId::Beer => 200
WareId::Salt => 200
WareId::Honey => 200
WareId::Spices => 200
WareId::Wine => 200
WareId::Cloth => 200
WareId::Skins => 200
WareId::WhaleOil => 200
WareId::Timber => 2000
WareId::IronGoods => 200
WareId::Leather => 200
WareId::Wool => 2000
WareId::Pitch => 200
WareId::PigIron => 2000
WareId::Hemp => 2000
WareId::Pottery => 200
WareId::Bricks => 2000
WareId::Sword => 10
WareId::Bow => 10
WareId::Crossbow => 10
WareId::Carbine => 10
}

Buildings

New building ids, used in operations that create new buildings, are represented by the following enum:

#![allow(unused)]
fn main() {
pub enum NewBuildingId {
    Well = 0x28,
    Tower = 0x29, // Cannon, Bombard; Gate, Port
    HousePoor = 0x2a,
    PitchShoot = 0x2f,
    HouseWealthy = 0x50,
    HouseRich = 0x51,
    FarmGrain = 0x53,
    FarmHemp = 0x54,
    FarmSheep = 0x55,
    FarmCattle = 0x56,
    FishermansHouse = 0x57,
    Brewery = 0x58,
    Apiary = 0x59,
    WeavingMill = 0x5a,
    Workshop = 0x5b,
    Vineyard = 0x5c,
    HuntingLodge = 0x5d,
    Saltworks = 0x5e,
    IronSmelter = 0x60,
    Pitchmaker = 0x61,
    Brickworks = 0x62,
    Pottery = 0x63,
    Sawmill = 0x64,
    Hospital = 0x65,
    Warehouse = 0x66,
    Mint = 0x67,
    School = 0x68,
    Chapel = 0x69,
}
}

At 0x672fbf is a table that maps building ids to new building ids:

BuildingIdNewBuildingId
0x000x2c
0x010x1b
0x020x0b
0x030x08
0x04HuntingLodge
0x05FishermansHouse
0x06Brewery
0x07Workshop
0x08Apiary
0x09FarmGrain
0x0aFarmCattle
0x0bSawmill
0x0cWeavingMill
0x0dSaltworks
0x0eIronSmelter
0x0fFarmSheep
0x10Vineyard
0x11Pottery
0x12Brickworks
0x13Pitchmaker
0x14FarmHemp
0x15HouseRich
0x16HouseRich
0x17HouseRich
0x18HouseWealthy
0x19HouseWealthy
0x1aHouseWealthy
0x1bHousePoor
0x1cHousePoor
0x1dHousePoor
0x1eWarehouse
0x1f0x04
0x200x03
0x210x0b
0x220x07
0x230x00
0x240x02
0x250x05
0x260x09
0x270x33
0x280x52
0x29Hospital
0x2aMint
0x2bSchool
0x2cChapel
0x2d0x37
0x2e0x01
0x2f0x5f
0x30Tower
0x31Tower
0x32Tower
0x33Tower
0x34PitchShoot
0x35PitchShoot

Facilities

Business and miscellaneous buildings are grouped into facilities.

#![allow(unused)]
fn main() {
pub enum FacilityId {
    Militia = 0x00,
    Shipyard = 0x01,
    Weaponsmith = 0x03,
    HuntingLodge = 0x04,
    FishermansHut = 0x05,
    Brewery = 0x06,
    Workshop = 0x07,
    Apiary = 0x08,
    FarmGrain = 0x09,
    FarmCattle = 0x0a,
    Sawmill = 0x0b,
    WeavingMill = 0x0c,
    Saltworks = 0x0d,
    IronSmelter = 0x0e,
    FarmSheep = 0x0f,
    Vineyard = 0x10,
    Pottery = 0x11,
    Brickworks = 0x12,
    Pitchmaker = 0x13,
    FarmHemp = 0x14,
}
}

The town's facilities are stored within the town struct.

00000000 struct facility // sizeof=0x10
00000000 {                                       // XREF: town/r
00000000     int field_0_efficiency;
00000004     unsigned __int16 field_4_employees;
00000006     unsigned __int8 field_6_type;
00000007     unsigned __int8 field_7_town_index;
00000008     __int16 field_8_productivity;
0000000A     __int16 field_A;
0000000C     __int16 field_C;
0000000E     unsigned __int16 field_E;
00000010 };

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.

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.

Ticking Objects

Different game objects tick at different intervals. Information about what happens in those ticks can be found in the respective chapters.

Towns

Towns tick if one of the following equations is true:

game_time & 0b111 == 0b011 &&
town_index == (((unsigned __int8)game_time) + 255) >> 3
game_time & 0b111 == 0b111 &&
town_index == ((unsigned __int8)game_time) >> 3

This results in the following town tick behaviour:

Town IndexGame Time LSB
320b00000_011
330b00001_011
340b00010_011
350b00011_011
......
390b00111_011
000b00000_111
010b00001_111
020b00010_111
030b00011_111
......
310b11111_111

Facilities

All facilities tick when their town ticks.

Storage

The storage struct contains a town's or an office's current wares and related data. Some of the fields have been identified:

00000000 ; derived from post-malloc initialization of office and town
00000000 storage         struc ; (sizeof=0x2C0, mappedto_146)
00000000                                         ; XREF: town/r
00000000                                         ; office/r
00000000 field_0         dw ?
00000002 field_2         dw ?
00000004 field_4_current_wares dd 24 dup(?)
00000064 field_64_daily_consumptions_businesses dd 24 dup(?)
000000C4 field_C4_daily_production dd 24 dup(?)
00000124 field_124_ship_weapons dd 6 dup(?)
0000013C field_13C_prod_time_series storage_production_time_series 20 dup(?)
0000027C field_27C       dd ?
00000280 field_280       dd ?
00000284 field_284       dd ?
00000288 field_288       dd ?
0000028C field_28C       dd ?
00000290 field_290       dd ?
00000294 field_294       dd ?
00000298 field_298       dd ?
0000029C field_29C       dd ?
000002A0 field_2A0       dd ?
000002A4 field_2A4       dd ?
000002A8 field_2A8       dd ?
000002AC field_2AC       dd ?
000002B0 field_2B0       dd ?
000002B4 field_2B4       dd ?
000002B8 field_2B8       dd ?
000002BC field_2BC_cutlasses dd ?
000002C0 storage         ends

Ship Types

Ship types are represented by the following enum:

#![allow(unused)]
fn main() {
pub enum ShipTypeId {
    Snaikkka = 0x00,
    Craier = 0x01,
    Cog = 0x02,
    Hulk = 0x03,
}
}

Ship Artillery

Ship Weapons are represented by the following enum:

#![allow(unused)]
fn main() {
pub enum ShipWeaponId {
    SmallCatapult = 0x00,
    SmallBallista = 0x01,
    LargeCatapult = 0x02,
    LargeBallista = 0x03,
    Bombard = 0x04,
    Cannon = 0x05,
}
}

Cutlasses are not ship weapons.

Ship Artillery Scaling

The amounts P3 displays in-game are not the values the game uses under the hood. Every ship weapon has a scaling factor, through which the game divides the actual values.

A table that maps every ShipWeaponId to its scaling factor can be found at 0x00672CB4. This reveals the following factors:

#![allow(unused)]
fn main() {
ShipWeaponId::SmallCatapult => 1000
ShipWeaponId::SmallBallista => 1000
ShipWeaponId::LargeCatapult => 2000
ShipWeaponId::LargeBallista => 2000
ShipWeaponId::Bombard => 2000
ShipWeaponId::Cannon => 1000
}

Ship Artillery Slots

A ship's artillery slots are filled with the following enum:

enum ship_artillery_slot : unsigned __int8
{
  ship_artillery_slot_small_catapult = 0u,
  ship_artillery_slot_small_ballista = 1u,
  ship_artillery_slot_large_catapult = 2u,
  ship_artillery_slot_large_ballista = 3u,
  ship_artillery_slot_bombard = 4u,
  ship_artillery_slot_cannon = 5u,
  ship_artillery_slot_large_neighbor = 6u,
  ship_artillery_slot_unavailable = 7u,
  ship_artillery_slot_empty = 255u,
};

The slots are indexed as indicated here:

Operations

P3 handles most changes to the world and its elements through operations. An operation is a 20 bytes wide struct, where the first u32 denotes the operation's type, and the following 16 bytes contain the operation's arguments.

Scheduling

While the game executes operation handlers directly sometimes, usually operations are scheduled by inserting them into the pending operations queue in the static operations struct at 0x006DF2F0:

00000000 operations      struc ; (sizeof=0x948, mappedto_126)
[...]
00000048 field_48_pending_operations operation_switch_input_container ?
0000046C field_46C       dd ?
00000470 field_470_pending_operations_in_use dd ?
00000474 field_474_current_operations operation_switch_input_container ?
[...]

The operation_switch_input_container struct contains an array of 53 operations. The function schedule_operation at 0x00543F10 inserts an operation at the next free position, creating a new operation_switch_input_container and appending it to the last full one if necessary.

Execution

The function execute_operations at 0x00546870 removes up to 53 operations from operations and executes them.

Debugging

The following IDC script adds scripted breakpoints to the executing and scheduling functions, allowing the investigation of P3's operation behavior:

static handle_operation_switch() {
    auto ptr = GetRegValue("esi");
    auto operationSwitchInput = OperationSwitchInput(ptr);
    // Ignore noisy operations
    if (operationSwitchInput.opcode() == 0x94) {
        return 0;
    }
    if (operationSwitchInput.opcode() == 0x24) {
        return 0;
    }
    if (operationSwitchInput.opcode() == 0x7B) {
        return 0;
    }
    
    Message(operationSwitchInput.toString());

    return 0;
}

static handle_insert_into_pending_operations_wrapper() {
    auto ptr = GetRegValue("eax");
    auto operationSwitchInput = OperationSwitchInput(ptr);
    // Ignore noisy operations
    if (operationSwitchInput.opcode() == 0x94) {
        return 0; // weird thing
    }

    Message("handle_insert_into_pending_operations_wrapper %s", operationSwitchInput.toString());

    return 0;
}

static main() {
    auto bp = AddBpt(0x0053576B);
    SetBptCnd(0x0053576B, "handle_operation_switch()");
    auto bp2 = AddBpt(0x0054AABD);
    SetBptCnd(0x0054AABD, "handle_insert_into_pending_operations_wrapper()");
}

Identified Operations

The following operations have been identified:

OpcodeTask
0x00Move Ship
0x01Sell Wares from Ship
0x02Buy Wares to Ship
0x03Repair Ship
0x04Hire Sailors
0x06Dismiss Captain
0x15Create Convoy
0x16Disband Convoy
0x1bMove Wares
0x1dRepair Convoy
0x24Build Town Wall
0x25Build Road
0x26Demolish Building
0x29Grant Loan
0x2cBuild Ship
0x30Feed the Poor
0x31Donate to Church Extension
0x32Donate to Church
0x37Join Guild
0x39Bathe
0x41Form Militia Squad
0x42Bath House Bribe Success
0x43Bath House Bribe Failure
0x48Make Town Hall Offer
0x52Tavern Interaction
0xc2Autosave
0xc4Advance Time
0x9fStart Ship Combat
0x96Steer Manually

Bath House Bribe Success

The handle_operation_bath_house_bribe_success function at 0x0053AC50 applies the effects of a successful bribe. Money is subtracted as expected, and the expenses are added to the monthly expenditure statistic under "miscellaneous".

The merchant's social reputation in the corresponding town is increased as explained in the reputation section.

There is a check which probably should prevent a merchant from bribing more than 2 councillors in one town. However, this check is bugged, as discussed in the Known Bugs chapter.

Bath House Bribe Failure

The handle_operation_bath_house_bribe_failure function at 0x0053AD10 applies the effects of a failed bribe.

The merchant's social reputation in the corresponding town is decreased as explained in the reputation section.

If the councillor was already successfully bribed by the merchant or not bribed by any merchant, he will now be bribed by the merchant with the index 1. This is wrong, as discussed in the Known Bugs chapter.

Make Town Hall Offer

The handle_operation_48_make_townhall_offer function at 0x0053B010 handles proposed changes to a town's policy.

struct operation_48_make_town_hall_offer
{
  signed __int32 field_0_meeting_timestamp;
  signed __int32 field_4_extra_tax_amount;
  unsigned __int8 field_8_town_index;
  council_meeting_type field_9_meeting_type;
  signed __int16 field_A_tax_per_head_amount;
  char field_C_merchant_index;
  char field_D;
  char field_E;
  char field_F;
};

Normal Towns

For normal towns a Council Meeting scheduled task is scheduled at the given meeting timestamp.

Hanseatic Settlements

Hanseatic settlements have no council and expand military or enlarge town wall offers. A change to the head tax is applied immediately, an Extra Tax scheduled task is scheduled at the next tick.

Tavern Interaction

The tavern interaction operations are enqueued by the tavern's panel when switching between the tavern's pages. The following fields have been identified:

struct operation_tavern_interaction
{
  int field_0_rand;
  int field_4_merchant_index;
  int field_8_town_index;
  tavern_interaction field_C_interaction_type;
};

Depending on the interaction type, one of the following actions may be done.

1

3 and 8

5

Weapons Dealer

If the interaction's merchant index is invalid, the town's weapons dealer is unlocked, and no other action is performed. This happens if a merchant navigates from the weapons dealer page to a different page.

Otherwise if the town's weapons dealder is unlocked, it'll be locked to the merchant, and a criminal investigation might be started. An investigation is started only if all of the following conditions are met:

  • The merchant is not the alderman
  • The merchant is not the mayor in the particular town
  • The town is not sieged, blocked, boycotted or under pirate attack
  • The following formula is true: (rand & 0x3ff) < 102
  • The following formula is true: weaponsdealer_timestamp < now + 0x200

If all conditions are met, a criminal investigation scheduled task is scheduled to (now + 0x200) | 0x80, and the weapons dealer timestamp is set to now.

Burglar

The burglar is handled like the weapons dealer, except the exceptions for alderman, local mayor and town status don't exist.

9

Leave

Start Criminal Investigation

The handle_operation_81_start_criminal_investigation at 0x0053FA80 schedules a Criminal Investigation task to an indicated timestamp. The following fields have been identified:

struct operation_81_start_criminal_investigation
{
  signed __int32 field_0_merchant_index;
  signed __int32 field_4_crime_type;
  signed __int32 field_8_delay;
  signed __int32 field_C_town_index;
};

The scheduled task's target git difftimestamp is calculated as follows:

(delay + now() + 0x100) & 0x00 | 0x80

Scheduled Tasks

P3 has a task queue for actions that shall be executed at a given tick in the future.

Scheduled Tasks Struct

00000000 struct scheduled_tasks // sizeof=0x14
00000000 {
00000000     scheduled_task *field_0_tasks;
00000004     int field_4_is_in_use;
00000008     unsigned __int16 field_8_earliest_scheduled_task_index;
0000000A     __int16 field_A;
0000000C     unsigned __int16 field_C_tasks_size;
0000000E     __int16 field_E;
00000010     int field_10;
00000014 };

The static scheduled tasks object is at 0x006DD73C. The handle_scheduled_tasks_tick function at 0x004D85C0 executes all tasks that are due. It is called at least once per tick.

Scheduled Task Struct

00000000 struct scheduled_task // sizeof=0x18
00000000 {
00000000     unsigned int field_0_due_timestamp;
00000004     unsigned __int16 field_4_next_task_index;
00000006     scheduled_task_opcode field_6_opcode;
00000008     scheduled_task_union field_8_data;
00000018 };

The scheduled task's opcdode field denotes which kind of task it is. Some task types are recurring, and reschedule themselves immediately when they are executed. The data field is a union containing all possible task arguments.

Identified Tasks

The following scheduled tasks have been identified:

OpcodeTask
0x01Debt Repayment
0x05Crime Investigation Result
0x06Update Shipard Experience
0x07Celebration
0x0cLand Transport Arrival
0x15Marriage
0x1aUpdate Sailor Pools
0x2eCouncil Meeting
0x35Unfreeze Harbor
AddressFunctionDescription
0x004D8DD0reschedule_first_taskMoves the first element to its appropriate position in the queue.

Criminal Investigation

Begin

When a criminal investigation task is executed for the first time, the status is crime_investigation_status_pending. The task handler sends a Charge or Indictment letter to the offending merchant, sets the status to crime_investigation_status_investigating, and reschedules the task according to the result of the following computation:

(now + (((now & 7) + 8) << 8)) | 0x80

The lower 3 bits of the current time are used as a synchronized pseudorandom number ranging from 0 to 7. To that number 8 is added, and the result is shifted by 8 to get a timespan between 8 to 15 days. That timespan is added to the current time, and the 8th bit is set to constrain the time of day between 12:00 and 24:00.

Both random fields are filled with the result of rand() with a RAND_MAX of 32767.

Verdict

TODO

Scheduled Task Data

The following task fields have been identified:

struct scheduled_task_criminal_investigation
{
  unsigned __int8 field_0_merchant_index __tabform(NODUPS);
  unsigned __int8 field_1_town_index;
  char field_2;
  unsigned __int8 field_3_hometown_index;
  int field_4_timestamp;
  crime_type field_8_crime_type;
  crime_investigation_status field_9_status;
  unsigned __int16 field_A_random1;
  unsigned __int16 field_C_random2;
  signed __int16 field_E;
};

where crime_type was found to be:

enum crime_type : unsigned __int8
{
  crime_type_criminal_plans = 0x0,
  crime_type_boycott_broken = 0x1,
  crime_type_pirate_attack = 0x2,
  crime_type_burglary = 0x3,
  crime_type_pirate_sponsor = 0x4,
  crime_type_indecent_behaviour = 0x5,
  crime_type_heresy = 0x6,
  crime_type_round_world = 0x7,
  crime_type_undermining_league = 0x8,
  crime_type_pirate_firing_on_ships = 0x9,
  crime_type_pirate_plundering_ships = 0xA,
  crime_type_pirate_sinking_ships = 0xB,
  crime_type_pirate_capturing_ships = 0xC,
  crime_type_pirate_attacking_town = 0xD,
  crime_type_pirate_firing_on_town = 0xE,
  crime_type_pirate_plundering_town = 0xF,
};

and crime_investigation_status was found to be:

enum crime_investigation_status : unsigned __int8
{
  crime_investigation_status_pending = 0x0,
  crime_investigation_status_investigating = 0x1,
  crime_investigation_status_confiscation_successful = 0x2,
  crime_investigation_status_unknown = 0x3,
};

Update Shipard Experience

The st_update_shipyard_experience function at 0x004E2144 updates the experience, utilization markups and ship quality levels of all shipyards.

Pending Experiene and Utilization Markup

Pending experience is added to the shipyard's experience and set to 0. The utilization is updated as follows:

capacity = 0
for ship_order in ship_orders:
    capacity += ship_order.capacity

markup_change = (capacity // 2000 + pending_experience // 19600) / 26
utilization_markup = markup_change + utilization_markup * 0.96153843

Unlocking Ship Quality Levels

The shipyard_level_requirements table at 0x00673818 defines the following base experience requirements:

TypeQL 0QL 1QL 2QL 3
Snaikka01003001050
Crayer0100600900
Cog0200400800
Holk3005006001200

A new quality level is unlocked, if the shipyard's experience exceeds 2800 * required_base_experience.

Interval

This scheduled task reschedules itself 0x700 ticks ahead, so it is executed once per week.

Celebration

The st_celebration function is at 0x004E23A4.

00000000 struct scheduled_task_celebration // sizeof=0x10
00000000 {
00000000     signed __int32 field_0_merchant_index;
00000004     signed __int32 field_4_town_index;
00000008     signed __int32 field_8_maybe_type;
0000000C     signed __int32 field_C;
00000010 };

Consumption

The celebration_base_consumption table at 0x006734E8 defines the base consumption per guest:

WareBase Consumption
Grain3
Meat2
Fish2
Beer2
Salt0
Honey1
Spices0
Wine2

The consumption per guest is calculated as follows:

celebration_wares = [
    WareId.Grain,
    WareId.Meat,
    WareId.Fish,
    WareId.Beer,
    WareId.Salt,
    WareId.Honey,
    WareId.Spices,
    WareId.Wine,
]

for ware in celebration_wares:
    base_consumption = guests * celebration_base_consumption[ware]
    scaled_consumption = base_consumption * (4 if has_famine else 2)

    # Ceil to the next barrel/bundle
    if is_barrel_ware[ware]:
        consumption = 200 * ((scaled_consumption + 199) / 200)
    else
        consumption = 2000 * ((scaled_consumption + 1999) / 2000)

Not having enough wares to cover the consumption does not impact celebration level, merchant popularity, or citizen satisfaction.

Attendance

The amount of guests is calculated as follows:

attendance_ratio = min(99, max(0, 39 + local_reputation))
eligible_citizens = attendance_ratio * total_citizens / 100
satisfied_wares = 0

for ware in celebration_wares:
    if celebration_base_consumption[ware] == 0:
        continue
    if celebration_base_consumption[ware] * eligible_citizens <= office.wares[ware]:
        satisfied_wares += 1

satisfaction_ratio = attendance_ratio * satisfied_wares // 6
capped_satisfaction_ratio = max(2, min(99, satisfaction_ratio))
guests = max(5, total_citizens * capped_satisfaction_ratio // 100)

Since exactly 6 wares have a non-zero consumption, satisfaction_ratio is equal to attendance_ratio if all wares are satisfied.

Levels

Depending on how many wares were available in sufficient amounts, the celebration is classified as one of the following levels:

LevelLetter
0This was not really a great celebration [...]
1The celebration was only moderately successful [...]
2The celebration was relatively successful [...]
3It was a fantastic celebration [...]

A celebration's level is calculated as follows:

level = satisfied_wares // 2

Consequently, a partially satisfied ware does not contribute to the celebration's success.

Satisfaction

Reputation

Under be assumption that the base_rep_factor is always 1, the impact of each celebration level is:

LevelSocial Reputation Impact
0-1
10.5
21
31.5

Update Sailor Pools

The st_update_sailor_pools function at 0x004F6C10 updates all sailor pools of all merchants. The pool size is calulated with the following formula:

increase = sailor_reputation // 4
beggar_multiplier = min(100, town.beggars)

new_value = merchant.sailor_pools[town_index] + increase
capped_value = (beggar_multiplier * sailor_reputation) // 20

merchant.sailor_pools[town_index] = min(new_value, capped_value)

The pool size cannot exceed capped_value, which cannot exceed (100*20)//20, so the pool size is capped at 100. Merchants cannot hire any sailors while their sailor reputation is below 4, because increase will be 0.

Interval

This scheduled task reschedules itself 64 ticks ahead, so it is executed at tick 0, 64, 128, and 192 every day.

Council Meeting

The handle_st_2e_council_meeting function at 0x004E9A94 handles the entire council meeting voting process by rescheduling itself multiple times.

struct scheduled_task_2e_council_meeting
{
  signed __int32 field_0_extra_tax_amount;
  unsigned __int8 field_4_town_index;
  council_meeting_type field_5_meeting_type;
  char field_6_pending_yes;
  char field_7_pending_no;
  unsigned __int8 field_8_yes;
  unsigned __int8 field_9_no;
  char field_A_maybe_abstain;
  unsigned __int8 field_B_player_vote_bitmask;
  signed __int16 field_C_tax_per_head_amount;
  char field_E_merchant_index;
  char field_F;
};

First Execution

On the first tick of the day (and thus the first tick of the meeting), the task is initialized:

  • pending_no is set to 22.
  • pending_no is decreased by 1 for every bribed councillor.
  • For every player merchant:
    • If the merchant is a councillor or higher, a notification is sent to the player.
    • pending_no is decreased by 1 for a councillor, by 2 for a patrician and mayor, and by 3 for an alderman.
  • Should pending_no be smaller than 4, it is set to 4.

Afterwards the offer-specific votes in favour are determined, and the task is rescheduled at 4 ticks in the future.

Extra Tax

TODO

Enlarge Town Walls

TODO

Expand Military

The expand military voting behaviour is calculated as follows:

if town.is_under_siege():
    total_citizens *= 2

t = 512 - 203 * town.approved_militia_size + total_citizens
if t < 100:
    pending_yes = pending_no // 10
elif t <= 920:
    pending_yes = pending_no * t // 256
else:
    pending_yes = 9 * pending_no // 10

pending_no -= pending_yes

This causes the following relationship of citizens and military size:

Change Tax per Head

TODO

Subsequent Executions

If pending_no and pending_yes are bigger than 0, the yes vote count is increased if the following formula is true:

yes + 3 * pending_yes <= no + 3 * pending_no

Otherwise the no vote count is increased.

If only one of pending_no and pending_yes is bigger than 0, the corresponding vote count is increased.

If both pending_no and pending_yes are 0, no vote count is increased.

If the least significant byte of the timestamp is smaller than 0xDC, the scheduled task is rescheduled at 4 ticks in the future. Otherwise the task is rescheduled at the next tick.

Final Execution and Result

During the execution of the task on the first tick of the following day the result of the ballot is determined and applied.

Extra Tax

TODO

Enlarge Town Walls

TODO

Expand Military

If the town's old approved military size is below 0x8000, it is increased by 10.

Change Tax per Head

TODO

Unfreeze Port

The st_unfreeze_port function is at 0x004E94A4. It removes the town's frozen flag and updates the UI.

00000000 struct scheduled_task_unfreeze_port // sizeof=0x4
00000000 {
00000000     signed __int32 field_0_town_index;
00000004 };

Towns

P3 has 40 different towns, which are assigned to one of 5 different regions:

IdName
0West
1North
2North Sea Area
3Baltic Sea Area
4East

Towns are indentified by their id, and scripts/StadtDaten.ini defines details such as the corresponding region:

IdNameRegion
0EdinburghWest
1NewcastleWest
2ScarboroughWest
3BostonWest
4LondonWest
5BrugesWest
6HaarlemNorth Sea Area
7HarlingenNorth Sea Area
8GroningenNorth Sea Area
9CologneNorth Sea Area
10BremenNorth Sea Area
11RipenNorth Sea Area
12HamburgNorth Sea Area
13FlensburgNorth Sea Area
14LuebeckNorth Sea Area
15RostockNorth Sea Area
16BergenNorth
17StavangerNorth
18ToensbergNorth
19OsloNorth
20AalborgNorth
21GoeteborgNorth
22NaestvedNorth
23MalmoeNorth
24AhusNorth
25StockholmNorth
26VisbyNorth
27HelsinkiNorth
28StettinBaltic Sea Area
29RuegenwaldBaltic Sea Area
30GdanskBaltic Sea Area
31TorunBaltic Sea Area
32KoenigsbergBaltic Sea Area
33MemelBaltic Sea Area
34WindauEast
35RigaEast
36PernauEast
37RevalEast
38LadogaEast
39NovgorodEast

The "Found Settlement" alderman mission UI does not use the definitions from scripts/StadtDaten.ini, but instead uses the following hardcoded mapping:

IdNameRegion
0EdinburghWest
1NewcastleWest
2ScarboroughWest
3BostonWest
4LondonWest
5BrugesWest
6HaarlemNorth Sea Area
7HarlingenNorth Sea Area
8GroningenNorth Sea Area
9CologneNorth Sea Area
10BremenNorth Sea Area
11RipenNorth Sea Area
12HamburgNorth Sea Area
13FlensburgBaltic Sea Area
14LuebeckBaltic Sea Area
15RostockBaltic Sea Area
16BergenNorth
17StavangerNorth
18ToensbergNorth
19OsloNorth
20AalborgNorth
21GoeteborgNorth
22NaestvedNorth
23MalmoeNorth
24AhusNorth
25StockholmNorth
26VisbyNorth
27HelsinkiNorth
28StettinBaltic Sea Area
29RuegenwaldBaltic Sea Area
30GdanskBaltic Sea Area
31TorunBaltic Sea Area
32KoenigsbergBaltic Sea Area
33MemelEast
34WindauEast
35RigaEast
36PernauEast
37RevalEast
38LadogaEast
39NovgorodEast

Town Names

There is an array of pointers (indexed by the town's index) to town names at 0x006DDA00.

Town Struct

The pointer to the towns array is stored in the static game_world struct at offset 0x68, and the length of that array at offset 0x10. A town's id is not its index in the towns array.

The following fields have been identified:

00000000 town struc ; (sizeof=0x9F8, mappedto_112)
00000000                                         ; XREF: town_wrapper/r
00000000 field_0_storage storage ?
000002C0 field_2C0_town_index db ?
000002C1 field_2C1_raw_town_id db ?
000002C2 field_2C2 db ?
000002C3 field_2C3_famine_counter db ?
000002C4 field_2C4 dw ?
000002C6 field_2C6_land_tax dw ?
000002C8 field_2C8_town_flags dd ?
000002CC field_2CC dd ?
000002D0 field_2D0_celebration_timestamp dd ?
000002D4 field_2D4_total_citizens dd ?
000002D8 field_2D8_citizens dd 4 dup(?)
000002E8 field_2E8_citizens_old dd 4 dup(?)
000002F8 field_2F8_citizens_dwellings_occupied dw 3 dup(?)
000002FE db ? ; undefined
000002FF db ? ; undefined
00000300 field_300_citizens_satisfaction dw 4 dup(?)
00000308 field_308 dw 4 dup(?)
00000310 field_310_daily_consumptions_citizens dd 24 dup(?)
00000370 field_370 dd ?
00000374 field_374 dd ?
00000378 field_378 dd ?
0000037C field_37C dd ?
00000380 field_380 dd ?
00000384 field_384 dd ?
00000388 field_388 dd ?
0000038C field_38C dd ?
00000390 field_390 dd ?
00000394 field_394 dd ?
00000398 field_398 dd ?
0000039C field_39C dd ?
000003A0 field_3A0 dd ?
000003A4 field_3A4 dd ?
000003A8 field_3A8 dd ?
000003AC field_3AC dd ?
000003B0 field_3B0 dd ?
000003B4 field_3B4 dd ?
000003B8 field_3B8 dd ?
000003BC field_3BC dd ?
000003C0 field_3C0 dd ?
000003C4 field_3C4 dd ?
000003C8 field_3C8 dd ?
000003CC field_3CC dd ?
000003D0 field_3D0_wares_copy dd 24 dup(?)
00000430 field_430_unknown_wares_data dd 24 dup(?)
00000490 field_490_weird_prods dd 24 dup(?)
000004F0 field_4F0_consumption_data consumption_data 24 dup(?)
00000670 field_670 dd ?
00000674 field_674 dd ?
00000678 field_678 dd ?
0000067C field_67C dd ?
00000680 field_680 dd ?
00000684 field_684 dd ?
00000688 field_688 dd ?
0000068C field_68C dd ?
00000690 field_690 dd ?
00000694 field_694 dd ?
00000698 field_698 dd ?
0000069C field_69C dd ?
000006A0 field_6A0 dd ?
000006A4 field_6A4 dd ?
000006A8 field_6A8 dd ?
000006AC field_6AC dd ?
000006B0 field_6B0 dd ?
000006B4 field_6B4 dd ?
000006B8 field_6B8 dd ?
000006BC field_6BC dd ?
000006C0 field_6C0 dd ?
000006C4 field_6C4 dd ?
000006C8 field_6C8 dd ?
000006CC field_6CC dd ?
000006D0 field_6D0 dd ?
000006D4 field_6D4 dd ?
000006D8 field_6D8 dd ?
000006DC field_6DC dd ?
000006E0 field_6E0 dd ?
000006E4 field_6E4 dd ?
000006E8 field_6E8 dd ?
000006EC field_6EC dd ?
000006F0 field_6F0 db ?
000006F1 field_6F1_mayor_id db ?
000006F2 field_6F2 db ?
000006F3 field_6F3 db ?
000006F4 field_6F4_recent_extra_taxes_amount dd ?
000006F8 field_6F8_head_tax_rate_and_extra_tax_timestamp dd ?
000006FC field_6FC dd ?
00000700 field_700_money dd ?
00000704 field_704_transactions dd 11 dup(?)
00000730 field_730 dd ?
00000734 field_734 dd ?
00000738 field_738 dd ?
0000073C field_73C dd ?
00000740 field_740 dd ?
00000744 field_744 dd ?
00000748 field_748 dd ?
0000074C field_74C dd ?
00000750 field_750 dd ?
00000754 field_754 dd ?
00000758 field_758 dd ?
0000075C field_75C dd ?
00000760 field_760 dd ?
00000764 field_764 dd ?
00000768 field_768 dd ?
0000076C field_76C_more_flags dd ?
00000770 field_770 db ?
00000771 field_771 db ?
00000772 field_772 db ?
00000773 field_773 db ?
00000774 field_774 dw ?
00000776 field_776 dw ?
00000778 field_778 dd ?
0000077C field_77C dd ?
00000780 field_780 dd ?
00000784 field_784_office_index dw ?
00000786 field_786 dw ?
00000788 field_788 db ?
00000789 field_789_wells db ?
0000078A field_78A db ?
0000078B field_78B db ?
0000078C field_78C dw ?
0000078E field_78E dw ?
00000790 field_790_streets_built dw ?
00000792 field_792_streets_total dw ?
00000794 field_794_church church ?
000007A4 field_7A4_town_class1 town_class1 ?
00000810 field_810 dd ?
00000814 field_814 dd ?
00000818 field_818 dd ?
0000081C field_81C dd ?
00000820 field_820 dd ?
00000824 field_824_current_ship_level db 4 dup(?)
00000828 field_828_always_zero db 4 dup(?)
0000082C field_82C dd ?
00000830 field_830 dd ?
00000834 field_834 db ?
00000835 field_835 db ?
00000836 field_836 db ?
00000837 field_837 db ?
00000838 field_838 dd ?
0000083C field_83C db ?
0000083D field_83D db ?
0000083E field_83E db ?
0000083F field_83F db ?
00000840 field_840_class12_array class12 20 dup(?)
00000980 field_980 dd ?
00000984 field_984 dd ?
00000988 field_988 dd ?
0000098C field_98C dd ?
00000990 field_990_outrigger_value dd ?
00000994 field_994_outrigger_id dw ?
00000996 field_996 dw ?
00000998 field_998 db ?
00000999 field_999 db ?
0000099A field_99A db ?
0000099B field_99B db ?
0000099C field_99C dd ?
000009A0 field_9A0 dd ?
000009A4 field_9A4 dd ?
000009A8 field_9A8 dd ?
000009AC field_9AC dd ?
000009B0 field_9B0 dd ?
000009B4 field_9B4 dd ?
000009B8 field_9B8 dd ?
000009BC field_9BC dd ?
000009C0 field_9C0 dd ?
000009C4 field_9C4 dd ?
000009C8 field_9C8 dd ?
000009CC field_9CC dd ?
000009D0 field_9D0 dd ?
000009D4 field_9D4 dd ?
000009D8 field_9D8 dd ?
000009DC field_9DC dd ?
000009E0 field_9E0 dd ?
000009E4 field_9E4 dd ?
000009E8 field_9E8 dd ?
000009EC field_9EC dd ?
000009F0 field_9F0 dd ?
000009F4 field_9F4 dd ?
000009F8 town ends

Ticks

The handle_town_tick function is at 0x0051BA10.

Population

Consumption

The do_population_consumption function is at 0x00527D40.

At 0x00672860 there is a table that contains the daily consumptions for 100 citizens of every population type:

WareRichWealthyPoorBeggars
Grain90120150120
Meat11087125
Fish4080100110
Beer651306575
Salt1111
Honey502552
Spices4220
Wine1503800
Cloth5035151
Skins603000
WhaleOil5035100
Timber80804020
IronGoods10075250
Leather443550
Wool1040205
Pitch0000
PigIron0000
Hemp5323
Pottery3018121
Bricks1100
Sword0000
Bow0000
Crossbow0000
Carbine0000

If a town is not under siege and a ware is in oversupply, more of it is consumed. TODO clarify TODO pitch consumption (sieged and unsieged), winter/famine/plague modifiers

Satisfaction

P3's setting "Needs of the citizens" changes how easy it is to increase the satisfaction, and how fast it changes. The satisfaction classes are displayed in-game: Very happy, happy, very satisfied, satisfied, dissatisfied, and annoyed. The satisfaction for each population type is stored in the town's satisfactions array at offset 0x300, holding an i16 for every population type except Beggars. The function prepare_citizens_menu_ui at 0x0040B570 calculates the satisfaction classes by converting the i16 into an f32, and picking the highest applicable class:

Satisfaction >Satisfaction Class
29.5Very Happy
19.5Happy
9.5Very Satisfied
0.5Satisfied
-10.5Dissatisfied
-InfinityAnnoyed

The function update_citizen_satisfaction at 0x0051C830 calculates the current satisfaction each population type would have. The satisfaction is then increased or decreaseed by the respective step size, depending on whether it was bigger or smaller than the current satisfaction, but it won't go beneath -40 or above 80. At 0x006736AC there is a table that contains for every difficulty the step sizes for increments and decrements to the satisfaction:

Needs SettingIncrementDecrement
Low31
Normal21
High12
Unused11

At 0x006736A0 there is a table that contains for every difficulty the base satisfaction for every population type:

Needs SettingRichWealthyPoor
Low-7-12-20
Normal-13-18-27
High-20-25-32

Within update_citizen_satisfaction 6 situational modifiers are implemented:

SituationImpact
Siege-10
Pirate Attack-8
Plague-10
Blocked-6
Boycotted-4
Famine-10

At 0x00672938 there is a table that defines ware satisfaction weights for every population type:

WareRichWealthyPoor
Grain248
Meat544
Fish266
Beer266
Salt224
Honey320
Spices300
Wine520
Cloth540
Skins320
WhaleOil344
Timber346
IronGoods220
Leather224
Wool264
Pitch000
PigIron000
Hemp000
Pottery324
Bricks000
Sword000
Bow000
Crossbow000
Carbine000

The current satisfaction is calculated as follows:

def get_ware_satisfaction(ware_id, population_type):
    if wares[ware_id] >= 2 * weekly_consumption[ware_id]:
        return satisfaction_weights[population_type][ware_id]
    else:
        return (wares[ware_id] - weekly_consumption[ware_id])
            * satisfaction_weights[population_type][ware_id]
            // weekly_consumption[ware_id]

current_satisfaction = 2 * (
    base_satisfaction
    + situational_modifiers
    + unknown_modifiers # 9 total, 8 capped at 4
    + ware_satisfactions
)

Ware Prices

The buying price depends the ratio of ware price thresholds and the remaining amount.

Base Price

The ware_base_prices table at 0x00673A18 defines the following base prices:

WareBase PriceBase Price per Barrel/Bundle
Grain0.055000003110.0
Meat0.47855002957.1
Fish0.22005001440.1
Beer0.1739999934.8
Salt0.142528.45
Honey0.55000001110.0
Spices1.4280.0
Wine1.1220.0
Cloth1.034206.8
Skins3.3824999676.5
WhaleOil0.4124999982.5
Timber0.02750000255.0
IronGoods1.278255.6
Leather1.12224.0
Wool0.44000003880.0
Pitch0.27855.6
PigIron0.44000003880.0
Hemp0.22000001440.0
Pottery0.85499996171.0
Bricks0.03990000579.8

Thresholds

Buying Price

The get_buy_price function at 0x0052E430 takes a ware, town, and buy amount and returns the transaction price.

Formula

The price formula operates on 5 intervals, and the 4 price thresholds specify the bounds.

IntervalBounds
0[0; \(t_0\)]
1[\(t_0\); \(t_1\)]
2[\(t_1\); \(t_2\)]
3[\(t_2\); \(t_3\)]
4[\(t_3\); \(\infty\)]

Within every interval \(i\) the price \(p_i\) is defined as: \[ \begin{aligned} p_{i} &= p_{base} * w_{i} * f_{i} \end{aligned} \]

where \(w_i\) is the amount being bought from \(i\) and \(f\) is defined as: \[ \begin{aligned} f_4 &= 0.6\\ f_{i} &= m_i - v_i \underbrace{\frac{w_{relative\_stock} + w_{relative\_remain}}{2 * \text{interval_width}}}_{\in [0; 1]} \end{aligned} \]

where \(w_{relative\_stock}\) and \(w_{relative\_remain}\) are the stock's and remainder's offsets in the interval and \(m_i\) and \(v_i\) are defined as:

Interval\(m_i\)\(v_i\)
042.5
11.50.5
21.00.2
30.80.2

Example

Let's assume we buy pig iron from a town with the following thresholds:

ThresholdValue
t020000
t160000
t270000
t380000

If we buy one bundle (2000), the resulting prices at different stock levels would be: image

Auto Trader Discount

Auto traders (captains and administrators) get a discount depending on their trade skill. The discount is calculated as follows:

100 - (2 * (50 - xp // 43))

image

The calculation can be observed at 0x004D5347 for captains and at 0x004FF7E8 for administrators. Since a new discount is unlocked every 43 experience, auto traders reach the maximum discount at 215 experience, way before they reach level 5 at 250 experience.

The discount is applied after the transaction's amount has been determined.

Selling Price

The get_sell_price function at 0x0052E1D0 takes a pointer to a class containing the mapped trade difficulty setting, a ware, town, and sell amount and returns the transaction price.

Formula

The price formula operates on 5 intervals, and the 4 price thresholds specify the bounds.

IntervalBounds
0[0; \(t_0\)]
1[\(t_0\); \(t_1\)]
2[\(t_1\); \(t_2\)]
3[\(t_2\); \(t_3\)]
4[\(t_3\); \(\infty\)]

Within every interval \(i\) the price \(p_i\) is defined as: \[ \begin{aligned} p_{i} &= p_{base} * w_{i} * f_{i} \end{aligned} \]

where \(w_i\) is the amount being sold to \(i\) and \(f\) is defined as: \[ \begin{aligned} f_4 &= 0.5\\ f_{i} &= m_i - v_i \underbrace{\frac{w_{relative\_stock} + w_{relative\_new\_stock}}{2 * \text{interval_width}}}_{\in [0; 1]}\\ f_0 &= d_{trade\_difficulty} - (v_i - d_{trade\_difficulty}) \underbrace{\frac{w_{relative\_stock} + w_{relative\_new\_stock}}{2 * \text{interval_width}}}_{\in [0; 1]} \end{aligned} \]

where \(w_{relative\_stock}\) and \(w_{relative\_new\_stock}\) are the stock's and new stock's offsets in the interval and \(m_i\) and \(v_i\) are defined as:

Bracket\(m_i\)\(v_i\)
0NaN1.4
11.40.4
21.00.3
30.70.2

and \(d_{trade\_difficulty}\) is defined as:

DifficultyValue
0 (low)2.2
1 (normal)2.0
2 (high)1.8

Example

Let's assume we sell pig iron to a town with the following thresholds:

ThresholdValue
t020000
t160000
t270000
t380000

If we sell one bundle (2000), the resulting prices at different stock levels would be: image

Shipyard

Shipbuilding

The handle_build_ship function is at 0x0052A360.

Build Capabilities

A shipyard's current quality level of each ship type is stored in the town's current ship quality level array, indexed by the ship type. The u8 values range from 0 to 3.

HP and Capacity

A ship's HP and capacity depend on the respective quality level. At 0x00673838 there is a table that holds the structure base values for every ship type and quality level:

TypeQuality Level 0Quality Level 1Quality Level 2Quality Level 3
Snaikka15192325
Craier28313435
Cog45485255
Hulk55596570

To the structure base value an unknown value is added, which appears to be always zero. The resulting structure value is capped at the value of QL 3.

HP and Capacity scale linearly with the structure value:

capacity = 2000 * structure_base_value
health = 2800 * structure_base_value

For a QL 3 hulk, this yields the expected capacity of 140.000 (700 barrels).

Resources and Price

The calculate_ship_build_cost function is at 0x0052B2C0. The shipyard charges a utilization markup that increases when the shipyard is in use, and decreases if it is not. At 0x0066DEB0 there is a table that contains the requirements of every ship type and quality level, capped at QL 2 (QL 3 does not increase cost):

TypeQLTimberClothHempPitchIron GoodsUnknownBase PriceUnknown
Snaikka0733320177,65011,414
Snaikka1933320208,20012,074
Snaikka21133320248,80012,784
Craier012555302918,26024,450
Craier114555303218,72025,010
Craier216555303419,89026,290
Cog018344404616,56022,296
Cog120344405016,50022,346
Cog222344405317,49023,446
Hulk03016168505822,96834,442
Hulk13316168506423,04034,579
Hulk23616168506924,84036,664

Wares are consumed as listed in the table. The ship price is calculated as follows:

def structure_markup(structure):
    if structure < 20:
        return 900
    if structure < 30:
        return 840
    if structure < 40:
        return 780
    if structure < 50:
        return 720
    if structure < 60:
        return 660
    else:
        return 600

price = base_price
    + structure_markup(structure_base_value)
    + utilization_markup
    + resource_prices

Repairs

Upgrade Levels

Lanterns and Materials

The displayed lanterns and materials correspond with the amount of employees the shipyard has:

EmployeesShipyard
0-15
16-21
22-27
28-33
34-39
40

Taxes

Tavern

Sailors

The "Sailors" page is visible when the merchant's sailor pool in the town is not empty. If the sailor pool exceeds 50, the UI caps the available amount to a random number.

Money Lender

Interest Rates

Pages

Grant Loan Page

During every preparation tick of the window the town's loan applications are converted to grant loan operations and stored in the window.

Grant Loan Confirm Page

When the confirm button is clicked and if the merchant has enough money, the selected grant loan operation is enqueued.

The interest rate is calculated as follows: \[ f_{setting} * (\sqrt{\frac{1}{\text{weeks} * \text{amount}}} * 300 + 1.2) * 0.1 \]

where \(f_{setting}\) is defined as:

SettingFactor
Very Low8
Low9
Normal10
High11
Very High12

Interest is applied weekly, the repayment sum is capped at 65000.

A loan's success is determined while on the grant loan page and when clicking the interest change buttons on the grant loan confirm page. The following table defines the safe repayment sums for each debtor's rank:

RankSafe Repayment Sum
Shopkeeper5000
Trader10000
Merchant15000
Travelling Merchant20000
Councillor25000
Patrician30000

If the repayment sum is bigger than the safe repayment sum, the following computation dedices whether the loan will default:

rand() & 0x3ff < 75 * ((repayment_sum - safe_repayment_sum + 1250) / 1250)

Bath House

Bribes

Every town with a bathhouse has 4 councillors which can be bribed.

Attendance

Between 0 and 4 bribable councillors attend the bath house. The bath house panel has an array of attendance selection timestamps at offset 0xd4, whose entries denote when the attendance in a given town will be updated. These timestamps are updated to now + 0x100 when

  • the bath house window is closed.
  • fast forward is activated.
  • the options screen is opened.
  • any window is opened after the bath house window has just been closed.

The pool of attending councillors is updated when the bath house window is opened and the selection timestamp is smaller than now. Therefore, continuously opening an individual bath house locks the current attendance.

Attendance is decided as follows:

def will_attend(rand: int):
    return rand % 100 < 7

so each councillor has a 7% chance to attend.

Price Formula

The town_calculate_expected_bribe function at 0x00529E20 determines the amount of money the merchant needs to offer for the bribe to be successful.

It defines the following bribe base factors for each rank:

RankBribe Base Factor
Shopkeeper0
Trader1
Merchant2
Travelling Merchant3
Councillor5
Patrician7
Mayor10
Alderman15

The bribe result is calculated as follows:

BRIBE_BASE_FACTORS = [0, 1, 2, 3, 5, 7, 10, 15]

def calculate_expected_bribe(rank: int, rand: int, already_bribed: bool):
    price = 500 * (rand % 11 + 4 * BRIBE_BASE_FACTORS[rank] + 16)
    if already_bribed:
        price *= 2
    return price

def calculate_bribe_result(amount: int, rank: int, rand: int, already_bribed: bool):
    price = calculcate_expected_bribe(rank, rand, already_bribed)
    if amount < price:
        return BribeResult.OK
    elif amount >= price * 1.5:
        return BribeResult.GOOD
    else:
        return BribeResult.FAILED

For unbribed councillors, this produces the following minimum and maximum bribes:

RankMinMax
Shopkeeper800013000
Trader1000015000
Merchant1200017000
Travelling Merchant1400019000
Councillor1800023000
Patrician2200027000
Mayor2800033000
Alderman3800043000

Result

The result can be identified by the councillor's response:

ResultResponse
Ok"Aha, a bribe eh! But all right, I'll take your gold. We'll see what I can do for you at the appointed time."
Good"Oh, that's a very enticing sum. You can be certain of my loyalty."
Failed"What am I supposed to do with this pittance? You ought to realise yourself, that a man in my position expects a little more from someone of your standing."

Both Ok and Good enqueue a Bath House Bribe Success operation, while Failed enqueues a Bath House Bribe Failure operation. The failure operation is bugged, as discussed in the Known Bugs chapter.

Status

The current status of a councillor can be inferred by his lines:

StatusResponse
Bribed by merchant"So we meet again, John Doe. I can very well remember how pleasant our last meeting was."
Bribed by other merchant"Ah! You're here as well, John Doe? I have only very recently spoken to one of your competitors."
Not bribed by anyone, annoyed"Are you there again?! Let me have my bath in peace, please."

The annoyance of a councillor with a given index is bugged and saved globally, as discussed in the Known Bugs chapter.

Limits

The success operation has a check which probably should prevent a merchant from bribing more than 2 councillors in one town. However, this check is bugged, as discussed in the Known Bugs chapter.

Sieges

Initialization

Sieges are started by operation type 0x8E, the handle_start_siege function is at 0x00633AF0.

Army Size

Siege TypeSwordsBowsCrossbowsCarbinesTrebuchets
026213199055
226213108755
32620998755

The attacking squads are then enforced to be below or equal to the following values defined at 0x0067B604:

SquadLimit
Swords40
Bows22
Crossbows18
Carbines15
Trebuchets6

Gate and Ram

Both the gate and the battering ram have 0x10000 (65536) HP.

Approach

Merchants

00000000 struct merchant // sizeof=0x650
00000000 {                                       // XREF: merchant_wrapper/r
00000000     int field_0_money __tabform(NODUPS);
00000004     int field_4;
00000008     __int16 field_8;
0000000A     __int16 field_A;
0000000C     unsigned __int16 field_C_first_office_index;
0000000E     unsigned __int16 field_E_first_ship_id;
00000010     int field_10;
00000014     char field_14;
00000015     unsigned __int8 field_15;
00000016     char field_16;
00000017     char field_17;
00000018     char field_18;
00000019     unsigned __int8 field_19_hometown_index;
0000001A     char field_1A_is_male;
0000001B     char field_1B;
0000001C     __int16 field_1C;
0000001E     char field_1E_decreasing_thing;
0000001F     unsigned __int8 field_1F_sailor_reputation;
00000020     __int16 field_20;
00000022     __int16 field_22;
00000024     char field_24;
00000025     char field_25;
00000026     char field_26;
00000027     char field_27;
00000028     char field_28;
00000029     char field_29;
0000002A     char field_2A;
0000002B     char field_2B;
0000002C     int field_2C;
00000030     char field_30;
00000031     unsigned __int8 field_31_spouse_hometown_index;
00000032     unsigned __int8 field_32_spouse_reputation_bonus;
00000033     char field_33;
00000034     int field_34;
00000038     char field_38;
00000039     unsigned __int8 field_39;
0000003A     unsigned __int8 field_3A;
0000003B     char field_3B;
0000003C     int field_3C;
00000040     int field_40;
00000044     int field_44;
00000048     int field_48;
0000004C     int field_4C;
00000050     int field_50;
00000054     int field_54;
00000058     int field_58;
0000005C     int field_5C;
00000060     int field_60;
00000064     int field_64;
00000068     int field_68;
0000006C     int field_6C;
00000070     int field_70;
00000074     int field_74;
00000078     int field_78;
0000007C     int field_7C;
00000080     int field_80;
00000084     int field_84;
00000088     int field_88;
0000008C     int field_8C;
00000090     int field_90;
00000094     int field_94;
00000098     int field_98;
0000009C     int field_9C;
000000A0     int field_A0;
000000A4     int field_A4;
000000A8     int field_A8;
000000AC     int field_AC;
000000B0     int field_B0;
000000B4     int field_B4;
000000B8     int field_B8;
000000BC     int field_BC;
000000C0     int field_C0;
000000C4     int field_C4;
000000C8     int field_C8;
000000CC     int field_CC;
000000D0     int field_D0;
000000D4     int field_D4;
000000D8     int field_D8;
000000DC     int field_DC;
000000E0     int field_E0;
000000E4     char *field_E4_family_name;
000000E8     char *field_E8_name;
000000EC     int field_EC;
000000F0     unsigned __int8 field_F0_sailor_pools[40];
00000118     int field_118;
0000011C     class25 field_11C_local_reputation_components[40];
000002FC     float field_2FC_latest_reputations[40];
0000039C     unsigned __int8 field_39C_ranks[40];
000003C4     float field_3C4_old_reputations[40];
00000464     float field_464_base_rep_factor;
00000468     int field_468;
0000046C     int field_46C_company_value;
00000470     int field_470_company_capacity;
00000474     int field_474;
00000478     unsigned __int16 field_478;
0000047A     __int16 field_47A;
0000047C     int field_47C;
00000480     int field_480;
00000484     int field_484;
00000488     int field_488;
0000048C     int field_48C;
00000490     int field_490;
00000494     int field_494;
00000498     int field_498;
0000049C     int field_49C;
000004A0     int field_4A0;
000004A4     int field_4A4;
000004A8     int field_4A8;
000004AC     int field_4AC;
000004B0     int field_4B0;
000004B4     int field_4B4;
000004B8     int field_4B8_recent_widthdrawals;
000004BC     int field_4BC_recent_donations;
000004C0     int field_4C0;
000004C4     int field_4C4;
000004C8     int field_4C8;
000004CC     int field_4CC;
000004D0     int field_4D0;
000004D4     int field_4D4;
000004D8     int field_4D8;
000004DC     int field_4DC;
000004E0     int field_4E0;
000004E4     int field_4E4;
000004E8     int field_4E8;
000004EC     int field_4EC;
000004F0     int field_4F0;
000004F4     int field_4F4;
000004F8     int field_4F8;
000004FC     int field_4FC;
00000500     int field_500;
00000504     int field_504;
00000508     int field_508;
0000050C     int field_50C;
00000510     int field_510;
00000514     int field_514;
00000518     int field_518;
0000051C     int field_51C;
00000520     int field_520;
00000524     int field_524;
00000528     int field_528;
0000052C     int field_52C;
00000530     int field_530;
00000534     int field_534;
00000538     int field_538;
0000053C     int field_53C;
00000540     int field_540;
00000544     int field_544;
00000548     int field_548;
0000054C     int field_54C;
00000550     int field_550;
00000554     int field_554;
00000558     int field_558;
0000055C     int field_55C;
00000560     int field_560;
00000564     int field_564;
00000568     int field_568;
0000056C     int field_56C;
00000570     int field_570;
00000574     int field_574;
00000578     int field_578;
0000057C     int field_57C;
00000580     int field_580;
00000584     int field_584;
00000588     int field_588;
0000058C     int field_58C;
00000590     int field_590;
00000594     int field_594;
00000598     int field_598;
0000059C     int field_59C;
000005A0     int field_5A0;
000005A4     int field_5A4;
000005A8     int field_5A8_last_autotrade_prices[20];
000005F8     int field_5F8;
000005FC     int field_5FC;
00000600     int field_600;
00000604     int field_604;
00000608     int field_608;
0000060C     int field_60C;
00000610     int field_610;
00000614     int field_614;
00000618     int field_618;
0000061C     int field_61C;
00000620     int field_620;
00000624     int field_624;
00000628     int field_628;
0000062C     int field_62C;
00000630     int field_630;
00000634     int field_634;
00000638     int field_638;
0000063C     int field_63C;
00000640     int field_640;
00000644     int field_644;
00000648     int field_648;
0000064C     int field_64C;
00000650 };

Ranks

Merchants may rank up in their hometown on the first day of every month. The office "Personal" page gives rough hints whether more wealth or more reputation is needed to get to the next level, until the following wealth and reputation requirements are met:

Minimum ReputationRank
5Trader
7.5Merchant
10Travelling Merchant
15Councillor
25Patrician
Minimum Company ValueRank
100,000Trader
200,000Merchant
300,000Travelling Merchant
500,000Councillor
900,000Patrician

However, to actually reach the next rank, the following reputation values must be reached:

Minimum ReputationRank
7Trader
12Merchant
20Travelling Merchant
40Councillor
60Patrician

Building Permits

Once you reach the Trader rank in a town, it'll grant you the building permit.

Company Value

Reputation

The function update_merchant_reputation_and_value at 0x004F7BB0 calculates the company value and reputation of a given merchant.

Reputation is calculated as follows:

reputation = min(0,
    outrigger_rep
    + tenants_rep
    + employment_rep
    + capacity_rep
    + company_value_rep
    + spouse_rep # only in hometown
    + local_social_rep
    + local_trading_rep
    + local_buildings_rep
)

Outrigger

If the merchant is providing the town's outrigger, outrigger_rep is set to 1.

Tenants

The reputation achieved through tenants is calculated as follows:

def get_tenant_reputation(population_type):
    rent_factor = rent_reputation_factors[rents[population_type]]
    return tenants[population_type] * rent_factor * 0.003

tenants_rep = get_tenant_reputation(rich)
    + get_tenant_reputation(wealthy)
    + get_tenant_reputation(poor)

The rent_reputation_factors table is at 0x00672DF0:

RentReputation Factor
None1.0
Low0.62
Normal0.42
High0.23
Very High0.0

Employment

TODO

Capacity

The merchant's cargo capacity reputation is calculated as follows:

capacity_rep = min(5.0, capacity / 100_000.0)

Company Value

The merchant's company value reputation is calculated as follows:

company_value_rep = min(5.0, company_value / 100_000.0)

Spouse

Every spouse has a fixed reputation bonus. TODO list options

Social

local_social_rep is the merchant's social reputation in the town. It is changed through many actions, and degrades over time.

Recurring Constants

The following values appear in multiple calculations, and appear to have fixed values.

NameValueLocation
base_rep_factor1.0Merchant
church_factor0.0GameWorld

Loans

When granting a loan, local_social_rep is increased as follows:

local_social_rep += amount / 80_000.0 * interest_factor * base_rep_factor

interest_factor depends on the chosen interest rate:

InterestFactor
Very Low4.0
Low3.0
Normal2.0
High1.0
Very High0.0

Church Donations

Donations to the church influence the local social reputation as follows:

money_capacity = 12_000 * (church_factor + 1)
effective_amount = min(amount, church_money_capacity - church_money)
local_social_rep += effective_amount
    * 0.0003
    / (church_factor + 1)
    * base_rep_factor

Church Extension Donation

Donations to the church extension influence the local social reputation as follows:

effective_amount = min(amount, church_extension_cost - church_extension_money)
local_social_rep += effective_amount
    * 0.0003
    / (church_factor + 1)
    * base_rep_factor

Feeding the Poor

The handle_feeding_the_poor function is at 0x004FE557. Food donations influence the local social reputation as follows:

for amount, ware_id in donation:
    local_social_rep += get_sell_price(ware_id, town_index, amount)
        * 0.0003
        / (church_factor + 1)
        * base_rep_factor

TODO: minimum wares threshold?

Town Coffers Access

Celebrations

Crime

The handle_crime_social_reputation_impact function at 0x004F8F10 reduces the merchant's social reputation according to the following table:

Crime TypeImpact
0x2-1.0
0x0-2.0
0x9-2.0
0xa-2.0
0xd-2.0
0x1-4.0
0xb-4.0
0x3-6.0
0x4-6.0
0xc-6.0
0xe-6.0
0xf-8.0

Bath House Bribes

A successful bribe increases the social reputation by min(amount * 0.000099999997, 3.0). Consequently, successfully bribing with 30000 or more gives the maximum social reputation gain.

A failed bribe decreases the social reputation by 2.0.

Degradation

Trading

TODO

Buildings

TODO

Sailor Reputation and Sailor Pools

Sailor Reputation

A merchant's sailor reputation is stored at 0x1f. It influences the growth or decline of the sailor pools. It ranges from 0 to 20 (inclusive). During the merchant's tick the sailor reputation is increased by 1, up to a maximum of 20. Dismissing a captain sets the sailor reputation to 0.

Sailor Pools

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.

Ships

Ships Struct

The static ships struct is at 0x006DD7A0. The following fields have been identified:

struct __declspec(align(4)) ships
{
  captain *field_0_captains_ptr __tabform(NODUPS);
  ship *field_4_ships;
  convoy *field_8_convoys;
  _DWORD field_C[5];
  class49 field_20_class49_array[16];
  __int16 field_E0;
  unsigned __int16 field_E2_unused_ship_index;
  int field_E4;
  __int16 field_E8_unknown_ship_id;
  __int16 field_EA_ship_index_1;
  signed __int16 field_EC;
  signed __int16 field_EE;
  __int16 field_F0;
  unsigned __int16 field_F2_captains_size;
  unsigned __int16 field_F4_ships_size;
  unsigned __int16 field_F6_convoys_size;
  signed __int16 field_F8;
  signed __int16 field_FA;
  unsigned __int16 field_FC_ship_engagement_range_squared;
  signed __int16 field_FE;
  int field_100;
  int field_104;
};

Ship Struct

The pointer to the ships array is stored in the static ships struct at offset 0x04, and the length of that array at offset 0xf4.

The following fields have been identified:

00000000 struct ship // sizeof=0x180
00000000 {
00000000     unsigned __int8 field_0_merchant_index __tabform(NODUPS);
00000001     char field_1;
00000002     unsigned __int16 field_2;
00000004     unsigned __int16 field_4_next_ship_of_merchant;
00000006     __int16 field_6_next_ship_index_in_convoy;
00000008     unsigned __int16 field_8_convoy_index;
0000000A     unsigned __int16 field_A_some_ship_id;
0000000C     unsigned __int16 field_C_next_spotted_candidate;
0000000E     unsigned __int8 field_E_ship_type;
0000000F     unsigned __int8 field_F_maybe_upgrade_level;
00000010     int field_10_capacity;
00000014     int field_14_max_health;
00000018     int field_18_current_health;
0000001C     int field_1C_x;
00000020     int field_20_y;
00000024     ship_route *field_24_route_ptr;
00000028     int field_28_x_delta;
0000002C     int field_2C_y_delta;
00000030     int field_30;
00000034     signed __int16 field_34;
00000036     char field_36_is_on_route;
00000037     unsigned __int8 field_37_unknown_town_index;
00000038     unsigned __int8 field_38_dest_town_index;
00000039     unsigned __int8 field_39_last_town_index;
0000003A     unsigned __int16 field_3A_target_ship_index;
0000003C     char field_3C;
0000003D     char field_3D;
0000003E     signed __int16 field_3E_maintenance;
00000040     unsigned __int16 field_40;
00000042     unsigned __int16 field_42_captain_index;
00000044     int field_44_timestamp2;
00000048     int field_48_maybe_calculated_arrival_timestamp;
0000004C     int field_4C_timestamp;
00000050     unsigned __int16 field_50_spotted_ships[2];
00000054     unsigned int field_54_wares[24];
000000B4     float field_B4_avg_prices[24];
00000114     int field_114_payload_buy_sum;
00000118     int field_118_maybe_used_capacity;
0000011C     int field_11C_maybe_arty_weight;
00000120     int field_120_arty_stuff;
00000124     int field_124;
00000128     int field_128;
0000012C     int field_12C;
00000130     __int16 field_130;
00000132     unsigned __int16 field_132;
00000134     __int16 field_134_status;
00000136     char field_136;
00000137     char field_137;
00000138     unsigned __int16 field_138_docking_counter;
0000013A     char field_13A;
0000013B     char field_13B;
0000013C     char field_13C_artillery[24];
00000154     int field_154;
00000158     int field_158;
0000015C     char field_15C_is_pirate;
0000015D     unsigned __int8 field_15D;
0000015E     __int16 field_15E;
00000160     char field_160_ship_name[32];
00000180 };

Sea Battles

Sea Battle Struct

PRNG

Every battle has its own PRNG state at offset 0xadc:

signed __int32 __thiscall get_battle_rand(sea_battle *this)
{
  signed __int32 v1; // edx
  unsigned __int32 v2; // eax
  signed __int32 result; // eax

  v1 = 1153374643 * this->field_ADC_prng_state;
  v2 = -1576685469 - v1;
  this->field_ADC_prng_state = -1576685469 - v1;
  if ( ((99 - (_BYTE)v1) & 1) != 0 )
    result = (v2 >> 1) | 0x80000000;            // shift in a leftmost 1
  else
    result = v2 >> 1;                           // shift in a leftmost 0
  this->field_ADC_prng_state = result;
  return result;
}

Wind

The wind's angle is stored in an u8 at offset 0x670.

ValueDirection
0x00North
0x40East
0x80South
0xc0West

The update_sea_battle_wind_direction function at 0x006113c9 changes the direction by adding or subtracting 8 from the current value, and thus changing the wind direction by 11.25°.

Hitboxes

Hitboxes are defined in a static ship hitbox table at 0x0067AB30:

ShipPoint 0Point 1Point 2Point 3Point 4
Snaikka(-19, -31)(0, -55)(19, -31)(19, 47)(-19, 47)
Crayer(-21, -36)(0, -66)(21, -36)(19, 55)(-19, 55)
Cog(-25, -14)(0, -54)(25, -14)(19, 67)(-19, 67)
Hulk(-22, -25)(0, -67)(22, -25)(19, 81)(-19, 81)

It creates the following shapes of ships facing north:

Projectile Collisions

The get_sea_battle_projectile_impact_direction function at 0x0060A73C determines whether and where a ship is hit by a projectile.

First it rotates the ship's hitbox coordinates (in its own coordinate space, i.e. around its origin). Since the ship's direction rotates clockwise and the Y-axis is inverted, the formula for counter-clockwise rotation can be used: \[x_{hitbox}' = x_{hitbox} * cos(a) - y_{hitbox} * sin(a)\] \[y_{hitbox}' = x_{hitbox} * sin(a) + y_{hitbox} * cos(a)\]

Then it adds the rotated hitbox coordinates to the ship coordinates, and transforms them into a projectile-centric space: \[x_{hitbox}'' = x_{ship} + x_{hitbox}'' - x_{projectile}\] \[y_{hitbox}'' = y_{ship} + y_{hitbox}'' - y_{projectile}\]

Finally it calculates the intersection (if any) of all ship hitbox lines and the line from the projectile's current position to its future position at the next tick, and decides whether the ship was hit on the port, starboard or a random side.

Impact Location

The get_sea_battle_projectile_impact_direction function returns the calculated impact location:

enum impact_location : __int8
{
    impact_location_none   = 0x0,
    impact_location_left   = 0x1,
    impact_location_right  = 0x2,
    impact_location_random = 0x3,
};

Should a projectile intersect hitbox lines with different associated impact locations, the result is set to random. The jump table at 0x0060AD69 maps intersections of the projectile's path with the ship's hitbox lines to impact locations:

Intersected LineLocation
Point 4 to Point 0Left
Point 3 to Point 4Left
Point 0 to Point 1Right
Point 1 to Point 2Right
Point 2 to Point 3Random

This defines the following line-to-location mapping:

The mapping is incorrect, as discussed in the Known Bugs chapter.

Projectiles

Ship Artillery Projectiles

The sea_battle_local_map_ship_fire_volley function at 0x0061E8EF loops over the artillery slots of the selected side. It restricts the target coordinates to be in the cone of fire, and calls sea_battle_local_map_ship_fire_shot at 0x006214CB for every artillery piece. The code for all plots can be found in the source code of this book.

Raw Damage

The raw damage of all projectiles of a volley is controlled through 3 tables in the sea_battle_local_map_ship_fire_volley function: Damage Component 1 at 0x00672CC0, Damage Component 2 at 0x00672CD0, and Damage Reduction at 0x00672CE4. The vanilla values are shown in the following table:

Artillery TypeDamage 1Damage 2Damage Reduction
Small Catapult3260480
Small Ballista3280160
Large Catapult7760480
Large Ballista7780160
Bombard9690120
Cannon5890120

The formula of the raw damage is as follows:

def calc_raw_damage(distance: int, artillery_type: int):
    return max(0, distance \
            * damage1[artillery_type] \
            * damage2[artillery_type] \
            // (-6 * reduction[artillery_type]) \
        + \
            damage1[artillery_type] \
            * damage2[artillery_type])

The following image shows the plot of raw damage and distance, with the damage values of double slot weapons adjusted by 0.5. image

While every individual projectile is subject to minor source and destination adjustments, the distance raw damage calculation is done once for the entire volley.

Scaling

The raw damage is scaled linearly in the sea_battle_local_map_ship_fire_shot function at 0x006214CB:

def calc_scaled_damage(raw_damage: int):
    return 2800 * (raw_damage // 64) // 100

The precision loss caused by the division by 64 has a slight effect on the granularity: image

While the function is called for every individual projectile, this calculation will yield the same values for every projectile of a volley.

Captain

Then the function uses the captain's combat experience (a value between 0 for a combat level 0 and 250 for a combat level 5 captain) to increase the damage:

def apply_captain_factor(scaled_damage: int, combat_experience: int):
    if not combat_experience:
        return scaled_damage
    else:
        return scaled_damage * (6 * combat_experience // 17 + 100) // 100

This factor is roughly (ignoring precision loss through divisions) equivalent to \(\frac{3 * combat\_experience}{850} + 1\) or \(0.17647058823 * combat\_level + 1\). The following figure highlights the impact of a captain on a projectile's damage: image

While the function is called for every individual projectile, this calculation will yield the same values for every projectile of a volley.

Difficulty and Maintenance

Then the function considers sea battle difficulty setting and the ship's current maintenance value to affect the damage as follows:

def apply_difficulty_and_maintenance(
    damage: int, difficulty: int, ship_maintenance: int, is_ai: bool
):
    f = min(4, max(ship_maintenance >> 8, 0))
    if not is_ai:
        match difficulty:
            case 0:  # Easy
                f += 2
            case 2:  # Hard
                f -= 2
    if f > 0:
        return damage + damage * (f - 2) // 20
    else:
        return damage + damage * (f - 1) // 20

Since f cannot exceed 6, the bonus damage from difficulty and maintenance will not exceed \(\frac{1}{5} * damage \).

While the function is called for every individual projectile, this calculation will yield the same values for every projectile of a volley.

Normal Distribution and Minimum

Then the function applies a factor with a discrete uniform (assuming the sea battle's PRNG works as intended) distribution in the discrete (up to the second decimal point) interval from 0.85 to 1.15, and enforces a minimum damage of 1:

# Discrete distribution
damage = damage * (battle_rand() % 31 + 85) // 100

# Minimum
damage = max(damage, 1)

Final Scaling

Finally the init_sea_battle_projectile function at 0x00602A90 multiplies the damage by 1.5, and stores the final damage in the projectile.

Range

A projectile's range is calculated in battle_projectile_calcs at 0x0061E8EF. It depends only the ship's relative angle to the wind at the time of firing, and disregards the relative angle of the projectile to the wind. The formula is defined as:

def calc_max_distance(ship_direction: int, wind_direction: int):
    relative_angle = wind_direction - ship_direction
    if relative_angle >= 0x80:
        bonus_range_factor = 0xc0 - relative_angle
    else:
        bonus_range_factor = relative_angle - 0x40
    
    return bonus_range_factor * 4320 // 3200 + 480

It causes the following range spread:

The following screenshots showcase the impact of 0°, 90°, 180° and 270°:

Impact

Once the get_sea_battle_projectile_impact_direction at 0x0060A73C has detected an impact, the projectile's damage is applied, and the object is freed. Projectiles can deal damage to a ship's HP, sailors and artillery.

HP Damage

The projectile's damage value is subtracted from its hitpoints, and the ship is killed if they drop to or below 0.

Sailor Damage

Damage to sailors is calculated as follows:

def calc_killed_sailors(
    sailors: int,
    damage: int,
    rng: int,
    pending_sailor_damage: int,
    ship_max_hp: int
) -> Tuple[int, int]:
    sailor_damage_rng = (rng & 0x400) + 3072 # 3072 or 4096
    scaled_sailor_damage = (sailors + 1) * sailor_damage_rng
    final_sailor_damage = pending_sailor_damage + scaled_sailor_damage / (ship_max_hp // 16)
    killed_sailors = final_sailor_damage >> 16
    new_pending_sailor_damage = final_sailor_damage & 0xffff
    return killed_sailors, new_pending_sailor_damage

Ship Artillery Slot Damage

Reefs

The tick_sea_battle_object_pair function at 0x00608850 applies the following damage to a ship on a reef:

def calc_reef_damage(ship_speed: int):
    return ship_speed // 1024

Reefs don't kill sailors or artillery pieces.

Auto Traders

Both captains and administrators are represented by the same struct. The following fields have been identified:

struct auto_trader
{
  unsigned __int16 field_0_next_auto_trader_index;
  signed __int16 field_2;
  int field_4_timestamp;
  unsigned __int8 field_8;
  unsigned __int8 field_9_navigation_skill;
  unsigned __int8 field_A_trade_skill;
  unsigned __int8 field_B_combat_skill;
  __int16 field_C_daily_wage;
  char field_E;
  unsigned __int8 field_F_merchant_index;
};

Letters

Charge

Charge letters are sent by criminal investigation scheduled tasks.

Text

A charge letter always starts with Today, you have been charged for the following reason. The following text depends on the crime type:

Crime TypeText
5You have been seen behaving indecently.
6You are accused of heresy.
7You have stated that the world is round.
8You stand accused of taking part in a punishable offence, undermining the good of the Hanseatic League.

Finally the suffix We will thoroughly examine this charge and inform you of the outcome of investigations as soon as possible is appended to every accusation letter.

Indictment

Indictment letters are sent by criminal investigation scheduled tasks.

Text

The text depends on the crime type:

Crime TypeContent
0Severe charges have been made against you. The court will be provided with evidence that you met in %s in the town of %s with lawless elements, and obviously made criminal plans with them.
1You are accused of recently committing a breach of the rules of the Hanseatic League by breaking the Boycott of %s. Several traders observed you in this law-breaking deed.
2Several witnesses have sworn that they recognised your ship %s during the recent pirates attack, and that it was flying the pirate's flag.
3A worthless scoundrel was taken into custody in %s today, as he was breaking into the trading office of %s %s in pursuit of his criminal activities. Under careful questioning, he admitted the deed, and said, that you had paid him, to carry out this treacherous burglary.
4A capable trader recently succeeded in boarding a cowardly pirate ship and arrested the captain. At first, he did not admit anything, but couldn't hold out against our penetrating interrogation, and admitted it was your idea and with your support that he did this.
8You stand accused of taking part in a punishable offence, undermining the good of the Hanseatic League.
9Several witnesses to a recent pirate attack, have sworn that they recognised your ship, %s with a pirate flag run up the mast and firing on Hanseatic League ships.
10Several witnesses to a recent pirate attack, have sworn that they recognised your ship %s with a pirate flag run up the mast and plundering Hanseatic League ships.
11Several witnesses to a recent pirate attack, have sworn that they recognised your ship %s with a pirate flag run up the mast and sinking Hanseatic League ships.
12Several witnesses to a recent pirate attack, have sworn that they recognised your ship %s with a pirate flag run up the mast and capturing Hanseatic Leagueague ships.
13Several witnesses to a recent pirate attack on %s have sworn that they recognised your ship %s displaying a pirate flag run up the mast.
14Several witnesses to a recent pirate attack on %s have sworn that they recognised your ship %s displaying a pirate flag run up the mast and firing on the town's defences.
15Several witnesses to a recent pirate attack on %s have sworn that they recognised your ship %s displaying a pirate flag run up the mast, breaching the town's defences and plundering the town's coffers.

The suffix We will investigate this atrocious charge. Expect a message in the near future, which will inform you of the results of our investigations is appended to every indictment letter.

Multiplayer

Threads

Game Thread

Network Thread

Lobby

Operation Synchronization

The following flowchart illustrates the logical path of an operation through the different P3s and data structures:

The ingress queues are skipped, if the current operations have enough free capacity. Green denotes the sending client, blue denotes the hosting client, and orange denotes a receiving client. The green client would receive the operation too, since the operation is sent to all connected clients.

All network operations are run by the network thread. The shared datastructures are guarded by a broken spinlock implementation, as discussed in the Known Bugs chapter.

The host chooses the order of operations to guarantee consistency between all clients.

Modloader

The Patrician3 Modloader loads binary modifications from ./mods.

Usage

p3_modloader.dll

The modloader's DllMain hooks the game's WinMain, and uses LoadLibraryW to load every .dll file in ./mods, and invokes their start functions. This function should return 0 on success, and an error code otherwise.

After all files in ./mods have been processed, it will call the original WinMain.

Patrician3_modloader.exe

Patrician3_modloader.exe is a minimally patched version of the vanilla executable. Its patch

  • adds a new mod section to the PE32 header
  • copies the Import Directory Table into .mod and appends an entry for p3_modloader.dll
  • amends the Import Data Directory Entry to use the new Import Directory Table from .mod

so the game will load p3_modloader.dll before executing start.

$ diff  <(xxd Patrician3.exe) <(xxd Patrician3_modloader.exe)
19c19
< 00000120: 5045 0000 4c01 0400 3c4d 543f 0000 0000  PE..L...<MT?....
---
> 00000120: 5045 0000 4c01 0500 3c4d 543f 0000 0000  PE..L...<MT?....
24c24
< 00000170: 0090 2f00 0010 0000 6d9f 2d00 0200 0000  ../.....m.-.....
---
> 00000170: 0090 3000 0010 0000 6d9f 2d00 0200 0000  ..0.....m.-.....
27c27
< 000001a0: c0ed 2800 2c01 0000 0020 2f00 d466 0000  ..(.,.... /..f..
---
> 000001a0: 0090 2f00 5301 0000 0020 2f00 d466 0000  ../.S.... /..f..
36c36
< 00000230: 0000 0000 0000 0000 0000 0000 2000 0060  ............ ..`
---
> 00000230: 0100 0000 0200 0000 0300 0400 2000 0060  ............ ..`
38,39c38,39
< 00000250: 0080 0200 00a0 2600 0000 0000 0000 0000  ......&.........
< 00000260: 0000 0000 4000 0040 2e64 6174 6100 0000  ....@..@.data...
---
> 00000250: 0080 0200 00a0 2600 0100 0000 0200 0000  ......&.........
> 00000260: 0300 0400 4000 0040 2e64 6174 6100 0000  ....@..@.data...
41c41
< 00000280: 0000 0000 0000 0000 0000 0000 4000 00c0  ............@...
---
> 00000280: 0100 0000 0200 0000 0300 0400 4000 00c0  ............@...
43,46c43,46
< 000002a0: 0070 0000 00c0 2c00 0000 0000 0000 0000  .p....,.........
< 000002b0: 0000 0000 4000 0040 0000 0000 0000 0000  ....@..@........
< 000002c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
< 000002d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
---
> 000002a0: 0070 0000 00c0 2c00 0100 0000 0200 0000  .p....,.........
> 000002b0: 0300 0400 4000 0040 2e6d 6f64 0000 0000  ....@..@.mod....
> 000002c0: 0000 0100 0090 2f00 0010 0000 0030 2d00  ....../......0-.
> 000002d0: 0100 0000 0200 0000 0300 0400 4000 00c0  ............@...
185088a185089,185344
> 002d3000: d4f1 2800 0000 0000 0000 0000 42f5 2800  ..(.........B.(.
> 002d3010: e8a2 2600 dcf1 2800 0000 0000 0000 0000  ..&...(.........
> 002d3020: 8cf8 2800 f0a2 2600 80f4 2800 0000 0000  ..(...&...(.....
> 002d3030: 0000 0000 02fd 2800 94a5 2600 3cf4 2800  ......(...&.<.(.
> 002d3040: 0000 0000 0000 0000 e0fd 2800 50a5 2600  ..........(.P.&.
> 002d3050: 04ef 2800 0000 0000 0000 0000 0eff 2800  ..(...........(.
> 002d3060: 18a0 2600 b8ef 2800 0000 0000 0000 0000  ..&...(.........
> 002d3070: ec07 2900 cca0 2600 28f2 2800 0000 0000  ..)...&.(.(.....
> 002d3080: 0000 0000 5810 2900 3ca3 2600 4cef 2800  ....X.).<.&.L.(.
> 002d3090: 0000 0000 0000 0000 ee11 2900 60a0 2600  ..........).`.&.
> 002d30a0: 78f4 2800 0000 0000 0000 0000 0812 2900  x.(...........).
> 002d30b0: 8ca5 2600 68f4 2800 0000 0000 0000 0000  ..&.h.(.........
> 002d30c0: 4c12 2900 7ca5 2600 ecee 2800 0000 0000  L.).|.&...(.....
> 002d30d0: 0000 0000 b012 2900 00a0 2600 c4f1 2800  ......)...&...(.
> 002d30e0: 0000 0000 0000 0000 ee12 2900 d8a2 2600  ..........)...&.
> 002d30f0: 44ef 2800 0000 0000 0000 0000 fa12 2900  D.(...........).
> 002d3100: 58a0 2600 58f4 2800 0000 0000 0000 0000  X.&.X.(.........
> 002d3110: 3813 2900 6ca5 2600 d8f1 2800 0000 0000  8.).l.&...(.....
> 002d3120: 0000 0000 4091 2f00 e8a2 2600 0000 0000  ....@./...&.....
> 002d3130: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3140: 7033 5f6d 6f64 6c6f 6164 6572 0000 2e64  p3_modloader...d
> 002d3150: 6c6c 0000 0000 0000 0000 0000 0000 0000  ll..............
> 002d3160: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3170: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3180: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3190: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d31a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d31b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d31c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d31d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d31e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d31f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3200: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3210: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3220: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3230: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3240: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3250: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3260: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3270: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3280: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3290: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d32a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d32b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d32c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d32d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d32e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d32f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3300: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3310: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3320: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3330: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3340: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3350: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3360: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3370: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3380: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3390: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d33a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d33b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d33c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d33d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d33e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d33f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3400: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3410: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3420: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3430: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3440: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3450: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3460: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3470: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3480: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3490: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d34a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d34b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d34c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d34d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d34e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d34f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3500: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3510: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3520: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3530: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3540: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3550: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3560: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3570: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3580: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3590: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d35a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d35b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d35c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d35d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d35e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d35f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3600: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3610: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3620: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3630: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3640: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3650: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3660: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3670: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3680: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3690: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d36a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d36b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d36c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d36d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d36e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d36f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3700: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3710: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3720: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3730: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3740: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3750: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3760: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3770: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3780: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3790: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d37a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d37b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d37c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d37d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d37e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d37f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3800: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3810: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3820: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3830: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3840: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3850: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3860: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3870: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3880: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3890: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d38a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d38b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d38c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d38d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d38e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d38f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3900: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3910: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3920: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3930: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3940: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3950: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3960: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3970: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3980: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3990: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d39a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d39b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d39c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d39d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d39e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d39f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a30: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a50: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3a90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3aa0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ab0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ac0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ad0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ae0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3af0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b30: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b50: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3b90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ba0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3bb0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3bc0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3bd0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3be0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3bf0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c30: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c50: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3c90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ca0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3cb0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3cc0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3cd0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ce0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3cf0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d30: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d50: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3d90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3da0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3db0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3dc0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3dd0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3de0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3df0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e30: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e50: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3e90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ea0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3eb0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ec0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ed0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ee0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ef0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f10: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f20: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f30: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f50: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f60: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f70: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f80: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3f90: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3fa0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3fb0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3fc0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3fd0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3fe0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 002d3ff0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Known Bugs

Bugs identified by the community are listed here.

New Settlement Ware Production Bug

Summary

Due to an off-by-one error, a town founded through the alderman mission does not produce the goods the Hanse needs most, but instead those with an adjacent facility id.

Details

The determine_new_settlement function at 0x00532E30 calculates the wares with the biggest need. Then it attempts to build a bitmap in which a 1 denotes that the nth production facility should be effective. This is pseudeocode of the bit position calculation:

(1 << (ware_to_facility_mapping[ware_id] - 3)) & 0xFFFFFF;

Production facilities start at id 0x04 (hunting lodge), but P3 subtracts only 3 from the facility id. Therefore, if the Hanse has a shortage of skins, the bitmap will have 0b0000000000000010 set instead of 0b0000000000000001. The second production facility is fisherman's hut (0x05), so a shortage of skins causes an effective fish production, and so forth.

Fix

Subtracting 4 instead of 3 fixes this bug. The subtraction is at 0x00532FF1:

.text:00532FE2 loc_532FE2:                             ; CODE XREF: determine_new_settlement+1EA↓j
.text:00532FE2                 mov     edx, [esp+edi*4+0F0h+effective_ware_ids]
.text:00532FE6                 mov     eax, 1
.text:00532FEB                 mov     cl, ds:ware_to_prod_mapping[edx]
.text:00532FF1                 sub     ecx, 3
.text:00532FF4                 shl     eax, cl
.text:00532FF6                 mov     ecx, [esi]
.text:00532FF8                 and     eax, 0FFFFFFh
.text:00532FFD                 test    eax, ecx
.text:00532FFF                 jnz     short loc_533007
.text:00533001                 or      ecx, eax
.text:00533003                 mov     [esi], ecx
.text:00533005                 jmp     short loc_53300C

To change the 3 to a 4, the operation 83 E9 03 needs to be replaced with 83 E9 04.

Market Hall Production of Town Bug

Summary

The market hall "Production" page states it displays weekly production, but in the "Town" column it displays the daily production.

Details

The ui_prepare_market_hall_window_production_page function at 0x005DE960 does not multiply the daily production of towns values with 7, as it does with merchant production.

Fix

Replacing the two basic blocks which prepare the market hall page (0x005DEA18 for barrel wares and 0x005DEA73 for bundle wares) with a copy that does an additional imul instruction solves this issue.

Siege Beggar Satisfaction Bonus Bug

Summary

The satisfaction of beggars never decreases, but increases if a town wins a siege.

Details

The update_citizen_satisfaction function at 0x0051C830 calculates the satisfaction for all population types except beggars, so the beggar satisfaction should not change throughout the game.

However, the tick_siege function at 0x00629A50 gives every population type including beggars a bonus of 20 satisfaction when the town repels the attackers, bypassing the satisfaction step size.

Since the beggar satisfaction is influencing the beggar immigration, this bug has a gameplay impact: The more sieges a town wins, the more beggars it will attract.

Fix

The following code distributes the satisfaction bonus, where edx contains the (decrementing) loop variable, ecx the current population type and eax the (u16) offset in the towns array.

.text:00629A32                 mov     edx, 4
.text:00629A37
.text:00629A37 loc_629A37:                             ; CODE XREF: tick_siege+1872↓j
.text:00629A37                 xor     eax, eax
.text:00629A39                 mov     al, [esi+5D3h]
.text:00629A3F                 lea     ebx, [eax+eax*4]
.text:00629A42                 shl     ebx, 6
.text:00629A45                 sub     ebx, eax
.text:00629A47                 lea     eax, [ecx+ebx*4]
.text:00629A4A                 mov     ebx, static_game_world.field_68_towns
.text:00629A50                 add     word ptr [ebx+eax*2+300h], 14h
.text:00629A59                 lea     eax, [ebx+eax*2+300h]
.text:00629A60                 inc     ecx
.text:00629A61                 dec     edx
.text:00629A62                 jnz     short loc_629A37
.text:00629A64                 mov     al, [esi+61Ch]
.text:00629A6A                 test    al, al
.text:00629A6C                 jz      short loc_629A8E
.text:00629A6E                 xor     eax, eax
.text:00629A70                 push    edi
.text:00629A71                 mov     al, [esi+5D3h]
.text:00629A77                 mov     ecx, eax
.text:00629A79                 shl     ecx, 7
.text:00629A7C                 add     ecx, eax
.text:00629A7E                 lea     edx, [eax+ecx*8]
.text:00629A81                 mov     eax, static_class26_ptr
.text:00629A86                 lea     ecx, [eax+edx*4]
.text:00629A89                 call    sub_634C50

Since the loop body does not interact with edx, it is sufficient to initialize edx to 3 instead of 4. To archive that, the operation BA 04 00 00 00 needs to be replaced with BA 03 00 00 00.

Damage to Offside Ship Artillery Bug

Summary

Incoming projectiles don't always damage weapons on the correct side of the ship, because the get_sea_battle_projectile_impact_direction function at 0x0060A73C, which calculates which side of the ship has been hit, is broken.

Details

The mapping between "left", "right" and "random" sides of the ship being hit is as follows:

Fix

To get the correct results, the shape defined in the static hitbox table at 0x0067AB30 must rotate by one point. The old table:

0067AB30  ED FF  E1 FF
0067AB34  00 00  C9 FF
0067AB38  13 00  E1 FF
0067AB3C  13 00  2F 00
0067AB40  ED FF  2F 00
0067AB44  EB FF  DC FF
0067AB48  00 00  BE FF
0067AB4C  15 00  DC FF
0067AB50  13 00  37 00
0067AB54  ED FF  37 00
0067AB58  E7 FF  F2 FF
0067AB5C  00 00  CA FF
0067AB60  19 00  F2 FF
0067AB64  13 00  43 00
0067AB68  ED FF  43 00
0067AB6C  EA FF  E7 FF
0067AB70  00 00  BD FF
0067AB74  16 00  E7 FF
0067AB78  13 00  51 00
0067AB7C  ED FF  51 00

must be replaced with this rotated variant:

0067AB30  00 00  C9 FF
0067AB34  13 00  E1 FF
0067AB38  13 00  2F 00
0067AB3c  ED FF  2F 00
0067AB40  ED FF  E1 FF
0067AB44  00 00  BE FF
0067AB48  15 00  DC FF
0067AB4c  13 00  37 00
0067AB50  ED FF  37 00
0067AB54  EB FF  DC FF
0067AB58  00 00  CA FF
0067AB5c  19 00  F2 FF
0067AB60  13 00  43 00
0067AB64  ED FF  43 00
0067AB68  E7 FF  F2 FF
0067AB6c  00 00  BD FF
0067AB70  16 00  E7 FF
0067AB74  13 00  51 00
0067AB78  ED FF  51 00
0067AB7C  EA FF  E7 FF

This will rotate the hitbox points, and define the following (correct) impact location mapping:

Invulnerable Ship Artillery Slots Bug

Summary

The apply_sea_battle_damage function at 0x0061F96F never destroys the artillery in the first two slots.

Details

The starting index of the loops over the artillery slots is too high to cover the first two slots. It is calculated as follows:

if impact_location == 3: # random
    slot_pattern = (get_battle_rand() & 1) + 1 # 1 or 2
elif impact_location == 1: # left
    slot_pattern = 2
elif impact_location == 2: # right
    slot_pattern = 1

i = slot_pattern * 2

Consequently, for a hit to the left side i is initialized to 4, which makes the slots 0 and 1 invulnerable.

Fix

To fix the issue, the index calculation must be changed to yield 0 for the left side.

Bath House Bribes Blunders

Summary

The bath house bribes are affected by three different bugs:

  • If a merchant's offer is lower than the councillor's expectations and the councillor hasn't been bribed by anyone else, the councillor is bribed by the merchant with the index 1.
  • Annoying a councillor in one town affects councillors in all towns.
  • Limits on the bribability don't have any effect.

Details

Attribution to Merchant 1

The handle_operation_bath_house_bribe_failure function at 0x0053AD10 sets the councillor's briber to 1, if the councillor was not already bribed by anyone.

This also prevents you from getting the "Are you there again?! Let me have my bath in peace, please." line you are supposed to get with annoyed councillors. Instead you'll get the "Ah! You're here as well, John Doe? I have only very recently spoken to one of your competitors." line, since the councillor is now bribed by merchant 1.

Unforgiving Bath Houses

The bath house remembers only the index of annoyed local councillors (0-3), and not to which town they belong. If you annoy the first councillor in one town, the first councillor in every town will stop talking to you, and so forth.

While the annoyed "Are you there again?! Let me have my bath in peace, please." line is unreachable in the correct town due to the attribution bug, you do get it if you encounter a councillor with the same index in a different town.

Limitless Corruption

The handle_operation_bath_house_bribe_success function at 0x0053AC50 applies the briber only if a particular value is smaller than 2:

bribed_councillors = 0;
previous_bribing_merchant = static_game_world.field_68_towns[town_index].field_6DC_councillor_bribes[this->args.unknown.arg1];
v6 = 4;
do
{
    if (previous_bribing_merchant == merchant_index)
        ++bribed_councillors;
    --v6;
}
while (v6);
merchant = get_merchant(&static_game_world, merchant_index);
old_recent_donations = merchant->field_4BC_recent_donations;
merchant->field_0_money -= this->args.unknown.arg3;
merchant->field_4BC_recent_donations = this->args.unknown.arg3 + old_recent_donations;
merchant_bribe_success_increase_social_reputation(merchant, this->args.unknown.arg4, this->args.unknown.arg3);
if (bribed_councillors < 2) {
    static_game_world.field_68_towns[this->args.unknown.arg4].field_6DC_councillor_bribes[this->args.unknown.arg1] = this->args.unknown.arg2;
}

Since neither previous_bribing_merchant nor merchant_index change during the loop, the final value of bribed_councillors is always either 0 or 4. This doesn't make any sense as it is, so it is assumed that bribed_councillors was intended to be the amount of already bribed councillors in that town, and that the comparison with 2 was intended to prevent a player from bribing the majority of councillors in one town.

Fix

All bugs are fixed by the fix-bath-house-bribe-blunders mod.

Attribution to Merchant 1

The councillor's briber must be set to -1 instead of 1 if the bribe fails and the councillor is not bribed by anyone else. The original instruction:

.text:0053AD85                 mov     byte ptr [ecx], 1

must be replaced with:

.text:0053AD85                 mov     byte ptr [ecx], -1

Unforgiving Bath Houses

This cannot be properly fixed easily. However, resetting the annoyance when opening the bath house is just a small change. Since the annoyances are reset if the bath house is not opened for 256 ticks, one can replace the conditional jump around the reset code with nops:

.text:005B17B7 jbe     short loc_5B17CC

Limitless Corruption

This issue is fixed by moving the previous_bribing_merchant assignment into the loop and using the loop counter instead of the argument to index the bribes array.

Footnotes

Since the probability of each councillor appearing is just 7%, you can use the debug bath house IDC script to change it to 100%.

Multiplayer Locks

Summary

The game thread and network thread access shared data structures without proper synchronization. These race conditions can cause operations to disappear, and that causes the game to desync.

Details

Despite locks and even lock-free concurrent data structures being well-understood in the mid 1980s, P3 does not use correct locks anywhere.

Execute Current Operations

The execute_operations function at 0x00546870 is not locking the current operations properly.

The basic block at 0x005468B3 attempts to lock the current operations: This is not how locks work.

Insert Pending Operations

The insert_into_pending_operations_warpper function at 0054AA70 is not locking the pending operations properly.

The basic blocks at 0054AA79 attempt to lock the pending operations: This is not now locks work.

Client Ingress Queue

The function at 0x0054B080 which moves operations from the ingress queue and the socket into the current operations is not locking the current operations properly at two locations.

The basic block at 0x0054B13F attempts to try-lock the current operations: This is not how locks work.

The basic block at 0x0054B200 attempts to try-lock the current operations: This is not how locks work.

Client Pending Operations

The function at 0x0054AFA0 which sends operations from the pending operations to the host is not locking the current operations properly: This is certainly not how locks work.

Host Egress Queue

The function at 0x0054B670 which moves operations from the host's pending operations and the client sockets into the egress queue is not locking the pending operations properly: This is not how locks work.

Host Ingress Queue

The function at 0x0054B960 which moves operations from the host's egress and ingress queues into the current operations is not locking the current operations properly: This is not how locks work.

Fix

All bugs are fixed by the fix-multiplayer-locks mod.

Execute Current Operations

To fix the problem at 0x005468B3 the following changes have to be made:

  • The "locking" basic blocks at 0x005468B3 must correctly lock the current operations. This can be achieved by inserting a call to a proper lock function.
  • The "unlocking" basic block at 0x00547254 must correctly unlock the current operations. This can be achieved by inserting a call to a proper unlock function.

Insert Pending Operations

To fix the problem at 0x0054AA79 the following changes have to be made:

  • The "locking" basic blocks at 0x0054AA79 must correctly lock the pending operations. This can be achieved by inserting a call to a proper lock function.
  • The "unlocking basic block at 0x0054AAC2 must correctly unlock the pending operations. This can be achieved by inserting a call to a proper unlock function.

Client Ingress Queue

To fix the problem at 0x0054B13F the following changes have to be made:

  • The "try-locking" basic block at 0x0054B13F must correctly try-lock the current operations, and continue into the basic block at 0x0054B14F only if the lock was acquired. This can be achieved by replacing the two mov instructions with a call instruction to a proper try-lock function which returns the result.
  • The "unlocking" basic block at 0x0054B198 must unlock the current operations only if they were locked by the basic block at 0x0054B13F. This can be achieved by replacing the mov instruction with a call instruction to a proper unlock function and making the jnz instruction target the next instruction after the call. The cmp instruction above it must be moved below it to ensure it always happens, so jnz must point to the moved cmp.

To fix the problem at 0x0054B200 the following changes have to be made:

  • The "try-locking" basic block at 0x0054B200 must correctly try-lock the current operations, and continue into the basic block at 0x0054B210 only if the lock was acquired. This can be achieved by replacing the two mov instructions with a call instruction to a proper try-lock function which returns the result.
  • The "unlocking" basic block at 0x0054B21C must unlock the current operations only if they were locked by the basic block at 0x0054B200. This can be achieved by replacing the mov instruction with a call instruction to a proper unlock function and making the jnz instruction target the next instruction after the call.

Client Pending Operations

To fix the problem at 0x0054AFB7 the following changes have to be made:

  • The "locking" basic block at 0x0054AFB7 must correctly lock the pending operations. This can be achieved by replacing the entire block and its successor with a call instruction to a proper lock function.
  • The two "unlocking" branches at 0x0054B049 and 0x0054B063 must unlock the pending operations. This can be achieved by replacing the respective mov instruction with a call instruction to a proper unlock function.

Host Egress Queue

To fix the problem at 0x0054B90D the following changes have to be made:

  • The "locking" basic block at 0x0054B90D must correctly lock the pending operations. This can be achieved by inserting a call to a proper lock function.
  • The "unlocking" instruction at 0x0x0054B949 must correctly unlock the pending operations. This can be achieved by inserting a call to a proper unlock function.

Host Ingress Queue

To fix the problem at 0x0054BCCB the following changes have to be made:

  • The "try-locking" basic block at 0x0054BCCB must correctly try-lock or lock the current operations, and continue into the basic block at 0x0054BCD9 only if the lock was acquired. This can be achieved by inserting a call to a proper lock function.
  • The "unlocking" instruction at 0x0054BD2C must correctly unlock the current operations if they were locked. If the "try-lock" was replaced with a lock, this can be achieved by inserting a call to a proper unlock function.

Uncompressed Trade Route Loading

Summary

The decompress_packed_trade_route function at 0x006387E0 can handle uncompressed trade routes if the output length is negative. However, its caller fails to allocate the correct amount of bytes, so the loading fails.

Details

Instead of calculating the absolute of the output size, the output size is anded with 0x3fffffff:

For negative output sizes the result is very big, so the allocation fails, and the trade route loading is aborted.

Fix

To fix this issue, the correct absolute value must be calculated.

Patches

High Res

Summary

This patch changes the game's resolution to 1920x1080 pixels (Full HD).

Details

Hooks are deployed to the following locations:

  • The options screen's exit function (to set the resolution)
  • The function that sets the resolution before the scene loads (to set the the resolution)
  • The function that sets the resolution after the scene has already loaded (to set the the resolution)
  • The function that sets the position of the UI elements on the right (to set them according to the resolution)
  • The function that sets the position of the UI elements at the top (to set them according to the resolution)
  • The function that renders all objects (to make the rectangle in the bottm right corner black)
  • The function in aim.dll that decodes image files (to replace the acceleration map with the upscaled variant)
  • The function that loads screen settings from accelMap.ini (to set the dimension of the acceleration map)

Patch

The source code for this patch can be found here.

Increase Alderman "Found Settlement" Mission Limit Patch

Summary

This patch allows the player build towns beyond the 26th town through the "Found Settlement" mission.

Details

The schedule_prep_alderman_missions function at 0x005326E0 schedules a prepare mission operation for every eligible alderman mission type. The new settlement mission is eligible if the total amount of towns is below 26.

Patch

The comparion is at 0x0053275C:

.text:0053275C                 cmp     dword ptr [ebp+10h], 1Ah

To replace the limit of 26, the 1A in the operation 83 7D 10 1A has to be replaced with a different immediate value.

Render All Ships Patch

Summary

This patch modifies P3 to render all ships on the scrollmap, not just the up to two ships spotted by each player ship or convoy.

Details

The first half of the draw_spotted_ships function at 004516B0 determines which ships should be rendered. It produces three things of interest:

  • the amount of ships that should be drawn
  • an array of ship indexes that should be drawn
  • an array of ship y coordinates

The rest of the function does not have to be modified.

Patch

The following function fixup_all_ships takes pointers to the two arrays as an argument, and returns the amount of ships that should be drawn. It only enqueues ships whose status is 0xf (merchant vessel at sea) and 0x12 (AI pirate vessel).

#include <stdint.h>
#define CLASS6_PTR 0x006DD7A0

inline void* get_ship_by_index(uint16_t index) {
    uint32_t ships_ptr =  *(uint32_t*) (CLASS6_PTR + 0x04);
    return (void*) ships_ptr + 0x180 * (uint32_t) index;
}

inline uint16_t get_ships_size() {
    return *(uint16_t*) (CLASS6_PTR + 0xf4);
}

inline uint16_t get_ship_status(void* ship) {
    return *(uint16_t*) (((uint32_t) ship) + 0x134);
}

inline uint16_t get_ship_y_high(void* ship) {
    return *(uint16_t*) (((uint32_t) ship) + 0x22);
}

uint32_t fixup_all_ships(uint32_t* spotted_y, uint32_t* spotted_index) {
    uint16_t ships_size = get_ships_size();
    int new_spotted_size = 0;

    for (uint16_t i = 0; i < ships_size; i++) {
        void* ship = get_ship_by_index(i);
        uint16_t status = get_ship_status(ship);

        if (status != 0x12 && status != 0xf) {
            continue;
        }

        spotted_y[new_spotted_size] = get_ship_y_high(ship);
        spotted_index[new_spotted_size] = i;

        new_spotted_size += 1;
    }

    return new_spotted_size;
}

Built with gcc 14.1 (-m32 -O3 -fno-stack-protector) this generates the following assembly:

fixup_all_ships:
        push    ebp
        push    edi
        push    esi
        push    ebx
        movzx   ebx, WORD PTR ds:7198868
        mov     esi, DWORD PTR [esp+20]
        mov     edi, DWORD PTR [esp+24]
        test    bx, bx
        je      .L6
        xor     ecx, ecx
        xor     eax, eax
.L5:
        lea     edx, [ecx+ecx*2]
        sal     edx, 7
        add     edx, DWORD PTR ds:7198628
        movzx   ebp, WORD PTR [edx+308]
        cmp     bp, 18
        je      .L7
        cmp     bp, 15
        jne     .L3
.L7:
        movzx   edx, WORD PTR [edx+34]
        mov     DWORD PTR [esi+eax*4], edx
        mov     DWORD PTR [edi+eax*4], ecx
        add     eax, 1
.L3:
        add     ecx, 1
        cmp     ebx, ecx
        jne     .L5
        pop     ebx
        pop     esi
        pop     edi
        pop     ebp
        ret
.L6:
        pop     ebx
        xor     eax, eax
        pop     esi
        pop     edi
        pop     ebp
        ret

A jump to fixup_all_ships has to be injected at 0x00451759 with the following assembly instructions. #ADDRESSOFPATCH needs to be replaced with the address of fixup_all_ships.

# save regs
push eax
push ecx
push edx

# call fixup_all_ships
mov eax, [esp+0x30]
mov ecx, [esp+0x2C]
push ecx
push eax
mov edx, #ADDRESSOFPATCH
call edx
mov ebp, eax
pop eax
pop eax

# restore regs
pop edx
pop ecx
pop eax

# jump to second part of draw_spotted_ships
mov eax, 0x00451B58
jmp eax

Tavern Show All Sailors Patch

Summary

This patch modifies P3 to show all available sailors in the tavern's "Hire Sailors" page.

Details

The "Hire Sailors" page displays a random amount of sailors if the sailor pool is bigger than 50.

Patch

The comparison is at 0x005D4CD4:

.text:005D4CCA                 call    get_available_sailors
.text:005D4CCF                 and     eax, 0FFh
.text:005D4CD4                 cmp     eax, 32h ; '2'
.text:005D4CD7                 mov     [esp+54h+a3], eax
.text:005D4CDB                 jle     short loc_5D4CF8
.text:005D4CDD                 fld     dword ptr [esi+1CA0h]
.text:005D4CE3                 fmul    ds:flt_672F04
.text:005D4CE9                 fadd    ds:dbl_679CA0
.text:005D4CEF                 call    __ftol
.text:005D4CF4                 mov     [esp+54h+a3], eax
.text:005D4CF8 loc_5D4CF8:
.text:005D4CF8                 cmp     [esi+1C38h], eax

To increase the threshold to 100, the operation 83 F8 32 has to be replaced with 83 F8 64.

Shipyard Details Patch

Summary

This patch display's the shipyard's employees, markup and experience.

Details

Hooks are deployed to the following locations:

  • The window's open function (to set the window's gradient)
  • The window's render_window function (to render additional text elements)

Patch

The source code for this patch can be found here.

Town Hall Details Patch

Summary

This patch display's the hanseatic stock/consumption ratios used to determine town and effective production, and shows details about the alderman missions on offer.

Details

Hooks are deployed to the following locations:

  • The window's open function (to set the window's background color gradient)
  • The sidepanel's set_selected_page function (to set the window's background color gradient)
  • The window's render_window function (to render additional text elements)

Patch

The source code for this patch can be found here.

File Formats

This chapter explains the custom file formats used throughout P3.

CPR

CPR are archive files used in multiple Ascaron games. The file format is defined as:

          |  00  01  02  03  |  04  05  06  07  |  08  09  0A  0B  |  0C  0D  0E 0F  |
00000000  | Header                                                                   |
00000010  | Version          | Padding                                               |
00000020  | Chunks                                                                   |

Each chunk is defined as:

00000000  | Index Size       | Unknown          | Files Count      | Data Size       |
00000010  | Index Entries                                                            |
          | Data                                                                     |

Index Size defines the byte size of the index entries. Files Count defines the amount of entries in the chunk. Data Size defines the size of the data block.

Each index entry is defined as:

00000000  | Offset           | Size             | Unknown          | Path            |
00000010  | Path (cont.)                                                             |

Offset defines the start position relative to the start of the file, and Size the size. Path is latin1 encoded and zero-terminated.

Multiple parsers exist, e.g. CPRreader and cprcli.

Trade Routes (.rou)

The saved trade routes are stored in ./Save/AutoRoute. An uncomporessed trade route is an array of route stops, 220 bytes each. The rou file format is defined as:

          | 00  01   02  03   04  05   06  07 |
00000000  | Output Length   | Data            |

If the output length is bigger than 0 the file is compressed. Otherwise the absolute value of the negative output length denotes the length.

Compression

The compression algorithm has not been identified, but the decompression was reproduced.

Trade Route Stops

A trade route stop is defined as:

          |           00           01           02           03           |
00000000  | Unused                        | Town Index | Action           |
00000004  | Ware Order Array                                              |
...
0000001c  | Ware Price Array                                              |
...
0000007c  | Ware Amount Array                                             |

The "direction" of a transaction is encoded in the price and amount:

PriceAmountDirection
0NegativeShip -> Office
0PositiveOffice -> Ship
PositivePositiveShip -> Town
NegativePositiveTown -> Ship

The "Max" amount is represented by 1_000_000_000 for both barrel and bundle wares.

Navigation Matrix

The vanilla navigation matrix is stored in ./navdata/nav_matrix.dat. The file format is defined as:

          | 00  01   02  03   04  05   06  07 |
00000000  | Width  | Height | Data            |
00000008  | Data (cont)                       |

where Width and Height denote the dimensions of a matrix, and Data is an u8 array of length Width*Height. A value of 0x00 denotes water, a value of 0x01 denotes land. The dimensions of the vanilla matrix are 640x472.

The following sample code converts the navigation matrix file into a png:

import struct
import imageio
import numpy

f = open("nav_matrix.dat", "rb")
width = struct.unpack("<H", f.read(2))[0]
height = struct.unpack("<H", f.read(2))[0]
print(f"Reading {width}x{height} image")
image = numpy.zeros((height, width, 3), dtype=numpy.uint8)

for y in range(0, height):
    for x in range(0, width):
        cell = f.read(1)
        if cell == b"\x00":
            image[y, x] = (0x00, 0xff, 0xff)
        elif cell == b"\x01":
            image[y, x] = (0x7f, 0x7f, 0x7f)
        else:
            raise Exception(f"{cell}")

imageio.imwrite('nav_matrix.png', image)

image

Navigation Vector

The vanilla navigation vector is stored in ./navdata/nav_vec.dat. The file format is defined as:

          | 00  01   02  03   04  05   06  07 |
00000000  | Length | 00  00 | X      | Y      |
00000008  | X      | Y      | ...             |

where Length denotes the amount of points, and X and Y denote the coordinates of each point.

The following sample code converts the navpoint matrix file into a png:

import struct
import imageio
import numpy

WIDTH = 640
HEIGHT = 472
f = open("nav_vec.dat", "rb")
length = struct.unpack("<H", f.read(2))[0]
f.read(2)
print(f"Reading {length} vecs into {WIDTH}x{HEIGHT} image")
image = numpy.zeros((HEIGHT, WIDTH, 3), dtype=numpy.uint8)

for i in range(0, length):
    x = struct.unpack("<H", f.read(2))[0]
    y = struct.unpack("<H", f.read(2))[0]
    image[y, x] = (0xff, 0x00, 0x00)

imageio.imwrite('nav_vec.png', image)

image

Navpoint Matrix

The vanilla navigation matrix is stored in ./navdata/matrix_int.dat. The file format is defined as:

          | 00  01   02  03   04  05   06  07 |
00000000  | Dist            | Next   | Dist   |
00000008  |        | Next   | Dist            |
0000000C  | ...                               |

It is a square matrix of the navpoints, denoting the distance and path. Every cell holds the total distance between x and y, and the next navpoint's index required to go from x to y: \[M[x][y] = (dist_{xy}, next_{xy}) \] Consequently the main diagonal's distance is always 0: \[ x = y \implies dist_{xy} = dist_{yx} = 0 \land next_{xy} = next_{yx} = x = y \] Cells of directly connected navpoints point to the other cell: \[ next_{xy} = y \implies next_{yx} = x \] Cells of navpoints connected through only one other navpoint are symmetric: \[ next_{xz} = y \land next_{yz} = z \implies next_{zx} = y \land next_{yx} = x \]

Given the distances of directly connected navpoints, the navpoint matrix can be calculated using pathfinding algorithms like A*.

The following sample code plots the direct connections:

import struct
from typing import Tuple
from PIL import Image, ImageDraw

UPSCALE = 11

# Load navigation matrix
nav_matrix_file = open("nav_matrix.dat", "rb")
width, height = struct.unpack("<HH", nav_matrix_file.read(4))
print(f"{width}x{height}")
image = Image.new("RGB", (width * UPSCALE, height * UPSCALE))
draw = ImageDraw.Draw(image)
navigation_matrix: dict[Tuple[int, int], Tuple[int, int, int]] = {}
for y in range(0, height):
    for x in range(0, width):
        cell = nav_matrix_file.read(1)
        if cell == b"\x00":
            navigation_matrix[(x, y)] = (0x00, 0xFF, 0xFF)
        elif cell == b"\x01":
            navigation_matrix[(x, y)] = (0x7F, 0x7F, 0x7F)
        else:
            raise Exception(f"{cell}")

# Load navigation vector
nav_vec_file = open("nav_vec.dat", "rb")
navpoints_count = struct.unpack("<H", nav_vec_file.read(2))[0]
print(f"{navpoints_count} navpoints")
nav_vec_file.read(2)
vector: list[Tuple[int, int]] = []
for i in range(0, navpoints_count):
    x = struct.unpack("<H", nav_vec_file.read(2))[0]
    y = struct.unpack("<H", nav_vec_file.read(2))[0]
    vector.append((x, y))

# Draw navigation matrix
for (x, y), val in navigation_matrix.items():
    for xi in range(0, UPSCALE):
        for yi in range(0, UPSCALE):
            image.putpixel((x * UPSCALE + xi, y * UPSCALE + yi), val)

# Load and draw navpoint matrix
matrix_int_file = open("matrix_int.dat", "rb")
direct_connections = 0
indirect_connections = 0
for source in range(0, navpoints_count):
    for destination in range(0, navpoints_count):
        total_distance = struct.unpack("<I", matrix_int_file.read(4))[0]
        next_hop = struct.unpack("<H", matrix_int_file.read(2))[0]
        if next_hop == destination:
            x_src, y_src = vector[source]
            x_dst, y_dst = vector[destination]
            draw.line(
                [
                    (x_src * UPSCALE + UPSCALE // 2, y_src * UPSCALE + UPSCALE // 2),
                    (x_dst * UPSCALE + UPSCALE // 2, y_dst * UPSCALE + UPSCALE // 2),
                ],
                width=1,
            )
            direct_connections += 1
        else:
            indirect_connections += 1
print(
    f"{direct_connections} direct connections, {indirect_connections} indirect connections"
)

# Draw navigation vector
for x, y in vector:
    for xi in range(0, UPSCALE):
        for yi in range(0, UPSCALE):
            image.putpixel((x * UPSCALE + xi, y * UPSCALE + yi), (0xFF, 0x00, 0x00))

image.save("matrix_int.png", "PNG")

image

Fixed Paths

Fixed paths are stored in ./navdata/wege_fix.dat. The standard game does not appear to load it and does not care if you replace it with arbitrary bytes. It appears to be a simple length-value encoding:

          | 00  01   02  03 | 04  05 | 06  07 |
00000000  | Length          | X      | Y      |
00000008  | ...             | Length          |
...       | X      | Y      | ...

The resulting coordinates somewhat resemble the coastlines, but they don't quite fit the navigation matrix.

Appendix

Cheat Engine

A CE cheat table can be found here.

IDA

IDC debugging scripts can be found here.