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
Abbreviation | Meaning |
---|---|
P3 | Patrician 3 |
Basics
This chapter describes ubiquitous functions, enums and structs whose understanding of is a prerequisite of the following chapters.
Enums
Functions
Address | Signature | Description |
---|---|---|
0x0064F7B9 | malloc_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:
BuildingId | NewBuildingId |
---|---|
0x00 | 0x2c |
0x01 | 0x1b |
0x02 | 0x0b |
0x03 | 0x08 |
0x04 | HuntingLodge |
0x05 | FishermansHouse |
0x06 | Brewery |
0x07 | Workshop |
0x08 | Apiary |
0x09 | FarmGrain |
0x0a | FarmCattle |
0x0b | Sawmill |
0x0c | WeavingMill |
0x0d | Saltworks |
0x0e | IronSmelter |
0x0f | FarmSheep |
0x10 | Vineyard |
0x11 | Pottery |
0x12 | Brickworks |
0x13 | Pitchmaker |
0x14 | FarmHemp |
0x15 | HouseRich |
0x16 | HouseRich |
0x17 | HouseRich |
0x18 | HouseWealthy |
0x19 | HouseWealthy |
0x1a | HouseWealthy |
0x1b | HousePoor |
0x1c | HousePoor |
0x1d | HousePoor |
0x1e | Warehouse |
0x1f | 0x04 |
0x20 | 0x03 |
0x21 | 0x0b |
0x22 | 0x07 |
0x23 | 0x00 |
0x24 | 0x02 |
0x25 | 0x05 |
0x26 | 0x09 |
0x27 | 0x33 |
0x28 | 0x52 |
0x29 | Hospital |
0x2a | Mint |
0x2b | School |
0x2c | Chapel |
0x2d | 0x37 |
0x2e | 0x01 |
0x2f | 0x5f |
0x30 | Tower |
0x31 | Tower |
0x32 | Tower |
0x33 | Tower |
0x34 | PitchShoot |
0x35 | PitchShoot |
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 Index | Game Time LSB |
---|---|
32 | 0b00000_011 |
33 | 0b00001_011 |
34 | 0b00010_011 |
35 | 0b00011_011 |
... | ... |
39 | 0b00111_011 |
00 | 0b00000_111 |
01 | 0b00001_111 |
02 | 0b00010_111 |
03 | 0b00011_111 |
... | ... |
31 | 0b11111_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:
Opcode | Task |
---|---|
0x00 | Move Ship |
0x01 | Sell Wares from Ship |
0x02 | Buy Wares to Ship |
0x03 | Repair Ship |
0x04 | Hire Sailors |
0x06 | Dismiss Captain |
0x15 | Create Convoy |
0x16 | Disband Convoy |
0x1b | Move Wares |
0x1d | Repair Convoy |
0x24 | Build Town Wall |
0x25 | Build Road |
0x26 | Demolish Building |
0x29 | Grant Loan |
0x2c | Build Ship |
0x30 | Feed the Poor |
0x31 | Donate to Church Extension |
0x32 | Donate to Church |
0x37 | Join Guild |
0x39 | Bathe |
0x41 | Form Militia Squad |
0x42 | Bath House Bribe Success |
0x43 | Bath House Bribe Failure |
0x48 | Make Town Hall Offer |
0x52 | Tavern Interaction |
0xc2 | Autosave |
0xc4 | Advance Time |
0x9f | Start Ship Combat |
0x96 | Steer 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:
Opcode | Task |
---|---|
0x01 | Debt Repayment |
0x05 | Crime Investigation Result |
0x06 | Update Shipard Experience |
0x07 | Celebration |
0x0c | Land Transport Arrival |
0x15 | Marriage |
0x1a | Update Sailor Pools |
0x2e | Council Meeting |
0x35 | Unfreeze Harbor |
Related Functions
Address | Function | Description |
---|---|---|
0x004D8DD0 | reschedule_first_task | Moves 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:
Type | QL 0 | QL 1 | QL 2 | QL 3 |
---|---|---|---|---|
Snaikka | 0 | 100 | 300 | 1050 |
Crayer | 0 | 100 | 600 | 900 |
Cog | 0 | 200 | 400 | 800 |
Holk | 300 | 500 | 600 | 1200 |
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:
Ware | Base Consumption |
---|---|
Grain | 3 |
Meat | 2 |
Fish | 2 |
Beer | 2 |
Salt | 0 |
Honey | 1 |
Spices | 0 |
Wine | 2 |
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:
Level | Letter |
---|---|
0 | This was not really a great celebration [...] |
1 | The celebration was only moderately successful [...] |
2 | The celebration was relatively successful [...] |
3 | It 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:
Level | Social Reputation Impact |
---|---|
0 | -1 |
1 | 0.5 |
2 | 1 |
3 | 1.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 to22
.pending_no
is decreased by1
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 by1
for a councillor, by2
for a patrician and mayor, and by3
for an alderman.
- Should
pending_no
be smaller than4
, it is set to4
.
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:
Id | Name |
---|---|
0 | West |
1 | North |
2 | North Sea Area |
3 | Baltic Sea Area |
4 | East |
Towns are indentified by their id, and scripts/StadtDaten.ini
defines details such as the corresponding region:
Id | Name | Region |
---|---|---|
0 | Edinburgh | West |
1 | Newcastle | West |
2 | Scarborough | West |
3 | Boston | West |
4 | London | West |
5 | Bruges | West |
6 | Haarlem | North Sea Area |
7 | Harlingen | North Sea Area |
8 | Groningen | North Sea Area |
9 | Cologne | North Sea Area |
10 | Bremen | North Sea Area |
11 | Ripen | North Sea Area |
12 | Hamburg | North Sea Area |
13 | Flensburg | North Sea Area |
14 | Luebeck | North Sea Area |
15 | Rostock | North Sea Area |
16 | Bergen | North |
17 | Stavanger | North |
18 | Toensberg | North |
19 | Oslo | North |
20 | Aalborg | North |
21 | Goeteborg | North |
22 | Naestved | North |
23 | Malmoe | North |
24 | Ahus | North |
25 | Stockholm | North |
26 | Visby | North |
27 | Helsinki | North |
28 | Stettin | Baltic Sea Area |
29 | Ruegenwald | Baltic Sea Area |
30 | Gdansk | Baltic Sea Area |
31 | Torun | Baltic Sea Area |
32 | Koenigsberg | Baltic Sea Area |
33 | Memel | Baltic Sea Area |
34 | Windau | East |
35 | Riga | East |
36 | Pernau | East |
37 | Reval | East |
38 | Ladoga | East |
39 | Novgorod | East |
The "Found Settlement" alderman mission UI does not use the definitions from scripts/StadtDaten.ini
, but instead uses the following hardcoded mapping:
Id | Name | Region |
---|---|---|
0 | Edinburgh | West |
1 | Newcastle | West |
2 | Scarborough | West |
3 | Boston | West |
4 | London | West |
5 | Bruges | West |
6 | Haarlem | North Sea Area |
7 | Harlingen | North Sea Area |
8 | Groningen | North Sea Area |
9 | Cologne | North Sea Area |
10 | Bremen | North Sea Area |
11 | Ripen | North Sea Area |
12 | Hamburg | North Sea Area |
13 | Flensburg | Baltic Sea Area |
14 | Luebeck | Baltic Sea Area |
15 | Rostock | Baltic Sea Area |
16 | Bergen | North |
17 | Stavanger | North |
18 | Toensberg | North |
19 | Oslo | North |
20 | Aalborg | North |
21 | Goeteborg | North |
22 | Naestved | North |
23 | Malmoe | North |
24 | Ahus | North |
25 | Stockholm | North |
26 | Visby | North |
27 | Helsinki | North |
28 | Stettin | Baltic Sea Area |
29 | Ruegenwald | Baltic Sea Area |
30 | Gdansk | Baltic Sea Area |
31 | Torun | Baltic Sea Area |
32 | Koenigsberg | Baltic Sea Area |
33 | Memel | East |
34 | Windau | East |
35 | Riga | East |
36 | Pernau | East |
37 | Reval | East |
38 | Ladoga | East |
39 | Novgorod | East |
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:
Ware | Rich | Wealthy | Poor | Beggars |
---|---|---|---|---|
Grain | 90 | 120 | 150 | 120 |
Meat | 110 | 87 | 12 | 5 |
Fish | 40 | 80 | 100 | 110 |
Beer | 65 | 130 | 65 | 75 |
Salt | 1 | 1 | 1 | 1 |
Honey | 50 | 25 | 5 | 2 |
Spices | 4 | 2 | 2 | 0 |
Wine | 150 | 38 | 0 | 0 |
Cloth | 50 | 35 | 15 | 1 |
Skins | 60 | 30 | 0 | 0 |
WhaleOil | 50 | 35 | 10 | 0 |
Timber | 80 | 80 | 40 | 20 |
IronGoods | 100 | 75 | 25 | 0 |
Leather | 44 | 35 | 5 | 0 |
Wool | 10 | 40 | 20 | 5 |
Pitch | 0 | 0 | 0 | 0 |
PigIron | 0 | 0 | 0 | 0 |
Hemp | 5 | 3 | 2 | 3 |
Pottery | 30 | 18 | 12 | 1 |
Bricks | 1 | 1 | 0 | 0 |
Sword | 0 | 0 | 0 | 0 |
Bow | 0 | 0 | 0 | 0 |
Crossbow | 0 | 0 | 0 | 0 |
Carbine | 0 | 0 | 0 | 0 |
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.5 | Very Happy |
19.5 | Happy |
9.5 | Very Satisfied |
0.5 | Satisfied |
-10.5 | Dissatisfied |
-Infinity | Annoyed |
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 Setting | Increment | Decrement |
---|---|---|
Low | 3 | 1 |
Normal | 2 | 1 |
High | 1 | 2 |
Unused | 1 | 1 |
At 0x006736A0
there is a table that contains for every difficulty the base satisfaction for every population type:
Needs Setting | Rich | Wealthy | Poor |
---|---|---|---|
Low | -7 | -12 | -20 |
Normal | -13 | -18 | -27 |
High | -20 | -25 | -32 |
Within update_citizen_satisfaction
6 situational modifiers are implemented:
Situation | Impact |
---|---|
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:
Ware | Rich | Wealthy | Poor |
---|---|---|---|
Grain | 2 | 4 | 8 |
Meat | 5 | 4 | 4 |
Fish | 2 | 6 | 6 |
Beer | 2 | 6 | 6 |
Salt | 2 | 2 | 4 |
Honey | 3 | 2 | 0 |
Spices | 3 | 0 | 0 |
Wine | 5 | 2 | 0 |
Cloth | 5 | 4 | 0 |
Skins | 3 | 2 | 0 |
WhaleOil | 3 | 4 | 4 |
Timber | 3 | 4 | 6 |
IronGoods | 2 | 2 | 0 |
Leather | 2 | 2 | 4 |
Wool | 2 | 6 | 4 |
Pitch | 0 | 0 | 0 |
PigIron | 0 | 0 | 0 |
Hemp | 0 | 0 | 0 |
Pottery | 3 | 2 | 4 |
Bricks | 0 | 0 | 0 |
Sword | 0 | 0 | 0 |
Bow | 0 | 0 | 0 |
Crossbow | 0 | 0 | 0 |
Carbine | 0 | 0 | 0 |
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:
Ware | Base Price | Base Price per Barrel/Bundle |
---|---|---|
Grain | 0.055000003 | 110.0 |
Meat | 0.47855002 | 957.1 |
Fish | 0.22005001 | 440.1 |
Beer | 0.17399999 | 34.8 |
Salt | 0.1425 | 28.45 |
Honey | 0.55000001 | 110.0 |
Spices | 1.4 | 280.0 |
Wine | 1.1 | 220.0 |
Cloth | 1.034 | 206.8 |
Skins | 3.3824999 | 676.5 |
WhaleOil | 0.41249999 | 82.5 |
Timber | 0.027500002 | 55.0 |
IronGoods | 1.278 | 255.6 |
Leather | 1.12 | 224.0 |
Wool | 0.44000003 | 880.0 |
Pitch | 0.278 | 55.6 |
PigIron | 0.44000003 | 880.0 |
Hemp | 0.22000001 | 440.0 |
Pottery | 0.85499996 | 171.0 |
Bricks | 0.039900005 | 79.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.
Interval | Bounds |
---|---|
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\) |
---|---|---|
0 | 4 | 2.5 |
1 | 1.5 | 0.5 |
2 | 1.0 | 0.2 |
3 | 0.8 | 0.2 |
Example
Let's assume we buy pig iron from a town with the following thresholds:
Threshold | Value |
---|---|
t0 | 20000 |
t1 | 60000 |
t2 | 70000 |
t3 | 80000 |
If we buy one bundle (2000), the resulting prices at different stock levels would be:
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))
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.
Interval | Bounds |
---|---|
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\) |
---|---|---|
0 | NaN | 1.4 |
1 | 1.4 | 0.4 |
2 | 1.0 | 0.3 |
3 | 0.7 | 0.2 |
and \(d_{trade\_difficulty}\) is defined as:
Difficulty | Value |
---|---|
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:
Threshold | Value |
---|---|
t0 | 20000 |
t1 | 60000 |
t2 | 70000 |
t3 | 80000 |
If we sell one bundle (2000), the resulting prices at different stock levels would be:
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:
Type | Quality Level 0 | Quality Level 1 | Quality Level 2 | Quality Level 3 |
---|---|---|---|---|
Snaikka | 15 | 19 | 23 | 25 |
Craier | 28 | 31 | 34 | 35 |
Cog | 45 | 48 | 52 | 55 |
Hulk | 55 | 59 | 65 | 70 |
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):
Type | QL | Timber | Cloth | Hemp | Pitch | Iron Goods | Unknown | Base Price | Unknown |
---|---|---|---|---|---|---|---|---|---|
Snaikka | 0 | 7 | 3 | 3 | 3 | 20 | 17 | 7,650 | 11,414 |
Snaikka | 1 | 9 | 3 | 3 | 3 | 20 | 20 | 8,200 | 12,074 |
Snaikka | 2 | 11 | 3 | 3 | 3 | 20 | 24 | 8,800 | 12,784 |
Craier | 0 | 12 | 5 | 5 | 5 | 30 | 29 | 18,260 | 24,450 |
Craier | 1 | 14 | 5 | 5 | 5 | 30 | 32 | 18,720 | 25,010 |
Craier | 2 | 16 | 5 | 5 | 5 | 30 | 34 | 19,890 | 26,290 |
Cog | 0 | 18 | 3 | 4 | 4 | 40 | 46 | 16,560 | 22,296 |
Cog | 1 | 20 | 3 | 4 | 4 | 40 | 50 | 16,500 | 22,346 |
Cog | 2 | 22 | 3 | 4 | 4 | 40 | 53 | 17,490 | 23,446 |
Hulk | 0 | 30 | 16 | 16 | 8 | 50 | 58 | 22,968 | 34,442 |
Hulk | 1 | 33 | 16 | 16 | 8 | 50 | 64 | 23,040 | 34,579 |
Hulk | 2 | 36 | 16 | 16 | 8 | 50 | 69 | 24,840 | 36,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:
Employees | Shipyard |
---|---|
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:
Setting | Factor |
---|---|
Very Low | 8 |
Low | 9 |
Normal | 10 |
High | 11 |
Very High | 12 |
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:
Rank | Safe Repayment Sum |
---|---|
Shopkeeper | 5000 |
Trader | 10000 |
Merchant | 15000 |
Travelling Merchant | 20000 |
Councillor | 25000 |
Patrician | 30000 |
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:
Rank | Bribe Base Factor |
---|---|
Shopkeeper | 0 |
Trader | 1 |
Merchant | 2 |
Travelling Merchant | 3 |
Councillor | 5 |
Patrician | 7 |
Mayor | 10 |
Alderman | 15 |
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:
Rank | Min | Max |
---|---|---|
Shopkeeper | 8000 | 13000 |
Trader | 10000 | 15000 |
Merchant | 12000 | 17000 |
Travelling Merchant | 14000 | 19000 |
Councillor | 18000 | 23000 |
Patrician | 22000 | 27000 |
Mayor | 28000 | 33000 |
Alderman | 38000 | 43000 |
Result
The result can be identified by the councillor's response:
Result | Response |
---|---|
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:
Status | Response |
---|---|
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 Type | Swords | Bows | Crossbows | Carbines | Trebuchets |
---|---|---|---|---|---|
0 | 262 | 131 | 99 | 0 | 55 |
2 | 262 | 131 | 0 | 87 | 55 |
3 | 262 | 0 | 99 | 87 | 55 |
The attacking squads are then enforced to be below or equal to the following values defined at 0x0067B604
:
Squad | Limit |
---|---|
Swords | 40 |
Bows | 22 |
Crossbows | 18 |
Carbines | 15 |
Trebuchets | 6 |
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 Reputation | Rank |
---|---|
5 | Trader |
7.5 | Merchant |
10 | Travelling Merchant |
15 | Councillor |
25 | Patrician |
Minimum Company Value | Rank |
---|---|
100,000 | Trader |
200,000 | Merchant |
300,000 | Travelling Merchant |
500,000 | Councillor |
900,000 | Patrician |
However, to actually reach the next rank, the following reputation values must be reached:
Minimum Reputation | Rank |
---|---|
7 | Trader |
12 | Merchant |
20 | Travelling Merchant |
40 | Councillor |
60 | Patrician |
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
:
Rent | Reputation Factor |
---|---|
None | 1.0 |
Low | 0.62 |
Normal | 0.42 |
High | 0.23 |
Very High | 0.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.
Name | Value | Location |
---|---|---|
base_rep_factor | 1.0 | Merchant |
church_factor | 0.0 | GameWorld |
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:
Interest | Factor |
---|---|
Very Low | 4.0 |
Low | 3.0 |
Normal | 2.0 |
High | 1.0 |
Very High | 0.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 Type | Impact |
---|---|
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
.
Value | Direction |
---|---|
0x00 | North |
0x40 | East |
0x80 | South |
0xc0 | West |
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
:
Ship | Point 0 | Point 1 | Point 2 | Point 3 | Point 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 Line | Location |
---|---|
Point 4 to Point 0 | Left |
Point 3 to Point 4 | Left |
Point 0 to Point 1 | Right |
Point 1 to Point 2 | Right |
Point 2 to Point 3 | Random |
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 Type | Damage 1 | Damage 2 | Damage Reduction |
---|---|---|---|
Small Catapult | 32 | 60 | 480 |
Small Ballista | 32 | 80 | 160 |
Large Catapult | 77 | 60 | 480 |
Large Ballista | 77 | 80 | 160 |
Bombard | 96 | 90 | 120 |
Cannon | 58 | 90 | 120 |
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
.
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:
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:
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 Type | Text |
---|---|
5 | You have been seen behaving indecently. |
6 | You are accused of heresy. |
7 | You have stated that the world is round. |
8 | You 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 Type | Content |
---|---|
0 | Severe 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. |
1 | You 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. |
2 | Several witnesses have sworn that they recognised your ship %s during the recent pirates attack, and that it was flying the pirate's flag. |
3 | A 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. |
4 | A 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. |
8 | You stand accused of taking part in a punishable offence, undermining the good of the Hanseatic League. |
9 | Several 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. |
10 | Several 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. |
11 | Several 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. |
12 | Several 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. |
13 | Several witnesses to a recent pirate attack on %s have sworn that they recognised your ship %s displaying a pirate flag run up the mast. |
14 | Several 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. |
15 | Several 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
- Download the latest release
- Move
p3_modloader.dll
andPatrician3_modloader.exe
into your Patrician3 folder - Create a folder called
mods
- Move all mods into that folder
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 forp3_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 at0x0054B14F
only if the lock was acquired. This can be achieved by replacing the twomov
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 at0x0054B13F
. This can be achieved by replacing themov
instruction with a call instruction to a proper unlock function and making thejnz
instruction target the next instruction after the call. Thecmp
instruction above it must be moved below it to ensure it always happens, sojnz
must point to the movedcmp
.
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 at0x0054B210
only if the lock was acquired. This can be achieved by replacing the twomov
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 at0x0054B200
. This can be achieved by replacing themov
instruction with a call instruction to a proper unlock function and making thejnz
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
and0x0054B063
must unlock the pending operations. This can be achieved by replacing the respectivemov
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 at0x0054BCD9
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 and
ed 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:
Price | Amount | Direction |
---|---|---|
0 | Negative | Ship -> Office |
0 | Positive | Office -> Ship |
Positive | Positive | Ship -> Town |
Negative | Positive | Town -> 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)
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)
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")
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.