Skip to content

Commit 9cd8240

Browse files
committed
hello world town hall
1 parent 4d36c64 commit 9cd8240

File tree

18 files changed

+668
-13
lines changed

18 files changed

+668
-13
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ members = [
1414
"mod-scrollmap-render-all-ships",
1515
"mod-shipyard-details",
1616
"mod-tavern-show-all-sailors",
17+
"mod-town-hall-details",
1718
#"p3-agent",
1819
"p3-agent-loader",
1920
"p3-aim",
@@ -29,9 +30,12 @@ exclude = [
2930
version = "0.1.6"
3031

3132
[workspace.dependencies]
32-
hooklet = "^0.1.5"
33+
#hooklet = "^0.1.5"
34+
hooklet = { path = "../hooklet" }
3335
clap = { version = "4.3.0", features = ["derive"] }
3436
simple_logger = "4.1"
3537
log = "0.4"
3638
sysinfo = "0.29"
3739
win_dbg_logger = "0.1"
40+
num-traits = "0.2"
41+
num-derive = "0.3"

mod-shipyard-details/src/ffi.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,10 @@ pub unsafe extern "thiscall" fn shipyard_window_open_hook() {
7171
let class48 = Class48Ptr::new();
7272
class48.set_ignore_below_gradient(0);
7373
class48.set_gradient_y(200);
74-
class48.clip_stuff();
7574
}
7675

7776
#[no_mangle]
7877
pub unsafe extern "thiscall" fn shipyard_rendering_hook() -> i32 {
79-
debug!("shipyard_rendering_hook");
8078
let window = UIShipyardWindowPtr::new();
8179
let town_index = window.get_town_index();
8280
let selected_page = window.get_selected_page();

mod-town-hall-details/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "mod-town-hall-details"
3+
edition = "2021"
4+
version.workspace = true
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
name="town_hall_details"
9+
10+
[dependencies]
11+
log = { workspace = true }
12+
win_dbg_logger = { workspace = true }
13+
p3-api = { path = "../p3-api" }
14+
hooklet = { workspace = true }
15+
num-traits = { workspace = true }
16+
num-derive = { workspace = true }
17+
18+
[dependencies.windows]
19+
version = "0.48"
20+
features = [
21+
"Win32_Foundation",
22+
"Win32_System_Memory",
23+
]

mod-town-hall-details/src/ffi.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use std::{
2+
arch::global_asm,
3+
ffi::{c_void, CStr, CString},
4+
ptr,
5+
sync::atomic::{AtomicPtr, Ordering},
6+
};
7+
8+
use hooklet::{FunctionPointerHook, X86Rel32Type};
9+
use log::debug;
10+
use p3_api::{
11+
data::{class48::Class48Ptr, ddraw_set_constant_color, ddraw_set_text_mode, ui_render_text_at},
12+
game_world::GAME_WORLD_PTR,
13+
missions::alderman_missions::{AldermanMissionDataPtr, FoundTownPtr},
14+
scheduled_tasks::{
15+
scheduled_task::{ScheduledTaskData, ScheduledTaskPtr},
16+
SCHEDULED_TASKS_PTR,
17+
},
18+
ui::{
19+
font::{self, get_normal_font},
20+
ui_town_hall_window::UITownHallWindowPtr,
21+
},
22+
};
23+
use windows::core::PCSTR;
24+
25+
pub static TOWN: &CStr = c"Town";
26+
pub static TASK_DUE_IN: &CStr = c"Rescheduling in";
27+
pub static EFFECTIVE_PRODUCTION: &CStr = c"Effective Production";
28+
29+
const TOWN_HALL_WINDOW_OPEN_POINTER_OFFSET: u32 = UITownHallWindowPtr::VTABLE_OFFSET + 0x120;
30+
pub static TOWN_HALL_WINDOW_OPEN_HOOK_PTR: AtomicPtr<FunctionPointerHook> = AtomicPtr::new(std::ptr::null_mut());
31+
32+
const LOAD_TOWN_HALL_SELECTED_PAGE_PATCH_ADDRESS: u32 = 0x005E09AC;
33+
static LOAD_TOWN_HALL_SELECTED_PAGE_CONTINUATION: u32 = 0x005E09B2;
34+
35+
#[no_mangle]
36+
pub unsafe extern "C" fn start() -> u32 {
37+
let _ = log::set_logger(&win_dbg_logger::DEBUGGER_LOGGER);
38+
log::set_max_level(log::LevelFilter::Trace);
39+
40+
debug!("Hooking town hall's open function through vtable");
41+
match hooklet::hook_function_pointer(
42+
PCSTR::from_raw(ptr::null()),
43+
TOWN_HALL_WINDOW_OPEN_POINTER_OFFSET,
44+
town_hall_open_hook as usize as u32,
45+
) {
46+
Ok(hook) => {
47+
debug!("Hook {hook:?} set");
48+
TOWN_HALL_WINDOW_OPEN_HOOK_PTR.store(Box::into_raw(Box::new(hook)), Ordering::SeqCst);
49+
}
50+
Err(_) => {
51+
return 1;
52+
}
53+
}
54+
55+
debug!("Detouring town hall's rendering function at the selected page switch");
56+
if hooklet::deploy_rel32_raw(
57+
LOAD_TOWN_HALL_SELECTED_PAGE_PATCH_ADDRESS as _,
58+
(&load_town_hall_selected_page_detour) as *const _ as _,
59+
X86Rel32Type::Jump,
60+
)
61+
.is_err()
62+
{
63+
return 2;
64+
}
65+
66+
0
67+
}
68+
69+
#[no_mangle]
70+
pub unsafe extern "thiscall" fn town_hall_open_hook(ui_town_hall_window_address: u32) {
71+
crate::handle_open(ui_town_hall_window_address)
72+
}
73+
74+
#[no_mangle]
75+
pub unsafe extern "thiscall" fn town_hall_rendering_hook() -> i32 {
76+
crate::handle_selected_page_switch()
77+
}
78+
79+
pub unsafe fn render_aldermans_office_modifications(window: &UITownHallWindowPtr) {
80+
let next_mission_index = window.get_next_mission_index();
81+
if next_mission_index == 0 {
82+
return;
83+
}
84+
85+
let selected_mission_index = window.get_selected_alderman_mission_index();
86+
if selected_mission_index == 0xff {
87+
return;
88+
}
89+
let task_index = window.get_task_index(selected_mission_index);
90+
let task = SCHEDULED_TASKS_PTR.get_scheduled_task(task_index);
91+
let data = match task.get_data() {
92+
Some(e) => e,
93+
None => return,
94+
};
95+
96+
let mission = match data {
97+
ScheduledTaskData::AldermanMission(mission) => mission,
98+
_ => return,
99+
};
100+
match mission.get_data() {
101+
AldermanMissionDataPtr::FoundTownPtr(ptr) => render_aldermans_office_modifications_found_town(window, &task, &ptr),
102+
AldermanMissionDataPtr::OverlandTradeRoute(_ptr) => {}
103+
AldermanMissionDataPtr::NotoriousPirate(_ptr) => {}
104+
AldermanMissionDataPtr::PirateHideout(_ptr) => {}
105+
AldermanMissionDataPtr::SupplyProblems(_ptr) => {}
106+
}
107+
}
108+
109+
unsafe fn render_aldermans_office_modifications_found_town(window: &UITownHallWindowPtr, task: &ScheduledTaskPtr, data: &FoundTownPtr) {
110+
let town = data.get_town();
111+
let effective = data.get_production_effective();
112+
let class48 = Class48Ptr::new();
113+
class48.set_ignore_below_gradient(0);
114+
class48.set_gradient_y(0);
115+
116+
ddraw_set_constant_color(0xff000000);
117+
ddraw_set_text_mode(2);
118+
font::ddraw_set_font(get_normal_font());
119+
let x = window.get_x();
120+
let mut y = window.get_y() + 60;
121+
122+
ui_render_text_at(x + 120, y, TASK_DUE_IN.to_bytes());
123+
let due_in = task.get_due_timestamp() - GAME_WORLD_PTR.get_game_time_raw();
124+
let task_due_in_cstring = CString::new(format!("{due_in}")).unwrap();
125+
ui_render_text_at(x + 420, y, task_due_in_cstring.to_bytes());
126+
y += 20;
127+
128+
ui_render_text_at(x + 120, y, TOWN.to_bytes());
129+
let town_cstring = CString::new(format!("{town:?}")).unwrap();
130+
ui_render_text_at(x + 420, y, town_cstring.to_bytes());
131+
y += 20;
132+
133+
ui_render_text_at(x + 120, y, EFFECTIVE_PRODUCTION.to_bytes());
134+
let mut effective_string = String::new();
135+
for facility in effective {
136+
effective_string.push_str(&format!("{facility:?}, "));
137+
}
138+
effective_string.pop();
139+
effective_string.pop();
140+
let effective_cstring = CString::new(effective_string).unwrap();
141+
ui_render_text_at(x + 420, y, effective_cstring.to_bytes());
142+
}
143+
144+
extern "C" {
145+
static load_town_hall_selected_page_detour: c_void;
146+
}
147+
148+
global_asm!("
149+
.global {load_town_hall_selected_page_detour}
150+
{load_town_hall_selected_page_detour}:
151+
# save regs
152+
push ecx
153+
push edx
154+
155+
call {town_hall_rendering_hook}
156+
157+
# restore regs
158+
pop edx
159+
pop ecx
160+
161+
jmp [{continuation}]
162+
",
163+
load_town_hall_selected_page_detour = sym load_town_hall_selected_page_detour,
164+
town_hall_rendering_hook = sym town_hall_rendering_hook,
165+
continuation = sym LOAD_TOWN_HALL_SELECTED_PAGE_CONTINUATION);

mod-town-hall-details/src/lib.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::{
2+
ffi::{CStr, CString},
3+
mem,
4+
sync::atomic::Ordering,
5+
};
6+
7+
use log::debug;
8+
use num_traits::cast::FromPrimitive;
9+
use p3_api::{
10+
data::{class48::Class48Ptr, ddraw_set_constant_color, ddraw_set_text_mode, enums::WareId, fill_p3_string, render_window_title, ui_render_text_at},
11+
game_world::GAME_WORLD_PTR,
12+
ui::{font, ui_town_hall_sidemenu::UITownHallSidemenuPtr, ui_town_hall_window::UITownHallWindowPtr},
13+
};
14+
15+
pub(crate) mod ffi;
16+
17+
pub static TITLE: &CStr = c"Details";
18+
pub static STOCK: &CStr = c"Stock";
19+
pub static CONSUMPTION: &CStr = c"Consumption";
20+
pub static RATIO: &CStr = c"Ratio";
21+
22+
#[derive(Debug, Default)]
23+
struct HanseaticWareData {
24+
total_wares: i32,
25+
total_consumption: i32,
26+
ware: usize,
27+
}
28+
29+
impl HanseaticWareData {
30+
fn get_ratio(&self) -> i32 {
31+
if self.total_consumption >= 1024 {
32+
self.total_wares / (self.total_consumption >> 10)
33+
} else {
34+
0
35+
}
36+
}
37+
}
38+
39+
pub(crate) unsafe fn handle_open(ui_town_hall_window_address: u32) {
40+
debug!("town_hall_open_hook");
41+
let orig_address = (*ffi::TOWN_HALL_WINDOW_OPEN_HOOK_PTR.load(Ordering::SeqCst)).old_absolute;
42+
let orig: extern "thiscall" fn(aa1: u32) = mem::transmute(orig_address);
43+
orig(ui_town_hall_window_address);
44+
45+
let class48 = Class48Ptr::new();
46+
class48.set_ignore_below_gradient(0);
47+
class48.set_gradient_y(0);
48+
}
49+
50+
pub(crate) unsafe fn handle_selected_page_switch() -> i32 {
51+
let window = UITownHallWindowPtr::new();
52+
let sidemenu = UITownHallSidemenuPtr::default();
53+
let class48 = Class48Ptr::new();
54+
let selected_page = window.get_selected_page();
55+
56+
if selected_page == -1 {
57+
let mut hanse_data: [HanseaticWareData; 20] = Default::default();
58+
let towns_count = GAME_WORLD_PTR.get_towns_count();
59+
60+
for town_index in 0..towns_count {
61+
let town = GAME_WORLD_PTR.get_town(town_index as _);
62+
let stock = town.get_storage().get_wares();
63+
let consumption_citizens = town.get_daily_consumptions_citizens();
64+
let consumption_businesses = town.get_storage().get_daily_consumptions_businesses();
65+
for ware in 0..20 {
66+
hanse_data[ware].total_wares += stock[ware];
67+
hanse_data[ware].total_consumption += consumption_citizens[ware];
68+
hanse_data[ware].total_consumption += consumption_businesses[ware];
69+
hanse_data[ware].ware = ware;
70+
}
71+
72+
let mut office_index = town.get_first_office_index();
73+
while office_index < GAME_WORLD_PTR.get_offices_count() {
74+
let office = GAME_WORLD_PTR.get_office(office_index);
75+
let office_stock = office.get_storage().get_wares();
76+
let office_consumption = office.get_storage().get_daily_consumptions_businesses();
77+
for ware in 0..20 {
78+
hanse_data[ware].total_wares += office_stock[ware];
79+
hanse_data[ware].total_consumption += office_consumption[ware];
80+
}
81+
office_index = office.next_office_in_town_index();
82+
}
83+
}
84+
//hanse_data.sort_by(|a, b| a.get_ratio().cmp(&b.get_ratio()));
85+
hanse_data.sort_by_key(|a| a.get_ratio());
86+
87+
let mut title_p3_string: u32 = 0;
88+
fill_p3_string((&mut title_p3_string) as *mut _ as _, TITLE.to_bytes());
89+
render_window_title(title_p3_string as _, window.address as _);
90+
91+
ddraw_set_constant_color(0xff000000);
92+
ddraw_set_text_mode(2);
93+
font::ddraw_set_font(font::get_normal_font());
94+
let x = window.get_x() + 80;
95+
let mut y = window.get_y() + 60;
96+
97+
font::ddraw_set_font(font::get_header_font());
98+
ui_render_text_at(x + 80, y, STOCK.to_bytes());
99+
ui_render_text_at(x + 160, y, CONSUMPTION.to_bytes());
100+
ui_render_text_at(x + 220, y, RATIO.to_bytes());
101+
y += 20;
102+
103+
for data in &hanse_data {
104+
let ware = WareId::from_usize(data.ware).unwrap();
105+
ui_render_text_at(x, y, CString::new(format!("{ware:?}")).unwrap().to_bytes());
106+
ui_render_text_at(x + 80, y, CString::new(format!("{}", data.total_wares)).unwrap().to_bytes());
107+
ui_render_text_at(x + 160, y, CString::new(format!("{}", data.total_consumption)).unwrap().to_bytes());
108+
if data.total_consumption >= 1024 {
109+
let ratio = data.get_ratio();
110+
ui_render_text_at(x + 220, y, CString::new(format!("{}", ratio)).unwrap().to_bytes());
111+
}
112+
y += 20;
113+
}
114+
} else if selected_page == 7 {
115+
// The gradient changes should be done before content rendering is done, but this works for now
116+
sidemenu.set_window_needs_redraw();
117+
class48.set_ignore_below_gradient(0);
118+
class48.set_gradient_y(0);
119+
ffi::render_aldermans_office_modifications(&window);
120+
}
121+
122+
selected_page
123+
}

p3-api/src/data/enums.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ pub enum WareId {
3232

3333
#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, EnumString, FromPrimitive)]
3434
#[repr(u16)]
35-
pub enum ProductionId {
35+
pub enum FacilityId {
3636
Militia = 0x00,
37+
Shipyard = 0x01,
38+
Construction = 0x02,
3739
Weaponsmith = 0x03,
3840
HuntingLodge = 0x04,
3941
FishermansHouse = 0x05,
@@ -129,6 +131,16 @@ pub enum BuildingId {
129131
Mint = 0x67,
130132
}
131133

134+
#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, FromPrimitive)]
135+
#[repr(u8)]
136+
pub enum AldermanMissionType {
137+
FoundTown = 0x00,
138+
OverlandTradeRoute = 0x01,
139+
NotoriousPirate = 0x02,
140+
PirateHideout = 0x03,
141+
SupplyProblems = 0x04,
142+
}
143+
132144
#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, FromPrimitive)]
133145
#[repr(u8)]
134146
// Extended from https://github.com/Sqoops/PatrizierKartenEditor

p3-api/src/data/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ pub fn fill_p3_string(p3_string: *const c_void, input: &[u8]) {
110110
function(p3_string, input.as_ptr() as _)
111111
}
112112

113-
pub fn ui_render_text_at(x: i32, y: i32, text: &[u8]) {
114-
let function: extern "cdecl" fn(x: i32, y: i32, text: *const c_void) = unsafe { mem::transmute(0x004BB3E0) };
113+
pub unsafe fn ui_render_text_at(x: i32, y: i32, text: &[u8]) {
114+
let function: extern "cdecl" fn(x: i32, y: i32, text: *const c_void) = mem::transmute(0x004BB3E0);
115115
function(x, y, text.as_ptr() as _)
116116
}
117117

p3-api/src/data/office.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl OfficePtr {
2020
unsafe { self.get(0x2c4) }
2121
}
2222

23-
pub fn next_office_id(&self) -> u16 {
23+
pub fn next_office_in_town_index(&self) -> u16 {
2424
unsafe { self.get(0x2ca) }
2525
}
2626
}

0 commit comments

Comments
 (0)