From c5d04746ac8d9b5bdeb398ab9beb53fe7c238547 Mon Sep 17 00:00:00 2001 From: Ha Luu Date: Sat, 15 Aug 2026 20:37:12 -0500 Subject: [PATCH] Add ToC-418 mobile-friendly large icons (Steam) for touchscreen play Large companion confirm/cancel buttons on vanilla popups and big up/down scroll arrows on the city build-picker, gated by Mobile_Friendly_Icons. Pure click translators: presses are redirected to the tiny vanilla controls, so vanilla behavior is unchanged. Missing art = silent stand-down. Co-Authored-By: Claude Fable 5 --- .../ToC_Mobile_Buttons_City_Screen-Small.pcx | Bin 0 -> 2305 bytes .../ToC_Mobile_Buttons_City_Screen.pcx | Bin 0 -> 4115 bytes .../ToC_Mobile_Buttons_Popup_Screen-Small.pcx | Bin 0 -> 2897 bytes .../ToC_Mobile_Buttons_Popup_Screen.pcx | Bin 0 -> 5429 bytes C3X.h | 56 +++ civ_prog_objects.csv | 2 + default.c3x_config.ini | 9 + injected_code.c | 463 ++++++++++++++++++ 8 files changed, 530 insertions(+) create mode 100644 Art/interface/ToC_Mobile_Buttons_City_Screen-Small.pcx create mode 100644 Art/interface/ToC_Mobile_Buttons_City_Screen.pcx create mode 100644 Art/interface/ToC_Mobile_Buttons_Popup_Screen-Small.pcx create mode 100644 Art/interface/ToC_Mobile_Buttons_Popup_Screen.pcx diff --git a/Art/interface/ToC_Mobile_Buttons_City_Screen-Small.pcx b/Art/interface/ToC_Mobile_Buttons_City_Screen-Small.pcx new file mode 100644 index 0000000000000000000000000000000000000000..263d4860f4eb5e6686ad28ea8844afb83a6e17d3 GIT binary patch literal 2305 zcmai$e@t7~6~|w;tjP)vHa5XcAkHrq|HPyrU<^sXb_h)f5MunGWf5KIV2I+TK?@?p zVxt)_)0Is9k!nnGx+;rQQqfIKsg|Zb_ul8=(AZIGEmcD@q*93o5sAAZQ|R)46KW@M5EQc$hg1C|hEvL=kd}ZwAta$e z?qM0B(y~%ErUAc$t9&|`jnvuf+5ZCqrhl0Q8d_$xJRD$^_OPuYJ9U5Rv1C@^P&mMt zEcqC=me+@7iUUKo@PqUGfh%EhX}egP)oya2`i!{Xz8XTZiuO+RlLY@A#k0zWDJ z^>X*1Gzm!RFvALFa>H4yyk8l1H+A)CB>LhfEc*RD(Ac&R$K7rh@ zg!K)y%R-hr`*xd1s??ewdr` z<3j}32(L`#cNndDtH*4*>Nj^8AZqL+DaI?|(vueUriDZ=O?s(?bnxyB-+`NWOD!;i ziK5A3cBODSS;(#wOUWkyr-hxYuqx>T$zuB+JbLTQ1s$Xr&Sf1z`i>!rH$FPZ&h7u~y~>Y9oV5l+<<@Pxb$L72ZLwr++N#v8T)7f|M7~@2?#}#dEdK4a znXiMf>z{oUy2yfmxcvEhfBIzj{Q2(D(bm4cy7u;}#>VpMYFl}^aeKk~O*%zp##Cufzh6A@*WY)<-W8?aw9wQ0|J>j7@iFJv@x$-5 z?+&yU-0{pj^0O<(M*A1|u^U0(g*=E7cGZii0MY*17i6uS+I?Rn2^ zTmSw3e$$Dc>$_gfd*$fn<|duHF6Tg{)=`#K@_c52A!C~^J$9peGT3rxIdKV-x#E&$&*YIW#mFG+>#`GQPRU217Gm*GF8XNBp2HfLs zm%ZI*?r$$RQr9~?aJZ-2)L5%`?8+%D$kgi;*{c?(T0R)I3oj?XY|HB1nRP&yF%erD zos9Q<7;7J4&3%`vyC+<&fg7)B&v`P2_U3dJW;d9!f;W~voJb6fe$(AO)%Mb-?wa>^ z?m8|!5R`FYF|qsh^|f$uo$%m6>PQO0CE-#MFG8|aX%pl>E%?wcpZ)ShI~i|Pj-A!U Ig(Tnq180V-tN;K2 literal 0 HcmV?d00001 diff --git a/Art/interface/ToC_Mobile_Buttons_City_Screen.pcx b/Art/interface/ToC_Mobile_Buttons_City_Screen.pcx new file mode 100644 index 0000000000000000000000000000000000000000..b1102ff4ebdda69872dec868570cf1f2d8ca7a27 GIT binary patch literal 4115 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 literal 0 HcmV?d00001 diff --git a/Art/interface/ToC_Mobile_Buttons_Popup_Screen-Small.pcx b/Art/interface/ToC_Mobile_Buttons_Popup_Screen-Small.pcx new file mode 100644 index 0000000000000000000000000000000000000000..c0a6fae1e0a0f301c3514007100400aa9845f17b GIT binary patch literal 2897 zcmai0eNYo;8efzuLL!NRa*E;?l8@aGs-i7^prE!IJ|w$YY!yke%SVdIMo}YWI@FI- z=OE!`daa#uGurDo&Qx!>yE*I4N#uPuAt(wCWX?_{)R|+Z+>L)YQ#%Ym`@GNZ`MtmAW0U$?QVIoEDYc3c;O|wJ@`{T}+C?Q%JE_&La{NE*iqfZ0>Kj&0 zn4%mS^x6h2w$o@Z${CYoWtNewooO3jY+kbFNX80<6~@5HgASvY9x#|;{Wo|Ja|T*g z%mxSi>oB5(X}~~(#Ss}a(5ee>GN3^-O3-LbPN3m_xN+ir1`X2=1I=Rvea|xn2aOW> ztys176T1bC(P+?Mh7AywH!%*TwI__e#bbBBMPu+8?y)#%Vn*L%o}tkY8VQ#punnm8 za$;vpGw!tSaAO@7>-?e=6a96fB-X#@Q_T?G#Q5H8l(2~F?uhlWDAgzV_@6w{FdA#D z6J<`k%V9wl>mkN4v68rGSX&P!NxZ{}(tjI)Gq4I{k?R-Os36G#7B$$Sy73Ge(tt&< zp0r9a!5>DGDFTPZ9t-}FF38x5#Xcd%3X&ehFgT~ZZ=7~E^rT@?kOD^nXe7qoKITM| z64=Sf$qoqY1AP*pkSGgm%#PWG+Yv^HzyO6|G$botvLj98tbqz#Q&AwvtUuh0-7Z3e zAc40}g(^b{FG0crYiFZ&ctbISa1cldb!jN!E0Yy8>_drEEC!B7Y64c+bNe^|VYjnI zkXm7jO-0(v0}z~f;bz(Ga-tC{q2uMz z(9alkfDwZzdcq^LCYU*mk}oS39*w0*py#oJHHUC*zpHZwRPBpdh~l53sE-Lk6|xUd5EKWtzdyFHVY?QAj#91P_K^H`Ft>2s-! zAX}^yK89@NY5+3-ncRAmFye+h$zrX^m8 zkQ_vKjugZp6bHp=)enRR9$-&UlAdcct3HwqQ%-Ye&@rX-y zokti{QZXF)KES}Fo~5;+Y}pQV z4GHkBgn^g$$O>=# zPz!ST(TEG;$5;sO4nsGIJzkSX^~4^gM;3=kKuZ&NFdj5CpA5G&a3IqShf~Ojdf>>@ z9!=2fXJE{7uo~q^pK%e-9sipoeTp`>;s#?Nt`U!k>NHo1e$o7@isyPu@Me^ z7lt-vp{HHV3`KBZ&~s6k$}p>w7<$Gp-(xhxLj^|Ppcm$EDjKuU1|JF8WMF)0^HQ;Q z&OOLRY8tH1aK#?pXGWuF&|<^}Y(#_oaL;z2Q5uc%>T$_wJU+*4VjO^b78t<;jH)~+ z0Rmy(gC5Pg4?~h?cm^P4#m9T1y!U=0CCYo4=~?>$%u3kq!wKq68Mc+da~HnJ>Wqas z904OR87jMHgJ)f$&sPQketwE;u6?-g^VWTzo~Q~PsXBYO>Vx|7fNPKcz-|v;BJA6C z&{o9n+E}@L-P=Xhl0tg3)$n%HmeRU6i)#yu-dR`3<`c9Wq@z3ig{_%TH{9`^Etd-C1`AggWKl)3$j@p0Mu=SF# z@!`#j4=RMsdXSr49R;_QzS?0;n9x!TcM(tLkwjg&--qP7^ zZRQil4c^1KqI=bW{W)x9_TF7tJ2q#&@%jR*F+DFw6MxWrv$ytQx2?HrQ*}H2+0U!dAaoLZ}**VFF$>5XUnPLqisdLwsoEsb3=Ys{{6FS0oo?Wxh1?kBGu)%tC`{tZ^Qu}Z&*)fd|JuUF^`Ds=gkI;JAsST;X* z@BCFI^H&tjUtXxqwrYQ6(k@<;wjfKR(Uq>ytXnvDMarD+iCdlVYt6lv*lQnc{-~91 zYp~W-wp=)E4>X&rD-1ifWUsd_Fyv?#&;6ml_VWvGQcmS|eP+vs%)L43-SMf;o8y5m z;tlPQ1Fg4g&E2KKr3X^hWk>q?o!L$67jx#!-Um}(bWfb`{JOchzy967vgMy{*m9J5 zPU*&}A11awy1xirUqU^9KK)WC>IQW~fe%U%beo{g;es!1>fNS(WZfJWbX^y-#wq9n GO8E~?WQBnM literal 0 HcmV?d00001 diff --git a/Art/interface/ToC_Mobile_Buttons_Popup_Screen.pcx b/Art/interface/ToC_Mobile_Buttons_Popup_Screen.pcx new file mode 100644 index 0000000000000000000000000000000000000000..ca6b1c2a2242042d6a448c4204a078721f47c620 GIT binary patch literal 5429 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 the feature silently stands down (mainline-C3X-safe). + enum init_state toc418_city_btns_state; + Sprite toc418_city_btns[2]; + enum init_state toc418_popup_btns_state; + Sprite toc418_popup_btns[2]; + // Live POPUP registry — parent GUI_Form_1-derived forms whose embedded + // OK_Btn/Cancel_Btn were seen drawing (filled by patch_Button_m22_Draw, read by the click + // remap; entries validated at click time against the self-calibrated Button-class vtable + // and cleared when stale, so a destroyed stack popup can never redirect a click). + // Per-popup data: the popup's live screen origin, calibrated by the + // GUI_Form_1_process_mouse_hover inlead (client mouse minus popup-local hover coords — + // no form field or canvas rect stores this: control canvases are DETACHED buffers whose + // abs rect is (0,0,W,H)), plus which twins have actually been DRAWN (a button's Status2 + // is only 5 while it is itself drawing, so shown-ness cannot be read from outside — the + // remap must only accept twins whose sprites really exist). + void * toc418_live_par[4]; + int toc418_par_ox[4]; + int toc418_par_oy[4]; + bool toc418_par_cal[4]; + bool toc418_par_drawn[4][2]; + void * toc418_button_vtable; // self-calibrated Button-class vtable (set by patch_Button_m22_Draw) + // Runtime per-object vtable swap on the city build-order window + // (p_city_form->Order_Queue_Dialog) so the big queue arrows draw AFTER the window's own + // draw (the window composites over anything painted from patch_City_Form_draw). The + // copy lives here (writable is-> memory); slot 22 points at toc418_dlg_draw and every + // other slot forwards to the original class vtable, which is remembered for re-install + // detection (a re-run constructor restores the class vtable; the next city draw re-swaps). + void * toc418_dlg_vtbl[128]; + void * toc418_dlg_orig_vtbl; + void * toc418_dlg_orig_draw; + // ToC-418: minimal WndProc subclass host (mouse position store + click-remap host; this + // branch has no other WndProc subclass, so ToC-418 carries its own). The subclass stays + // installed across exit-to-menu — proven safe. Field names kept as toc109_* to match the + // originating ToC branch, where ToC-109 owns the shared subclass. + int toc109_mouse_x; // last client-frame mouse position + int toc109_mouse_y; + bool toc109_wndproc_installed; + void * toc109_orig_wndproc; // original game WndProc, chained via CallWindowProcA + LONG (WINAPI * toc109_SetWindowLongA) (HWND, int, LONG); + LRESULT (WINAPI * toc109_CallWindowProcA) (void *, HWND, UINT, WPARAM, LPARAM); + HWND (WINAPI * toc109_GetActiveWindow) (void); + BOOL (WINAPI * toc109_GetClientRect) (HWND, RECT *); + // End ToC-418 + // ========== // } // ========== diff --git a/civ_prog_objects.csv b/civ_prog_objects.csv index cda10b17..db4ecd59 100644 --- a/civ_prog_objects.csv +++ b/civ_prog_objects.csv @@ -1007,3 +1007,5 @@ ignore, 0x4B9F60, 0x4C15D0, 0x4B9FF0, "City_add_population", "void (__fast ignore, 0x670234, 0x68D2E0, 0x670234, "Tile_m27_Check_Shield_Bonus", "bool (__fastcall *) (Tile * this)" ignore, 0x5f3448, 0x6032DF, 0x5F3378, "CHECK_SHIELD_BONUS_TO_CAN_SPAWN_RES_RETURN", "int" +repl vptr, 0x66572C, 0x682780, 0x66572C, "Button_m22_Draw", "void (__fastcall *) (Button * this)" +inlead, 0x4A5D60, 0x4A5D60, 0x4A5D60, "GUI_Form_1_process_mouse_hover", "void (__fastcall *) (Base_Form * this, int edx, int local_x, int local_y)" diff --git a/default.c3x_config.ini b/default.c3x_config.ini index 55784c4c..2cb4ed2b 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -373,6 +373,15 @@ patch_failure_to_find_new_city_build = true patch_passengers_out_of_order_on_menu = true patch_empty_army_combat_crash = true +; STEAM ONLY: Mobile-friendly big buttons for touchscreen play. Draws large companion confirm/cancel +; buttons beside the tiny checkbox-sized pair on vanilla popups, and large up/down scroll arrows +; inside the city screen's build-picker window. Pressing a big button simply redirects the click +; to the tiny vanilla control, so vanilla behavior is unchanged. Requires the art files +; ToC_Mobile_Buttons_Popup_Screen.pcx and ToC_Mobile_Buttons_City_Screen.pcx in the mod's +; Art/interface folder; if one is missing, that surface silently disables itself. +; Default: False , Set to True for Mobile Friendly Icons -- Currently works for the STEAM version ONLY. +Mobile_Friendly_Icons = true + ; At the beginning of each AI player's turn, deletes any units of theirs that are not on a valid tile. This addresses a rare crash that can occur when ; the unit AI invokes the pathfinder for an off-map unit. I don't know how AI units can end up off the map in the first place. delete_off_map_ai_units = true diff --git a/injected_code.c b/injected_code.c index e9d41b08..4a799000 100644 --- a/injected_code.c +++ b/injected_code.c @@ -19850,6 +19850,10 @@ patch_init_floating_point () {"patch_division_by_zero_in_ai_alliance_eval" , true , offsetof (struct c3x_config, patch_division_by_zero_in_ai_alliance_eval)}, {"patch_empty_army_movement" , true , offsetof (struct c3x_config, patch_empty_army_movement)}, {"patch_empty_army_combat_crash" , true , offsetof (struct c3x_config, patch_empty_army_combat_crash)}, + // ToC-418 add - Mobile-friendly big buttons (large popup confirm/cancel pair + city build-picker + // scroll arrows for touchscreen play). Key deliberately has no ToC_ prefix (upstreamable). + {"Mobile_Friendly_Icons" , false, offsetof (struct c3x_config, mobile_friendly_icons)}, + // END ToC-418 {"patch_premature_truncation_of_found_paths" , true , offsetof (struct c3x_config, patch_premature_truncation_of_found_paths)}, {"patch_zero_production_crash" , true , offsetof (struct c3x_config, patch_zero_production_crash)}, {"patch_ai_can_form_army_without_special_ability" , true , offsetof (struct c3x_config, patch_ai_can_form_army_without_special_ability)}, @@ -22953,6 +22957,443 @@ init_district_icons () pcx.vtable->destruct (&pcx, __, 0); } +// ToC-418: MOBILE-FRIENDLY BIG BUTTONS (config: Mobile_Friendly_Icons — deliberately no ToC_ +// prefix, intended for upstreaming to mainline C3X; default false). +// Vanilla button ART is replaceable but the HIT RECTS are hard-coded in the exe, so instead of +// resizing anything the big buttons are pure CLICK TRANSLATORS: pressing one redirects the +// mouse message to the tiny vanilla control's own coordinates, and vanilla does everything +// else. Two surfaces: +// 1. City screen: two 40x40 arrows at the bottom of the build-queue panel, redirecting to +// the queue scrollbar's up/down arrows. +// 2. Vanilla popups (GUI_Form_1 family): a big confirm + cancel pair drawn on the popup, +// redirecting to OK_Btn / Cancel_Btn (their Base_Data rects give live positions). +// Art: ToC_Mobile_Buttons_City_Screen.pcx (83x42, 2 cells of 40x40, 1px grid, magenta +// transparency) + ToC_Mobile_Buttons_Popup_Screen.pcx (103x52, 2 cells of 50x50). Missing +// art = feature silently stands down. +#define TOC418_CITY_BTN_SZ 40 +#define TOC418_POPUP_BTN_SZ 50 + +// ToC-418: lazily load + slice both button sheets (once per process; missing file latches IS_INIT_FAILED). +static void +init_toc418_mobile_buttons () +{ + struct { + enum init_state * st; Sprite * spr; char * file; int sz; char * errmsg; + } t418_sheets[2] = { + { &is->toc418_city_btns_state, is->toc418_city_btns, "interface/ToC_Mobile_Buttons_City_Screen.pcx", TOC418_CITY_BTN_SZ, + "[C3X] ToC-418: ToC_Mobile_Buttons_City_Screen.pcx not found - city-screen big arrows disabled.\n" }, + { &is->toc418_popup_btns_state, is->toc418_popup_btns, "interface/ToC_Mobile_Buttons_Popup_Screen.pcx", TOC418_POPUP_BTN_SZ, + "[C3X] ToC-418: ToC_Mobile_Buttons_Popup_Screen.pcx not found - popup big buttons disabled.\n" }, + }; + for (int si = 0; si < 2; si++) { + if (*t418_sheets[si].st != IS_UNINITED) + continue; + *t418_sheets[si].st = IS_INIT_FAILED; + PCX_Image pcx; + PCX_Image_construct (&pcx); + char temp_path[2*MAX_PATH]; + get_mod_art_path (t418_sheets[si].file, temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image == NULL) + (*p_OutputDebugStringA) (t418_sheets[si].errmsg); + else { + int sz = t418_sheets[si].sz; + for (int b = 0; b < 2; b++) { + Sprite_construct (&t418_sheets[si].spr[b]); + Sprite_slice_pcx (&t418_sheets[si].spr[b], __, &pcx, 1 + b * (sz + 1), 1, sz, sz, 1, 1); + } + *t418_sheets[si].st = IS_OK; + } + pcx.vtable->destruct (&pcx, __, 0); + } +} + +// ToC-418: ALL geometry comes from LIVE vanilla control rects, identification is by OBJECT IDENTITY, +// and screen positions come from hover calibration (each earlier guess — hardcoded offsets, +// ControlID fingerprints, Base_Form Left/Top as a screen origin — was disproven in playtests). + +// ToC-418: the popup registry slot for a parent form (shared by the Button draw hook, the +// hover-calibration inlead, and the click remap). add=true claims a slot (slot 0 evicted +// when full) and RESETS the per-popup calibration + drawn flags. +static int +toc418_par_slot (void * par, bool add) +{ + for (int n = 0; n < 4; n++) + if (is->toc418_live_par[n] == par) + return n; + if (! add) + return -1; + int slot = 0; + for (int n = 0; n < 4; n++) + if (is->toc418_live_par[n] == NULL) { slot = n; break; } + is->toc418_live_par[slot] = par; + is->toc418_par_cal[slot] = false; + is->toc418_par_drawn[slot][0] = is->toc418_par_drawn[slot][1] = false; + return slot; +} + +// ToC-418: a structurally valid checkbox-class button: vtable checked against the self-calibrated +// Button-class vtable (a stale or garbage object can never pass), checkbox-sized, not +// parked at the canvas origin (template junk). NO Status2 test: a button's Status2 is +// 5 only WHILE IT IS ITSELF DRAWING (each button of a pair always sees its partner as != 5, +// flapping the layout between pair and single spacing and stamping ghost twins at both +// positions). Visibility is instead conveyed by the fact that a button DRAWS (its own m22 +// wrap fires) — see toc418_par_drawn. +static bool +toc418_btn_is_active (Button * b) +{ + if ((is->toc418_button_vtable == NULL) || ((void *) b->vtable != is->toc418_button_vtable)) + return false; + int W = b->Base_Data.Right - b->Base_Data.Left, H = b->Base_Data.Bottom - b->Base_Data.Top; + if ((W < 12) || (W > 30) || (H < 12) || (H > 30)) + return false; + if ((b->Base_Data.Left <= 2) && (b->Base_Data.Top <= 2)) + return false; + return true; +} + +// ToC-418: twin layout for one popup form (any GUI_Form_1-derived class — the global PopupForm and +// every stack-allocated popup embed their confirm/cancel as OK_Btn/Cancel_Btn at the same +// offsets). The big pair sits SIDE BY SIDE immediately right of the vanilla pair +// ([OK][X] [big-OK][big-X]), flipping to its left when the popup edge is too close, +// vertically centered on the vanilla buttons. All coordinates are parent-canvas-local — +// the frame the button rects live in (proven: popup Base_Form Left/Top hold border offsets +// like (3,35), NOT a screen origin — a "-parent origin" shift pushes every twin 35px up). +// Returns false when neither button is usable. +static bool +toc418_popup_pair_layout (GUI_Form_1 * g, bool * out_ok_shown, bool * out_x_shown, + int * out_ok_bx, int * out_x_bx, int * out_by) +{ + bool ok_shown = toc418_btn_is_active (&g->OK_Btn); + bool x_shown = toc418_btn_is_active (&g->Cancel_Btn); + // A cancel wildly far from the confirm means the Cancel_Btn slot read garbage (a class + // that embeds only an OK): trust the pair only when both look like one control group. + if (ok_shown && x_shown) { + int dt = g->Cancel_Btn.Base_Data.Top - g->OK_Btn.Base_Data.Top; + int dx = g->Cancel_Btn.Base_Data.Left - g->OK_Btn.Base_Data.Right; + if ((dt < -30) || (dt > 30) || (dx < -80) || (dx > 80)) + x_shown = false; + } + if (! ok_shown && ! x_shown) + return false; + PCX_Image * cv = &g->Base_Data.Canvas; + if (cv->JGL.Image == NULL) + return false; + int cw = cv->JGL.Image->vtable->m54_Get_Width (cv->JGL.Image); + int ch = cv->JGL.Image->vtable->m55_Get_Height (cv->JGL.Image); + if ((cw < 80) || (ch < 60)) + return false; + int L = 0x7FFFFFFF, T = 0x7FFFFFFF, R = -0x7FFFFFFF, B = -0x7FFFFFFF; + if (ok_shown) { + Base_Form_Data * d = &g->OK_Btn.Base_Data; + if (d->Left < L) L = d->Left; if (d->Top < T) T = d->Top; + if (d->Right > R) R = d->Right; if (d->Bottom > B) B = d->Bottom; + } + if (x_shown) { + Base_Form_Data * d = &g->Cancel_Btn.Base_Data; + if (d->Left < L) L = d->Left; if (d->Top < T) T = d->Top; + if (d->Right > R) R = d->Right; if (d->Bottom > B) B = d->Bottom; + } + int n418 = (ok_shown && x_shown) ? 2 : 1; + int need = n418 * TOC418_POPUP_BTN_SZ + (n418 - 1) * 6; + int bx0 = (R + 8 + need <= cw - 2) ? (R + 8) // right of the vanilla pair + : (L - 8 - need); // popup edge too close: left of it + if (bx0 < 2) + bx0 = 2; + int by = (T + B) / 2 - TOC418_POPUP_BTN_SZ / 2 + 2; // slight downward bias + if (by > ch - TOC418_POPUP_BTN_SZ - 2) by = ch - TOC418_POPUP_BTN_SZ - 2; + if (by < 2) by = 2; + *out_ok_shown = ok_shown; + *out_x_shown = x_shown; + *out_ok_bx = bx0; + *out_x_bx = (ok_shown && x_shown) ? bx0 + TOC418_POPUP_BTN_SZ + 6 : bx0; + *out_by = by; + return true; +} + +// ToC-418: geometry for big city queue arrow i (0 = up, 1 = down) in DIALOG-LOCAL coordinates (the +// build-order window composites itself AFTER the city form draw, so the arrows are drawn on +// ITS canvas from toc418_dlg_draw — anything painted from patch_City_Form_draw gets covered +// while the window is open). Also outputs the dialog-local remap targets: the window's tiny +// scrollbar arrows at its right edge. Gate: city form shown AND dialog Status2 == 5 +// (5 = open, 4 = closed — each click on the current-build box toggles it; a {4,5} gate +// shows the arrows on a CLOSED window). +static bool +toc418_city_arrow_geometry (int i, int * out_bx, int * out_by, int * out_vcx, int * out_vcy) +{ + City_Form * cf = p_city_form; + if ((cf == NULL) || (cf->Base.Data.Status2 != 5)) + return false; + Base_Form_Data * dd = &cf->Order_Queue_Dialog.Data; + if (dd->Status2 != 5) + return false; + int W = dd->Right - dd->Left, H = dd->Bottom - dd->Top; + if ((W < 60) || (H < 100)) + return false; // sane window rect only + *out_bx = W - (2 - i) * (TOC418_CITY_BTN_SZ + 6) - 14; + *out_by = H - TOC418_CITY_BTN_SZ - 6; + *out_vcx = W - 13; + // Down target H-21 NOT H-8: up presses at local (W-13,72) scroll fine, but the scroll + // column ends above the frame — H-8 lands on the window's bottom BORDER a few px below + // the vanilla down-arrow and does nothing. + *out_vcy = (i == 0) ? 72 : H - 21; + return true; +} + +// ToC-418: the build-order window's origin in the wndproc client frame = its own Base_Data rect +// corner (proven visually: arrows drawn at dialog-local (bx,by) appear on screen at exactly +// (Left+bx, Top+by)). NOT the canvas abs rect — control canvases are DETACHED buffers +// whose rect is (0,0,W,H); a canvas-derived origin of (0,0) kills every city arrow click. +static void +toc418_city_dlg_origin (int * out_px, int * out_py) +{ + Base_Form_Data * dd = &p_city_form->Order_Queue_Dialog.Data; + *out_px = dd->Left; + *out_py = dd->Top; +} + +// ToC-418: the click translator. Returns true and fills the redirect target when (x,y) lies inside a +// ToC-418 big button that is currently on screen. Called from the wndproc for L-button +// down/up/dblclk; the caller rewrites the message's lparam so vanilla handles the click at +// the tiny control's own coordinates. +static bool +toc418_remap_click (int x, int y, int * out_tx, int * out_ty) +{ + if (! is->current_config.mobile_friendly_icons || (p_bic_data == NULL)) + return false; + // City queue arrows: dialog-local geometry translated to the client frame by the + // window's live rect origin. + if ((is->toc418_city_btns_state == IS_OK) && (p_city_form != NULL)) + for (int i = 0; i < 2; i++) { + int bx, by, vcx, vcy; + if (! toc418_city_arrow_geometry (i, &bx, &by, &vcx, &vcy)) + continue; + int px, py; + toc418_city_dlg_origin (&px, &py); + if (x >= px + bx && x < px + bx + TOC418_CITY_BTN_SZ && + y >= py + by && y < py + by + TOC418_CITY_BTN_SZ) { + *out_tx = px + vcx; *out_ty = py + vcy; + return true; + } + } + // Popup twins hit-test against the LIVE-POPUP REGISTRY filled by patch_Button_m22_Draw + // (any GUI_Form_1-derived popup, global or stack-allocated). Each entry is re-validated + // live — embedded-button vtable identity, sane geometry — and cleared when validation + // fails, so a destroyed stack popup can never redirect a click. Twin rects come from the + // SAME layout function the draw uses (popup-local frame), translated to the client frame + // by the popup's HOVER-CALIBRATED origin (client mouse minus popup-local hover coords, + // captured by the GUI_Form_1_process_mouse_hover inlead — no form field or canvas rect + // stores the popup's screen position; control canvases are detached (0,0) buffers). A + // twin is only accepted if its sprite was actually drawn (toc418_par_drawn). + if (is->toc418_popup_btns_state == IS_OK) + for (int n = 0; n < 4; n++) { + GUI_Form_1 * g = (GUI_Form_1 *) is->toc418_live_par[n]; + if (g == NULL) + continue; + bool oks, xs; + int obx, xbx, tby; + if (! toc418_popup_pair_layout (g, &oks, &xs, &obx, &xbx, &tby)) { + is->toc418_live_par[n] = NULL; + continue; + } + if (! is->toc418_par_cal[n]) + continue; // no origin yet — the mouse has not moved over this popup + int px = is->toc418_par_ox[n], py = is->toc418_par_oy[n]; + for (int k = 0; k < 2; k++) { + if (! is->toc418_par_drawn[n][k]) + continue; + if ((k == 0) ? ! oks : ! xs) + continue; + Button * b = (k == 0) ? &g->OK_Btn : &g->Cancel_Btn; + int sx = px + ((k == 0) ? obx : xbx), sy = py + tby; + if (x >= sx && x < sx + TOC418_POPUP_BTN_SZ && + y >= sy && y < sy + TOC418_POPUP_BTN_SZ) { + *out_tx = px + (b->Base_Data.Left + b->Base_Data.Right) / 2; + *out_ty = py + (b->Base_Data.Top + b->Base_Data.Bottom) / 2; + return true; + } + } + } + return false; +} + +// ToC-418: minimal WndProc subclass (this branch has no other WndProc subclass, so ToC-418 +// carries its own; state fields keep their toc109_* names from the originating ToC branch, +// where ToC-109 owns the shared subclass). It does ONLY three things: store the mouse +// position on WM_MOUSEMOVE (feeds hover calibration), remap L-button messages that land on +// a big button, and chain EVERYTHING to the original game WndProc via CallWindowProcA. +// The subclass stays installed across exit-to-menu — proven safe. +static LRESULT WINAPI +toc109_wndproc (HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) +{ + if (msg == 0x0200) { // WM_MOUSEMOVE + // Must run BEFORE chaining to the engine, so that when the engine's dispatch of this + // same mousemove reaches GUI_Form_1_process_mouse_hover, the stored client position + // and the popup-local arguments describe the SAME mouse event. + is->toc109_mouse_x = (int)(short)(lparam & 0xFFFF); + is->toc109_mouse_y = (int)(short)((lparam >> 16) & 0xFFFF); + } else if ((msg == 0x0201) || (msg == 0x0203) || (msg == 0x0202)) { // WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP + // ToC-418: presses inside a big button are redirected to the tiny vanilla control's + // own coordinates BEFORE anything else sees the message. The RELEASE must land on + // the same redirected coordinates as the press, or the vanilla control never + // registers a completed click — that's why WM_LBUTTONUP is remapped too. + int t418_tx, t418_ty; + if (toc418_remap_click ((int)(short)(lparam & 0xFFFF), (int)(short)((lparam >> 16) & 0xFFFF), &t418_tx, &t418_ty)) + lparam = ((unsigned)(t418_ty & 0xFFFF) << 16) | (unsigned)(t418_tx & 0xFFFF); + } + return is->toc109_CallWindowProcA (is->toc109_orig_wndproc, hwnd, msg, wparam, lparam); +} + +// ToC-418: install the WndProc subclass (idempotent, gated on toc109_wndproc_installed). +// Called both from patch_Button_m22_Draw (covers a FRESH LAUNCH: pregame popups like the +// "Saved Game" info box on load would otherwise draw dead twins) and from toc418_dlg_draw +// (covers the city arrows even if no popup has ever been shown). Only subclasses the +// window whose client rect equals ScreenWidth x ScreenHeight — i.e. the game window. +static void +toc418_try_install_wndproc () +{ + if (is->toc109_wndproc_installed) + return; + if (is->toc109_SetWindowLongA == NULL) { + HMODULE t418_u32 = (*p_GetModuleHandleA) ("user32.dll"); + if (t418_u32) { + is->toc109_SetWindowLongA = (void *) (*p_GetProcAddress) (t418_u32, "SetWindowLongA"); + is->toc109_CallWindowProcA = (void *) (*p_GetProcAddress) (t418_u32, "CallWindowProcA"); + is->toc109_GetActiveWindow = (void *) (*p_GetProcAddress) (t418_u32, "GetActiveWindow"); + is->toc109_GetClientRect = (void *) (*p_GetProcAddress) (t418_u32, "GetClientRect"); + } + } + if (is->toc109_SetWindowLongA != NULL && is->toc109_CallWindowProcA != NULL && + is->toc109_GetActiveWindow != NULL && is->toc109_GetClientRect != NULL && + (p_bic_data != NULL)) { + HWND t418_hwnd = is->toc109_GetActiveWindow (); + RECT t418_rc; + if (t418_hwnd != NULL && is->toc109_GetClientRect (t418_hwnd, &t418_rc) && + t418_rc.right == p_bic_data->ScreenWidth && t418_rc.bottom == p_bic_data->ScreenHeight) { + is->toc109_orig_wndproc = (void *) is->toc109_SetWindowLongA (t418_hwnd, -4, (LONG) toc109_wndproc); + if (is->toc109_orig_wndproc != NULL) is->toc109_wndproc_installed = true; + } + } +} + +// ToC-418: the wrapped draw for the city build-order window, installed by a runtime per-object +// vtable swap in patch_City_Form_draw (this window is a static singleton embedded in +// City_Form; slot 22 = draw per the Base_Form-derived decimal rule). Drawing right after +// the window's own draw guarantees the arrows sit ON TOP of it. +static void __fastcall +toc418_dlg_draw (Base_Form * this) +{ + ((void (__fastcall *) (Base_Form *)) is->toc418_dlg_orig_draw) (this); + if (! is->current_config.mobile_friendly_icons || + (p_city_form == NULL) || (this != &p_city_form->Order_Queue_Dialog)) + return; + init_toc418_mobile_buttons (); + if (is->toc418_city_btns_state != IS_OK) + return; + toc418_try_install_wndproc (); // city arrows need the click remap even if no popup was ever shown + Base_Form_Data * dd = &this->Data; + int W = dd->Right - dd->Left; + // Adaptive local origin for DRAWING on this canvas: window-sized canvas -> (0,0); + // client-frame canvas -> the window's own rect corner. + int ox = 0, oy = 0; + if (dd->Canvas.JGL.Image != NULL) { + int cw = dd->Canvas.JGL.Image->vtable->m54_Get_Width (dd->Canvas.JGL.Image); + if (cw > W + 60) { ox = dd->Left; oy = dd->Top; } + } + for (int i = 0; i < 2; i++) { + int bx, by, vx, vy; + if (toc418_city_arrow_geometry (i, &bx, &by, &vx, &vy)) + Sprite_draw (&is->toc418_city_btns[i], __, &dd->Canvas, ox + bx, oy + by, NULL); + } +} +// End ToC-418 core + +// ToC-418: draw big popup twins from the Button's OWN class draw. Buttons ARE +// Base_Form-derived — slot 22 = m22_Draw — and a button draws AFTER its owning form, so +// drawing the big twin right after the vanilla button guarantees correct z-order and live +// geometry, and the coordinate frame is automatically the one the button itself just drew +// in. The wrap runs for EVERY button in the game: one bool test of overhead, then bail. +void __fastcall +patch_Button_m22_Draw (Button * this) +{ + Button_m22_Draw (this); + + if (! is->current_config.mobile_friendly_icons) + return; + + // Self-calibrating Button-class identity (used by the remap to validate registry entries + // without a hardcoded per-install vtable address). + is->toc418_button_vtable = (void *) this->vtable; + + // Recognize confirm/cancel by OBJECT IDENTITY, never ControlID (the same checkbox pair + // carries ids (0,-1) on the pre-game setup screens but (-1,-2) in-game — the id is some + // per-form counter, not a role). Every vanilla popup class — the global PopupForm + // included — derives from GUI_Form_1, which embeds the pair as OK_Btn/Cancel_Btn at + // fixed offsets, so a button whose ADDRESS equals &parent->OK_Btn IS the confirm + // (pointer arithmetic only, no reads of unproven memory). + Base_Form * t418_par = this->Base_Data.Parent; + if (t418_par == NULL) + return; + GUI_Form_1 * t418_g = (GUI_Form_1 *) t418_par; + int t418_kind; + if (this == &t418_g->OK_Btn) t418_kind = 0; + else if (this == &t418_g->Cancel_Btn) t418_kind = 1; + else + return; + if (! toc418_btn_is_active (this)) + return; + init_toc418_mobile_buttons (); + if (is->toc418_popup_btns_state != IS_OK) + return; + + // Register the PARENT popup for the click remap and the hover calibration. + int t418_slot = toc418_par_slot ((void *) t418_par, true); + + // Draw this button's big twin on the PARENT form's canvas (the button's own canvas is + // button-sized). Coordinates are parent-canvas-local, the same frame the button rects + // live in: NO origin shift. Layout membership is STRUCTURAL (no Status2), so the + // positions are stable across every draw — no pair/single flapping ghosts; a hidden + // partner simply never draws its own twin. + bool t418_oks, t418_xs; + int t418_obx, t418_xbx, t418_by; + if (! toc418_popup_pair_layout (t418_g, &t418_oks, &t418_xs, &t418_obx, &t418_xbx, &t418_by)) + return; + if ((t418_kind == 0) ? ! t418_oks : ! t418_xs) + return; + int t418_bx = (t418_kind == 0) ? t418_obx : t418_xbx; + Sprite_draw (&is->toc418_popup_btns[t418_kind], __, &t418_par->Data.Canvas, t418_bx, t418_by, NULL); + is->toc418_par_drawn[t418_slot][t418_kind] = true; + + // On a FRESH LAUNCH nothing else may have installed the WndProc subclass (click remap + + // mouse capture for hover calibration), so PREGAME popups (the "Saved Game" info box on + // load) would draw dead twins. Install it from here too; idempotent. + toc418_try_install_wndproc (); +} +// End ToC-418 popup pair + +// ToC-418: hover-calibration inlead on the popups' SHARED m27_process_mouse_hover (one +// implementation for the whole GUI_Form_1 family, global PopupForm included). The engine +// calls it with POPUP-LOCAL coordinates (it forwards them straight to the popup's +// local-rect row hit-tests), while our wndproc has just stored the same message's +// CLIENT-frame mouse position — the difference IS the popup's live screen origin, which no +// form field or canvas rect provides (control canvases are detached (0,0) buffers). +// Calibration is continuous, so drag-moved popups stay correct. Only popups already +// registered by the Button draw wrap are tracked. +void __fastcall +patch_GUI_Form_1_process_mouse_hover (Base_Form * this, int edx, int local_x, int local_y) +{ + if (is->current_config.mobile_friendly_icons && is->toc109_wndproc_installed) { + int t418_slot = toc418_par_slot ((void *) this, false); + if (t418_slot >= 0) { + is->toc418_par_ox[t418_slot] = is->toc109_mouse_x - local_x; + is->toc418_par_oy[t418_slot] = is->toc109_mouse_y - local_y; + is->toc418_par_cal[t418_slot] = true; + } + } + GUI_Form_1_process_mouse_hover (this, __, local_x, local_y); +} +// End ToC-418 hover calibration + void __fastcall patch_City_Form_draw (City_Form * this) { @@ -22969,6 +23410,28 @@ patch_City_Form_draw (City_Form * this) City_Form_draw (this); + // ToC-418: the big build-queue scroll arrows are NOT drawn here — the build-order + // window (Order_Queue_Dialog) composites AFTER this draw and covers them whenever it + // is open. Instead this hook INSTALLS a per-object vtable swap on that window, + // wrapping its own draw (slot 22) with toc418_dlg_draw so the arrows land on top. + // The copy lives in is-> (writable); a re-run constructor restores the class vtable + // and the next city draw simply re-installs — self-healing, no csv row needed. + if (is->current_config.mobile_friendly_icons) { + init_toc418_mobile_buttons (); + if (is->toc418_city_btns_state == IS_OK) { + Base_Form * t418_dlg = &this->Order_Queue_Dialog; + void * t418_cur = *(void **) t418_dlg; + if ((t418_cur != NULL) && (t418_cur != (void *) is->toc418_dlg_vtbl)) { + memcpy (is->toc418_dlg_vtbl, t418_cur, sizeof is->toc418_dlg_vtbl); + is->toc418_dlg_orig_vtbl = t418_cur; + is->toc418_dlg_orig_draw = is->toc418_dlg_vtbl[22]; + is->toc418_dlg_vtbl[22] = (void *) toc418_dlg_draw; + *(void **) t418_dlg = (void *) is->toc418_dlg_vtbl; + } + } + } + // End ToC-418 + if (is->current_config.show_detailed_city_production_info) { City * city = this->CurrentCity; int order_type = city->Body.Order_Type,