Spellforce-Spell-framework
Loading...
Searching...
No Matches
sf_campaign_hook.c
Go to the documentation of this file.
1
5
6#include "../sf_wrappers.h"
7#include "../sf_ui_wrappers.h"
8#include "sf_campaign_hook.h"
9#include "../sf_hooks.h"
10
11#include "sf_prepare_new_game.h"
12
13#include <stdint.h>
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17#include <direct.h>
18#include <sys/stat.h>
19
20static gameInfoSetMapPathFull_ptr s_gameinfo_set_map_path; // deprecated I think
21static appMenuEnterCampaignFlow_ptr s_enter_campaign_flow;
22
23static videoSequenceStop_ptr s_video_sequence_stop;
24
25static void hook_getsavepath();
26
27static avatarInternalCopy_ptr s_avatar_internal_copy;
28static avatarVectorsCopy_ptr s_avatar_vectors_copy;
29static void hook_initfirstmap();
30
31// used in prepare new game
36
37typedef LPCSTR *(__cdecl *GetDataStorageLocation_ptr)(char **param_1, uint32_t type);
38typedef void (__thiscall *prepareTransition_ptr)(CAppMenu *_this,uint32_t param_1,uint32_t param_2);
39typedef SF_GameInfo *(__thiscall *initDefaultInfo_ptr)(SF_GameInfo *_this);
40typedef void (__thiscall *AC82_Zero_ptr)(AutoClass82 *_this);
41typedef void (__thiscall *some_vector_fun_ptr)(void *_this, uint32_t *size);
42typedef uint32_t (__thiscall *CMnuBaseFlagGetter_ptr)(CMnuBase *_this);
43typedef void (__thiscall *CUtlCallBackInit_ptr)(void *_this, void *param1, void *param2, uint32_t param3);
44typedef void (__thiscall *CUiGameZero_ptr)(void *_this);
45typedef void (__thiscall *CUiGame_009a1fd0_ptr)(void *_this, uint32_t param1);
46
47
48static GetDataStorageLocation_ptr s_getDataStorageLocation;
55typedef SF_String *(__thiscall *AC95_get_figure_name_ptr)(void *AC95, SF_String *name_buffer, uint32_t figure_id);
56
58
62
63// Declared in sf_campaign_hook.h so the core modules can validate their assets.
65
66static void hook_qs_load();
67static void hook_check_dirs();
68static void hook_ql_helper();
69
70static uint32_t s_ql_return_ok;
71static uint32_t s_ql_return_fail;
72static uint32_t s_hdr_result_ok;
74{
75
76 s_getDataStorageLocation = (GetDataStorageLocation_ptr)(ASI::AddrOf(0x1ee9f0));
77 s_gameinfo_set_map_path = (gameInfoSetMapPathFull_ptr)(ASI::AddrOf(0x1762d0));
78 s_enter_campaign_flow = (appMenuEnterCampaignFlow_ptr)(ASI::AddrOf(0x1936a0));
80 s_video_sequence_stop = (videoSequenceStop_ptr)(ASI::AddrOf(0x3d7b90));
82
84
85 s_avatar_internal_copy = (avatarInternalCopy_ptr)(ASI::AddrOf(0x1759e0));
86 s_avatar_vectors_copy = (avatarVectorsCopy_ptr)(ASI::AddrOf(0x175860));
87
91 AC82_Zero = (AC82_Zero_ptr)(ASI::AddrOf(0x19e730));
92 s_getDataStorageLocation = (GetDataStorageLocation_ptr)(ASI::AddrOf(0x1ee9f0));
95 some_vector_init = (some_vector_fun_ptr)(ASI::AddrOf(0x3264d0)); //FUN_007264d0
97
101
102 s_ql_return_ok = ASI::AddrOf(0x5ef663);
103 s_ql_return_fail = ASI::AddrOf(0x5ef74c);
104 s_hdr_result_ok = ASI::AddrOf(0x5fb864);
105 hook_initfirstmap();
106 hook_getsavepath();
107 hook_qs_load();
108 hook_check_dirs();
109 hook_ql_helper();
110
112
113 /* Campaigns themselves are registered by the Custom Campaign Module from
114 * sfsf\campaigns\*.json - see initialize_campaign_module(). */
115}
116
118uint32_t g_campaign_count = 0;
121
123{
124 if (g_campaign_count >= SFSF_MAX_CAMPAIGNS || def == NULL)
125 {
126 log_error("Campaign registry full or NULL definition");
127 return -1;
128 }
129
131 if (g_campaigns[g_campaign_count].avatar_type == -1)
133
134 log_info("Registered campaign %u: %s (type %u, map: %s, saves: %s)",
137 def->start_map, def->campaign_folder);
138 return (int32_t)g_campaign_count++;
139}
140
141/* Un'Schtalch's code block - updated and reworked a bit for you ~Muddykat*/
142
143int isDirectoryExists(const char *path)
144{
145 struct stat stats;
146 if (stat(path, &stats) == 0)
147 {
148 return S_ISDIR(stats.st_mode);
149 }
150 return 0;
151}
152
154{
155 SF_String base_dirs[5];
156 char *paths[3];
157 SF_String base_path;
158 SF_String back_slash;
159
160 uiAPI.SFStringConstructor_char(&back_slash, "\\");
161 uiAPI.SFStringConstructor_char(&base_dirs[0], "save");
162 uiAPI.SFStringConstructor_char(&base_dirs[1], "temp");
163 uiAPI.SFStringConstructor_char(&base_dirs[2], "char");
164 uiAPI.SFStringConstructor_char(&base_dirs[3], "campaign2");
165 uiAPI.SFStringConstructor_char(&base_dirs[4], "campaign3");
166
167 s_getDataStorageLocation(paths, 0);
168 uiAPI.SFStringConstructor_char(&base_path, paths[0]);
169
170 uiAPI.SFStringConcat(&base_path, &base_dirs[0]);
171 char *real_path = uiAPI.SFStringCMbStr(&base_path);
172 if (!isDirectoryExists(real_path))
173 {
174 _mkdir(real_path);
175 }
176
177 //vanilla campaigns
178 for (int i = 1; i < 5; i++)
179 {
180 SF_String temp;
181 uiAPI.SFStringConstructor_char(&temp, real_path);
182 uiAPI.SFStringConcat(&temp, &back_slash);
183 uiAPI.SFStringConcat(&temp, &base_dirs[i]);
184 char *temp_path = uiAPI.SFStringCMbStr(&temp);
185 if (!isDirectoryExists(temp_path))
186 {
187 _mkdir(temp_path);
188 }
189 uiAPI.SFStringDestructor(&temp);
190 }
191
192 for (uint32_t i = 0; i < g_campaign_count; i++)
193 {
194 const SFSF_CampaignDef *custom = NULL;
195 custom = &g_campaigns[i];
196 if (custom != NULL)
197 {
198 SF_String temp;
199 SF_String camp_folder;
200 uiAPI.SFStringConstructor_char(&temp, real_path);
201 uiAPI.SFStringConstructor_char(&camp_folder, custom->campaign_folder);
202
203 uiAPI.SFStringConcat(&temp, &back_slash);
204 uiAPI.SFStringConcat(&temp, &camp_folder);
205 char *temp_path = uiAPI.SFStringCMbStr(&temp);
206 if (!isDirectoryExists(temp_path))
207 {
208 _mkdir(temp_path);
209 }
210 uiAPI.SFStringDestructor(&temp);
211 uiAPI.SFStringDestructor(&camp_folder);
212
213 }
214
215 }
216
217 for (int i = 0; i < 5; i++)
218 {
219 uiAPI.SFStringDestructor(&base_dirs[i]);
220 }
221 uiAPI.SFStringDestructor(&base_path);
222 uiAPI.SFStringDestructor(&back_slash);
223}
224
225
226SF_String * __thiscall getSavePath(CAppSession *_this, SF_String *output, uint32_t campaign_type)
227{
228 char *paths[3];
229 SF_String base_save;
230 s_getDataStorageLocation(paths, 0);
231 uiAPI.SFStringConstructor_char(output, paths[0]);
232 uiAPI.SFStringConstructor_wchar(&base_save, L"save\\");
233 uiAPI.SFStringConcat(output, &base_save);
234 uiAPI.SFStringDestructor(&base_save);
235
236 switch (campaign_type)
237 {
238 case 0:
239 {
240 //don't need to do anything
241 break;
242 }
243 case 1:
244 {
245 SF_String campaign_path;
246 uiAPI.SFStringConstructor_wchar(&campaign_path, L"campaign2\\");
247 uiAPI.SFStringConcat(output, &campaign_path);
248 uiAPI.SFStringDestructor(&campaign_path);
249
250 break;
251 }
252 case 2:
253 {
254 SF_String campaign_path;
255 uiAPI.SFStringConstructor_wchar(&campaign_path, L"campaign3\\");
256 uiAPI.SFStringConcat(output, &campaign_path);
257 uiAPI.SFStringDestructor(&campaign_path);
258 break;
259 }
260 default:
261 {
262 //place to add for custom campaign stuff
263 //log_info("Loading into getSavePath");
264 const SFSF_CampaignDef *custom = NULL;
265 int custom_idx = (int)campaign_type - SFSF_CAMPAIGN_TYPE_BASE;
266 if (custom_idx >= 0 && custom_idx < (int32_t)g_campaign_count)
267 {
268 custom = &g_campaigns[custom_idx];
269 }
270
271 /* Unknown campaign type: fall back to the base save folder rather
272 * than dereferencing a campaign that was never registered. */
273 if (custom == NULL)
274 {
275 log_error("getSavePath: no registered campaign for type %u, using the base save folder",
276 campaign_type);
277 break;
278 }
279
280 SF_String campaign_path;
281 SF_String back_slash;
282
283 uiAPI.SFStringConstructor_char(&campaign_path, custom->campaign_folder);
284 uiAPI.SFStringConstructor_char(&back_slash, "\\");
285 uiAPI.SFStringConcat(output, &campaign_path);
286 uiAPI.SFStringConcat(output, &back_slash);
287 uiAPI.SFStringDestructor(&campaign_path);
288 uiAPI.SFStringDestructor(&back_slash);
289 break;
290 }
291 }
292 return output;
293}
294
295
296uint8_t __thiscall quickLoad_helper(SF_CUiMain *_this)
297{
298 uint16_t player_id = _this->CUiMain_data.CGdControllerClient->data.current_player;
299 if (player_id == 0)
300 {
301 return 0;
302 }
303
305 if (players->players[player_id].use == 0)
306 {
307 return 0;
308 }
309 SF_String avatar_name;
310 SF_String postfix;
311 SF_String base_path;
312 uiAPI.SFStringConstructor(&avatar_name);
313 AC95_get_figure_name(_this->CUiMain_data.AC95,&avatar_name, players->players[player_id].avatar_figure_index);
314 uiAPI.SFStringConstructor_char(&postfix, "~QUICKSAVE.sav");
315 uiAPI.SFStringConcat(&avatar_name, &postfix);
316 //Not a bug. First parameter is unused inside, since it's static member function.
317 getSavePath((void *)_this, &base_path, _this->CUiMain_data.campaign_type);
318 uiAPI.SFStringConcat(&base_path,&avatar_name);
319 uint32_t shallContinue = false;
320 if (checkFileExists(&base_path))
321 {
322 if ((CMnuBaseIsVisible((CMnuBase *)_this->CUiMain_data.CUiGame) == 1) &&
323 (CMnuBaseIsVisible(*(CMnuBase **)&_this->CUiMain_data.unkn7[0x1128]) == 0))
324 {
325 shallContinue = true;
326 }
327 }
328
329 uiAPI.SFStringDestructor(&avatar_name);
330 uiAPI.SFStringDestructor(&postfix);
331 uiAPI.SFStringDestructor(&base_path);
332
333 if (!shallContinue)
334 {
335 return 0;
336 }
337
338 return 1;
339}
340
341//We have inlined and optimized getSavePath over there, so let's just plug-in
342
343static void __declspec(naked) quickload_hook()
344{
345 asm ("mov %%esi, %%ecx \n\t" // Getting CUIMain
346 "call %P0 \n\t" // Calling the Hook Function
347 "test %%eax, %%eax \n\t" // checking what have we returned
348 "jne 1f \n\t"
349 "jmp *%2 \n\t"
350 "1: jmp *%1 \n\t" : : "i" (quickLoad_helper),
351 "o" (s_ql_return_ok),"o" (s_ql_return_fail) );
352}
353
354uint32_t __thiscall getHdrStringID(uint32_t camp_type)
355{
356 if (camp_type == 2)
357 {
358 return 0x1cb8;
359 }
360 if (camp_type == 1)
361 {
362 return 0x1bbf;
363 }
364 if (camp_type == 0)
365 {
366 return 0x1979;
367 }
368
369 int custom_idx = (int)camp_type - SFSF_CAMPAIGN_TYPE_BASE;
370 if (g_campaigns[custom_idx].campaign_name_id)
371 {
372 return g_campaigns[custom_idx].campaign_name_id;
373 }
374 else
375 {
376 return 0x1979;
377 }
378}
379
380static void __declspec(naked) hdr_helper()
381{
382 asm ("mov 0x3c4(%%ebx), %%eax \n\t" // Getting campaign type
383 "mov %%eax, %%ecx \n\t"
384 "call %P0 \n\t" // Calling the Hook Function
385 "mov %%eax, %%ecx \n\t" // saving the result
386 "lea -0x330(%%ebp), %%eax \n\t"
387 "push %%eax \n\t"
388 "push %%ecx \n\t"
389 "jmp *%1 \n\t" : : "i" (getHdrStringID),
390 "o" (s_hdr_result_ok) );
391}
392
393void hook_ql_helper()
394{
395 ASI::MemoryRegion mreg_qs(ASI::AddrOf(0x5ef43a), 6);
396 ASI::BeginRewrite(mreg_qs);
397 *(unsigned char *)(ASI::AddrOf(0x5ef43a)) = 0x90; // NOP
398 *(unsigned char *)(ASI::AddrOf(0x5ef43b)) = 0xE9; // JMP instruction
399 *(int *)(ASI::AddrOf(0x5ef43c)) = (int)(&quickload_hook) - ASI::AddrOf(0x5ef440);
400 ASI::EndRewrite(mreg_qs);
401
402 ASI::MemoryRegion mreg_hdr(ASI::AddrOf(0x5fb827), 6);
403 ASI::BeginRewrite(mreg_hdr);
404 *(unsigned char *)(ASI::AddrOf(0x5fb827)) = 0x90; // NOP
405 *(unsigned char *)(ASI::AddrOf(0x5fb828)) = 0xE9; // JMP instruction
406 *(int *)(ASI::AddrOf(0x5fb829)) = (int)(&hdr_helper) - ASI::AddrOf(0x5fb82d);
407 ASI::EndRewrite(mreg_hdr);
408
409}
410
411
412void __thiscall loadQuickSave(CAppMenu *_this, uint32_t unknown)
413{
414 CAppSession *session = _this->CAppMenu_data.CAppSession;
415 SF_CGdMain *main = session->data.CGdMain;
416 CGdControllerClient *client = session->data.controllerClient;
417 SF_CGdPlayer *player = main->data.CGdPlayer;
418 uint16_t player_id = client->data.current_player;
419
420 bool inUse = (player->players[player_id].use != 0);
421 uint16_t figure_id = player->players[player_id].avatar_figure_index;
422
423 if (!inUse)
424 {
425 return;
426 }
427 SF_String avatar_name;
428 SF_String tilda;
429 SF_String quicksave;
430 SF_String base_path;
431 AC95_get_figure_name(_this->CAppMenu_data.AC95, &avatar_name, figure_id);
432 uiAPI.SFStringFromWchar(&tilda, L'~', 1);
433 uiAPI.SFStringConstructor_wchar(&quicksave, L"QUICKSAVE.SAV");
434 uiAPI.SFStringConcat(&avatar_name, &tilda);
435 uiAPI.SFStringConcat(&avatar_name, &quicksave);
436 getSavePath(session, &base_path, _this->CAppMenu_data.campaign_type);
437 //log_info("Base path %ls", base_path.raw_data);
438
439 uiAPI.SFStringConcat(&base_path, &avatar_name);
440 if (checkFileExists(&base_path))
441 {
442 //log_info("Full save path %ls", base_path.raw_data);
443
444 prepareTransition(_this, 1, 1);
445 SF_GameInfo newInfo;
446 initDefaultInfo(&newInfo);
448 uiAPI.SFStringDeepCopy(&newInfo.filename, &avatar_name);
449 newInfo.is_tutorial = 0;
450 newInfo.unknown_0xf4 = 2;
451 newInfo.start_mode = 0;
452 SF_String dummy;
453 uiAPI.SFStringConstructor(&dummy);
454 s_start_game(_this, &newInfo, 100, 0, 0, 0, &dummy);
455 uiAPI.SFStringDestructor(&dummy);
456
457 AC82_Zero(&newInfo.AC82_1);
458 uiAPI.SFStringDestructor(&newInfo.starter_kit_name);
459 uiAPI.SFStringDestructor(&newInfo.template_name);
460 uiAPI.SFStringDestructor(&newInfo.filename);
461 AC82_Zero(&newInfo.AC82);
462 }
463 uiAPI.SFStringDestructor(&avatar_name);
464 uiAPI.SFStringDestructor(&tilda);
465 uiAPI.SFStringDestructor(&quicksave);
466 uiAPI.SFStringDestructor(&base_path);
467}
468
469void hook_qs_load()
470{
471 ASI::MemoryRegion mreg_qs(ASI::AddrOf(0x185c00), 5);
472 ASI::BeginRewrite(mreg_qs);
473 *(unsigned char *)(ASI::AddrOf(0x185c00)) = 0xE9; // JMP instruction
474 *(int *)(ASI::AddrOf(0x185c01)) = (int)(&loadQuickSave) - ASI::AddrOf(0x185c05);
475 ASI::EndRewrite(mreg_qs);
476}
477
478static void hook_check_dirs()
479{
480 ASI::MemoryRegion mreg(ASI::AddrOf(0x1b6fa0), 5);
481 ASI::BeginRewrite(mreg);
482 *(unsigned char *)(ASI::AddrOf(0x1b6fa0)) = 0xE9; // JMP instruction
483 *(int *)(ASI::AddrOf(0x1b6fa1)) = (int)(&checkDirs) - ASI::AddrOf(0x1b6fa5);
484 ASI::EndRewrite(mreg);
485}
486
487
488
498void __thiscall initFirstMap(SF_GameInfo *_this, uint32_t skip_tutorial, uint8_t skill,
499 uint8_t subskill, uint32_t campaign_id, bool is_shadowblade)
500{
501 SF_String default_template;
502 SF_String template_path;
503 SF_String full_template;
504 SF_String dot_map;
505 SF_String campagn_path;
506 SF_String intial_map_name;
507 //log_info("initFirstMap Hook: Starter Kit Start ");
508
509 //log_info("Values: %x %d %d %d %d %d", (uint32_t)_this, skip_tutorial, skill, subskill, campaign_id, is_shadowblade);
510
511 //log_info("initFirstMap Hook: Check for Custom Campaign");
512 /* -- active custom campaign? Resolved up front: the starter kit below
513 * depends on it. -- */
514 const SFSF_CampaignDef *custom = NULL;
515 int custom_idx = (int)campaign_id - SFSF_CAMPAIGN_TYPE_BASE;
516 if (custom_idx >= 0 && custom_idx < (int32_t)g_campaign_count)
517 {
518 custom = &g_campaigns[custom_idx];
519 }
520
521 /* -- starter kit: figure_template\starterkit<kit>.des. A campaign may pin
522 * its own kit file, which replaces the whole SK_<skill><subskill> name;
523 * an empty value falls back to the vanilla skill-derived one. -- */
524 uiAPI.SFStringConstructor_char(&dot_map, ".map");
525 uiAPI.SFStringConstructor(&full_template);
526 if (custom != NULL && custom->starterkit[0] != '\0')
527 {
528 uiAPI.SFStringConstructor_char(&default_template, custom->starterkit);
529 }
530 else
531 {
532 uiAPI.SFStringConstructor(&default_template);
533 uiAPI.SFprintf(&default_template, L"SK_%02d%02d.des", skill, subskill);
534 }
535 uiAPI.SFStringConstructor_char(&template_path, "figure_template\\starterkit\\");
536 uiAPI.SFStringConcat(&full_template, &template_path);
537 uiAPI.SFStringConcat(&full_template, &default_template);
538 uiAPI.SFStringDeepCopy(&_this->starter_kit_name, &full_template);
539
540 //log_info("initFirstMap Hook: Avatar Snapshot to deep Copy");
541 /* -- avatar snapshot AC82 -> AC82_1: FULL deep copy (vanilla parity).
542 * Downstream flows (restart map, PrepareNewGame) read AC82_1;
543 * the shallow field copies alone are NOT sufficient. -- */
544 _this->AC82_1.unknown1 = _this->AC82.unknown1;
545 _this->AC82_1.unknown2 = _this->AC82.unknown2;
546 _this->AC82_1.unknown3 = _this->AC82.unknown3;
547 _this->AC82_1.kit_index = _this->AC82.kit_index;
548 s_avatar_internal_copy(&_this->AC82_1.avatarData.internal, &_this->AC82.avatarData.internal);
549 uint32_t proper_offset = (uint32_t)(&_this->AC82.avatarData.begin) - 0x10;
550 s_avatar_vectors_copy(&_this->AC82_1.avatarData.begin, proper_offset);
551
552 /* -- folder + start map: selected FIRST, tutorial branch only overrides
553 * the map NAME (vanilla order - vanilla tutorials live in their own
554 * campaign's folder, e.g. map\Campaign2\tutorial.map). -- */
555 if (custom != NULL)
556 {
557 //log_info("initFirstMap Hook: Attempting to load Custom Campaign");
558 char folder[160];
559 snprintf(folder, sizeof(folder), "map\\%s\\", custom->campaign_folder);
560 uiAPI.SFStringConstructor_char(&campagn_path, folder);
561 uiAPI.SFStringConstructor_char(&intial_map_name, custom->start_map);
562
563 /* No tutorial map defined: force a direct start even if the player
564 * left the tutorial checkbox on. */
565 if (!skip_tutorial && custom->tutorial_map[0] == '\0')
566 {
567 skip_tutorial = 1;
568 }
569 //log_info("Campaign '%s': maps from %s", custom->name, folder);
570 }
571 else
572 {
573 //log_info("initFirstMap Hook: Not Custom - Deverting to Vanilla Flow");
574 switch (campaign_id)
575 {
576 case 1:
577 //log_info("initFirstMap Hook: Engine Type 1");
578 uiAPI.SFStringConstructor_char(&campagn_path, "map\\Campaign2\\");
579 uiAPI.SFStringConstructor_wchar(&intial_map_name, L"P101_Mirraw_Thur");
580 break;
581 case 2:
582 //log_info("initFirstMap Hook: Engine Type 2");
583 uiAPI.SFStringConstructor_char(&campagn_path, "map\\Campaign3\\");
584 if (is_shadowblade)
585 {
586 uiAPI.SFStringConstructor_wchar(&intial_map_name, L"P202_City_Of_Souls");
587 }
588 else
589 {
590 uiAPI.SFStringConstructor_wchar(&intial_map_name, L"P201_Blackwater_Coast");
591 }
592 break;
593 case 0:
594 default:
595 //log_info("initFirstMap Hook: Campaign Type != (1||2)");
596 /* default falls back to Order so the strings are ALWAYS
597 * constructed - the epilogue destructors depend on it.
598 *
599 * A campaign_id above 2 that reached this branch means the
600 * lookup above found no registered campaign for it, so there is
601 * no definition to read - fall through to Order rather than
602 * dereferencing a NULL custom. */
603 if (campaign_id > 2)
604 {
605 log_error("initFirstMap: campaign type %u has no registered campaign, starting Order instead",
606 campaign_id);
607 }
608
609 uiAPI.SFStringConstructor_char(&campagn_path, "map\\Campaign\\");
610 uiAPI.SFStringConstructor_wchar(&intial_map_name, L"000_Greyfell");
611 break;
612 }
613 }
614
615 //log_info("initFirstMap Hook: Check Tutorial");
616 if (skip_tutorial)
617 {
618 //log_info("initFirstMap Hook: Skipping Tutorial");
619 /* Direct start: Assign player their skill-derived starter kit (armor equipment ect). */
620 uiAPI.SFStringDeepCopy(&_this->template_name, &_this->starter_kit_name);
621 //log_info("initFirstMap Hook; setting GameInfo");
622 _this->start_mode = 2;
623 _this->is_tutorial = 0;
624 }
625 else
626 {
627 //log_info("initFirstMap Hook: Loading Tutorial");
628 /* Tutorial: swap the map NAME (folder already selected above),
629 * fixed tutorial figure template. */
630 uiAPI.SFStringDestructor(&intial_map_name);
631 if (custom != NULL)
632 {
633 uiAPI.SFStringConstructor_char(&intial_map_name, custom->tutorial_map);
634 }
635 else
636 {
637 uiAPI.SFStringConstructor_wchar(&intial_map_name, L"tutorial");
638 }
639
640 //log_info("initFirstMap Hook: Tutorial Kit Load");
641 SF_String tutorial_template;
642 SF_String tutorial_kit_name;
643 SF_String tutorial_template_path;
644 uiAPI.SFStringConstructor_wchar(&tutorial_template, L"FT_Tutorial.des");
645 uiAPI.SFStringConstructor_char(&tutorial_template_path, "figure_template\\");
646 uiAPI.SFStringConstructor(&tutorial_kit_name);
647 uiAPI.SFStringConcat(&tutorial_kit_name, &tutorial_template_path);
648 uiAPI.SFStringConcat(&tutorial_kit_name, &tutorial_template);
649 uiAPI.SFStringDeepCopy(&_this->template_name, &tutorial_kit_name);
650 uiAPI.SFStringDestructor(&tutorial_kit_name);
651 uiAPI.SFStringDestructor(&tutorial_template);
652 uiAPI.SFStringDestructor(&tutorial_template_path);
653
654 //log_info("initFirstMap Hook: Updating GameInfo");
655 _this->start_mode = 1;
656 _this->is_tutorial = 1;
657 }
658
659 //log_info("initFirstMap Hook: prepping map file for loading");
660 /* -- filename = <folder><map>.map + branch-invariant state -- */
661 uiAPI.SFStringConcat(&campagn_path, &intial_map_name);
662 uiAPI.SFStringConcat(&campagn_path, &dot_map);
663 uiAPI.SFStringDeepCopy(&_this->filename, &campagn_path);
664 _this->unknown_0xf0 = 1;
665 _this->unknown_0xf4 = 3;
666
667 //log_info("initFirstMap Hook; Cleanup");
668 uiAPI.SFStringDestructor(&default_template);
669 uiAPI.SFStringDestructor(&template_path);
670 uiAPI.SFStringDestructor(&full_template);
671 uiAPI.SFStringDestructor(&dot_map);
672 uiAPI.SFStringDestructor(&campagn_path);
673 uiAPI.SFStringDestructor(&intial_map_name);
674 //log_info("initFirstMap Hook; Complete Returning");
675}
676
677void hook_initfirstmap()
678{
679 ASI::MemoryRegion mreg(ASI::AddrOf(0x176040), 5);
680 ASI::BeginRewrite(mreg);
681 *(unsigned char *)(ASI::AddrOf(0x176040)) = 0xE9;
682 *(int *)(ASI::AddrOf(0x176041)) = (int)(&initFirstMap) - (int)(ASI::AddrOf(0x176045));
683 ASI::EndRewrite(mreg);
684}
685
686/* Detail panel geometry, relative to the 443x619 right hand container. */
687#define CAMPAIGN_PANEL_WIDTH 443
688#define CAMPAIGN_NAME_Y 32
689#define CAMPAIGN_NAME_H 48
690#define CAMPAIGN_AUTHOR_Y 88
691#define CAMPAIGN_AUTHOR_H 28
692#define CAMPAIGN_DESC_Y 140
693#define CAMPAIGN_DESC_H 360
694#define CAMPAIGN_PLAY_X 108
695#define CAMPAIGN_PLAY_Y 540
696#define CAMPAIGN_PLAY_W 227
697#define CAMPAIGN_PLAY_H 36
698
700#define CAMPAIGN_DESC_WRAP 40
701
702/* One row per campaign; the registry caps at SFSF_MAX_CAMPAIGNS so they all fit
703 * in the left panel without paging. */
704#define CAMPAIGN_ROW_X 108
705#define CAMPAIGN_ROW_Y_START 40
706#define CAMPAIGN_ROW_PITCH 36
707#define CAMPAIGN_ROW_W 227
708#define CAMPAIGN_ROW_H 30
709
710static bool s_screen_exists = false;
711static bool s_screen_visible = false;
712static CMnuContainer *s_campaign_screen = NULL;
713static CMnuSmpButton *s_campaign_buttons[SFSF_MAX_CAMPAIGNS];
714static CMnuLabel *s_campaign_name_label = NULL;
715static CMnuLabel *s_campaign_author_label = NULL;
716static CMnuLabel *s_campaign_desc_label = NULL;
717
719static int32_t s_selected_campaign = -1;
720
722{
723 g_campaign_app_menu = app_menu;
725 s_screen_exists = false;
726 s_screen_visible = false;
727 s_campaign_screen = NULL;
728 s_campaign_name_label = NULL;
729 s_campaign_author_label = NULL;
730 s_campaign_desc_label = NULL;
731 s_selected_campaign = -1;
732 memset(s_campaign_buttons, 0, sizeof(s_campaign_buttons));
733}
734
735static void hook_getsavepath()
736{
737 ASI::MemoryRegion mreg(ASI::AddrOf(0x1b89d0), 5);
738 ASI::BeginRewrite(mreg);
739 *(unsigned char *)(ASI::AddrOf(0x1b89d0)) = 0xE9; /* JMP */
740 *(int *)(ASI::AddrOf(0x1b89d1)) = (int)(&getSavePath) - ASI::AddrOf(0x1b89d5);
741 ASI::EndRewrite(mreg);
742}
743
745{
748 if (seq != NULL)
749 {
750 s_video_sequence_stop(seq);
751 app_menu->CAppMenu_data.CUiVideoSequence_ptr = NULL;
752 }
753}
754
755void campaign_launch_flow(int32_t campaign_index)
756{
757 CAppMenu *app_menu = g_campaign_app_menu;
758 const SFSF_CampaignDef *def = &g_campaigns[campaign_index];
759
760 if (app_menu == NULL || s_gameinfo_set_avatar_type == NULL)
761 {
762 log_error("Campaign launch unavailable: missing CAppMenu or engine addresses");
763 return;
764 }
765
766 stop_intro_video(app_menu);
767 g_active_custom_campaign = campaign_index;
768 app_menu->CAppMenu_data.game_info.is_coop = 0;
769 app_menu->CAppMenu_data.campaign_type = SFSF_CAMPAIGN_TYPE_BASE + campaign_index;
771 s_enter_campaign_flow(app_menu, 1);
772}
773
777static void show_campaign_details(int32_t campaign_index)
778{
779 if (campaign_index < 0 || campaign_index >= (int32_t)g_campaign_count)
780 {
781 return;
782 }
783
784 s_selected_campaign = campaign_index;
785 const SFSF_CampaignDef *def = &g_campaigns[campaign_index];
786
787 char author[128];
788 snprintf(author, sizeof(author), "by %s",
789 (def->author[0] != '\0') ? def->author : "Unknown");
790
791 char wrapped_description[1024] = {0};
792 wrap_text(def->description, wrapped_description, CAMPAIGN_DESC_WRAP);
793
794 set_centred_label_text(s_campaign_name_label, def->name,
796 set_centred_label_text(s_campaign_author_label, author,
798 set_centred_label_text(s_campaign_desc_label, wrapped_description,
800}
801
803void __thiscall on_campaign_selected(CMnuSmpButton *_this)
804{
805 for (uint32_t i = 0; i < g_campaign_count; i++)
806 {
807 if (s_campaign_buttons[i] == _this)
808 {
809 show_campaign_details((int32_t)i);
810 return;
811 }
812 }
813 log_error("Campaign button not found in registry");
814}
815
817void __thiscall on_campaign_play(CMnuSmpButton *_this)
818{
819 if (s_selected_campaign < 0 || s_selected_campaign >= (int32_t)g_campaign_count)
820 {
821 log_error("Play pressed with no campaign selected");
822 return;
823 }
824
825 campaign_launch_flow(s_selected_campaign);
826}
827
829{
830 if (s_campaign_screen != NULL)
831 {
832 uiAPI.setContainerVisible(s_campaign_screen, false, 0);
833 }
834 s_screen_visible = false;
835}
836
841{
843
844 if (s_screen_exists && s_campaign_screen != NULL)
845 {
846 s_screen_visible = !s_screen_visible;
847 uiAPI.setContainerVisible(s_campaign_screen, s_screen_visible, 0);
848 return;
849 }
850
851 s_campaign_screen = uiAPI.createContainer(0, 0, 1024, 768,
852 "ui_bgr_landscape_bg.msb", "", 0.99f);
853 CMnuContainer *frame = uiAPI.createContainer(11, 6, 1008, 757,
854 "ui_bgr_pregame_border_transparency.msb",
855 "ui_bgr_pregame_border.msb", 0.5f);
856 CMnuContainer *list_panel = uiAPI.createContainer(59, 50, 443, 619,
857 "ui_bgr_pregame_border_left_transparency.msb",
858 "ui_bgr_pregame_border_left.msb", 0.5f);
859 CMnuContainer *detail_panel = uiAPI.createContainer(502, 50, 443, 619,
860 "ui_bgr_pregame_border_right_transparency.msb",
861 "ui_bgr_pregame_border_right.msb", 0.5f);
862
863 if (!s_campaign_screen || !frame || !list_panel || !detail_panel)
864 {
865 log_error("Unable to create Campaign Menu containers");
866
867 if (s_campaign_screen)
868 {
869 uiAPI.destroyContainer(s_campaign_screen);
870 }
871 if (frame)
872 {
873 uiAPI.destroyContainer(frame);
874 }
875 if (list_panel)
876 {
877 uiAPI.destroyContainer(list_panel);
878 }
879 if (detail_panel)
880 {
881 uiAPI.destroyContainer(detail_panel);
882 }
883
884 s_campaign_screen = NULL;
885 s_screen_exists = false;
886 s_screen_visible = false;
887 return;
888 }
889
890 uiAPI.containerAddControl(parent, (CMnuBase *)s_campaign_screen, '\x01', '\x01', 0);
891 uiAPI.containerAddControl(s_campaign_screen, (CMnuBase *)frame, '\x01', '\x01', 0);
892 uiAPI.containerAddControl(frame, (CMnuBase *)list_panel, '\x01', '\x01', 0);
893 uiAPI.containerAddControl(frame, (CMnuBase *)detail_panel, '\x01', '\x01', 0);
894
895 char title[32] = "Custom Campaigns";
896 CMnuLabel *title_label = uiAPI.attachLabel(NULL, frame, title, 6, 468, 16, 128, 16);
897 uiAPI.setMenuID(title_label, 0x6);
898 apply_label_colour(title_label, 0.85f, 0.64f, 0.12f);
899
900 char btn_default[32] = "ui_mainmenu_button_default.msh";
901 char btn_pressed[32] = "ui_mainmenu_button_pressed.msh";
902 char btn_disabled[32] = "ui_mainmenu_button_disabled.msh";
903 char btn_load[1] = "";
904
905 for (uint32_t i = 0; i < g_campaign_count; i++)
906 {
907 s_campaign_buttons[i] = uiAPI.attachNewButton(
908 list_panel,
909 btn_default, btn_pressed, btn_load, btn_disabled,
910 g_campaigns[i].name,
911 7,
914 32 + i,
915 (uint32_t)&on_campaign_selected);
916 }
917
918 char placeholder[2] = " ";
919
920 s_campaign_name_label = uiAPI.attachLabel(NULL, detail_panel, placeholder, 6,
923 uiAPI.setMenuID(s_campaign_name_label, 0x6);
924 apply_label_colour(s_campaign_name_label, 0.85f, 0.64f, 0.12f);
925
926 s_campaign_author_label = uiAPI.attachLabel(NULL, detail_panel, placeholder, 6,
929 uiAPI.setMenuID(s_campaign_author_label, 0x6);
930 apply_label_colour(s_campaign_author_label, 0.45f, 0.72f, 0.95f);
931
932 s_campaign_desc_label = uiAPI.attachLabel(NULL, detail_panel, placeholder, 11,
935 uiAPI.setMenuID(s_campaign_desc_label, 0x6);
936
937 char play_label[8] = "PLAY";
938 uiAPI.attachNewButton(detail_panel,
939 btn_default, btn_pressed, btn_load, btn_disabled,
940 play_label, 7,
943 30,
944 (uint32_t)&on_campaign_play);
945
946 char back_default[32] = "ui_btn_nav_back_default.msh";
947 char back_pressed[32] = "ui_btn_nav_back_pressed.msh";
948 char back_disabled[32] = "ui_btn_nav_back_disabled.msh";
949 char back_load[1] = "";
950 char back_label[1] = "";
951 uiAPI.attachNewButton(s_campaign_screen,
952 back_default, back_pressed, back_load, back_disabled,
953 back_label, 7, 52, 678, 48, 48, 31,
955
956 s_screen_exists = true;
957 s_screen_visible = true;
958
959 show_campaign_details(0);
960}
961
#define CAMPAIGN_ROW_H
CUtlCallBackInit_ptr CUtlCallBackInit
void campaign_launch_flow(int32_t campaign_index)
void(__thiscall * videoSequenceStop_ptr)(SF_CUiVideoSequence *_this)
#define CAMPAIGN_ROW_X
void close_campaign_screen_callback(CMnuSmpButton *button)
void(__thiscall * appMenuEnterCampaignFlow_ptr)(CAppMenu *_this, uint32_t param_1)
#define CAMPAIGN_ROW_Y_START
SF_String *(__thiscall * AC95_get_figure_name_ptr)(void *AC95, SF_String *name_buffer, uint32_t figure_id)
void(__thiscall * AC82_Zero_ptr)(AutoClass82 *_this)
#define CAMPAIGN_NAME_Y
CMnuBaseFlagGetter_ptr CMnuBaseIsVisible
#define CAMPAIGN_DESC_Y
void(__thiscall * CUiGame_009a1fd0_ptr)(void *_this, uint32_t param1)
#define CAMPAIGN_AUTHOR_Y
gameInfoSetAvatarType_ptr s_gameinfo_set_avatar_type
uint32_t __thiscall getHdrStringID(uint32_t camp_type)
#define CAMPAIGN_DESC_WRAP
void(__thiscall * some_vector_fun_ptr)(void *_this, uint32_t *size)
uint32_t g_campaign_count
void(__thiscall * gameInfoSetMapPathFull_ptr)(SF_GameInfo *_this, SF_String *map_path)
uint8_t __thiscall quickLoad_helper(SF_CUiMain *_this)
void(__thiscall * prepareTransition_ptr)(CAppMenu *_this, uint32_t param_1, uint32_t param_2)
uint32_t(__cdecl * checkFileExists_ptr)(SF_String *name)
The engine's file existence check, resolved by initialize_campaign_hooks().
SF_String *__thiscall getSavePath(CAppSession *_this, SF_String *output, uint32_t campaign_type)
void stop_intro_video(CAppMenu *app_menu)
uint32_t(__thiscall * CMnuBaseFlagGetter_ptr)(CMnuBase *_this)
int32_t register_campaign(const SFSF_CampaignDef *def)
void(__thiscall * gameInfoSetAvatarType_ptr)(SF_GameInfo *_this, uint16_t avatar_type)
#define CAMPAIGN_PANEL_WIDTH
void __thiscall show_custom_campaign_screen(CMnuSmpButton *_this)
Builds the campaign screen.
#define CAMPAIGN_PLAY_Y
some_vector_fun_ptr some_vector_dispose
void(__thiscall * avatarVectorsCopy_ptr)(void *dest_vectors, void *src_vectors)
startGame_ptr s_start_game
initDefaultInfo_ptr initDefaultInfo
void initialize_preparenewgame_rewrite()
#define CAMPAIGN_ROW_PITCH
SFSF_CampaignDef g_campaigns[16]
void(__fastcall * playCampaignIntro_ptr)(CAppMenu *_this)
some_vector_fun_ptr some_vector_init
void __thiscall on_campaign_selected(CMnuSmpButton *_this)
Left panel row callback.
LPCSTR *(__cdecl * GetDataStorageLocation_ptr)(char **param_1, uint32_t type)
#define CAMPAIGN_PLAY_X
getBasePathString_ptr s_get_base_path_string
#define CAMPAIGN_DESC_H
int isDirectoryExists(const char *path)
#define CAMPAIGN_PLAY_W
#define SFSF_CAMPAIGN_TYPE_BASE
void(__thiscall * CUiGameZero_ptr)(void *_this)
GdAvatarInternal *(__thiscall * avatarInternalCopy_ptr)(GdAvatarInternal *dest, GdAvatarInternal *src)
CUiGameZero_ptr CUiGameZero
#define CAMPAIGN_ROW_W
SF_GameInfo *(__thiscall * initDefaultInfo_ptr)(SF_GameInfo *_this)
void __thiscall on_campaign_play(CMnuSmpButton *_this)
Play button callback - launches whatever the list has selected.
checkFileExists_ptr checkFileExists
void __thiscall initFirstMap(SF_GameInfo *_this, uint32_t skip_tutorial, uint8_t skill, uint8_t subskill, uint32_t campaign_id, bool is_shadowblade)
Proper map selection code for the future.
#define CAMPAIGN_NAME_H
void initialize_campaign_hooks()
#define CAMPAIGN_AUTHOR_H
void __thiscall loadQuickSave(CAppMenu *_this, uint32_t unknown)
CUiGame_009a1fd0_ptr CUiGame_009a1fd0
playCampaignIntro_ptr s_play_campaign_intro
#define SFSF_MAX_CAMPAIGNS
int32_t g_active_custom_campaign
void campaign_hook_on_main_menu(CAppMenu *app_menu)
void(__thiscall * startGame_ptr)(CAppMenu *_this, SF_GameInfo *game_info, uint32_t param_2, uint32_t param_3, uint32_t param_4, uint32_t coord, SF_String *param_6)
AC82_Zero_ptr AC82_Zero
char **(__cdecl * getBasePathString_ptr)(void *out_string_obj)
#define CAMPAIGN_PLAY_H
AC95_get_figure_name_ptr AC95_get_figure_name
CAppMenu * g_campaign_app_menu
void(__thiscall * CUtlCallBackInit_ptr)(void *_this, void *param1, void *param2, uint32_t param3)
prepareTransition_ptr prepareTransition
void checkDirs(CAppSession *_this)
UiFunctions uiAPI
Definition sf_hooks.c:50
void log_error(const char *format,...)
void log_info(const char *format,...)
int AddrOf(int offset)
returns "real" virtual address of given memory offset
Definition sf_asi.h:135
bool BeginRewrite(MemoryRegion &mem_region)
allows rewriting code in a given memory region by overwriting permissions for that memory region
Definition sf_asi.cpp:6
bool EndRewrite(MemoryRegion &mem_region)
ends rewriting by restoring old permissions in a given memory region
Definition sf_asi.cpp:14
void wrap_text(const char *input, char *output, size_t max_width)
Wraps text to a character width, inserting newlines.
void apply_label_colour(CMnuLabel *label, float red, float green, float blue)
Sets both colour slots of a label. (normal and on hover).
void set_centred_label_text(CMnuLabel *label, const char *text, int panel_width, int y, int height)
Repositions a label so its text block sits centred, then sets the text.
MemoryRegion is used for rewriting code in a given memory block.
Definition sf_asi.h:17
GdAvatarData avatarData
CAppSession * CAppSession
SF_CUiVideoSequence * CUiVideoSequence_ptr
CAppMenu_data CAppMenu_data
CGdControllerClient * controllerClient
CAppSession_data data
CGdControllerClientData data
SF_CGdPlayer * CGdPlayer
CMnuBase_data CMnuBase_data
uint8_t unkn7[0x1152]
CGdControllerClient * CGdControllerClient
GdAvatarInternal internal
uint16_t avatar_figure_index
CUiMain_data CUiMain_data