diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index ab91aca242..467849f761 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -68,13 +68,18 @@ body: required: true - type: dropdown - id: game-region + id: game-disc attributes: - label: Game Region - description: The game region you are playing on + label: Game Disc + description: The game disc you are playing on options: - - NTSC-U (North America) - - PAL (Europe) + - GameCube NTSC-U (North America) + - GameCube PAL (Europe) + - GameCube NTSC-J (Japan) + - Wii NTSC-U (North America) Revision 0 + - Wii NTSC-U (North America) Revision 2 + - Wii PAL (Europe) + - Wii NTSC-J (Japan) validations: required: true diff --git a/CMakeLists.txt b/CMakeLists.txt index f89d82f587..16cbaa03d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -519,6 +519,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR add_subdirectory(mods/ao_mod) add_subdirectory(mods/shadow_mod) add_subdirectory(mods/window_demo) + add_subdirectory(mods/flow_demo) endif () if (APPLE) diff --git a/README.md b/README.md index b5a9ecae72..473efd8b68 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,10 @@ It aims to be as accurate as possible to the original while also providing new o You must dump your own copy of the game. Please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to `.rvz` to save space. -Currently, only the GameCube releases are supported. Support for other versions of the game is planned in the future. +Dusklight currently supports all commercial discs except for Wii's Korean release. + +> [!NOTE] +> Dusklight is based on the [Twilight Princess decompilation](https://github.com/zeldaret/tp), which is currently only matching for GameCube. As a result, even when playing Dusklight with a Wii disc, you will be presented with the GameCube version's HUD and certain other specificities. ### 2. Install Dusklight diff --git a/docs/ios-install-altstore.md b/docs/ios-install-altstore.md index 5c0f071878..0c2e9e36a6 100644 --- a/docs/ios-install-altstore.md +++ b/docs/ios-install-altstore.md @@ -5,7 +5,13 @@ - A Windows, Linux, or macOS device - iOS device connected to computer via USB - Dusklight IPA file (download the latest `Dusklight-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusklight/releases)) -- Legally acquired game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL) +- Legally acquired game disc: + - `GZ2E01` (GameCube USA) + - `GZ2P01` (GameCube PAL) + - `GZ2J01` (GameCube JPN) + - `RZDE01` (Wii USA Rev. 0 *or* Rev. 2) + - `RZDP01` (Wii PAL) + - `RZDJ01` (Wii JPN) ## 1. Install iloader diff --git a/docs/modding.md b/docs/modding.md index ab100b5add..d9726e3dc4 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -262,14 +262,16 @@ Installs hooks on game functions and resolves symbols by name. You'll rarely cal ### OverlayService (`mods/svc/overlay.h`) Registers DVD file overlays at runtime: the dynamic counterpart to the static `overlay/` directory (see -[Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, or with a caller-owned buffer +[Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, a file within an archive, +or with a caller-owned buffer (copied on registration): ```cpp IMPORT_SERVICE(OverlayService, svc_overlay); OverlayHandle handle = 0; -svc_overlay->add_file(mod_ctx, "/res/Msgus.arc", "res/replacement.arc", &handle); +svc_overlay->add_file(mod_ctx, "/Movie/demo_movie98_00.thp", "res/replacement.thp", &handle); // Replaces the demo movie +svc_overlay->add_file(mod_ctx, "/res/Object/Kmdl/archive/bmwr/al.bmd", "res/link_model.bmd", &handle); // Replaces link's model svc_overlay->add_buffer(mod_ctx, "/generated.txt", data, size, nullptr); svc_overlay->remove(mod_ctx, handle); ``` @@ -278,6 +280,9 @@ svc_overlay->remove(mod_ctx, handle); on the disc are added as new files. Changes are applied at the next frame boundary, and data the game already read stays in memory until the file is re-read: sometimes a scene reload, and in the worst case, a full restart. +Dusklight reloads core archive files during scene transitions so modifications to Link, Midna or other globally-loaded +data get refreshed without a full restart. + See [Asset Overlays](#asset-overlays) for priority and conflict handling. ### TextureService (`mods/svc/texture.h`) @@ -636,6 +641,125 @@ first in-game frame. Projection matrices match the renderer's WebGPU clip conven Camera operators allow overriding the main camera. When an operator callback returns true, its values replace the camera state for the current frame. Register and unregister using `register_camera_operator` / `unregister_camera_operator`. +### GameModeService (`mods/svc/game_mode.h`) + +Allows a mod to register a game mode with callbacks for key gameplay and save lifecycle events. Registered game modes +appear in the prelaunch menu. Game modes may use a unique set of saves by configuring `save_name`; leave it empty to use +the vanilla `gczelda2` save. + +```cpp +IMPORT_SERVICE(LogService, svc_log); +IMPORT_SERVICE(HookService, svc_hook); +IMPORT_SERVICE(GameModeService, svc_game_mode); + +DEFINE_HOOK(fopAcM_createItem, CreateItem); + +#define MY_GAME_MODE_ID "game-mode-id" + +static HookAction my_function_hook(ModContext* ctx, void* args, void*, void*) { + // If we wish to have this hook only run while the gamemode is registered, we need to hook the function from the + // gamemode's onActivatedFunction, and uninstall the hook during the onDeactivatedFunction. Example below. + // Alternatively, check with `svc_game_mode->is_active(mod_ctx, MY_GAME_MODE_ID, &active) == MOD_OK && active`. + return HOOK_CONTINUE; +} + +ModResult on_game_mode_activated(void*, ModError* outError) { + // Setup the gamemode, Add any hooks that are gamemode specific + // Overlay any files that are gamemode specific + ModResult result = mods::hook_add_pre(svc_hook, my_function_hook); + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to install fopAcM_createItem hook"); + } + return MOD_OK; +} + +ModResult on_game_mode_deactivated(void*, ModError* outError) { + // Uninstall any hooks that are gamemode specific + // Remove any file overlays that are gamemode specific + ModResult result = mods::hook_uninstall(); + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to uninstall fopAcM_createItem hook"); + } + return MOD_OK; +} + +ModResult on_save_loaded(void*, ModError*) { + // This function will be invoked by the game as a save is loaded + return MOD_OK; +} + +const GameModeDesc gameModeDesc = { + .struct_size = sizeof(GameModeDesc), + .game_mode_id = MY_GAME_MODE_ID, + .full_name = "My Game Mode", + .save_name = "my-unique-save", + .user_data = nullptr, + .on_activated = on_game_mode_activated, + .on_deactivated = on_game_mode_deactivated, + .on_save_loaded = on_save_loaded, +}; +svc_game_mode->register_game_mode(mod_ctx, &gameModeDesc); +``` + +A game mode can also open UI for per-save settings when creating a new file. The state begins as +`GAME_MODE_STATE_PENDING` and remains valid until the mod selects `PROCEED` or `RETURN`. + +```cpp +IMPORT_SERVICE(GameModeService, svc_game_mode); +IMPORT_SERVICE(UiService, svc_ui); + +ModResult on_new_save_select(void*, GameModeNewSaveState* state, ModError* outError) { + static GameModeNewSaveState* newSaveState; + static UiWindowHandle windowHandle; + + newSaveState = state; + + UiTabDesc tabs[1]{}; + + tabs[0].struct_size = sizeof(UiTabDesc); + tabs[0].title = "Play"; + tabs[0].build = [](ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { + UiControlDesc desc = UI_CONTROL_DESC_INIT; + desc.kind = UI_CONTROL_BUTTON; + desc.label = "Play"; + desc.help_rml = "Play Button"; + desc.on_pressed = [](ModContext* ctx, void* userdata) { + *newSaveState = GAME_MODE_STATE_PROCEED; + svc_ui->window_close(ctx, *static_cast(userdata)); + }; + desc.user_data = &windowHandle; + svc_ui->pane_add_control(mod_ctx, leftPane, &desc, nullptr); + return MOD_OK; + }; + + UiWindowDesc desc = UI_WINDOW_DESC_INIT; + desc.tabs = tabs; + desc.tab_count = 1; + desc.on_closed = [](ModContext *, UiWindowHandle, void *userdata) { + // If closing the window through backing out, return to file select + if (*newSaveState == GAME_MODE_STATE_PENDING) { + *newSaveState = GAME_MODE_STATE_RETURN; + } + }; + + ModResult result = svc_ui->window_push(mod_ctx, &desc, &windowHandle); + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to open new-save settings"); + } + return MOD_OK; +} + +const GameModeDesc gameModeDesc = { + .struct_size = sizeof(GameModeDesc), + .game_mode_id = "my-game-mode-id", + .full_name = "My Game Mode", + .save_name = "my-unique-save", + .on_new_save_select = on_new_save_select, +}; +svc_game_mode->register_game_mode(mod_ctx, &gameModeDesc); + +``` + --- ## Hooking Game Functions @@ -763,6 +887,11 @@ For reference parameters (e.g. `const cXyz& pos`), `arg_ref` yields a dire Files placed under `overlay/` in the `.dusk` archive override game files at the corresponding path, equivalent to replacing files in the .iso. This requires no code: an archive with just `mod.json` and `overlay/` is a complete mod. +To replace a file within an `.arc` archive, replace the archive suffix with a directory and place the replacement at +its path within the archive. + +- `overlay/Audiores/Stream/menu_select.ast` replaces the main title's audio stream. +- `overlay/res/Layout/main2D/main2d/timg/midona64.bti` replaces Midna's UI icon inside `main2D.arc`. Files placed under `textures/` register as texture replacements, and act just like the user's general `texture_replacements/` directory: Dolphin-style naming, matched by texture hash diff --git a/extern/aurora b/extern/aurora index 8005d336ab..0f15fb65d1 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 8005d336ab02f0c617fc27fabbea4af42ab04caa +Subproject commit 0f15fb65d13e4a7145667934e0291bad09806bfd diff --git a/files.cmake b/files.cmake index 1196f6c503..f53b91d196 100644 --- a/files.cmake +++ b/files.cmake @@ -1412,6 +1412,7 @@ set(DOLPHIN_FILES set(DUSK_FILES include/helpers/batch.hpp + include/helpers/bits.hpp include/helpers/endian_gx.hpp src/d/actor/d_a_alink_dusk.cpp src/dusk/OSContext.cpp @@ -1435,12 +1436,14 @@ set(DUSK_FILES src/dusk/commands.cpp src/dusk/commands.hpp src/dusk/game_clock.cpp + src/dusk/game_mode.cpp src/dusk/gamepad_color.cpp src/dusk/globals.cpp src/dusk/gyro.cpp src/dusk/game_combos.cpp src/dusk/d_trigger_view.cpp #src/dusk/m_Do_ext_dusk.cpp + src/dusk/hq_minimap.cpp src/dusk/imgui/ImGuiActorSpawner.cpp src/dusk/imgui/ImGuiBloomWindow.cpp src/dusk/imgui/ImGuiBloomWindow.hpp @@ -1461,6 +1464,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStubLog.cpp src/dusk/io.cpp src/dusk/iso_validate.cpp + src/dusk/language.cpp + src/dusk/language.hpp src/dusk/layout.cpp src/dusk/livesplit.cpp src/dusk/logging.cpp @@ -1477,6 +1482,9 @@ set(DUSK_FILES src/dusk/mods/loader/native_module.hpp src/dusk/mods/loader/prepatch.cpp src/dusk/mods/loader/prepatch.hpp + src/dusk/mods/item.hpp + src/dusk/mods/item_checks.cpp + src/dusk/mods/item_gives.cpp src/dusk/mods/log_buffer.cpp src/dusk/mods/log_buffer.hpp src/dusk/mods/manifest.cpp @@ -1486,8 +1494,11 @@ set(DUSK_FILES src/dusk/mods/svc/config.hpp src/dusk/mods/svc/game.cpp src/dusk/mods/svc/gfx.cpp + src/dusk/mods/svc/flow.cpp src/dusk/mods/svc/hook.cpp src/dusk/mods/svc/host.cpp + src/dusk/mods/svc/item.cpp + src/dusk/mods/svc/item.hpp src/dusk/mods/svc/log.cpp src/dusk/mods/svc/overlay.cpp src/dusk/mods/svc/registry.cpp @@ -1496,6 +1507,7 @@ set(DUSK_FILES src/dusk/mods/svc/texture.cpp src/dusk/mods/svc/ui.cpp src/dusk/mods/svc/ui.hpp + src/dusk/mods/svc/game_mode.cpp src/dusk/mods/svc/window.cpp src/dusk/mods/svc/window.hpp src/dusk/mods/svc/save.cpp diff --git a/include/d/actor/d_a_e_hp.h b/include/d/actor/d_a_e_hp.h index 8941f1e2a6..594c0de01f 100644 --- a/include/d/actor/d_a_e_hp.h +++ b/include/d/actor/d_a_e_hp.h @@ -96,6 +96,9 @@ public: /* 0xDE0 */ cXyz field_0xde0; /* 0xDEC */ u8 field_0xdec[0xdf9 - 0xdec]; /* 0xDF9 */ u8 field_0xdf9; +#if TARGET_PC + bool mItemCheckOverridden; +#endif }; STATIC_ASSERT(sizeof(daE_HP_c) == 0xdfc); diff --git a/include/d/actor/d_a_obj_life_container.h b/include/d/actor/d_a_obj_life_container.h index 14187d8289..1b98c1afe8 100644 --- a/include/d/actor/d_a_obj_life_container.h +++ b/include/d/actor/d_a_obj_life_container.h @@ -88,6 +88,11 @@ private: /* 0x99C */ dPa_followEcallBack mEffect2; /* 0x9B0 */ Z2SoundObjSimple mSound; /* 0x9D0 */ u8 mIsHookCarry; +#if TARGET_PC + u8 mOriginalItemNo; + bool mItemOverridden; + bool mOverrideHover; +#endif }; STATIC_ASSERT(sizeof(daObjLife_c) == 0x9d4); diff --git a/include/d/actor/d_a_tbox.h b/include/d/actor/d_a_tbox.h index 2b48a156eb..6d0528d29d 100644 --- a/include/d/actor/d_a_tbox.h +++ b/include/d/actor/d_a_tbox.h @@ -194,6 +194,9 @@ private: /* 0x9FC */ u8 field_0x9fc; /* 0x9FD */ u8 field_0x9fd; /* 0xA00 */ Mtx mDrawMtx; +#if TARGET_PC + u8 mOriginalItemNo; +#endif }; STATIC_ASSERT(sizeof(daTbox_c) == 0xA30); diff --git a/include/d/actor/d_a_tbox2.h b/include/d/actor/d_a_tbox2.h index 3f176ade5e..9b06e685a3 100644 --- a/include/d/actor/d_a_tbox2.h +++ b/include/d/actor/d_a_tbox2.h @@ -83,6 +83,10 @@ private: /* 0xAC0 */ u8 field_0xAC0[0xAC4 - 0xAC0]; /* 0xAC4 */ int mStaffIdx; /* 0xAC8 */ dBgW* mBoxBgW; +#if TARGET_PC + bool mParamsInit; + u8 mOriginalItemNo; +#endif }; STATIC_ASSERT(sizeof(daTbox2_c) == 0xACC); diff --git a/include/d/d_a_item_static.h b/include/d/d_a_item_static.h index 36c445d511..6b8b321b2b 100644 --- a/include/d/d_a_item_static.h +++ b/include/d/d_a_item_static.h @@ -145,6 +145,10 @@ public: /* 0x9BC */ u8 field_0x9bc[4]; /* 0x9C0 */ u8 field_0x9c0; /* 0x9C1 */ u8 field_0x9c1; +#if TARGET_PC + u8 mOriginalItemNo; + bool mItemOverridden; +#endif }; // Size: 0x9C4 #endif /* D_A_D_A_ITEM_STATIC_H */ diff --git a/include/d/d_a_shop_item_static.h b/include/d/d_a_shop_item_static.h index eeda9185da..7126578092 100644 --- a/include/d/d_a_shop_item_static.h +++ b/include/d/d_a_shop_item_static.h @@ -50,6 +50,9 @@ public: csXyz* getRotateP(); cXyz* getPosP(); const char* getShopArcname(); +#if TARGET_PC + const ResourceData& getResourceData() const; +#endif u16 getHeapSize(); void CreateInit(); void set_mtx(); @@ -110,6 +113,10 @@ private: /* 0x960 */ s16 mAngleX; /* 0x962 */ s16 mAngleY; /* 0x964 */ u8 mShopItemID; +#if TARGET_PC + ResourceData mOverrideData; + bool mItemOverridden; +#endif }; int CheckShopItemCreateHeap(fopAc_ac_c* i_this); diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index 634c0db352..41349acac3 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -9,6 +9,10 @@ #include "JSystem/J3DGraphLoader/J3DModelLoader.h" #include "JSystem/J3DGraphLoader/J3DAnmLoader.h" +#if TARGET_PC +#include "mods/svc/game_mode.h" +#endif + class dFile_info_c; class J2DPicture; @@ -420,6 +424,13 @@ public: bool pointerMenuSelect(); bool pointerCopyDataToSelect(); bool pointerYesNoSelect(bool errorSelect); + void backToDataSelectMove() { + headerTxtSet(0x43, 1, 0); + fileRecScaleAnmInitSet2(0.0f, 1.0f); + nameMoveAnmInitSet(0xd29, 0xd1f); + modoruTxtDispAnmInit(0); + mDataSelProc = DATASELPROC_NAME_TO_DATA_SELECT_MOVE; + } #endif void _draw(); void errorMoveAnmInitSet(int, int); @@ -733,6 +744,8 @@ public: #endif #ifdef TARGET_PC dDlst_FileSelFade_c mFadeDlst; + bool mGameModeSaveStartBuildUi = true; + GameModeNewSaveState mGameModeNewSaveState = GAME_MODE_STATE_PENDING; #endif #if PLATFORM_WII || PLATFORM_SHIELD diff --git a/include/d/d_item.h b/include/d/d_item.h index d9bdfd18c9..bf1252f9d6 100644 --- a/include/d/d_item.h +++ b/include/d/d_item.h @@ -11,7 +11,9 @@ public: static DUSK_GAME_DATA u8* mData; }; -void execItemGet(u8 item_id); +class fopAc_ac_c; +void execItemGet( + u8 item_id IF_DUSK_ARG(u32 item_give_tag = 0) IF_DUSK_ARG(fopAc_ac_c* giver = NULL)); void item_func_HEART(); void item_func_GREEN_RUPEE(); diff --git a/include/d/d_msg_class.h b/include/d/d_msg_class.h index a8bba32351..2735561c05 100644 --- a/include/d/d_msg_class.h +++ b/include/d/d_msg_class.h @@ -27,18 +27,20 @@ public: // Attributes /* 0x04 */ BE(u16) message_id; - /* 0x06 */ BE(u16) event_label_id; - /* 0x08 */ u8 se_speaker; - /* 0x09 */ u8 fuki_kind; - /* 0x0A */ u8 output_type; - /* 0x0B */ u8 fuki_pos_type; - /* 0x0C */ u8 unk_0xc; - /* 0x0D */ u8 unk_0xd; - /* 0x0E */ u8 se_mood; - /* 0x0F */ u8 camera_id; - /* 0x10 */ u8 base_anm_id; - /* 0x11 */ u8 face_anm_id; - /* 0x12 */ BE(u16) unk_0x12; + /* 0x06 */ BE(u16) event_label_id; // saveBitLabels index set when the message displays + /* 0x08 */ u8 speaker; // Z2SpeechMgr2 voice bank ID + /* 0x09 */ u8 box_kind; // screen class, see dMsgObject_c::talkStartInit + /* 0x0A */ u8 draw_type; // text pacing, see jmessage_tSequenceProcessor::do_begin + /* 0x0B */ u8 box_position; // see dMsgObject_c::fukiPosCalc + /* 0x0C */ u8 item_no; // unused; legacy dItemNo, 0xFF = none + /* 0x0D */ u8 line_alignment; // 0 centered (JP only), 1 left; also copied to + // jmessage_tReference::mForm + /* 0x0E */ u8 speaker_mood; // grunt emotion index for the voice bank + /* 0x0F */ u8 camera_attr; // 1-10 talk-actor slot, >=11 talk-camera style + /* 0x10 */ u8 talk_anim; // NPC talk motion attribute + /* 0x11 */ u8 face_anim; // NPC talk face attribute + /* 0x12 */ u8 lines_per_page; // unused; runtime uses getLineMax() + /* 0x13 */ u8 _pad; }; class JMSMesgInfo_c { diff --git a/include/d/d_msg_flow.h b/include/d/d_msg_flow.h index 233dad7170..cff5857cb7 100644 --- a/include/d/d_msg_flow.h +++ b/include/d/d_msg_flow.h @@ -22,7 +22,7 @@ struct msg_class; // all mesg_flow_node structs members might be wrong struct mesg_flow_node { /* 0x00 */ u8 type; - /* 0x01 */ u8 field_0x1; + /* 0x01 */ u8 subtype; /* 0x02 */ BE(u16) msg_index; /* 0x04 */ BE(u16) next_node_idx; /* 0x06 */ BE(u16) unk_0x6; @@ -30,7 +30,7 @@ struct mesg_flow_node { struct mesg_flow_node_branch { /* 0x00 */ u8 type; - /* 0x01 */ u8 field_0x1; + /* 0x01 */ u8 result_count; /* 0x02 */ BE(u16) query_idx; /* 0x04 */ BE(u16) param; /* 0x06 */ BE(u16) next_node_idx; diff --git a/include/d/d_msg_object.h b/include/d/d_msg_object.h index 7ca53064a7..3e8c535800 100644 --- a/include/d/d_msg_object.h +++ b/include/d/d_msg_object.h @@ -280,13 +280,13 @@ public: /* 0x150 */ f32 field_0x150; /* 0x154 */ u32 mMessageID; /* 0x158 */ u32 field_0x158; - /* 0x15C */ u32 field_0x15c; + /* 0x15C */ u32 mSelectMessageID; // message ID of the selection options message; 1000 = none /* 0x160 */ int mIdx; /* 0x164 */ u16 mNodeIdx; /* 0x166 */ u16 field_0x166; /* 0x168 */ u16 field_0x168; /* 0x16A */ s16 field_0x16a; - /* 0x16C */ s16 field_0x16c; + /* 0x16C */ s16 mCurrentGroupID; // group whose BMG is parsed; -1 none, 0 common, 1-8 stage /* 0x16E */ s16 field_0x16e; /* 0x170 */ s16 mNowTalkFlowNo; /* 0x172 */ s16 field_0x172; diff --git a/include/f_op/f_op_actor.h b/include/f_op/f_op_actor.h index 1dce990866..2ac262ced1 100644 --- a/include/f_op/f_op_actor.h +++ b/include/f_op/f_op_actor.h @@ -318,6 +318,11 @@ public: /* 0x566 */ s8 field_0x566; /* 0x567 */ s8 field_0x567; +#if TARGET_PC + u32 mItemGiveTag; + u8 mItemGiveOriginalNo; +#endif + #if !__MWERKS__ s8 actor_last_base_field; #endif diff --git a/include/f_op/f_op_actor_mng.h b/include/f_op/f_op_actor_mng.h index 6461c823d1..b23fc40383 100644 --- a/include/f_op/f_op_actor_mng.h +++ b/include/f_op/f_op_actor_mng.h @@ -79,6 +79,10 @@ struct fopAcM_prm_class { /* 0x1C */ fpc_ProcID parent_id; /* 0x20 */ s8 argument; /* 0x21 */ s8 room_no; +#if TARGET_PC + u32 mItemGiveTag; + u8 mItemGiveOriginalNo; +#endif }; struct fopAcM_search4ev_prm { @@ -516,8 +520,9 @@ s32 fopAcM_SearchByName(s16 i_procName, fopAc_ac_c** i_outActor); fopAcM_prm_class* fopAcM_CreateAppend(); fopAcM_prm_class* createAppend(u16 i_setId, u32 i_parameters, const cXyz* i_pos, int i_roomNo, - const csXyz* i_angle, const cXyz* i_scale, s8 i_argument, - fpc_ProcID i_parentId); + const csXyz* i_angle, const cXyz* i_scale, s8 i_argument, + fpc_ProcID i_parentId IF_DUSK_ARG(u32 i_itemGiveTag = 0) + IF_DUSK_ARG(u8 i_itemOriginalNo = 0xFF)); void fopAcM_Log(fopAc_ac_c const* i_actor, char const* i_message); @@ -526,19 +531,20 @@ s32 fopAcM_delete(fopAc_ac_c* i_actor); s32 fopAcM_delete(fpc_ProcID i_actorID); fpc_ProcID fopAcM_create(s16 i_procName, u16 i_setId, u32 i_parameters, const cXyz* i_pos, - int i_roomNo, const csXyz* i_angle, const cXyz* i_scale, s8 i_argument, - createFunc i_createFunc); + int i_roomNo, const csXyz* i_angle, const cXyz* i_scale, s8 i_argument, + createFunc i_createFunc IF_DUSK_ARG(u32 i_itemGiveTag = 0)); fpc_ProcID fopAcM_create(s16 i_procName, u32 i_parameters, const cXyz* i_pos, int i_roomNo, - const csXyz* i_angle, const cXyz* i_scale, s8 i_argument); + const csXyz* i_angle, const cXyz* i_scale, s8 i_argument IF_DUSK_ARG(u32 i_itemGiveTag = 0)); inline fpc_ProcID fopAcM_Create(s16 i_procName, createFunc i_createFunc, void* params) { return fpcM_Create(i_procName, i_createFunc,params); } fopAc_ac_c* fopAcM_fastCreate(s16 i_procName, u32 i_parameters, const cXyz* i_pos, int i_roomNo, - const csXyz* i_angle, const cXyz* i_scale, s8 i_argument, - createFunc i_createFunc, void* i_createFuncData); + const csXyz* i_angle, const cXyz* i_scale, s8 i_argument, createFunc i_createFunc, + void* i_createFuncData IF_DUSK_ARG(u32 i_itemGiveTag = 0) + IF_DUSK_ARG(u8 i_itemOriginalNo = 0xFF)); fopAc_ac_c* fopAcM_fastCreate(const char* i_actorname, u32 i_parameters, const cXyz* i_pos, int i_roomNo, const csXyz* i_angle, const cXyz* i_scale, @@ -623,11 +629,11 @@ fopAc_ac_c* fopAcM_getItemEventPartner(const fopAc_ac_c*); fopAc_ac_c* fopAcM_getEventPartner(const fopAc_ac_c*); fpc_ProcID fopAcM_createItemForPresentDemo(cXyz const* i_pos, int i_itemNo, u8 param_2, - int i_itemBitNo, int i_roomNo, csXyz const* i_angle, - cXyz const* i_scale); + int i_itemBitNo, int i_roomNo, csXyz const* i_angle, + cXyz const* i_scale IF_DUSK_ARG(u32 i_itemGiveTag = 0)); fpc_ProcID fopAcM_createItemForTrBoxDemo(cXyz const* i_pos, int i_itemNo, int i_itemBitNo, - int i_roomNo, csXyz const* i_angle, cXyz const* i_scale); + int i_roomNo, csXyz const* i_angle, cXyz const* i_scale IF_DUSK_ARG(u32 i_itemGiveTag = 0)); u8 fopAcM_getItemNoFromTableNo(u8 i_tableNo); @@ -641,11 +647,12 @@ fpc_ProcID fopAcM_createItemFromTable(cXyz const* i_pos, int i_tableNo, int i_it bool i_createDirect); fpc_ProcID fopAcM_createDemoItem(const cXyz* i_pos, int i_itemNo, int i_itemBitNo, - const csXyz* i_angle, int i_roomNo, const cXyz* scale, u8 param_7); + const csXyz* i_angle, int i_roomNo, const cXyz* scale, + u8 param_7 IF_DUSK_ARG(u32 i_itemGiveTag = 0)); fpc_ProcID fopAcM_createItemForBoss(const cXyz* i_pos, int i_itemNo, int i_roomNo, - const csXyz* i_angle, const cXyz* i_scale, f32 i_speedF, - f32 i_speedY, int param_8); + const csXyz* i_angle, const cXyz* i_scale, f32 i_speedF, f32 i_speedY, + int param_8 IF_DUSK_ARG(const char* i_itemCheckName = NULL)); fpc_ProcID fopAcM_createItemForMidBoss(const cXyz* i_pos, int i_itemNo, int i_roomNo, const csXyz* i_angle, const cXyz* i_scale, int param_6, diff --git a/include/f_op/f_op_msg.h b/include/f_op/f_op_msg.h index 21694f0af1..1338732a4b 100644 --- a/include/f_op/f_op_msg.h +++ b/include/f_op/f_op_msg.h @@ -30,7 +30,7 @@ struct msg_class { /* 0xDC */ fopAc_ac_c* talk_actor; /* 0xE0 */ cXyz pos; /* 0xEC */ u32 msg_idx; - /* 0xF0 */ u32 field_0xf0; + /* 0xF0 */ u32 select_msg_idx; // selection options message ID; 1000 = none /* 0xF4 */ u32 field_0xf4; /* 0xF8 */ u16 mode; /* 0xFA */ u8 select_idx; diff --git a/include/f_op/f_op_msg_mng.h b/include/f_op/f_op_msg_mng.h index 330347fc18..35d0e28ea7 100644 --- a/include/f_op/f_op_msg_mng.h +++ b/include/f_op/f_op_msg_mng.h @@ -19,7 +19,7 @@ struct fopMsg_prm_class { /* 0x00 */ fopAc_ac_c* talk_actor; /* 0x04 */ cXyz pos; /* 0x10 */ u32 msg_idx; - /* 0x14 */ u32 field_0x14; + /* 0x14 */ u32 select_msg_idx; // selection options message ID; 1000 = none /* 0x18 */ fpc_ProcID field_0x18; }; // Size: 0x1C @@ -46,8 +46,8 @@ void fopMsgM_setMessageID(fpc_ProcID msg_id); void fopMsgM_destroyExpHeap(JKRExpHeap* i_heap); f32 fopMsgM_valueIncrease(int param_0, int param_1, u8 i_type); s32 fopMsgM_setStageLayer(void* i_process); -fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, fopAc_ac_c* i_talkActor, u32 param_2); -fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, u32 param_1); +fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, fopAc_ac_c* i_talkActor, u32 i_selectMsgIdx); +fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, u32 i_selectMsgIdx); fpc_ProcID fopMsgM_messageSetDemo(u32 i_msgidx); msg_class* fopMsgM_SearchByID(fpc_ProcID i_id); TEXT_SPAN fopMsgM_messageGet(TEXT_SPAN i_stringBuf, u32 i_msgId); diff --git a/include/global.h b/include/global.h index f77694c133..4a86f0678f 100644 --- a/include/global.h +++ b/include/global.h @@ -256,4 +256,17 @@ using std::isnan; #define DUSK_CONST IF_DUSK(const) #define DUSK_CONSTEXPR IF_DUSK(constexpr) +#if TARGET_PC && defined(DUSK_BUILDING_GAME) +#include "dusk/mods/item.hpp" +#define DUSK_ITEM_CHECK(name, item_no, giver) \ + (item_no) = ::dusk::mods::item_check(name, (item_no), giver) +#define DUSK_ITEM_CHECK_EXPR(name, item_no, giver) \ + (::dusk::mods::item_check(name, (item_no), giver)) +#define DUSK_GIVE_TAG(name) IF_DUSK_ARG(::dusk::mods::item_give_tag(name)) +#else +#define DUSK_ITEM_CHECK(name, item_no, giver) +#define DUSK_ITEM_CHECK_EXPR(name, item_no, giver) (item_no) +#define DUSK_GIVE_TAG(name) +#endif + #endif diff --git a/include/helpers/bits.hpp b/include/helpers/bits.hpp new file mode 100644 index 0000000000..73442d337b --- /dev/null +++ b/include/helpers/bits.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace dusk { +namespace detail { + +template +struct uint_of_size; + +template <> +struct uint_of_size<1> { + using type = uint8_t; +}; + +template <> +struct uint_of_size<2> { + using type = uint16_t; +}; + +template <> +struct uint_of_size<4> { + using type = uint32_t; +}; + +template <> +struct uint_of_size<8> { + using type = uint64_t; +}; + +template +using uint_of_size_t = uint_of_size::type; + +template + requires(std::is_trivially_copyable_v) +T unaligned_load(const void* source) noexcept { + T value; + std::memcpy(&value, source, sizeof(value)); + return value; +} + +template + requires(std::is_trivially_copyable_v) +void unaligned_store(void* destination, T value) noexcept { + std::memcpy(destination, &value, sizeof(value)); +} + +} // namespace detail + +template + requires(std::is_unsigned_v) +constexpr T bswap(T value) noexcept { + if constexpr (sizeof(T) == 1) { + return value; + } else if constexpr (sizeof(T) == 2) { + return static_cast((value << 8) | (value >> 8)); + } else if constexpr (sizeof(T) == 4) { + return static_cast(((value & 0x000000ffU) << 24) | ((value & 0x0000ff00U) << 8) | + ((value & 0x00ff0000U) >> 8) | ((value & 0xff000000U) >> 24)); + } else { + static_assert(sizeof(T) == 8); + return static_cast( + ((value & 0x00000000000000ffULL) << 56) | ((value & 0x000000000000ff00ULL) << 40) | + ((value & 0x0000000000ff0000ULL) << 24) | ((value & 0x00000000ff000000ULL) << 8) | + ((value & 0x000000ff00000000ULL) >> 8) | ((value & 0x0000ff0000000000ULL) >> 24) | + ((value & 0x00ff000000000000ULL) >> 40) | ((value & 0xff00000000000000ULL) >> 56)); + } +} + +/// Reads an unaligned integral value in the specified byte order. +template + requires(std::is_integral_v && !std::is_same_v) +T read_bits(const void* source, std::endian endian = std::endian::big) noexcept { + using Bits = std::make_unsigned_t; + Bits value = detail::unaligned_load(source); + if constexpr (sizeof(Bits) > 1) { + if (endian != std::endian::native) { + value = bswap(value); + } + } + return std::bit_cast(value); +} + +template + requires(std::is_integral_v && !std::is_same_v) +constexpr T read_bits(const uint8_t* source, std::endian endian = std::endian::big) noexcept { + if (!std::is_constant_evaluated()) { + return read_bits(static_cast(source), endian); + } + using Bits = std::make_unsigned_t; + Bits value{}; + if (endian == std::endian::big) { + for (size_t i = 0; i < sizeof(Bits); ++i) { + value = static_cast((value << 8) | source[i]); + } + } else { + for (size_t i = 0; i < sizeof(Bits); ++i) { + value |= static_cast(source[i]) << (i * 8); + } + } + return std::bit_cast(value); +} + +/// Reads an unaligned floating-point value in the specified byte order. +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +T read_bits(const void* source, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + return std::bit_cast(read_bits(source, endian)); +} + +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +constexpr T read_bits(const uint8_t* source, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + return std::bit_cast(read_bits(source, endian)); +} + +/// Writes an unaligned integral value in the specified byte order. +template + requires(std::is_integral_v && !std::is_same_v) +void write_bits(void* destination, T value, std::endian endian = std::endian::big) noexcept { + using Bits = std::make_unsigned_t; + Bits bits = std::bit_cast(value); + if constexpr (sizeof(Bits) > 1) { + if (endian != std::endian::native) { + bits = bswap(bits); + } + } + detail::unaligned_store(destination, bits); +} + +template + requires(std::is_integral_v && !std::is_same_v) +constexpr void write_bits( + uint8_t* destination, T value, std::endian endian = std::endian::big) noexcept { + if (!std::is_constant_evaluated()) { + write_bits(static_cast(destination), value, endian); + return; + } + using Bits = std::make_unsigned_t; + const Bits bits = std::bit_cast(value); + if (endian == std::endian::big) { + for (size_t i = 0; i < sizeof(Bits); ++i) { + destination[sizeof(Bits) - i - 1] = static_cast(bits >> (i * 8)); + } + } else { + for (size_t i = 0; i < sizeof(Bits); ++i) { + destination[i] = static_cast(bits >> (i * 8)); + } + } +} + +/// Writes an unaligned floating-point value in the specified byte order. +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +void write_bits(void* destination, T value, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + write_bits(destination, std::bit_cast(value), endian); +} + +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +constexpr void write_bits( + uint8_t* destination, T value, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + write_bits(destination, std::bit_cast(value), endian); +} + +} // namespace dusk diff --git a/include/m_Do/m_Do_MemCard.h b/include/m_Do/m_Do_MemCard.h index 555d7a6a9f..84fc246e09 100644 --- a/include/m_Do/m_Do_MemCard.h +++ b/include/m_Do/m_Do_MemCard.h @@ -112,6 +112,11 @@ public: mSerialNo = serial_no; } +#ifdef TARGET_PC + void setFileName(const std::string& fileName); + const char* getFileName(); +#endif + /* 0x0000 */ u8 mData[SAVEFILE_SIZE]; /* 0x1FBC */ u8 mChannel; /* 0x1FBD */ u8 mCopyToPos; @@ -124,6 +129,11 @@ public: /* 0x1FEC */ s32 mNandState; /* 0x1FF0 */ u64 mSerialNo; /* 0x1FF8 */ u32 mDataVersion; +#ifdef TARGET_PC + bool mInitialized; + std::string mFileName; +#endif + }; // Size: 0x2000 STATIC_ASSERT(sizeof(mDoMemCd_Ctrl_c) == 8192); @@ -230,4 +240,14 @@ inline s32 mDoMemCd_checkNANDFile() { } #endif +#ifdef TARGET_PC +inline void mDoMemCd_SetFileName(const std::string& fileName) { + g_mDoMemCd_control.setFileName(fileName); +} + +inline const char* mDoMemCd_GetFileName() { + return g_mDoMemCd_control.getFileName(); +} +#endif + #endif /* M_DO_M_DO_MEMCARD_H */ diff --git a/libs/JSystem/include/JSystem/JKernel/JKRArchive.h b/libs/JSystem/include/JSystem/JKernel/JKRArchive.h index 4ac5de7a46..bcedc7834e 100644 --- a/libs/JSystem/include/JSystem/JKernel/JKRArchive.h +++ b/libs/JSystem/include/JSystem/JKernel/JKRArchive.h @@ -6,11 +6,17 @@ #include "global.h" #include "helpers/endian.h" +#if TARGET_PC +#include +#include +#include +#endif + class JKRHeap; /** * @ingroup jsystem-jkernel - * + * */ struct SArcHeader { /* 0x00 */ BE(u32) signature; @@ -25,7 +31,7 @@ struct SArcHeader { /** * @ingroup jsystem-jkernel - * + * */ struct SArcDataInfo { /* 0x00 */ BE(u32) num_nodes; @@ -59,7 +65,7 @@ extern u32 sCurrentDirID__10JKRArchive; // JKRArchive::sCurrentDirID /** * @ingroup jsystem-jkernel - * + * */ class JKRArchive : public JKRFileLoader { public: @@ -192,9 +198,7 @@ public: u32 countFile() const { return mArcInfoBlock->num_file_entries; } s32 countDirectory() const { return mArcInfoBlock->num_nodes; } u8 getMountMode() const { return mMountMode; } - bool isFileEntry(u32 param_0) const { - return getFileAttribute(param_0) & 1; - } + bool isFileEntry(u32 param_0) const { return getFileAttribute(param_0) & 1; } public: /* 0x00 */ // vtable @@ -210,7 +214,36 @@ public: /* 0x54 */ const char* mStringTable; #if TARGET_PC + u32 getFileSize(SDIFileEntry* entry) const; + void* getOverlayData(SDIFileEntry* entry, u32* outSize); + bool getOverlayFileSize(SDIFileEntry* entry, u32* outSize) const; + static void notifyOverlayFilesChanged(); + +protected: void** mFileData; + + struct ArcOverlayResource { + u32 entryIndex; + u32 size; + u64 generation; + }; + + // Resource pointers remain valid until removed through the JKR resource APIs. + // That way, any pointers to old overlay data remain valid even when the overlay changed. + std::unordered_map mArcOverlayResources; + mutable std::unordered_map mActiveArcOverlayResources; + mutable std::string mArcOverlaysPath; + mutable std::unordered_map mIdxToPathMap; + mutable bool mArcOverlaysPathResolved = false; + + bool buildArcOverlaysPath() const; + void buildIndexToPathMap(u32 dirIndex, const std::string& currentPath) const; + bool getOverlayPath(SDIFileEntry* entry, std::string& path) const; + void* getActiveOverlayData(SDIFileEntry* entry, u32* outSize) const; + bool copyOverlayData(void* buffer, u32 bufferSize, SDIFileEntry* entry, u32* outSize); + bool getOverlayResourceSize(const void* data, u32* outSize) const; + bool removeOverlayResource(void* resource, bool freeResource); + void removeAllOverlayResources(); #endif protected: @@ -234,7 +267,7 @@ public: } else if (attr & JKRARCHIVE_ATTR_YAZ0) { return COMPRESSION_YAZ0; } else { - return COMPRESSION_YAY0; + return COMPRESSION_YAY0; } } @@ -243,6 +276,9 @@ public: protected: static DUSK_GAME_DATA u32 sCurrentDirID; +#if TARGET_PC + static std::atomic sArcOverlayGeneration; +#endif }; inline JKRCompression JKRConvertAttrToCompressionType(int attr) { @@ -261,8 +297,8 @@ inline bool JKRRemoveResource(void* resource, JKRFileLoader* fileLoader) { return JKRFileLoader::removeResource(resource, fileLoader); } -inline JKRArchive* JKRMountArchive(void* ptr, JKRHeap* heap, - JKRArchive::EMountDirection mountDirection) { +inline JKRArchive* JKRMountArchive( + void* ptr, JKRHeap* heap, JKRArchive::EMountDirection mountDirection) { return JKRArchive::mount(ptr, heap, mountDirection); } diff --git a/libs/JSystem/include/JSystem/JMessage/processor.h b/libs/JSystem/include/JSystem/JMessage/processor.h index cfb20c2c46..efd4c3629e 100644 --- a/libs/JSystem/include/JSystem/JMessage/processor.h +++ b/libs/JSystem/include/JSystem/JMessage/processor.h @@ -174,10 +174,14 @@ struct TProcessor { } void on_character(int iCharacter) { do_character(iCharacter); } +#if TARGET_PC + const char* on_message_limited(u16 u16Index) const; +#else const char* on_message_limited(u16 u16Index) const { JUT_ASSERT(482, pResourceCache_!=NULL); return pResourceCache_->getMessageText_messageIndex(u16Index); } +#endif bool on_setBegin_isReady_() const { return do_setBegin_isReady_(); } @@ -195,6 +199,10 @@ struct TProcessor { return 1; } +#if TARGET_PC + void* getMessageEntry_messageCode(u16 u16Code, u16 u16Index) const; + const char* getMessageText_messageCode(u16 u16Code, u16 u16Index) const; +#else void* getMessageEntry_messageCode(u16 u16Code, u16 u16Index) const { const TResource* pResource = getResource_groupID(u16Code); @@ -214,6 +222,7 @@ struct TProcessor { return pResourceCache_->getMessageText_messageEntry(pEntry); } +#endif void stack_pushCurrent_(const char* pszText) { oStack_.push(getCurrent()); diff --git a/libs/JSystem/src/JAudio2/JASResArcLoader.cpp b/libs/JSystem/src/JAudio2/JASResArcLoader.cpp index a55c73469c..57e6742c51 100644 --- a/libs/JSystem/src/JAudio2/JASResArcLoader.cpp +++ b/libs/JSystem/src/JAudio2/JASResArcLoader.cpp @@ -17,7 +17,7 @@ size_t JASResArcLoader::getResSize(JKRArchive const* i_archiveP, u16 i_resourceI return 0; } - return file->data_size; + return DUSK_IF_ELSE(i_archiveP->getFileSize(file), file->data_size); } size_t JASResArcLoader::getResMaxSize(JKRArchive const* i_archiveP) { @@ -27,9 +27,16 @@ size_t JASResArcLoader::getResMaxSize(JKRArchive const* i_archiveP) { for (index = 0; index < fileEntries; index++) { JKRArchive::SDIFileEntry* file = i_archiveP->findIdxResource(index); if (file) { +#if TARGET_PC + const u32 fileSize = i_archiveP->getFileSize(file); + if (maxSize < fileSize) { + maxSize = fileSize; + } +#else if (maxSize < file->data_size) { maxSize = file->data_size; } +#endif } } diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 0c26e77a53..466335eae9 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -38,7 +38,7 @@ void JFWDisplay::ctor_subroutine(bool enableAlpha) { mTickRate = 0; mCombinationRatio = 0.0f; field_0x30 = 0; - field_0x2c = OSGetTick(); + field_0x2c = DUSK_IF_ELSE(static_cast(OSGetNativeTime()), OSGetTick()); field_0x34 = 0; field_0x48 = 0; field_0x4a = 0; @@ -258,7 +258,7 @@ void JFWDisplay::beginRender() { waitForTick(mTickRate, mFrameRate); JUTVideo::getManager()->waitRetraceIfNeed(); - OSTick tick = OSGetTick(); + OSTick tick = DUSK_IF_ELSE(static_cast(OSGetNativeTime()), OSGetTick()); field_0x30 = tick - field_0x2c; field_0x2c = tick; field_0x34 = field_0x2c - JUTVideo::getVideoLastTick(); @@ -371,7 +371,7 @@ constexpr auto FRAME_PERIOD = std::chrono::duration_cast(sleepTime) / static_cast(targetNs)); } @@ -381,15 +381,12 @@ static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC static Limiter limiter; - if (dusk::frame_interp::is_enabled() && !dusk::getTransientSettings().skipFrameRateLimit) { - dusk::frameUsagePct = 0.f; - return; + if (dusk::frame_interp::is_enabled() || dusk::getTransientSettings().turboMode) { + limiter.Reset(); + dusk::frameUsagePct = 0.f; + return; } - if (dusk::getTransientSettings().skipFrameRateLimit) { - p1 = OS_TIMER_CLOCK / 120; - } - if (fopOvlpM_IsPeek() && dusk::getTransientSettings().stateShareLoadActive) { return; } @@ -399,7 +396,6 @@ static void waitForTick(u32 p1, u16 p2) { if (p1 != 0) { #if TARGET_PC - static Limiter limiter; waitPrecise(limiter, static_cast(OSTicksToMicroseconds(p1)) * 1000ULL); #else static OSTime nextTick = OSGetTime(); @@ -413,7 +409,6 @@ static void waitForTick(u32 p1, u16 p2) { } else { u32 uVar1 = (p2 == 0) ? 1 : p2; #if TARGET_PC - static Limiter limiter; waitPrecise(limiter, static_cast((RETRACE_PERIOD * uVar1).count())); #else static u32 nextCount = VIGetRetraceCount(); diff --git a/libs/JSystem/src/JKernel/JKRAramArchive.cpp b/libs/JSystem/src/JKernel/JKRAramArchive.cpp index d6923ee06b..fcb54271e4 100644 --- a/libs/JSystem/src/JKernel/JKRAramArchive.cpp +++ b/libs/JSystem/src/JKernel/JKRAramArchive.cpp @@ -196,6 +196,13 @@ cleanup: void* JKRAramArchive::fetchResource(SDIFileEntry* pEntry, u32* pOutSize) { JUT_ASSERT(442, isMounted()); + +#if TARGET_PC + if (void* data = getOverlayData(pEntry, pOutSize); data != nullptr) { + return data; + } +#endif + u32 outSize; u8* outBuf; if (pOutSize == NULL) { @@ -231,6 +238,13 @@ void* JKRAramArchive::fetchResource(SDIFileEntry* pEntry, u32* pOutSize) { void* JKRAramArchive::fetchResource(void* buffer, u32 bufferSize, SDIFileEntry* pEntry, u32* resourceSize) { JUT_ASSERT(515, isMounted()); + +#if TARGET_PC + if (copyOverlayData(buffer, bufferSize, pEntry, resourceSize)) { + return buffer; + } +#endif + u32 size = pEntry->data_size; if (size > bufferSize) { size = bufferSize; @@ -337,6 +351,12 @@ u32 JKRAramArchive::getExpandedResSize(const void* ptr) const { return this->getResSize(ptr); } +#if TARGET_PC + if (u32 size; getOverlayResourceSize(ptr, &size)) { + return size; + } +#endif + JKRArchive::SDIFileEntry* entry = this->findPtrResource(ptr); if (entry == NULL) { return 0xFFFFFFFF; diff --git a/libs/JSystem/src/JKernel/JKRArchivePri.cpp b/libs/JSystem/src/JKernel/JKRArchivePri.cpp index a14ab763b0..eed317a357 100644 --- a/libs/JSystem/src/JKernel/JKRArchivePri.cpp +++ b/libs/JSystem/src/JKernel/JKRArchivePri.cpp @@ -7,6 +7,37 @@ #if TARGET_PC #include +#include +#include +#include +#include +#include +#include "JSystem/JKernel/JKRDvdRipper.h" +#if _WIN32 +#include +#endif + +std::atomic JKRArchive::sArcOverlayGeneration{0}; + +namespace { + +void* alloc_overlay_buffer(u32 size) { +#if _WIN32 + return _aligned_malloc(size, alignof(std::max_align_t)); +#else + return std::malloc(size); +#endif +} + +void free_overlay_buffer(void* data) { +#if _WIN32 + _aligned_free(data); +#else + std::free(data); +#endif +} + +} // namespace #endif DUSK_GAME_DATA u32 JKRArchive::sCurrentDirID; @@ -42,6 +73,7 @@ JKRArchive::JKRArchive(s32 entryNumber, JKRArchive::EMountMode mountMode) { JKRArchive::~JKRArchive() { #if TARGET_PC + removeAllOverlayResources(); if (mFileData != nullptr) { JKRHeap::getSystemHeap()->free(mFileData); mFileData = nullptr; @@ -270,4 +302,240 @@ void JKRArchive::initFileDataPointers() { mFiles[i].index = i; } } + +void JKRArchive::notifyOverlayFilesChanged() { + sArcOverlayGeneration.fetch_add(1, std::memory_order_release); +} + +bool JKRArchive::buildArcOverlaysPath() const { + if (mArcOverlaysPathResolved) { + return !mArcOverlaysPath.empty(); + } + mArcOverlaysPathResolved = true; + + if (mEntryNum < 0) { + return false; + } + + char pathBuffer[1024]; + if (!DVDConvertEntrynumToPath(mEntryNum, pathBuffer, sizeof(pathBuffer))) { + return false; + } + + std::string path{pathBuffer}; + constexpr std::string_view extension{".arc"}; + if (path.size() < extension.size() || + path.compare(path.size() - extension.size(), extension.size(), extension) != 0) + { + return false; + } + path.resize(path.size() - extension.size()); + path.push_back('/'); + + mArcOverlaysPath = std::move(path); + return true; +} + +void JKRArchive::buildIndexToPathMap(u32 dirIndex, const std::string& currentPath) const { + const SDIDirEntry& dir = mNodes[dirIndex]; + for (int i = 0; i < dir.num_entries; i++) { + const SDIFileEntry& entry = mFiles[dir.first_file_index + i]; + std::string entryName{&mStringTable[entry.getNameOffset()]}; + if (entryName == "." || entryName == "..") { + continue; + } + if (entry.isDirectory()) { + buildIndexToPathMap(entry.data_offset, currentPath + entryName + "/"); + } else { + mIdxToPathMap[entry.index] = currentPath + entryName; + } + } +} + +bool JKRArchive::getOverlayPath(SDIFileEntry* entry, std::string& path) const { + if (entry == nullptr || !buildArcOverlaysPath()) { + return false; + } + + if (mIdxToPathMap.empty()) { + buildIndexToPathMap(0, std::string{&mStringTable[mNodes[0].name_offset]} + "/"); + } + + const auto pathIt = mIdxToPathMap.find(entry->index); + if (pathIt == mIdxToPathMap.end()) { + return false; + } + + path = mArcOverlaysPath + pathIt->second; + return true; +} + +void* JKRArchive::getActiveOverlayData(SDIFileEntry* entry, u32* outSize) const { + const auto activeIt = mActiveArcOverlayResources.find(entry->index); + if (activeIt == mActiveArcOverlayResources.end()) { + return nullptr; + } + + const auto resourceIt = mArcOverlayResources.find(activeIt->second); + const u64 generation = sArcOverlayGeneration.load(std::memory_order_acquire); + if (resourceIt == mArcOverlayResources.end() || resourceIt->second.generation != generation) { + // Keep the allocation owned so raw pointers returned by earlier fetches remain valid. + mActiveArcOverlayResources.erase(activeIt); + return nullptr; + } + + if (outSize != nullptr) { + *outSize = resourceIt->second.size; + } + return resourceIt->first; +} + +void* JKRArchive::getOverlayData(SDIFileEntry* entry, u32* outSize) { + if (entry == nullptr) { + return nullptr; + } + + if (void* data = getActiveOverlayData(entry, outSize)) { + return data; + } + + std::string path; + if (!getOverlayPath(entry, path)) { + return nullptr; + } + + constexpr u32 alignmentMask = 0x1f; + const u64 generation = sArcOverlayGeneration.load(std::memory_order_acquire); + DVDFileInfo fileInfo{}; + if (!DVDOpen(path.c_str(), &fileInfo)) { + return nullptr; + } + + const u32 logicalSize = fileInfo.length; + if (logicalSize > static_cast(std::numeric_limits::max()) - alignmentMask) { + DVDClose(&fileInfo); + return nullptr; + } + + const u32 readSize = ALIGN_NEXT(logicalSize, 0x20); + const u32 allocationSize = readSize == 0 ? 1 : readSize; + void* data = alloc_overlay_buffer(allocationSize); + if (data == nullptr) { + DVDClose(&fileInfo); + return nullptr; + } + + const s32 status = DVDReadPrio(&fileInfo, data, readSize, 0, 2); + DVDClose(&fileInfo); + if (status < DVD_RESULT_GOOD || static_cast(status) != logicalSize) { + free_overlay_buffer(data); + return nullptr; + } + + mArcOverlayResources.emplace(data, ArcOverlayResource{ + .entryIndex = entry->index, + .size = logicalSize, + .generation = generation, + }); + mActiveArcOverlayResources[entry->index] = data; + + if (outSize != nullptr) { + *outSize = logicalSize; + } + return data; +} + +bool JKRArchive::copyOverlayData(void* buffer, u32 bufferSize, SDIFileEntry* entry, u32* outSize) { + u32 overlaySize; + const void* overlayData = getOverlayData(entry, &overlaySize); + if (overlayData == nullptr) { + return false; + } + + const u32 copySize = overlaySize < bufferSize ? overlaySize : bufferSize; + if (copySize != 0) { + memcpy(buffer, overlayData, copySize); + } + if (outSize != nullptr) { + *outSize = copySize; + } + return true; +} + +bool JKRArchive::getOverlayResourceSize(const void* data, u32* outSize) const { + const auto resourceIt = mArcOverlayResources.find(const_cast(data)); + if (resourceIt == mArcOverlayResources.end()) { + return false; + } + + if (outSize != nullptr) { + *outSize = resourceIt->second.size; + } + return true; +} + +bool JKRArchive::getOverlayFileSize(SDIFileEntry* entry, u32* outSize) const { + if (entry == nullptr) { + return false; + } + + u32 activeSize; + if (getActiveOverlayData(entry, &activeSize) != nullptr) { + if (outSize != nullptr) { + *outSize = activeSize; + } + return true; + } + + std::string path; + if (!getOverlayPath(entry, path)) { + return false; + } + + DVDFileInfo fileInfo{}; + if (!DVDOpen(path.c_str(), &fileInfo)) { + return false; + } + + if (outSize != nullptr) { + *outSize = fileInfo.length; + } + DVDClose(&fileInfo); + return true; +} + +u32 JKRArchive::getFileSize(SDIFileEntry* entry) const { + u32 size; + if (getOverlayFileSize(entry, &size)) { + return size; + } + return entry != nullptr ? entry->getSize() : 0; +} + +bool JKRArchive::removeOverlayResource(void* resource, bool freeResource) { + const auto resourceIt = mArcOverlayResources.find(resource); + if (resourceIt == mArcOverlayResources.end()) { + return false; + } + + const auto activeIt = mActiveArcOverlayResources.find(resourceIt->second.entryIndex); + if (activeIt != mActiveArcOverlayResources.end() && activeIt->second == resource) { + mActiveArcOverlayResources.erase(activeIt); + } + + if (freeResource) { + free_overlay_buffer(resource); + } + mArcOverlayResources.erase(resourceIt); + return true; +} + +void JKRArchive::removeAllOverlayResources() { + mActiveArcOverlayResources.clear(); + for (const auto& key : mArcOverlayResources | std::views::keys) { + free_overlay_buffer(key); + } + mArcOverlayResources.clear(); +} + #endif diff --git a/libs/JSystem/src/JKernel/JKRArchivePub.cpp b/libs/JSystem/src/JKernel/JKRArchivePub.cpp index 731a049457..97306c9033 100644 --- a/libs/JSystem/src/JKernel/JKRArchivePub.cpp +++ b/libs/JSystem/src/JKernel/JKRArchivePub.cpp @@ -246,6 +246,7 @@ u32 JKRArchive::readResource(void* buffer, u32 bufferSize, u16 id) { void JKRArchive::removeResourceAll() { if (mArcInfoBlock && mMountMode != MOUNT_MEM) { + IF_DUSK(removeAllOverlayResources();) SDIFileEntry* fileEntry = mFiles; for (int i = 0; i < mArcInfoBlock->num_file_entries; i++) { if (JKAR_DATA(fileEntry)) { @@ -259,6 +260,13 @@ void JKRArchive::removeResourceAll() { bool JKRArchive::removeResource(void* resource) { JUT_ASSERT(678, resource != NULL); + +#if TARGET_PC + if (removeOverlayResource(resource, true)) { + return true; + } +#endif + SDIFileEntry* fileEntry = findPtrResource(resource); if (fileEntry == NULL) return false; @@ -270,6 +278,13 @@ bool JKRArchive::removeResource(void* resource) { bool JKRArchive::detachResource(void* resource) { JUT_ASSERT(707, resource != NULL); + +#if TARGET_PC + if (removeOverlayResource(resource, false)) { + return true; + } +#endif + SDIFileEntry* fileEntry = findPtrResource(resource); if (fileEntry == NULL) return false; @@ -280,6 +295,13 @@ bool JKRArchive::detachResource(void* resource) { u32 JKRArchive::getResSize(const void* resource) const { JUT_ASSERT(732, resource != NULL); + +#if TARGET_PC + if (u32 size; getOverlayResourceSize(resource, &size)) { + return size; + } +#endif + SDIFileEntry* fileEntry = findPtrResource(resource); if (fileEntry == NULL) return -1; diff --git a/libs/JSystem/src/JKernel/JKRCompArchive.cpp b/libs/JSystem/src/JKernel/JKRCompArchive.cpp index 716aaa2838..acac590ec0 100644 --- a/libs/JSystem/src/JKernel/JKRCompArchive.cpp +++ b/libs/JSystem/src/JKernel/JKRCompArchive.cpp @@ -226,6 +226,13 @@ bool JKRCompArchive::open(s32 entryNum) { void* JKRCompArchive::fetchResource(SDIFileEntry *fileEntry, u32 *pSize) { JUT_ASSERT(597, isMounted()); + +#if TARGET_PC + if (void* data = getOverlayData(fileEntry, pSize); data != nullptr) { + return data; + } +#endif + u32 ptrSize; u32 size = fileEntry->data_size; int compression = JKRConvertAttrToCompressionType(u8(fileEntry->type_flags_and_name_offset >> 0x18)); @@ -274,6 +281,13 @@ void *JKRCompArchive::fetchResource(void *data, u32 compressedSize, SDIFileEntry { u32 size = 0; JUT_ASSERT(708, isMounted()); + +#if TARGET_PC + if (copyOverlayData(data, compressedSize, fileEntry, pSize)) { + return data; + } +#endif + u32 fileSize = fileEntry->data_size; u32 alignedSize = ALIGN_NEXT(fileSize, 32); u32 fileFlag = fileEntry->type_flags_and_name_offset >> 0x18; @@ -319,6 +333,7 @@ void *JKRCompArchive::fetchResource(void *data, u32 compressedSize, SDIFileEntry void JKRCompArchive::removeResourceAll() { if (mArcInfoBlock != NULL && mMountMode != MOUNT_MEM) { + IF_DUSK(removeAllOverlayResources();) SDIFileEntry* fileEntry = mFiles; for (int i = 0; i < mArcInfoBlock->num_file_entries; i++) { int tmp = fileEntry->type_flags_and_name_offset >> 0x18; @@ -336,6 +351,12 @@ void JKRCompArchive::removeResourceAll() { } bool JKRCompArchive::removeResource(void* resource) { +#if TARGET_PC + if (removeOverlayResource(resource, true)) { + return true; + } +#endif + SDIFileEntry* fileEntry = findPtrResource(resource); if (!fileEntry) return false; @@ -356,6 +377,12 @@ u32 JKRCompArchive::getExpandedResSize(const void *resource) const return getResSize(resource); } +#if TARGET_PC + if (u32 size; getOverlayResourceSize(resource, &size)) { + return size; + } +#endif + SDIFileEntry *fileEntry = findPtrResource(resource); if(!fileEntry) { return 0xffffffff; diff --git a/libs/JSystem/src/JKernel/JKRDvdArchive.cpp b/libs/JSystem/src/JKernel/JKRDvdArchive.cpp index b82302c900..d599879f8f 100644 --- a/libs/JSystem/src/JKernel/JKRDvdArchive.cpp +++ b/libs/JSystem/src/JKernel/JKRDvdArchive.cpp @@ -147,6 +147,13 @@ cleanup: void* JKRDvdArchive::fetchResource(SDIFileEntry* fileEntry, u32* returnSize) { JUT_ASSERT(428, isMounted()); + +#if TARGET_PC + if (void* data = getOverlayData(fileEntry, returnSize); data != nullptr) { + return data; + } +#endif + u32 tempReturnSize; if (returnSize == NULL) { returnSize = &tempReturnSize; @@ -181,6 +188,13 @@ void* JKRDvdArchive::fetchResource(SDIFileEntry* fileEntry, u32* returnSize) { void* JKRDvdArchive::fetchResource(void* buffer, u32 bufferSize, SDIFileEntry* fileEntry, u32* returnSize) { JUT_ASSERT(504, isMounted()); + +#if TARGET_PC + if (copyOverlayData(buffer, bufferSize, fileEntry, returnSize)) { + return buffer; + } +#endif + u32 size = fileEntry->data_size; JKRCompression fileCompression = JKRConvertAttrToCompressionType(u8(fileEntry->type_flags_and_name_offset >> 24)); @@ -344,6 +358,12 @@ u32 JKRDvdArchive::getExpandedResSize(const void* resource) const { return getResSize(resource); } +#if TARGET_PC + if (u32 size; getOverlayResourceSize(resource, &size)) { + return size; + } +#endif + SDIFileEntry* fileEntry = findPtrResource(resource); if (!fileEntry) { return -1; diff --git a/libs/JSystem/src/JKernel/JKRMemArchive.cpp b/libs/JSystem/src/JKernel/JKRMemArchive.cpp index d3b17d8a63..f0feabe3be 100644 --- a/libs/JSystem/src/JKernel/JKRMemArchive.cpp +++ b/libs/JSystem/src/JKernel/JKRMemArchive.cpp @@ -135,6 +135,13 @@ bool JKRMemArchive::open(void* buffer, u32 bufferSize, JKRMemBreakFlag flag) { void* JKRMemArchive::fetchResource(SDIFileEntry* fileEntry, u32* resourceSize) { JUT_ASSERT(555, isMounted()); + +#if TARGET_PC + if (void* data = getOverlayData(fileEntry, resourceSize); data != nullptr) { + return data; + } +#endif + if (!JKAR_DATA(fileEntry)) { JKAR_DATA(fileEntry) = mArchiveData + fileEntry->data_offset; } @@ -149,6 +156,13 @@ void* JKRMemArchive::fetchResource(SDIFileEntry* fileEntry, u32* resourceSize) { void* JKRMemArchive::fetchResource(void* buffer, u32 bufferSize, SDIFileEntry* fileEntry, u32* resourceSize) { JUT_ASSERT(595, isMounted()); + +#if TARGET_PC + if (copyOverlayData(buffer, bufferSize, fileEntry, resourceSize)) { + return buffer; + } +#endif + u32 srcLength = fileEntry->data_size; if (srcLength > bufferSize) { srcLength = bufferSize; @@ -173,6 +187,8 @@ void* JKRMemArchive::fetchResource(void* buffer, u32 bufferSize, SDIFileEntry* f void JKRMemArchive::removeResourceAll(void) { JUT_ASSERT(642, isMounted()); + IF_DUSK(removeAllOverlayResources();) + if (mArcInfoBlock == NULL) return; if (mMountMode == MOUNT_MEM) @@ -192,6 +208,12 @@ void JKRMemArchive::removeResourceAll(void) { bool JKRMemArchive::removeResource(void* resource) { JUT_ASSERT(673, isMounted()); +#if TARGET_PC + if (removeOverlayResource(resource, true)) { + return true; + } +#endif + SDIFileEntry* fileEntry = findPtrResource(resource); if (!fileEntry) return false; @@ -231,6 +253,12 @@ u32 JKRMemArchive::fetchResource_subroutine(u8* src, u32 srcLength, u8* dst, u32 } u32 JKRMemArchive::getExpandedResSize(const void* resource) const { +#if TARGET_PC + if (u32 overlaySize; getOverlayResourceSize(resource, &overlaySize)) { + return overlaySize; + } +#endif + SDIFileEntry* fileEntry = findPtrResource(resource); if (fileEntry == NULL) return -1; diff --git a/libs/JSystem/src/JMessage/control.cpp b/libs/JSystem/src/JMessage/control.cpp index acd5387f77..e739d2f672 100644 --- a/libs/JSystem/src/JMessage/control.cpp +++ b/libs/JSystem/src/JMessage/control.cpp @@ -7,6 +7,10 @@ #include "JSystem/JMessage/control.h" +#if TARGET_PC +#include "dusk/mods/svc/flow.hpp" +#endif + JMessage::TControl::TControl() : pSequenceProcessor_(NULL), pRenderingProcessor_(NULL), @@ -19,9 +23,16 @@ JMessage::TControl::TControl() pMessageText_current_(NULL) {} -JMessage::TControl::~TControl() {} +JMessage::TControl::~TControl() { +#if TARGET_PC + dusk::flow::release_message_control(this); +#endif +} void JMessage::TControl::reset() { +#if TARGET_PC + dusk::flow::release_message_control(this); +#endif pEntry_ = NULL; pMessageText_begin_ = NULL; pszText_update_current_ = NULL; @@ -78,18 +89,57 @@ int JMessage::TControl::setMessageID(u32 uMsgID, u32 param_1, bool* pbValid) { } bool JMessage::TControl::setMessageCode_inSequence_(JMessage::TProcessor const* pProcessor, u16 u16GroupID, u16 u16Index) { +#if TARGET_PC + const TResource* resource = NULL; + const void* resolvedEntry = NULL; + const char* resolvedText = NULL; + if (u16Index >= dusk::flow::kCustomMessageMin) { + if (!dusk::flow::custom_message_for_processor( + this, pProcessor, u16Index, resource, resolvedEntry, resolvedText)) + { + return false; + } + const_cast(pProcessor)->setResourceCache(const_cast(resource)); + } else { + resource = pProcessor->getResource_groupID(u16GroupID); + if (resource == NULL) { + return false; + } + const void* nativeEntry = resource->getMessageEntry_messageIndex(u16Index); + const char* nativeText = + nativeEntry != NULL ? resource->getMessageText_messageEntry(nativeEntry) : NULL; + resolvedEntry = nativeEntry; + resolvedText = nativeText; + dusk::flow::resolve_message_for_control(this, resource->oParse_THeader_.getRaw(), u16Index, + nativeEntry, nativeText, resolvedEntry, resolvedText); + } + pEntry_ = const_cast(resolvedEntry); + if (pEntry_ == NULL || resolvedText == NULL) { + return false; + } +#else pEntry_ = pProcessor->getMessageEntry_messageCode(u16GroupID, u16Index); if (pEntry_ == NULL) { return false; } +#endif +#if TARGET_PC + uMessageGroupID_ = resource->getGroupID(); + pResourceCache_ = resource; +#else uMessageGroupID_ = u16GroupID; - uMessageID_ = u16Index; - pResourceCache_ = pProcessor->getResourceCache(); +#endif + uMessageID_ = u16Index; + JUT_ASSERT(155, pResourceCache_!=NULL); +#if TARGET_PC + pMessageText_begin_ = resolvedText; +#else pMessageText_begin_ = pResourceCache_->getMessageText_messageEntry(pEntry_); +#endif pMessageText_current_ = pMessageText_begin_; oStack_renderingProcessor_.clear(); return true; diff --git a/libs/JSystem/src/JMessage/processor.cpp b/libs/JSystem/src/JMessage/processor.cpp index eef50e7516..08b0af0b7a 100644 --- a/libs/JSystem/src/JMessage/processor.cpp +++ b/libs/JSystem/src/JMessage/processor.cpp @@ -5,15 +5,26 @@ #include "JSystem/JUtility/JUTAssert.h" #include +#if TARGET_PC +#include "dusk/mods/svc/flow.hpp" +#endif + JMessage::TReference::~TReference() {} const char* JMessage::TReference::do_word(u32 param_0) const { return NULL; } -JMessage::TProcessor::~TProcessor() {} +JMessage::TProcessor::~TProcessor() { +#if TARGET_PC + dusk::flow::release_message_processor(this); +#endif +} void JMessage::TProcessor::reset() { +#if TARGET_PC + dusk::flow::release_message_processor(this); +#endif on_resetStatus_(NULL); do_reset(); } @@ -44,6 +55,15 @@ const JMessage::TResource* JMessage::TProcessor::getResource_groupID(u16 u16Grou } u32 JMessage::TProcessor::toMessageCode_messageID(u32 uMsgID, u32 param_1, bool* pbValid) const { +#if TARGET_PC + u32 customCode = 0; + if (dusk::flow::message_code_for_id(this, uMsgID, customCode)) { + if (pbValid != NULL) { + *pbValid = true; + } + return customCode; + } +#endif const TResource* pResourceCache = (const TResource*)getResourceCache(); if (pResourceCache != NULL) { u16 u16Index = pResourceCache->toMessageIndex_messageID(uMsgID, param_1, pbValid); @@ -73,6 +93,74 @@ u32 JMessage::TProcessor::toMessageCode_messageID(u32 uMsgID, u32 param_1, bool* return 0xFFFFFFFF; } +#if TARGET_PC +void* JMessage::TProcessor::getMessageEntry_messageCode(u16 groupId, u16 index) const { + if (index >= dusk::flow::kCustomMessageMin) { + const TResource* customResource = NULL; + const void* customEntry = NULL; + const char* customText = NULL; + if (!dusk::flow::custom_message_for_processor( + NULL, this, index, customResource, customEntry, customText)) + { + return NULL; + } + const_cast(this)->setResourceCache(const_cast(customResource)); + return const_cast(customEntry); + } + const TResource* resource = getResource_groupID(groupId); + if (resource == NULL) { + return NULL; + } + void* nativeEntry = resource->getMessageEntry_messageIndex(index); + const char* nativeText = + nativeEntry != NULL ? resource->getMessageText_messageEntry(nativeEntry) : NULL; + const void* resolvedEntry = nativeEntry; + const char* resolvedText = nativeText; + dusk::flow::resolve_message(this, resource->oParse_THeader_.getRaw(), index, nativeEntry, + nativeText, resolvedEntry, resolvedText); + return const_cast(resolvedEntry); +} + +const char* JMessage::TProcessor::getMessageText_messageCode(u16 groupId, u16 index) const { + if (index >= dusk::flow::kCustomMessageMin) { + const TResource* customResource = NULL; + const void* customEntry = NULL; + const char* customText = NULL; + if (!dusk::flow::custom_message_for_processor( + NULL, this, index, customResource, customEntry, customText)) + { + return NULL; + } + const_cast(this)->setResourceCache(const_cast(customResource)); + return customText; + } + const TResource* resource = getResource_groupID(groupId); + if (resource == NULL) { + return NULL; + } + const void* nativeEntry = resource->getMessageEntry_messageIndex(index); + const char* nativeText = + nativeEntry != NULL ? resource->getMessageText_messageEntry(nativeEntry) : NULL; + const void* resolvedEntry = nativeEntry; + const char* resolvedText = nativeText; + dusk::flow::resolve_message(this, resource->oParse_THeader_.getRaw(), index, nativeEntry, + nativeText, resolvedEntry, resolvedText); + return resolvedText; +} + +const char* JMessage::TProcessor::on_message_limited(u16 index) const { + JUT_ASSERT(482, pResourceCache_!=NULL); + const void* nativeEntry = pResourceCache_->getMessageEntry_messageIndex(index); + const char* nativeText = + nativeEntry != NULL ? pResourceCache_->getMessageText_messageEntry(nativeEntry) : NULL; + const void* resolvedEntry = nativeEntry; + const char* resolvedText = nativeText; + dusk::flow::resolve_message(this, pResourceCache_->oParse_THeader_.getRaw(), index, + nativeEntry, nativeText, resolvedEntry, resolvedText); + return resolvedText; +} +#endif + void JMessage::TProcessor::on_select_begin(char const* (*pfn)(JMessage::TProcessor*), void const* pOffset, char const* pcBase, u32 uNumber) { JUT_ASSERT(191, uNumber>0); diff --git a/libs/JSystem/src/JUtility/JUTResFont.cpp b/libs/JSystem/src/JUtility/JUTResFont.cpp index 4ec23dff44..3f31961d1f 100644 --- a/libs/JSystem/src/JUtility/JUTResFont.cpp +++ b/libs/JSystem/src/JUtility/JUTResFont.cpp @@ -71,7 +71,7 @@ void JUTResFont::initJoinedTexture() { int pageCount = 0; u32 pageNumCells = block.numRows * block.numColumns; - if (dusk::version::getGameVersion() == dusk::version::GameVersion::GcnJpn) { + if (dusk::version::isRegionJpn()) { pageCount = 1; if (pageNumCells > 0 && block.endCode > block.startCode) { pageCount = (block.endCode - block.startCode + pageNumCells - 1) / pageNumCells; diff --git a/libs/JSystem/src/JUtility/JUTVideo.cpp b/libs/JSystem/src/JUtility/JUTVideo.cpp index 2135c8fc22..663c357e17 100644 --- a/libs/JSystem/src/JUtility/JUTVideo.cpp +++ b/libs/JSystem/src/JUtility/JUTVideo.cpp @@ -42,7 +42,7 @@ JUTVideo::JUTVideo(GXRenderModeObj const* param_0) { mRetraceCount = VIGetRetraceCount(); field_0x10 = 1; field_0x18 = 0; - sVideoLastTick = OSGetTick(); + sVideoLastTick = DUSK_IF_ELSE(static_cast(OSGetNativeTime()), OSGetTick()); sVideoInterval = 670000; mPreRetraceCallback = VISetPreRetraceCallback(preRetraceProc); mPostRetraceCallback = VISetPostRetraceCallback(postRetraceProc); @@ -68,8 +68,13 @@ void JUTVideo::preRetraceProc(u32 retrace_count) { (*sManager->mPreCallback)(retrace_count); } - OSTick tick = OSGetTick(); + OSTick tick = DUSK_IF_ELSE(static_cast(OSGetNativeTime()), OSGetTick()); sVideoInterval = tick - sVideoLastTick; +#if TARGET_PC + if (sVideoInterval <= 0) { + sVideoInterval = 1; + } +#endif sVideoLastTick = tick; JUTXfb* xfb = JUTXfb::getManager(); diff --git a/mods/ao_mod/src/mod.cpp b/mods/ao_mod/src/mod.cpp index fb36ddb40c..8a96ae3c95 100644 --- a/mods/ao_mod/src/mod.cpp +++ b/mods/ao_mod/src/mod.cpp @@ -53,6 +53,7 @@ ResourceBuffer g_denoiseSource = RESOURCE_BUFFER_INIT; ResourceBuffer g_compositeSource = RESOURCE_BUFFER_INIT; GfxDeviceInfo g_deviceInfo = GFX_DEVICE_INFO_INIT; +GfxRenderTargetLayout g_sceneTargetLayout = GFX_RENDER_TARGET_LAYOUT_INIT; WGPUComputePipeline g_preprocessPipeline = nullptr; WGPUComputePipeline g_mip4Pipeline = nullptr; WGPUComputePipeline g_gtaoPipeline = nullptr; @@ -226,19 +227,17 @@ bool build_composite_pipeline( .dstFactor = WGPUBlendFactor_One, }, }; - WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT; - colorTarget.format = g_deviceInfo.color_format; - if (blend) { - colorTarget.blend = &blendState; - } + WGPUColorTargetState colorTargets[GFX_MAX_COLOR_ATTACHMENTS]; + const uint32_t colorTargetCount = gfx_init_color_target_states( + &g_sceneTargetLayout, colorTargets, blend ? &blendState : nullptr, WGPUColorWriteMask_All); WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT; fragment.module = module; fragment.entryPoint = {"fs_main", WGPU_STRLEN}; - fragment.targetCount = 1; - fragment.targets = &colorTarget; + fragment.targetCount = colorTargetCount; + fragment.targets = colorTargets; // Depth state must match the EFB pass despite never touching depth. WGPUDepthStencilState depthStencil = WGPU_DEPTH_STENCIL_STATE_INIT; - depthStencil.format = g_deviceInfo.depth_format; + depthStencil.format = g_sceneTargetLayout.depth_stencil_format; depthStencil.depthWriteEnabled = WGPUOptionalBool_False; depthStencil.depthCompare = WGPUCompareFunction_Always; @@ -248,7 +247,7 @@ bool build_composite_pipeline( pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN}; pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList; pipelineDesc.depthStencil = &depthStencil; - pipelineDesc.multisample.count = g_deviceInfo.sample_count; + pipelineDesc.multisample.count = g_sceneTargetLayout.sample_count; pipelineDesc.fragment = &fragment; outPipeline = wgpuDeviceCreateRenderPipeline(g_deviceInfo.device, &pipelineDesc); wgpuShaderModuleRelease(module); @@ -504,7 +503,7 @@ void on_compute( // Render worker thread: composite the AO over the scene (or show it, in debug view). void on_draw( ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) { - if (payloadSize != sizeof(CompositePayload)) { + if (payloadSize != sizeof(CompositePayload) || ctx->layout.key != g_sceneTargetLayout.key) { return; } CompositePayload data; @@ -816,6 +815,9 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) { return mods::set_error(error, MOD_ERROR, "failed to query device info"); } + if (svc_gfx->get_scene_target_layout(mod_ctx, &g_sceneTargetLayout) != MOD_OK) { + return mods::set_error(error, MOD_ERROR, "failed to query scene target layout"); + } if (!build_compute_pipeline("AO preprocess depth", g_preprocessSource, "preprocess_depth", g_preprocessPipeline, g_preprocessLayout) || !build_compute_pipeline("AO downsample mip4", g_preprocessSource, "downsample_mip4", diff --git a/mods/flow_demo/CMakeLists.txt b/mods/flow_demo/CMakeLists.txt new file mode 100644 index 0000000000..8487073dae --- /dev/null +++ b/mods/flow_demo/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.25) +project(flow_demo CXX) + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root") + option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + if (DUSK_MOD_USE_FULL_TREE) + add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL) + else () + add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL) + endif () +endif () + +add_mod(flow_demo + FEATURES fmt + SOURCES src/mod.cpp + MOD_JSON mod.json +) diff --git a/mods/flow_demo/mod.json b/mods/flow_demo/mod.json new file mode 100644 index 0000000000..90d395bdcc --- /dev/null +++ b/mods/flow_demo/mod.json @@ -0,0 +1,7 @@ +{ + "id": "dev.twilitrealm.flow_demo", + "name": "[Demo] Flow & Messages", + "version": "1.0.0", + "author": "Twilit Realm", + "description": "Adds a FlowService demonstration submenu to Midna's menu." +} diff --git a/mods/flow_demo/src/mod.cpp b/mods/flow_demo/src/mod.cpp new file mode 100644 index 0000000000..e4d18a90f0 --- /dev/null +++ b/mods/flow_demo/src/mod.cpp @@ -0,0 +1,300 @@ +#include "mods/service.hpp" +#include "mods/svc/flow.hpp" +#include "mods/svc/log.hpp" + +#include +#include +#include +#include +#include +#include + +DEFINE_MOD(); +IMPORT_SERVICE(LogService, svc_log); +IMPORT_SERVICE(FlowService, svc_flow); +IMPORT_SERVICE(MessageService, svc_message); + +namespace { + +// The Midna flow lives in BMG group 0. +constexpr uint16_t kMessageGroup = 0; +// Midna's "speaker" value for messages. +constexpr uint16_t kMidnaSpeaker = 21; + +// Vanilla flow node/branch/edge IDs that we reference or override. +constexpr uint16_t kMidnaPromptHumanNode = 0x018c; +constexpr uint16_t kMidnaPromptWolfNode = 0x018d; +constexpr uint16_t kMidnaHumanBranch = 0x0190; +constexpr uint16_t kMidnaWolfBranch = 0x0193; +constexpr uint16_t kMidnaTalkNode = 0x018f; +constexpr uint16_t kMidnaHumanTalkEdge = 0x0113; +constexpr uint16_t kMidnaWolfTalkEdge = 0x0119; + +// For vanilla messages, we need both the entry index and the message ID. +// For registered custom messages, they're the same. +constexpr uint16_t kMidnaMenuPromptEntry = 3003; // message index used in flow message data +constexpr uint16_t kMidnaMenuPromptId = 2042; // message ID used to override + +constexpr std::array kAllLanguages{ + MESSAGE_LANGUAGE_ENGLISH, + MESSAGE_LANGUAGE_GERMAN, + MESSAGE_LANGUAGE_FRENCH, + MESSAGE_LANGUAGE_SPANISH, + MESSAGE_LANGUAGE_ITALIAN, + MESSAGE_LANGUAGE_JAPANESE, +}; + +constexpr mods::flow::MessageStyle kResponseStyle = + mods::flow::MessageStyle{}.speaker(kMidnaSpeaker).box_kind(MESSAGE_BOX_MIDNA); +constexpr mods::flow::MessageStyle kPromptStyle = + kResponseStyle.draw_type(MESSAGE_DRAW_INSTANT).talk_anim(31).face_anim(31); + +mods::flow::Query g_demoQuery; +mods::flow::Event g_demoEvent; +mods::flow::Graph g_graph; +std::vector g_messages; +std::vector g_overrides; +std::vector g_visitedPromptText; +uint32_t g_queryExecutions = 0; +uint32_t g_eventExecutions = 0; + +mods::flow::MessageBuilder build_prompt(std::string_view suffix) { + return mods::flow::MessageBuilder{kPromptStyle} + .text("What is it, ") + .text_color(MESSAGE_COLOR_RED) + .player_name() + .text_color(MESSAGE_COLOR_DEFAULT) + .text("?\n") + .text_scale(85) + .text(suffix) + .text_scale(100) + .await_choice(); +} + +// Registers a single message for all languages. +mods::flow::RegisteredMessage register_message(const mods::flow::MessageBuilder& builder) { + std::vector variants; + variants.reserve(kAllLanguages.size()); + for (const MessageLanguage language : kAllLanguages) { + variants.push_back(builder.build(language)); + } + return mods::flow::register_message(kMessageGroup, variants); +} + +ModResult add_message(const mods::flow::MessageBuilder& builder, MessageId& outId) { + auto message = register_message(builder); + if (!message) { + return message.result(); + } + outId = message.id(); + g_messages.push_back(std::move(message)); + return MOD_OK; +} + +ModResult add_fixed_override( + uint16_t messageId, const std::vector& text, bool addCallback) { + for (const MessageLanguage language : kAllLanguages) { + auto fixed = + mods::flow::override_message(kMessageGroup, messageId, language, std::span{text}); + if (!fixed) { + return fixed.result(); + } + g_overrides.push_back(std::move(fixed)); + if (!addCallback) { + continue; + } + auto callback = mods::flow::override_message_fn(kMessageGroup, messageId, language, + [](ModContext*, const MessageOverrideContext*, MessageTextData* outText, + void*) -> bool { + if (g_eventExecutions == 0 || outText == nullptr) { + return false; + } + outText->text = g_visitedPromptText.data(); + outText->text_size = g_visitedPromptText.size(); + return true; + }); + if (!callback) { + return callback.result(); + } + g_overrides.push_back(std::move(callback)); + } + return MOD_OK; +} + +uint16_t demo_query(ModContext*, const FlowQueryContext* query, void*) { + if (query == nullptr || query->parameter != 1234 || query->result_count < 3) { + return 0; + } + const auto result = static_cast(g_queryExecutions % 3); + if (query->phase == FLOW_QUERY_PHASE_EXECUTE) { + ++g_queryExecutions; + mods::log::info("Flow demo query selected result {}", result); + } + return result; +} + +void demo_event(ModContext*, const FlowEventContext* event, void*) { + ++g_eventExecutions; + const uint8_t path = event != nullptr ? event->parameters[3] : 0; + mods::log::info("Flow demo event {} executed (activation {})", path, g_eventExecutions); +} + +} // namespace + +extern "C" { +MOD_EXPORT ModResult mod_initialize(ModError* outError) { + g_queryExecutions = 0; + g_eventExecutions = 0; + + MessageId humanSelectionId = 0; + MessageId wolfSelectionId = 0; + MessageId labPromptId = 0; + MessageId labSelectionId = 0; + MessageId formattedId = 0; + MessageId evenId = 0; + MessageId oddId = 0; + MessageId timeoutId = 0; + + auto result = add_message(mods::flow::MessageBuilder{} + .speaker(kMidnaSpeaker) + .options("Transform into human", "Warp", "Flow demo"), + humanSelectionId); + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{} + .speaker(kMidnaSpeaker) + .options("Transform into wolf", "Warp", "Flow demo"), + wolfSelectionId); + } + if (result == MOD_OK) { + result = add_message(build_prompt("Choose a FlowService test."), labPromptId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{} + .speaker(kMidnaSpeaker) + .options("Formatted message", "Callback branch", "Talk to Midna"), + labSelectionId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .text_color(MESSAGE_COLOR_DEFAULT) + .text("Hello from ") + .text_color(MESSAGE_COLOR_RED) + .text("FlowService") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(", ") + .player_name() + .text("!\n") + .character_delay(3) + .text("This text is slow. ") + .character_delay(0) + .pause(12) + .text_scale(125) + .text("Big") + .text_scale(100) + .text(" text too.") + .input_after_delay(15), + formattedId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .text("Query returned ") + .text_color(MESSAGE_COLOR_GREEN) + .text("zero") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(".") + .auto_advance(75), + evenId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .box_kind(MESSAGE_BOX_LIGHT_SPIRIT) + .text("Query returned ") + .text_color(MESSAGE_COLOR_RED) + .text("one") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(".") + .auto_advance(75), + oddId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .text("Query returned ") + .text_color(MESSAGE_COLOR_YELLOW) + .text("three") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(".\nThis will timeout after three seconds.") + .input_or_timeout(90), + timeoutId); + } + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to register custom demo messages"); + } + + g_visitedPromptText = + build_prompt("A custom event has run.").build(MESSAGE_LANGUAGE_ENGLISH).text(); + result = add_fixed_override(kMidnaMenuPromptId, + build_prompt("Flow demo is installed.").build(MESSAGE_LANGUAGE_ENGLISH).text(), true); + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to register demo message overrides"); + } + + g_demoQuery = mods::flow::register_query("flow_demo cycling branch", demo_query); + g_demoEvent = mods::flow::register_event("flow_demo activation", demo_event); + if (!g_demoQuery || !g_demoEvent) { + const auto callbackResult = !g_demoQuery ? g_demoQuery.result() : g_demoEvent.result(); + return mods::set_error(outError, callbackResult, "failed to register flow callbacks"); + } + + // Set up our demo graph + mods::flow::GraphBuilder graph{kMessageGroup}; + const auto demoSetup = graph.add_event(FLOW_EVENT_SELECT_VERTICAL, {0, 0, 0, 4}); + const auto formattedMessage = graph.add_message(formattedId).next(demoSetup); + const auto formattedEvent = + graph.add_event(g_demoEvent.id(), {0, 0, 0, 1}).next(formattedMessage); + const auto evenMessage = graph.add_message(evenId).next(demoSetup); + const auto evenEvent = graph.add_event(g_demoEvent.id(), {0, 0, 0, 2}).next(evenMessage); + const auto oddMessage = graph.add_message(oddId).next(demoSetup); + const auto oddEvent = graph.add_event(g_demoEvent.id(), {0, 0, 0, 3}).next(oddMessage); + const auto timeoutMessage = graph.add_message(timeoutId).next(demoSetup); + const auto timeoutEvent = graph.add_event(g_demoEvent.id(), {0, 0, 0, 4}).next(timeoutMessage); + const auto callbackBranch = + graph.add_branch(g_demoQuery.id(), 1234).results({evenEvent, oddEvent, timeoutEvent}); + const auto choiceBranch = + graph.add_branch(FLOW_QUERY_SELECT_3_CANCEL, 0) + .results({formattedEvent, callbackBranch, kMidnaTalkNode, mods::flow::kEnd}); + const auto demoSelection = graph.add_message(labSelectionId).next(choiceBranch); + const auto demoPrompt = graph.add_message(labPromptId).next(demoSelection); + demoSetup.next(demoPrompt); + + // Patch original nodes and edges so they flow into our custom graph + const auto humanSelection = graph.add_message(humanSelectionId).next(kMidnaHumanBranch); + const auto wolfSelection = graph.add_message(wolfSelectionId).next(kMidnaWolfBranch); + graph.patch_node( + kMidnaPromptHumanNode, mods::flow::message(0, kMidnaMenuPromptEntry, humanSelection)); + graph.patch_node( + kMidnaPromptWolfNode, mods::flow::message(0, kMidnaMenuPromptEntry, wolfSelection)); + graph.patch_edge(kMidnaHumanTalkEdge, demoSetup); + graph.patch_edge(kMidnaWolfTalkEdge, demoSetup); + + g_graph = graph.commit(); + if (!g_graph) { + return mods::set_error(outError, g_graph.result(), "failed to commit the flow demo graph"); + } + + mods::log::info("Flow demo ready: {} custom messages", g_messages.size()); + return MOD_OK; +} + +MOD_EXPORT ModResult mod_update(ModError*) { + return MOD_OK; +} + +MOD_EXPORT ModResult mod_shutdown(ModError*) { + mods::log::info("Flow demo unloaded after {} events", g_eventExecutions); + g_graph.reset(); + g_overrides.clear(); + g_messages.clear(); + g_visitedPromptText.clear(); + return MOD_OK; +} +} diff --git a/mods/shadow_mod/src/mod.cpp b/mods/shadow_mod/src/mod.cpp index 36f33de41d..fa4056040b 100644 --- a/mods/shadow_mod/src/mod.cpp +++ b/mods/shadow_mod/src/mod.cpp @@ -20,12 +20,12 @@ #include "dolphin/gx/GXPixel.h" #include "dolphin/gx/GXTransform.h" #include "m_Do/m_Do_mtx.h" -#include "mods/svc/hook.hpp" #include "mods/service.hpp" #include "mods/svc/camera.h" #include "mods/svc/config.h" #include "mods/svc/gfx.h" #include "mods/svc/hook.h" +#include "mods/svc/hook.hpp" #include "mods/svc/log.h" #include "mods/svc/resource.h" #include "mods/svc/ui.h" @@ -69,6 +69,7 @@ GfxStageHookHandle g_frameBeforeHudHook = 0; UiWindowHandle g_controlsWindow = 0; ResourceBuffer g_shaderSource = RESOURCE_BUFFER_INIT; GfxDeviceInfo g_deviceInfo = GFX_DEVICE_INFO_INIT; +GfxRenderTargetLayout g_sceneTargetLayout = GFX_RENDER_TARGET_LAYOUT_INIT; WGPURenderPipeline g_compositePipeline = nullptr; // multiply blend WGPURenderPipeline g_compositeDebugPipeline = nullptr; // no blend (debug views) WGPUBindGroupLayout g_compositeLayout = nullptr; @@ -399,18 +400,16 @@ bool build_composite_pipeline( .srcFactor = WGPUBlendFactor_Zero, .dstFactor = WGPUBlendFactor_One}, }; - WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT; - colorTarget.format = g_deviceInfo.color_format; - if (blend) { - colorTarget.blend = &blendState; - } + WGPUColorTargetState colorTargets[GFX_MAX_COLOR_ATTACHMENTS]; + const uint32_t colorTargetCount = gfx_init_color_target_states( + &g_sceneTargetLayout, colorTargets, blend ? &blendState : nullptr, WGPUColorWriteMask_All); WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT; fragment.module = module; fragment.entryPoint = {"fs_main", WGPU_STRLEN}; - fragment.targetCount = 1; - fragment.targets = &colorTarget; + fragment.targetCount = colorTargetCount; + fragment.targets = colorTargets; WGPUDepthStencilState depthStencil = WGPU_DEPTH_STENCIL_STATE_INIT; - depthStencil.format = g_deviceInfo.depth_format; + depthStencil.format = g_sceneTargetLayout.depth_stencil_format; depthStencil.depthWriteEnabled = WGPUOptionalBool_False; depthStencil.depthCompare = WGPUCompareFunction_Always; @@ -420,7 +419,7 @@ bool build_composite_pipeline( pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN}; pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList; pipelineDesc.depthStencil = &depthStencil; - pipelineDesc.multisample.count = g_deviceInfo.sample_count; + pipelineDesc.multisample.count = g_sceneTargetLayout.sample_count; pipelineDesc.fragment = &fragment; outPipeline = wgpuDeviceCreateRenderPipeline(g_deviceInfo.device, &pipelineDesc); wgpuShaderModuleRelease(module); @@ -518,7 +517,7 @@ WGPUBindGroup create_composite_bind_group(WGPUDevice device, WGPUBindGroupLayout // Render worker thread: fullscreen deferred-shadow composite. void on_draw( ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) { - if (payloadSize != sizeof(DrawPayload)) { + if (payloadSize != sizeof(DrawPayload) || ctx->layout.key != g_sceneTargetLayout.key) { return; } DrawPayload data; @@ -1243,6 +1242,9 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) { return mods::set_error(error, MOD_ERROR, "failed to query device info"); } + if (svc_gfx->get_scene_target_layout(mod_ctx, &g_sceneTargetLayout) != MOD_OK) { + return mods::set_error(error, MOD_ERROR, "failed to query scene target layout"); + } if (!build_composite_pipeline(true, g_compositePipeline, g_compositeLayout) || !build_composite_pipeline(false, g_compositeDebugPipeline, g_compositeDebugLayout)) { diff --git a/mods/window_demo/src/mod.cpp b/mods/window_demo/src/mod.cpp index fb3cc31809..00d6f1240b 100644 --- a/mods/window_demo/src/mod.cpp +++ b/mods/window_demo/src/mod.cpp @@ -189,12 +189,6 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { return mods::set_error(error, MOD_ERROR, "failed to register mod panel"); } - if (open_window() != MOD_OK) { - svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook); - g_stageHook = 0; - return mods::set_error(error, MOD_ERROR, "failed to open auxiliary window"); - } - mods::log::info("auxiliary WebGPU window ready"); return MOD_OK; } diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 054b6e0183..6cc5da65ae 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -143,6 +143,74 @@ eyebrow span { transition: decorator color opacity 0.1s linear-in-out; } +#menu-list button.game-mode-button { + position: relative; + padding: 0; + overflow: visible; +} + +#menu-list game-mode-label-viewport { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 8dp; + pointer-events: none; + z-index: 0; +} + +#menu-list game-mode-label { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: 8dp 16dp; + opacity: 0; + text-overflow: ellipsis; + white-space: nowrap; + transition: opacity 0.24s cubic-in-out; +} + +#menu-list game-mode-label.active { + opacity: 1; +} + +#menu-list game-mode-previous, +#menu-list game-mode-next { + display: none; + position: absolute; + top: 0; + width: 48dp; + height: 54dp; + align-items: center; + justify-content: center; + color: #FFFFFF; + font-family: "Material Symbols Rounded"; + font-weight: normal; + font-size: 30dp; + z-index: 1; +} + +#menu-list game-mode-previous { + left: -48dp; + decorator: text("" center center); +} + +#menu-list game-mode-next { + right: -48dp; + decorator: text("" center center); +} + +#menu-list button.game-mode-button.can-cycle game-mode-previous, +#menu-list button.game-mode-button.can-cycle game-mode-next { + display: flex; +} + #menu-list button:hover, #menu-list button:focus-visible { color: black; diff --git a/res/rml/window.rcss b/res/rml/window.rcss index ab338e97c7..8ca2f80760 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -519,3 +519,13 @@ progress.verification-progress-bar { flex: 0 0 auto; padding-top: 4dp; } + +.modal-actions-vertical { + flex-direction: column; + align-items: stretch; +} + +.modal-actions-vertical button.modal-btn { + flex: 0 0 auto; + width: 100%; +} diff --git a/sdk/include/mods/bits.hpp b/sdk/include/mods/bits.hpp new file mode 100644 index 0000000000..b8da8ad93e --- /dev/null +++ b/sdk/include/mods/bits.hpp @@ -0,0 +1,6 @@ +#pragma once + +// Really lazy way to avoid duplicating these helpers +#define dusk mods +#include "../../../include/helpers/bits.hpp" +#undef dusk diff --git a/sdk/include/mods/svc/flow.h b/sdk/include/mods/svc/flow.h new file mode 100644 index 0000000000..a9028df0a4 --- /dev/null +++ b/sdk/include/mods/svc/flow.h @@ -0,0 +1,182 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define FLOW_SERVICE_ID "dev.twilitrealm.dusklight.flow" +#define FLOW_SERVICE_MAJOR 1u +#define FLOW_SERVICE_MINOR 0u + +typedef uint64_t FlowGraphHandle; +typedef uint16_t FlowQueryId; +typedef uint8_t FlowEventId; + +/* FLW1 node data */ +typedef struct FlowNodeData { + uint8_t bytes[8]; +} FlowNodeData; +static_assert(sizeof(FlowNodeData) == 8); + +/* Built-in branch queries. The result selects the edge slot next_node_index + result; + * [param] is the branch node's 16-bit parameter. */ +typedef enum FlowGameQuery { + FLOW_QUERY_SELECT_2 = 0, /* two-way selection result: 0 first, 1 second */ + FLOW_QUERY_EVENT_FLAG = 1, /* 0 if event flag [param] is set */ + FLOW_QUERY_PLAYER_FORM = 2, /* 0 human, 1 wolf, 2 riding */ + FLOW_QUERY_RANDOM = 3, /* random result in [0, param) */ + FLOW_QUERY_SELECT_3 = 4, /* three-way selection result: 0/1/2 */ + FLOW_QUERY_TALK_DISTANCE = 5, /* player within talk range; param overrides max distance */ + FLOW_QUERY_RUPEES = 6, /* 0 if rupees >= param; param 0 checks wallet max */ + FLOW_QUERY_SWORD_TUTORIAL_STEP = 7, /* 0 if the scarecrow tutorial step matches param */ + FLOW_QUERY_SWORD_TUTORIAL_RESULT = 8, /* 0 on tutorial success */ + FLOW_QUERY_SWORD_TUTORIAL_COUNT = 9, /* 0 if first success */ + FLOW_QUERY_TEMP_FLAG = 10, /* 0 if temporary event flag [param] is set */ + FLOW_QUERY_CHEST_FLAG = 11, /* 0 if treasure chest flag [param] is set */ + FLOW_QUERY_SAVE_SWITCH = 12, /* 0 if save switch [param] is set */ + FLOW_QUERY_SAVE_ITEM_FLAG = 13, + FLOW_QUERY_DUNGEON_SWITCH = 14, + FLOW_QUERY_DUNGEON_ITEM_FLAG = 15, + FLOW_QUERY_ZONE_SWITCH = 16, + FLOW_QUERY_ZONE_ITEM_FLAG = 17, + FLOW_QUERY_ONE_ZONE_SWITCH = 18, + FLOW_QUERY_ONE_ZONE_ITEM_FLAG = 19, + FLOW_QUERY_EQUIPPED = 20, /* 1 if item [param] is equipped or on an item slot */ + FLOW_QUERY_ITEM_OWNED = 21, /* 0 if item [param] is owned */ + FLOW_QUERY_BOMB_BAG_COUNT = 22, /* number of bomb bags owned: 0-3 */ + FLOW_QUERY_ARROWS = 23, /* 0 if arrows >= param */ + FLOW_QUERY_EMPTY_BOTTLES = 24, /* 0 if empty bottles >= param */ + FLOW_QUERY_SHOP_CLERK = 25, /* shop system conversation flag */ + FLOW_QUERY_TEARS_OF_LIGHT = 26, /* 0 if tears >= param; param 0 uses the required count */ + /* 0 if goat-herding time <= param seconds; publishes the time for display */ + FLOW_QUERY_HERDING_TIME = 27, + FLOW_QUERY_LANTERN_OIL = 28, /* 0 full, 1 partial, 2 empty */ + FLOW_QUERY_REGISTER = 29, /* flow scratch register value */ + FLOW_QUERY_GOATS_CAUGHT = 30, /* 0 if caught runaway goats >= param */ + FLOW_QUERY_HEARTS = 31, /* 0 if life >= param */ + FLOW_QUERY_HOLDING_LANTERN = 32, /* 0 if the player has the lantern out */ + FLOW_QUERY_TIME_OF_DAY = 33, /* current hour of game time (0-23) */ + FLOW_QUERY_MAGIC = 34, /* 0 if magic >= param */ + FLOW_QUERY_SELECT_2_CANCEL = 35, /* 0/1 choice, 2 on B cancel */ + FLOW_QUERY_SELECT_3_CANCEL = 36, /* 0/1/2 choice, 3 on B cancel */ + FLOW_QUERY_BOMB_BAG_CONTENTS = 37, /* 0 empty, 1 bombs, 2 water bombs, 3 bomblings */ + FLOW_QUERY_BOMBS_FIT = 38, /* 1 if param more bombs fit in the bag, 0 if over max */ + FLOW_QUERY_BOMB_BAG_FILL = 39, /* 0 empty, 1 partial, 2 full */ + FLOW_QUERY_WATER_BOMBS_FIT = 40, + /* 0 clear, 1 NPC near, 2 NPC far, 3 environment, 4 Sacred Grove */ + FLOW_QUERY_TRANSFORM_BLOCKED = 41, + FLOW_QUERY_BOMBLINGS_FIT = 42, + FLOW_QUERY_WARP_ALLOWED = 43, /* 0 if a dungeon warp is accepted here */ + FLOW_QUERY_GOLDEN_BUGS = 44, /* 0 none, 1 1-11, 2 12-22, 3 23, 4 all 24 */ + FLOW_QUERY_UNDELIVERED_BUG = 45, /* 1 if carrying a golden bug not yet delivered to Agitha */ + FLOW_QUERY_UNUSED_46 = 46, /* asserts; do not use */ + FLOW_QUERY_NEW_LETTERS = 47, /* 0 none, 1 one (& stores its name for the tag), 2 more */ + FLOW_QUERY_POE_SOULS = 48, /* 0 none, 1 <20, 2 <40, 3 <60, 4 60+ */ + FLOW_QUERY_DONATION_TOTAL = 49, /* 0 if donations >= param */ + FLOW_QUERY_BALLOON_SCORE = 50, /* 0 zero, 1 <1000, 2 <10000, 3 <61454, 4 max */ + FLOW_QUERY_IN_WATER = 51, /* 1 if the player is swimming */ + FLOW_QUERY_IRON_BOOTS = 52, /* 1 if iron boots are equipped */ + FLOW_QUERY_BUILTIN_COUNT, +} FlowGameQuery; + +/* Built-in event actions. Params (big-endian): one u32, two u16 (p0, p1), or four u8. */ +typedef enum FlowGameEvent { + FLOW_EVENT_SET_EVENT_FLAG = 0, /* sets event flags [p0] and [p1]; 0 = none */ + FLOW_EVENT_CLEAR_EVENT_FLAG = 1, + FLOW_EVENT_ADD_RUPEES = 2, + FLOW_EVENT_REMOVE_RUPEES = 3, + FLOW_EVENT_ADD_HEARTS = 4, + FLOW_EVENT_REMOVE_HEARTS = 5, + FLOW_EVENT_ADD_MAGIC = 6, + FLOW_EVENT_REMOVE_MAGIC = 7, + FLOW_EVENT_START_EVENT = 8, /* publish (p0 event id, p1 item id) for the speaker to poll */ + FLOW_EVENT_JUMP_FLOW = 9, /* continue at flow [p]; 0 jumps to the stage/Midna flow */ + FLOW_EVENT_SET_TEMP_FLAG = 10, + FLOW_EVENT_CLEAR_TEMP_FLAG = 11, + FLOW_EVENT_OPEN_DOOR = 12, /* marks the flow as a door unlock path (probe only) */ + FLOW_EVENT_SELECT_VERTICAL = 13, /* vertical selection; p = result index chosen on B cancel */ + FLOW_EVENT_SET_SWITCH = 14, /* p0 scope: 0 save, 1 dungeon, 2 zone, 3 one-zone; p1 bit */ + FLOW_EVENT_CLEAR_SWITCH = 15, + FLOW_EVENT_SHOP_SELECT = 16, /* start shop item selection; four u8 shop params */ + FLOW_EVENT_GIVE_ITEM = 17, /* p0 item number, p1 count */ + /* four u8 direction values for the speaker; p3 plays a sound */ + FLOW_EVENT_STAGE_DIRECTION = 18, + FLOW_EVENT_SET_SPEAKER = 19, /* point the box at talk partner [p1] */ + FLOW_EVENT_WARP_PLAYER = 20, /* move the player to the room spawn tagged [p] */ + FLOW_EVENT_WAIT = 21, /* close the box and wait [p] frames */ + FLOW_EVENT_FILL_LANTERN = 22, /* refill oil to [p] percent; 0 = full */ + /* p: 1-3 red/green/blue potion, 4 milk, 5 half milk, 6 oil, 7 hot spring water */ + FLOW_EVENT_FILL_BOTTLE = 23, + FLOW_EVENT_SHOP_SOLD_OUT = 24, + FLOW_EVENT_SET_REGISTER = 25, /* set the flow scratch register (see FLOW_QUERY_REGISTER) */ + FLOW_EVENT_TENT_PURCHASE = 26, /* unattended stand purchase; marks the item sold out */ + FLOW_EVENT_FILL_BOMBS = 27, /* u8 p0 bag select, u8 p1 operation; u16 p1 count */ + FLOW_EVENT_SELL_BOMBS = 28, /* empty the selected bag and pay out */ + /* horizontal selection; p = result index chosen on B cancel */ + FLOW_EVENT_SELECT_HORIZONTAL = 29, + FLOW_EVENT_FILL_ARROWS = 30, /* p1 count, 0 = max; p0 nonzero defers the refill */ + FLOW_EVENT_RETURN_RENTAL_BOMB_BAG = 31, + FLOW_EVENT_FADE_IN = 32, /* p0: 0 black, 1 white; p1 frames */ + FLOW_EVENT_FADE_OUT = 33, + FLOW_EVENT_SET_TRADE_ITEM = 34, /* set the trade-quest item */ + FLOW_EVENT_REMOVE_ITEM = 35, + FLOW_EVENT_SET_SAVE_SWITCH = 36, /* set save switch (p0 area, p1 bit) */ + FLOW_EVENT_CLEAR_SAVE_SWITCH = 37, + FLOW_EVENT_RECEIVE_LETTER = 38, + FLOW_EVENT_UNLOCK_MAP_REGION = 39, + FLOW_EVENT_EMPTY_BOTTLE = 40, /* p as in FLOW_EVENT_FILL_BOTTLE */ + FLOW_EVENT_ADD_DONATION = 41, + FLOW_EVENT_UNUSED_42 = 42, /* no-op */ + FLOW_EVENT_BUILTIN_COUNT, +} FlowGameEvent; + +typedef enum FlowQueryPhase { + FLOW_QUERY_PHASE_PROBE = 0, + FLOW_QUERY_PHASE_EXECUTE = 1, +} FlowQueryPhase; + +typedef struct FlowQueryContext { + const void* speaker_actor; + uint16_t parameter; + uint8_t result_count; + uint8_t phase; /* FlowQueryPhase */ +} FlowQueryContext; + +typedef uint16_t (*FlowQueryFn)(ModContext* ctx, const FlowQueryContext* query, void* user_data); + +typedef struct FlowEventContext { + const void* speaker_actor; + uint8_t parameters[4]; +} FlowEventContext; + +typedef void (*FlowEventFn)(ModContext* ctx, const FlowEventContext* event, void* user_data); + +typedef struct FlowService { + ServiceHeader header; + + ModResult (*begin_graph)(ModContext* ctx, uint16_t group, FlowGraphHandle* out_handle); + /* Allocates one node ID. Fill it with fill_node before commit_graph. */ + ModResult (*allocate_node)(ModContext* ctx, FlowGraphHandle handle, uint16_t* out_id); + /* Adds a series of edges with the given targets. */ + ModResult (*add_edges)(ModContext* ctx, FlowGraphHandle handle, const uint16_t* targets, + uint16_t count, uint16_t* out_first); + /* May be called again to replace a node until commit_graph. */ + ModResult (*fill_node)( + ModContext* ctx, FlowGraphHandle handle, uint16_t node_index, const FlowNodeData* node); + /* Replace a native node or edge when the graph commits; reverted when it is removed. */ + ModResult (*patch_node)( + ModContext* ctx, FlowGraphHandle handle, uint16_t node_index, const FlowNodeData* node); + ModResult (*patch_edge)( + ModContext* ctx, FlowGraphHandle handle, uint16_t edge_index, uint16_t target_node); + ModResult (*commit_graph)(ModContext* ctx, FlowGraphHandle handle); + ModResult (*remove_graph)(ModContext* ctx, FlowGraphHandle handle); + + ModResult (*register_query)(ModContext* ctx, const char* debug_name, FlowQueryFn fn, + void* user_data, FlowQueryId* out_id); + ModResult (*register_event)(ModContext* ctx, const char* debug_name, FlowEventFn fn, + void* user_data, FlowEventId* out_id); +} FlowService; + +MOD_DECLARE_SERVICE(FlowService, svc_flow, FLOW_SERVICE_ID, FLOW_SERVICE_MAJOR, FLOW_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/flow.hpp b/sdk/include/mods/svc/flow.hpp new file mode 100644 index 0000000000..2b1cb1b594 --- /dev/null +++ b/sdk/include/mods/svc/flow.hpp @@ -0,0 +1,633 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mods::flow { + +static_assert(sizeof(FlowNodeData) == 8); +static_assert(sizeof(MessageEntryData) == 20); + +inline constexpr uint16_t kCustomNodeMin = 0x8000; +inline constexpr uint16_t kCustomEdgeMin = 0x8000; +inline constexpr uint16_t kCustomMessageMin = 0x8000; +inline constexpr uint16_t kEnd = 0xffff; + +/* entryIndex is the INF1 entry index, not the message ID stored inside the entry. + * For custom messages the entry index equals the MessageId. */ +constexpr FlowNodeData message( + uint8_t subtype, uint16_t entryIndex, uint16_t nextNode, uint16_t unknown = 0) { + FlowNodeData node{}; + node.bytes[0] = 1; + node.bytes[1] = subtype; + write_bits(node.bytes + 2, entryIndex); + write_bits(node.bytes + 4, nextNode); + write_bits(node.bytes + 6, unknown); + return node; +} + +constexpr FlowNodeData branch( + uint8_t resultCount, FlowQueryId query, uint16_t parameter, uint16_t firstEdge) { + FlowNodeData node{}; + node.bytes[0] = 2; + node.bytes[1] = resultCount; + write_bits(node.bytes + 2, query); + write_bits(node.bytes + 4, parameter); + write_bits(node.bytes + 6, firstEdge); + return node; +} + +constexpr FlowNodeData event(FlowEventId eventId, uint16_t edge, std::array params) { + FlowNodeData node{}; + node.bytes[0] = 3; + node.bytes[1] = eventId; + write_bits(node.bytes + 2, edge); + for (size_t i = 0; i < params.size(); ++i) { + node.bytes[4 + i] = params[i]; + } + return node; +} + +class Graph { +public: + Graph() = default; + Graph(FlowGraphHandle handle, ModResult result) : mHandle{handle}, mResult{result} {} + Graph(const Graph&) = delete; + Graph& operator=(const Graph&) = delete; + Graph(Graph&& other) noexcept { *this = std::move(other); } + Graph& operator=(Graph&& other) noexcept { + if (this != &other) { + reset(); + mHandle = std::exchange(other.mHandle, 0); + mResult = other.mResult; + } + return *this; + } + ~Graph() { reset(); } + + explicit operator bool() const { return mResult == MOD_OK; } + ModResult result() const { return mResult; } + FlowGraphHandle handle() const { return mHandle; } + void reset() { + if (mHandle != 0 && svc_flow != nullptr) { + svc_flow->remove_graph(mod_ctx, mHandle); + mHandle = 0; + } + } + +private: + FlowGraphHandle mHandle{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +class GraphBuilder; + +class NodeRef { +public: + NodeRef() = default; + + /* Set the successor of a message or event node: a node ID or kEnd */ + NodeRef next(uint16_t target) const; + /* Set a branch node's result targets */ + NodeRef results(std::initializer_list targets) const; + + uint16_t id() const { return mId; } + operator uint16_t() const { return mId; } + +private: + friend class GraphBuilder; + NodeRef(GraphBuilder* builder, uint16_t id) : mBuilder{builder}, mId{id} {} + + GraphBuilder* mBuilder = nullptr; + uint16_t mId = 0; +}; + +class GraphBuilder { +public: + explicit GraphBuilder(uint16_t group) { + if (svc_flow == nullptr) { + mResult = MOD_UNAVAILABLE; + return; + } + mResult = svc_flow->begin_graph(mod_ctx, group, &mHandle); + } + GraphBuilder(const GraphBuilder&) = delete; + GraphBuilder& operator=(const GraphBuilder&) = delete; + ~GraphBuilder() { + if (mHandle != 0 && svc_flow != nullptr) { + svc_flow->remove_graph(mod_ctx, mHandle); + } + } + + /* entryIndex is an INF1 entry index for native messages, or a registered MessageId */ + NodeRef add_message(uint16_t entryIndex, uint8_t subtype = 0) { + return append(message(subtype, entryIndex, 0), true); + } + NodeRef add_branch(FlowQueryId query, uint16_t parameter) { + return append(branch(0, query, parameter, 0), true); + } + NodeRef add_event(FlowEventId eventId, std::array params) { + return append(event(eventId, 0, params), true); + } + NodeRef add_node(const FlowNodeData& node) { return append(node, false); } + + GraphBuilder& patch_node(uint16_t nodeIndex, const FlowNodeData& node) { + if (mResult == MOD_OK) { + mResult = svc_flow->patch_node(mod_ctx, mHandle, nodeIndex, &node); + } + return *this; + } + GraphBuilder& patch_edge(uint16_t edgeIndex, uint16_t targetNode) { + if (mResult == MOD_OK) { + mResult = svc_flow->patch_edge(mod_ctx, mHandle, edgeIndex, targetNode); + } + return *this; + } + + Graph commit() { + for (const auto& node : mNodes) { + if (node.typed && !node.wired) { + mResult = MOD_INVALID_ARGUMENT; + } + } + for (const auto& node : mNodes) { + if (mResult != MOD_OK) { + break; + } + mResult = svc_flow->fill_node(mod_ctx, mHandle, node.id, &node.data); + } + if (mResult == MOD_OK) { + mResult = svc_flow->commit_graph(mod_ctx, mHandle); + } + if (mResult != MOD_OK) { + return {0, mResult}; // the builder destructor removes the graph + } + return {std::exchange(mHandle, 0), MOD_OK}; + } + +private: + friend class NodeRef; + + struct BuilderNode { + uint16_t id; + FlowNodeData data; + bool typed; + bool wired; + }; + + NodeRef append(const FlowNodeData& node, bool typed) { + uint16_t id = 0; + if (mResult == MOD_OK) { + mResult = svc_flow->allocate_node(mod_ctx, mHandle, &id); + } + if (mResult == MOD_OK) { + mNodes.push_back({id, node, typed, false}); + } + return {this, id}; + } + + BuilderNode* find(uint16_t id) { + for (auto& node : mNodes) { + if (node.id == id) { + return &node; + } + } + return nullptr; + } + + void set_next(uint16_t id, uint16_t target) { + if (mResult != MOD_OK) { + return; + } + auto* node = find(id); + if (node == nullptr || node->wired) { + mResult = MOD_INVALID_ARGUMENT; + return; + } + node->wired = true; + switch (node->data.bytes[0]) { + case 1: + write_bits(node->data.bytes + 4, target); + break; + case 3: { + uint16_t first = 0; + mResult = svc_flow->add_edges(mod_ctx, mHandle, &target, 1, &first); + if (mResult == MOD_OK) { + write_bits(node->data.bytes + 2, first); + } + break; + } + default: + mResult = MOD_INVALID_ARGUMENT; + } + } + + void set_results(uint16_t id, std::initializer_list targets) { + if (mResult != MOD_OK) { + return; + } + auto* node = find(id); + if (node == nullptr || node->wired || node->data.bytes[0] != 2 || targets.size() == 0 || + targets.size() > 0xff) + { + mResult = MOD_INVALID_ARGUMENT; + return; + } + node->wired = true; + uint16_t first = 0; + mResult = svc_flow->add_edges( + mod_ctx, mHandle, targets.begin(), static_cast(targets.size()), &first); + if (mResult == MOD_OK) { + node->data.bytes[1] = static_cast(targets.size()); + write_bits(node->data.bytes + 6, first); + } + } + + FlowGraphHandle mHandle{}; + std::vector mNodes; + ModResult mResult = MOD_OK; +}; + +inline NodeRef NodeRef::next(uint16_t target) const { + if (mBuilder != nullptr) { + mBuilder->set_next(mId, target); + } + return *this; +} + +inline NodeRef NodeRef::results(std::initializer_list targets) const { + if (mBuilder != nullptr) { + mBuilder->set_results(mId, targets); + } + return *this; +} + +/* Single-patch convenience functions */ +inline Graph patch_node(uint16_t group, uint16_t nodeIndex, const FlowNodeData& node) { + GraphBuilder builder{group}; + builder.patch_node(nodeIndex, node); + return builder.commit(); +} + +inline Graph patch_edge(uint16_t group, uint16_t edgeIndex, uint16_t targetNode) { + GraphBuilder builder{group}; + builder.patch_edge(edgeIndex, targetNode); + return builder.commit(); +} + +class Query { +public: + Query() = default; + Query(FlowQueryId id, ModResult result) : mId{id}, mResult{result} {} + explicit operator bool() const { return mResult == MOD_OK; } + FlowQueryId id() const { return mId; } + ModResult result() const { return mResult; } + +private: + FlowQueryId mId{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline Query register_query(const char* debugName, FlowQueryFn fn, void* userData = nullptr) { + FlowQueryId id{}; + const ModResult result = svc_flow != nullptr ? + svc_flow->register_query(mod_ctx, debugName, fn, userData, &id) : + MOD_UNAVAILABLE; + return {id, result}; +} + +class Event { +public: + Event() = default; + Event(FlowEventId id, ModResult result) : mId{id}, mResult{result} {} + explicit operator bool() const { return mResult == MOD_OK; } + FlowEventId id() const { return mId; } + ModResult result() const { return mResult; } + +private: + FlowEventId mId{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline Event register_event(const char* debugName, FlowEventFn fn, void* userData = nullptr) { + FlowEventId id{}; + const ModResult result = svc_flow != nullptr ? + svc_flow->register_event(mod_ctx, debugName, fn, userData, &id) : + MOD_UNAVAILABLE; + return {id, result}; +} + +/* Message presentation/style attributes (INF1) */ +class MessageStyle { +public: + constexpr MessageStyle() { mData.bytes[12] = 0xff; } + constexpr explicit MessageStyle(MessageEntryData data) : mData{data} {} + + /* saveBitLabels index set when the message displays. */ + [[nodiscard]] constexpr MessageStyle event_label_id(uint16_t value) const { + return set_u16(6, value); + } + /* Z2SpeechMgr2 voice bank ID */ + [[nodiscard]] constexpr MessageStyle speaker(uint8_t value) const { return set_u8(8, value); } + [[nodiscard]] constexpr MessageStyle box_kind(MessageBoxKind value) const { + return set_u8(9, static_cast(value)); + } + [[nodiscard]] constexpr MessageStyle draw_type(MessageDrawType value) const { + return set_u8(10, static_cast(value)); + } + [[nodiscard]] constexpr MessageStyle box_position(MessageBoxPosition value) const { + return set_u8(11, static_cast(value)); + } + /* 0 centered (JP builds only), 1 left */ + [[nodiscard]] constexpr MessageStyle line_alignment(uint8_t value) const { + return set_u8(13, value); + } + /* Grunt emotion index for the voice bank */ + [[nodiscard]] constexpr MessageStyle speaker_mood(uint8_t value) const { + return set_u8(14, value); + } + /* 1-10 focus talk-actor slot, >=11 talk-camera style, 0 none */ + [[nodiscard]] constexpr MessageStyle camera_attr(uint8_t value) const { + return set_u8(15, value); + } + /* NPC talk motion attribute */ + [[nodiscard]] constexpr MessageStyle talk_anim(uint8_t value) const { + return set_u8(16, value); + } + /* NPC talk face attribute */ + [[nodiscard]] constexpr MessageStyle face_anim(uint8_t value) const { + return set_u8(17, value); + } + [[nodiscard]] constexpr const MessageEntryData& data() const { return mData; } + +private: + [[nodiscard]] constexpr MessageStyle set_u8(size_t offset, uint8_t value) const { + MessageStyle style = *this; + style.mData.bytes[offset] = value; + return style; + } + [[nodiscard]] constexpr MessageStyle set_u16(size_t offset, uint16_t value) const { + MessageStyle style = *this; + write_bits(style.mData.bytes + offset, value); + return style; + } + MessageEntryData mData{}; +}; + +class MessageVariant { +public: + MessageVariant() = default; + MessageVariant(MessageLanguage language, MessageEntryData entry, std::vector text, + ModResult result = MOD_OK) + : mLanguage{language}, mEntry{entry}, mText{std::move(text)}, mResult{result} {} + + explicit operator bool() const { return mResult == MOD_OK; } + ModResult result() const { return mResult; } + MessageLanguage language() const { return mLanguage; } + const MessageEntryData& entry() const { return mEntry; } + const std::vector& text() const { return mText; } + MessageVariantData data() const { + return {static_cast(mLanguage), mEntry, mText.data(), mText.size()}; + } + +private: + MessageLanguage mLanguage = MESSAGE_LANGUAGE_ENGLISH; + MessageEntryData mEntry{}; + std::vector mText; + ModResult mResult = MOD_UNAVAILABLE; +}; + +class MessageBuilder { +public: + explicit MessageBuilder(MessageStyle style = {}) : mStyle{style} { mText.push_back(0); } + + /* Style setters forwarded to MessageStyle */ + MessageBuilder& event_label_id(uint16_t value) { + return style(&MessageStyle::event_label_id, value); + } + MessageBuilder& speaker(uint8_t value) { return style(&MessageStyle::speaker, value); } + MessageBuilder& box_kind(MessageBoxKind value) { return style(&MessageStyle::box_kind, value); } + MessageBuilder& draw_type(MessageDrawType value) { + return style(&MessageStyle::draw_type, value); + } + MessageBuilder& box_position(MessageBoxPosition value) { + return style(&MessageStyle::box_position, value); + } + MessageBuilder& line_alignment(uint8_t value) { + return style(&MessageStyle::line_alignment, value); + } + MessageBuilder& speaker_mood(uint8_t value) { + return style(&MessageStyle::speaker_mood, value); + } + MessageBuilder& camera_attr(uint8_t value) { return style(&MessageStyle::camera_attr, value); } + MessageBuilder& talk_anim(uint8_t value) { return style(&MessageStyle::talk_anim, value); } + MessageBuilder& face_anim(uint8_t value) { return style(&MessageStyle::face_anim, value); } + + /* Content builder functions */ + MessageBuilder& text(std::string_view value) { + if (!mText.empty()) { + mText.pop_back(); + } + mText.insert(mText.end(), value.begin(), value.end()); + mText.push_back(0); + return *this; + } + MessageBuilder& raw_tag(uint8_t group, uint16_t type, std::span arguments) { + if (arguments.size() > 250) { + mResult = MOD_INVALID_ARGUMENT; + return *this; + } + mText.pop_back(); + mText.push_back(0x1a); + mText.push_back(static_cast(5 + arguments.size())); + mText.push_back(group); + mText.push_back(static_cast(type >> 8)); + mText.push_back(static_cast(type)); + mText.insert(mText.end(), arguments.begin(), arguments.end()); + mText.push_back(0); + return *this; + } + MessageBuilder& text_color(MessageTextColor color) { + const auto index = static_cast(color); + return raw_tag(255, 0, {&index, 1}); + } + MessageBuilder& text_scale(uint16_t percent) { return timed_tag(255, 1, percent); } + MessageBuilder& character_delay(uint16_t frames) { return timed_tag(0, 6, frames); } + MessageBuilder& pause(uint16_t frames) { return timed_tag(0, 7, frames); } + MessageBuilder& auto_advance(uint16_t frames) { return timed_tag(0, 4, frames); } + MessageBuilder& auto_advance_alternate(uint16_t frames) { return timed_tag(0, 3, frames); } + MessageBuilder& input_or_timeout(uint16_t frames) { return timed_tag(0, 5, frames); } + MessageBuilder& input_after_delay(uint16_t frames) { return timed_tag(0, 54, frames); } + /* Insert the player's name. */ + MessageBuilder& player_name() { return raw_tag(0, 0, {}); } + /* End a prompt that presents a selection; the options live in the target flow node. */ + MessageBuilder& await_choice() { return raw_tag(0, 32, {}); } + /* Option list for a vertical selection message. Only two or three options are supported. + * `initial` configures the highlighted option on open. */ + MessageBuilder& options(std::string_view first, std::string_view second, uint8_t initial = 0) { + option(8, 0, initial, first); + return option(8, 1, initial, second); + } + MessageBuilder& options(std::string_view first, std::string_view second, std::string_view third, + uint8_t initial = 0) { + option(9, 0, initial, first); + option(9, 1, initial, second); + return option(9, 2, initial, third); + } + MessageVariant build(MessageLanguage language) const { + return {language, mStyle.data(), mText, mResult}; + } + +private: + MessageBuilder& option( + uint16_t type, uint8_t position, uint8_t initial, std::string_view value) { + if (position > 0) { + text("\n"); + } + uint8_t marker = static_cast(position + 1); + if (position == initial) { + marker = 1; + } else if (position == 0) { + marker = static_cast(initial + 1); + } + raw_tag(0, type, {&marker, 1}); + return text(value); + } + + template + MessageBuilder& style(Setter setter, Value value) { + mStyle = (mStyle.*setter)(value); + return *this; + } + + MessageBuilder& timed_tag(uint8_t group, uint16_t type, uint16_t value) { + return raw_tag( + group, type, std::array{static_cast(value >> 8), static_cast(value)}); + } + + MessageStyle mStyle; + std::vector mText; + ModResult mResult = MOD_OK; +}; + +class MessageOverride { +public: + MessageOverride() = default; + MessageOverride(MessageOverrideHandle handle, ModResult result) + : mHandle{handle}, mResult{result} {} + MessageOverride(const MessageOverride&) = delete; + MessageOverride& operator=(const MessageOverride&) = delete; + MessageOverride(MessageOverride&& other) noexcept { *this = std::move(other); } + MessageOverride& operator=(MessageOverride&& other) noexcept { + if (this != &other) { + reset(); + mHandle = std::exchange(other.mHandle, 0); + mResult = other.mResult; + } + return *this; + } + ~MessageOverride() { reset(); } + + explicit operator bool() const { return mResult == MOD_OK; } + ModResult result() const { return mResult; } + MessageOverrideHandle handle() const { return mHandle; } + void reset() { + if (mHandle != 0 && svc_message != nullptr) { + svc_message->remove_override(mod_ctx, mHandle); + mHandle = 0; + } + } + +private: + MessageOverrideHandle mHandle{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline MessageOverride override_message( + uint16_t group, uint16_t messageId, MessageLanguage language, std::span text) { + MessageOverrideHandle handle{}; + const ModResult result = svc_message != nullptr ? svc_message->override_message(mod_ctx, group, + messageId, static_cast(language), + text.data(), text.size(), &handle) : + MOD_UNAVAILABLE; + return {handle, result}; +} + +inline MessageOverride override_message_fn(uint16_t group, uint16_t messageId, + MessageLanguage language, MessageOverrideFn fn, void* userData = nullptr) { + MessageOverrideHandle handle{}; + const ModResult result = svc_message != nullptr ? + svc_message->override_message_fn(mod_ctx, group, messageId, + static_cast(language), fn, userData, &handle) : + MOD_UNAVAILABLE; + return {handle, result}; +} + +class RegisteredMessage { +public: + RegisteredMessage() = default; + RegisteredMessage(MessageId id, MessageHandle handle, ModResult result) + : mId{id}, mHandle{handle}, mResult{result} {} + RegisteredMessage(const RegisteredMessage&) = delete; + RegisteredMessage& operator=(const RegisteredMessage&) = delete; + RegisteredMessage(RegisteredMessage&& other) noexcept { *this = std::move(other); } + RegisteredMessage& operator=(RegisteredMessage&& other) noexcept { + if (this != &other) { + reset(); + mId = other.mId; + mHandle = std::exchange(other.mHandle, 0); + mResult = other.mResult; + } + return *this; + } + ~RegisteredMessage() { reset(); } + explicit operator bool() const { return mResult == MOD_OK; } + MessageId id() const { return mId; } + ModResult result() const { return mResult; } + void reset() { + if (mHandle != 0 && svc_message != nullptr) { + svc_message->remove_message(mod_ctx, mHandle); + mHandle = 0; + } + } + +private: + MessageId mId{}; + MessageHandle mHandle{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline RegisteredMessage register_message( + uint16_t group, std::span variants) { + std::vector data; + data.reserve(variants.size()); + for (const auto& variant : variants) { + if (!variant) { + return {0, 0, variant.result()}; + } + data.push_back(variant.data()); + } + MessageId id{}; + MessageHandle handle{}; + const ModResult result = svc_message != nullptr ? svc_message->register_message(mod_ctx, group, + data.data(), data.size(), &id, &handle) : + MOD_UNAVAILABLE; + return {id, handle, result}; +} + +inline RegisteredMessage register_message( + uint16_t group, std::initializer_list variants) { + return register_message(group, std::span{variants.begin(), variants.size()}); +} + +} // namespace mods::flow diff --git a/sdk/include/mods/svc/game.h b/sdk/include/mods/svc/game.h index ecb8bcfeb7..32de49903f 100644 --- a/sdk/include/mods/svc/game.h +++ b/sdk/include/mods/svc/game.h @@ -16,7 +16,7 @@ * of letting them corrupt memory. */ #define GAME_SERVICE_ID "dev.twilitrealm.dusklight.game" -#define GAME_SERVICE_MAJOR 1u +#define GAME_SERVICE_MAJOR 2u #define GAME_SERVICE_MINOR 0u typedef struct GameService { diff --git a/sdk/include/mods/svc/game_mode.h b/sdk/include/mods/svc/game_mode.h new file mode 100644 index 0000000000..8ccc5d64f1 --- /dev/null +++ b/sdk/include/mods/svc/game_mode.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#define GAME_MODE_SERVICE_ID "dev.twilitrealm.dusklight.gamemode" +#define GAME_MODE_SERVICE_MAJOR 1u +#define GAME_MODE_SERVICE_MINOR 0u + +/* MOD_OK on success; failures disable the mod. */ +typedef ModResult (*GameModeCallback)(void* user_data, ModError* out_error); + +typedef enum GameModeNewSaveState { + GAME_MODE_STATE_PENDING = 0, + GAME_MODE_STATE_PROCEED, + GAME_MODE_STATE_RETURN, +} GameModeNewSaveState; + +/* Set state to PROCEED after custom UI completes, or RETURN to cancel. + * State pointer is valid until selection completes. */ +typedef ModResult (*GameModeNewSaveSelectCallback)( + void* user_data, GameModeNewSaveState* state, ModError* out_error); + +typedef struct { + uint32_t struct_size; + const char* game_mode_id; + const char* full_name; + const char save_name[32]; // Empty uses default (gczelda2); max 31 chars + void* user_data; // Pointer will be passed to all callbacks + GameModeCallback on_activated; // Called when the game mode is selected + GameModeCallback on_deactivated; // Called when the game mode is deselected + GameModeCallback on_play; // Called when play is pressed + GameModeCallback on_save_loaded; // Called whenever a save file is loaded + GameModeCallback on_new_save; // Called when a new save is created + GameModeNewSaveSelectCallback on_new_save_select; + GameModeCallback on_game_reset; // Called when the game is reset + GameModeCallback on_tick; // Called on every game tick while active +} GameModeDesc; + +#define GAME_MODE_DESC_INIT {sizeof(GameModeDesc)} + +typedef struct GameModeService { + ServiceHeader header; + ModResult (*register_game_mode)(ModContext* ctx, const GameModeDesc* desc); + ModResult (*unregister_game_mode)(ModContext* ctx, const char* id); + ModResult (*is_active)(ModContext* ctx, const char* game_mode_id, bool* out_active); +} GameModeService; + +MOD_DECLARE_SERVICE(GameModeService, svc_game_mode, GAME_MODE_SERVICE_ID, GAME_MODE_SERVICE_MAJOR, + GAME_MODE_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/gfx.h b/sdk/include/mods/svc/gfx.h index 5041c611e3..0eee026984 100644 --- a/sdk/include/mods/svc/gfx.h +++ b/sdk/include/mods/svc/gfx.h @@ -33,10 +33,68 @@ #define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx" #define GFX_SERVICE_MAJOR 1u -#define GFX_SERVICE_MINOR 1u +#define GFX_SERVICE_MINOR 2u /* Maximum size for push_draw payload */ #define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u +#define GFX_MAX_COLOR_ATTACHMENTS 8u +#define GFX_SCENE_COLOR_ATTACHMENT_INDEX 0u + +typedef enum GfxAttachmentSemantic { + GFX_ATTACHMENT_SCENE_COLOR, + GFX_ATTACHMENT_NORMAL, + GFX_ATTACHMENT_AUXILIARY, +} GfxAttachmentSemantic; + +typedef struct GfxColorAttachmentLayout { + GfxAttachmentSemantic semantic; + WGPUTextureFormat format; + uint32_t width; + uint32_t height; +} GfxColorAttachmentLayout; + +typedef struct GfxRenderTargetLayout { + uint32_t struct_size; + uint64_t key; + /* At least one; scene color is at GFX_SCENE_COLOR_ATTACHMENT_INDEX. */ + uint32_t color_attachment_count; + GfxColorAttachmentLayout color_attachments[GFX_MAX_COLOR_ATTACHMENTS]; + WGPUTextureFormat depth_stencil_format; + uint32_t sample_count; +} GfxRenderTargetLayout; + +#define GFX_RENDER_TARGET_LAYOUT_INIT \ + {sizeof(GfxRenderTargetLayout), 0u, 0u, \ + {{GFX_ATTACHMENT_AUXILIARY, WGPUTextureFormat_Undefined}}, WGPUTextureFormat_Undefined, \ + 1u} + +/* + * Initializes pipeline color targets for a render-target layout. Only scene color is writable; + * callers that write another semantic should override that target afterward. + */ +static uint32_t gfx_init_color_target_states(const GfxRenderTargetLayout* layout, + WGPUColorTargetState targets[GFX_MAX_COLOR_ATTACHMENTS], const WGPUBlendState* scene_blend, + WGPUColorWriteMask scene_write_mask) { + if (layout == NULL || targets == NULL) { + return 0; + } + const uint32_t count = layout->color_attachment_count < GFX_MAX_COLOR_ATTACHMENTS ? + layout->color_attachment_count : + GFX_MAX_COLOR_ATTACHMENTS; + for (uint32_t i = 0; i < GFX_MAX_COLOR_ATTACHMENTS; ++i) { + WGPUColorTargetState target = WGPU_COLOR_TARGET_STATE_INIT; + if (i < count) { + target.format = layout->color_attachments[i].format; + target.writeMask = WGPUColorWriteMask_None; + } + targets[i] = target; + } + if (count != 0) { + targets[GFX_SCENE_COLOR_ATTACHMENT_INDEX].blend = scene_blend; + targets[GFX_SCENE_COLOR_ATTACHMENT_INDEX].writeMask = scene_write_mask; + } + return count; +} /* 0 is never a valid handle. */ typedef uint64_t GfxDrawTypeHandle; @@ -51,8 +109,8 @@ typedef struct GfxRange { } GfxRange; /* - * Device and scene pass configuration. Valid from mod_initialize onward and stable for the - * session. Offscreen passes from create_pass are always single-sample. + * Device and legacy primary scene-pass configuration. Use get_scene_target_layout when creating + * scene pipelines. Offscreen passes from create_pass are always single-sample. */ typedef struct GfxDeviceInfo { uint32_t struct_size; @@ -83,12 +141,18 @@ typedef struct GfxDrawContext { WGPUBuffer index_buffer; WGPUBuffer uniform_buffer; WGPUBuffer storage_buffer; + /* deprecated: use layout.color_attachments[GFX_SCENE_COLOR_ATTACHMENT_INDEX].format */ WGPUTextureFormat color_format; + /* deprecated: use layout.depth_stencil_format */ WGPUTextureFormat depth_format; + /* deprecated: use layout.sample_count */ uint32_t sample_count; + /* deprecated: use layout.color_attachments[GFX_SCENE_COLOR_ATTACHMENT_INDEX].width */ uint32_t target_width; + /* deprecated: use layout.color_attachments[GFX_SCENE_COLOR_ATTACHMENT_INDEX].height */ uint32_t target_height; bool uses_reversed_z; + GfxRenderTargetLayout layout; /* added in GfxService 1.2 */ } GfxDrawContext; typedef void (*GfxDrawFn)(ModContext* ctx, const GfxDrawContext* draw_ctx, const void* payload, @@ -269,6 +333,10 @@ typedef struct GfxService { */ ModResult (*push_present)( ModContext* ctx, GfxPresentTargetHandle handle, const void* payload, size_t payload_size); + + /* Minor version 2 */ + + ModResult (*get_scene_target_layout)(ModContext* ctx, GfxRenderTargetLayout* out_layout); } GfxService; MOD_DECLARE_SERVICE(GfxService, svc_gfx, GFX_SERVICE_ID, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/item.h b/sdk/include/mods/svc/item.h new file mode 100644 index 0000000000..81a2589f6d --- /dev/null +++ b/sdk/include/mods/svc/item.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define ITEM_SERVICE_ID "dev.twilitrealm.dusklight.item" +#define ITEM_SERVICE_MAJOR 2u +#define ITEM_SERVICE_MINOR 0u + +/* 0 is never a valid handle. */ +typedef uint64_t ItemCheckHandle; +typedef uint64_t ItemGiveHandle; + +/* + * Item check resolution and inventory grants. + * + * Check names are case-sensitive. Resolvers must be free of side effects because the game may + * resolve a check more than once, including once for display and again when granting the item. + * Registrations and pending grants are removed when the calling mod is detached. Callbacks run + * on the game thread. + */ + +/* Host-owned callback data, valid only for the duration of the callback. */ +typedef struct ItemCheckInfo { + const char* name; + const void* giver_actor; /* fopAc_ac_c*, or NULL when unavailable */ + uint8_t vanilla_item; + uint8_t current_item; +} ItemCheckInfo; + +/* Return true and write out_item to replace current_item, or false to leave it unchanged. */ +typedef bool (*ItemCheckResolveFn)( + ModContext* ctx, const ItemCheckInfo* info, uint8_t* out_item, void* user_data); + +typedef enum ItemGiveOrigin { + ITEM_GIVE_ORIGIN_GAME = 0, + ITEM_GIVE_ORIGIN_QUEUE = 1, + ITEM_GIVE_ORIGIN_QUEUE_SILENT = 2, +} ItemGiveOrigin; + +/* Host-owned callback data, valid only for the duration of the callback. */ +typedef struct ItemGiveInfo { + const char* check_name; /* NULL when the grant is not attributed to a check */ + const void* giver_actor; /* fopAc_ac_c*, or NULL when unavailable */ + uint8_t item; + uint8_t origin; /* ItemGiveOrigin */ +} ItemGiveInfo; + +typedef void (*ItemGiveObserveFn)(ModContext* ctx, const ItemGiveInfo* info, void* user_data); + +enum { + /* Apply the inventory change without a get-item demo. */ + ITEM_GIVE_SILENT = 1u << 0, + /* Resolve item_no as the named check's vanilla item when the queue entry is dispatched. */ + ITEM_GIVE_RESOLVE = 1u << 1, +}; + +typedef struct ItemService { + ServiceHeader header; + + /* Set or replace the calling mod's fixed override for name. */ + ModResult (*set_check_override)(ModContext* ctx, const char* name, uint8_t item_no); + + ModResult (*clear_check_override)(ModContext* ctx, const char* name); + + /* Register for name, or for every check when name is NULL. out_handle may be NULL. */ + ModResult (*set_check_resolver)(ModContext* ctx, const char* name, ItemCheckResolveFn fn, + void* user_data, ItemCheckHandle* out_handle); + + ModResult (*clear_check_resolver)(ModContext* ctx, ItemCheckHandle handle); + + /* Resolve without granting an item or notifying give observers. */ + ModResult (*resolve_check)( + ModContext* ctx, const char* name, uint8_t vanilla_item, uint8_t* out_item); + + /* + * Add a grant to the global FIFO. check_name may be NULL unless ITEM_GIVE_RESOLVE is set. + * Returns MOD_UNAVAILABLE when the queue is full. Entries wait until gameplay is in a safe + * state and are cleared when the active save slot changes. + */ + ModResult (*give_item)( + ModContext* ctx, const char* check_name, uint8_t item_no, uint32_t flags); + + /* Observe inventory grants. out_handle may be NULL. */ + ModResult (*observe_gives)( + ModContext* ctx, ItemGiveObserveFn fn, void* user_data, ItemGiveHandle* out_handle); + + ModResult (*unobserve_gives)(ModContext* ctx, ItemGiveHandle handle); +} ItemService; + +MOD_DECLARE_SERVICE(ItemService, svc_item, ITEM_SERVICE_ID, ITEM_SERVICE_MAJOR, ITEM_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/message.h b/sdk/include/mods/svc/message.h new file mode 100644 index 0000000000..172dce4895 --- /dev/null +++ b/sdk/include/mods/svc/message.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define MESSAGE_SERVICE_ID "dev.twilitrealm.dusklight.message" +#define MESSAGE_SERVICE_MAJOR 1u +#define MESSAGE_SERVICE_MINOR 0u + +typedef uint16_t MessageId; +typedef uint64_t MessageHandle; +typedef uint64_t MessageOverrideHandle; + +/* INF1 entry data */ +typedef struct MessageEntryData { + uint8_t bytes[20]; +} MessageEntryData; +static_assert(sizeof(MessageEntryData) == 20); + +/* INF1 box kind (offset 0x09): message screen class */ +typedef enum MessageBoxKind { + MESSAGE_BOX_TALK = 0, /* ordinary dialogue box */ + MESSAGE_BOX_DEMO_CAPTION = 1, /* boxless cutscene caption */ + MESSAGE_BOX_SIGN = 2, /* signs and posted notices */ + MESSAGE_BOX_PLAIN = 5, /* boxless system text */ + MESSAGE_BOX_KANBAN = 6, /* signboard screen class; unused */ + MESSAGE_BOX_STAFF_ROLL = 7, + MESSAGE_BOX_LIGHT_SPIRIT = 8, /* spirit text window and glow */ + MESSAGE_BOX_ITEM_GET = 9, /* centered item-get box */ + MESSAGE_BOX_ITEM_NAME = 11, /* UI string fetch, no box */ + MESSAGE_BOX_PLACE_NAME = 12, /* area intro banner */ + MESSAGE_BOX_MIDNA = 13, /* Midna dialogue colors and glow */ + MESSAGE_BOX_ANIMAL = 14, /* wolf-form animal speech glow */ + MESSAGE_BOX_NOTICE = 15, /* floating gameplay notice; can't be used during dialogue */ + MESSAGE_BOX_SAVE = 16, /* save and memory card prompts */ + MESSAGE_BOX_HOWL = 17, /* howling stone UI */ + MESSAGE_BOX_BOSS_NAME = 19, /* boss intro banner */ +} MessageBoxKind; + +/* INF1 draw type (offset 0x0A): text pacing */ +typedef enum MessageDrawType { + MESSAGE_DRAW_TYPED = 0, /* types per-character; A skips typing */ + MESSAGE_DRAW_INSTANT = 1, /* whole page at once (menus, prompts) */ + MESSAGE_DRAW_TYPED_NO_SKIP = 2, /* types per-character; A does not skip */ + MESSAGE_DRAW_FADE = 3, /* page fades in */ + MESSAGE_DRAW_UI_NAME = 4, /* UI string fetch (item names), no box pacing */ + MESSAGE_DRAW_TYPED_SLOW = 5, /* weighted slow typing (light spirit speech) */ + MESSAGE_DRAW_UI_ACTION = 7, /* UI string fetch (action button labels) */ + MESSAGE_DRAW_FADE_SLOW = 9, /* slow page fade (staff credits) */ +} MessageDrawType; + +/* INF1 box position (offset 0x0B) */ +typedef enum MessageBoxPosition { + MESSAGE_POSITION_BOTTOM = 0, + MESSAGE_POSITION_TOP = 1, + MESSAGE_POSITION_MIDDLE = 2, + MESSAGE_POSITION_AUTO = 3, /* top or bottom, avoiding the speaker on screen */ +} MessageBoxPosition; + +/* Applies to both the text and gradient colors. */ +typedef enum MessageTextColor { + MESSAGE_COLOR_DEFAULT = 0, /* box default */ + MESSAGE_COLOR_RED = 1, /* 0xF07878 */ + MESSAGE_COLOR_GREEN = 2, /* 0xAADC8C */ + MESSAGE_COLOR_BLUE = 3, /* 0xA0B4DC */ + MESSAGE_COLOR_YELLOW = 4, /* 0xDCDC82 */ + MESSAGE_COLOR_SKY = 5, /* 0xB4C8E6 */ + MESSAGE_COLOR_PURPLE = 6, /* 0xC8A0DC */ + MESSAGE_COLOR_WHITE = 7, /* 0xFFFFFF */ + MESSAGE_COLOR_ORANGE = 8, /* 0xDCAA78 */ +} MessageTextColor; + +/* Message text language. 5 (Dutch) is unused. */ +typedef enum MessageLanguage { + MESSAGE_LANGUAGE_ENGLISH = 0, + MESSAGE_LANGUAGE_GERMAN = 1, + MESSAGE_LANGUAGE_FRENCH = 2, + MESSAGE_LANGUAGE_SPANISH = 3, + MESSAGE_LANGUAGE_ITALIAN = 4, + MESSAGE_LANGUAGE_JAPANESE = 6, +} MessageLanguage; + +typedef struct MessageVariantData { + uint8_t language; /* MessageLanguage */ + MessageEntryData entry; + const uint8_t* text; + size_t text_size; +} MessageVariantData; + +typedef struct MessageTextData { + const uint8_t* text; + size_t text_size; +} MessageTextData; + +typedef struct MessageOverrideContext { + uint16_t group; + uint16_t message_id; + uint8_t language; /* MessageLanguage */ + const uint8_t* original_text; + size_t original_text_size; +} MessageOverrideContext; + +/* Return true with a valid out_text to override, or false to try the next registration. */ +typedef bool (*MessageOverrideFn)(ModContext* ctx, const MessageOverrideContext* message, + MessageTextData* out_text, void* user_data); + +typedef struct MessageService { + ServiceHeader header; + + ModResult (*override_message)(ModContext* ctx, uint16_t group, uint16_t message_id, + uint8_t language, const uint8_t* text, size_t text_size, MessageOverrideHandle* out_handle); + ModResult (*override_message_fn)(ModContext* ctx, uint16_t group, uint16_t message_id, + uint8_t language, MessageOverrideFn fn, void* user_data, MessageOverrideHandle* out_handle); + ModResult (*remove_override)(ModContext* ctx, MessageOverrideHandle handle); + + ModResult (*register_message)(ModContext* ctx, uint16_t group, + const MessageVariantData* variants, size_t variant_count, MessageId* out_id, + MessageHandle* out_handle); + ModResult (*remove_message)(ModContext* ctx, MessageHandle handle); +} MessageService; + +MOD_DECLARE_SERVICE( + MessageService, svc_message, MESSAGE_SERVICE_ID, MESSAGE_SERVICE_MAJOR, MESSAGE_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index 5dd9066421..e265f306fa 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -9,7 +9,7 @@ #define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" #define UI_SERVICE_MAJOR 1u -#define UI_SERVICE_MINOR 1u +#define UI_SERVICE_MINOR 2u /* * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts, @@ -291,6 +291,10 @@ typedef struct UiService { /* Enqueue a toast notification. */ ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc); + + /* Minor version 2 */ + ModResult (*get_clipboard_text)(ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength); + ModResult (*set_clipboard_text)(ModContext* ctx, const char* text); } UiService; MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/ui.hpp b/sdk/include/mods/svc/ui.hpp new file mode 100644 index 0000000000..7994cad1dc --- /dev/null +++ b/sdk/include/mods/svc/ui.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include + +namespace mods::ui { + +inline ModResult get_clipboard_text(std::string& outText) { + outText.clear(); + + size_t textLength = 0; + auto result = svc_ui->get_clipboard_text(mod_ctx, nullptr, 0, &textLength); + if (result != MOD_OK) { + return result; + } + + for (int attempt = 0; attempt < 3; ++attempt) { + std::string buffer(textLength + 1, '\0'); + size_t actualLength = 0; + + result = svc_ui->get_clipboard_text( + mod_ctx, buffer.data(), buffer.size(), &actualLength); + + if (result == MOD_OK) { + buffer.resize(actualLength); + outText = std::move(buffer); + return MOD_OK; + } + + // Retry if the clipboard grew between the two calls. + if (result != MOD_INVALID_ARGUMENT || actualLength <= textLength) { + return result; + } + + textLength = actualLength; + } + + return MOD_ERROR; +} + +inline ModResult set_clipboard_text(const std::string& text) { + return svc_ui->set_clipboard_text(mod_ctx, text.c_str()); +} + +} // namespace mods::ui \ No newline at end of file diff --git a/src/Z2AudioLib/Z2SeqMgr.cpp b/src/Z2AudioLib/Z2SeqMgr.cpp index a3e45d8acd..0658f7eb54 100644 --- a/src/Z2AudioLib/Z2SeqMgr.cpp +++ b/src/Z2AudioLib/Z2SeqMgr.cpp @@ -11,7 +11,10 @@ #include "Z2AudioLib/SpotName.h" #include "os_report.h" +#if TARGET_PC #include "dusk/audio.h" +#include "dusk/version.hpp" +#endif Z2SeqMgr::Z2SeqMgr() : JASGlobalInstance(true) { mMainBgmMaster.forceIn(); @@ -584,7 +587,7 @@ void Z2SeqMgr::bgmStreamPlay() { } #if !PLATFORM_SHIELD - else if (getStreamBgmID() == 0x2000000) { + else if (getStreamBgmID() == 0x2000000 IF_DUSK(&& dusk::version::isGcn())) { if (mStreamBgmHandle) { mStreamBgmHandle->stop(); } diff --git a/src/d/actor/d_a_alink_bow.inc b/src/d/actor/d_a_alink_bow.inc index 600dcae2f4..aae80e8b88 100644 --- a/src/d/actor/d_a_alink_bow.inc +++ b/src/d/actor/d_a_alink_bow.inc @@ -551,7 +551,7 @@ void daAlink_c::setBowSight() { getArrowFlyData(&dist, &speed, TRUE); checkSightLine(dist, &sight_pos); mSight.setPos(&sight_pos); - mSight.offDrawFlg(); + DUSK_IF_ELSE(dusk::getSettings().game.aimingReticle ? mSight.onDrawFlg() : mSight.offDrawFlg(), mSight.offDrawFlg()); } else { mSight.offDrawFlg(); } diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index cf1a222c9c..e8812c6e41 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,10 +23,13 @@ #include "d/actor/d_a_npc_tkc.h" #include +#ifdef TARGET_PC +#include "dusk/game_mode.hpp" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" #include "dusk/speedrun.h" #include "dusk/version.hpp" +#endif BOOL daAlink_c::checkEventRun() const { return dComIfGp_event_runCheck() || checkPlayerDemoMode(); @@ -2260,6 +2263,12 @@ int daAlink_c::procCoGetItemInit() { s16 var_r22 = 0; BOOL var_r31 = FALSE; BOOL var_r30 = FALSE; +#if TARGET_PC + // Queued grants have no event partner, so create the demo item from the event's GtItm. + if (dusk::mods::item_give_queue_dispatching()) { + mDemo.setParam0(0x100); + } +#endif if (mProcID == PROC_GET_ITEM || mProcID == PROC_INSECT_CATCH || (mProcID == PROC_PREACTION_UNEQUIP && !checkNoUpperAnime())) { @@ -2299,7 +2308,7 @@ int daAlink_c::procCoGetItemInit() { } fpc_ProcID item_partner_id = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 0, -1, - fopAcM_GetRoomNo(this), NULL, NULL); + fopAcM_GetRoomNo(this), NULL, NULL IF_DUSK_ARG(dusk::mods::item_give_queue_take_tag())); if (item_partner_id != fpcM_ERROR_PROCESS_ID_e) { dComIfGp_event_setItemPartnerId(item_partner_id); } @@ -4010,9 +4019,9 @@ int daAlink_c::procGanonFinishInit() { onEndResetFlg1(ERFLG1_SHIELD_BACKBONE); #if TARGET_PC - if (dusk::getSettings().game.speedrunMode) { - if (dusk::m_speedrunInfo.m_isRunStarted) { - dusk::m_speedrunInfo.stopRun(); + if (dusk::speedrun::isActive()) { + if (dusk::speedrun::g_speedrunInfo.m_isRunStarted) { + dusk::speedrun::g_speedrunInfo.stopRun(); } } #endif diff --git a/src/d/actor/d_a_b_ds.cpp b/src/d/actor/d_a_b_ds.cpp index 17bdd9564b..e0623acaba 100644 --- a/src/d/actor/d_a_b_ds.cpp +++ b/src/d/actor/d_a_b_ds.cpp @@ -4084,6 +4084,10 @@ void daB_DS_c::executeBattle2Dead() { camera->mCamera.SetTrimSize(0); dComIfGp_event_reset(); dComIfGs_onStageBossEnemy(0x13); +#if TARGET_PC + // This reward has no original grant at this point in the cutscene. + dusk::mods::item_check_enqueue("Arbiters Grounds Dungeon Reward", dItemNo_NONE_e); +#endif /* dSv_event_flag_c::F_0265 - Arbiter's Grounds - Arbiter's Grounds clear */ dComIfGs_onEventBit(0x2010); fopAcM_delete(this); diff --git a/src/d/actor/d_a_demo_item.cpp b/src/d/actor/d_a_demo_item.cpp index 209bd483c4..5876c26718 100644 --- a/src/d/actor/d_a_demo_item.cpp +++ b/src/d/actor/d_a_demo_item.cpp @@ -170,7 +170,7 @@ void daDitem_c::actionEvent() { if (chkDead()) { if (!chkArgFlag(0x1)) { - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); } if (m_itemNo == dItemNo_KAKERA_HEART_e) { diff --git a/src/d/actor/d_a_e_hp.cpp b/src/d/actor/d_a_e_hp.cpp index 51ac16fcf3..8c9d9398ea 100644 --- a/src/d/actor/d_a_e_hp.cpp +++ b/src/d/actor/d_a_e_hp.cpp @@ -726,11 +726,18 @@ void daE_HP_c::executeDead() { fopAcM_onSwitch(this, bitSw); } - dComIfGs_addPohSpiritNum(); +#if TARGET_PC + mItemCheckOverridden = + dusk::mods::item_check_poe(bitSw, dItemNo_POU_SPIRIT_e, this) != dItemNo_POU_SPIRIT_e; + if (mItemCheckOverridden) { + dusk::mods::item_check_enqueue_poe(bitSw, dItemNo_POU_SPIRIT_e); + } else +#endif + dComIfGs_addPohSpiritNum(); field_0x784 = -1; - if (dComIfGs_getPohSpiritNum() == 20) { + if (dComIfGs_getPohSpiritNum() == 20 IF_DUSK(&&!mItemCheckOverridden)) { dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[0x1c9]); } @@ -752,13 +759,13 @@ void daE_HP_c::executeDead() { field_0x788 = 1; } } - } else if (field_0x788 != 0) { + } else if (field_0x788 != 0 IF_DUSK(|| mItemCheckOverridden)) { fopAcM_createDisappear(this, ¤t.pos, 8, 3, 0xff); fopAcM_delete(this); } else { if (field_0x784 == -1) { - field_0x784 = fopAcM_createItemForPresentDemo(¤t.pos, dItemNo_POU_SPIRIT_e, 0, -1, - -1, 0, 0); + field_0x784 = fopAcM_createItemForPresentDemo(¤t.pos, dItemNo_POU_SPIRIT_e, 0, + -1, -1, 0, 0 IF_DUSK_ARG(dusk::mods::item_give_tag_poe(bitSw))); } if (fopAcM_IsExecuting(field_0x784) != FALSE) { diff --git a/src/d/actor/d_a_e_po.cpp b/src/d/actor/d_a_e_po.cpp index 1c4e95fa61..f2971f23a3 100644 --- a/src/d/actor/d_a_e_po.cpp +++ b/src/d/actor/d_a_e_po.cpp @@ -1132,12 +1132,20 @@ static void e_po_dead(e_po_class* i_this) { camera_player->mCamera.Start(); camera_player->mCamera.SetTrimSize(0); dComIfGp_event_reset(); - dComIfGs_addPohSpiritNum(); +#if TARGET_PC + if (dusk::mods::item_check_poe(i_this->BitSW, dItemNo_POU_SPIRIT_e, a_this) == + dItemNo_POU_SPIRIT_e) + { +#endif + dComIfGs_addPohSpiritNum(); #if !PLATFORM_SHIELD if (dComIfGs_getPohSpiritNum() == 0x14) { /* dSv_event_flag_c::F_0457 - Castle Town - Revived cat */ dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[457]); } +#endif +#if TARGET_PC + } #endif daPy_getPlayerActorClass()->cancelOriginalDemo(); } else if (mArg0Check(i_this, 0) != 0) { @@ -1265,8 +1273,15 @@ static void e_po_dead(e_po_class* i_this) { } } else { if (i_this->field_0x75C == -1) { +#if TARGET_PC + const u8 itemNo = + dusk::mods::item_check_poe(i_this->BitSW, dItemNo_POU_SPIRIT_e, a_this); + i_this->field_0x75C = fopAcM_createItemForPresentDemo(&a_this->current.pos, itemNo, + 0, -1, -1, NULL, NULL, dusk::mods::item_give_tag_poe(i_this->BitSW)); +#else i_this->field_0x75C = fopAcM_createItemForPresentDemo(&a_this->current.pos, 0xE0, 0, -1, -1, NULL, NULL); +#endif } if (fopAcM_IsExecuting(i_this->field_0x75C)) { i_this->field_0x762 = diff --git a/src/d/actor/d_a_e_rdb.cpp b/src/d/actor/d_a_e_rdb.cpp index 7eccf34ef5..c467f38fd6 100644 --- a/src/d/actor/d_a_e_rdb.cpp +++ b/src/d/actor/d_a_e_rdb.cpp @@ -1250,7 +1250,9 @@ static void demo_camera(e_rdb_class* i_this) { } if (iVar1 != 0) { - daPy_getPlayerActorClass()->changeDemoMode(11, 32, 0, 0); + daPy_getPlayerActorClass()->changeDemoMode(11, + DUSK_ITEM_CHECK_EXPR("bulblin_key:D_MN09", dItemNo_SMALL_KEY_e, &i_this->enemy), 0, + 0); i_this->mDemoMode = 12; i_this->field_0x10aa = 0; i_this->field_0xfe5 = 1; diff --git a/src/d/actor/d_a_e_th_ball.cpp b/src/d/actor/d_a_e_th_ball.cpp index e9be033af2..fcb87ada24 100644 --- a/src/d/actor/d_a_e_th_ball.cpp +++ b/src/d/actor/d_a_e_th_ball.cpp @@ -931,7 +931,9 @@ static void get_demo(e_th_ball_class* i_this) { case 0: break; case 1: - demo_id = fopAcM_createItemForTrBoxDemo(&i_this->current.pos, dItemNo_IRONBALL_e, -1, fopAcM_GetRoomNo(i_this), NULL, NULL); + demo_id = fopAcM_createItemForTrBoxDemo(&i_this->current.pos, + DUSK_ITEM_CHECK_EXPR("ball_and_chain:D_MN11", dItemNo_IRONBALL_e, i_this), -1, + fopAcM_GetRoomNo(i_this), NULL, NULL DUSK_GIVE_TAG("ball_and_chain:D_MN11")); JUT_ASSERT(1670, demo_id != fpcM_ERROR_PROCESS_ID_e); i_this->mDemoMode = 2; break; diff --git a/src/d/actor/d_a_mant.cpp b/src/d/actor/d_a_mant.cpp index 449efcd672..cc83028b9c 100644 --- a/src/d/actor/d_a_mant.cpp +++ b/src/d/actor/d_a_mant.cpp @@ -15,12 +15,50 @@ #include "dusk/dvd_asset.hpp" #include "dusk/frame_interpolation.h" -using GameVersion = dusk::version::GameVersion; +#include + +using namespace dusk::version; + +#define MANT_REL_PATH platformSelect("/rel/Final/Release/d_a_mant.rel", "/rel/Rfinal/Release/d_a_mant.rel") + +#define DEFINE_MANT_ASSET(name, type, count, ...) \ + static type* name##_get() { \ + alignas(32) static std::remove_cv_t buf[count]; \ + static bool _ = (dusk::LoadRelAsset(buf, MANT_REL_PATH, __VA_ARGS__), true); \ + return buf; \ + } // keep the original version of the cape texture const so we don't need to reload the file -static u8 const * l_Egnd_mantTEX_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x1C00}, {GameVersion::GcnPal, 0x1C00}, {GameVersion::GcnJpn, 0x1C00}}, 0x4000), true); return buf; } -static u8* l_Egnd_mantTEX_U_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x5C00}, {GameVersion::GcnPal, 0x5C00}, {GameVersion::GcnJpn, 0x5C00}}, 0x4000), true); return buf; } -static u8* l_Egnd_mantPAL_get() { alignas(32) static u8 buf[0x60]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x9C00}, {GameVersion::GcnPal, 0x9C00}, {GameVersion::GcnJpn, 0x9C00}}, 0x60), true); return buf; } +DEFINE_MANT_ASSET(l_Egnd_mantTEX, u8 const, 0x4000, { + {GameVersion::GcnUsa, 0x1C00}, + {GameVersion::GcnPal, 0x1C00}, + {GameVersion::GcnJpn, 0x1C00}, + {GameVersion::WiiUsaRev0, 0x1B00}, + {GameVersion::WiiUsa, 0x1900}, + {GameVersion::WiiPal, 0x1900}, + {GameVersion::WiiJpn, 0x1900}, +}); + +DEFINE_MANT_ASSET(l_Egnd_mantTEX_U, u8, 0x4000, { + {GameVersion::GcnUsa, 0x5C00}, + {GameVersion::GcnPal, 0x5C00}, + {GameVersion::GcnJpn, 0x5C00}, + {GameVersion::WiiUsaRev0, 0x5B00}, + {GameVersion::WiiUsa, 0x5900}, + {GameVersion::WiiPal, 0x5900}, + {GameVersion::WiiJpn, 0x5900}, +}); + +DEFINE_MANT_ASSET(l_Egnd_mantPAL, u8, 0x60, { + {GameVersion::GcnUsa, 0x9C00}, + {GameVersion::GcnPal, 0x9C00}, + {GameVersion::GcnJpn, 0x9C00}, + {GameVersion::WiiUsaRev0, 0x9B00}, + {GameVersion::WiiUsa, 0x9900}, + {GameVersion::WiiPal, 0x9900}, + {GameVersion::WiiJpn, 0x9900}, +}); + #define l_Egnd_mantTEX (l_Egnd_mantTEX_get()) #define l_Egnd_mantTEX_U (l_Egnd_mantTEX_U_get()) #define l_Egnd_mantPAL (l_Egnd_mantPAL_get()) @@ -35,14 +73,39 @@ static TGXTexObj mainTexObj; static TGXTexObj undersideTexObj; // l_pos is unused -//static f32* l_pos_get() { alignas(32) static f32 buf[507]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0xA44C}, {GameVersion::GcnPal, 0xA44C}}, sizeof(buf)), true); return buf; } -static f32* l_normal_get() { alignas(32) static f32 buf[3]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x9C60}, {GameVersion::GcnPal, 0x9C60}, {GameVersion::GcnJpn, 0x9C60}}, sizeof(buf)), true); return buf; } -static f32* l_texCoord_get() { alignas(32) static f32 buf[338]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0xA458}, {GameVersion::GcnPal, 0xA458}, {GameVersion::GcnJpn, 0xA458}}, sizeof(buf)), true); return buf; } -//#define l_pos (l_pos_get()) -#define l_normal (l_normal_get()) +// DEFINE_MANT_ASSET(l_pos, f32, 507, { +// {GameVersion::GcnUsa, 0x9C60}, +// {GameVersion::GcnPal, 0x9C60}, +// }); + +alignas(32) static f32 const l_normal[3] = {0.0f, 1.0f, 0.0f}; + +DEFINE_MANT_ASSET(l_texCoord, f32, 338, { + {GameVersion::GcnUsa, 0xA458}, + {GameVersion::GcnPal, 0xA458}, + {GameVersion::GcnJpn, 0xA458}, + {GameVersion::WiiUsaRev0, 0x9B60}, + {GameVersion::WiiUsa, 0x9960}, + {GameVersion::WiiPal, 0x9960}, + {GameVersion::WiiJpn, 0x9960}, +}); + +// #define l_pos (l_pos_get()) #define l_texCoord (l_texCoord_get()) static bool l_Egnd_mantTEX_hasReplacement = false; + +DEFINE_MANT_ASSET(l_Egnd_mantDL, u8, 0x3EC, { + {GameVersion::GcnUsa, 0xA9A0}, + {GameVersion::GcnPal, 0xA9A0}, + {GameVersion::GcnJpn, 0xA9A0}, + {GameVersion::WiiUsaRev0, 0xA0C0}, + {GameVersion::WiiUsa, 0x9EC0}, + {GameVersion::WiiPal, 0x9EC0}, + {GameVersion::WiiJpn, 0x9EC0}, +}); + +#define l_Egnd_mantDL (l_Egnd_mantDL_get()) #else #include "assets/l_Egnd_mantTEX.h" @@ -52,16 +115,9 @@ static bool l_Egnd_mantTEX_hasReplacement = false; #endif #include "d/d_s_play.h" -#if TARGET_PC -using GameVersion = dusk::version::GameVersion; - -static u8* l_Egnd_mantDL_get() { alignas(32) static u8 buf[0x3EC]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0xA9A0}, {GameVersion::GcnPal, 0xA9A0}, {GameVersion::GcnJpn, 0xA9A0}}, 0x3EC), true); return buf; } -#define l_Egnd_mantDL (l_Egnd_mantDL_get()) -#else -#include "assets/l_Egnd_mantDL.h" -#endif - #if !TARGET_PC +#include "assets/l_Egnd_mantDL.h" + static void* pal_d = (void*)&l_Egnd_mantPAL; static void* tex_d[2] = { diff --git a/src/d/actor/d_a_mg_rod.cpp b/src/d/actor/d_a_mg_rod.cpp index f6f3b8d6c6..545b6c3069 100644 --- a/src/d/actor/d_a_mg_rod.cpp +++ b/src/d/actor/d_a_mg_rod.cpp @@ -5874,8 +5874,8 @@ static int dmg_rod_Execute(dmg_rod_class* i_this) { #if TARGET_PC if (dusk::getSettings().game.buttonFishing) { if ((item_any_fishing_rod(dComIfGp_getSelectItem(0)) && mDoCPd_c::getHoldX(PAD_1)) || - (item_any_fishing_rod(dComIfGp_getSelectItem(1)) && mDoCPd_c::getHoldY(PAD_1))) - { + (item_any_fishing_rod(dComIfGp_getSelectItem(1)) && mDoCPd_c::getHoldY(PAD_1)) || + (i_this->action == ACTION_LURE_STANDBY && mDoCPd_c::getTrigB(PAD_1))) { i_this->rod_stick_y = -1.0f; i_this->rod_substick_y = -1.0f; } diff --git a/src/d/actor/d_a_npc_aru.cpp b/src/d/actor/d_a_npc_aru.cpp index 7e6558b877..5b6444123c 100644 --- a/src/d/actor/d_a_npc_aru.cpp +++ b/src/d/actor/d_a_npc_aru.cpp @@ -1744,7 +1744,9 @@ int daNpc_Aru_c::cutSpeakTo(int i_staffID) { switch (eventId) { case 1: if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { - mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("goats_reward", itemNo, this); + mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, + 0, -1, -1, NULL, NULL DUSK_GIVE_TAG("goats_reward")); } if (fopAcM_IsExecuting(mItemPartnerId)) { diff --git a/src/d/actor/d_a_npc_ashB.cpp b/src/d/actor/d_a_npc_ashB.cpp index a4dd015906..f9c7df2cc2 100644 --- a/src/d/actor/d_a_npc_ashB.cpp +++ b/src/d/actor/d_a_npc_ashB.cpp @@ -1001,8 +1001,9 @@ BOOL daNpcAshB_c::EvCut_Appear(int i_staffID) { case '0008': local_30[0] = 0; if (mFlow.getEventId(local_30) == 1) { - mItemPartnerId = - fopAcM_createItemForPresentDemo(¤t.pos, local_30[0], 0, -1, -1, 0, 0); + DUSK_ITEM_CHECK("ashei_sketch", local_30[0], this); + mItemPartnerId = fopAcM_createItemForPresentDemo( + ¤t.pos, local_30[0], 0, -1, -1, 0, 0 DUSK_GIVE_TAG("ashei_sketch")); dComIfGp_event_setItemPartnerId(mItemPartnerId); mItemPartnerId = -1; } diff --git a/src/d/actor/d_a_npc_chin.cpp b/src/d/actor/d_a_npc_chin.cpp index f31cac9822..fb91bd79d8 100644 --- a/src/d/actor/d_a_npc_chin.cpp +++ b/src/d/actor/d_a_npc_chin.cpp @@ -1804,8 +1804,19 @@ int daNpcChin_c::_Evt_GameSucceed_CutMain(const int& param_0) { itemId1 = 0; } +#if TARGET_PC + const char* itemCheckName = nullptr; + if (itemId1 == dItemNo_ARROW_LV2_e) { + itemCheckName = "star_reward_1"; + } else if (itemId1 == dItemNo_ARROW_LV3_e) { + itemCheckName = "star_reward_2"; + } + if (itemCheckName != nullptr) { + itemId1 = dusk::mods::item_check(itemCheckName, itemId1, this); + } +#endif fpc_ProcID itemId2 = fopAcM_createItemForPresentDemo(¤t.pos, itemId1, 0, -1, -1, - 0, 0); + 0, 0 IF_DUSK_ARG(dusk::mods::item_give_tag(itemCheckName))); if (itemId2 != -1) { dComIfGp_event_setItemPartnerId(itemId2); } diff --git a/src/d/actor/d_a_npc_fairy.cpp b/src/d/actor/d_a_npc_fairy.cpp index 5de667f237..0b911973e4 100644 --- a/src/d/actor/d_a_npc_fairy.cpp +++ b/src/d/actor/d_a_npc_fairy.cpp @@ -1333,7 +1333,9 @@ void daNpc_Fairy_c::PresentDemoCall() { item_no = 0; } - fpc_ProcID id = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("fairy_reward:D_SB01", item_no, this); + fpc_ProcID id = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 0, -1, -1, NULL, + NULL DUSK_GIVE_TAG("fairy_reward:D_SB01")); if (id != fpcM_ERROR_PROCESS_ID_e) { dComIfGp_event_setItemPartnerId(id); } diff --git a/src/d/actor/d_a_npc_gra.cpp b/src/d/actor/d_a_npc_gra.cpp index a43491a57a..4300348ff9 100644 --- a/src/d/actor/d_a_npc_gra.cpp +++ b/src/d/actor/d_a_npc_gra.cpp @@ -4018,8 +4018,15 @@ BOOL daNpc_grA_c::talk(void*) { } if (r26 && talkProc(NULL, TRUE, NULL)) { if (mFlow.getEventId(&sp8) == 1) { - field_0x1480 = - fopAcM_createItemForPresentDemo(¤t.pos, sp8, 0, -1, -1, NULL, NULL); +#if TARGET_PC + const char* itemCheckName = nullptr; + if (sp8 == dItemNo_BOMB_IN_BAG_e) { + itemCheckName = "goron_reward:F_SP113"; + sp8 = dusk::mods::item_check(itemCheckName, sp8, this); + } +#endif + field_0x1480 = fopAcM_createItemForPresentDemo(¤t.pos, sp8, 0, -1, -1, NULL, + NULL IF_DUSK_ARG(dusk::mods::item_give_tag(itemCheckName))); if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) { s16 r25 = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff); dComIfGp_getEvent()->reset(this); diff --git a/src/d/actor/d_a_npc_gro.cpp b/src/d/actor/d_a_npc_gro.cpp index bdf984f35f..e1b9e457de 100644 --- a/src/d/actor/d_a_npc_gro.cpp +++ b/src/d/actor/d_a_npc_gro.cpp @@ -1681,7 +1681,9 @@ int daNpc_grO_c::talk(void* param_1) { if (facePlayerFlag && talkProc(NULL, TRUE, NULL)) { if (mType == TYPE_MINES) { if (mFlow.getEventId(&itemId) == 1) { - mItemID = fopAcM_createItemForPresentDemo(¤t.pos, itemId, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("key_shard_2:D_MN04", itemId, this); + mItemID = fopAcM_createItemForPresentDemo(¤t.pos, itemId, 0, -1, -1, + NULL, NULL DUSK_GIVE_TAG("key_shard_2:D_MN04")); if (mItemID != fpcM_ERROR_PROCESS_ID_e) { s16 eventIdx = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xFF); dComIfGp_getEvent()->reset(this); diff --git a/src/d/actor/d_a_npc_grr.cpp b/src/d/actor/d_a_npc_grr.cpp index ee0d50d858..4a978c586e 100644 --- a/src/d/actor/d_a_npc_grr.cpp +++ b/src/d/actor/d_a_npc_grr.cpp @@ -1338,7 +1338,9 @@ int daNpc_grR_c::talk(void* param_1) { if (bVar1 && talkProc(NULL, TRUE, NULL)) { if (mType == TYPE_0) { if (mFlow.getEventId(&i_itemNo) == 1) { - mItemID = fopAcM_createItemForPresentDemo(¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("key_shard_3:D_MN04", i_itemNo, this); + mItemID = fopAcM_createItemForPresentDemo(¤t.pos, i_itemNo, 0, -1, -1, + NULL, NULL DUSK_GIVE_TAG("key_shard_3:D_MN04")); if (mItemID != fpcM_ERROR_PROCESS_ID_e) { s16 i_eventID = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xFF); diff --git a/src/d/actor/d_a_npc_grs.cpp b/src/d/actor/d_a_npc_grs.cpp index abab07d22b..462f77b8a7 100644 --- a/src/d/actor/d_a_npc_grs.cpp +++ b/src/d/actor/d_a_npc_grs.cpp @@ -1190,8 +1190,9 @@ int daNpc_grS_c::talk(void* param_0) { if (unkFlag1 && talkProc(NULL, 1, NULL)) { if (mType == 0) { if (mFlow.getEventId(&unkInt2) == 1) { - mPresentItemId = - fopAcM_createItemForPresentDemo(¤t.pos, unkInt2, 0, -1, -1, 0, 0); + DUSK_ITEM_CHECK("key_shard_1:D_MN04", unkInt2, this); + mPresentItemId = fopAcM_createItemForPresentDemo(¤t.pos, unkInt2, 0, -1, + -1, 0, 0 DUSK_GIVE_TAG("key_shard_1:D_MN04")); if (mPresentItemId != fpcM_ERROR_PROCESS_ID_e) { s16 eventIdx = diff --git a/src/d/actor/d_a_npc_impal.cpp b/src/d/actor/d_a_npc_impal.cpp index 5ac057ce06..171c36c289 100644 --- a/src/d/actor/d_a_npc_impal.cpp +++ b/src/d/actor/d_a_npc_impal.cpp @@ -971,8 +971,9 @@ BOOL daNpcImpal_c::EvCut_ImpalAppear1(int i_cut_index) { if (talkProc(NULL, 1, NULL)) { int evt_id = 0; if (mFlow.getEventId(&evt_id) == 1) { - mItemPartnerId = - fopAcM_createItemForPresentDemo(¤t.pos, evt_id, 0, -1, -1, 0, 0); + DUSK_ITEM_CHECK("ilia_charm", evt_id, this); + mItemPartnerId = fopAcM_createItemForPresentDemo( + ¤t.pos, evt_id, 0, -1, -1, 0, 0 DUSK_GIVE_TAG("ilia_charm")); if (mItemPartnerId != 0xffffffff) { s16 evt_idx = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff); @@ -1060,8 +1061,9 @@ BOOL daNpcImpal_c::EvCut_CopyRod(int i_cut_index) { case '0003': int evt_id = 0; if (mFlow.getEventId(&evt_id) == 1) { - mItemPartnerId = - fopAcM_createItemForPresentDemo(¤t.pos, evt_id, 0, -1, -1, 0, 0); + DUSK_ITEM_CHECK("skybook", evt_id, this); + mItemPartnerId = fopAcM_createItemForPresentDemo( + ¤t.pos, evt_id, 0, -1, -1, 0, 0 DUSK_GIVE_TAG("skybook")); dComIfGp_event_setItemPartnerId(mItemPartnerId); mItemPartnerId = -1; } diff --git a/src/d/actor/d_a_npc_ins.cpp b/src/d/actor/d_a_npc_ins.cpp index 0ce91ca914..ede3f53664 100644 --- a/src/d/actor/d_a_npc_ins.cpp +++ b/src/d/actor/d_a_npc_ins.cpp @@ -11,6 +11,10 @@ #include "d/d_msg_object.h" #include +#if TARGET_PC +static u8 s_givenInsectId = dItemNo_NONE_e; +#endif + enum Ins_RES_File_ID { /* BCK */ /* 0x06 */ BCK_INS_F_HAPPY = 0x6, @@ -1259,6 +1263,7 @@ int daNpcIns_c::waitPresent(void* param_1) { daPy_py_c* player = daPy_getPlayerActorClass(); player->changeOriginalDemo(); player->changeDemoMode(0x25, 2, type, 0); + IF_DUSK(s_givenInsectId = type;) } else { mInsectMsgNo = 0x719; } @@ -1487,7 +1492,17 @@ int daNpcIns_c::talk(void* param_1) { OS_REPORT("会話終了時 イベントID=%d アイテムNo=%d\n", eventID, itemNo); if (eventID == 1) { - mItemID = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, NULL); +#if TARGET_PC + u32 itemGiveTag = 0; + if (s_givenInsectId != dItemNo_NONE_e) { + itemNo = dusk::mods::item_check_bug( + s_givenInsectId, itemNo & 0xFF, this); + itemGiveTag = dusk::mods::item_give_tag_bug(s_givenInsectId); + s_givenInsectId = dItemNo_NONE_e; + } +#endif + mItemID = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, + -1, NULL, NULL IF_DUSK_ARG(itemGiveTag)); if (mItemID != fpcM_ERROR_PROCESS_ID_e) { daPy_getPlayerActorClass()->cancelOriginalDemo(); diff --git a/src/d/actor/d_a_npc_kkri.cpp b/src/d/actor/d_a_npc_kkri.cpp index 9b6b17d2e9..4d9c882d03 100644 --- a/src/d/actor/d_a_npc_kkri.cpp +++ b/src/d/actor/d_a_npc_kkri.cpp @@ -1181,7 +1181,16 @@ int daNpc_Kkri_c::talk(void*) { switch (eventId) { case 1: if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { - mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 0, -1, -1, NULL, NULL); +#if TARGET_PC + const char* itemCheckName = nullptr; + if (item_no == dItemNo_OIL_BOTTLE3_e) { + itemCheckName = "coro_bottle"; + item_no = dusk::mods::item_check(itemCheckName, item_no, this); + } +#endif + mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, item_no, + 0, -1, -1, NULL, + NULL IF_DUSK_ARG(dusk::mods::item_give_tag(itemCheckName))); } if (fopAcM_IsExecuting(mItemPartnerId)) { diff --git a/src/d/actor/d_a_npc_len.cpp b/src/d/actor/d_a_npc_len.cpp index 7b7acab6fd..2568462e8b 100644 --- a/src/d/actor/d_a_npc_len.cpp +++ b/src/d/actor/d_a_npc_len.cpp @@ -1236,8 +1236,9 @@ int daNpc_Len_c::talk(void* param_0) { switch (evt_id) { case 1: if (mItemPartnerId == -1) { + DUSK_ITEM_CHECK("renado_letter", local_18, this); mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, local_18, - 0, -1, -1, 0, 0); + 0, -1, -1, 0, 0 DUSK_GIVE_TAG("renado_letter")); } if (fopAcM_IsExecuting(mItemPartnerId)) { mEvtNo = 1; diff --git a/src/d/actor/d_a_npc_maro.cpp b/src/d/actor/d_a_npc_maro.cpp index 4e2c3ad870..d985349a48 100644 --- a/src/d/actor/d_a_npc_maro.cpp +++ b/src/d/actor/d_a_npc_maro.cpp @@ -2393,7 +2393,10 @@ int daNpc_Maro_c::cutArrowTutorial(int arg0) { switch (evt_ret) { case 1: { if (mItemPartnerId == -1) { - mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, evt_id, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("archery_reward:F_SP109", evt_id, this); + mItemPartnerId = + fopAcM_createItemForPresentDemo(¤t.pos, evt_id, 0, -1, -1, + NULL, NULL DUSK_GIVE_TAG("archery_reward:F_SP109")); } if (fopAcM_IsExecuting(mItemPartnerId)) { diff --git a/src/d/actor/d_a_npc_myna2.cpp b/src/d/actor/d_a_npc_myna2.cpp index ede18be556..faa5301bb8 100644 --- a/src/d/actor/d_a_npc_myna2.cpp +++ b/src/d/actor/d_a_npc_myna2.cpp @@ -1191,7 +1191,15 @@ int daNpc_myna2_c::ECut_gameGoalSuccess(int i_staffId) { case 20: { int itemNo = 0; if (mFlow.getEventId(&itemNo) == 1) { - mItemPid = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, NULL); +#if TARGET_PC + const char* itemCheckName = nullptr; + if (itemNo == dItemNo_KAKERA_HEART_e) { + itemCheckName = "plumm_minigame_reward"; + itemNo = dusk::mods::item_check(itemCheckName, itemNo, this); + } +#endif + mItemPid = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, + NULL IF_DUSK_ARG(dusk::mods::item_give_tag(itemCheckName))); } break; } diff --git a/src/d/actor/d_a_npc_pouya.cpp b/src/d/actor/d_a_npc_pouya.cpp index 72bad48e4b..6101b64a93 100644 --- a/src/d/actor/d_a_npc_pouya.cpp +++ b/src/d/actor/d_a_npc_pouya.cpp @@ -958,8 +958,19 @@ int daNpc_Pouya_c::cutHaveFavorToAsk(int param_0) { switch (evt_id) { case 1: if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { +#if TARGET_PC + const char* itemCheckName = nullptr; + if (local_64 == dItemNo_DROP_BOTTLE_e) { + itemCheckName = "jovani_reward_1"; + } else if (local_64 == dItemNo_SILVER_RUPEE_e) { + itemCheckName = "jovani_reward_2"; + } + if (itemCheckName != nullptr) { + local_64 = dusk::mods::item_check(itemCheckName, local_64, this); + } +#endif mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, local_64, 0, - -1, -1, 0, 0); + -1, -1, 0, 0 IF_DUSK_ARG(dusk::mods::item_give_tag(itemCheckName))); } if (fopAcM_IsExecuting(mItemPartnerId)) { field_0xfce = 1; diff --git a/src/d/actor/d_a_npc_prayer.cpp b/src/d/actor/d_a_npc_prayer.cpp index 9191bc0315..1e10121f56 100644 --- a/src/d/actor/d_a_npc_prayer.cpp +++ b/src/d/actor/d_a_npc_prayer.cpp @@ -720,7 +720,8 @@ fpc_ProcID daNpcPray_c::createHeart() { mDoMtx_stack_c::ZXYrotS(rot); mDoMtx_stack_c::multVec(&offset, &offset); pos += offset; - return fopAcM_createItemForBoss(&pos, dItemNo_KAKERA_HEART_e, fopAcM_GetRoomNo(this), &rot, &size, 0.0f, 0.0f, 0); + return fopAcM_createItemForBoss(&pos, dItemNo_KAKERA_HEART_e, fopAcM_GetRoomNo(this), &rot, + &size, 0.0f, 0.0f, 0 IF_DUSK_ARG("prayer_reward")); } BOOL daNpcPray_c::_Evt_GetHeart(int i_staffID) { diff --git a/src/d/actor/d_a_npc_rafrel.cpp b/src/d/actor/d_a_npc_rafrel.cpp index 8a4e5d03ef..d51d514785 100644 --- a/src/d/actor/d_a_npc_rafrel.cpp +++ b/src/d/actor/d_a_npc_rafrel.cpp @@ -1310,7 +1310,9 @@ bool daNpcRafrel_c::talk(void* param_0) { OS_REPORT("会話終了時 イベントID=%d アイテムNo=%d\n", eventId, itemNo); if (eventId == 1) { - field_0xe00 = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("auru_memo", itemNo, this); + field_0xe00 = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, + NULL, NULL DUSK_GIVE_TAG("auru_memo")); if (field_0xe00 != fpcM_ERROR_PROCESS_ID_e) { s16 eventIdx = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xFF); dComIfGp_getEvent()->reset(this); @@ -1562,7 +1564,9 @@ int daNpcRafrel_c::EvCut_Appear(int i_staffId) { int itemNo = 0; u16 eventId = mFlow.getEventId(&itemNo); if (eventId == 1) { - field_0xe00 = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("auru_memo", itemNo, this); + field_0xe00 = fopAcM_createItemForPresentDemo( + ¤t.pos, itemNo, 0, -1, -1, NULL, NULL DUSK_GIVE_TAG("auru_memo")); dComIfGp_event_setItemPartnerId(field_0xe00); field_0xe00 = fpcM_ERROR_PROCESS_ID_e; } diff --git a/src/d/actor/d_a_npc_the.cpp b/src/d/actor/d_a_npc_the.cpp index 01b7fbb66c..7291e14715 100644 --- a/src/d/actor/d_a_npc_the.cpp +++ b/src/d/actor/d_a_npc_the.cpp @@ -823,8 +823,9 @@ BOOL daNpcThe_c::talk(void* param_0) { } int item_no = 0; if (mFlow.getEventId(&item_no) == 1) { + DUSK_ITEM_CHECK("telma_invoice", item_no, this); mItemID = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 0, -1, -1, - NULL, NULL); + NULL, NULL DUSK_GIVE_TAG("telma_invoice")); if (mItemID != -1) { s16 event_id = dComIfGp_getEventManager().getEventIdx( this, "DEFAULT_GETITEM", 0xff); diff --git a/src/d/actor/d_a_npc_uri.cpp b/src/d/actor/d_a_npc_uri.cpp index 44df1060d2..afb1d11b1b 100644 --- a/src/d/actor/d_a_npc_uri.cpp +++ b/src/d/actor/d_a_npc_uri.cpp @@ -1165,8 +1165,9 @@ int daNpc_Uri_c::cutEndCarryTutorial(int param_1) { (s32)mFlow.getEventId(&local_48) == 1) { if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { - mItemPartnerId = - fopAcM_createItemForPresentDemo(¤t.pos, local_48, 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("uli_cradle_reward", local_48, this); + mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, local_48, 0, -1, -1, + NULL, NULL DUSK_GIVE_TAG("uli_cradle_reward")); } if (fopAcM_IsExecuting(mItemPartnerId)) { /* T_0007 - Ordon Village - During Uli's pick-up tutorial */ diff --git a/src/d/actor/d_a_npc_ykw.cpp b/src/d/actor/d_a_npc_ykw.cpp index 7ca9c4f36f..7b84e1a1bb 100644 --- a/src/d/actor/d_a_npc_ykw.cpp +++ b/src/d/actor/d_a_npc_ykw.cpp @@ -2213,8 +2213,9 @@ int daNpc_ykW_c::cutEndSnowboardRace(int param_0) { switch (eventId) { case 1: if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { - mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, itemId, 0, - -1, -1, 0, 0); + DUSK_ITEM_CHECK("snowboard_race_reward", itemId, this); + mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, itemId, 0, -1, + -1, 0, 0 DUSK_GIVE_TAG("snowboard_race_reward")); } if (fopAcM_IsExecuting(mItemPartnerId)) { @@ -2911,8 +2912,9 @@ int daNpc_ykW_c::talk(void* param_0) { switch (eventId) { case 1: if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { - mItemPartnerId = - fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, 0, 0); + DUSK_ITEM_CHECK("dungeon_map:D_MN11", itemNo, this); + mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, + 0, -1, -1, 0, 0 DUSK_GIVE_TAG("dungeon_map:D_MN11")); } if (fopAcM_IsExecuting(mItemPartnerId)) { diff --git a/src/d/actor/d_a_npc_zra.inc b/src/d/actor/d_a_npc_zra.inc index 2ada4601c8..70426d5f99 100644 --- a/src/d/actor/d_a_npc_zra.inc +++ b/src/d/actor/d_a_npc_zra.inc @@ -2609,8 +2609,19 @@ BOOL daNpc_zrA_c::ECut_thanksBlast(int i_staffID) { case 31: { int item_id = 0; if (mFlow.getEventId(&item_id) == 1) { - mItemID = fopAcM_createItemForPresentDemo(¤t.pos, item_id, - 0, -1, -1, NULL, NULL); +#if TARGET_PC + const char* itemCheckName = nullptr; + if (item_id == dItemNo_BOMB_IN_BAG_e) { + itemCheckName = "iza_reward_1"; + } else if (item_id == dItemNo_BOMB_BAG_LV2_e) { + itemCheckName = "iza_reward_2"; + } + if (itemCheckName != nullptr) { + item_id = dusk::mods::item_check(itemCheckName, item_id, this); + } +#endif + mItemID = fopAcM_createItemForPresentDemo(¤t.pos, item_id, 0, -1, -1, NULL, + NULL IF_DUSK_ARG(dusk::mods::item_give_tag(itemCheckName))); } field_0x9eb = true; break; diff --git a/src/d/actor/d_a_npc_zrc.cpp b/src/d/actor/d_a_npc_zrc.cpp index 60ae6713e5..f94dbaf087 100644 --- a/src/d/actor/d_a_npc_zrc.cpp +++ b/src/d/actor/d_a_npc_zrc.cpp @@ -1681,8 +1681,9 @@ BOOL daNpc_zrC_c::ECut_earringGet(int i_staffID) { case 40: { int item_no = 0; if (mFlow.getEventId(&item_no) == 1) { - mItemID = fopAcM_createItemForPresentDemo(¤t.pos, item_no, - 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("coral_earring", item_no, this); + mItemID = fopAcM_createItemForPresentDemo( + ¤t.pos, item_no, 0, -1, -1, NULL, NULL DUSK_GIVE_TAG("coral_earring")); } break; } diff --git a/src/d/actor/d_a_npc_zrz.cpp b/src/d/actor/d_a_npc_zrz.cpp index 9e45b34f86..0662a290cd 100644 --- a/src/d/actor/d_a_npc_zrz.cpp +++ b/src/d/actor/d_a_npc_zrz.cpp @@ -1729,8 +1729,9 @@ BOOL daNpc_zrZ_c::ECut_clothesGet(int i_staffID) { } item_no = 0; if (mFlow.getEventId(&item_no) == 1) { - mItemID = fopAcM_createItemForPresentDemo(¤t.pos, item_no, - 0, -1, -1, NULL, NULL); + DUSK_ITEM_CHECK("zora_armor", item_no, this); + mItemID = fopAcM_createItemForPresentDemo( + ¤t.pos, item_no, 0, -1, -1, NULL, NULL DUSK_GIVE_TAG("zora_armor")); } break; diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index 0dc9ff833b..1176f9aab3 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -206,7 +206,7 @@ void daItem_c::CreateInit() { initBaseMtx(); animPlay(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f); - if (m_itemNo == dItemNo_BOOMERANG_e) { + if (m_itemNo == dItemNo_BOOMERANG_e IF_DUSK(&&!mItemOverridden)) { itemGetNextExecute(); } else if ((m_itemNo == dItemNo_ORANGE_RUPEE_e || m_itemNo == dItemNo_SILVER_RUPEE_e) && mSparkleEmtr.getEmitter() == NULL) @@ -264,6 +264,17 @@ int daItem_c::_daItem_create() { shape_angle.z = 0; shape_angle.x = 0; +#if TARGET_PC + const u32 params = fopAcM_GetParam(this); + mOriginalItemNo = params & 0xFF; + const u8 resolvedItem = dusk::mods::item_check_freestanding( + daItem_prm::getItemBitNo(this), mOriginalItemNo, this); + mItemOverridden = resolvedItem != mOriginalItemNo; + mItemGiveTag = dusk::mods::item_give_tag_freestanding(daItem_prm::getItemBitNo(this)); + if (mItemOverridden) { + fopAcM_SetParam(this, (params & 0xFFFFFF00) | resolvedItem); + } +#endif field_0x95d = true; } @@ -507,9 +518,18 @@ void daItem_c::procInitGetDemoEvent() { fopAcM_orderItemEvent(this, 0, 0); eventInfo.onCondition(dEvtCnd_CANGETITEM_e); - m_item_id = fopAcM_createItemForTrBoxDemo(¤t.pos, m_itemNo, -1, fopAcM_GetRoomNo(this), - NULL, NULL); +#if TARGET_PC + const u8 displayItemNo = m_itemNo; + if (mItemOverridden) { + m_itemNo = dusk::mods::item_check_tagged(mItemGiveTag, mOriginalItemNo, this); + } +#endif + m_item_id = fopAcM_createItemForTrBoxDemo( + ¤t.pos, m_itemNo, -1, fopAcM_GetRoomNo(this), NULL, NULL IF_DUSK_ARG(mItemGiveTag)); JUT_ASSERT(0, m_item_id != fpcM_ERROR_PROCESS_ID_e); +#if TARGET_PC + m_itemNo = displayItemNo; +#endif setStatus(STATUS_WAIT_GET_DEMO_EVENT_e); } @@ -521,7 +541,7 @@ void daItem_c::procWaitGetDemoEvent() { dComIfGp_event_setItemPartnerId(m_item_id); } } else { - if (m_itemNo == dItemNo_BOOMERANG_e) { + if (m_itemNo == dItemNo_BOOMERANG_e IF_DUSK(&&!mItemOverridden)) { fopAcM_orderItemEvent(this, 0, 0); eventInfo.onCondition(dEvtCnd_CANGETITEM_e); return; @@ -537,7 +557,9 @@ void daItem_c::procWaitGetDemoEvent() { procInitSimpleGetDemo(); itemGet(); - if (!haveItem) { + if (!haveItem IF_DUSK(&&(!mItemOverridden || (m_itemNo >= dItemNo_GREEN_RUPEE_e && + m_itemNo <= dItemNo_SILVER_RUPEE_e)))) + { dComIfGs_offItemFirstBit(m_itemNo); } } else { @@ -864,6 +886,12 @@ void daItem_c::itemGetNextExecute() { procInitGetDemoEvent(); break; default: +#if TARGET_PC + if (mItemOverridden) { + procInitGetDemoEvent(); + break; + } +#endif // "[daItem_c] Get process not defined[%d]\n" OS_REPORT_ERROR("[daItem_c]ゲット処理が定義されていません[%d]\n", m_itemNo); } @@ -877,38 +905,48 @@ void daItem_c::itemGetNextExecute() { } void daItem_c::itemGet() { +#if TARGET_PC + const u8 displayItemNo = m_itemNo; + if (mItemOverridden) { + m_itemNo = dusk::mods::item_check_tagged(mItemGiveTag, mOriginalItemNo, this); + } +#endif switch (m_itemNo) { +#if TARGET_PC + case dItemNo_UTAWA_HEART_e: + case dItemNo_KAKERA_HEART_e: +#endif case dItemNo_HEART_e: mDoAud_seStart(Z2SE_HEART_PIECE_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_GREEN_RUPEE_e: mDoAud_seStart(Z2SE_GREEN_LUPY_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_BLUE_RUPEE_e: mDoAud_seStart(Z2SE_BLUE_LUPY_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_YELLOW_RUPEE_e: mDoAud_seStart(Z2SE_BLUE_LUPY_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_RED_RUPEE_e: mDoAud_seStart(Z2SE_RED_LUPY_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_PURPLE_RUPEE_e: mDoAud_seStart(Z2SE_RED_LUPY_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_ORANGE_RUPEE_e: mDoAud_seStart(Z2SE_RED_LUPY_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_SILVER_RUPEE_e: mDoAud_seStart(Z2SE_RED_LUPY_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_BOOMERANG_e: break; @@ -918,12 +956,25 @@ void daItem_c::itemGet() { case dItemNo_ARROW_1_e: case dItemNo_PACHINKO_SHOT_e: mDoAud_seStart(Z2SE_CONSUMP_ITEM_GET, NULL, 0, 0); - execItemGet(m_itemNo); + execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); +#if TARGET_PC + break; +#endif default: +#if TARGET_PC + if (mItemOverridden) { + mDoAud_seStart(Z2SE_CONSUMP_ITEM_GET, NULL, 0, 0); + execItemGet(m_itemNo, mItemGiveTag, this); + break; + } +#endif // "[daItem_c] Get process not defined[%d]\n" OS_REPORT_ERROR("[daItem_c]ゲット処理が定義されていません[%d]\n", m_itemNo); break; } +#if TARGET_PC + m_itemNo = displayItemNo; +#endif } BOOL daItem_c::checkCountTimer() { diff --git a/src/d/actor/d_a_obj_life_container.cpp b/src/d/actor/d_a_obj_life_container.cpp index 22706ba447..3c800e946f 100644 --- a/src/d/actor/d_a_obj_life_container.cpp +++ b/src/d/actor/d_a_obj_life_container.cpp @@ -97,6 +97,12 @@ int daObjLife_c::Create() { field_0x94c = 0.7f; mRotateSpeed = 7000; +#if TARGET_PC + if (mOverrideHover) { + fopAcM_SetGravity(this, 0.0f); + mRotateSpeed = 550; + } +#endif setEffect(); mSound.init(¤t.pos, 1); return 1; @@ -140,6 +146,27 @@ int daObjLife_c::create() { home.angle.x = home.angle.z = 0; current.angle.x = current.angle.z = 0; shape_angle.x = shape_angle.z = 0; +#if TARGET_PC + const u32 params = fopAcM_GetParam(this); + const u8 parameterItemNo = params & 0xFF; + if (mItemGiveOriginalNo == dItemNo_NONE_e) { + mOriginalItemNo = parameterItemNo; + const u8 resolvedItem = + dusk::mods::item_check_freestanding(getSaveBitNo(), mOriginalItemNo, this); + mItemGiveTag = dusk::mods::item_give_tag_freestanding(getSaveBitNo()); + mItemOverridden = resolvedItem != mOriginalItemNo; + if (mItemOverridden) { + fopAcM_SetParam(this, (params & 0xFFFFFF00) | resolvedItem); + } + } else { + mOriginalItemNo = mItemGiveOriginalNo; + mItemOverridden = parameterItemNo != mItemGiveOriginalNo; + } + mOverrideHover = + mItemOverridden && + (mOriginalItemNo == dItemNo_UTAWA_HEART_e || + (mOriginalItemNo >= dItemNo_M_BEETLE_e && mOriginalItemNo <= dItemNo_F_MAYFLY_e)); +#endif mIsPrmsInit = true; } @@ -153,7 +180,7 @@ int daObjLife_c::create() { return cPhs_ERROR_e; } - if (m_itemNo == dItemNo_UTAWA_HEART_e && dComIfGs_isStageLife()) { + if (m_itemNo == dItemNo_UTAWA_HEART_e && dComIfGs_isStageLife() IF_DUSK(&&!mItemOverridden)) { return cPhs_ERROR_e; } @@ -296,8 +323,18 @@ int daObjLife_c::initActionOrderGetDemo() { fopAcM_orderItemEvent(this, 0, 0); eventInfo.onCondition(dEvtCnd_CANGETITEM_e); - mItemId = fopAcM_createItemForTrBoxDemo(¤t.pos, m_itemNo, -1, fopAcM_GetRoomNo(this), NULL, NULL); +#if TARGET_PC + const u8 displayItemNo = m_itemNo; + if (mItemOverridden) { + m_itemNo = dusk::mods::item_check_tagged(mItemGiveTag, mOriginalItemNo, this); + } +#endif + mItemId = fopAcM_createItemForTrBoxDemo( + ¤t.pos, m_itemNo, -1, fopAcM_GetRoomNo(this), NULL, NULL IF_DUSK_ARG(mItemGiveTag)); JUT_ASSERT(699, mItemId != fpcM_ERROR_PROCESS_ID_e); +#if TARGET_PC + m_itemNo = displayItemNo; +#endif setStatus(STATUS_ORDER_GET_DEMO_e); return 1; diff --git a/src/d/actor/d_a_obj_shield.cpp b/src/d/actor/d_a_obj_shield.cpp index 3941011043..abe56ad6c8 100644 --- a/src/d/actor/d_a_obj_shield.cpp +++ b/src/d/actor/d_a_obj_shield.cpp @@ -109,6 +109,7 @@ int daItemShield_c::__CreateHeap() { int daItemShield_c::create() { fopAcM_ct(this, daItemShield_c); m_itemNo = dItemNo_WOOD_SHIELD_e; + DUSK_ITEM_CHECK("ordon_shield", m_itemNo, this); if (fopAcM_isSwitch(this, getSwBit2())) { OS_REPORT("木の盾:もう取ったので出ません\n"); return cPhs_ERROR_e; @@ -240,8 +241,9 @@ int daItemShield_c::initActionOrderGetDemo() { daItemBase_c::hide(); fopAcM_orderItemEvent(this, 0, 0); eventInfo.onCondition(dEvtCnd_CANGETITEM_e); - mItemId = - fopAcM_createItemForTrBoxDemo(¤t.pos, m_itemNo, -1, fopAcM_GetRoomNo(this), 0, 0); + mItemId = fopAcM_createItemForTrBoxDemo(¤t.pos, + DUSK_ITEM_CHECK_EXPR("ordon_shield", dItemNo_WOOD_SHIELD_e, this), -1, + fopAcM_GetRoomNo(this), 0, 0 DUSK_GIVE_TAG("ordon_shield")); JUT_ASSERT(682, mItemId != fpcM_ERROR_PROCESS_ID_e) setStatus(STATUS_ORDERGETDEMO); return 1; diff --git a/src/d/actor/d_a_obj_sword.cpp b/src/d/actor/d_a_obj_sword.cpp index b346c4232e..d7bf0cfaab 100644 --- a/src/d/actor/d_a_obj_sword.cpp +++ b/src/d/actor/d_a_obj_sword.cpp @@ -37,6 +37,7 @@ int daObjSword_c::Create() { cPhs_Step daObjSword_c::create() { fopAcM_ct(this, daObjSword_c); m_itemNo = 0x28; + DUSK_ITEM_CHECK("ordon_sword", m_itemNo, this); if (fopAcM_isItem(this, getItemBit())) { return cPhs_ERROR_e; } @@ -71,8 +72,9 @@ int daObjSword_c::initActionOrderGetDemo() { hide(); fopAcM_orderItemEvent(this, 0, 0); eventInfo.onCondition(8); - mProcID = fopAcM_createItemForTrBoxDemo(¤t.pos, m_itemNo, -1, fopAcM_GetRoomNo(this), - NULL, NULL); + mProcID = + fopAcM_createItemForTrBoxDemo(¤t.pos, DUSK_ITEM_CHECK_EXPR("ordon_sword", 0x28, this), + -1, fopAcM_GetRoomNo(this), NULL, NULL DUSK_GIVE_TAG("ordon_sword")); setStatus(1); return 1; } diff --git a/src/d/actor/d_a_obj_wood_statue.cpp b/src/d/actor/d_a_obj_wood_statue.cpp index 7c6d69d576..9656036235 100644 --- a/src/d/actor/d_a_obj_wood_statue.cpp +++ b/src/d/actor/d_a_obj_wood_statue.cpp @@ -255,8 +255,9 @@ int daObjWStatue_c::initActionOrderGetDemo() { s16 eventIdx = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff); dComIfGp_getEvent()->reset(this); fopAcM_orderChangeEventId(this, eventIdx, 1, 0xffff); - mItemId = fopAcM_createItemForTrBoxDemo(¤t.pos, m_itemNo, 0xffffffff, - fopAcM_GetRoomNo(this), 0, 0); + mItemId = fopAcM_createItemForTrBoxDemo(¤t.pos, + DUSK_ITEM_CHECK_EXPR("wood_statue", m_itemNo, this), 0xffffffff, fopAcM_GetRoomNo(this), + 0, 0 DUSK_GIVE_TAG("wood_statue")); JUT_ASSERT(544, mItemId != fpcM_ERROR_PROCESS_ID_e); setStatus(STATUS_ORDER_GET_DEMO); return 1; diff --git a/src/d/actor/d_a_obj_zcloth.cpp b/src/d/actor/d_a_obj_zcloth.cpp index 49580d2589..7d9ca8fc51 100644 --- a/src/d/actor/d_a_obj_zcloth.cpp +++ b/src/d/actor/d_a_obj_zcloth.cpp @@ -32,6 +32,7 @@ int daObjZCloth_c::Create() { int daObjZCloth_c::create() { fopAcM_ct(this, daObjZCloth_c); m_itemNo = 0x31; + DUSK_ITEM_CHECK("zora_armor", m_itemNo, this); int phase = dComIfG_resLoad(&mPhase, dItem_data::getFieldArc(m_itemNo)); if (phase == cPhs_COMPLEATE_e) { if (!fopAcM_entrySolidHeap(this, (heapCallbackFunc)CheckFieldItemCreateHeap, 0x2fb0)) { diff --git a/src/d/actor/d_a_player.cpp b/src/d/actor/d_a_player.cpp index 8ab70d8907..48ad37e2ec 100644 --- a/src/d/actor/d_a_player.cpp +++ b/src/d/actor/d_a_player.cpp @@ -375,10 +375,14 @@ static const u8* l_sightDL_get() { static bool _ = ( dusk::LoadDolAsset( buf, -{ - {GameVersion::GcnUsa, 0x803BA0C0}, - {GameVersion::GcnPal, 0x803BBDA0}, - {GameVersion::GcnJpn, 0x803B4220} + { + {GameVersion::GcnUsa, 0x803BA0C0}, + {GameVersion::GcnPal, 0x803BBDA0}, + {GameVersion::GcnJpn, 0x803B4220}, + {GameVersion::WiiUsaRev0, 0x803F63C0}, + {GameVersion::WiiUsa, 0x803E1640}, + {GameVersion::WiiPal, 0x803E23A0}, + {GameVersion::WiiJpn, 0x803DF600} }, 0x89 ), diff --git a/src/d/actor/d_a_shop_item.cpp b/src/d/actor/d_a_shop_item.cpp index e8fafd1012..3e768a1684 100644 --- a/src/d/actor/d_a_shop_item.cpp +++ b/src/d/actor/d_a_shop_item.cpp @@ -11,6 +11,20 @@ #include "m_Do/m_Do_lib.h" #include +#if TARGET_PC +#include "d/d_item_data.h" + +const ResourceData& daShopItem_c::getResourceData() const { + if (mItemOverridden) { + return mOverrideData; + } + return mData[mShopItemID]; +} +#define SHOP_RESOURCE_DATA getResourceData() +#else +#define SHOP_RESOURCE_DATA mData[mShopItemID] +#endif + const char* daShopItem_c::getShopArcname() { switch (m_itemNo) { case dItemNo_NONE_e: @@ -88,7 +102,29 @@ const char* daShopItem_c::getShopArcname() { return NULL; } - return mData[mShopItemID].get_arcName(); +#if TARGET_PC + if (m_itemNo != dItemNo_NONE_e && mItemGiveOriginalNo == dItemNo_NONE_e) { + mItemGiveOriginalNo = m_itemNo; + const u8 resolvedItem = dusk::mods::item_check_shop(mItemGiveOriginalNo, this); + mItemOverridden = resolvedItem != mItemGiveOriginalNo; + if (mItemOverridden) { + mOverrideData = mData[mShopItemID]; + mOverrideData.mArcName = dItem_data::getArcName(resolvedItem); + mOverrideData.mBmdName = dItem_data::getBmdName(resolvedItem); + mOverrideData.mBtkName = dItem_data::getBtkName(resolvedItem); + mOverrideData.mBckName = dItem_data::getBckName(resolvedItem); + mOverrideData.mBrkName = dItem_data::getBrkName(resolvedItem); + mOverrideData.mBtpName = dItem_data::getBtpName(resolvedItem); + mOverrideData.mTevFrm = dItem_data::getTevFrm(resolvedItem); + mOverrideData.mBtpFrm = -1; + mOverrideData.mFlag = static_cast(-1); + mOverrideData.mOffsetY = mShopItemID == SHOP_ITEMNO_ARMOR ? 60.0f : 15.0f; + mOverrideData.mScale = 1.0f; + } + } +#endif + + return SHOP_RESOURCE_DATA.get_arcName(); } DUSK_GAME_DATA const f32 daShopItem_c::m_cullfar_max = 5000.0f; @@ -104,6 +140,11 @@ u16 daShopItem_c::getHeapSize() { OS_REPORT("ShopItemID [%u]\n", a_ShopItemID); ASSERT(a_ShopItemID < SHOP_ITEMNO_MAX); +#if TARGET_PC + if (mItemOverridden) { + return 0x8000; + } +#endif return HeapSizeTbl[a_ShopItemID]; } @@ -119,11 +160,11 @@ void daShopItem_c::CreateInit() { if (strcmp("R_SP109", dComIfGp_getStartStageName()) == 0 && dComIfGp_getStartStageRoomNo() == 1) { - scale.set(mData[mShopItemID].get_scale() * 0.8f, mData[mShopItemID].get_scale() * 0.8f, - mData[mShopItemID].get_scale() * 0.8f); + scale.set(SHOP_RESOURCE_DATA.get_scale() * 0.8f, SHOP_RESOURCE_DATA.get_scale() * 0.8f, + SHOP_RESOURCE_DATA.get_scale() * 0.8f); } else { - scale.set(mData[mShopItemID].get_scale(), mData[mShopItemID].get_scale(), - mData[mShopItemID].get_scale()); + scale.set(SHOP_RESOURCE_DATA.get_scale(), SHOP_RESOURCE_DATA.get_scale(), + SHOP_RESOURCE_DATA.get_scale()); } home.pos = current.pos; @@ -137,8 +178,8 @@ void daShopItem_c::set_mtx() { if (daShopItem_prm::getGroup(this) == 15) { mDoMtx_stack_c::transS(current.pos.x, current.pos.y, current.pos.z); } else { - mDoMtx_stack_c::transS(current.pos.x, current.pos.y + mData[mShopItemID].get_offsetY(), - current.pos.z); + mDoMtx_stack_c::transS( + current.pos.x, current.pos.y + SHOP_RESOURCE_DATA.get_offsetY(), current.pos.z); } MTXCopy(mDoMtx_stack_c::get(), mMtx); @@ -147,8 +188,8 @@ void daShopItem_c::set_mtx() { if (daShopItem_prm::getGroup(this) == 15) { mDoMtx_stack_c::ZXYrotM(-11300, 32700, 7300); } else { - mDoMtx_stack_c::ZXYrotM(mAngleX + mData[mShopItemID].get_angleX(), - mData[mShopItemID].get_angleY(), mData[mShopItemID].get_angleZ()); + mDoMtx_stack_c::ZXYrotM(mAngleX + SHOP_RESOURCE_DATA.get_angleX(), + SHOP_RESOURCE_DATA.get_angleY(), SHOP_RESOURCE_DATA.get_angleZ()); } mDoMtx_stack_c::ZXYrotM(current.angle.x, current.angle.y, current.angle.z); @@ -156,7 +197,7 @@ void daShopItem_c::set_mtx() { if (daShopItem_prm::getGroup(this) == 15) { mDoMtx_stack_c::XrotM(0); } else { - mDoMtx_stack_c::XrotM(mData[mShopItemID].get_angleOffsetX()); + mDoMtx_stack_c::XrotM(SHOP_RESOURCE_DATA.get_angleOffsetX()); } mpModel->setBaseTRMtx(mDoMtx_stack_c::get()); @@ -190,29 +231,31 @@ void daShopItem_c::setShadow() { } BOOL daShopItem_c::chkFlag(int i_flag) { - return mData[mShopItemID].get_flag() & i_flag; + return SHOP_RESOURCE_DATA.get_flag() & i_flag; } s8 daShopItem_c::getTevFrm() { - return mData[mShopItemID].get_tevfrm(); + return SHOP_RESOURCE_DATA.get_tevfrm(); } s8 daShopItem_c::getBtpFrm() { - return mData[mShopItemID].get_btpfrm(); + return SHOP_RESOURCE_DATA.get_btpfrm(); } u8 daShopItem_c::getShadowSize() { - return mData[mShopItemID].get_shadowSize(); + return SHOP_RESOURCE_DATA.get_shadowSize(); } u8 daShopItem_c::getCollisionH() { - return mData[mShopItemID].get_collisionH(); + return SHOP_RESOURCE_DATA.get_collisionH(); } u8 daShopItem_c::getCollisionR() { - return mData[mShopItemID].get_collisionR(); + return SHOP_RESOURCE_DATA.get_collisionR(); } +#undef SHOP_RESOURCE_DATA + int daShopItem_c::_create() { fopAcM_ct(this, daShopItem_c); diff --git a/src/d/actor/d_a_tag_statue_evt.cpp b/src/d/actor/d_a_tag_statue_evt.cpp index 8a4c0c4397..de5666aef9 100644 --- a/src/d/actor/d_a_tag_statue_evt.cpp +++ b/src/d/actor/d_a_tag_statue_evt.cpp @@ -362,9 +362,11 @@ int daTagStatue_c::demoProc() { item = dItemNo_AIR_LETTER_e; } - mItemId = - fopAcM_createItemForTrBoxDemo(¤t.pos, item, -1, - fopAcM_GetRoomNo(this), 0, 0); +#if TARGET_PC + item = dusk::mods::item_check_sky_character(item & 0xFF, this); +#endif + mItemId = fopAcM_createItemForTrBoxDemo(¤t.pos, item, -1, + fopAcM_GetRoomNo(this), 0, 0 IF_DUSK_ARG(dusk::mods::item_give_tag_sky_character())); JUT_ASSERT(580, mItemId != fpcM_ERROR_PROCESS_ID_e); diff --git a/src/d/actor/d_a_tbox.cpp b/src/d/actor/d_a_tbox.cpp index 1d1826b7e7..c77478da91 100644 --- a/src/d/actor/d_a_tbox.cpp +++ b/src/d/actor/d_a_tbox.cpp @@ -1390,6 +1390,10 @@ u8 daTbox_c::getBombItemNoMain(u8 i_itemNo) { int daTbox_c::setGetDemoItem() { u8 item_no = getItemNo(); +#if TARGET_PC + const u32 giveTag = dusk::mods::item_give_tag_chest(getTboxNo()); + item_no = dusk::mods::item_check_tagged(giveTag, mOriginalItemNo, this); +#endif if (item_no == dItemNo_BOMB_5_e || item_no == dItemNo_BOMB_10_e || item_no == dItemNo_BOMB_20_e || item_no == dItemNo_BOMB_30_e || item_no == dItemNo_WATER_BOMB_5_e || item_no == dItemNo_WATER_BOMB_10_e || item_no == dItemNo_WATER_BOMB_20_e || item_no == dItemNo_WATER_BOMB_30_e || item_no == dItemNo_BOMB_INSECT_5_e || item_no == dItemNo_BOMB_INSECT_10_e || item_no == dItemNo_BOMB_INSECT_20_e || item_no == dItemNo_BOMB_INSECT_30_e) @@ -1399,9 +1403,11 @@ int daTbox_c::setGetDemoItem() { fpc_ProcID item_id; if (field_0x718) { - item_id = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 1, -1, -1, NULL, NULL); + item_id = fopAcM_createItemForPresentDemo( + ¤t.pos, item_no, 1, -1, -1, NULL, NULL IF_DUSK_ARG(giveTag)); } else { - item_id = fopAcM_createItemForTrBoxDemo(¤t.pos, item_no, -1, -1, NULL, NULL); + item_id = fopAcM_createItemForTrBoxDemo( + ¤t.pos, item_no, -1, -1, NULL, NULL IF_DUSK_ARG(giveTag)); } if (item_id != fpcM_ERROR_PROCESS_ID_e) { @@ -1785,6 +1791,11 @@ void daTbox_c::mode_exec() { cPhs_Step daTbox_c::create1st() { if (!mParamsInit) { field_0x980 = home.angle.x; +#if TARGET_PC + mOriginalItemNo = (home.angle.z >> 8) & 0xFF; + const u8 resolvedItem = dusk::mods::item_check_chest(getTboxNo(), mOriginalItemNo, this); + home.angle.z = static_cast((home.angle.z & ~0xFF00) | (resolvedItem << 8)); +#endif field_0x982 = home.angle.z; home.angle.z = 0; home.angle.x = 0; diff --git a/src/d/actor/d_a_tbox2.cpp b/src/d/actor/d_a_tbox2.cpp index 2f2a3a8276..54c3e3135e 100644 --- a/src/d/actor/d_a_tbox2.cpp +++ b/src/d/actor/d_a_tbox2.cpp @@ -138,6 +138,17 @@ int daTbox2_c::create1st() { fopAcM_ct(this, daTbox2_c); mModelType = getModelType(); +#if TARGET_PC + if (!mParamsInit) { + mOriginalItemNo = getItemNo(); + int tboxNo = fopAcM_GetParamBit(this, 16, 8); + const u8 resolvedItem = dusk::mods::item_check_chest(tboxNo, mOriginalItemNo, this); + u32 params = (fopAcM_GetParam(this) & 0xFFFFFF00) | resolvedItem; + fopAcM_SetParam(this, params); + mParamsInit = true; + } +#endif + int phase_state = dComIfG_resLoad(&mPhase, l_arcName); if (phase_state == cPhs_COMPLEATE_e) { u32 heap_size; @@ -370,12 +381,17 @@ void daTbox2_c::actionOpenWait() { int daTbox2_c::setGetDemoItem() { u8 item_no = getItemNo(); +#if TARGET_PC + int tboxNo = fopAcM_GetParamBit(this, 16, 8); + const u32 giveTag = dusk::mods::item_give_tag_chest(tboxNo); + item_no = dusk::mods::item_check_tagged(giveTag, mOriginalItemNo, this); +#endif u32 partner_id; if (mReturnRupee) { - partner_id = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 1, -1, -1, NULL, NULL); + partner_id = fopAcM_createItemForPresentDemo(¤t.pos, item_no, 1, -1, -1, NULL, NULL IF_DUSK_ARG(giveTag)); } else { - partner_id = fopAcM_createItemForTrBoxDemo(¤t.pos, item_no, -1, -1, NULL, NULL); + partner_id = fopAcM_createItemForTrBoxDemo(¤t.pos, item_no, -1, -1, NULL, NULL IF_DUSK_ARG(giveTag)); } if (partner_id != -1) { diff --git a/src/d/actor/d_a_title.cpp b/src/d/actor/d_a_title.cpp index 5ff7dad881..b9edd72fa1 100644 --- a/src/d/actor/d_a_title.cpp +++ b/src/d/actor/d_a_title.cpp @@ -52,7 +52,7 @@ static u8 const lit_3772[12] = { #if TARGET_PC using namespace dusk::version; -#define l_arcName versionSelect({{GameVersion::GcnPal, "TitlePal"}}, "Title") +#define l_arcName regionSelect("Title", "TitlePal", "Title") #elif VERSION == VERSION_GCN_PAL static char const l_arcName[] = "TitlePal"; #else diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index f8fe0bed10..9e3d77efd0 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -11,9 +11,16 @@ const u16 l_J_Ohana00_64TEX__width = 63; const u16 l_J_Ohana00_64TEX__height = 63; #if TARGET_PC -#include "dusk/dvd_asset.hpp" -using GameVersion = dusk::version::GameVersion; -static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9060}, {GameVersion::GcnPal, 0x9060}, {GameVersion::GcnJpn, 0x9060}}, 0x800), true); return buf; } +DEFINE_GRASS_ASSET(l_J_Ohana00_64TEX, u8, 0x800, { + {GameVersion::GcnUsa, 0x9060}, + {GameVersion::GcnPal, 0x9060}, + {GameVersion::GcnJpn, 0x9060}, + {GameVersion::WiiUsaRev0, 0x7B60}, + {GameVersion::WiiUsa, 0x7BC0}, + {GameVersion::WiiPal, 0x7BC0}, + {GameVersion::WiiJpn, 0x7BC0}, +}); + #define l_J_Ohana00_64TEX (l_J_Ohana00_64TEX_get()) // from d_grass.inc @@ -115,12 +122,46 @@ static u8 l_flowerTexCoord[] = { 0x3E, 0xA7, 0x72, 0xD6, 0xBD, 0x2F, 0x46, 0xAA}; #if TARGET_PC -using GameVersion = dusk::version::GameVersion; +DEFINE_GRASS_ASSET(l_J_hana00DL, u8, 0x150, { + {GameVersion::GcnUsa, 0x9D20}, + {GameVersion::GcnPal, 0x9D20}, + {GameVersion::GcnJpn, 0x9D20}, + {GameVersion::WiiUsaRev0, 0x8820}, + {GameVersion::WiiUsa, 0x8880}, + {GameVersion::WiiPal, 0x8880}, + {GameVersion::WiiJpn, 0x8880}, +}); + +DEFINE_GRASS_ASSET(l_J_hana00_cDL, u8, 0xDE, { + {GameVersion::GcnUsa, 0x9E80}, + {GameVersion::GcnPal, 0x9E80}, + {GameVersion::GcnJpn, 0x9E80}, + {GameVersion::WiiUsaRev0, 0x8980}, + {GameVersion::WiiUsa, 0x89E0}, + {GameVersion::WiiPal, 0x89E0}, + {GameVersion::WiiJpn, 0x89E0}, +}); + +DEFINE_GRASS_ASSET(l_matDL, u8, 0x99, { + {GameVersion::GcnUsa, 0x9F60}, + {GameVersion::GcnPal, 0x9F60}, + {GameVersion::GcnJpn, 0x9F60}, + {GameVersion::WiiUsaRev0, 0x8A60}, + {GameVersion::WiiUsa, 0x8AC0}, + {GameVersion::WiiPal, 0x8AC0}, + {GameVersion::WiiJpn, 0x8AC0}, +}); + +DEFINE_GRASS_ASSET(l_matLight4DL, u8, 0x99, { + {GameVersion::GcnUsa, 0xA000}, + {GameVersion::GcnPal, 0xA000}, + {GameVersion::GcnJpn, 0xA000}, + {GameVersion::WiiUsaRev0, 0x8B00}, + {GameVersion::WiiUsa, 0x8B60}, + {GameVersion::WiiPal, 0x8B60}, + {GameVersion::WiiJpn, 0x8B60}, +}); -static u8* l_J_hana00DL_get() { static u8 buf[0x150]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9D20}, {GameVersion::GcnPal, 0x9D20}, {GameVersion::GcnJpn, 0x9D20}}, 0x150), true); return buf; } -static u8* l_J_hana00_cDL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9E80}, {GameVersion::GcnPal, 0x9E80}, {GameVersion::GcnJpn, 0x9E80}}, 0xDE), true); return buf; } -static u8* l_matDL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9F60}, {GameVersion::GcnPal, 0x9F60}, {GameVersion::GcnJpn, 0x9F60}}, 0x99), true); return buf; } -static u8* l_matLight4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xA000}, {GameVersion::GcnPal, 0xA000}, {GameVersion::GcnJpn, 0xA000}}, 0x99), true); return buf; } #define l_J_hana00DL (l_J_hana00DL_get()) #define l_J_hana00_cDL (l_J_hana00_cDL_get()) #define l_matDL (l_matDL_get()) @@ -141,8 +182,16 @@ const u16 l_J_Ohana01_64128_0419TEX__width = 63; const u16 l_J_Ohana01_64128_0419TEX__height = 127; #if TARGET_PC -using GameVersion = dusk::version::GameVersion; -static u8* l_J_Ohana01_64128_0419TEX_get() { static u8 buf[0x1000]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xA0A0}, {GameVersion::GcnPal, 0xA0A0}, {GameVersion::GcnJpn, 0xA0A0}}, 0x1000), true); return buf; } +DEFINE_GRASS_ASSET(l_J_Ohana01_64128_0419TEX, u8, 0x1000, { + {GameVersion::GcnUsa, 0xA0A0}, + {GameVersion::GcnPal, 0xA0A0}, + {GameVersion::GcnJpn, 0xA0A0}, + {GameVersion::WiiUsaRev0, 0x8BA0}, + {GameVersion::WiiUsa, 0x8C00}, + {GameVersion::WiiPal, 0x8C00}, + {GameVersion::WiiJpn, 0x8C00}, +}); + #define l_J_Ohana01_64128_0419TEX (l_J_Ohana01_64128_0419TEX_get()) #else #include "assets/l_J_Ohana01_64128_0419TEX.h" @@ -274,13 +323,56 @@ static u8 l_flowerTexCoord2[] = { 0x40, 0x1B, 0x7D, 0x52, 0x3F, 0x80, 0x3F, 0x79, 0x40, 0x1B, 0x7D, 0x52, 0x3F, 0x51, 0x10, 0x6F}; #if TARGET_PC -using GameVersion = dusk::version::GameVersion; +DEFINE_GRASS_ASSET(l_J_hana01DL, u8, 0x138, { + {GameVersion::GcnUsa, 0xB7C0}, + {GameVersion::GcnPal, 0xB7C0}, + {GameVersion::GcnJpn, 0xB7C0}, + {GameVersion::WiiUsaRev0, 0xA2C0}, + {GameVersion::WiiUsa, 0xA320}, + {GameVersion::WiiPal, 0xA320}, + {GameVersion::WiiJpn, 0xA320}, +}); + +DEFINE_GRASS_ASSET(l_J_hana01_c_00DL, u8, 0xDE, { + {GameVersion::GcnUsa, 0xB900}, + {GameVersion::GcnPal, 0xB900}, + {GameVersion::GcnJpn, 0xB900}, + {GameVersion::WiiUsaRev0, 0xA400}, + {GameVersion::WiiUsa, 0xA460}, + {GameVersion::WiiPal, 0xA460}, + {GameVersion::WiiJpn, 0xA460}, +}); + +DEFINE_GRASS_ASSET(l_J_hana01_c_01DL, u8, 0x128, { + {GameVersion::GcnUsa, 0xB9E0}, + {GameVersion::GcnPal, 0xB9E0}, + {GameVersion::GcnJpn, 0xB9E0}, + {GameVersion::WiiUsaRev0, 0xA4E0}, + {GameVersion::WiiUsa, 0xA540}, + {GameVersion::WiiPal, 0xA540}, + {GameVersion::WiiJpn, 0xA540}, +}); + +DEFINE_GRASS_ASSET(l_mat2DL, u8, 0x99, { + {GameVersion::GcnUsa, 0xBB20}, + {GameVersion::GcnPal, 0xBB20}, + {GameVersion::GcnJpn, 0xBB20}, + {GameVersion::WiiUsaRev0, 0xA620}, + {GameVersion::WiiUsa, 0xA680}, + {GameVersion::WiiPal, 0xA680}, + {GameVersion::WiiJpn, 0xA680}, +}); + +DEFINE_GRASS_ASSET(l_mat2Light4DL, u8, 0x99, { + {GameVersion::GcnUsa, 0xBBC0}, + {GameVersion::GcnPal, 0xBBC0}, + {GameVersion::GcnJpn, 0xBBC0}, + {GameVersion::WiiUsaRev0, 0xA6C0}, + {GameVersion::WiiUsa, 0xA720}, + {GameVersion::WiiPal, 0xA720}, + {GameVersion::WiiJpn, 0xA720}, +}); -static u8* l_J_hana01DL_get() { static u8 buf[0x138]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB7C0}, {GameVersion::GcnPal, 0xB7C0}, {GameVersion::GcnJpn, 0xB7C0}}, 0x138), true); return buf; } -static u8* l_J_hana01_c_00DL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB900}, {GameVersion::GcnPal, 0xB900}, {GameVersion::GcnJpn, 0xB900}}, 0xDE), true); return buf; } -static u8* l_J_hana01_c_01DL_get() { static u8 buf[0x128]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB9E0}, {GameVersion::GcnPal, 0xB9E0}, {GameVersion::GcnJpn, 0xB9E0}}, 0x128), true); return buf; } -static u8* l_mat2DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xBB20}, {GameVersion::GcnPal, 0xBB20}, {GameVersion::GcnJpn, 0xBB20}}, 0x99), true); return buf; } -static u8* l_mat2Light4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xBBC0}, {GameVersion::GcnPal, 0xBBC0}, {GameVersion::GcnJpn, 0xBBC0}}, 0x99), true); return buf; } #define l_J_hana01DL (l_J_hana01DL_get()) #define l_J_hana01_c_00DL (l_J_hana01_c_00DL_get()) #define l_J_hana01_c_01DL (l_J_hana01_c_01DL_get()) diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 2e20394620..6c7a898c56 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -21,8 +21,39 @@ const u16 l_M_kusa05_RGBATEX__height = 31; #if TARGET_PC #include "dusk/dvd_asset.hpp" using GameVersion = dusk::version::GameVersion; -static u8* l_M_kusa05_RGBATEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x7680}, {GameVersion::GcnPal, 0x7680}, {GameVersion::GcnJpn, 0x7680}}, 0x800), true); return buf; } -static u8* l_M_Hijiki00TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x7E80}, {GameVersion::GcnPal, 0x7E80}, {GameVersion::GcnJpn, 0x7E80}}, 0x800), true); return buf; } + +template +static bool LoadGrassAsset(T (&dst)[N], std::initializer_list offset) { + return dusk::LoadArchivedRelAsset(dst, 'AMEM', "d_a_grass.rel", offset); +} + +#define DEFINE_GRASS_ASSET(name, type, count, ...) \ + static type* name##_get() { \ + static type buf[count]; \ + static bool _ = (LoadGrassAsset(buf, __VA_ARGS__), true); \ + return buf; \ + } + +DEFINE_GRASS_ASSET(l_M_kusa05_RGBATEX, u8, 0x800, { + {GameVersion::GcnUsa, 0x7680}, + {GameVersion::GcnPal, 0x7680}, + {GameVersion::GcnJpn, 0x7680}, + {GameVersion::WiiUsaRev0, 0x6180}, + {GameVersion::WiiUsa, 0x61E0}, + {GameVersion::WiiPal, 0x61E0}, + {GameVersion::WiiJpn, 0x61E0}, +}); + +DEFINE_GRASS_ASSET(l_M_Hijiki00TEX, u8, 0x800, { + {GameVersion::GcnUsa, 0x7E80}, + {GameVersion::GcnPal, 0x7E80}, + {GameVersion::GcnJpn, 0x7E80}, + {GameVersion::WiiUsaRev0, 0x6980}, + {GameVersion::WiiUsa, 0x69E0}, + {GameVersion::WiiPal, 0x69E0}, + {GameVersion::WiiJpn, 0x69E0}, +}); + #define l_M_kusa05_RGBATEX (l_M_kusa05_RGBATEX_get()) #define l_M_Hijiki00TEX (l_M_Hijiki00TEX_get()) #else @@ -114,14 +145,66 @@ static u8 l_texCoord[160] = { }; #if TARGET_PC -using GameVersion = dusk::version::GameVersion; +DEFINE_GRASS_ASSET(l_M_Kusa_9qDL, u8, 0xCB, { + {GameVersion::GcnUsa, 0x8B00}, + {GameVersion::GcnPal, 0x8B00}, + {GameVersion::GcnJpn, 0x8B00}, + {GameVersion::WiiUsaRev0, 0x7600}, + {GameVersion::WiiUsa, 0x7660}, + {GameVersion::WiiPal, 0x7660}, + {GameVersion::WiiJpn, 0x7660}, +}); + +DEFINE_GRASS_ASSET(l_M_Kusa_9q_cDL, u8, 0xCB, { + {GameVersion::GcnUsa, 0x8BE0}, + {GameVersion::GcnPal, 0x8BE0}, + {GameVersion::GcnJpn, 0x8BE0}, + {GameVersion::WiiUsaRev0, 0x76E0}, + {GameVersion::WiiUsa, 0x7740}, + {GameVersion::WiiPal, 0x7740}, + {GameVersion::WiiJpn, 0x7740}, +}); + +DEFINE_GRASS_ASSET(l_M_TenGusaDL, u8, 0xD4, { + {GameVersion::GcnUsa, 0x8CC0}, + {GameVersion::GcnPal, 0x8CC0}, + {GameVersion::GcnJpn, 0x8CC0}, + {GameVersion::WiiUsaRev0, 0x77C0}, + {GameVersion::WiiUsa, 0x7820}, + {GameVersion::WiiPal, 0x7820}, + {GameVersion::WiiJpn, 0x7820}, +}); + +DEFINE_GRASS_ASSET(l_Tengusa_matDL, u8, 0xA8, { + {GameVersion::GcnUsa, 0x8DA0}, + {GameVersion::GcnPal, 0x8DA0}, + {GameVersion::GcnJpn, 0x8DA0}, + {GameVersion::WiiUsaRev0, 0x78A0}, + {GameVersion::WiiUsa, 0x7900}, + {GameVersion::WiiPal, 0x7900}, + {GameVersion::WiiJpn, 0x7900}, +}); + +DEFINE_GRASS_ASSET(l_kusa9q_matDL, u8, 0xA8, { + {GameVersion::GcnUsa, 0x8E60}, + {GameVersion::GcnPal, 0x8E60}, + {GameVersion::GcnJpn, 0x8E60}, + {GameVersion::WiiUsaRev0, 0x7960}, + {GameVersion::WiiUsa, 0x79C0}, + {GameVersion::WiiPal, 0x79C0}, + {GameVersion::WiiJpn, 0x79C0}, +}); + +DEFINE_GRASS_ASSET(l_kusa9q_l4_matDL, u8, 0xA8, { + {GameVersion::GcnUsa, 0x8F20}, + {GameVersion::GcnPal, 0x8F20}, + {GameVersion::GcnJpn, 0x8F20}, + {GameVersion::WiiUsaRev0, 0x7A20}, + {GameVersion::WiiUsa, 0x7A80}, + {GameVersion::WiiPal, 0x7A80}, + {GameVersion::WiiJpn, 0x7A80}, +}); -static u8* l_M_Kusa_9qDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8B00}, {GameVersion::GcnPal, 0x8B00}, {GameVersion::GcnJpn, 0x8B00}}, 0xCB), true); return buf; } -static u8* l_M_Kusa_9q_cDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8BE0}, {GameVersion::GcnPal, 0x8BE0}, {GameVersion::GcnJpn, 0x8BE0}}, 0xCB), true); return buf; } -static u8* l_M_TenGusaDL_get() { static u8 buf[0xD4]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8CC0}, {GameVersion::GcnPal, 0x8CC0}, {GameVersion::GcnJpn, 0x8CC0}}, 0xD4), true); return buf; } -static u8* l_Tengusa_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8DA0}, {GameVersion::GcnPal, 0x8DA0}, {GameVersion::GcnJpn, 0x8DA0}}, 0xA8), true); return buf; } -static u8* l_kusa9q_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8E60}, {GameVersion::GcnPal, 0x8E60}, {GameVersion::GcnJpn, 0x8E60}}, 0xA8), true); return buf; } -static u8* l_kusa9q_l4_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8F20}, {GameVersion::GcnPal, 0x8F20}, {GameVersion::GcnJpn, 0x8F20}}, 0xA8), true); return buf; } #define l_M_Kusa_9qDL (l_M_Kusa_9qDL_get()) #define l_M_Kusa_9q_cDL (l_M_Kusa_9q_cDL_get()) #define l_M_TenGusaDL (l_M_TenGusaDL_get()) diff --git a/src/d/d_a_shop_item_static.cpp b/src/d/d_a_shop_item_static.cpp index ece0a288c5..635f07f5ec 100644 --- a/src/d/d_a_shop_item_static.cpp +++ b/src/d/d_a_shop_item_static.cpp @@ -45,6 +45,12 @@ int CheckShopItemCreateHeap(fopAc_ac_c* i_this) { daShopItem_c* a_this1 = static_cast(i_this); daShopItem_c* a_this2 = static_cast(i_this); +#if TARGET_PC + const ResourceData& data = a_this2->getResourceData(); + return a_this1->CreateItemHeap(data.get_arcName(), data.get_bmdName(), data.get_btk1Name(), + data.get_bpk1Name(), data.get_bck1Name(), data.get_bxa1Name(), data.get_brk1Name(), + data.get_btp1Name()); +#else u8 a_ShopItemID = a_this2->getShopItemID(); return a_this1->CreateItemHeap(daShopItem_c::mData[a_ShopItemID].get_arcName(), daShopItem_c::mData[a_ShopItemID].get_bmdName(), @@ -54,4 +60,5 @@ int CheckShopItemCreateHeap(fopAc_ac_c* i_this) { daShopItem_c::mData[a_ShopItemID].get_bxa1Name(), daShopItem_c::mData[a_ShopItemID].get_brk1Name(), daShopItem_c::mData[a_ShopItemID].get_btp1Name()); +#endif } diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index cbea55100a..7ec4f09b10 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -9,11 +9,15 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_msg_string.h" -#include "dusk/livesplit.h" -#include "dusk/imgui/ImGuiConsole.hpp" -#include "dusk/speedrun.h" #include "m_Do/m_Do_controller_pad.h" + +#ifdef TARGET_PC #include +#include "dusk/game_mode.hpp" +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/livesplit.h" +#include "dusk/speedrun.h" +#endif #include "dusk/version.hpp" @@ -83,7 +87,9 @@ void dBrightCheck_c::screenSet() { JUT_ASSERT(0, mBrightCheck.Scr != NULL); mBrightCheck.Scr->setPriority("zelda_option_check.blo", 0x1100000, mArchive); + IF_DUSK_BLOCK(dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) mBrightCheck.Scr->search(MULTI_CHAR('g_abtn_n'))->hide(); + IF_DUSK_BLOCK_END #if TARGET_PC J2DTextBox* settings_text; @@ -113,9 +119,11 @@ void dBrightCheck_c::screenSet() { J2DTextBox* btna_text[5]; for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { btna_text[i] = (J2DTextBox*)mBrightCheck.Scr->search(tv_btnA[i]); - mBrightCheck.Scr->search(ftv_btnA[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mBrightCheck.Scr->search(ftv_btnA[i])->hide(); + } } else { btna_text[i] = (J2DTextBox*)mBrightCheck.Scr->search(ftv_btnA[i]); mBrightCheck.Scr->search(tv_btnA[i])->hide(); @@ -182,15 +190,6 @@ void dBrightCheck_c::modeMove() { if (mDoCPd_c::getTrigA(PAD_1) || mDoCPd_c::getTrigStart(PAD_1)) { mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); #ifdef TARGET_PC - if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) { - // start a new run if a run isn't already in progress - if (!dusk::m_speedrunInfo.m_isRunStarted) { - dusk::resetForSpeedrunMode(); - dusk::m_speedrunInfo.startRun(); - dusk::speedrun::start(); - } - } - toggleAutoSave(true); #endif mCompleteCheck = true; @@ -222,7 +221,9 @@ void dBrightCheck_c::brightCheckWide() { // Confirm A Button mBrightCheck.Scr->search(MULTI_CHAR('abtn_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); - mBrightCheck.Scr->search(MULTI_CHAR('gcabtn_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mBrightCheck.Scr->search(MULTI_CHAR('gcabtn_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + } // Text mBrightCheck.Scr->search(MULTI_CHAR('menu_6n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); diff --git a/src/d/d_event_manager.cpp b/src/d/d_event_manager.cpp index 154a1c9288..da50ec7d96 100644 --- a/src/d/d_event_manager.cpp +++ b/src/d/d_event_manager.cpp @@ -335,6 +335,14 @@ bool dEvent_manager_c::setObjectArchive(DUSK_CONST char* arcname) { if (arcname != NULL) { rt = dComIfG_getObjectRes(arcname, DataFileName); +#if TARGET_PC + if (rt != nullptr && strcmp(arcname, "Prayer") == 0) { + // pointer to Prayer event `011get_item` prm0 in it's event_list.dat + u8* itemNo = static_cast(rt) + 0x927; + *itemNo = + dusk::mods::item_check("prayer_reward", dItemNo_KAKERA_HEART_e, NULL); + } +#endif int base_status = mEventList[BASE_ACTOR].init((char*)rt, -1); #if DEBUG diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 936a368f81..e075e2b8a6 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -26,9 +26,10 @@ #include "dusk/version.hpp" #if TARGET_PC +#include "dusk/game_mode.hpp" #include "dusk/menu_pointer.h" -#include "helpers/string.hpp" #include "dusk/mods/svc/save.hpp" +#include "helpers/string.hpp" namespace { constexpr u8 pointer_target(u8 group, u8 index) noexcept { @@ -1310,12 +1311,42 @@ void dFile_select_c::selectDataOpenMove() { void dFile_select_c::selectDataNameMove() { bool isHeaderTxtChange = headerTxtChangeAnm(); bool isFileRecScale = fileRecScaleAnm2(); - bool isNameMove = nameMoveAnm(); + IF_NOT_DUSK(bool isNameMove = nameMoveAnm();) bool isModoruTxtDisp = modoruTxtDispAnm(); +#ifdef TARGET_PC + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + if (isHeaderTxtChange == true && isFileRecScale == true && isModoruTxtDisp == true) { + if (mGameModeSaveStartBuildUi) { + gameMode->invokeOnNewSaveSelectFunction(&mGameModeNewSaveState); + mGameModeSaveStartBuildUi = false; + } + if (mGameModeNewSaveState == GAME_MODE_STATE_RETURN) { + backToDataSelectMove(); + mGameModeSaveStartBuildUi = true; + mGameModeNewSaveState = GAME_MODE_STATE_PENDING; + return; + } + if (mGameModeNewSaveState != GAME_MODE_STATE_PROCEED) { + return; + } + } else { + return; + } + } +#endif + + IF_DUSK(bool isNameMove = nameMoveAnm();) + if (isHeaderTxtChange == true && isFileRecScale == true && isNameMove == true && isModoruTxtDisp == true) { +#ifdef TARGET_PC + mGameModeSaveStartBuildUi = true; + mGameModeNewSaveState = GAME_MODE_STATE_PENDING; +#endif mDataSelProc = DATASELPROC_NAME_INPUT_WAIT; } } @@ -1397,6 +1428,12 @@ void dFile_select_c::menuSelectStart() { dComIfGs_setDataNum(mSelectNum); #if TARGET_PC dusk::mods::svc::save_slot_loaded(mSelectNum, &mSaveData[mSelectNum]); + + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + gameMode->invokeOnSaveLoadedFunction(); + } #endif } else if (mSelectMenuNum == 0) { mSelIcon->setAlphaRate(0.0f); @@ -1750,6 +1787,12 @@ void dFile_select_c::nameInput2() { mIsSelectEnd = true; #if TARGET_PC dusk::mods::svc::save_slot_new(mSelectNum); + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + gameMode->invokeOnNewSaveFunction(); + gameMode->invokeOnSaveLoadedFunction(); + } #endif mDataSelProc = DATASELPROC_NEXT_MODE_WAIT; } diff --git a/src/d/d_item.cpp b/src/d/d_item.cpp index 8ca950a52a..5484bd584a 100644 --- a/src/d/d_item.cpp +++ b/src/d/d_item.cpp @@ -274,8 +274,9 @@ inline void getItemFunc(u8 i_itemNo) { item_func_ptr[i_itemNo](); } -void execItemGet(u8 i_itemNo) { +void execItemGet(u8 i_itemNo IF_DUSK_ARG(u32 i_itemGiveTag) IF_DUSK_ARG(fopAc_ac_c* i_giver)) { getItemFunc(i_itemNo); + IF_DUSK(dusk::mods::item_granted(i_itemNo, i_itemGiveTag, i_giver);) } static int (*item_getcheck_func_ptr[256])() = { diff --git a/src/d/d_kankyo.cpp b/src/d/d_kankyo.cpp index e6f155a989..f9d77837f9 100644 --- a/src/d/d_kankyo.cpp +++ b/src/d/d_kankyo.cpp @@ -2346,7 +2346,7 @@ void dScnKy_env_light_c::setLight() { u8 next_pal_end_id; #if TARGET_PC const f32 deltaTime = dusk::game_clock::consume_interval(this); - timeScale = deltaTime / dusk::game_clock::period_for_original_frames(1.0f); + timeScale = deltaTime / dusk::game_clock::kSimPeriod; #endif setLight_palno_get(&g_env_light.PrevCol, &g_env_light.UseCol, &g_env_light.wether_pat0, &g_env_light.wether_pat1, &prev_pal_start_id, &prev_pal_end_id, diff --git a/src/d/d_kyeff.cpp b/src/d/d_kyeff.cpp index 984478f74d..1ba4ffa96f 100644 --- a/src/d/d_kyeff.cpp +++ b/src/d/d_kyeff.cpp @@ -118,7 +118,7 @@ static int dKyeff_Create(kankyo_class* i_this) { if (strcmp(dComIfGp_getStartStageName(), "Name") == 0) { camera_process_class* camera = dComIfGp_getCamera(0); - OSTime time = OSGetTime(); + OSTime time = DUSK_IF_ELSE(OSGetSystemTime(), OSGetTime()); OSTicksToCalendarTime(time, &calendar); g_env_light.global_wind_influence.vec.x = 1.0f; diff --git a/src/d/d_lib.cpp b/src/d/d_lib.cpp index d83d4f62c0..902e6d4b24 100644 --- a/src/d/d_lib.cpp +++ b/src/d/d_lib.cpp @@ -311,6 +311,11 @@ u32 dLib_getExpandSizeFromAramArchive(JKRAramArchive* i_aramArchive, char const* JUT_ASSERT(1260, readAddress == header); JKRArchive::SDIFileEntry* entry = i_aramArchive->findFsResource(param_2, 0); JUT_ASSERT(1263, entry != NULL); +#if TARGET_PC + if (u32 size; i_aramArchive->getOverlayFileSize(entry, &size)) { + return ALIGN_NEXT(size, 32); + } +#endif u32 uVar1 = ALIGN_NEXT(JKRDecompExpandSize(header), 32); u32 uVar5 = ALIGN_NEXT(entry->data_size, 32); return uVar1 > uVar5 ? uVar1 : uVar5; diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index 08591007ec..41d5108e32 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -16,23 +16,14 @@ #ifdef TARGET_PC #include "dusk/settings.h" +#include "dusk/hq_minimap.hpp" #include "m_Do/m_Do_graphic.h" #include #include #include -#include #include -#include #include -#include -#include - -constexpr u16 kMapIconResolutionMultiplier = 4; -constexpr u16 kMapImageSide = 16 * kMapIconResolutionMultiplier; -constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide; - -typedef std::function PaintI8Fn; u16 scaled_map_axis(u16 value, f32 scale) { const auto scaledValue = @@ -59,27 +50,6 @@ aurora::Vec2 map_render_size_for(u16 width, u16 height) { scaled_map_axis(height, irScaleY * hudScale), }; } - -void paint_i8(std::span dst, size_t width, PaintI8Fn paint) { - const auto blocksAcross = width >> 3; - - for (size_t i = 0; i < dst.size(); i++) { - // 8x4 block swizzling for I8 - const auto blockIdx = i >> 5; - const auto localIdx = i & 31; - - const auto blockY = blockIdx / blocksAcross; - const auto blockX = blockIdx % blocksAcross; - - const auto localY = localIdx >> 3; - const auto localX = localIdx & 7; - - const auto x = (blockX << 3) + localX; - const auto y = (blockY << 2) + localY; - - dst[i] = paint(x, y); - } -} #endif void dMpath_n::dTexObjAggregate_c::create() { @@ -100,106 +70,11 @@ void dMpath_n::dTexObjAggregate_c::create() { JUT_ASSERT(72, image != NULL); JUT_ASSERT(73, image->minFilter == GX_NEAR); JUT_ASSERT(74, image->magFilter == GX_NEAR); + IF_DUSK(dusk::hq_minimap::register_pointer(data[lp1], reinterpret_cast(image) + image->imageOffset)); mDoLib_setResTimgObj(image, mp_texObj[lp1], 0, NULL); } -#if TARGET_PC - static bool hqTexsDrawn = false; - - static u8 hqCircleData[kMapImageTotalPixels]; - static u8 hqCircleAltData[kMapImageTotalPixels]; - static u8 hqNijumaruData[kMapImageTotalPixels]; - static u8 hqEnterData[kMapImageTotalPixels]; - static u8 hqTryForceData[kMapImageTotalPixels]; - - if (!hqTexsDrawn) { - constexpr auto center = kMapImageSide / 2.0f; - constexpr auto radiusSq = center * center; - - // 6: map_icon_circle16x16_4i.bti - simple circle - paint_i8(std::span{hqCircleData}, kMapImageSide, [=](auto x, auto y) { - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - return (dx * dx + dy * dy < radiusSq) ? 0x11 : 0; - }); - - // 4: im_map_icon_circle_4i.bti - outlined circle - paint_i8(std::span{hqCircleAltData}, kMapImageSide, [=](auto x, auto y) { - constexpr auto innerRadius = kMapImageSide * 3.0f / 8.0f; - constexpr auto innerRadiusSq = innerRadius * innerRadius; - - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - const auto dSq = dx * dx + dy * dy; - - return dSq < radiusSq ? (dSq < innerRadiusSq ? 0x22 : 0x11) : 0; - }); - - // 3: im_map_icon_nijumaru_4i.bti - concentric rings - paint_i8(std::span{hqNijumaruData}, kMapImageSide, [=](auto x, auto y) { - constexpr u8 nijumaruRings[] = {0x11, 0x22, 0x11, 0x11, 0x22, 0x22}; - - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - const auto dSq = dx * dx + dy * dy; - - if (dSq < radiusSq) { - const auto ringIndex = - static_cast(std::trunc(std::sqrt(dSq) / kMapImageSide * 12)); - return nijumaruRings[ringIndex]; - } - return u8{0}; - }); - - // 2: im_map_icon_enter_4i.bti - outlined octagram - paint_i8(std::span{hqEnterData}, kMapImageSide, [=](auto x, auto y) { - constexpr auto outlineWidth = kMapImageSide / 6.0f; - - const auto adx = std::abs((x + 0.5f) - center); - const auto ady = std::abs((y + 0.5f) - center); - const auto dist = - std::min(adx + ady, std::max(adx, ady) * std::numbers::sqrt2_v) - - kMapImageSide / 2.0f; - - return dist > 0.0f ? 0 : (dist > -outlineWidth ? 0x22 : 0x33); - }); - - // 5: im_map_icon_try_force_4i.bti - outlined circle with triangle - paint_i8(std::span{hqTryForceData}, kMapImageSide, [=](auto x, auto y) { - constexpr auto innerRadiusNorm = 5.0f / 12.0f; - constexpr auto innerRadius = kMapImageSide * innerRadiusNorm; - constexpr auto innerRadiusSq = innerRadius * innerRadius; - constexpr auto triRadius = kMapImageSide * innerRadiusNorm / 2.0f; - - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - const auto dSq = dx * dx + dy * dy; - const auto triSideDist = (std::numbers::sqrt3_v * std::abs(dx) - dy) * 0.5f; - const auto insideTri = std::max(dy, triSideDist) < triRadius; - - return insideTri ? 0x22 : (dSq < radiusSq ? (dSq < innerRadiusSq ? 0x33 : 0x22) : 0); - }); - - hqTexsDrawn = true; - } - - constexpr auto replacements = std::to_array >({ - {2, hqEnterData}, - {3, hqNijumaruData}, - {4, hqCircleAltData}, - {5, hqTryForceData}, - {6, hqCircleData}, - }); - - for (const auto& [idx, data] : replacements) { - JKR_DELETE(mp_texObj[idx]); - const auto texobj = JKR_NEW TGXTexObj(); - GXInitTexObj( - texobj, data, kMapImageSide, kMapImageSide, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_FALSE); - GXInitTexObjLOD(texobj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); - mp_texObj[idx] = texobj; - } -#endif + IF_DUSK(dusk::hq_minimap::initialize_if_needed()); } void dMpath_n::dTexObjAggregate_c::remove() { diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index 6feb191562..4557edfb4c 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -363,13 +363,15 @@ void dMenu_DmapBg_c::buttonIconScreenInit() { for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { ((J2DTextBox*)mButtonScreen->search(cont_at[i]))->setFont(mDoExt_getMesgFont()); ((J2DTextBox*)mButtonScreen->search(cont_bt[i]))->setFont(mDoExt_getMesgFont()); ((J2DTextBox*)mButtonScreen->search(cont_at[i]))->setString(32, ""); ((J2DTextBox*)mButtonScreen->search(cont_bt[i]))->setString(32, ""); - ((J2DTextBox*)mButtonScreen->search(font_at[i]))->hide(); - ((J2DTextBox*)mButtonScreen->search(font_bt[i]))->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + ((J2DTextBox*)mButtonScreen->search(font_at[i]))->hide(); + ((J2DTextBox*)mButtonScreen->search(font_bt[i]))->hide(); + } } else { ((J2DTextBox*)mButtonScreen->search(font_at[i]))->setFont(mDoExt_getMesgFont()); ((J2DTextBox*)mButtonScreen->search(font_bt[i]))->setFont(mDoExt_getMesgFont()); @@ -400,7 +402,7 @@ void dMenu_DmapBg_c::buttonIconScreenInit() { J2DTextBox* textBox; for (int i = 0; i < 2; i++) { - textBox = ((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? c_tag_jpn[i] : c_tag[i], c_tag[i]))); + textBox = (J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? c_tag_jpn[i] : c_tag[i], c_tag[i])); textBox->setFont(mDoExt_getMesgFont()); textBox->setString(32, ""); } @@ -436,9 +438,9 @@ void dMenu_DmapBg_c::setAButtonString(u32 i_msgNo) { for (int i = 0; i < 5; i++) { if (i_msgNo == 0) { - SAFE_STRCPY(((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? cont_at_jpn[i] : cont_at[i], cont_at[i])))->getStringPtr(), ""); + SAFE_STRCPY(((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? cont_at_jpn[i] : cont_at[i], cont_at[i])))->getStringPtr(), ""); } else { - dMeter2Info_getStringKanji(i_msgNo, ((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? cont_at_jpn[i] : cont_at[i], cont_at[i])))->getStringPtr(), NULL); + dMeter2Info_getStringKanji(i_msgNo, ((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? cont_at_jpn[i] : cont_at[i], cont_at[i])))->getStringPtr(), NULL); } } } @@ -463,9 +465,9 @@ void dMenu_DmapBg_c::setBButtonString(u32 i_msgNo) { for (int i = 0; i < 5; i++) { if (i_msgNo == 0) { - SAFE_STRCPY(((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? cont_bt_jpn[i] : cont_bt[i], cont_bt[i])))->getStringPtr(), ""); + SAFE_STRCPY(((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? cont_bt_jpn[i] : cont_bt[i], cont_bt[i])))->getStringPtr(), ""); } else { - dMeter2Info_getStringKanji(i_msgNo, ((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? cont_bt_jpn[i] : cont_bt[i], cont_bt[i])))->getStringPtr(), NULL); + dMeter2Info_getStringKanji(i_msgNo, ((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? cont_bt_jpn[i] : cont_bt[i], cont_bt[i])))->getStringPtr(), NULL); } } } @@ -504,12 +506,12 @@ void dMenu_DmapBg_c::setCButtonString(u32 i_msgNo) { if (msgNo == 0) { for (i = 0; i < 2; i++) { - SAFE_STRCPY(((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? c_tag_jpn[i] : c_tag[i], c_tag[i])))->getStringPtr(), ""); + SAFE_STRCPY(((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? c_tag_jpn[i] : c_tag[i], c_tag[i])))->getStringPtr(), ""); } mpCButton->setAlphaRate(0.5f); } else { for (i = 0; i < 2; i++) { - dMeter2Info_getStringKanji(msgNo, ((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? c_tag_jpn[i] : c_tag[i], c_tag[i])))->getStringPtr(), NULL); + dMeter2Info_getStringKanji(msgNo, ((J2DTextBox*)mButtonScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? c_tag_jpn[i] : c_tag[i], c_tag[i])))->getStringPtr(), NULL); } mpCButton->setAlphaRate(1.0f); } @@ -979,8 +981,10 @@ void dMenu_DmapBg_c::dMapBgWide() { mButtonScreen->search(MULTI_CHAR('c_btn'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); mButtonScreen->search(MULTI_CHAR('c_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); mButtonScreen->search(MULTI_CHAR('c_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); - mButtonScreen->search(MULTI_CHAR('f_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); - mButtonScreen->search(MULTI_CHAR('f_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mButtonScreen->search(MULTI_CHAR('f_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + mButtonScreen->search(MULTI_CHAR('f_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + } // Decorations mButtonScreen->search(MULTI_CHAR('kazari_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); diff --git a/src/d/d_menu_fishing.cpp b/src/d/d_menu_fishing.cpp index eb24852160..2579d328fb 100644 --- a/src/d/d_menu_fishing.cpp +++ b/src/d/d_menu_fishing.cpp @@ -343,7 +343,9 @@ void dMenu_Fishing_c::screenSetBase() { field_0x19c[1][i]->setString(0x20, ""); mpFishNameString[i] = (J2DTextBox*)mpScreen->search(name_0[i]); + IF_DUSK_BLOCK(dusk::version::isGcn()) mpScreen->search(fname_0[i])->hide(); + IF_DUSK_BLOCK_END mpFishNameString[i]->setFont(mDoExt_getSubFont()); mpFishNameString[i]->setString(0x20, ""); dMeter2Info_getStringKanji(name_id[i], mpFishNameString[i]->getStringPtr(), NULL); diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 9d1d17ca0e..cd2e09c6d7 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -2420,10 +2420,12 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { static const u64 farea_name[3] = {MULTI_CHAR('f_name_1'), MULTI_CHAR('f_name3'), MULTI_CHAR('f_name2')}; for (int i = 0; i < 3; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { static_cast(mpTitleScreen->search(area_name[i]))->setFont(mDoExt_getRubyFont()); static_cast(mpTitleScreen->search(area_name[i]))->setString(0x40, ""); - mpTitleScreen->search(farea_name[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTitleScreen->search(farea_name[i])->hide(); + } } else { static_cast(mpTitleScreen->search(farea_name[i]))->setFont(mDoExt_getRubyFont()); static_cast(mpTitleScreen->search(farea_name[i]))->setString(0x40, ""); @@ -2458,10 +2460,12 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { #endif for (int i = 0; i < 7; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { static_cast(mpTitleScreen->search(sfont_name[i]))->setFont(mDoExt_getRubyFont()); static_cast(mpTitleScreen->search(sfont_name[i]))->setString(0x40, ""); - mpTitleScreen->search(ffont_name[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTitleScreen->search(ffont_name[i])->hide(); + } } else { static_cast(mpTitleScreen->search(ffont_name[i]))->setFont(mDoExt_getRubyFont()); static_cast(mpTitleScreen->search(ffont_name[i]))->setString(0x40, ""); @@ -2485,10 +2489,12 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { static const u64 font_zt[5] = {MULTI_CHAR('font_zt1'), MULTI_CHAR('font_zt2'), MULTI_CHAR('font_zt3'), MULTI_CHAR('font_zt4'), MULTI_CHAR('font_zt5')}; for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { static_cast(mpTitleScreen->search(cont_zt[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(cont_zt[i]))->setString(0x20, ""); - mpTitleScreen->search(font_zt[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTitleScreen->search(font_zt[i])->hide(); + } } else { static_cast(mpTitleScreen->search(font_zt[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(font_zt[i]))->setString(0x20, ""); @@ -2516,10 +2522,12 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { #endif for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { static_cast(mpTitleScreen->search(cont_bt[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(cont_bt[i]))->setString(0x20, ""); - mpTitleScreen->search(font_bt[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTitleScreen->search(font_bt[i])->hide(); + } } else { static_cast(mpTitleScreen->search(font_bt[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(font_bt[i]))->setString(0x20, ""); @@ -2543,10 +2551,12 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { static const u64 font_at[5] = {MULTI_CHAR('font_at1'), MULTI_CHAR('font_at2'), MULTI_CHAR('font_at3'), MULTI_CHAR('font_at4'), MULTI_CHAR('font_at5')}; for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { static_cast(mpTitleScreen->search(cont_at[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(cont_at[i]))->setString(0x20, ""); - mpTitleScreen->search(font_at[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTitleScreen->search(font_at[i])->hide(); + } } else { static_cast(mpTitleScreen->search(font_at[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(font_at[i]))->setString(0x20, ""); @@ -2571,10 +2581,12 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { static const u64 fuji_c[5] = {MULTI_CHAR('fuji_c00'), MULTI_CHAR('fuji_c01'), MULTI_CHAR('fuji_c02'), MULTI_CHAR('fuji_c03'), MULTI_CHAR('fuji_c04')}; for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { static_cast(mpTitleScreen->search(juji_c[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(juji_c[i]))->setString(0x20, ""); - mpTitleScreen->search(fuji_c[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTitleScreen->search(fuji_c[i])->hide(); + } } else { static_cast(mpTitleScreen->search(fuji_c[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(fuji_c[i]))->setString(0x20, ""); @@ -2598,10 +2610,12 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { static const u64 fst_c[5] = {MULTI_CHAR('fst_00'), MULTI_CHAR('fst_01'), MULTI_CHAR('fst_02'), MULTI_CHAR('fst_03'), MULTI_CHAR('fst_04')}; for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { static_cast(mpTitleScreen->search(ast_c[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(ast_c[i]))->setString(0x20, ""); - mpTitleScreen->search(fst_c[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTitleScreen->search(fst_c[i])->hide(); + } } else { static_cast(mpTitleScreen->search(fst_c[i]))->setFont(mDoExt_getMesgFont()); static_cast(mpTitleScreen->search(fst_c[i]))->setString(0x20, ""); @@ -2863,8 +2877,7 @@ void dMenu_Fmap2DTop_c::setTitleNameString(u32 param_0) { static const u64 ffont_name[7] = { MULTI_CHAR('ffont00'), MULTI_CHAR('ffontl0'), MULTI_CHAR('ffontl1'), MULTI_CHAR('ffontl2'), MULTI_CHAR('ffontb0'), MULTI_CHAR('ffontb3'), MULTI_CHAR('ffontb4') }; - - auto setTitleNameString_font_name = dusk::version::isRegionJpn() ? sfont_name : ffont_name; + auto setTitleNameString_font_name = dusk::version::isJpnOrLessThanWiiJpn() ? sfont_name : ffont_name; #elif VERSION == VERSION_GCN_JPN static const u64 sfont_name[7] = { MULTI_CHAR('sfont00'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2') @@ -2883,15 +2896,9 @@ void dMenu_Fmap2DTop_c::setTitleNameString(u32 param_0) { #endif for (int i = 0; i < 7; i++) { if (param_0 == 0) { - SAFE_STRCPY(((J2DTextBox*)(mpTitleScreen->search(setTitleNameString_font_name[i]))) - ->getStringPtr(), - ""); + SAFE_STRCPY(((J2DTextBox*)(mpTitleScreen->search(setTitleNameString_font_name[i])))->getStringPtr(), ""); } else { - dMeter2Info_getStringKanji( - param_0, - ((J2DTextBox*)(mpTitleScreen->search(setTitleNameString_font_name[i]))) - ->getStringPtr(), - NULL); + dMeter2Info_getStringKanji(param_0, ((J2DTextBox*)(mpTitleScreen->search(setTitleNameString_font_name[i])))->getStringPtr(), NULL); } } } @@ -2900,7 +2907,7 @@ void dMenu_Fmap2DTop_c::setAreaNameString(u32 param_0) { #if TARGET_PC static const u64 iarea_name[3] = {MULTI_CHAR('i_name_s'), MULTI_CHAR('i_name'), MULTI_CHAR('i_name1')}; static const u64 farea_name[3] = {MULTI_CHAR('f_name_1'), MULTI_CHAR('f_name3'), MULTI_CHAR('f_name2')}; - auto setAreaNameString_area_name = dusk::version::isRegionJpn() ? iarea_name : farea_name; + auto setAreaNameString_area_name = dusk::version::isJpnOrLessThanWiiJpn() ? iarea_name : farea_name; #elif VERSION == VERSION_GCN_JPN static const u64 iarea_name[3] = {MULTI_CHAR('i_name_s'), MULTI_CHAR('i_name'), MULTI_CHAR('i_name1')}; #define setAreaNameString_area_name iarea_name @@ -2910,15 +2917,9 @@ void dMenu_Fmap2DTop_c::setAreaNameString(u32 param_0) { #endif for (int i = 0; i < 3; i++) { if (param_0 == 0) { - SAFE_STRCPY(((J2DTextBox*)(mpTitleScreen->search(setAreaNameString_area_name[i]))) - ->getStringPtr(), - ""); + SAFE_STRCPY(((J2DTextBox*)(mpTitleScreen->search(setAreaNameString_area_name[i])))->getStringPtr(), ""); } else { - dMeter2Info_getStringKanji( - param_0, - ((J2DTextBox*)(mpTitleScreen->search(setAreaNameString_area_name[i]))) - ->getStringPtr(), - NULL); + dMeter2Info_getStringKanji(param_0, ((J2DTextBox*)(mpTitleScreen->search(setAreaNameString_area_name[i])))->getStringPtr(), NULL); } } } @@ -2937,7 +2938,7 @@ void dMenu_Fmap2DTop_c::setZButtonString(u32 param_0, u8 i_alpha) { #if TARGET_PC static const u64 cont_zt[5] = {MULTI_CHAR('cont_zt'), MULTI_CHAR('cont_zt1'), MULTI_CHAR('cont_zt2'), MULTI_CHAR('cont_zt3'), MULTI_CHAR('cont_zt4')}; static const u64 font_zt[5] = {MULTI_CHAR('font_zt1'), MULTI_CHAR('font_zt2'), MULTI_CHAR('font_zt3'), MULTI_CHAR('font_zt4'), MULTI_CHAR('font_zt5')}; - auto setZButtonString_font_zt = dusk::version::isRegionJpn() ? cont_zt : font_zt; + auto setZButtonString_font_zt = dusk::version::isJpnOrLessThanWiiJpn() ? cont_zt : font_zt; #elif VERSION == VERSION_GCN_JPN static const u64 cont_zt[5] = {MULTI_CHAR('cont_zt'), MULTI_CHAR('cont_zt1'), MULTI_CHAR('cont_zt2'), MULTI_CHAR('cont_zt3'), MULTI_CHAR('cont_zt4')}; #define setZButtonString_font_zt cont_zt @@ -2953,10 +2954,7 @@ void dMenu_Fmap2DTop_c::setZButtonString(u32 param_0, u8 i_alpha) { #endif } else { for (int i = 0; i < 5; i++) { - dMeter2Info_getStringKanji( - param_0, - ((J2DTextBox*)(mpTitleScreen->search(setZButtonString_font_zt[i])))->getStringPtr(), - NULL); + dMeter2Info_getStringKanji(param_0, ((J2DTextBox*)(mpTitleScreen->search(setZButtonString_font_zt[i])))->getStringPtr(), NULL); } if (i_alpha == ALPHA_DEFAULT) { @@ -2975,7 +2973,7 @@ void dMenu_Fmap2DTop_c::setBButtonString(u32 param_0, u8 i_alpha) { #if TARGET_PC static const u64 cont_bt[5] = {MULTI_CHAR('cont_bt1'), MULTI_CHAR('cont_bt2'), MULTI_CHAR('cont_bt3'), MULTI_CHAR('cont_bt4'), MULTI_CHAR('cont_bt')}; static const u64 font_bt[5] = {MULTI_CHAR('font_bt1'), MULTI_CHAR('font_bt2'), MULTI_CHAR('font_bt3'), MULTI_CHAR('font_bt4'), MULTI_CHAR('font_bt5')}; - auto setBButtonString_font_bt = dusk::version::isRegionJpn() ? cont_bt : font_bt; + auto setBButtonString_font_bt = dusk::version::isJpnOrLessThanWiiJpn() ? cont_bt : font_bt; #elif VERSION == VERSION_GCN_JPN static const u64 cont_bt[5] = {MULTI_CHAR('cont_bt1'), MULTI_CHAR('cont_bt2'), MULTI_CHAR('cont_bt3'), MULTI_CHAR('cont_bt4'), MULTI_CHAR('cont_bt')}; #define setBButtonString_font_bt cont_bt @@ -2987,10 +2985,7 @@ void dMenu_Fmap2DTop_c::setBButtonString(u32 param_0, u8 i_alpha) { mAlphaButtonB = ALPHA_MIN; } else { for (int i = 0; i < 5; i++) { - dMeter2Info_getStringKanji( - param_0, - ((J2DTextBox*)(mpTitleScreen->search(setBButtonString_font_bt[i])))->getStringPtr(), - NULL); + dMeter2Info_getStringKanji(param_0, ((J2DTextBox*)(mpTitleScreen->search(setBButtonString_font_bt[i])))->getStringPtr(), NULL); } if (i_alpha == ALPHA_DEFAULT) { @@ -3005,7 +3000,7 @@ void dMenu_Fmap2DTop_c::setAButtonString(u32 param_0, u8 i_alpha) { #if TARGET_PC static const u64 cont_at[5] = {MULTI_CHAR('cont_at'), MULTI_CHAR('cont_at1'), MULTI_CHAR('cont_at2'), MULTI_CHAR('cont_at3'), MULTI_CHAR('cont_at4')}; static const u64 font_at[5] = {MULTI_CHAR('font_at1'), MULTI_CHAR('font_at2'), MULTI_CHAR('font_at3'), MULTI_CHAR('font_at4'), MULTI_CHAR('font_at5')}; - auto setAButtonString_font_at = dusk::version::isRegionJpn() ? cont_at : font_at; + auto setAButtonString_font_at = dusk::version::isJpnOrLessThanWiiJpn() ? cont_at : font_at; #elif VERSION == VERSION_GCN_JPN static const u64 cont_at[5] = {MULTI_CHAR('cont_at'), MULTI_CHAR('cont_at1'), MULTI_CHAR('cont_at2'), MULTI_CHAR('cont_at3'), MULTI_CHAR('cont_at4')}; #define setAButtonString_font_at cont_at @@ -3017,10 +3012,7 @@ void dMenu_Fmap2DTop_c::setAButtonString(u32 param_0, u8 i_alpha) { mAlphaButtonA = ALPHA_MIN; } else { for (int i = 0; i < 5; i++) { - dMeter2Info_getStringKanji( - param_0, - ((J2DTextBox*)(mpTitleScreen->search(setAButtonString_font_at[i])))->getStringPtr(), - NULL); + dMeter2Info_getStringKanji(param_0, ((J2DTextBox*)(mpTitleScreen->search(setAButtonString_font_at[i])))->getStringPtr(), NULL); } if (i_alpha == ALPHA_DEFAULT) { @@ -3034,8 +3026,8 @@ void dMenu_Fmap2DTop_c::setAButtonString(u32 param_0, u8 i_alpha) { void dMenu_Fmap2DTop_c::setCrossLRString(u32 param_0) { #if PLATFORM_GCN || (VERSION == VERSION_SHIELD) #if TARGET_PC - static const u64 juji_c_jpn[5] = {MULTI_CHAR('juji_c00'), MULTI_CHAR('juji_c01'), MULTI_CHAR('juji_c02'), MULTI_CHAR('juji_c03'), MULTI_CHAR('juji_c04')}; - static const u64 juji_c[5] = {MULTI_CHAR('fuji_c00'), MULTI_CHAR('fuji_c01'), MULTI_CHAR('fuji_c02'), MULTI_CHAR('fuji_c03'), MULTI_CHAR('fuji_c04')}; + static const u64 juji_c[5] = {MULTI_CHAR('juji_c00'), MULTI_CHAR('juji_c01'), MULTI_CHAR('juji_c02'), MULTI_CHAR('juji_c03'), MULTI_CHAR('juji_c04')}; + static const u64 fuji_c[5] = {MULTI_CHAR('fuji_c00'), MULTI_CHAR('fuji_c01'), MULTI_CHAR('fuji_c02'), MULTI_CHAR('fuji_c03'), MULTI_CHAR('fuji_c04')}; #elif VERSION == VERSION_GCN_JPN static const u64 juji_c[5] = {MULTI_CHAR('juji_c00'), MULTI_CHAR('juji_c01'), MULTI_CHAR('juji_c02'), MULTI_CHAR('juji_c03'), MULTI_CHAR('juji_c04')}; #else @@ -3043,14 +3035,14 @@ void dMenu_Fmap2DTop_c::setCrossLRString(u32 param_0) { #endif if (param_0 == 0) { for (int i = 0; i < 5; i++) { - J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? juji_c_jpn[i] : juji_c[i], juji_c[i]))); + J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? juji_c[i] : fuji_c[i], juji_c[i]))); SAFE_STRCPY(text_box->getStringPtr(), ""); } mpTitleScreen->search(MULTI_CHAR('juy_sha0'))->show(); mAlphaDpad = 1; } else { for (int i = 0; i < 5; i++) { - J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? juji_c_jpn[i] : juji_c[i], juji_c[i]))); + J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? juji_c[i] : fuji_c[i], juji_c[i]))); dMeter2Info_getStringKanji(param_0, text_box->getStringPtr(), NULL); } mpTitleScreen->search(MULTI_CHAR('juy_sha0'))->show(); @@ -3062,8 +3054,8 @@ void dMenu_Fmap2DTop_c::setCrossLRString(u32 param_0) { void dMenu_Fmap2DTop_c::set3DStickString(u32 param_0) { #if PLATFORM_GCN || (VERSION == VERSION_SHIELD) #if TARGET_PC - static const u64 ast_c_jpn[5] = {MULTI_CHAR('ast_00'), MULTI_CHAR('ast_01'), MULTI_CHAR('ast_02'), MULTI_CHAR('ast_03'), MULTI_CHAR('ast_04')}; - static const u64 ast_c[5] = {MULTI_CHAR('fst_00'), MULTI_CHAR('fst_01'), MULTI_CHAR('fst_02'), MULTI_CHAR('fst_03'), MULTI_CHAR('fst_04')}; + static const u64 ast_c[5] = {MULTI_CHAR('ast_00'), MULTI_CHAR('ast_01'), MULTI_CHAR('ast_02'), MULTI_CHAR('ast_03'), MULTI_CHAR('ast_04')}; + static const u64 fst_c[5] = {MULTI_CHAR('fst_00'), MULTI_CHAR('fst_01'), MULTI_CHAR('fst_02'), MULTI_CHAR('fst_03'), MULTI_CHAR('fst_04')}; #elif VERSION == VERSION_GCN_JPN static const u64 ast_c[5] = {MULTI_CHAR('ast_00'), MULTI_CHAR('ast_01'), MULTI_CHAR('ast_02'), MULTI_CHAR('ast_03'), MULTI_CHAR('ast_04')}; #else @@ -3071,14 +3063,14 @@ void dMenu_Fmap2DTop_c::set3DStickString(u32 param_0) { #endif if (param_0 == 0) { for (int i = 0; i < 5; i++) { - J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? ast_c_jpn[i] : ast_c[i], ast_c[i]))); + J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? ast_c[i] : fst_c[i], ast_c[i]))); SAFE_STRCPY(text_box->getStringPtr(), ""); } mpTitleScreen->search(MULTI_CHAR('as_sha0'))->show(); mAlphaAnalogStick = 1; } else { for (int i = 0; i < 5; i++) { - J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isRegionJpn() ? ast_c_jpn[i] : ast_c[i], ast_c[i]))); + J2DTextBox* text_box = static_cast(mpTitleScreen->search(DUSK_IF_ELSE(dusk::version::isJpnOrLessThanWiiJpn() ? ast_c[i] : fst_c[i], ast_c[i]))); dMeter2Info_getStringKanji(param_0, text_box->getStringPtr(), NULL); } mpTitleScreen->search(MULTI_CHAR('as_sha0'))->show(); diff --git a/src/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index cb9af850c0..9d6f0816e2 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -73,6 +73,26 @@ static calibrationFunc calibration_process[] = { static dusk::menu_pointer::TargetId option_yes_no_target(u8 index) noexcept { return static_cast(0x100 + index); } + +static u8 option_to_proc(u8 select) noexcept { + if (!dusk::version::isRegionJpn() && select >= 1) { + return static_cast(select + 1); + } + return select; +} + +static u8 option_to_select(u8 proc) noexcept { + if (!dusk::version::isRegionJpn() && proc >= 2) { + return static_cast(proc - 1); + } + return proc; +} + +#define OPTION_SELECT(proc_e) (option_to_select(proc_e)) +#define OPTION_PROC(select) (option_to_proc(select)) +#else +#define OPTION_SELECT(proc_e) (proc_e) +#define OPTION_PROC(select) (select) #endif dMenu_Option_c::dMenu_Option_c(JKRArchive* i_archive, STControl* i_stick) { @@ -159,7 +179,9 @@ void dMenu_Option_c::_create() { mpTVButtonText = JKR_NEW CPaneMgr(mpTVScreen, MULTI_CHAR('a_text_n'), 0, NULL); JUT_ASSERT(298, mpTVButtonText != NULL); + IF_DUSK_BLOCK(dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) mpTVScreen->search(MULTI_CHAR('g_abtn_n'))->hide(); + IF_DUSK_BLOCK_END mpScreenIcon = JKR_NEW J2DScreen(); JUT_ASSERT(325, mpScreenIcon != NULL); @@ -257,11 +279,11 @@ void dMenu_Option_c::_create() { field_0x3e1 = 10; field_0x3e2 = 0xff; field_0x3e3 = 0xc0; - field_0x3ef = PROC_ATTEN_e; + field_0x3ef = OPTION_SELECT(PROC_ATTEN_e); field_0x3f0 = 0xff; field_0x3f1 = 0xff; field_0x3f2 = 0; - field_0x3f5 = PROC_ATTEN_e; + field_0x3f5 = OPTION_SELECT(PROC_ATTEN_e); field_0x3f3 = 5; field_0x3f4 = 5; field_0x334 = 0.0f; @@ -485,9 +507,9 @@ void dMenu_Option_c::_move() { } if (mDoGph_gInf_c::getFader()->getStatus() == 1) { - if (mDoCPd_c::getTrigA(PAD_1) != 0 && field_0x3ef != PROC_CHANGE_MOVE_e && field_0x3f3 == 5) { - if (field_0x3f4 == 5 && field_0x3ef != PROC_CONFIRM_OPEN_MOVE_e && field_0x3ef != PROC_CONFIRM_MOVE_MOVE_e && field_0x3ef != PROC_CONFIRM_SELECT_MOVE_e && - field_0x3ef != PROC_CONFIRM_CLOSE_MOVE_e) + if (mDoCPd_c::getTrigA(PAD_1) != 0 && field_0x3ef != OPTION_SELECT(PROC_CHANGE_MOVE_e) && field_0x3f3 == 5) { + if (field_0x3f4 == 5 && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e) && + field_0x3ef != OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e)) { if (mDoCPd_c::getTrigStart(PAD_1) == 0 && mDoCPd_c::getTrigB(PAD_1) == 0) { if (mDoCPd_c::getTrigUp(PAD_1) == 0 && mDoCPd_c::getTrigDown(PAD_1) == 0 && @@ -495,17 +517,17 @@ void dMenu_Option_c::_move() { { field_0x3f7 = 1; field_0x3f5 = field_0x3ef; - field_0x3ef = PROC_CONFIRM_OPEN_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e); dMeter2Info_set2DVibration(); - (this->*init[field_0x3ef])(); + (this->*init[OPTION_PROC(field_0x3ef)])(); goto skip; } } } } - if (mDoCPd_c::getTrigB(PAD_1) != 0 && field_0x3ef != PROC_CHANGE_MOVE_e && field_0x3f3 == 5 && - field_0x3ef != PROC_CONFIRM_OPEN_MOVE_e && field_0x3ef != PROC_CONFIRM_MOVE_MOVE_e && field_0x3ef != PROC_CONFIRM_SELECT_MOVE_e && field_0x3ef != PROC_CONFIRM_CLOSE_MOVE_e) + if (mDoCPd_c::getTrigB(PAD_1) != 0 && field_0x3ef != OPTION_SELECT(PROC_CHANGE_MOVE_e) && field_0x3f3 == 5 && + field_0x3ef != OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e)) { if (field_0x3f4 == 5 && mDoCPd_c::getTrigStart(PAD_1) == 0 && mDoCPd_c::getTrigA(PAD_1) == 0 && mDoCPd_c::getTrigUp(PAD_1) == 0 && @@ -514,16 +536,16 @@ void dMenu_Option_c::_move() { { field_0x3f7 = 0; field_0x3f5 = field_0x3ef; - field_0x3ef = PROC_CONFIRM_OPEN_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e); dMeter2Info_set2DVibration(); - (this->*init[field_0x3ef])(); + (this->*init[OPTION_PROC(field_0x3ef)])(); } } #if TARGET_PC - if (field_0x3f4 == 5 && field_0x3ef != PROC_CHANGE_MOVE_e && field_0x3f3 == 5 && - field_0x3ef != PROC_CONFIRM_OPEN_MOVE_e && field_0x3ef != PROC_CONFIRM_MOVE_MOVE_e && field_0x3ef != PROC_CONFIRM_SELECT_MOVE_e && - field_0x3ef != PROC_CONFIRM_CLOSE_MOVE_e && pointerConfirmSelect()) + if (field_0x3f4 == 5 && field_0x3ef != OPTION_SELECT(PROC_CHANGE_MOVE_e) && field_0x3f3 == 5 && + field_0x3ef != OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e) && + field_0x3ef != OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e) && pointerConfirmSelect()) { goto skip; } @@ -531,7 +553,7 @@ void dMenu_Option_c::_move() { } skip: u8 oldValue = field_0x3ef; - if (field_0x3f3 == 5 && oldValue != PROC_CONFIRM_OPEN_MOVE_e && oldValue != PROC_CONFIRM_MOVE_MOVE_e && oldValue != PROC_CONFIRM_SELECT_MOVE_e && oldValue != PROC_CONFIRM_CLOSE_MOVE_e) { + if (field_0x3f3 == 5 && oldValue != OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e) && oldValue != OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e) && oldValue != OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e) && oldValue != OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e)) { dpdMenuMove(); } @@ -545,10 +567,10 @@ skip: field_0x3f0 = 0xff; } - (this->*process[field_0x3ef])(); + (this->*process[OPTION_PROC(field_0x3ef)])(); mpSelectScreen->animation(); if (oldValue != field_0x3ef) { - (this->*init[field_0x3ef])(); + (this->*init[OPTION_PROC(field_0x3ef)])(); } setHIO(false); @@ -610,8 +632,8 @@ void dMenu_Option_c::drawHaihai() { field_0x3f6 = 0; field_0x3f6 |= 1; field_0x3f6 |= 4; - if (selectType < PROC_CONFIRM_OPEN_MOVE_e && field_0x3f6 != 0 && field_0x3f3 == 5 && field_0x3ef != PROC_CONFIRM_OPEN_MOVE_e && - field_0x3ef != PROC_CONFIRM_MOVE_MOVE_e && field_0x3ef != PROC_CONFIRM_SELECT_MOVE_e && field_0x3ef != PROC_CONFIRM_CLOSE_MOVE_e) + if (selectType < OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e) && field_0x3f6 != 0 && field_0x3f3 == 5 && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e) && + field_0x3ef != OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e) && field_0x3ef != OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e)) { mpMeterHaihai->_execute(0); Vec haihaiPosL = @@ -752,13 +774,7 @@ void dMenu_Option_c::atten_move() { if (field_0x3f3 != 5) { (this->*tv_process[field_0x3f3])(); } else if (downTrigger) { -#if TARGET_PC - field_0x3ef = dusk::version::isRegionJpn() ? PROC_RUBY_e : PROC_VIB_e; -#elif VERSION == VERSION_GCN_JPN - field_0x3ef = PROC_RUBY_e; -#else - field_0x3ef = PROC_VIB_e; -#endif + field_0x3ef = 1; Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (leftTrigger) { if (field_0x3e4 == 0) { @@ -768,8 +784,8 @@ void dMenu_Option_c::atten_move() { field_0x3e4 = 0; field_0x3da = -5; } - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_ATTEN_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_ATTEN_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (rightTrigger) { if (field_0x3e4 == 0) { @@ -779,8 +795,8 @@ void dMenu_Option_c::atten_move() { field_0x3e4 = 0; field_0x3da = 5; } - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_ATTEN_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_ATTEN_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else { changeTVCheck(); @@ -818,10 +834,10 @@ void dMenu_Option_c::ruby_move() { if (field_0x3f3 != 5) { (this->*tv_process[field_0x3f3])(); } else if (upTrigger) { - field_0x3ef = PROC_ATTEN_e; + field_0x3ef = OPTION_SELECT(PROC_ATTEN_e); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (downTrigger) { - field_0x3ef = PROC_VIB_e; + field_0x3ef = OPTION_SELECT(PROC_VIB_e); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (leftTrigger) { if (field_0x3e5_JPN == 0) { @@ -831,8 +847,8 @@ void dMenu_Option_c::ruby_move() { field_0x3e5_JPN = 0; field_0x3da = -5; } - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_RUBY_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_RUBY_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (rightTrigger) { if (field_0x3e5_JPN == 0) { @@ -842,8 +858,8 @@ void dMenu_Option_c::ruby_move() { field_0x3e5_JPN = 0; field_0x3da = 5; } - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_RUBY_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_RUBY_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else { changeTVCheck(); @@ -853,7 +869,7 @@ void dMenu_Option_c::ruby_move() { void dMenu_Option_c::vib_init() { mpDrawCursor->setAlphaRate(1.0f); - setCursorPos(PROC_VIB_e); + setCursorPos(OPTION_SELECT(PROC_VIB_e)); setAButtonString(0x40C); setBButtonString(0x3F9); } @@ -868,7 +884,7 @@ void dMenu_Option_c::vib_move() { (this->*tv_process[field_0x3f3])(); } else if (upTrigger) { #if TARGET_PC - field_0x3ef = dusk::version::isRegionJpn() ? PROC_RUBY_e : PROC_ATTEN_e; + field_0x3ef = OPTION_SELECT(dusk::version::isRegionJpn() ? PROC_RUBY_e : PROC_ATTEN_e); #elif VERSION == VERSION_GCN_JPN field_0x3ef = PROC_RUBY_e; #else @@ -876,7 +892,7 @@ void dMenu_Option_c::vib_move() { #endif Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (downTrigger) { - field_0x3ef = PROC_SOUND_e; + field_0x3ef = OPTION_SELECT(PROC_SOUND_e); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (leftTrigger) { if (isRumbleSupported()) { @@ -888,8 +904,8 @@ void dMenu_Option_c::vib_move() { field_0x3ea = 0; field_0x3da = -5; } - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_VIB_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_VIB_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } @@ -903,8 +919,8 @@ void dMenu_Option_c::vib_move() { field_0x3ea = 0; field_0x3da = 5; } - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_VIB_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_VIB_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } @@ -915,7 +931,7 @@ void dMenu_Option_c::vib_move() { void dMenu_Option_c::sound_init() { mpDrawCursor->setAlphaRate(1.0f); - setCursorPos(PROC_SOUND_e); + setCursorPos(OPTION_SELECT(PROC_SOUND_e)); setAButtonString(0x40C); setBButtonString(0x3F9); } @@ -929,7 +945,7 @@ void dMenu_Option_c::sound_move() { if (field_0x3f3 != 5) { (this->*tv_process[field_0x3f3])(); } else if (upTrigger) { - field_0x3ef = PROC_VIB_e; + field_0x3ef = OPTION_SELECT(PROC_VIB_e); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (leftTrigger) { if (field_0x3e9 == 2) { @@ -954,8 +970,8 @@ void dMenu_Option_c::sound_move() { } mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]); setSoundMode(dMo_soundMode[field_0x3e9]); - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_SOUND_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_SOUND_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else if (rightTrigger) { if (field_0x3e9 == 0) { @@ -980,8 +996,8 @@ void dMenu_Option_c::sound_move() { } mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]); setSoundMode(dMo_soundMode[field_0x3e9]); - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_SOUND_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_SOUND_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } else { changeTVCheck(); @@ -1002,15 +1018,15 @@ void dMenu_Option_c::change_move() { field_0x3da++; } u8 index; - switch (field_0x3f5) { + switch (OPTION_PROC(field_0x3f5)) { case PROC_ATTEN_e: - index = PROC_ATTEN_e; + index = field_0x3f5; if (field_0x3da == 0) { setAttenString(); } break; case PROC_RUBY_e: - index = PROC_RUBY_e; + index = field_0x3f5; if (field_0x3da == 0) { IF_DUSK_BLOCK(dusk::version::isRegionJpn()) setRubyString(); @@ -1018,13 +1034,13 @@ void dMenu_Option_c::change_move() { } break; case PROC_VIB_e: - index = DUSK_IF_ELSE(dusk::version::isRegionJpn() ? 2 : 1, PROC_VIB_e); + index = field_0x3f5; if (field_0x3da == 0) { setVibString(); } break; case PROC_SOUND_e: - index = DUSK_IF_ELSE(dusk::version::isRegionJpn() ? 3 : 2, PROC_SOUND_e); + index = field_0x3f5; if (field_0x3da == 0) { setSoundString(); } @@ -1090,7 +1106,7 @@ void dMenu_Option_c::confirm_open_move() { } if (status == 1 && yesNoMenuMove == 1 && field_0x374 == 1.0f) { yesnoCursorShow(); - field_0x3ef = PROC_CONFIRM_MOVE_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e); } mpWarning->_move(); setAnimation(); @@ -1120,21 +1136,21 @@ void dMenu_Option_c::confirm_move_move() { field_0x3f9 = i; if (clicked) { yesNoSelectStart(); - field_0x3ef = PROC_CONFIRM_CLOSE_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); mpWarning->_move(); setAnimation(); return; } yesnoSelectAnmSet(); - field_0x3ef = PROC_CONFIRM_SELECT_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e); mpWarning->_move(); setAnimation(); return; } if (clicked) { yesNoSelectStart(); - field_0x3ef = PROC_CONFIRM_CLOSE_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); mpWarning->_move(); setAnimation(); @@ -1145,12 +1161,12 @@ void dMenu_Option_c::confirm_move_move() { if (mDoCPd_c::getTrigA(PAD_1) != 0) { yesNoSelectStart(); - field_0x3ef = PROC_CONFIRM_CLOSE_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); } else if (mDoCPd_c::getTrigB(PAD_1) != 0) { field_0x3f9 = 0; yesnoCancelAnmSet(); - field_0x3ef = PROC_CONFIRM_CLOSE_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); } else if (rightTrigger != 0) { if (field_0x3f9 != 0) { @@ -1159,7 +1175,7 @@ void dMenu_Option_c::confirm_move_move() { field_0x3fa = field_0x3f9; field_0x3f9 = 0; yesnoSelectAnmSet(); - field_0x3ef = PROC_CONFIRM_SELECT_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e); } } else if (leftTrigger != 0) { if (field_0x3f9 != 1) { @@ -1168,7 +1184,7 @@ void dMenu_Option_c::confirm_move_move() { field_0x3fa = field_0x3f9; field_0x3f9 = 1; yesnoSelectAnmSet(); - field_0x3ef = PROC_CONFIRM_SELECT_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_SELECT_MOVE_e); } } mpWarning->_move(); @@ -1202,14 +1218,14 @@ void dMenu_Option_c::confirm_select_move() { dusk::menu_pointer::Context::Options, option_yes_no_target(field_0x3f9))) { yesNoSelectStart(); - field_0x3ef = PROC_CONFIRM_CLOSE_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_CLOSE_MOVE_e); dMeter2Info_set2DVibrationM(); mpWarning->_move(); setAnimation(); return; } #endif - field_0x3ef = PROC_CONFIRM_MOVE_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_MOVE_MOVE_e); } mpWarning->_move(); setAnimation(); @@ -1353,13 +1369,7 @@ void dMenu_Option_c::calibration_close2_move() { void dMenu_Option_c::menuVisible() { for (int i = 0; i < 6; i++) { -#if TARGET_PC - if ((dusk::version::isRegionJpn() && i < 4) || i < 3) -#elif VERSION == VERSION_GCN_JPN - if (i < 4) -#else - if (i < 3) -#endif + if (i < OPTION_SELECT(PROC_CHANGE_MOVE_e)) { menuShow(i); } else { @@ -1710,9 +1720,11 @@ void dMenu_Option_c::screenSet() { mpString->getString(0x55C, field_0x270[2], NULL, NULL, NULL, 0); for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { field_0x25c[i] = (J2DTextBox*)mpTVScreen->search(tv_btnA[i]); - mpTVScreen->search(ftv_btnA[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpTVScreen->search(ftv_btnA[i])->hide(); + } } else { field_0x25c[i] = (J2DTextBox*)mpTVScreen->search(ftv_btnA[i]); mpTVScreen->search(tv_btnA[i])->hide(); @@ -2210,19 +2222,6 @@ void dMenu_Option_c::setSoundString() { } void dMenu_Option_c::setCursorPos(u8 i_index) { -#if TARGET_PC - if (!dusk::version::isRegionJpn()) { - switch (i_index) { - case PROC_VIB_e: - i_index = 1; - break; - case PROC_SOUND_e: - i_index = 2; - break; - } - } -#endif - #if TARGET_PC || VERSION != VERSION_GCN_JPN IF_DUSK_BLOCK(!dusk::version::isRegionJpn()) if (i_index == 4) { @@ -2279,33 +2278,13 @@ void dMenu_Option_c::setSelectColor(u8 param_0, bool param_1) { } u8 dMenu_Option_c::getSelectType() { -#if TARGET_PC - u8 proc = 0; - if (field_0x3ef < PROC_CHANGE_MOVE_e) { - proc = field_0x3ef; - } else if (field_0x3f5 < PROC_CHANGE_MOVE_e) { - proc = field_0x3f5; - } - - if (!dusk::version::isRegionJpn()) { - switch (proc) { - case PROC_VIB_e: - return 1; - case PROC_SOUND_e: - return 2; - } - } - - return proc; -#else - if (field_0x3ef < PROC_CHANGE_MOVE_e) { + if (field_0x3ef < OPTION_SELECT(PROC_CHANGE_MOVE_e)) { return field_0x3ef; } - if (field_0x3f5 < PROC_CHANGE_MOVE_e) { + if (field_0x3f5 < OPTION_SELECT(PROC_CHANGE_MOVE_e)) { return field_0x3f5; } return 0; -#endif } void dMenu_Option_c::changeBarColor(bool i_changeColor) { @@ -2513,9 +2492,9 @@ bool dMenu_Option_c::pointerConfirmSelect() { field_0x3f7 = 1; field_0x3f5 = field_0x3ef; - field_0x3ef = PROC_CONFIRM_OPEN_MOVE_e; + field_0x3ef = OPTION_SELECT(PROC_CONFIRM_OPEN_MOVE_e); dMeter2Info_set2DVibration(); - (this->*init[field_0x3ef])(); + (this->*init[OPTION_PROC(field_0x3ef)])(); return true; } #endif @@ -2560,8 +2539,8 @@ bool dMenu_Option_c::dpdMenuMove() { } if (getSelectType() != i) { - field_0x3ef = selectProc; - setCursorPos(field_0x3ef); + field_0x3ef = OPTION_SELECT(selectProc); + setCursorPos(i); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } @@ -2573,8 +2552,8 @@ bool dMenu_Option_c::dpdMenuMove() { case PROC_ATTEN_e: field_0x3e4 ^= 1; field_0x3da = 5; - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_ATTEN_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_ATTEN_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); return true; @@ -2586,8 +2565,8 @@ bool dMenu_Option_c::dpdMenuMove() { field_0x3e5_JPN = 0; field_0x3da = 5; } - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_RUBY_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_RUBY_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); return true; case PROC_VIB_e: @@ -2597,8 +2576,8 @@ bool dMenu_Option_c::dpdMenuMove() { mDoCPd_c::startMotorWave(0, &field_0x3e0, JUTGamePad::CRumble::VAL_0, 0x3c); } field_0x3da = 5; - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_VIB_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_VIB_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } @@ -2612,8 +2591,8 @@ bool dMenu_Option_c::dpdMenuMove() { field_0x3da = 5; mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]); setSoundMode(dMo_soundMode[field_0x3e9]); - field_0x3ef = PROC_CHANGE_MOVE_e; - field_0x3f5 = PROC_SOUND_e; + field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); + field_0x3f5 = OPTION_SELECT(PROC_SOUND_e); Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); return true; diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index 3fe55c2b6c..ed79d40524 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -333,11 +333,13 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i mpScreen->search(MULTI_CHAR('yx_te_s3'))->hide(); mpScreen->search(MULTI_CHAR('yx_te_s4'))->hide(); mpScreen->search(MULTI_CHAR('yx_text'))->hide(); + IF_DUSK_BLOCK(dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) mpScreen->search(MULTI_CHAR('fyx_te_1'))->hide(); mpScreen->search(MULTI_CHAR('fyx_te_2'))->hide(); mpScreen->search(MULTI_CHAR('fyx_te_3'))->hide(); mpScreen->search(MULTI_CHAR('fyx_te_4'))->hide(); mpScreen->search(MULTI_CHAR('fyx_tex'))->hide(); + IF_DUSK_BLOCK_END mpScreen->search(MULTI_CHAR('x_btn_n'))->hide(); mpScreen->search(MULTI_CHAR('y_btn_n'))->hide(); } @@ -345,9 +347,11 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i for (i = 0; i < 5; i++) { #if TARGET_PC J2DTextBox* fxy_TextBox; - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { fxy_TextBox = (J2DTextBox*)mpScreen->search(xy_text[i]); - mpScreen->search(fxy_text[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpScreen->search(fxy_text[i])->hide(); + } } else { fxy_TextBox = (J2DTextBox*)mpScreen->search(fxy_text[i]); mpScreen->search(xy_text[i])->hide(); @@ -366,13 +370,17 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i for (i = 0; i < 5; i++) { #if TARGET_PC J2DTextBox* fc_TextBox; - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { if (dusk::getSettings().game.swapDirectSelect) { fc_TextBox = (J2DTextBox*)mpScreen->search(c_text1[i]); - mpScreen->search(fc_text1[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpScreen->search(fc_text1[i])->hide(); + } } else { fc_TextBox = (J2DTextBox*)mpScreen->search(c_text[i]); - mpScreen->search(fc_text[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpScreen->search(fc_text[i])->hide(); + } } } else { if (dusk::getSettings().game.swapDirectSelect) { @@ -397,13 +405,17 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i for (i = 0; i < 5; i++) { #if TARGET_PC J2DTextBox* fc1_TextBox; - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { if (dusk::getSettings().game.swapDirectSelect) { fc1_TextBox = (J2DTextBox*)mpScreen->search(c_text[i]); - mpScreen->search(fc_text[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpScreen->search(fc_text[i])->hide(); + } } else { fc1_TextBox = (J2DTextBox*)mpScreen->search(c_text1[i]); - mpScreen->search(fc_text1[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpScreen->search(fc_text1[i])->hide(); + } } } else { if (dusk::getSettings().game.swapDirectSelect) { @@ -427,9 +439,11 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i } for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { mpComboOffString[i] = (J2DTextBox*)mpScreen->search(t_on[i]); - mpScreen->search(ft_on[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpScreen->search(ft_on[i])->hide(); + } } else { mpComboOffString[i] = (J2DTextBox*)mpScreen->search(ft_on[i]); mpScreen->search(t_on[i])->hide(); @@ -447,9 +461,11 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i } for (int i = 0; i < 5; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { mpBowArrowComboString[i] = (J2DTextBox*)mpScreen->search(t_off[i]); - mpScreen->search(ft_off[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpScreen->search(ft_off[i])->hide(); + } } else { mpBowArrowComboString[i] = (J2DTextBox*)mpScreen->search(ft_off[i]); mpScreen->search(t_off[i])->hide(); @@ -486,19 +502,21 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i mpCircle = JKR_NEW CPaneMgr(mpCenterScreen, MULTI_CHAR('circle_n'), 2, NULL); J2DTextBox* textBox[4]; #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { textBox[0] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n04')); textBox[1] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n05')); textBox[2] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n06')); textBox[3] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n07')); - J2DPane* pane = mpCenterScreen->search(MULTI_CHAR('fitem_n1')); - pane->mVisible = false; - pane = mpCenterScreen->search(MULTI_CHAR('fitem_n2')); - pane->mVisible = false; - pane = mpCenterScreen->search(MULTI_CHAR('fitem_n3')); - pane->mVisible = false; - pane = mpCenterScreen->search(MULTI_CHAR('fitem_n4')); - pane->mVisible = false; + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + J2DPane* pane = mpCenterScreen->search(MULTI_CHAR('fitem_n1')); + pane->mVisible = false; + pane = mpCenterScreen->search(MULTI_CHAR('fitem_n2')); + pane->mVisible = false; + pane = mpCenterScreen->search(MULTI_CHAR('fitem_n3')); + pane->mVisible = false; + pane = mpCenterScreen->search(MULTI_CHAR('fitem_n4')); + pane->mVisible = false; + } } else { textBox[0] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('fitem_n1')); textBox[1] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('fitem_n2')); @@ -1292,17 +1310,11 @@ void dMenu_Ring_c::setScale() { void dMenu_Ring_c::setNameString(u32 i_stringID) { J2DTextBox* textBox[4]; #if TARGET_PC - if (dusk::version::isRegionJpn()) { - textBox[0] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n04')); - textBox[1] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n05')); - textBox[2] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n06')); - textBox[3] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n07')); - } else { - textBox[0] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('fitem_n1')); - textBox[1] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('fitem_n2')); - textBox[2] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('fitem_n3')); - textBox[3] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('fitem_n4')); - } + const bool useDomestic = dusk::version::isJpnOrLessThanWiiJpn(); + textBox[0] = (J2DTextBox*)mpCenterScreen->search(useDomestic ? MULTI_CHAR('item_n04') : MULTI_CHAR('fitem_n1')); + textBox[1] = (J2DTextBox*)mpCenterScreen->search(useDomestic ? MULTI_CHAR('item_n05') : MULTI_CHAR('fitem_n2')); + textBox[2] = (J2DTextBox*)mpCenterScreen->search(useDomestic ? MULTI_CHAR('item_n06') : MULTI_CHAR('fitem_n3')); + textBox[3] = (J2DTextBox*)mpCenterScreen->search(useDomestic ? MULTI_CHAR('item_n07') : MULTI_CHAR('fitem_n4')); #elif VERSION == VERSION_GCN_JPN textBox[0] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n04')); textBox[1] = (J2DTextBox*)mpCenterScreen->search(MULTI_CHAR('item_n05')); @@ -1798,7 +1810,7 @@ void dMenu_Ring_c::drawSelectItem() { if (field_0x674[i] != 0) { #if TARGET_PC mSelectItemSlideElapsed[i] += dusk::game_clock::consume_interval(this); - const f32 u = std::min(mSelectItemSlideElapsed[i] / dusk::game_clock::period_for_original_frames(10.0f), 1.0f); + const f32 u = std::min(mSelectItemSlideElapsed[i] / (dusk::game_clock::kSimPeriod * 10.0f), 1.0f); if (u >= 1.0f) { setSelectItemForce(i); } else { diff --git a/src/d/d_menu_window.cpp b/src/d/d_menu_window.cpp index 7873632265..eb67822899 100644 --- a/src/d/d_menu_window.cpp +++ b/src/d/d_menu_window.cpp @@ -123,7 +123,13 @@ public: #endif } - void setCaptureFlag() { mFlag = 1; } + void setCaptureFlag() { + mFlag = 1; + #ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); + #endif + } + bool checkDraw() { return mFlag; } u8 getAlpha() { return mAlpha; } u8 getTopFlag() { return mTopFlag; } @@ -1092,10 +1098,6 @@ void dMw_c::dMw_ring_create(u8 i_origin) { } mpCapture->setCaptureFlag(); - -#ifdef TARGET_PC - dusk::frame_interp::request_presentation_sync(); -#endif } bool dMw_c::dMw_ring_delete() { diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index 09963e8cc7..865b57be59 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -1386,6 +1386,22 @@ void dMeter2Draw_c::initButtonCross() { dMeter2Info_getString( 0x62, static_cast(mpScreen->search(MULTI_CHAR('cont_ju9')))->getStringPtr(), NULL); +#if TARGET_PC + // These panes are not wide enough for French text (and possibly other languages) + // on Wii PAL. Resize them to always match their counterparts in later releases. + static u64 const juTags[] = { + MULTI_CHAR('cont_ju0'), MULTI_CHAR('cont_ju1'), MULTI_CHAR('cont_ju2'), + MULTI_CHAR('cont_ju3'), MULTI_CHAR('cont_ju4'), MULTI_CHAR('cont_ju5'), + MULTI_CHAR('cont_ju6'), MULTI_CHAR('cont_ju7'), MULTI_CHAR('cont_ju8'), + MULTI_CHAR('cont_ju9'), + }; + + for (u64 tag : juTags) { + J2DPane* pane = mpScreen->search(tag); + pane->resize(120.0f, pane->getHeight()); + } +#endif + mpButtonCrossParent->setAlphaRate(0.0f); drawButtonCross(g_drawHIO.mButtonCrossOFFPosX, g_drawHIO.mButtonCrossOFFPosY); } @@ -3475,7 +3491,7 @@ char* dMeter2Draw_c::getActionString(u8 i_action, u8 i_type, u8* param_2) { } if (param_2 != NULL) { - *param_2 = mesg_entry.output_type; + *param_2 = mesg_entry.draw_type; if (g_drawHIO.mButtonATextActionID == 0x3E6) { *param_2 = 7; @@ -3492,7 +3508,7 @@ char* dMeter2Draw_c::getActionString(u8 i_action, u8 i_type, u8* param_2) { } if (param_2 != NULL) { - *param_2 = mesg_entry.output_type; + *param_2 = mesg_entry.draw_type; if (i_action_num[i_action] == 0x3E6) { *param_2 = 7; diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index 573147f888..56d756ef44 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -1208,9 +1208,11 @@ void dMeterButton_c::screenInitButton() { for (int i = 0; i < 10; i++) { #if TARGET_PC - if (dusk::version::isRegionJpn()) { + if (dusk::version::isJpnOrLessThanWiiJpn()) { mpTextBox[i] = (J2DTextBox*)mpButtonScreen->search(text_tag[i]); - mpButtonScreen->search(ftext_tag[i])->hide(); + if (dusk::version::getGameVersion() >= dusk::version::GameVersion::WiiJpn) { + mpButtonScreen->search(ftext_tag[i])->hide(); + } } else { mpTextBox[i] = (J2DTextBox*)mpButtonScreen->search(ftext_tag[i]); mpButtonScreen->search(text_tag[i])->hide(); diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index f299049f28..46b246264a 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -927,15 +927,15 @@ void jmessage_tMeasureProcessor::do_begin(void const* pEntry, char const* pszTex pReference->setRevoMessageID(0); field_0x38 = 1.0f; - mSeSpeaker = ((JMSMesgEntry_c*)pEntry)->se_speaker; - mSeMood = ((JMSMesgEntry_c*)pEntry)->se_mood; + mSeSpeaker = ((JMSMesgEntry_c*)pEntry)->speaker; + mSeMood = ((JMSMesgEntry_c*)pEntry)->speaker_mood; for (int i = 0; i < DUSK_IF_ELSE((dusk::version::isRegionJpn() ? 30 : D_MSG_CLASS_PAGE_CNT_MAX), D_MSG_CLASS_PAGE_CNT_MAX); i++) { pReference->setLineLength(i, 0.0f, 0.0f); pReference->setPageLine(i, 0); pReference->setPageLineMax(i, 0); pReference->setPageType(i, 0); - pReference->setLineArrange(i, ((JMSMesgEntry_c*)pEntry)->unk_0xd); + pReference->setLineArrange(i, ((JMSMesgEntry_c*)pEntry)->line_alignment); pReference->setLineScale(i, 100); if (pReference->isPlaceName() || pReference->isStaffRoll() || pReference->isBossName() || @@ -948,11 +948,11 @@ void jmessage_tMeasureProcessor::do_begin(void const* pEntry, char const* pszTex } #if TARGET_PC - if (!dusk::version::isRegionJpn() && ((JMSMesgEntry_c*)pEntry)->unk_0xd == 0) { + if (!dusk::version::isRegionJpn() && ((JMSMesgEntry_c*)pEntry)->line_alignment == 0) { pReference->setLineArrange(i, 1); } #elif !REGION_JPN - if (((JMSMesgEntry_c*)pEntry)->unk_0xd == 0) { + if (((JMSMesgEntry_c*)pEntry)->line_alignment == 0) { pReference->setLineArrange(i, 1); } #endif @@ -1896,7 +1896,7 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe mpEntry = pEntry; mpText = pszText; - if (((JMSMesgEntry_c*)pEntry)->fuki_kind == 8) { + if (((JMSMesgEntry_c*)pEntry)->box_kind == 8) { field_0xa8 = g_MsgObject_HIO_c.mDisplaySpeedSpirit; } else { field_0xa8 = g_MsgObject_HIO_c.mDisplaySpeed; @@ -1910,8 +1910,8 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe field_0xb2 = 0; field_0xaa = 0; field_0xac = 0; - field_0xb4 = ((JMSMesgEntry_c*)pEntry)->se_speaker; - field_0xb3 = ((JMSMesgEntry_c*)pEntry)->se_mood; + field_0xb4 = ((JMSMesgEntry_c*)pEntry)->speaker; + field_0xb3 = ((JMSMesgEntry_c*)pEntry)->speaker_mood; jmessage_tReference* pReference = (jmessage_tReference*)getReference(); pReference->resetCharCnt(); @@ -1924,8 +1924,8 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe pReference->setNowTagScale(0); pReference->calcDistance(); - dComIfGp_setMesgAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->base_anm_id); - dComIfGp_setMesgFaceAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->face_anm_id); + dComIfGp_setMesgAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->talk_anim); + dComIfGp_setMesgFaceAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->face_anim); if (dComIfGp_isHeapLockFlag() == 2) { pReference->setFukiPosType(1); @@ -1933,20 +1933,20 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe if (dComIfGp_isHeapLockFlag() == 3) { pReference->setFukiPosType(0); } else { - pReference->setFukiPosType(((JMSMesgEntry_c*)pEntry)->fuki_pos_type); + pReference->setFukiPosType(((JMSMesgEntry_c*)pEntry)->box_position); } } - pReference->setFukiKind(((JMSMesgEntry_c*)pEntry)->fuki_kind); + pReference->setFukiKind(((JMSMesgEntry_c*)pEntry)->box_kind); if (dMsgObject_getMsgOutputType() != 0xFF) { pReference->setForm(dMsgObject_getMsgOutputType()); } else { - pReference->setForm(((JMSMesgEntry_c*)pEntry)->output_type); + pReference->setForm(((JMSMesgEntry_c*)pEntry)->draw_type); } - pReference->setArrange(((JMSMesgEntry_c*)pEntry)->unk_0xd); - pReference->setForm(((JMSMesgEntry_c*)pEntry)->unk_0xd); + pReference->setArrange(((JMSMesgEntry_c*)pEntry)->line_alignment); + pReference->setForm(((JMSMesgEntry_c*)pEntry)->line_alignment); pReference->setMsgID(((JMSMesgEntry_c*)pEntry)->message_id); if (((JMSMesgEntry_c*)pEntry)->event_label_id != 0) { @@ -1980,7 +1980,7 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe if (dMsgObject_getMsgOutputType() != 0xFF) { field_0xae = dMsgObject_getMsgOutputType(); } else { - field_0xae = ((JMSMesgEntry_c*)pEntry)->output_type; + field_0xae = ((JMSMesgEntry_c*)pEntry)->draw_type; } if (mForceForm != 0xFF) { diff --git a/src/d/d_msg_flow.cpp b/src/d/d_msg_flow.cpp index 094189679c..9b4ea2164c 100644 --- a/src/d/d_msg_flow.cpp +++ b/src/d/d_msg_flow.cpp @@ -15,6 +15,37 @@ #include "SSystem/SComponent/c_math.h" #include +#if TARGET_PC +#include "dusk/mods/svc/flow.hpp" + +namespace { + +template +bool resolve_flow_node(u16 index, Node& outNode) { + FlowNodeData data{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), index, data)) { + return false; + } + static_assert(sizeof(Node) == sizeof(data.bytes)); + std::memcpy(&outNode, data.bytes, sizeof(outNode)); + return true; +} + +bool resolve_flow_edge(u16 index, u16& outTarget) { + return dusk::flow::resolve_edge(dMsgObject_getMsgDtPtr(), index, outTarget); +} + +bool resolve_flow_message(u16 index, MessageEntryData& outEntry) { + return dusk::flow::resolve_message_entry(dMsgObject_getMsgDtPtr(), index, outEntry); +} + +u16 message_id(const MessageEntryData& entry) { + return static_cast(static_cast(entry.bytes[4]) << 8 | entry.bytes[5]); +} + +} // namespace +#endif + dMsgFlow_c::dMsgFlow_c() { mNonStopJunpFlowFlag = 0; setInitValue(1); @@ -38,6 +69,11 @@ void dMsgFlow_c::init(fopAc_ac_c* i_partner, int i_flowID, int param_2, fopAc_ac dMsgObject_changeFlowGroup(i_flowID); +#if TARGET_PC + dusk::flow::bind_resource(dMsgObject_getMsgDtPtr(), + i_flowID >= 3000 ? 0 : static_cast(dMsgObject_getGroupID())); +#endif + if (param_2 == 0) { setInitValue(1); @@ -107,17 +143,37 @@ int dMsgFlow_c::checkOpenDoor(fopAc_ac_c* i_speaker_p, int* param_2) { mesg_flow_node_event* event_node = NULL; while ((nodeIdx != 0xFFFF && !var_r27) && !var_r25) { +#if TARGET_PC + FlowNodeData resolvedData{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), nodeIdx, resolvedData)) { + break; + } + u8 type = resolvedData.bytes[0]; +#else u8 type = mFlowNodeTBL[nodeIdx].message.type; +#endif switch(type) { case NODETYPE_MESSAGE_e: { +#if TARGET_PC + mesg_flow_node resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + msg_node = &resolved; +#else msg_node = &mFlowNodeTBL[nodeIdx].message; +#endif nodeIdx = msg_node->next_node_idx; var_r26++; break; } case NODETYPE_BRANCH_e: { +#if TARGET_PC + mesg_flow_node_branch resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + branch_node = &resolved; +#else branch_node = (mesg_flow_node_branch*)&mFlowNodeTBL[nodeIdx].branch; +#endif switch(branch_node->query_idx) { case 0: @@ -130,13 +186,38 @@ int dMsgFlow_c::checkOpenDoor(fopAc_ac_c* i_speaker_p, int* param_2) { break; } +#if TARGET_PC + u16 query_ret; + if (branch_node->query_idx >= dusk::flow::kCustomQueryMin) { + query_ret = dusk::flow::dispatch_query(branch_node->query_idx, i_speaker_p, + branch_node->param, branch_node->result_count, FLOW_QUERY_PHASE_PROBE, nodeIdx); + } else if (branch_node->query_idx < FLOW_QUERY_BUILTIN_COUNT) { + query_ret = + (this->*mQueryList[branch_node->query_idx])(branch_node, i_speaker_p, 0); + } else { + break; + } +#else u16 query_ret = (this->*mQueryList[branch_node->query_idx])(branch_node, i_speaker_p, 0); +#endif u16 spE = branch_node->next_node_idx + query_ret; +#if TARGET_PC + if (!resolve_flow_edge(spE, nodeIdx)) { + nodeIdx = 0xffff; + } +#else nodeIdx = mFlowIdxTBL[spE]; +#endif break; } case NODETYPE_EVENT_e: { +#if TARGET_PC + mesg_flow_node_event resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + event_node = &resolved; +#else event_node = &mFlowNodeTBL[nodeIdx].event; +#endif switch(event_node->event_idx) { case 12: @@ -151,7 +232,13 @@ int dMsgFlow_c::checkOpenDoor(fopAc_ac_c* i_speaker_p, int* param_2) { var_r25 = TRUE; break; default: +#if TARGET_PC + if (!resolve_flow_edge(event_node->next_node_idx, nodeIdx)) { + nodeIdx = 0xffff; + } +#else nodeIdx = mFlowIdxTBL[event_node->next_node_idx]; +#endif break; } break; @@ -311,6 +398,10 @@ void dMsgFlow_c::setInitValueGroupChange(int i_msgNo, fopAc_ac_c** i_talkPartner u16 var_r28 = i_msgNo; dMsgObject_changeFlowGroup(i_msgNo); +#if TARGET_PC + dusk::flow::bind_resource(dMsgObject_getMsgDtPtr(), + i_msgNo >= 3000 ? 0 : static_cast(dMsgObject_getGroupID())); +#endif setInitValue(0); mFlow_p = getMsgDataBlock("FLW1"); @@ -387,7 +478,16 @@ void dMsgFlow_c::setNodeIndex(u16 i_nodeIdx, fopAc_ac_c** i_talkPartners) { dMsgObject_endFlowGroup(); field_0x26 = 1; } else { +#if TARGET_PC + FlowNodeData resolvedData{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), i_nodeIdx, resolvedData)) { + setNodeIndex(0xffff, i_talkPartners); + return; + } + switch (resolvedData.bytes[0]) { +#else switch (mFlowNodeTBL[i_nodeIdx].message.type) { +#endif case 0: break; case NODETYPE_MESSAGE_e: @@ -397,7 +497,13 @@ void dMsgFlow_c::setNodeIndex(u16 i_nodeIdx, fopAc_ac_c** i_talkPartners) { break; case NODETYPE_EVENT_e: mesg_flow_node_event* node = NULL; +#if TARGET_PC + mesg_flow_node_event resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + node = &resolved; +#else node = &mFlowNodeTBL[i_nodeIdx].event; +#endif if (node->event_idx == 21 || node->event_idx == 32 || node->event_idx == 33) { if (node->event_idx == 21) { @@ -447,13 +553,24 @@ int dMsgFlow_c::setSelectMsg(mesg_flow_node* i_flowNode_p, mesg_flow_node* param mesg_flow_node* var_r29 = NULL; +#if TARGET_PC + MessageEntryData selectionEntry{}; + MessageEntryData messageEntry{}; + if (!resolve_flow_message(param_2->msg_index, selectionEntry) || + !resolve_flow_message(i_flowNode_p->msg_index, messageEntry)) + { + return 0; + } + temp_r25 = message_id(selectionEntry); + msg_no = message_id(messageEntry); +#else inf_p = (BE(u16)*)getMsgDataBlock("INF1"); - var_r29 = param_2; temp_r25 = ((inf_p + (var_r29->msg_index) * 10))[10]; var_r29 = i_flowNode_p; msg_no = ((inf_p + (var_r29->msg_index) * 10))[10]; +#endif // "Message Set (Select)" OS_REPORT("\x1B[44;37mメッセ−ジセット(選択)      \x1B[m|:"); @@ -495,9 +612,17 @@ int dMsgFlow_c::setNormalMsg(mesg_flow_node* i_flowNode_p, fopAc_ac_c* i_speaker mesg_flow_node* var_r29 = NULL; u16 msg_no; +#if TARGET_PC + MessageEntryData messageEntry{}; + if (!resolve_flow_message(i_flowNode_p->msg_index, messageEntry)) { + return 0; + } + msg_no = message_id(messageEntry); +#else var_r29 = i_flowNode_p; inf_p = (BE(u16)*)getMsgDataBlock("INF1"); msg_no = (inf_p + (var_r29->msg_index) * 10)[10]; +#endif // "Message Set" OS_REPORT("\x1B[44;37mメッセ−ジセット          \x1B[m|:"); @@ -536,13 +661,45 @@ int dMsgFlow_c::setNormalMsg(mesg_flow_node* i_flowNode_p, fopAc_ac_c* i_speaker int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { mesg_flow_node* flowNode_p = NULL; +#if TARGET_PC + mesg_flow_node resolvedFlowNode{}; + if (!resolve_flow_node(mNodeIdx, resolvedFlowNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + flowNode_p = &resolvedFlowNode; +#else flowNode_p = &mFlowNodeTBL[mNodeIdx].message; +#endif if (field_0x25 != 0) { if (mSelType != SELTYPE_NONE_e) { u16 aNextNodeIndex = flowNode_p->next_node_idx; JUT_ASSERT(1051, 0xFFFF != aNextNodeIndex); +#if TARGET_PC + mesg_flow_node nextNode{}; + if (!resolve_flow_node(aNextNodeIndex, nextNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + if (mSelType == SELTYPE_VERTICAL_e && nextNode.type == NODETYPE_MESSAGE_e) { + if (setSelectMsg(flowNode_p, &nextNode, i_speaker_p)) { + mNodeIdx = aNextNodeIndex; + mSelType = SELTYPE_NONE_e; + field_0x25 = 0; + } + } else if (mSelType == SELTYPE_HORIZONTAL_e && nextNode.type == NODETYPE_BRANCH_e) { + if (setNormalMsg(flowNode_p, i_speaker_p)) { + mSelType = SELTYPE_NONE_e; + field_0x25 = 0; + } + } else { + OS_REPORT("★sel select mesg ===> %d, %d, %d\n", mSelType, aNextNodeIndex, nextNode.type); + setNodeIndex(0xffff, i_talkPartners); + return 1; + } +#else if (mSelType == SELTYPE_VERTICAL_e && mFlowNodeTBL[aNextNodeIndex].message.type == NODETYPE_MESSAGE_e) { JUT_ASSERT(1056, NODETYPE_MESSAGE_e == mFlowNodeTBL[aNextNodeIndex].message.type); if (setSelectMsg(&mFlowNodeTBL[mNodeIdx].message, &mFlowNodeTBL[aNextNodeIndex].message, i_speaker_p)) { @@ -559,8 +716,13 @@ int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPart OS_REPORT("★sel select mesg ===> %d, %d, %d\n", mSelType, aNextNodeIndex, mFlowNodeTBL[aNextNodeIndex].message.type); JUT_ASSERT(1077, FALSE); } +#endif } else { +#if TARGET_PC + if (setNormalMsg(flowNode_p, i_speaker_p)) { +#else if (setNormalMsg(&mFlowNodeTBL[mNodeIdx].message, i_speaker_p)) { +#endif field_0x25 = 0; } } @@ -609,8 +771,20 @@ int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPart case 18: setNodeIndex(flowNode_p->next_node_idx, i_talkPartners); +#if TARGET_PC + if (flowNode_p->next_node_idx == 0xffff) { + return 1; + } + mesg_flow_node resolvedNext{}; + if (!resolve_flow_node(flowNode_p->next_node_idx, resolvedNext)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + mesg_flow_node* var_r26 = &resolvedNext; +#else mesg_flow_node* var_r26 = &mFlowNodeTBL[flowNode_p->next_node_idx].message; - if (var_r26->field_0x1 == 0x15 || var_r26->field_0x1 == 0x20 || var_r26->field_0x1 == 0x21) { +#endif + if (var_r26->subtype == 0x15 || var_r26->subtype == 0x20 || var_r26->subtype == 0x21) { return 0; } @@ -623,23 +797,81 @@ int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPart int dMsgFlow_c::branchNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { mesg_flow_node_branch* node = NULL; +#if TARGET_PC + mesg_flow_node_branch resolvedNode{}; + if (!resolve_flow_node(mNodeIdx, resolvedNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + node = &resolvedNode; + u16 proc_status; + if (node->query_idx >= dusk::flow::kCustomQueryMin) { + proc_status = dusk::flow::dispatch_query(node->query_idx, i_speaker_p, node->param, + node->result_count, FLOW_QUERY_PHASE_EXECUTE, mNodeIdx); + } else if (node->query_idx < FLOW_QUERY_BUILTIN_COUNT) { + proc_status = (this->*mQueryList[node->query_idx])(node, i_speaker_p, 1); + } else { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } +#else node = &mFlowNodeTBL[mNodeIdx].branch; u16 proc_status = (this->*mQueryList[node->query_idx])(node, i_speaker_p, 1); +#endif u16 var_r28 = node->next_node_idx + proc_status; +#if TARGET_PC + u16 target = 0xffff; + resolve_flow_edge(var_r28, target); + setNodeIndex(target, i_talkPartners); +#else setNodeIndex(mFlowIdxTBL[var_r28], i_talkPartners); +#endif return 1; } int dMsgFlow_c::eventNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { mesg_flow_node_event* node = NULL; +#if TARGET_PC + mesg_flow_node_event resolvedNode{}; + if (!resolve_flow_node(mNodeIdx, resolvedNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + node = &resolvedNode; + int proc_status = 1; + if (node->event_idx >= dusk::flow::kCustomEventMin) { + dusk::flow::dispatch_event(node->event_idx, i_speaker_p, node->params); + } else if (node->event_idx < FLOW_EVENT_BUILTIN_COUNT) { + proc_status = (this->*mEventList[node->event_idx])(node, i_speaker_p); + } else { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } +#else node = &mFlowNodeTBL[mNodeIdx].event; int proc_status = (this->*mEventList[node->event_idx])(node, i_speaker_p); +#endif + +#if TARGET_PC + if (node->event_idx >= dusk::flow::kCustomEventMin) { + u16 target = 0xffff; + resolve_flow_edge(node->next_node_idx, target); + setNodeIndex(target, i_talkPartners); + return 1; + } +#endif switch (node->event_idx) { case 8: { getParam(&mEventId, &field_0x30, node->params); +#if TARGET_PC + u16 target = 0xffff; + resolve_flow_edge(node->next_node_idx, target); + setNodeIndex(target, i_talkPartners); +#else setNodeIndex(mFlowIdxTBL[node->next_node_idx], i_talkPartners); +#endif if (field_0x26 != 0) { break; @@ -684,7 +916,13 @@ int dMsgFlow_c::eventNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartne return 0; } default: +#if TARGET_PC + u16 target = 0xffff; + resolve_flow_edge(node->next_node_idx, target); + setNodeIndex(target, i_talkPartners); +#else setNodeIndex(mFlowIdxTBL[node->next_node_idx], i_talkPartners); +#endif } return 1; @@ -705,7 +943,16 @@ int dMsgFlow_c::nodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { aSpeaker_p = i_talkPartners[field_0x38]; } +#if TARGET_PC + FlowNodeData resolvedNode{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), mNodeIdx, resolvedNode)) { + setNodeIndex(0xffff, i_talkPartners); + break; + } + u8 type = resolvedNode.bytes[0]; +#else u8 type = mFlowNodeTBL[mNodeIdx].message.type; +#endif switch (type) { case NODETYPE_MESSAGE_e: proc_status = messageNodeProc(aSpeaker_p, i_talkPartners); diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index fc6b03d6df..3dbaa02dec 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -30,12 +30,15 @@ #include "m_Do/m_Do_lib.h" #if TARGET_PC +#include +#include +#include +#include "dusk/language.hpp" +#include "dusk/logging.h" #include "dusk/menu_pointer.h" +#include "dusk/mods/svc/flow.hpp" #include "dusk/settings.h" #include "dusk/version.hpp" -#include -#include -#include #endif static void dMsgObject_addFundRaising(s16 param_0); @@ -323,7 +326,7 @@ int dMsgObject_c::_create(msg_class* param_1) { field_0x124 = NULL; field_0x100 = param_1; - field_0x16c = -1; + mCurrentGroupID = -1; field_0x16e = -1; mNowTalkFlowNo = 0; mpTalkActor = NULL; @@ -410,7 +413,7 @@ int dMsgObject_c::_create(msg_class* param_1) { field_0x197 = 0; mMessageID = 1000; field_0x158 = mMessageID; - field_0x15c = 0; + mSelectMessageID = 0; field_0x172 = 0; setStatusLocal(1); mpMsgString = JKR_NEW dMsgString_c(); @@ -678,14 +681,14 @@ static u32 getMirrorMsgOverride(u32 msgId) { } #endif -void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 param_2, bool param_3) { +void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 i_selectMsgID, bool param_3) { field_0x158 = revoIndex; revoIndex = getRevoMessageIndex(revoIndex); if (field_0x4cc == 0) { mNoDemoFlag = 1; } mMessageID = revoIndex; - field_0x15c = param_2; + mSelectMessageID = i_selectMsgID; field_0x4d1 = 0; if (mpTalkPartner != field_0x13c && mpTalkPartner != NULL) { dComIfGp_event_setTalkPartner(mpTalkPartner); @@ -700,12 +703,58 @@ void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 param_2, bool param_3) { JMSMesgInfo_c* pMsg = (JMSMesgInfo_c*)((char*)mpMsgDt + 0x20); u8* iVar2 = (u8*)pMsg + pMsg->header.size; - u32 msg_id = getMessageIndex(revoIndex); - dComIfGp_setMesgCameraAttrInfo(pMsg->entries[msg_id].camera_id); - if (field_0x15c == 1000) { - mpRefer->setSelMsgPtr(NULL); +#if TARGET_PC + const void* customEntry = NULL; + const char* customText = NULL; + u16 customGroup = 0; + if (dusk::flow::custom_message_group(static_cast(revoIndex), customGroup)) { + if (dusk::flow::custom_message_for_control( + mpCtrl, static_cast(revoIndex), customEntry, customText)) + { + dComIfGp_setMesgCameraAttrInfo(static_cast(customEntry)[0x0f]); + } } else { - u32 msgIndex = getMessageIndex(field_0x15c); +#endif + u32 msg_id = getMessageIndex(revoIndex); + dComIfGp_setMesgCameraAttrInfo(pMsg->entries[msg_id].camera_attr); +#if TARGET_PC + } + const auto setSelectionMessage = [&] { + const void* selectionEntry = NULL; + const char* selectionText = NULL; + u16 selectionGroup = 0; + if (dusk::flow::custom_message_group(static_cast(mSelectMessageID), selectionGroup)) { + if (dusk::flow::custom_message_for_control( + mpCtrl, static_cast(mSelectMessageID), selectionEntry, selectionText)) + { + mpRefer->setSelMsgPtr(const_cast(selectionText)); + } else { + mpRefer->setSelMsgPtr(NULL); + } + return; + } + u32 msgIndex = getMessageIndex(mSelectMessageID); + if (msgIndex == 0x264) { + mpRefer->setSelMsgPtr(NULL); + return; + } + char* nativeText = (char*)(iVar2 + pMsg->entries[msgIndex].string_offset + 8); + const void* resolvedEntry = &pMsg->entries[msgIndex]; + const char* resolvedText = nativeText; + dusk::flow::resolve_message_for_control(mpCtrl, mpMsgDt, static_cast(msgIndex), + resolvedEntry, nativeText, resolvedEntry, resolvedText); + mpRefer->setSelMsgPtr(const_cast(resolvedText)); + }; +#endif + if (mSelectMessageID == 1000) { + mpRefer->setSelMsgPtr(NULL); +#if TARGET_PC + } else { + setSelectionMessage(); + } +#else + } else { + u32 msgIndex = getMessageIndex(mSelectMessageID); if (msgIndex == 0x264) { mpRefer->setSelMsgPtr(NULL); } else { @@ -713,6 +762,7 @@ void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 param_2, bool param_3) { mpRefer->setSelMsgPtr(my_ptr); } } +#endif if (param_3) { mpCtrl->setMessageID(mMessageID, 0, NULL); } @@ -725,7 +775,7 @@ void dMsgObject_c::setMessageIndexDemo(u32 revoMsgIndex, bool param_2) { field_0x4d4 = 1; dMsgObject_onCameraCancelFlag(); mMessageID = revoMsgIndex; - field_0x15c = 0x264; + mSelectMessageID = 0x264; field_0x4d1 = 0; if (mpTalkPartner != field_0x13c && mpTalkPartner != NULL) { dComIfGp_event_setTalkPartner(mpTalkPartner); @@ -739,8 +789,23 @@ void dMsgObject_c::setMessageIndexDemo(u32 revoMsgIndex, bool param_2) { mpRefer->setPageNum(field_0x172); JMSMesgInfo_c* info_header_p = (JMSMesgInfo_c*)((char*)mpMsgDt + 0x20); JMSMesgInfo_c* reg_25 = (JMSMesgInfo_c*)((char*) info_header_p + info_header_p->header.size); +#if TARGET_PC + const void* customEntry = NULL; + const char* customText = NULL; + u16 customGroup = 0; + if (dusk::flow::custom_message_group(static_cast(revoMsgIndex), customGroup)) { + if (dusk::flow::custom_message_for_control( + mpCtrl, static_cast(revoMsgIndex), customEntry, customText)) + { + dComIfGp_setMesgCameraAttrInfo(static_cast(customEntry)[0x0f]); + } + } else { +#endif int ind = getMessageIndex(revoMsgIndex); - dComIfGp_setMesgCameraAttrInfo(info_header_p->entries[ind].camera_id); + dComIfGp_setMesgCameraAttrInfo(info_header_p->entries[ind].camera_attr); +#if TARGET_PC + } +#endif mpRefer->setSelMsgPtr(NULL); if (param_2) { mpCtrl->setMessageID(mMessageID, 0, NULL); @@ -766,10 +831,21 @@ u32 dMsgObject_c::getMessageIndex(u32 param_0) { } u32 dMsgObject_c::getRevoMessageIndex(u32 param_1) { -#if TARGET_PC - if (!dusk::getSettings().game.enableMirrorMode) { - if (!g_MsgObject_HIO_c.mMessageDisplay) { return param_1; } } - if (param_1 == getMirrorMsgOverride(param_1)) { return param_1; } +#if TARGET_PC + u16 customGroup = 0; + if (param_1 <= 0xffff && + dusk::flow::custom_message_group(static_cast(param_1), customGroup)) + { + return param_1; + } + if (!dusk::getSettings().game.enableMirrorMode) { + if (!g_MsgObject_HIO_c.mMessageDisplay) { + return param_1; + } + } + if (param_1 == getMirrorMsgOverride(param_1)) { + return param_1; + } #else if (!g_MsgObject_HIO_c.mMessageDisplay) { return param_1; } #endif @@ -825,6 +901,14 @@ u32 dMsgObject_c::getMessageIDAlways(u32 param_0) { } s16 dMsgObject_c::getMessageGroup(u32 param_0) { +#if TARGET_PC + u16 customGroup = 0; + if (param_0 <= 0xffff && + dusk::flow::custom_message_group(static_cast(param_0), customGroup)) + { + return static_cast(customGroup); + } +#endif s16 messageGroup = 0; OS_REPORT("getMessgeGroup! msg no====>%d\n", param_0); if (param_0 > 5000) { @@ -851,7 +935,7 @@ void dMsgObject_c::waitProc() { if (mMessageID >= 0x47f && mMessageID <= 0x487) { setMessageIndexDemo(mMessageID, true); } else { - setMessageIndex(mMessageID, field_0x15c, true); + setMessageIndex(mMessageID, mSelectMessageID, true); } } } @@ -1098,7 +1182,7 @@ void dMsgObject_c::continueProc() { field_0x199 = 0; updateEquipBombInfoLocal(); offAutoMessageFlagLocal(); - setMessageIndex(field_0x100->msg_idx, field_0x100->field_0xf0, true); + setMessageIndex(field_0x100->msg_idx, field_0x100->select_msg_idx, true); mpScrnDraw->fukiPosCalc(pRef->getFukiPosType()); SAFE_STRCPY(pRef->getTextPtr(), ""); SAFE_STRCPY(pRef->getTextSPtr(), ""); @@ -1350,7 +1434,7 @@ void dMsgObject_c::endProc() { } mMessageID = 0; field_0x158 = mMessageID; - field_0x15c = 1000; + mSelectMessageID = 1000; field_0x172 = 0; field_0x199 = 0; mpRefer->setPageNum(field_0x172); @@ -1422,12 +1506,22 @@ void dMsgObject_c::talkStartInit() { field_0x19b = 0; bool bVar1 = false; if (mFukiKind != mpRefer->getFukiKind()) { +#if TARGET_PC + // Safety check if MESSAGE_BOX_NOTICE is requested during a conversation + if (mpScrnDraw != NULL && mpRefer->getFukiKind() == 15 && + dComIfGp_isHeapLockFlag() == 5) { + DuskLog.error("MESSAGE_BOX_NOTICE cannot be created during a conversation\n"); + } else { +#endif if (mpScrnDraw != NULL) { delete_screen(false); dVar19 = 1.0f; bVar1 = true; } mFukiKind = mpRefer->getFukiKind(); +#if TARGET_PC + } +#endif } if (dComIfGp_isHeapLockFlag() == 8 || (dComIfGp_isHeapLockFlag() == 5 && dMeter2Info_isFloatingMessageVisible() && !field_0x4cd)) @@ -1731,7 +1825,9 @@ void dMsgObject_c::readMessageGroupLocal(mDoDvdThd_mountXArchive_c** p_arcMount) #endif int msgGroup = dStage_stagInfo_GetMsgGroup(dComIfGp_getStage()->getStagInfo()); - #if REGION_PAL +#if TARGET_PC + snprintf(arcName, sizeof(arcName), "/res/%s/bmgres%d.arc", dusk::language::msg_folder(), msgGroup); +#elif REGION_PAL switch (dComIfGs_getPalLanguage()) { case dSv_player_config_c::LANGUAGE_GERMAN: sprintf(arcName, "/res/Msgde/bmgres%d.arc", msgGroup); @@ -1748,39 +1844,11 @@ void dMsgObject_c::readMessageGroupLocal(mDoDvdThd_mountXArchive_c** p_arcMount) default: sprintf(arcName, "/res/Msguk/bmgres%d.arc", msgGroup); } - #elif REGION_JPN +#elif REGION_JPN sprintf(arcName, "/res/Msgjp/bmgres%d.arc", msgGroup); - #else -#if TARGET_PC - // Original game UB - - if (dusk::version::isRegionPal()) { - switch (dComIfGs_getPalLanguage()) { - case dSv_player_config_c::LANGUAGE_GERMAN: - snprintf(arcName, sizeof(arcName), "/res/Msgde/bmgres%d.arc", msgGroup); - break; - case dSv_player_config_c::LANGUAGE_FRENCH: - snprintf(arcName, sizeof(arcName), "/res/Msgfr/bmgres%d.arc", msgGroup); - break; - case dSv_player_config_c::LANGUAGE_SPANISH: - snprintf(arcName, sizeof(arcName), "/res/Msgsp/bmgres%d.arc", msgGroup); - break; - case dSv_player_config_c::LANGUAGE_ITALIAN: - snprintf(arcName, sizeof(arcName), "/res/Msgit/bmgres%d.arc", msgGroup); - break; - default: - snprintf(arcName, sizeof(arcName), "/res/Msguk/bmgres%d.arc", msgGroup); - } - } else if (dusk::version::isRegionJpn()) { - snprintf(arcName, sizeof(arcName), "/res/Msgjp/bmgres%d.arc", msgGroup); - } else { - snprintf(arcName, sizeof(arcName), "/res/Msgus/bmgres%d.arc", msgGroup); - } - #else sprintf(arcName, "/res/Msgus/bmgres%d.arc", msgGroup); #endif - #endif *p_arcMount = mDoDvdThd_mountXArchive_c::create(arcName, 0, JKRArchive::MOUNT_MEM, NULL); @@ -1807,25 +1875,28 @@ void dMsgObject_c::endFlowGroupLocal() { void dMsgObject_c::changeGroupLocal(s16 param_1) { JKRHeap* prevHeap = mDoExt_setCurrentHeap(dComIfGp_getMsgExpHeap()); - if (field_0x16c != param_1) { + if (mCurrentGroupID != param_1) { if (mFlowChk != 0) { JUT_ASSERT(3688, mFlowChk != 2); mFlowChk = 2; } OS_REPORT("group change =====> %d\n", param_1); if (param_1 >= 1) { - OS_REPORT("bmg data change =====> %d --> %d\n", field_0x16c, param_1); - if (field_0x16c == 0) { + OS_REPORT("bmg data change =====> %d --> %d\n", mCurrentGroupID, param_1); + if (mCurrentGroupID == 0) { field_0x19d = 1; } mpMsgDt = dMeter2Info_getStageMsgResource(); } else { mpMsgDt = mpMsgRes; } - if (field_0x16c >= 0) { + if (mCurrentGroupID >= 0) { field_0x124->parse(mpMsgDt, 0x80); } - field_0x16c = param_1; +#if TARGET_PC + dusk::flow::bind_resource(mpMsgDt, static_cast(param_1)); +#endif + mCurrentGroupID = param_1; } mDoExt_setCurrentHeap(prevHeap); } diff --git a/src/d/d_resorce.cpp b/src/d/d_resorce.cpp index b224343983..97a230b2e0 100644 --- a/src/d/d_resorce.cpp +++ b/src/d/d_resorce.cpp @@ -335,7 +335,7 @@ int dRes_info_c::loadResource() { #endif void* res = mArchive->getIdxResource(fileIndex); #if TARGET_PC - u32 size = mArchive->findIdxResource(fileIndex)->data_size; + u32 size = mArchive->getFileSize(mArchive->findIdxResource(fileIndex)); std::string fileName = mArchive->mStringTable + (mArchive->findIdxResource(fileIndex)->type_flags_and_name_offset & 0xFFFFFF); DuskLog.debug("Loading Resource: {} (Size: {})", fileName, size); @@ -369,7 +369,7 @@ int dRes_info_c::loadResource() { parentHeap = NULL; } - int rt = dComIfG_setObjectRes(arcName, res, entry->data_size, parentHeap); + int rt = dComIfG_setObjectRes(arcName, res, DUSK_IF_ELSE(mArchive->getFileSize(entry),entry->data_size), parentHeap); JUT_ASSERT(788, rt); } else if (nodeType == 'BMDP') { #if DEBUG diff --git a/src/d/d_s_logo.cpp b/src/d/d_s_logo.cpp index 366d87c02b..48c7519ded 100644 --- a/src/d/d_s_logo.cpp +++ b/src/d/d_s_logo.cpp @@ -24,6 +24,8 @@ #include "JSystem/JUtility/JUTConsole.h" #ifdef TARGET_PC +#include "dusk/game_mode.hpp" +#include "dusk/language.hpp" #include "dusk/logging.h" #include "dusk/main.h" #include "dusk/mods/svc/save.hpp" @@ -53,8 +55,8 @@ struct homeBtnData { #if TARGET_PC using namespace dusk::version; -#define LOGO_ARC versionSelect({{GameVersion::GcnJpn, "Logo"}, {GameVersion::GcnPal, "LogoPal"}}, "LogoUs") -#define MSG_PATH versionSelect({{GameVersion::GcnJpn, "/res/Msgjp/bmgres.arc"}}, "/res/Msgus/bmgres.arc") +#define LOGO_ARC regionSelect("LogoUs", "LogoPal", "Logo") +#define MSG_PATH regionSelect("/res/Msgus/bmgres.arc", "/res/Msgus/bmgres.arc", "/res/Msgjp/bmgres.arc") #elif VERSION == VERSION_SHIELD #define LOGO_ARC "LogoUs" #define MSG_PATH "/res/Msgcn/bmgres.arc" @@ -77,7 +79,20 @@ using namespace dusk::version; #define PROGRESSIVE_MODE_ON OS_PROGRESSIVE_MODE_ON #endif -#if PLATFORM_WII || VERSION == VERSION_SHIELD_DEBUG +// TODO: Probably shouldn't actually load LayoutRevo, so I disabled it for now +#if TARGET_PC && 0 +using namespace dusk::version; + +#define FMAP_RES_PATH platformSelect("/res/Layout/fmapres.arc", "/res/LayoutRevo/fmapresR.arc") +#define DMAP_RES_PATH platformSelect("/res/Layout/dmapres.arc", "/res/LayoutRevo/dmapresR.arc") +#define COLLECT_RES_PATH platformSelect("/res/Layout/clctres.arc", "/res/LayoutRevo/clctresR.arc") + +#define MSG_COM_PATH platformSelect("/res/Layout/msgcom.arc", "/res/LayoutRevo/msgcomR.arc") +#define MSG_RES0_PATH platformSelect("/res/Layout/msgres00.arc", "/res/LayoutRevo/msgres00R.arc") +#define MSG_RES1_PATH platformSelect("/res/Layout/msgres01.arc", "/res/LayoutRevo/msgres01R.arc") +#define MSG_RES2_PATH platformSelect("/res/Layout/msgres02.arc", "/res/LayoutRevo/msgres02R.arc") +#define MSG_RES3_PATH platformSelect("/res/Layout/msgres03.arc", "/res/LayoutRevo/msgres03R.arc") +#elif PLATFORM_WII || VERSION == VERSION_SHIELD_DEBUG #define FMAP_RES_PATH "/res/LayoutRevo/fmapresR.arc" #define DMAP_RES_PATH "/res/LayoutRevo/dmapresR.arc" #define COLLECT_RES_PATH "/res/LayoutRevo/clctresR.arc" @@ -99,7 +114,10 @@ using namespace dusk::version; #define MSG_RES3_PATH "/res/Layout/msgres03.arc" #endif -#if PLATFORM_WII || PLATFORM_SHIELD +#if TARGET_PC +#define ICON_RES_PATH "/res/CardIcon/cardicon.arc" +#define PARTICLE_COM_PATH platformSelect("/res/Particle/common.jpc", "/res/Particle/common-r.jpc") +#elif PLATFORM_WII || PLATFORM_SHIELD #define ICON_RES_PATH "/res/WiiBannerIcon/bannerIcon.arc" #define PARTICLE_COM_PATH "/res/Particle/common-r.jpc" #else @@ -107,7 +125,13 @@ using namespace dusk::version; #define PARTICLE_COM_PATH "/res/Particle/common.jpc" #endif -#if PLATFORM_WII +// TODO: Probably shouldn't actually load LayoutRevo, so I disabled it for now +#if TARGET_PC && 0 +#define RING_RES_PATH platformSelect("/res/Layout/ringres.arc", "/res/LayoutRevo/ringresR.arc") +#define ITEM_INF_RES_PATH platformSelect("/res/Layout/itmInfRes.arc", "/res/LayoutRevo/itmInfResR.arc") +#define BUTTON_RES_PATH platformSelect("/res/Layout/button.arc", "/res/LayoutRevo/buttonR.arc") +#define MAIN2D_PATH platformSelect("/res/Layout/main2D.arc", "/res/LayoutRevo/main2DR.arc") +#elif PLATFORM_WII #define RING_RES_PATH "/res/LayoutRevo/ringresR.arc" #define ITEM_INF_RES_PATH "/res/LayoutRevo/itmInfResR.arc" #define BUTTON_RES_PATH "/res/LayoutRevo/buttonR.arc" @@ -785,6 +809,11 @@ void dScnLogo_c::nextSceneChange() { if (status == 1) { dusk::mods::svc::save_slot_loaded( saveSlot, buf + saveSlot * SAVEDATA_SIZE); + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + gameMode->invokeOnSaveLoadedFunction(); + } } dComIfGs_gameStart(); @@ -862,7 +891,7 @@ dScnLogo_c::~dScnLogo_c() { JKR_DELETE(mProgressiveSel); #if TARGET_PC - if (getGameVersion() == GameVersion::GcnPal) { + if (isRegionPal()) { mpPalLogoResCommand->getArchive()->removeResourceAll(); mpPalLogoResCommand->getArchive()->unmount(); mpPalLogoResCommand->destroy(); @@ -974,7 +1003,7 @@ dScnLogo_c::~dScnLogo_c() { mDoExt_setAraCacheSize(free_size - aram_heap->getTotalFreeSize()); #if TARGET_PC - if (getGameVersion() == GameVersion::GcnJpn) { + if (isRegionJpn()) { if (dComIfGp_getFontArchive() != NULL) { dComIfGp_getFontArchive()->unmount(); dComIfGp_setFontArchive(NULL); @@ -1037,7 +1066,7 @@ static int phase_0(dScnLogo_c* i_this) { JKRHEAP_NAME(i_this->mLogo01Heap, "Logo01"); #if TARGET_PC || VERSION == VERSION_GCN_PAL - IF_DUSK_BLOCK(getGameVersion() == GameVersion::GcnPal) + IF_DUSK_BLOCK(isRegionPal()) switch (i_this->getPalLanguage()) { case 1: i_this->mpPalLogoResCommand = mDoDvdThd_mountArchive_c::create("/res/Layout/LogoPalGm.arc", 0, NULL); @@ -1078,7 +1107,7 @@ static int phase_1(dScnLogo_c* i_this) { #endif #if TARGET_PC || VERSION == VERSION_GCN_PAL - IF_DUSK_BLOCK(getGameVersion() == GameVersion::GcnPal) + IF_DUSK_BLOCK(isRegionPal()) if (!mDoDvdThd::SyncWidthSound) { return cPhs_INIT_e; } @@ -1313,7 +1342,7 @@ void dScnLogo_c::logoInitGC() { mDolbyLogo = JKR_NEW dDlst_2D_c(dolbyImg, 189, 150, 232, 112, 255); #if TARGET_PC - if (getGameVersion() == GameVersion::GcnPal) { + if (isRegionPal()) { u8 language = getPalLanguage(); if (language >= 5) { language = 0; @@ -1576,30 +1605,15 @@ void dScnLogo_c::dvdDataLoad() { mpButtonCommand = aramMount(BUTTON_RES_PATH, mDoExt_getJ2dHeap()); mpCardIconCommand = aramMount(ICON_RES_PATH, mDoExt_getJ2dHeap()); - #if TARGET_PC - if (getGameVersion() == GameVersion::GcnPal) { - switch (getPalLanguage()) { - case 1: - mpBmgResCommand = onMemMount("/res/Msgde/bmgres.arc"); - break; - case 2: - mpBmgResCommand = onMemMount("/res/Msgfr/bmgres.arc"); - break; - case 3: - mpBmgResCommand = onMemMount("/res/Msgsp/bmgres.arc"); - break; - case 4: - mpBmgResCommand = onMemMount("/res/Msgit/bmgres.arc"); - break; - case 0: - default: - mpBmgResCommand = onMemMount("/res/Msguk/bmgres.arc"); - break; - } - } else { - mpBmgResCommand = onMemMount(MSG_PATH); - } - #elif VERSION == VERSION_GCN_PAL +#if TARGET_PC +#if AVOID_UB + static char bmgPath[32]; +#else + static char bmgPath[22]; +#endif + snprintf(bmgPath, sizeof(bmgPath), "/res/%s/bmgres.arc", dusk::language::msg_folder()); + mpBmgResCommand = onMemMount(bmgPath); +#elif VERSION == VERSION_GCN_PAL switch (getPalLanguage()) { case 1: mpBmgResCommand = onMemMount("/res/Msgde/bmgres.arc"); @@ -1618,7 +1632,7 @@ void dScnLogo_c::dvdDataLoad() { mpBmgResCommand = onMemMount("/res/Msguk/bmgres.arc"); break; } - #elif VERSION == VERSION_SHIELD_DEBUG +#elif VERSION == VERSION_SHIELD_DEBUG switch (getPalLanguage()) { case 2: mpBmgResCommand = onMemMount("/res/Msgfr/bmgres.arc"); @@ -1630,9 +1644,9 @@ void dScnLogo_c::dvdDataLoad() { mpBmgResCommand = onMemMount("/res/Msgus/bmgres.arc"); break; } - #else +#else mpBmgResCommand = onMemMount(MSG_PATH); - #endif +#endif mpMsgComCommand = aramMount(MSG_COM_PATH, mDoExt_getJ2dHeap()); mpMsgResCommand[0] = aramMount(MSG_RES0_PATH, mDoExt_getJ2dHeap()); @@ -1640,8 +1654,10 @@ void dScnLogo_c::dvdDataLoad() { mpMsgResCommand[2] = aramMount(MSG_RES2_PATH, mDoExt_getJ2dHeap()); mpMsgResCommand[3] = aramMount(MSG_RES3_PATH, mDoExt_getJ2dHeap()); #if TARGET_PC - const auto res4Path = versionSelect({{GameVersion::GcnJpn, "/res/Layout/msgres04.arc"}}, "/res/Layout/msgres04F.arc"); - mpMsgResCommand[4] = aramMount( res4Path, mDoExt_getJ2dHeap()); + const auto res4Path = regionSelect("/res/Layout/msgres04F.arc", + "/res/Layout/msgres04F.arc", + "/res/Layout/msgres04.arc"); + mpMsgResCommand[4] = aramMount(res4Path, mDoExt_getJ2dHeap()); #elif VERSION == VERSION_GCN_JPN mpMsgResCommand[4] = aramMount("/res/Layout/msgres04.arc", mDoExt_getJ2dHeap()); #else @@ -1653,16 +1669,12 @@ void dScnLogo_c::dvdDataLoad() { mpMain2DCommand = onMemMount(MAIN2D_PATH); #if TARGET_PC - const auto fontResPath = versionSelect( - { - {GameVersion::GcnJpn, "/res/Fontjp/fontres.arc"}, - {GameVersion::GcnPal, "/res/Fonteu/fontres.arc"}, - }, "/res/Fontus/fontres.arc"); - const auto fontRubyPath = versionSelect( - { - {GameVersion::GcnJpn, "/res/Fontjp/rubyres.arc"}, - {GameVersion::GcnPal, "/res/Fonteu/rubyres.arc"}, - }, "/res/Fontus/rubyres.arc"); + const auto fontResPath = regionSelect("/res/Fontus/fontres.arc", + "/res/Fonteu/fontres.arc", + "/res/Fontjp/fontres.arc"); + const auto fontRubyPath = regionSelect("/res/Fontus/rubyres.arc", + "/res/Fonteu/rubyres.arc", + "/res/Fontjp/rubyres.arc"); // Note: GCN_JPN mounts this archive as tail instead of head. // I'm guessing this is fine since we have more RAM. diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index d0e33adce4..de66b0317d 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -9,11 +9,6 @@ #include "d/d_com_inf_game.h" #include "d/d_meter2_info.h" #include "d/d_s_name.h" -#include "dusk/imgui/ImGuiConsole.hpp" -#include "dusk/livesplit.h" -#include "dusk/memory.h" -#include "dusk/speedrun.h" -#include "dusk/settings.h" #include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene_mng.h" #include "m_Do/m_Do_Reset.h" @@ -21,7 +16,16 @@ #include "m_Do/m_Do_machine.h" #include "m_Do/m_Do_main.h" #include "m_Do/m_Do_mtx.h" -#include + +#ifdef TARGET_PC +#include "dusk/autosave.h" +#include "dusk/game_mode.hpp" +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/livesplit.h" +#include "dusk/memory.h" +#include "dusk/settings.h" +#include "dusk/speedrun.h" +#endif #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -418,15 +422,6 @@ void dScnName_c::changeGameScene() { dComIfGs_setRestartRoomParam(0); #if TARGET_PC - if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) { - // start a new run on file load if a run isn't already in progress - if (!dusk::m_speedrunInfo.m_isRunStarted) { - dusk::resetForSpeedrunMode(); - dusk::m_speedrunInfo.startRun(); - dusk::speedrun::start(); - } - } - toggleAutoSave(true); #endif } diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index cdd5c467ef..7caba5c9c8 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -28,8 +28,9 @@ #endif #if TARGET_PC -#include "dusk/settings.h" #include +#include "dusk/game_mode.hpp" +#include "dusk/settings.h" #include "helpers/string.hpp" #define strcpy SafeStringCopy @@ -1086,7 +1087,7 @@ void dSv_player_config_c::setVibration(u8 i_status) { u8 dSv_player_config_c::getPalLanguage() const { #if TARGET_PC || VERSION == VERSION_GCN_PAL - IF_DUSK_BLOCK(dusk::version::getGameVersion() == dusk::version::GameVersion::GcnPal) + IF_DUSK_BLOCK(dusk::version::isRegionPal()) switch (OSGetLanguage()) { case 0: return LANGUAGE_ENGLISH; @@ -1828,7 +1829,7 @@ int dSv_info_c::memory_to_card(char* card_ptr, int dataNum) { savedata->getPlayer().getPlayerInfo().setTotalTime(play_time); } - savedata->getPlayer().getPlayerStatusB().setDateIpl(OSGetTime()); + savedata->getPlayer().getPlayerStatusB().setDateIpl(DUSK_IF_ELSE(OSGetSystemTime(), OSGetTime())); memcpy(card_ptr, savedata, sizeof(dSv_save_c)); card_ptr += 0x958; @@ -2035,7 +2036,7 @@ void flagFile_c::listenPropertyEvent(const JORPropertyEvent* i_event) { } case 102: { OSCalendarTime time; - OSTicksToCalendarTime(OSGetTime(), &time); + OSTicksToCalendarTime(DUSK_IF_ELSE(OSGetSystemTime(), OSGetTime()), &time); const char* start_stage_name = dComIfGp_getStartStageName(); char filename[64]; diff --git a/src/d/d_shop_system.cpp b/src/d/d_shop_system.cpp index 5871d65d2a..bf2d3650af 100644 --- a/src/d/d_shop_system.cpp +++ b/src/d/d_shop_system.cpp @@ -905,8 +905,16 @@ int dShopSystem_c::seq_start(fopAc_ac_c* actor, dMsgFlow_c* i_flow) { int itemNo; if (mFlow.getEventId(&itemNo) == 1) { if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { - mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, - -1, NULL, NULL); +#if TARGET_PC + const char* itemCheckName = nullptr; + if (itemNo == dItemNo_HALF_MILK_BOTTLE_e) { + itemCheckName = "sera_reward"; + itemNo = dusk::mods::item_check(itemCheckName, itemNo, actor); + } +#endif + mItemPartnerId = + fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, + NULL IF_DUSK_ARG(dusk::mods::item_give_tag(itemCheckName))); } if (fpcEx_IsExist(mItemPartnerId)) { @@ -1195,8 +1203,12 @@ int dShopSystem_c::seq_decide_yes(fopAc_ac_c* actor, dMsgFlow_c* i_flow) { if (mFlow.getEventId(&itemNo) == 1) { if (i_flow->doFlow(actor, NULL, 0)) { if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { - mItemPartnerId = - fopAcM_createItemForPresentDemo(¤t.pos, itemNo, 0, -1, -1, NULL, NULL); +#if TARGET_PC + const u32 itemGiveTag = dusk::mods::item_give_tag_shop(itemNo & 0xFF); + itemNo = dusk::mods::item_check_shop(itemNo & 0xFF, actor); +#endif + mItemPartnerId = fopAcM_createItemForPresentDemo( + ¤t.pos, itemNo, 0, -1, -1, NULL, NULL IF_DUSK_ARG(itemGiveTag)); } if (fpcEx_IsExist(mItemPartnerId)) { diff --git a/src/dusk/OSThread.cpp b/src/dusk/OSThread.cpp index 7b97026e13..ac86a0ecce 100644 --- a/src/dusk/OSThread.cpp +++ b/src/dusk/OSThread.cpp @@ -509,6 +509,12 @@ BOOL OSJoinThread(OSThread* thread, void** val) { *(s32*)val = (s32)(intptr_t)thread->val; } thread->state = 0; + + { + std::lock_guard mapLock(GetThreadDataMutex()); + GetThreadDataMap().erase(thread); + } + sActiveThreadCount--; return 1; } return 0; diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 6c4de43487..1c8f74f004 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -1,25 +1,26 @@ #include "dusk/achievements.h" -#include "dusk/io.hpp" -#include "dusk/main.h" -#include "d/d_com_inf_game.h" -#include "d/d_item_data.h" -#include "d/d_map_path_fmap.h" -#include "d/d_stage.h" -#include "d/d_menu_fmap.h" #include "JSystem/JKernel/JKRArchive.h" -#include "d/d_meter2_info.h" #include "d/actor/d_a_alink.h" -#include "d/actor/d_a_ni.h" -#include "d/actor/d_a_npc4.h" #include "d/actor/d_a_b_gnd.h" #include "d/actor/d_a_b_ob.h" +#include "d/actor/d_a_ni.h" +#include "d/actor/d_a_npc4.h" #include "d/actor/d_a_player.h" +#include "d/d_com_inf_game.h" #include "d/d_demo.h" +#include "d/d_item_data.h" +#include "d/d_map_path_fmap.h" +#include "d/d_menu_fmap.h" +#include "d/d_meter2_info.h" +#include "d/d_stage.h" +#include "dusk/game_mode.hpp" +#include "dusk/io.hpp" +#include "dusk/logging.h" +#include "dusk/main.h" +#include "dusk/speedrun.h" #include "dusk/ui/ui.hpp" -#include "f_pc/f_pc_name.h" #include "f_op/f_op_actor_mng.h" #include "f_pc/f_pc_name.h" -#include "dusk/logging.h" #include #include @@ -1302,6 +1303,12 @@ void AchievementSystem::processEntry(Entry& e) { } void AchievementSystem::tick() { + // Until we implement an AchievementService, achievements will be unavailible in custom gamemodes + if (dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::gamemode::kVanillaGameModeId) == false + && dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::speedrun::kSpeedrunGameModeId) == false) { + m_signals.clear(); + return; + } if (!m_loaded) { load(); } diff --git a/src/dusk/action_bindings.cpp b/src/dusk/action_bindings.cpp index 68b07d6c0a..c919c9d4fe 100644 --- a/src/dusk/action_bindings.cpp +++ b/src/dusk/action_bindings.cpp @@ -41,6 +41,15 @@ bool isActionBound(ActionBinds action, u32 port) { return getActionBindButton(action, port) != PAD_NATIVE_BUTTON_INVALID; } +bool isActionBoundAnyPort(ActionBinds action) { + for (u32 port = 0; port < PAD_CHANMAX; ++port) { + if (isActionBound(action, port)) { + return true; + } + } + return false; +} + void updateActionBindings() { for (u32 port = 0; port < PAD_CHANMAX; ++port) { // Move the current press to the previous frame diff --git a/src/dusk/action_bindings.h b/src/dusk/action_bindings.h index a71dac5dfe..77ce9cb815 100644 --- a/src/dusk/action_bindings.h +++ b/src/dusk/action_bindings.h @@ -31,6 +31,7 @@ using ActionBindsMap = std::unordered_map; ActionBindsMap& getActionBinds(); bool isActionBound(ActionBinds action, u32 port); +bool isActionBoundAnyPort(ActionBinds action); void updateActionBindings(); diff --git a/src/dusk/audio/DuskAudioSystem.cpp b/src/dusk/audio/DuskAudioSystem.cpp index 3577d3c9c7..3fda98a436 100644 --- a/src/dusk/audio/DuskAudioSystem.cpp +++ b/src/dusk/audio/DuskAudioSystem.cpp @@ -35,7 +35,7 @@ static void SDLCALL GetNewAudio( /** * Render an entire new frame of audio and output it to SDL3. * Note: "audio frames" are unrelated to video frames. - * @return Amount of audio samples rendered. + * @return Amount of audio samples rendered in bytes. */ static int RenderNewAudioFrame(); @@ -121,7 +121,7 @@ int RenderNewAudioFrame() { JASAudioThread::snIntCount -= 1; } - return static_cast(countSubframes) * DSP_SUBFRAME_SIZE; + return static_cast(countSubframes) * sizeof(OutputSubframe); } static void InterleaveOutputData(const OutputSubframe& data, std::span target) { diff --git a/src/dusk/commands.cpp b/src/dusk/commands.cpp index 8af2498035..3077e0bdbd 100644 --- a/src/dusk/commands.cpp +++ b/src/dusk/commands.cpp @@ -245,7 +245,7 @@ void processCameraCommands() { eye = s_cameraFly.endEye; center = s_cameraFly.endCenter; } else { - s_cameraFly.elapsed += dusk::game_clock::sim_pace(); + s_cameraFly.elapsed += dusk::game_clock::kSimPeriod; const float t = smoothstep(s_cameraFly.elapsed / s_cameraFly.duration); eye = s_cameraFly.startEye + (s_cameraFly.endEye - s_cameraFly.startEye) * t; center = s_cameraFly.startCenter + (s_cameraFly.endCenter - s_cameraFly.startCenter) * t; @@ -859,7 +859,7 @@ void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutp output("killall Delete all actors of a type"); output("list [id|name] List actors in scene"); output("pos Print player position and stage"); - output("rate [hz] Get or set sim rate (1-1000, default 30)"); + output("rate [hz] Get or set sim rate (1-480, default 30)"); output("reset Soft reset"); output("rupees [amount] Get or set rupee count"); output("spawn [params] [x y z] [angle] Spawn actor"); diff --git a/src/dusk/dvd_asset.hpp b/src/dusk/dvd_asset.hpp index 8594d4631c..d1ce5f3e39 100644 --- a/src/dusk/dvd_asset.hpp +++ b/src/dusk/dvd_asset.hpp @@ -23,9 +23,20 @@ bool LoadDolAsset(void* dst, std::initializer_list virtualAddress */ bool LoadRelAsset(void* dst, const char* dvdPath, std::initializer_list offset, s32 size); +template +bool LoadRelAsset(T (&dst)[N], const char* dvdPath, std::initializer_list offset) { + return LoadRelAsset(static_cast(dst), dvdPath, offset, static_cast(sizeof(dst))); +} + /** * Load bytes from a REL inside RELS.arc */ bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, std::initializer_list offset, s32 size); +template +bool LoadArchivedRelAsset(T (&dst)[N], u32 memType, const char* relFileName, + std::initializer_list offset) { + return LoadArchivedRelAsset(static_cast(dst), memType, relFileName, offset, static_cast(sizeof(dst))); +} + } // namespace dusk diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index be03d51e96..ca30ec0db5 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -147,6 +147,10 @@ void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step) { g_enabled = mode != FrameInterpMode::Off; g_is_sim_frame = is_sim_frame; g_step = std::clamp(step, 0.0f, 1.0f); + if (!g_enabled) { + g_interpolating = false; + clear_replacements(); + } } bool is_enabled() { diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index bfc60eea49..a38a08c7d2 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -1,115 +1,145 @@ #include "dusk/game_clock.h" #include +#include #include #include -#include #include +#include namespace dusk::game_clock { -using clock = std::chrono::steady_clock; +using native_clock = aurora::time::native_clock; +using game_clock = aurora::time::game_clock; +FrameTiming g_frameTiming; + +namespace { bool s_initialized = false; -clock::time_point s_previous_sample{}; -clock::time_point s_current_snapshot_time{}; +bool s_fixedStepActive = false; +bool s_simTickActive = false; +native_clock::time_point s_previousNativeSample{}; +game_clock::time_point s_latestGameSample{}; +game_clock::time_point s_currentSnapshotTime{}; +game_clock::time_point s_pendingSimTime{}; -std::unordered_map s_interval_last_sample; +std::unordered_map s_intervalLastSample; -float s_sim_rate_hz = 30.0f; -clock::duration s_sim_period_duration = std::chrono::duration_cast(std::chrono::duration(sim_pace())); +constexpr game_clock::duration kSimPeriodDuration = + std::chrono::duration_cast(std::chrono::duration(kSimPeriod)); +constexpr native_clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250); +constexpr int kMaxSimTicksPerFrame = static_cast(aurora::time::kMaximumTimeScale) * 4; +} // namespace -constexpr clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250); -constexpr int kMaxSimTicksPerFrame = 2; - -void ensure_initialized() { +void initialize() { if (s_initialized) { return; } - s_previous_sample = clock::now(); - s_current_snapshot_time = s_previous_sample; + s_previousNativeSample = native_clock::now(); + s_latestGameSample = game_clock::now(); + s_currentSnapshotTime = s_latestGameSample; + s_pendingSimTime = s_latestGameSample; s_initialized = true; } -void reset_frame_timer() { - s_previous_sample = clock::now(); - s_current_snapshot_time = s_previous_sample - s_sim_period_duration; +void reset() { + s_previousNativeSample = native_clock::now(); + s_latestGameSample = game_clock::now(); + s_currentSnapshotTime = s_latestGameSample - kSimPeriodDuration; + s_pendingSimTime = s_currentSnapshotTime; + s_simTickActive = false; } void set_sim_rate(float hz) { - s_sim_rate_hz = std::max(1.0f, std::min(hz, 1000.0f)); - s_sim_period_duration = std::chrono::duration_cast(std::chrono::duration(1.0f / s_sim_rate_hz)); - reset_frame_timer(); + const float maximumHz = aurora::time::kMaximumTimeScale / kSimPeriod; + aurora::time::set_scale(std::clamp(hz, 1.0f, maximumHz) * kSimPeriod); + reset(); } float get_sim_rate() { - return s_sim_rate_hz; + return aurora::time::scale() / kSimPeriod; } -MainLoopPacer advance_main_loop() { - ensure_initialized(); +const FrameTiming& advance() { + const auto nativeNow = native_clock::now(); + const auto gameNow = game_clock::now(); + const auto nativeFrameGap = nativeNow - s_previousNativeSample; + s_previousNativeSample = nativeNow; + s_latestGameSample = gameNow; - const clock::time_point now = clock::now(); - const clock::duration frame_gap = now - s_previous_sample; - const float presentation_dt = std::chrono::duration(frame_gap).count(); - s_previous_sample = now; + auto& out = g_frameTiming; + out = {.dt = std::chrono::duration().count()}; - MainLoopPacer out{}; - out.presentation_dt_seconds = presentation_dt; - out.sim_pace = 1.0f / s_sim_rate_hz; + const float timeScale = aurora::time::scale(); + const bool interpolating = + getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Off; + const bool separatePresentation = interpolating || timeScale != 1.0f; + out.interpolating = interpolating; + out.separatePresentation = separatePresentation; + s_fixedStepActive = separatePresentation; - const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation.getValue() != - dusk::FrameInterpMode::Off && - !dusk::getTransientSettings().skipFrameRateLimit; - out.is_interpolating = should_interpolate; - - if (!should_interpolate) { - s_current_snapshot_time = now; - out.sim_ticks_to_run = 1; + if (!separatePresentation) { + s_currentSnapshotTime = gameNow; + out.numSimTicks = 1; return out; } - if (frame_gap > kAbnormalGapResetThreshold) { - s_current_snapshot_time = now - s_sim_period_duration; - out.sim_ticks_to_run = 0; + const auto simulationTarget = interpolating ? gameNow - kSimPeriodDuration : gameNow; + if (timeScale == 0.f || nativeFrameGap > kAbnormalGapResetThreshold) { + s_currentSnapshotTime = simulationTarget; + out.numSimTicks = 0; return out; } - int sim_ticks_to_run = 0; - clock::time_point projected_snapshot_time = s_current_snapshot_time; - const clock::time_point render_time = now - s_sim_period_duration; - while (sim_ticks_to_run < kMaxSimTicksPerFrame && projected_snapshot_time < render_time) { - projected_snapshot_time += s_sim_period_duration; - sim_ticks_to_run++; + int numSimTicks = 0; + auto projectedSnapshotTime = s_currentSnapshotTime; + while (numSimTicks < kMaxSimTicksPerFrame) { + const bool tickDue = interpolating ? + projectedSnapshotTime < simulationTarget : + projectedSnapshotTime + kSimPeriodDuration <= simulationTarget; + if (!tickDue) { + break; + } + projectedSnapshotTime += kSimPeriodDuration; + numSimTicks++; } - out.sim_ticks_to_run = sim_ticks_to_run; + out.numSimTicks = numSimTicks; return out; } +void begin_sim_tick() { + s_pendingSimTime = + s_fixedStepActive ? s_currentSnapshotTime + kSimPeriodDuration : s_latestGameSample; + s_simTickActive = true; +} + void commit_sim_tick() { - ensure_initialized(); - s_current_snapshot_time += s_sim_period_duration; + if (s_simTickActive) { + s_currentSnapshotTime = s_pendingSimTime; + s_simTickActive = false; + } else { + s_currentSnapshotTime += kSimPeriodDuration; + } } float sample_interpolation_step() { - ensure_initialized(); const float step = - std::chrono::duration(clock::now() - s_current_snapshot_time).count() / (1.0f / s_sim_rate_hz); + std::chrono::duration(game_clock::now() - s_currentSnapshotTime).count() / + kSimPeriod; return std::clamp(step, 0.0f, 1.0f); } float consume_interval(const void* consumer) { - ensure_initialized(); - const uintptr_t key = reinterpret_cast(consumer); - const clock::time_point now = clock::now(); - - float dt = ui_initial_dt(); - const auto it = s_interval_last_sample.find(key); - if (it != s_interval_last_sample.end()) { + const auto key = reinterpret_cast(consumer); + const auto now = s_simTickActive ? s_pendingSimTime : game_clock::now(); + const float timeScale = aurora::time::scale(); + float dt = kUiInitialDt * timeScale; + if (const auto it = s_intervalLastSample.find(key); it != s_intervalLastSample.end()) { dt = std::chrono::duration(now - it->second).count(); - dt = std::min(dt, ui_maximum_dt()); + const float maximumDt = std::max(kUiMaximumDt * timeScale, kSimPeriod); + dt = std::min(dt, maximumDt); } - s_interval_last_sample[key] = now; + s_intervalLastSample[key] = now; return dt; } diff --git a/src/dusk/game_clock.h b/src/dusk/game_clock.h index 6ad9c2c752..5e71ea4c0f 100644 --- a/src/dusk/game_clock.h +++ b/src/dusk/game_clock.h @@ -2,28 +2,33 @@ namespace dusk::game_clock { -void ensure_initialized(); -void reset_frame_timer(); +// Amount of time that a simulation tick advances +constexpr float kSimPeriod = 1.0f / 30.0f; +constexpr float kUiMaximumDt = 0.05f; +constexpr float kUiInitialDt = 1.0f / 60.0f; -constexpr float sim_pace() { return 1.0f / 30.0f; } -constexpr float period_for_original_frames(float frame_count) { return frame_count * sim_pace(); } -constexpr float ui_maximum_dt() { return 0.05f; } -constexpr float ui_initial_dt() { return 1.0f / 60.0f; } - -struct MainLoopPacer { - float presentation_dt_seconds; - bool is_interpolating; - int sim_ticks_to_run; - float sim_pace; +struct FrameTiming { + // Amount of time elapsed in seconds since the last advance + float dt; + // Whether interpolation is active + bool interpolating; + // Run simulation and presentation separately (for interpolation or time scaling) + bool separatePresentation; + // Number of simulation ticks to run + int numSimTicks; }; +extern FrameTiming g_frameTiming; -MainLoopPacer advance_main_loop(); +void initialize(); +void reset(); +const FrameTiming& advance(); +void begin_sim_tick(); void commit_sim_tick(); float sample_interpolation_step(); float consume_interval(const void* consumer); -// Runtime sim rate override (default 30 hz). Resets the frame timer. +// Sets the effective simulation rate through the game clock time scale. void set_sim_rate(float hz); float get_sim_rate(); diff --git a/src/dusk/game_mode.cpp b/src/dusk/game_mode.cpp new file mode 100644 index 0000000000..8fdb3106f3 --- /dev/null +++ b/src/dusk/game_mode.cpp @@ -0,0 +1,97 @@ +#include "dusk/game_mode.hpp" +#include "JSystem/JUtility/JUTGamePad.h" +#include "aurora/lib/logging.hpp" +#include "dusk/config.hpp" +#include "dusk/ui/prelaunch.hpp" +#include "m_Do/m_Do_MemCard.h" + +namespace dusk::gamemode { +namespace { +aurora::Module Log("dusk::gamemode"); +} + +GameModeManager g_GameModeManager; + +GameModeManager::GameModeManager() { + GameMode vanilla{kVanillaGameModeId, "Vanilla"}; + mRegisteredGameModes.emplace(vanilla.getId(), std::move(vanilla)); + mCurrentGameModeId = kVanillaGameModeId; +} + +void GameModeManager::setGameModeToPrevious() { + // Restore the previously selected game mode if still registered. + GameModeId id = getSettings().game.lastSelectedGameModeId; + if (!mRegisteredGameModes.contains(id)) { + setCurrentGameMode(kVanillaGameModeId); + return; + } + setCurrentGameMode(id); +} + +void GameModeManager::registerGameMode(const GameMode& gameMode) { + if (gameMode.getId().empty()) { + Log.fatal("No game mode ID specified in GameModeManager::registerGameMode"); + } + if (gameMode.getFullName().empty()) { + Log.fatal("No display name specified for game mode {}", gameMode.getId()); + } + + if (mRegisteredGameModes.contains(gameMode.getId())) { + Log.warn("Attempting to re-register existing game mode {}", gameMode.getId()); + return; + } + + mRegisteredGameModes.emplace(gameMode.getId(), gameMode); + ui::Prelaunch::refresh_menu_buttons(); +} + +void GameModeManager::unregisterGameMode(const GameModeId& gameModeId) { + const auto& it = mRegisteredGameModes.find(gameModeId); + if (it == mRegisteredGameModes.end()) { + Log.warn("Attempting to unregister unknown game mode {}", gameModeId); + return; + } + + if (mCurrentGameModeId == gameModeId) { + // Reset to prelaunch before unloading callbacks belonging to the active mod. + ui::prelaunch_state().returnToPrelaunchOnReset = true; + JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; + setCurrentGameMode(kVanillaGameModeId); + } + mRegisteredGameModes.erase(it); + ui::Prelaunch::refresh_menu_buttons(); +} + +bool GameModeManager::setCurrentGameMode(const GameModeId& id) { + if (mCurrentGameModeId == id) { + return true; + } + if (!mRegisteredGameModes.contains(id)) { + Log.warn("Attempting to configure unknown game mode {}", id); + return false; + } + const GameMode* currentGameMode = getCurrentGameMode(); + if (currentGameMode) { + currentGameMode->invokeOnDeactivatedFunction(); + } + mCurrentGameModeId = id; + + currentGameMode = getCurrentGameMode(); + if (currentGameMode) { + mDoMemCd_SetFileName(currentGameMode->getSaveName()); + if (!currentGameMode->invokeOnActivatedFunction()) { + mCurrentGameModeId = kVanillaGameModeId; + currentGameMode = getCurrentGameMode(); + mDoMemCd_SetFileName(currentGameMode->getSaveName()); + currentGameMode->invokeOnActivatedFunction(); + getSettings().game.lastSelectedGameModeId.setValue(kVanillaGameModeId); + config::save(); + return false; + } + } + getSettings().game.lastSelectedGameModeId.setValue(id); + config::save(); + return true; +} + +} // namespace dusk::gamemode diff --git a/src/dusk/game_mode.hpp b/src/dusk/game_mode.hpp new file mode 100644 index 0000000000..3c2b63a484 --- /dev/null +++ b/src/dusk/game_mode.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include "d/d_file_select.h" +#include "mods/svc/game_mode.h" + +#include +#include +#include +#include + +namespace dusk::gamemode { +using GameModeId = std::string; + +constexpr const char* kVanillaGameModeId = "vanilla"; +constexpr const char* kDefaultGameModeSaveName = "gczelda2"; + +// Holds a game mode definition and its lifecycle callbacks. +class GameMode { +public: + using Callback = std::function; + using NewSaveSelectCallback = std::function; + + GameMode(GameModeId id, std::string fullName, std::string saveName = {}) + : mId{std::move(id)}, mFullName{std::move(fullName)}, + mSaveName{saveName.empty() ? kDefaultGameModeSaveName : std::move(saveName)} {} + const GameModeId& getId() const { return mId; } + const std::string& getFullName() const { return mFullName; } + const std::string& getSaveName() const { return mSaveName; } + + GameModeId mId; + std::string mFullName; + std::string mSaveName; + + bool invokeOnActivatedFunction() const { + if (mOnActivatedFunction) { + return mOnActivatedFunction(); + } + return true; + } + + bool invokeOnDeactivatedFunction() const { + if (mOnDeactivatedFunction) { + return mOnDeactivatedFunction(); + } + return true; + } + + bool invokeOnPlayFunction() const { + if (mOnPlayFunction) { + return mOnPlayFunction(); + } + return true; + } + + bool invokeOnSaveLoadedFunction() const { + if (mOnSaveLoadedFunction) { + return mOnSaveLoadedFunction(); + } + return true; + } + + bool invokeOnNewSaveFunction() const { + if (mOnNewSaveFunction) { + return mOnNewSaveFunction(); + } + return true; + } + + bool invokeOnNewSaveSelectFunction(GameModeNewSaveState* state) const { + *state = GAME_MODE_STATE_PENDING; + if (mOnNewSaveSelectFunction) { + if (mOnNewSaveSelectFunction(state)) { + return true; + } + *state = GAME_MODE_STATE_RETURN; + return false; + } + *state = GAME_MODE_STATE_PROCEED; + return true; + } + + bool invokeOnGameResetFunction() const { + if (mOnGameResetFunction) { + return mOnGameResetFunction(); + } + return true; + } + + bool invokeOnTickFunction() const { + if (mOnTickFunction) { + return mOnTickFunction(); + } + return true; + } + + Callback mOnActivatedFunction; + Callback mOnDeactivatedFunction; + Callback mOnPlayFunction; + Callback mOnSaveLoadedFunction; + Callback mOnNewSaveFunction; + NewSaveSelectCallback mOnNewSaveSelectFunction; + Callback mOnGameResetFunction; + Callback mOnTickFunction; +}; + +class GameModeManager { +public: + GameModeManager(); + void registerGameMode(const GameMode& gameMode); + void unregisterGameMode(const GameModeId& gameModeId); + + const GameMode* getCurrentGameMode() const { + const auto& it = mRegisteredGameModes.find(mCurrentGameModeId); + return it != mRegisteredGameModes.end() ? &it->second : + &mRegisteredGameModes.at(kVanillaGameModeId); + } + bool isCurrentGameMode(const GameModeId& id) const { + const GameMode* gameMode = getCurrentGameMode(); + if (gameMode && gameMode->getId() == id) { + return true; + } + return false; + } + bool setCurrentGameMode(const GameModeId& id); + void setGameModeToPrevious(); + + const std::map& getRegisteredGameModes() const { + return mRegisteredGameModes; + } + +private: + GameModeId mCurrentGameModeId; + std::map mRegisteredGameModes; +}; + +extern GameModeManager g_GameModeManager; + +inline GameModeManager& getGameModeManager() { + return g_GameModeManager; +} + +} // namespace dusk::gamemode diff --git a/src/dusk/hq_minimap.cpp b/src/dusk/hq_minimap.cpp new file mode 100644 index 0000000000..a4d73cbcf1 --- /dev/null +++ b/src/dusk/hq_minimap.cpp @@ -0,0 +1,210 @@ +#include "aurora/texture.hpp" +#include "dusk/texture_replacements.hpp" +#include "fmt/format.h" + +#include +#include +#include +#include + +namespace { + +constexpr u16 kMapIconResolutionMultiplier = 4; +constexpr u16 kMapImageSide = 16 * kMapIconResolutionMultiplier; +constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide; + +// give higher priority to user and mod replacements +constexpr auto kInternalTextureReplacementPriority = dusk::texture_replacements::kUserTextureReplacementPriority - 1; + +typedef std::function PaintI8Fn; + +enum class ArcIndex : int { + Circle16 = 82, // map_icon_circle16x16_4i.bti - simple circle + Circle = 76, // im_map_icon_circle_4i.bti - outlined circle + Nijumaru = 78, // im_map_icon_nijumaru_4i.bti - concentric rings + Enter = 77, // im_map_icon_enter_4i.bti - outlined octagram + TryForce = 81, // im_map_icon_try_force_4i.bti - outlined circle with triangle +}; + +struct Replacement { + ArcIndex index; + PaintI8Fn painter; +}; + +struct Icon { + u8* origData = nullptr; + std::unique_ptr newData; + std::string label; + std::optional reg; +}; + +bool s_initialized = false; +bool s_active = false; +std::unordered_map s_icons; + +void paint_i8(std::span dst, size_t width, PaintI8Fn paint) { + assert(width % 8 == 0 && dst.size() % 32 == 0); + + const auto blocksAcross = width >> 3; + + for (size_t i = 0; i < dst.size(); i++) { + // 8x4 block swizzling for I8 + const auto blockIdx = i >> 5; + const auto localIdx = i & 31; + + const auto blockY = blockIdx / blocksAcross; + const auto blockX = blockIdx % blocksAcross; + + const auto localY = localIdx >> 3; + const auto localX = localIdx & 7; + + const auto x = (blockX << 3) + localX; + const auto y = (blockY << 2) + localY; + + dst[i] = paint(x, y); + } +} + +void draw_all_replacements() { + constexpr auto center = kMapImageSide / 2.0f; + constexpr auto radiusSq = center * center; + + // clang-format off + const auto replacements = std::to_array({ + { + ArcIndex::Circle16, + [=](auto x, auto y) { + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + return (dx * dx + dy * dy < radiusSq) ? 0x11 : 0; + } + }, + { + ArcIndex::Circle, + [=](auto x, auto y) { + constexpr auto innerRadius = kMapImageSide * 3.0f / 8.0f; + constexpr auto innerRadiusSq = innerRadius * innerRadius; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + + return dSq < radiusSq ? (dSq < innerRadiusSq ? 0x22 : 0x11) : 0; + } + }, + { + ArcIndex::Nijumaru, + [=](auto x, auto y) { + constexpr u8 nijumaruRings[] = {0x11, 0x22, 0x11, 0x11, 0x22, 0x22}; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + + if (dSq < radiusSq) { + auto ringIndex = static_cast(std::trunc(std::sqrt(dSq) / kMapImageSide * 12)); + ringIndex = std::min(ringIndex, sizeof(nijumaruRings) - 1); + return nijumaruRings[ringIndex]; + } + return u8{0}; + } + }, + { + ArcIndex::Enter, + [=](auto x, auto y) { + constexpr auto outlineWidth = kMapImageSide / 6.0f; + + const auto adx = std::abs((x + 0.5f) - center); + const auto ady = std::abs((y + 0.5f) - center); + const auto dist = + std::min(adx + ady, std::max(adx, ady) * std::numbers::sqrt2_v) - + kMapImageSide / 2.0f; + + return dist > 0.0f ? 0 : (dist > -outlineWidth ? 0x22 : 0x33); + } + }, + { + ArcIndex::TryForce, + [=](auto x, auto y) { + constexpr auto innerRadiusNorm = 5.0f / 12.0f; + constexpr auto innerRadius = kMapImageSide * innerRadiusNorm; + constexpr auto innerRadiusSq = innerRadius * innerRadius; + constexpr auto triRadius = kMapImageSide * innerRadiusNorm / 2.0f; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + const auto triSideDist = (std::numbers::sqrt3_v * std::abs(dx) - dy) * 0.5f; + const auto insideTri = std::max(dy, triSideDist) < triRadius; + + return insideTri ? 0x22 : (dSq < radiusSq ? (dSq < innerRadiusSq ? 0x33 : 0x22) : 0); + } + } + }); + // clang-format on + + for (const auto r : replacements) { + auto pixels = std::make_unique_for_overwrite(kMapImageTotalPixels); + paint_i8(std::span{pixels.get(), kMapImageTotalPixels}, kMapImageSide, r.painter); + + auto& icon = s_icons[static_cast(r.index)]; + icon.newData = std::move(pixels); + icon.label = fmt::format("hq minimap icon {}", static_cast(r.index)); + } +} + +} // namespace + +namespace dusk::hq_minimap { + +void register_pointer(int idx, u8* ptr) { + if (s_initialized) { + return; + } + + s_icons[idx].origData = ptr; +} + +void set_active(bool active) { + s_active = active; +} + +void update() { + if (!s_initialized) { + return; + } + + for (auto& [idx, icon] : s_icons) { + const bool shouldBeRegistered = s_active && icon.origData && icon.newData; + + if (shouldBeRegistered && !icon.reg) { + aurora::texture::ReplacementKey key{aurora::texture::TexturePointerKey{icon.origData}}; + aurora::texture::RawTextureReplacement repl{ + .bytes = std::span{icon.newData.get(), kMapImageTotalPixels}, + .width = kMapImageSide, + .height = kMapImageSide, + .mipCount = 1, + .gxFormat = GX_TF_I8, + .label = icon.label, + }; + icon.reg = aurora::texture::register_replacement( + key, repl, {.priority = kInternalTextureReplacementPriority}); + } else if (!shouldBeRegistered && icon.reg) { + aurora::texture::unregister_replacement(*icon.reg); + icon.reg.reset(); + } + } +} + +void initialize_if_needed() { + if (s_initialized) { + return; + } + + draw_all_replacements(); + s_initialized = true; + + update(); +} + +} // namespace dusk::hq_minimap diff --git a/src/dusk/hq_minimap.hpp b/src/dusk/hq_minimap.hpp new file mode 100644 index 0000000000..5ca89ac7d1 --- /dev/null +++ b/src/dusk/hq_minimap.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace dusk::hq_minimap { + +/// Adds a mapping of an image resource index (into `Always.arc`) to the pointer to its image data. +/// If `initialize_if_needed` has been called, this is a no-op. Pointers are expected to be stable +/// and valid for the program's entire lifetime. +void register_pointer(int idx, u8* ptr); + +/// Sets whether HQ minimap texture replacements should be active or not. Does not manage +/// replacement registrations itself; see `update`. +void set_active(bool active); + +/// Registers or unregisters texture replacements depending on active state. +void update(); + +/// Called once after registering image pointers, in which their HQ replacements are procedurally +/// drawn and `update` is called. Further calls are no-ops. +void initialize_if_needed(); + +} diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 21201492de..d15e1c1b8e 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -1,14 +1,14 @@ #include #include +#include +#include #include #include -#include #define IMGUI_DEFINE_MATH_OPERATORS #include "imgui.h" #include -#include "fmt/format.h" #include "ImGuiConsole.hpp" #include "ImGuiEngine.hpp" #include "JSystem/JUtility/JUTGamePad.h" @@ -18,12 +18,15 @@ #include "dusk/data.hpp" #include "dusk/dusk.h" #include "dusk/frame_interpolation.h" +#include "dusk/game_mode.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" +#include "dusk/presentation.hpp" #include "dusk/settings.h" #include "dusk/ui/ui.hpp" #include "f_pc/f_pc_manager.h" #include "f_pc/f_pc_name.h" +#include "fmt/format.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_main.h" #include "tracy/Tracy.hpp" @@ -37,6 +40,8 @@ using namespace std::string_literals; using namespace std::string_view_literals; namespace { +constexpr float kTurboTimeScale = 4.f; + ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) { while (window != nullptr) { const bool canScrollX = window->ScrollMax.x > 0.0f; @@ -235,9 +240,30 @@ namespace dusk { } void ImGuiConsole::UpdateSettings() { - getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && - (ImGui::IsKeyDown(ImGuiKey_Tab) || getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON)); + static bool previousTurboActive = false; + static bool previousSlowActive = false; + static float previousTimeScale = 1.0f; + const bool turboBound = isActionBoundAnyPort(ActionBinds::TURBO_SPEED_BUTTON); + const bool turboActive = + getSettings().game.enableTurboKeybind && + (turboBound ? getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON) : + ImGui::IsKeyDown(ImGuiKey_Tab)); + const bool slowDown = turboActive && ImGui::GetIO().KeyShift; + if (turboActive != previousTurboActive) { + getTransientSettings().turboMode = turboActive; + presentation::update_frame_rate_preference(); + if (turboActive) { + previousTimeScale = aurora_get_timescale(); + aurora_set_timescale(slowDown ? 1.f / kTurboTimeScale : kTurboTimeScale); + } else { + aurora_set_timescale(previousTimeScale); + } + } else if (turboActive && slowDown != previousSlowActive) { + aurora_set_timescale(slowDown ? 1.f / kTurboTimeScale : kTurboTimeScale); + } + previousTurboActive = turboActive; + previousSlowActive = slowDown; } void ImGuiConsole::PreDraw() { @@ -264,7 +290,7 @@ namespace dusk { m_isHidden = true; } } - + bool showMenu = !m_isHidden; // The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg, @@ -279,7 +305,7 @@ namespace dusk { if (dusk::IsGameLaunched && !m_isLaunchInitialized) { m_isLaunchInitialized = true; - if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) { + if (dusk::speedrun::isActive() && getSettings().game.liveSplitEnabled) { dusk::speedrun::connectLiveSplit(); } } @@ -351,7 +377,7 @@ namespace dusk { m_menuTools.ShowInputViewer(); - if (dusk::IsGameLaunched && !dusk::getSettings().game.speedrunMode) { + if (dusk::IsGameLaunched && !dusk::speedrun::isActive()) { m_menuTools.ShowDebugOverlay(); m_menuTools.ShowCameraOverlay(); m_menuTools.ShowProcessManager(); diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 0e161b0b4b..7657c48815 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -14,6 +14,7 @@ #include "d/d_com_inf_game.h" #include "dusk/data.hpp" #include "dusk/dusk.h" +#include "dusk/speedrun.h" #include "dusk/main.h" #include "dusk/os.h" #include "m_Do/m_Do_main.h" @@ -25,10 +26,6 @@ #include #endif -namespace aurora::gx { -extern bool enableLodBias; -} - namespace dusk { ImGuiMenuTools::ImGuiMenuTools() {} @@ -38,7 +35,7 @@ namespace dusk { ImGui::BeginDisabled(); } - ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::BeginDisabled(dusk::speedrun::isActive()); ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); @@ -60,7 +57,7 @@ namespace dusk { } if (ImGui::BeginMenu("Debug")) { - ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::BeginDisabled(dusk::speedrun::isActive()); bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { @@ -76,7 +73,6 @@ namespace dusk { getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction); config::save(); } - ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); ImGui::EndMenu(); } diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp index 380c1860a1..26fdef11e9 100644 --- a/src/dusk/iso_validate.cpp +++ b/src/dusk/iso_validate.cpp @@ -40,10 +40,25 @@ constexpr auto AcceptedDiscs = std::to_array({ .gameId = "GZ2P01", .expectedHash = borealis::disc::parse_xxh3_128("9ef597588b0035ca9e91b333fa9a8a7e"), }, + { + .gameId = "RZDE01", .revision = 0, + .expectedHash = borealis::disc::parse_xxh3_128("b3d91fbea59e5c66934d04c01566728e"), + }, + { + .gameId = "RZDE01", .revision = 2, + .expectedHash = borealis::disc::parse_xxh3_128("c3ec420921a1b36d6ae43f576491d25c"), + }, + { + .gameId = "RZDJ01", + .expectedHash = borealis::disc::parse_xxh3_128("d3866821c7fc6999e6e8bbef8b6875aa"), + }, + { + .gameId = "RZDP01", + .expectedHash = borealis::disc::parse_xxh3_128("6095a924a57e5fb4294ac96fb85a09a1"), + }, }); -constexpr auto RecognizedGameIds = - std::to_array({"RZDE01", "RZDJ01", "RZDK01", "RZDP01"}); +constexpr auto RecognizedGameIds = std::to_array({"RZDK01"}); constexpr borealis::disc::Catalog DiscCatalog{ .acceptedDiscs = AcceptedDiscs, @@ -92,6 +107,7 @@ void update_info(const borealis::disc::Result& result, DiscInfo& info) noexcept if (!result.metadata.gameId.empty()) { info.platform = result.metadata.platform; info.region = region_from_game_id(result.metadata.gameId); + info.revision = result.metadata.revision; } } diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp index b9267a6a64..e5a8eac1ef 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -1,6 +1,7 @@ #ifndef DUSK_ISO_VALIDATE_HPP #define DUSK_ISO_VALIDATE_HPP +#include "dusk/settings.h" #include #include @@ -37,6 +38,7 @@ using VerificationStatus = borealis::disc::Progress; struct DiscInfo { Platform platform = Platform::Unknown; Region region = Region::NorthAmerica; + std::uint8_t revision = 0; }; ValidationError inspect(const char* path, DiscInfo& info); diff --git a/src/dusk/language.cpp b/src/dusk/language.cpp new file mode 100644 index 0000000000..62d30df816 --- /dev/null +++ b/src/dusk/language.cpp @@ -0,0 +1,79 @@ +#include "dusk/language.hpp" + +#include "dusk/version.hpp" + +namespace dusk::language { +namespace { + +constexpr GameLanguage kEnglishOnly[] = {GameLanguage::English}; +constexpr GameLanguage kJapaneseOnly[] = {GameLanguage::Japanese}; +constexpr GameLanguage kPalLanguages[] = { + GameLanguage::English, + GameLanguage::German, + GameLanguage::French, + GameLanguage::Spanish, + GameLanguage::Italian, +}; +constexpr GameLanguage kWiiUsaLanguages[] = { + GameLanguage::English, + GameLanguage::French, + GameLanguage::Spanish, +}; + +} // namespace + +std::span available_languages(const iso::DiscInfo& info) noexcept { + switch (info.region) { + case iso::Region::Japan: + return kJapaneseOnly; + case iso::Region::Europe: + return kPalLanguages; + case iso::Region::NorthAmerica: + if (info.platform == iso::Platform::Wii && info.revision == 2) { + return kWiiUsaLanguages; + } + return kEnglishOnly; + default: + return kEnglishOnly; + } +} + +const char* language_name(GameLanguage language) noexcept { + switch (language) { + case GameLanguage::English: + return "English"; + case GameLanguage::German: + return "German"; + case GameLanguage::French: + return "French"; + case GameLanguage::Spanish: + return "Spanish"; + case GameLanguage::Italian: + return "Italian"; + case GameLanguage::Japanese: + return "Japanese"; + } + return "English"; +} + +const char* msg_folder() noexcept { + using namespace version; + + switch (getSettings().game.language.getValue()) { + case GameLanguage::German: + return "Msgde"; + case GameLanguage::French: + return "Msgfr"; + case GameLanguage::Spanish: + return "Msgsp"; + case GameLanguage::Italian: + return "Msgit"; + case GameLanguage::Japanese: + return "Msgjp"; + case GameLanguage::English: + default: + return isRegionPal() ? "Msguk" : "Msgus"; + } +} + +} // namespace dusk::language diff --git a/src/dusk/language.hpp b/src/dusk/language.hpp new file mode 100644 index 0000000000..3da488b538 --- /dev/null +++ b/src/dusk/language.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "dusk/iso_validate.hpp" +#include "dusk/settings.h" + +#include + +namespace dusk::language { + +std::span available_languages(const iso::DiscInfo& info) noexcept; + +const char* language_name(GameLanguage language) noexcept; +const char* msg_folder() noexcept; + +} // namespace dusk::language diff --git a/src/dusk/livesplit.cpp b/src/dusk/livesplit.cpp index 80c29379e5..d583ef75fa 100644 --- a/src/dusk/livesplit.cpp +++ b/src/dusk/livesplit.cpp @@ -1,46 +1,48 @@ #if _WIN32 - #include - #include - using socket_t = SOCKET; - static void closeSocket(socket_t s) { - LINGER li{1, 0}; - setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast(&li), sizeof(li)); - closesocket(s); - } - static int socketError(socket_t s) { - int err = 0; int len = sizeof(err); - getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast(&err), &len); - return err; - } - static constexpr int kSendFlags = 0; +#include +#include +using socket_t = SOCKET; +static void closeSocket(socket_t s) { + LINGER li{1, 0}; + setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast(&li), sizeof(li)); + closesocket(s); +} +static int socketError(socket_t s) { + int err = 0; + int len = sizeof(err); + getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast(&err), &len); + return err; +} +static constexpr int kSendFlags = 0; #else - #include - #include - #include - #include - #include - #include - #include - using socket_t = int; - static void closeSocket(socket_t s) { - struct linger li{1, 0}; - setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li)); - close(s); - } - static int socketError(socket_t s) { - int err = 0; socklen_t len = sizeof(err); - getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len); - return err; - } - #ifndef INVALID_SOCKET - #define INVALID_SOCKET -1 - #endif +#include +#include +#include +#include +#include +#include +#include +using socket_t = int; +static void closeSocket(socket_t s) { + struct linger li{1, 0}; + setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li)); + close(s); +} +static int socketError(socket_t s) { + int err = 0; + socklen_t len = sizeof(err); + getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len); + return err; +} +#ifndef INVALID_SOCKET +#define INVALID_SOCKET -1 +#endif - #if defined(__APPLE__) - static constexpr int kSendFlags = 0; - #else - static constexpr int kSendFlags = MSG_NOSIGNAL; - #endif +#if defined(__APPLE__) +static constexpr int kSendFlags = 0; +#else +static constexpr int kSendFlags = MSG_NOSIGNAL; +#endif #endif #include @@ -49,18 +51,18 @@ namespace dusk::speedrun { -static bool running = false; -static bool startPending = false; -static uint64_t frameCount = 0; -static socket_t sock = INVALID_SOCKET; -static bool wasLoading = false; -static bool connected = false; -static bool connectPending = false; -static bool disconnectPending = false; -static uint32_t idleProbeCounter = 0; -static uint32_t reconnectCounter = 0; -static char storedHost[64] = "127.0.0.1"; -static int storedPort = 16834; +static bool running = false; +static bool startPending = false; +static uint64_t frameCount = 0; +static socket_t sock = INVALID_SOCKET; +static bool wasLoading = false; +static bool connected = false; +static bool connectPending = false; +static bool disconnectPending = false; +static uint32_t idleProbeCounter = 0; +static uint32_t reconnectCounter = 0; +static char storedHost[64] = "127.0.0.1"; +static int storedPort = 16834; static void sendCmd(const char* cmd) { if (sock == INVALID_SOCKET) { @@ -122,9 +124,11 @@ void onGameFrame() { } void start() { - if (running) { + if (g_speedrunInfo.m_isRunStarted || running) { return; } + resetForSpeedrunMode(); + g_speedrunInfo.startRun(); running = true; startPending = true; @@ -214,8 +218,16 @@ void disconnectLiveSplit() { connected = connectPending = disconnectPending = false; } -bool consumeConnectedEvent() { bool v = connectPending; connectPending = false; return v; } -bool consumeDisconnectedEvent() { bool v = disconnectPending; disconnectPending = false; return v; } +bool consumeConnectedEvent() { + bool v = connectPending; + connectPending = false; + return v; +} +bool consumeDisconnectedEvent() { + bool v = disconnectPending; + disconnectPending = false; + return v; +} void updateLiveSplit() { if (sock == INVALID_SOCKET) { @@ -267,7 +279,8 @@ void updateLiveSplit() { #else || (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK) #endif - ) { + ) + { if (connected) { disconnectPending = true; } @@ -280,15 +293,12 @@ void updateLiveSplit() { return; } - const uint64_t totalMs = frameCount * 1000 / 30; + const uint64_t totalMs = frameCount * 1000 / 30; const uint64_t totalSec = totalMs / 1000; char cmd[32]; snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u", - static_cast(totalSec / 3600), - static_cast((totalSec / 60) % 60), - static_cast(totalSec % 60), - static_cast(totalMs % 1000) - ); + static_cast(totalSec / 3600), static_cast((totalSec / 60) % 60), + static_cast(totalSec % 60), static_cast(totalMs % 1000)); sendCmd(cmd); } @@ -299,4 +309,4 @@ void shutdown() { #endif } -} +} // namespace dusk::speedrun diff --git a/src/dusk/livesplit.h b/src/dusk/livesplit.h index b283a29af4..3196956be6 100644 --- a/src/dusk/livesplit.h +++ b/src/dusk/livesplit.h @@ -1,6 +1,8 @@ #pragma once #include +#include "dusk/game_mode.hpp" +#include "dusk/speedrun.h" namespace dusk::speedrun { void onGameFrame(); diff --git a/src/dusk/main.h b/src/dusk/main.h index 1378e48a1b..e3db4420d1 100644 --- a/src/dusk/main.h +++ b/src/dusk/main.h @@ -1,7 +1,6 @@ #pragma once #include - namespace dusk { extern bool IsRunning; @@ -21,8 +20,6 @@ struct StageRequest { }; extern StageRequest StageRequested; - - #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ (defined(TARGET_OS_TV) && TARGET_OS_TV) inline constexpr bool SupportsProcessRestart = false; diff --git a/src/dusk/mods/item.hpp b/src/dusk/mods/item.hpp new file mode 100644 index 0000000000..ab98b899e3 --- /dev/null +++ b/src/dusk/mods/item.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +class fopAc_ac_c; + +namespace dusk::mods { + +uint8_t item_check(const char* name, uint8_t itemNo, fopAc_ac_c* giver); +uint8_t item_check_tagged(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c* giver); + +uint8_t item_check_chest(uint8_t boxNo, uint8_t itemNo, fopAc_ac_c* chest); +uint8_t item_check_boss(uint8_t itemNo, fopAc_ac_c* boss); +uint8_t item_check_freestanding(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* item); +uint8_t item_check_poe(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* poe); +uint8_t item_check_shop(uint8_t itemNo, fopAc_ac_c* giver); +uint8_t item_check_bug(uint8_t insectId, uint8_t itemNo, fopAc_ac_c* agitha); +uint8_t item_check_sky_character(uint8_t itemNo, fopAc_ac_c* statue); + +uint32_t item_give_tag(const char* name); +uint32_t item_give_tag_chest(uint8_t boxNo); +uint32_t item_give_tag_boss(); +uint32_t item_give_tag_freestanding(uint8_t bitNo); +uint32_t item_give_tag_poe(uint8_t bitNo); +uint32_t item_give_tag_shop(uint8_t itemNo); +uint32_t item_give_tag_bug(uint8_t insectId); +uint32_t item_give_tag_sky_character(); + +void item_check_enqueue(const char* name, uint8_t itemNo); +void item_check_enqueue_poe(uint8_t bitNo, uint8_t itemNo); + +void item_granted(uint8_t itemNo, uint32_t giveTag, fopAc_ac_c* giver); + +bool item_give_queue_dispatching(); +uint32_t item_give_queue_take_tag(); + +} // namespace dusk::mods diff --git a/src/dusk/mods/item_checks.cpp b/src/dusk/mods/item_checks.cpp new file mode 100644 index 0000000000..79a6c993b9 --- /dev/null +++ b/src/dusk/mods/item_checks.cpp @@ -0,0 +1,294 @@ +#include "item.hpp" + +#include "dusk/mod_loader.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "dusk/mods/svc/item.hpp" + +#include "aurora/lib/logging.hpp" +#include "d/d_com_inf_game.h" + +#include + +#include +#include +#include +#include +#include + +namespace dusk::mods { +namespace { + +aurora::Module Log{"dusk::mods::item_checks"}; + +struct CheckOverride { + std::string name; + uint8_t itemNo = 0; +}; + +struct CheckResolver { + ItemCheckHandle handle = 0; + std::string name; + ItemCheckResolveFn fn = nullptr; + void* userData = nullptr; +}; + +struct ModItemChecks { + std::vector overrides; + std::vector resolvers; +}; + +struct PendingResolve { + LoadedMod* mod = nullptr; + bool fixedValue = false; + uint8_t itemNo = 0; + ItemCheckResolveFn fn = nullptr; + void* userData = nullptr; +}; + +std::unordered_map s_modChecks; +std::unordered_set s_warnedCollisions; +ItemCheckHandle s_nextCheckHandle = 1; + +const char* current_stage_name() { + const char* stageName = dComIfGp_getStartStageName(); + return stageName != nullptr ? stageName : ""; +} + +std::string chest_check_name(uint8_t boxNo) { + return fmt::format("chest:{}:{}", current_stage_name(), boxNo); +} + +std::string boss_check_name() { + return fmt::format("boss:{}", current_stage_name()); +} + +std::string freestanding_check_name(uint8_t bitNo) { + return fmt::format("freestanding:{}:{}", current_stage_name(), bitNo); +} + +std::string poe_check_name(uint8_t bitNo) { + return fmt::format("poe:{}:{}", current_stage_name(), bitNo); +} + +std::string shop_check_name(uint8_t itemNo) { + return fmt::format("shop:{}:{}", current_stage_name(), itemNo); +} + +std::string bug_check_name(uint8_t insectId) { + return fmt::format("bug:{}", insectId); +} + +std::string sky_character_check_name() { + return fmt::format("skychar:{}:{}", current_stage_name(), dStage_roomControl_c::getStayNo()); +} + +} // namespace + +uint8_t item_check(const char* name, uint8_t itemNo, fopAc_ac_c* giver) { + if (name == nullptr || *name == '\0' || s_modChecks.empty()) { + return itemNo; + } + + // Callbacks may change registrations, so copy the applicable chain before invoking one. + std::vector resolves; + LoadedMod* previousOverrideOwner = nullptr; + for (auto& mod : ModLoader::instance().mods()) { + if (!mod.active) { + continue; + } + + const auto modIt = s_modChecks.find(&mod); + if (modIt == s_modChecks.end()) { + continue; + } + + for (const auto& checkOverride : modIt->second.overrides) { + if (checkOverride.name != name) { + continue; + } + if (previousOverrideOwner != nullptr && s_warnedCollisions.emplace(name).second) { + Log.warn("check '{}' is overridden by [{}] and [{}]; [{}] wins by load order", name, + previousOverrideOwner->metadata.id, mod.metadata.id, mod.metadata.id); + } + previousOverrideOwner = &mod; + resolves.push_back({.mod = &mod, .fixedValue = true, .itemNo = checkOverride.itemNo}); + } + + for (const auto& resolver : modIt->second.resolvers) { + if (resolver.name.empty() || resolver.name == name) { + resolves.push_back({.mod = &mod, .fn = resolver.fn, .userData = resolver.userData}); + } + } + } + + ItemCheckInfo info{ + .name = name, + .giver_actor = giver, + .vanilla_item = itemNo, + .current_item = itemNo, + }; + for (const auto& resolve : resolves) { + if (!resolve.mod->active) { + continue; + } + if (resolve.fixedValue) { + info.current_item = resolve.itemNo; + continue; + } + + uint8_t resolvedItem = info.current_item; + try { + if (resolve.fn(resolve.mod->context.get(), &info, &resolvedItem, resolve.userData)) { + info.current_item = resolvedItem; + } + } catch (const std::exception& e) { + fail_mod(*resolve.mod, MOD_ERROR, + fmt::format("Exception in item check resolver for '{}': {}", name, e.what())); + } catch (...) { + fail_mod(*resolve.mod, MOD_ERROR, + fmt::format("Unknown exception in item check resolver for '{}'", name)); + } + } + return info.current_item; +} + +uint8_t item_check_chest(uint8_t boxNo, uint8_t itemNo, fopAc_ac_c* chest) { + if (s_modChecks.empty()) { + return itemNo; + } + const auto name = chest_check_name(boxNo); + return item_check(name.c_str(), itemNo, chest); +} + +uint8_t item_check_boss(uint8_t itemNo, fopAc_ac_c* boss) { + if (s_modChecks.empty()) { + return itemNo; + } + const auto name = boss_check_name(); + return item_check(name.c_str(), itemNo, boss); +} + +uint8_t item_check_freestanding(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* item) { + if (s_modChecks.empty()) { + return itemNo; + } + const auto name = freestanding_check_name(bitNo); + return item_check(name.c_str(), itemNo, item); +} + +uint8_t item_check_poe(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* poe) { + if (s_modChecks.empty()) { + return itemNo; + } + const auto name = poe_check_name(bitNo); + return item_check(name.c_str(), itemNo, poe); +} + +uint8_t item_check_shop(uint8_t itemNo, fopAc_ac_c* giver) { + if (s_modChecks.empty()) { + return itemNo; + } + const auto name = shop_check_name(itemNo); + return item_check(name.c_str(), itemNo, giver); +} + +uint8_t item_check_bug(uint8_t insectId, uint8_t itemNo, fopAc_ac_c* agitha) { + if (s_modChecks.empty()) { + return itemNo; + } + const auto name = bug_check_name(insectId); + return item_check(name.c_str(), itemNo, agitha); +} + +uint8_t item_check_sky_character(uint8_t itemNo, fopAc_ac_c* statue) { + if (s_modChecks.empty()) { + return itemNo; + } + const auto name = sky_character_check_name(); + return item_check(name.c_str(), itemNo, statue); +} + +uint32_t item_give_tag_chest(uint8_t boxNo) { + return item_give_tag(chest_check_name(boxNo).c_str()); +} + +uint32_t item_give_tag_boss() { + return item_give_tag(boss_check_name().c_str()); +} + +uint32_t item_give_tag_freestanding(uint8_t bitNo) { + return item_give_tag(freestanding_check_name(bitNo).c_str()); +} + +uint32_t item_give_tag_poe(uint8_t bitNo) { + return item_give_tag(poe_check_name(bitNo).c_str()); +} + +uint32_t item_give_tag_shop(uint8_t itemNo) { + return item_give_tag(shop_check_name(itemNo).c_str()); +} + +uint32_t item_give_tag_bug(uint8_t insectId) { + return item_give_tag(bug_check_name(insectId).c_str()); +} + +uint32_t item_give_tag_sky_character() { + return item_give_tag(sky_character_check_name().c_str()); +} + +void item_check_enqueue_poe(uint8_t bitNo, uint8_t itemNo) { + item_check_enqueue(poe_check_name(bitNo).c_str(), itemNo); +} + +namespace svc { + +ModResult item_check_set_override(LoadedMod& mod, const char* name, uint8_t itemNo) { + auto& checks = s_modChecks[&mod]; + for (auto& checkOverride : checks.overrides) { + if (checkOverride.name == name) { + checkOverride.itemNo = itemNo; + return MOD_OK; + } + } + checks.overrides.push_back({.name = name, .itemNo = itemNo}); + return MOD_OK; +} + +ModResult item_check_clear_override(LoadedMod& mod, const char* name) { + const auto modIt = s_modChecks.find(&mod); + if (modIt == s_modChecks.end()) { + return MOD_INVALID_ARGUMENT; + } + const auto removed = std::erase_if(modIt->second.overrides, + [&](const auto& checkOverride) { return checkOverride.name == name; }); + return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT; +} + +ModResult item_check_add_resolver(LoadedMod& mod, const char* name, ItemCheckResolveFn fn, + void* userData, ItemCheckHandle& outHandle) { + auto& resolver = s_modChecks[&mod].resolvers.emplace_back(); + resolver.handle = s_nextCheckHandle++; + resolver.name = name != nullptr ? name : ""; + resolver.fn = fn; + resolver.userData = userData; + outHandle = resolver.handle; + return MOD_OK; +} + +ModResult item_check_remove_resolver(LoadedMod& mod, ItemCheckHandle handle) { + const auto modIt = s_modChecks.find(&mod); + if (modIt == s_modChecks.end()) { + return MOD_INVALID_ARGUMENT; + } + const auto removed = std::erase_if( + modIt->second.resolvers, [&](const auto& resolver) { return resolver.handle == handle; }); + return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT; +} + +void item_checks_remove_mod(LoadedMod& mod) { + s_modChecks.erase(&mod); + s_warnedCollisions.clear(); +} + +} // namespace svc +} // namespace dusk::mods diff --git a/src/dusk/mods/item_gives.cpp b/src/dusk/mods/item_gives.cpp new file mode 100644 index 0000000000..bfe92d3cff --- /dev/null +++ b/src/dusk/mods/item_gives.cpp @@ -0,0 +1,343 @@ +#include "item.hpp" + +#include "dusk/mod_loader.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "dusk/mods/svc/item.hpp" + +#include "aurora/lib/logging.hpp" +#include "d/actor/d_a_alink.h" +#include "d/d_com_inf_game.h" +#include "d/d_item.h" +#include "d/d_item_data.h" +#include "f_op/f_op_actor_mng.h" + +#include + +#include +#include +#include +#include +#include + +namespace dusk::mods { +namespace { + +aurora::Module Log{"dusk::mods::item_gives"}; + +// deque keeps previously returned c_str pointers valid if a callback interns another name. +std::deque s_giveNames; +std::unordered_map s_giveNameIds; + +const char* item_give_name(uint32_t tag) { + if (tag == 0 || tag > s_giveNames.size()) { + return nullptr; + } + return s_giveNames[tag - 1].c_str(); +} + +struct GiveObserver { + ItemGiveHandle handle = 0; + ItemGiveObserveFn fn = nullptr; + void* userData = nullptr; +}; + +struct PendingObserver { + LoadedMod* mod = nullptr; + ItemGiveObserveFn fn = nullptr; + void* userData = nullptr; +}; + +std::unordered_map> s_modObservers; +ItemGiveHandle s_nextGiveHandle = 1; +size_t s_observerCount = 0; + +void notify_gives(const char* checkName, uint8_t itemNo, fopAc_ac_c* giver, ItemGiveOrigin origin) { + if (s_observerCount == 0) { + return; + } + + // Callbacks may change registrations, so copy them before invoking one. + std::vector observers; + for (auto& mod : ModLoader::instance().mods()) { + if (!mod.active) { + continue; + } + const auto modIt = s_modObservers.find(&mod); + if (modIt == s_modObservers.end()) { + continue; + } + for (const auto& observer : modIt->second) { + observers.push_back({.mod = &mod, .fn = observer.fn, .userData = observer.userData}); + } + } + + const ItemGiveInfo info{ + .check_name = checkName, + .giver_actor = giver, + .item = itemNo, + .origin = static_cast(origin), + }; + for (const auto& observer : observers) { + if (!observer.mod->active) { + continue; + } + try { + observer.fn(observer.mod->context.get(), &info, observer.userData); + } catch (const std::exception& e) { + fail_mod(*observer.mod, MOD_ERROR, + fmt::format("Exception in item give observer: {}", e.what())); + } catch (...) { + fail_mod(*observer.mod, MOD_ERROR, "Unknown exception in item give observer"); + } + } +} + +constexpr size_t kGiveQueueLimit = 64; +constexpr int kGiveMaxRetries = 5; + +struct QueuedGive { + LoadedMod* owner = nullptr; + uint32_t tag = 0; + uint8_t itemNo = 0; + bool silent = false; + bool resolveAtDispatch = false; +}; + +std::deque s_giveQueue; +QueuedGive s_inFlightGive{}; +uint8_t s_inFlightItem = 0; +int s_inFlightRetries = 0; +bool s_inFlight = false; +bool s_inFlightSpawned = false; +bool s_dispatchingSilent = false; + +bool safe_to_dispatch() { + daAlink_c* link = daAlink_getAlinkActorClass(); + if (link == nullptr) { + return false; + } + + // make sure player is in a safe action and not already in an event before dispatching + switch (link->mProcID) { + case daAlink_c::PROC_WAIT: + case daAlink_c::PROC_TIRED_WAIT: + case daAlink_c::PROC_MOVE: + case daAlink_c::PROC_WOLF_WAIT: + case daAlink_c::PROC_WOLF_TIRED_WAIT: + case daAlink_c::PROC_WOLF_MOVE: + case daAlink_c::PROC_ATN_MOVE: + case daAlink_c::PROC_WOLF_ATN_AC_MOVE: + break; + default: + return false; + } + if (link->checkEventRun()) { + return false; + } + + int itemId = 0; + return link->mMsgFlow.getEventId(&itemId) == 0; +} + +bool resolve_queued_give(const QueuedGive& give, ItemGiveOrigin origin, uint8_t& outItem) { + outItem = give.itemNo; + if (give.resolveAtDispatch) { + outItem = item_check(item_give_name(give.tag), give.itemNo, nullptr); + } + if (outItem != dItemNo_NONE_e) { + return true; + } + + notify_gives(item_give_name(give.tag), dItemNo_NONE_e, nullptr, origin); + return false; +} + +void dispatch_silent_give(const QueuedGive& give) { + uint8_t itemNo = 0; + if (!resolve_queued_give(give, ITEM_GIVE_ORIGIN_QUEUE_SILENT, itemNo)) { + return; + } + + Log.debug("dispatching silent item {:#x} for '{}'", itemNo, + item_give_name(give.tag) != nullptr ? item_give_name(give.tag) : ""); + s_dispatchingSilent = true; + execItemGet(itemNo, give.tag, nullptr); + s_dispatchingSilent = false; +} + +void dispatch_demo_give() { + Log.debug("dispatching item {:#x} for '{}'", s_inFlightItem, + item_give_name(s_inFlightGive.tag) != nullptr ? item_give_name(s_inFlightGive.tag) : ""); + + daAlink_c* link = daAlink_getAlinkActorClass(); + dComIfGp_getEvent()->setGtItm(s_inFlightItem); + link->procCoGetItemInit(); + const s16 eventIndex = dComIfGp_getEventManager().getEventIdx(link, "DEFAULT_GETITEM", 0xFF); + fopAcM_orderChangeEventId(link, eventIndex, 1, 0xFFFF); +} + +} // namespace + +uint32_t item_give_tag(const char* name) { + if (name == nullptr || *name == '\0') { + return 0; + } + if (const auto it = s_giveNameIds.find(name); it != s_giveNameIds.end()) { + return it->second; + } + + s_giveNames.emplace_back(name); + const auto tag = static_cast(s_giveNames.size()); + s_giveNameIds.emplace(s_giveNames.back(), tag); + return tag; +} + +uint8_t item_check_tagged(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c* giver) { + const char* name = item_give_name(giveTag); + return name != nullptr ? item_check(name, itemNo, giver) : itemNo; +} + +void item_check_enqueue(const char* name, uint8_t itemNo) { + if (s_giveQueue.size() >= kGiveQueueLimit) { + Log.warn("item give queue is full; dropping check '{}'", name != nullptr ? name : ""); + return; + } + s_giveQueue.push_back({ + .tag = item_give_tag(name), + .itemNo = itemNo, + .resolveAtDispatch = true, + }); +} + +void item_granted(uint8_t itemNo, uint32_t giveTag, fopAc_ac_c* giver) { + ItemGiveOrigin origin = ITEM_GIVE_ORIGIN_GAME; + if (s_dispatchingSilent) { + origin = ITEM_GIVE_ORIGIN_QUEUE_SILENT; + } else if (s_inFlight && itemNo == s_inFlightItem && giveTag == s_inFlightGive.tag) { + origin = ITEM_GIVE_ORIGIN_QUEUE; + s_inFlight = false; + s_inFlightSpawned = false; + } + notify_gives(item_give_name(giveTag), itemNo, giver, origin); +} + +bool item_give_queue_dispatching() { + return s_inFlight && !s_inFlightSpawned; +} + +uint32_t item_give_queue_take_tag() { + if (!item_give_queue_dispatching()) { + return 0; + } + s_inFlightSpawned = true; + return s_inFlightGive.tag; +} + +namespace svc { + +void item_gives_tick() { + if ((!s_inFlight && s_giveQueue.empty()) || !safe_to_dispatch()) { + return; + } + + if (s_inFlight) { + if (++s_inFlightRetries > kGiveMaxRetries) { + Log.error("item {:#x} for '{}' did not complete after {} attempts; dropping it", + s_inFlightItem, + item_give_name(s_inFlightGive.tag) != nullptr ? item_give_name(s_inFlightGive.tag) : + "", + kGiveMaxRetries); + s_inFlight = false; + s_inFlightSpawned = false; + return; + } + s_inFlightSpawned = false; + dispatch_demo_give(); + return; + } + + while (!s_giveQueue.empty() && s_giveQueue.front().silent) { + const QueuedGive give = s_giveQueue.front(); + s_giveQueue.pop_front(); + dispatch_silent_give(give); + } + if (s_giveQueue.empty()) { + return; + } + + const QueuedGive give = s_giveQueue.front(); + uint8_t itemNo = 0; + if (!resolve_queued_give(give, ITEM_GIVE_ORIGIN_QUEUE, itemNo)) { + s_giveQueue.pop_front(); + return; + } + + s_giveQueue.pop_front(); + s_inFlightGive = give; + s_inFlightItem = itemNo; + s_inFlightRetries = 0; + s_inFlight = true; + s_inFlightSpawned = false; + dispatch_demo_give(); +} + +void item_gives_clear() { + if (!s_giveQueue.empty() || s_inFlight) { + Log.info("dropping {} pending item give(s)", + s_giveQueue.size() + static_cast(s_inFlight)); + } + s_giveQueue.clear(); + s_inFlight = false; + s_inFlightSpawned = false; +} + +ModResult item_give_enqueue(LoadedMod& mod, const char* checkName, uint8_t itemNo, uint32_t flags) { + if (s_giveQueue.size() >= kGiveQueueLimit) { + return MOD_UNAVAILABLE; + } + s_giveQueue.push_back({ + .owner = &mod, + .tag = item_give_tag(checkName), + .itemNo = itemNo, + .silent = (flags & ITEM_GIVE_SILENT) != 0, + .resolveAtDispatch = (flags & ITEM_GIVE_RESOLVE) != 0, + }); + return MOD_OK; +} + +ModResult item_give_add_observer( + LoadedMod& mod, ItemGiveObserveFn fn, void* userData, ItemGiveHandle& outHandle) { + auto& observer = s_modObservers[&mod].emplace_back(); + observer.handle = s_nextGiveHandle++; + observer.fn = fn; + observer.userData = userData; + outHandle = observer.handle; + ++s_observerCount; + return MOD_OK; +} + +ModResult item_give_remove_observer(LoadedMod& mod, ItemGiveHandle handle) { + const auto modIt = s_modObservers.find(&mod); + if (modIt == s_modObservers.end()) { + return MOD_INVALID_ARGUMENT; + } + const auto removed = std::erase_if( + modIt->second, [&](const auto& observer) { return observer.handle == handle; }); + s_observerCount -= removed; + return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT; +} + +void item_gives_remove_mod(LoadedMod& mod) { + if (const auto modIt = s_modObservers.find(&mod); modIt != s_modObservers.end()) { + s_observerCount -= modIt->second.size(); + s_modObservers.erase(modIt); + } + std::erase_if(s_giveQueue, [&](const QueuedGive& give) { return give.owner == &mod; }); + if (s_inFlight && s_inFlightGive.owner == &mod) { + // The event system already owns this grant, so it can no longer be canceled safely. + s_inFlightGive.owner = nullptr; + } +} + +} // namespace svc +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index 6c3a01bd7a..83c7660410 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -899,6 +899,7 @@ bool ModLoader::activate_mod(LoadedMod& mod) { } void ModLoader::deactivate_mod(LoadedMod& mod) { + svc::modules_mod_deactivating(mod); if (mod.initialized && mod.native && mod.native->fn_shutdown) { log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_shutdown"); try { diff --git a/src/dusk/mods/svc/flow.cpp b/src/dusk/mods/svc/flow.cpp new file mode 100644 index 0000000000..b55006c3b0 --- /dev/null +++ b/src/dusk/mods/svc/flow.cpp @@ -0,0 +1,1438 @@ +#include "flow.hpp" + +#include "registry.hpp" + +#include "dusk/logging.h" +#include "dusk/mods/loader/loader.hpp" +#include "dusk/settings.h" + +#include "helpers/bits.hpp" + +#include "JSystem/JMessage/control.h" +#include "JSystem/JMessage/processor.h" +#include "JSystem/JMessage/resource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::flow { +namespace { + +constexpr uint16_t kCustomMax = 0xfffe; +constexpr uint16_t kGroupMax = 8; +constexpr size_t kDebugNameMax = 256; + +struct ResourceInfo { + const uint8_t* bmg = nullptr; + uint16_t group = 0; + const uint8_t* nodes = nullptr; + const uint8_t* edges = nullptr; + uint16_t nodeCount = 0; + uint16_t edgeCount = 0; + const uint8_t* entries = nullptr; + uint16_t entryCount = 0; + uint16_t entrySize = 0; + const uint8_t* textBegin = nullptr; + const uint8_t* textEnd = nullptr; + bool valid = false; +}; + +struct PatchRecord { + FlowGraphHandle graph{}; + mods::LoadedMod* owner = nullptr; + uint16_t group = 0; + uint16_t index = 0; + uint64_t sequence = 0; + bool edge = false; + bool active = false; + FlowNodeData node{}; + uint16_t target = 0; +}; + +struct GraphRecord { + FlowGraphHandle handle{}; + mods::LoadedMod* owner = nullptr; + uint16_t group = 0; + std::unordered_map> nodes; + std::unordered_map edges; + std::vector> edgeRuns; + bool committed = false; +}; + +struct QueryRecord { + mods::LoadedMod* owner = nullptr; + std::string debugName; + FlowQueryFn fn = nullptr; + void* userData = nullptr; +}; + +struct EventRecord { + mods::LoadedMod* owner = nullptr; + std::string debugName; + FlowEventFn fn = nullptr; + void* userData = nullptr; +}; + +struct MessageVariantRecord { + uint8_t language = 0; + MessageEntryData entry{}; + std::vector text; +}; + +struct MessageRecord { + MessageHandle handle{}; + MessageId id{}; + uint16_t group = 0; + mods::LoadedMod* owner = nullptr; + std::unordered_map> variants; +}; + +struct OverrideKey { + uint16_t group = 0; + uint16_t messageId = 0; + uint8_t language = 0; + bool operator==(const OverrideKey&) const = default; +}; + +struct OverrideKeyHash { + size_t operator()(const OverrideKey& key) const { + return static_cast(key.group) << 24 | static_cast(key.messageId) << 8 | + key.language; + } +}; + +struct OverrideRecord { + MessageOverrideHandle handle{}; + mods::LoadedMod* owner = nullptr; + uint64_t sequence = 0; + bool callback = false; + std::vector text; + MessageOverrideFn fn = nullptr; + void* userData = nullptr; +}; + +struct ActiveBinding { + std::vector> variants; + std::vector>> texts; +}; + +std::unordered_map s_resources; +std::vector s_patches; +std::unordered_map s_graphs; +std::unordered_map s_queries; +std::unordered_map s_events; +std::unordered_map> s_messages; +std::unordered_map> s_messagesByHandle; +std::unordered_map, OverrideKeyHash> s_overrides; +std::unordered_map s_bindings; +std::unordered_map s_processorBindings; + +std::array s_nextNode{}; +std::array s_nextEdge{}; +uint16_t s_nextMessage = kCustomMessageMin; +uint16_t s_nextQuery = kCustomQueryMin; +uint16_t s_nextEvent = kCustomEventMin; +std::vector s_freeQueries; +std::vector s_freeEvents; +std::array, kGroupMax + 1> s_freeNodes; +std::array>, kGroupMax + 1> s_freeEdgeRuns; +std::vector s_freeMessages; +uint64_t s_nextHandle = 1; +uint64_t s_nextSequence = 1; + +std::unordered_set s_warnedConflicts; +std::unordered_set s_warnedMissingCallbacks; +std::unordered_set s_warnedUnresolved; + +uint8_t active_language() { + return static_cast(getSettings().game.language.getValue()); +} + +bool valid_group(uint16_t group) { + return group <= kGroupMax; +} + +bool valid_debug_name(const char* name) { + if (name == nullptr) { + return false; + } + const std::string_view value{name}; + return !value.empty() && value.size() <= kDebugNameMax; +} + +int32_t mod_priority(const mods::LoadedMod& mod) { + int32_t priority = 0; + for (const auto& candidate : mods::ModLoader::instance().mods()) { + ++priority; + if (&candidate == &mod) { + return priority; + } + } + return priority + 1; +} + +const ResourceInfo* find_resource(const void* bmgData) { + const auto found = s_resources.find(bmgData); + return found != s_resources.end() && found->second.valid ? &found->second : nullptr; +} + +bool parse_resource(const void* data, uint16_t group, ResourceInfo& out) { + const auto* bmg = static_cast(data); + if (bmg == nullptr || std::memcmp(bmg, "MESGbmg1", 8) != 0) { + return false; + } + if (read_bits(bmg + 8) < 0x20) { + return false; + } + + out = {.bmg = bmg, .group = group}; + const uint8_t* section = nullptr; + size_t sectionSize = 0; + if (detail::find_section(bmg, MULTI_CHAR('INF1'), section, sectionSize)) { + if (sectionSize < 16) { + return false; + } + out.entryCount = read_bits(section + 8); + out.entrySize = read_bits(section + 10); + if (out.entrySize < sizeof(MessageEntryData) || + static_cast(out.entryCount) * out.entrySize > sectionSize - 16) + { + return false; + } + out.entries = section + 16; + } + if (detail::find_section(bmg, MULTI_CHAR('DAT1'), section, sectionSize)) { + out.textBegin = section + 8; + out.textEnd = section + sectionSize; + } + if (detail::find_section(bmg, MULTI_CHAR('FLW1'), section, sectionSize)) { + if (sectionSize < 16) { + return false; + } + out.nodeCount = read_bits(section + 8); + out.edgeCount = read_bits(section + 10); + const auto nodesSize = static_cast(out.nodeCount) * 8; + const auto edgesSize = static_cast(out.edgeCount) * 2; + if (nodesSize + edgesSize > sectionSize - 16) { + return false; + } + out.nodes = section + 16; + out.edges = out.nodes + nodesSize; + } + + if (out.nodeCount >= kCustomNodeMin || out.edgeCount >= kCustomEdgeMin || + out.entryCount >= kCustomMessageMin) + { + return false; + } + for (uint16_t i = 0; i < out.entryCount; ++i) { + const uint8_t* entry = out.entries + static_cast(i) * out.entrySize; + if (read_bits(entry + 4) >= kCustomMessageMin) { + return false; + } + } + for (uint16_t i = 0; i < out.nodeCount; ++i) { + const uint8_t* node = out.nodes + static_cast(i) * 8; + if (node[0] == 1 && (read_bits(node + 2) >= kCustomMessageMin || + (read_bits(node + 4) != kEnd && + read_bits(node + 4) >= kCustomNodeMin))) + { + return false; + } + if (node[0] == 2) { + const auto lastEdge = static_cast(read_bits(node + 6)) + + (node[1] == 0 ? 0 : node[1] - 1); + if (read_bits(node + 2) >= kCustomQueryMin || lastEdge >= kCustomEdgeMin) { + return false; + } + } + if (node[0] == 3 && (node[1] >= kCustomEventMin || + (node[1] != 9 && read_bits(node + 2) >= kCustomEdgeMin))) + { + return false; + } + } + for (uint16_t i = 0; i < out.edgeCount; ++i) { + const auto target = read_bits(out.edges + static_cast(i) * 2); + if (target != kEnd && target >= kCustomNodeMin) { + return false; + } + } + out.valid = true; + return true; +} + +const GraphRecord* graph_for_node(uint16_t group, uint16_t index, bool requireCommitted) { + for (const auto& [handle, graph] : s_graphs) { + if (graph.group == group && graph.nodes.contains(index) && + (!requireCommitted || graph.committed)) + { + return &graph; + } + } + return nullptr; +} + +const GraphRecord* graph_for_edge(uint16_t group, uint16_t index, bool requireCommitted) { + for (const auto& [handle, graph] : s_graphs) { + if (graph.group == group && graph.edges.contains(index) && + (!requireCommitted || graph.committed)) + { + return &graph; + } + } + return nullptr; +} + +bool owner_has_message(const mods::LoadedMod& owner, uint16_t group, uint16_t id) { + const auto found = s_messages.find(id); + return found != s_messages.end() && found->second->owner == &owner && + found->second->group == group; +} + +bool valid_target_owner(const mods::LoadedMod& owner, uint16_t group, uint16_t target) { + if (target == kEnd || target < kCustomNodeMin) { + return true; + } + const auto* graph = graph_for_node(group, target, false); + return graph != nullptr && graph->owner == &owner; +} + +bool valid_edge_owner(const mods::LoadedMod& owner, uint16_t group, uint16_t edge) { + if (edge < kCustomEdgeMin) { + return true; + } + const auto* graph = graph_for_edge(group, edge, false); + return graph != nullptr && graph->owner == &owner; +} + +bool validate_node_owner(const mods::LoadedMod& owner, uint16_t group, const FlowNodeData& node) { + const auto* bytes = node.bytes; + switch (bytes[0]) { + case 1: { + const auto messageIndex = read_bits(bytes + 2); + const auto target = read_bits(bytes + 4); + if (messageIndex >= kCustomMessageMin && + (messageIndex > kCustomMessageMax || !owner_has_message(owner, group, messageIndex))) + { + return false; + } + return valid_target_owner(owner, group, target); + } + case 2: { + const uint8_t resultCount = bytes[1]; + const auto query = read_bits(bytes + 2); + const auto firstEdge = read_bits(bytes + 6); + if (resultCount == 0 || static_cast(firstEdge) + resultCount - 1 > kCustomMax) { + return false; + } + if (query >= kCustomQueryMin) { + const auto found = s_queries.find(query); + if (query > kCustomMax || found == s_queries.end() || found->second.owner != &owner) { + return false; + } + } else if (query >= FLOW_QUERY_BUILTIN_COUNT) { + return false; + } + for (uint16_t i = 0; i < resultCount; ++i) { + if (!valid_edge_owner(owner, group, static_cast(firstEdge + i))) { + return false; + } + } + return true; + } + case 3: { + const uint8_t event = bytes[1]; + const auto edge = read_bits(bytes + 2); + if (event >= kCustomEventMin) { + const auto found = s_events.find(event); + if (event == 0xff || found == s_events.end() || found->second.owner != &owner) { + return false; + } + } else if (event >= FLOW_EVENT_BUILTIN_COUNT) { + return false; + } + return event == 9 || valid_edge_owner(owner, group, edge); + } + default: + return false; + } +} + +const PatchRecord* winning_patch(uint16_t group, uint16_t index, bool edge) { + const PatchRecord* winner = nullptr; + int32_t winnerPriority = 0; + const mods::LoadedMod* firstOwner = nullptr; + for (const auto& patch : s_patches) { + if (patch.group != group || patch.index != index || patch.edge != edge || !patch.active || + !patch.owner->active) + { + continue; + } + if (firstOwner == nullptr) { + firstOwner = patch.owner; + } else if (firstOwner != patch.owner) { + const uint64_t key = + static_cast(edge) << 63 | static_cast(group) << 16 | index; + if (s_warnedConflicts.insert(key).second) { + DuskLog.warn("flow {} {}:{} is patched by multiple mods; later load wins", + edge ? "edge" : "node", group, index); + } + } + const int32_t priority = mod_priority(*patch.owner); + if (winner == nullptr || priority > winnerPriority || + (priority == winnerPriority && patch.sequence > winner->sequence)) + { + winner = &patch; + winnerPriority = priority; + } + } + return winner; +} + +bool target_in_resource(const ResourceInfo& resource, uint16_t target) { + if (target == kEnd) { + return true; + } + if (target < kCustomNodeMin) { + return target < resource.nodeCount; + } + return graph_for_node(resource.group, target, true) != nullptr; +} + +bool edge_in_resource(const ResourceInfo& resource, uint16_t edge) { + if (edge < kCustomEdgeMin) { + return edge < resource.edgeCount; + } + return graph_for_edge(resource.group, edge, true) != nullptr; +} + +bool node_resolves(const ResourceInfo& resource, const FlowNodeData& node) { + switch (node.bytes[0]) { + case 1: { + const auto messageIndex = read_bits(node.bytes + 2); + const auto customMessage = s_messages.find(messageIndex); + const bool messageValid = + messageIndex < kCustomMessageMin ? + messageIndex < resource.entryCount : + customMessage != s_messages.end() && + customMessage->second->group == resource.group && + customMessage->second->variants.contains(active_language()); + return messageValid && target_in_resource(resource, read_bits(node.bytes + 4)); + } + case 2: { + const auto query = read_bits(node.bytes + 2); + if (node.bytes[1] == 0 || (query >= FLOW_QUERY_BUILTIN_COUNT && query < kCustomQueryMin) || + query == kEnd) + { + return false; + } + const auto firstEdge = read_bits(node.bytes + 6); + for (uint16_t i = 0; i < node.bytes[1]; ++i) { + if (!edge_in_resource(resource, static_cast(firstEdge + i))) { + return false; + } + } + return true; + } + case 3: { + const uint8_t event = node.bytes[1]; + if ((event >= FLOW_EVENT_BUILTIN_COUNT && event < kCustomEventMin) || event == 0xff) { + return false; + } + return event == 9 || edge_in_resource(resource, read_bits(node.bytes + 2)); + } + default: + return false; + } +} + +bool valid_encoded_text(const uint8_t* text, size_t size) { + if (text == nullptr || size == 0) { + return false; + } + size_t offset = 0; + while (offset < size) { + if (text[offset] == 0) { + return offset + 1 == size; + } + if (text[offset] == 0x1a) { + if (offset + 2 > size || text[offset + 1] < 5 || text[offset + 1] > size - offset) { + return false; + } + offset += text[offset + 1]; + } else { + ++offset; + } + } + return false; +} + +size_t encoded_text_size(const ResourceInfo& resource, const char* text) { + const auto* bytes = reinterpret_cast(text); + if (bytes == nullptr || bytes < resource.textBegin || bytes >= resource.textEnd) { + return 0; + } + const auto maximum = static_cast(resource.textEnd - bytes); + size_t offset = 0; + while (offset < maximum) { + if (bytes[offset] == 0) { + return offset + 1; + } + if (bytes[offset] == 0x1a) { + if (offset + 2 > maximum || bytes[offset + 1] < 5 || + bytes[offset + 1] > maximum - offset) + { + return 0; + } + offset += bytes[offset + 1]; + } else { + ++offset; + } + } + return 0; +} + +std::shared_ptr active_variant_shared(const MessageRecord& message) { + const auto found = message.variants.find(active_language()); + return found != message.variants.end() ? found->second : nullptr; +} + +std::shared_ptr> resolve_override( + const ResourceInfo& resource, uint16_t messageId, const char* originalText) { + const OverrideKey key{resource.group, messageId, active_language()}; + const auto found = s_overrides.find(key); + if (found == s_overrides.end()) { + return nullptr; + } + std::vector candidates; + for (const auto& record : found->second) { + if (record.owner->active) { + candidates.push_back(record); + } + } + std::ranges::sort(candidates, [](const auto& left, const auto& right) { + const int32_t leftPriority = mod_priority(*left.owner); + const int32_t rightPriority = mod_priority(*right.owner); + return leftPriority != rightPriority ? leftPriority > rightPriority : + left.sequence > right.sequence; + }); + + const size_t originalSize = encoded_text_size(resource, originalText); + for (const auto& candidate : candidates) { + if (!candidate.callback) { + return std::make_shared>(candidate.text); + } + const OverrideRecord local = candidate; + MessageTextData resolved{}; + const MessageOverrideContext context{ + resource.group, + messageId, + active_language(), + reinterpret_cast(originalText), + originalSize, + }; + try { + const bool accepted = + local.fn(local.owner->context.get(), &context, &resolved, local.userData); + if (accepted && valid_encoded_text(resolved.text, resolved.text_size)) { + return std::make_shared>( + resolved.text, resolved.text + resolved.text_size); + } + if (accepted) { + dusk::mods::fail_mod(*local.owner, MOD_INVALID_ARGUMENT, + "message override returned malformed encoded text"); + } + } catch (const std::exception& error) { + dusk::mods::fail_mod(*local.owner, MOD_ERROR, + fmt::format("exception in message override: {}", error.what())); + } catch (...) { + dusk::mods::fail_mod(*local.owner, MOD_ERROR, "unknown exception in message override"); + } + } + return nullptr; +} + +void retain_binding(JMessage::TControl* control, const JMessage::TProcessor* processor, + std::shared_ptr variant, + std::shared_ptr> text) { + ActiveBinding* binding = nullptr; + if (control != nullptr) { + binding = &s_bindings[control]; + } else if (processor != nullptr) { + binding = &s_processorBindings[processor]; + } else { + return; + } + if (variant != nullptr && + std::ranges::find(binding->variants, variant) == binding->variants.end()) + { + binding->variants.push_back(std::move(variant)); + } + if (text != nullptr && std::ranges::find(binding->texts, text) == binding->texts.end()) { + binding->texts.push_back(std::move(text)); + } +} + +JMessage::TControl* control_for_processor(const JMessage::TProcessor* processor) { + for (const auto& entry : s_bindings) { + const auto* control = entry.first; + if (control->pSequenceProcessor_ == processor || control->pRenderingProcessor_ == processor) + { + return const_cast(control); + } + } + return nullptr; +} + +ModResult add_patch( + GraphRecord& graph, uint16_t index, bool edge, const FlowNodeData* node, uint16_t target) { + if (index >= kCustomNodeMin || (edge && index >= kCustomEdgeMin) || (!edge && node == nullptr)) + { + return MOD_INVALID_ARGUMENT; + } + PatchRecord record{ + .graph = graph.handle, + .owner = graph.owner, + .group = graph.group, + .index = index, + .edge = edge, + .target = target, + }; + if (node != nullptr) { + record.node = *node; + } + s_patches.push_back(record); + return MOD_OK; +} + +void reset_state() { + s_resources.clear(); + s_patches.clear(); + s_graphs.clear(); + s_queries.clear(); + s_events.clear(); + s_messages.clear(); + s_messagesByHandle.clear(); + s_overrides.clear(); + s_bindings.clear(); + s_processorBindings.clear(); + s_nextNode.fill(kCustomNodeMin); + s_nextEdge.fill(kCustomEdgeMin); + s_nextMessage = kCustomMessageMin; + s_nextQuery = kCustomQueryMin; + s_nextEvent = kCustomEventMin; + s_freeQueries.clear(); + s_freeEvents.clear(); + for (auto& freeNodes : s_freeNodes) { + freeNodes.clear(); + } + for (auto& freeRuns : s_freeEdgeRuns) { + freeRuns.clear(); + } + s_freeMessages.clear(); + s_nextHandle = 1; + s_nextSequence = 1; + s_warnedConflicts.clear(); + s_warnedMissingCallbacks.clear(); + s_warnedUnresolved.clear(); +} + +void remove_mod(mods::LoadedMod& mod) { + std::erase_if(s_patches, [&](const auto& patch) { return patch.owner == &mod; }); + + for (auto iterator = s_graphs.begin(); iterator != s_graphs.end();) { + auto& graph = iterator->second; + if (graph.owner == &mod) { + auto& freeNodes = s_freeNodes[graph.group]; + for (const auto& [id, data] : graph.nodes) { + freeNodes.push_back(id); + } + auto& freeRuns = s_freeEdgeRuns[graph.group]; + freeRuns.insert(freeRuns.end(), graph.edgeRuns.begin(), graph.edgeRuns.end()); + iterator = s_graphs.erase(iterator); + } else { + ++iterator; + } + } + + for (auto iterator = s_queries.begin(); iterator != s_queries.end();) { + if (iterator->second.owner == &mod) { + s_freeQueries.push_back(iterator->first); + iterator = s_queries.erase(iterator); + } else { + ++iterator; + } + } + for (auto iterator = s_events.begin(); iterator != s_events.end();) { + if (iterator->second.owner == &mod) { + s_freeEvents.push_back(iterator->first); + iterator = s_events.erase(iterator); + } else { + ++iterator; + } + } + + for (auto iterator = s_messagesByHandle.begin(); iterator != s_messagesByHandle.end();) { + if (iterator->second->owner == &mod) { + s_freeMessages.push_back(iterator->second->id); + s_messages.erase(iterator->second->id); + iterator = s_messagesByHandle.erase(iterator); + } else { + ++iterator; + } + } + for (auto iterator = s_overrides.begin(); iterator != s_overrides.end();) { + std::erase_if(iterator->second, [&](const auto& record) { return record.owner == &mod; }); + iterator = iterator->second.empty() ? s_overrides.erase(iterator) : std::next(iterator); + } +} + +} // namespace + +bool bind_resource(const void* bmgData, uint16_t group) { + if (bmgData == nullptr || !valid_group(group)) { + return false; + } + ResourceInfo resource; + if (!parse_resource(bmgData, group, resource)) { + DuskLog.error( + "message resource for group {} violates the custom ID reservation or is malformed", + group); + s_resources.insert_or_assign( + bmgData, ResourceInfo{.bmg = static_cast(bmgData), .group = group}); + return false; + } + s_resources.insert_or_assign(bmgData, resource); + for (const auto& patch : s_patches) { + if (patch.group != group || !patch.active) { + continue; + } + const bool resolves = + patch.edge ? + patch.index < resource.edgeCount && target_in_resource(resource, patch.target) : + patch.index < resource.nodeCount && node_resolves(resource, patch.node); + if (resolves) { + continue; + } + const uint64_t key = static_cast(patch.edge) << 63 | + static_cast(group) << 32 | patch.index; + if (s_warnedUnresolved.insert(key).second) { + DuskLog.error("[{}] flow {} patch {}:{:#06x} is unresolved for the loaded resource", + patch.owner->metadata.id, patch.edge ? "edge" : "node", group, patch.index); + } + } + return true; +} + +bool resolve_node(const void* bmgData, uint16_t nodeIndex, FlowNodeData& outNode) { + const auto* resource = find_resource(bmgData); + if (resource == nullptr || nodeIndex == kEnd) { + return false; + } + const auto unresolved = [&] { + const uint64_t key = static_cast(resource->group) << 32 | nodeIndex; + if (s_warnedUnresolved.insert(key).second) { + DuskLog.error("flow node {}:{:#06x} is unresolved; terminating at END", resource->group, + nodeIndex); + } + return false; + }; + if (nodeIndex < kCustomNodeMin) { + if (nodeIndex >= resource->nodeCount) { + return unresolved(); + } + if (const auto* patch = winning_patch(resource->group, nodeIndex, false)) { + outNode = patch->node; + } else { + std::memcpy(outNode.bytes, resource->nodes + static_cast(nodeIndex) * 8, 8); + } + } else { + const auto* graph = graph_for_node(resource->group, nodeIndex, true); + if (graph == nullptr) { + return unresolved(); + } + outNode = *graph->nodes.at(nodeIndex); + } + if (!node_resolves(*resource, outNode)) { + return unresolved(); + } + return true; +} + +bool resolve_edge(const void* bmgData, uint16_t edgeIndex, uint16_t& outTarget) { + const auto* resource = find_resource(bmgData); + if (resource == nullptr) { + outTarget = kEnd; + return false; + } + const auto unresolved = [&] { + const uint64_t key = + uint64_t{1} << 63 | static_cast(resource->group) << 32 | edgeIndex; + if (s_warnedUnresolved.insert(key).second) { + DuskLog.error( + "flow edge {}:{:#06x} is unresolved; targeting END", resource->group, edgeIndex); + } + outTarget = kEnd; + return false; + }; + if (edgeIndex < kCustomEdgeMin) { + if (edgeIndex >= resource->edgeCount) { + return unresolved(); + } + if (const auto* patch = winning_patch(resource->group, edgeIndex, true)) { + outTarget = patch->target; + } else { + outTarget = read_bits(resource->edges + static_cast(edgeIndex) * 2); + } + } else { + const auto* graph = graph_for_edge(resource->group, edgeIndex, true); + if (graph == nullptr) { + return unresolved(); + } + outTarget = graph->edges.at(edgeIndex); + } + if (!target_in_resource(*resource, outTarget)) { + return unresolved(); + } + return true; +} + +bool resolve_message_entry(const void* bmgData, uint16_t messageIndex, MessageEntryData& outEntry) { + const auto* resource = find_resource(bmgData); + if (resource == nullptr) { + return false; + } + if (messageIndex < kCustomMessageMin) { + if (messageIndex >= resource->entryCount) { + return false; + } + std::memcpy(outEntry.bytes, + resource->entries + static_cast(messageIndex) * resource->entrySize, + sizeof(outEntry.bytes)); + return true; + } + const auto found = s_messages.find(messageIndex); + if (found == s_messages.end() || found->second->group != resource->group) { + return false; + } + const auto variant = active_variant_shared(*found->second); + if (variant == nullptr) { + return false; + } + outEntry = variant->entry; + return true; +} + +uint16_t dispatch_query(uint16_t queryId, const void* speakerActor, uint16_t parameter, + uint8_t resultCount, FlowQueryPhase phase, uint16_t nodeIndex) { + const auto found = s_queries.find(queryId); + if (found == s_queries.end()) { + if (s_warnedMissingCallbacks.insert(queryId).second) { + DuskLog.warn("custom flow query {:#06x} is not registered; using result zero", queryId); + } + return 0; + } + const QueryRecord local = found->second; + const FlowQueryContext context{ + speakerActor, parameter, resultCount, static_cast(phase)}; + try { + const uint16_t result = local.fn(local.owner->context.get(), &context, local.userData); + if (result < resultCount) { + return result; + } + dusk::mods::fail_mod(*local.owner, MOD_INVALID_ARGUMENT, + fmt::format("flow query '{}' returned {} for {} results at node {:#06x}", + local.debugName, result, resultCount, nodeIndex)); + } catch (const std::exception& error) { + dusk::mods::fail_mod(*local.owner, MOD_ERROR, + fmt::format("exception in flow query '{}': {}", local.debugName, error.what())); + } catch (...) { + dusk::mods::fail_mod(*local.owner, MOD_ERROR, + fmt::format("unknown exception in flow query '{}'", local.debugName)); + } + return 0; +} + +void dispatch_event(uint8_t eventId, const void* speakerActor, const uint8_t parameters[4]) { + const auto found = s_events.find(eventId); + if (found == s_events.end()) { + const uint32_t warningKey = 0x10000 | eventId; + if (s_warnedMissingCallbacks.insert(warningKey).second) { + DuskLog.warn("custom flow event {:#04x} is not registered; continuing", eventId); + } + return; + } + const EventRecord local = found->second; + FlowEventContext context{.speaker_actor = speakerActor}; + std::memcpy(context.parameters, parameters, sizeof(context.parameters)); + try { + local.fn(local.owner->context.get(), &context, local.userData); + } catch (const std::exception& error) { + dusk::mods::fail_mod(*local.owner, MOD_ERROR, + fmt::format("exception in flow event '{}': {}", local.debugName, error.what())); + } catch (...) { + dusk::mods::fail_mod(*local.owner, MOD_ERROR, + fmt::format("unknown exception in flow event '{}'", local.debugName)); + } +} + +bool custom_message_group(uint16_t messageId, uint16_t& outGroup) { + const auto found = s_messages.find(messageId); + if (found == s_messages.end()) { + return false; + } + outGroup = found->second->group; + return true; +} + +bool custom_message_for_control( + JMessage::TControl* control, uint16_t messageId, const void*& outEntry, const char*& outText) { + const auto found = s_messages.find(messageId); + if (found == s_messages.end()) { + return false; + } + const auto variant = active_variant_shared(*found->second); + if (variant == nullptr) { + return false; + } + outEntry = &variant->entry; + outText = reinterpret_cast(variant->text.data()); + retain_binding(control, nullptr, variant, nullptr); + return true; +} + +static const JMessage::TResource* processor_resource_for_group( + const JMessage::TProcessor* processor, uint16_t group) { + if (processor == nullptr) { + return nullptr; + } + const auto matches_group = [group](const JMessage::TResource* resource) { + if (resource == nullptr) { + return false; + } + const auto* info = find_resource(resource->oParse_THeader_.getRaw()); + return info != nullptr && info->group == group; + }; + if (matches_group(processor->getResourceCache())) { + return processor->getResourceCache(); + } + if (processor->getResourceContainer() == nullptr) { + return nullptr; + } + const auto* resources = processor->getResourceContainer()->getResourceContainer(); + JGadget::TContainerEnumerator_const iterator{resources}; + while (iterator) { + const JMessage::TResource& resource = *iterator; + if (matches_group(&resource)) { + return &resource; + } + } + return nullptr; +} + +bool message_code_for_id( + const JMessage::TProcessor* processor, uint32_t messageId, uint32_t& outCode) { + if (processor == nullptr || messageId > std::numeric_limits::max()) { + return false; + } + uint16_t group = 0; + if (!custom_message_group(static_cast(messageId), group)) { + return false; + } + const auto* resource = processor_resource_for_group(processor, group); + if (resource == nullptr) { + return false; + } + outCode = static_cast(resource->getGroupID()) << 16 | messageId; + return true; +} + +bool custom_message_for_processor(JMessage::TControl* control, + const JMessage::TProcessor* processor, uint16_t messageIndex, + const JMessage::TResource*& outResource, const void*& outEntry, const char*& outText) { + const auto found = s_messages.find(messageIndex); + if (found == s_messages.end()) { + return false; + } + const auto* resource = processor_resource_for_group(processor, found->second->group); + const auto variant = active_variant_shared(*found->second); + if (resource == nullptr || variant == nullptr) { + return false; + } + outResource = resource; + outEntry = &variant->entry; + outText = reinterpret_cast(variant->text.data()); + retain_binding(control, processor, variant, nullptr); + return true; +} + +static bool resolve_message_impl(JMessage::TControl* control, const JMessage::TProcessor* processor, + const void* bmgData, uint16_t messageIndex, const void* nativeEntry, const char* nativeText, + const void*& outEntry, const char*& outText) { + const auto* resource = find_resource(bmgData); + if (resource == nullptr) { + return false; + } + if (control != nullptr) { + s_bindings.try_emplace(control); + } + if (messageIndex >= kCustomMessageMin) { + const auto found = s_messages.find(messageIndex); + if (found == s_messages.end() || found->second->group != resource->group) { + return false; + } + const auto variant = active_variant_shared(*found->second); + if (variant == nullptr) { + return false; + } + outEntry = &variant->entry; + outText = reinterpret_cast(variant->text.data()); + retain_binding(control, processor, variant, nullptr); + return true; + } + if (nativeEntry == nullptr || nativeText == nullptr) { + return false; + } + const auto* bytes = static_cast(nativeEntry); + const auto messageId = read_bits(bytes + 4); + auto overrideText = resolve_override(*resource, messageId, nativeText); + if (overrideText == nullptr) { + return false; + } + outEntry = nativeEntry; + outText = reinterpret_cast(overrideText->data()); + retain_binding(control, processor, nullptr, overrideText); + return true; +} + +bool resolve_message_for_control(JMessage::TControl* control, const void* bmgData, + uint16_t messageIndex, const void* nativeEntry, const char* nativeText, const void*& outEntry, + const char*& outText) { + return resolve_message_impl( + control, nullptr, bmgData, messageIndex, nativeEntry, nativeText, outEntry, outText); +} + +bool resolve_message(const JMessage::TProcessor* processor, const void* bmgData, + uint16_t messageIndex, const void* nativeEntry, const char* nativeText, const void*& outEntry, + const char*& outText) { + return resolve_message_impl(control_for_processor(processor), processor, bmgData, messageIndex, + nativeEntry, nativeText, outEntry, outText); +} + +void release_message_control(const JMessage::TControl* control) { + s_bindings.erase(control); +} + +void release_message_processor(const JMessage::TProcessor* processor) { + s_processorBindings.erase(processor); +} + +} // namespace dusk::flow + +namespace dusk::mods::svc { +namespace { + +ModResult begin_graph(ModContext* context, uint16_t group, FlowGraphHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || outHandle == nullptr || !flow::valid_group(group)) { + return MOD_INVALID_ARGUMENT; + } + const FlowGraphHandle handle = flow::s_nextHandle++; + flow::s_graphs.emplace( + handle, flow::GraphRecord{.handle = handle, .owner = mod, .group = group}); + *outHandle = handle; + return MOD_OK; +} + +flow::GraphRecord* owned_graph(ModContext* context, FlowGraphHandle handle) { + const auto* mod = mod_from_context(context); + const auto found = flow::s_graphs.find(handle); + return mod != nullptr && found != flow::s_graphs.end() && found->second.owner == mod ? + &found->second : + nullptr; +} + +ModResult allocate_node(ModContext* context, FlowGraphHandle handle, uint16_t* outId) { + if (outId != nullptr) { + *outId = 0; + } + auto* graph = owned_graph(context, handle); + if (graph == nullptr || graph->committed || outId == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auto& freeNodes = flow::s_freeNodes[graph->group]; + uint16_t id = 0; + if (!freeNodes.empty()) { + id = freeNodes.back(); + freeNodes.pop_back(); + } else if (flow::s_nextNode[graph->group] < flow::kEnd) { + id = flow::s_nextNode[graph->group]++; + } else { + DuskLog.error("[{}] custom flow node pool for group {} is exhausted", + graph->owner->metadata.id, graph->group); + return MOD_UNAVAILABLE; + } + graph->nodes.emplace(id, std::nullopt); + *outId = id; + return MOD_OK; +} + +ModResult add_edges(ModContext* context, FlowGraphHandle handle, const uint16_t* targets, + uint16_t count, uint16_t* outFirst) { + if (outFirst != nullptr) { + *outFirst = 0; + } + auto* graph = owned_graph(context, handle); + if (graph == nullptr || graph->committed || targets == nullptr || count == 0 || + outFirst == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + auto& freeRuns = flow::s_freeEdgeRuns[graph->group]; + const auto run = + std::ranges::find_if(freeRuns, [&](const auto& item) { return item.second >= count; }); + uint16_t first = 0; + if (run != freeRuns.end()) { + first = run->first; + run->first += count; + run->second -= count; + if (run->second == 0) { + freeRuns.erase(run); + } + } else if (static_cast(flow::s_nextEdge[graph->group]) + count <= flow::kEnd) { + first = flow::s_nextEdge[graph->group]; + flow::s_nextEdge[graph->group] = static_cast(first + count); + } else { + DuskLog.error("[{}] custom flow edge pool for group {} is exhausted", + graph->owner->metadata.id, graph->group); + return MOD_UNAVAILABLE; + } + for (uint16_t i = 0; i < count; ++i) { + graph->edges.emplace(static_cast(first + i), targets[i]); + } + graph->edgeRuns.emplace_back(first, count); + *outFirst = first; + return MOD_OK; +} + +ModResult fill_node( + ModContext* context, FlowGraphHandle handle, uint16_t nodeIndex, const FlowNodeData* node) { + auto* graph = owned_graph(context, handle); + if (graph == nullptr || graph->committed || node == nullptr) { + return MOD_INVALID_ARGUMENT; + } + const auto slot = graph->nodes.find(nodeIndex); + if (slot == graph->nodes.end()) { + return MOD_INVALID_ARGUMENT; + } + slot->second = *node; + return MOD_OK; +} + +ModResult patch_node( + ModContext* context, FlowGraphHandle handle, uint16_t nodeIndex, const FlowNodeData* node) { + auto* graph = owned_graph(context, handle); + if (graph == nullptr || graph->committed) { + return MOD_INVALID_ARGUMENT; + } + return flow::add_patch(*graph, nodeIndex, false, node, 0); +} + +ModResult patch_edge( + ModContext* context, FlowGraphHandle handle, uint16_t edgeIndex, uint16_t targetNode) { + auto* graph = owned_graph(context, handle); + if (graph == nullptr || graph->committed) { + return MOD_INVALID_ARGUMENT; + } + return flow::add_patch(*graph, edgeIndex, true, nullptr, targetNode); +} + +ModResult commit_graph(ModContext* context, FlowGraphHandle handle) { + auto* graph = owned_graph(context, handle); + const bool hasPatches = + graph != nullptr && std::ranges::any_of(flow::s_patches, + [&](const auto& patch) { return patch.graph == handle; }); + if (graph == nullptr || graph->committed || + (graph->nodes.empty() && graph->edges.empty() && !hasPatches) || + std::ranges::any_of( + graph->nodes, [](const auto& item) { return !item.second.has_value(); })) + { + return MOD_INVALID_ARGUMENT; + } + for (const auto& [id, node] : graph->nodes) { + if (!flow::validate_node_owner(*graph->owner, graph->group, *node)) { + return MOD_INVALID_ARGUMENT; + } + } + for (const auto& [id, target] : graph->edges) { + if (!flow::valid_target_owner(*graph->owner, graph->group, target)) { + return MOD_INVALID_ARGUMENT; + } + } + for (const auto& patch : flow::s_patches) { + if (patch.graph != handle) { + continue; + } + const bool valid = patch.edge ? + flow::valid_target_owner(*graph->owner, graph->group, patch.target) : + flow::validate_node_owner(*graph->owner, graph->group, patch.node); + if (!valid) { + return MOD_INVALID_ARGUMENT; + } + } + for (auto& patch : flow::s_patches) { + if (patch.graph == handle) { + patch.sequence = flow::s_nextSequence++; + patch.active = true; + } + } + graph->committed = true; + return MOD_OK; +} + +ModResult remove_graph(ModContext* context, FlowGraphHandle handle) { + auto* graph = owned_graph(context, handle); + if (graph == nullptr) { + return MOD_INVALID_ARGUMENT; + } + std::erase_if(flow::s_patches, [&](const auto& patch) { return patch.graph == handle; }); + flow::s_graphs.erase(handle); + return MOD_OK; +} + +ModResult register_query(ModContext* context, const char* debugName, FlowQueryFn fn, void* userData, + FlowQueryId* outId) { + if (outId != nullptr) { + *outId = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || !flow::valid_debug_name(debugName) || fn == nullptr || outId == nullptr) { + return MOD_INVALID_ARGUMENT; + } + uint16_t id = 0; + if (!flow::s_freeQueries.empty()) { + id = flow::s_freeQueries.back(); + flow::s_freeQueries.pop_back(); + } else if (flow::s_nextQuery <= flow::kCustomMax) { + id = flow::s_nextQuery++; + } else { + DuskLog.error("[{}] flow query pool exhausted (32767 registrations)", mod->metadata.id); + return MOD_UNAVAILABLE; + } + flow::s_queries.emplace(id, flow::QueryRecord{mod, debugName, fn, userData}); + *outId = id; + return MOD_OK; +} + +ModResult register_event(ModContext* context, const char* debugName, FlowEventFn fn, void* userData, + FlowEventId* outId) { + if (outId != nullptr) { + *outId = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || !flow::valid_debug_name(debugName) || fn == nullptr || outId == nullptr) { + return MOD_INVALID_ARGUMENT; + } + uint8_t id = 0; + if (!flow::s_freeEvents.empty()) { + id = flow::s_freeEvents.back(); + flow::s_freeEvents.pop_back(); + } else if (flow::s_nextEvent <= 0xfe) { + id = static_cast(flow::s_nextEvent++); + } else { + DuskLog.error("[{}] flow event pool exhausted (127 registrations)", mod->metadata.id); + return MOD_UNAVAILABLE; + } + flow::s_events.emplace(id, flow::EventRecord{mod, debugName, fn, userData}); + *outId = id; + return MOD_OK; +} + +ModResult override_message(ModContext* context, uint16_t group, uint16_t messageId, + uint8_t language, const uint8_t* text, size_t textSize, MessageOverrideHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || outHandle == nullptr || !flow::valid_group(group) || + messageId >= flow::kCustomMessageMin || !flow::valid_encoded_text(text, textSize)) + { + return MOD_INVALID_ARGUMENT; + } + flow::OverrideRecord record{ + .handle = flow::s_nextHandle++, + .owner = mod, + .sequence = flow::s_nextSequence++, + .text = {text, text + textSize}, + }; + *outHandle = record.handle; + flow::s_overrides[{group, messageId, language}].push_back(std::move(record)); + return MOD_OK; +} + +ModResult override_message_fn(ModContext* context, uint16_t group, uint16_t messageId, + uint8_t language, MessageOverrideFn fn, void* userData, MessageOverrideHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || outHandle == nullptr || !flow::valid_group(group) || + messageId >= flow::kCustomMessageMin || fn == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + flow::OverrideRecord record{ + .handle = flow::s_nextHandle++, + .owner = mod, + .sequence = flow::s_nextSequence++, + .callback = true, + .fn = fn, + .userData = userData, + }; + *outHandle = record.handle; + flow::s_overrides[{group, messageId, language}].push_back(std::move(record)); + return MOD_OK; +} + +ModResult remove_override(ModContext* context, MessageOverrideHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + for (auto iterator = flow::s_overrides.begin(); iterator != flow::s_overrides.end(); ++iterator) + { + const size_t removed = std::erase_if(iterator->second, + [&](const auto& record) { return record.handle == handle && record.owner == mod; }); + if (removed != 0) { + if (iterator->second.empty()) { + flow::s_overrides.erase(iterator); + } + return MOD_OK; + } + } + return MOD_INVALID_ARGUMENT; +} + +ModResult register_message(ModContext* context, uint16_t group, const MessageVariantData* variants, + size_t variantCount, MessageId* outId, MessageHandle* outHandle) { + if (outId != nullptr) { + *outId = 0; + } + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || !flow::valid_group(group) || variants == nullptr || variantCount == 0 || + outId == nullptr || outHandle == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + uint16_t messageId = 0; + if (!flow::s_freeMessages.empty()) { + messageId = flow::s_freeMessages.back(); + flow::s_freeMessages.pop_back(); + } else if (flow::s_nextMessage <= flow::kCustomMessageMax) { + messageId = flow::s_nextMessage++; + } else { + DuskLog.error("[{}] custom message ID pool exhausted", mod->metadata.id); + return MOD_UNAVAILABLE; + } + auto message = std::make_shared(); + message->handle = flow::s_nextHandle++; + message->id = messageId; + message->group = group; + message->owner = mod; + for (size_t i = 0; i < variantCount; ++i) { + const auto& input = variants[i]; + if (read_bits(input.entry.bytes) != 0 || + read_bits(input.entry.bytes + 4) != 0 || + !flow::valid_encoded_text(input.text, input.text_size) || + message->variants.contains(input.language)) + { + return MOD_INVALID_ARGUMENT; + } + auto variant = std::make_shared(); + variant->language = input.language; + variant->entry = input.entry; + write_bits(variant->entry.bytes + 4, message->id); + variant->text.assign(input.text, input.text + input.text_size); + message->variants.emplace(input.language, std::move(variant)); + } + flow::s_messages.emplace(message->id, message); + flow::s_messagesByHandle.emplace(message->handle, message); + *outId = message->id; + *outHandle = message->handle; + return MOD_OK; +} + +ModResult remove_message(ModContext* context, MessageHandle handle) { + auto* mod = mod_from_context(context); + const auto found = flow::s_messagesByHandle.find(handle); + if (mod == nullptr || handle == 0 || found == flow::s_messagesByHandle.end() || + found->second->owner != mod) + { + return MOD_INVALID_ARGUMENT; + } + flow::s_messages.erase(found->second->id); + flow::s_messagesByHandle.erase(found); + return MOD_OK; +} + +constexpr FlowService s_flowService{ + .header = SERVICE_HEADER(FlowService, FLOW_SERVICE_MAJOR, FLOW_SERVICE_MINOR), + .begin_graph = begin_graph, + .allocate_node = allocate_node, + .add_edges = add_edges, + .fill_node = fill_node, + .patch_node = patch_node, + .patch_edge = patch_edge, + .commit_graph = commit_graph, + .remove_graph = remove_graph, + .register_query = register_query, + .register_event = register_event, +}; + +constexpr MessageService s_messageService{ + .header = SERVICE_HEADER(MessageService, MESSAGE_SERVICE_MAJOR, MESSAGE_SERVICE_MINOR), + .override_message = override_message, + .override_message_fn = override_message_fn, + .remove_override = remove_override, + .register_message = register_message, + .remove_message = remove_message, +}; + +} // namespace + +constinit const ServiceModule g_flowModule{ + .id = FLOW_SERVICE_ID, + .majorVersion = FLOW_SERVICE_MAJOR, + .minorVersion = FLOW_SERVICE_MINOR, + .service = &s_flowService, + .initialize = flow::reset_state, + .modDetached = flow::remove_mod, + .shutdown = flow::reset_state, +}; + +constinit const ServiceModule g_messageModule{ + .id = MESSAGE_SERVICE_ID, + .majorVersion = MESSAGE_SERVICE_MAJOR, + .minorVersion = MESSAGE_SERVICE_MINOR, + .service = &s_messageService, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/flow.hpp b/src/dusk/mods/svc/flow.hpp new file mode 100644 index 0000000000..7e054a7e0a --- /dev/null +++ b/src/dusk/mods/svc/flow.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "helpers/bits.hpp" +#include "mods/svc/flow.h" +#include "mods/svc/message.h" + +#include + +namespace JMessage { +struct TControl; +struct TProcessor; +struct TResource; +} // namespace JMessage + +namespace dusk::flow { + +inline constexpr uint16_t kCustomNodeMin = 0x8000; +inline constexpr uint16_t kCustomEdgeMin = 0x8000; +inline constexpr uint16_t kCustomMessageMin = 0x8000; +inline constexpr uint16_t kCustomMessageMax = 0xfeff; +inline constexpr uint16_t kCustomQueryMin = 0x8000; +inline constexpr uint8_t kCustomEventMin = 0x80; +inline constexpr uint16_t kEnd = 0xffff; + +/* Associates a parsed BMG resource with its service group and validates its reserved ranges. */ +bool bind_resource(const void* bmgData, uint16_t group); + +/* All resolver outputs are copies so callbacks may mutate service registrations safely. */ +bool resolve_node(const void* bmgData, uint16_t nodeIndex, FlowNodeData& outNode); +bool resolve_edge(const void* bmgData, uint16_t edgeIndex, uint16_t& outTarget); +bool resolve_message_entry( + const void* bmgData, uint16_t messageIndex, MessageEntryData& outEntry); + +uint16_t dispatch_query(uint16_t queryId, const void* speakerActor, uint16_t parameter, + uint8_t resultCount, FlowQueryPhase phase, uint16_t nodeIndex); +void dispatch_event(uint8_t eventId, const void* speakerActor, const uint8_t parameters[4]); + +bool custom_message_group(uint16_t messageId, uint16_t& outGroup); +bool custom_message_for_control( + JMessage::TControl* control, uint16_t messageId, const void*& outEntry, const char*& outText); + +/* JMessage bridges. Return true when the service supplied a custom or overridden value. */ +bool message_code_for_id( + const JMessage::TProcessor* processor, uint32_t messageId, uint32_t& outCode); +bool custom_message_for_processor(JMessage::TControl* control, + const JMessage::TProcessor* processor, uint16_t messageIndex, + const JMessage::TResource*& outResource, const void*& outEntry, const char*& outText); +bool resolve_message_for_control(JMessage::TControl* control, const void* bmgData, + uint16_t messageIndex, const void* nativeEntry, const char* nativeText, const void*& outEntry, + const char*& outText); +bool resolve_message(const JMessage::TProcessor* processor, const void* bmgData, + uint16_t messageIndex, const void* nativeEntry, const char* nativeText, const void*& outEntry, + const char*& outText); +void release_message_control(const JMessage::TControl* control); +void release_message_processor(const JMessage::TProcessor* processor); + +namespace detail { +inline bool find_section( + const uint8_t* bmg, uint32_t tag, const uint8_t*& outSection, size_t& outSize) { + const uint32_t sectionCount = read_bits(bmg + 0x0c); + size_t offset = 0x20; + for (uint32_t i = 0; i < sectionCount; ++i) { + if (offset > std::numeric_limits::max() - 8) { + return false; + } + const size_t sectionSize = read_bits(bmg + offset + 4); + if (sectionSize < 8 || offset > std::numeric_limits::max() - sectionSize) { + return false; + } + if (read_bits(bmg + offset) == tag) { + outSection = bmg + offset; + outSize = sectionSize; + return true; + } + offset += sectionSize; + } + return false; +} +} // namespace detail + +} // namespace dusk::flow diff --git a/src/dusk/mods/svc/game_mode.cpp b/src/dusk/mods/svc/game_mode.cpp new file mode 100644 index 0000000000..cfa655baf8 --- /dev/null +++ b/src/dusk/mods/svc/game_mode.cpp @@ -0,0 +1,199 @@ +#include "mods/svc/game_mode.h" +#include "dusk/game_mode.hpp" + +#include "config.hpp" +#include "registry.hpp" +#include "slot_map.hpp" + +#include "aurora/lib/logging.hpp" +#include "dusk/mod_loader.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "fmt/format.h" + +#include +#include +#include +#include +#include +#include + +namespace dusk::mods::svc::game_mode_impl { +namespace { + +aurora::Module Log("dusk::mods::game_mode"); + +// Track ownership of mod ID to game modes +std::unordered_map> s_gameModesByMod; + +template +bool invoke_mod_callback(LoadedMod& mod, const char* what, Fn&& fn) { + if (!mod.active) { + return false; + } + + ModError error = MOD_ERROR_INIT; + ModResult result = MOD_OK; + try { + result = fn(&error); + } catch (const std::exception& exception) { + fail_mod(mod, MOD_ERROR, fmt::format("exception in {}: {}", what, exception.what())); + return false; + } catch (...) { + fail_mod(mod, MOD_ERROR, fmt::format("unknown exception in {}", what)); + return false; + } + + if (result != MOD_OK && mod.active) { + fail_mod(mod, result, + error.message[0] != '\0' ? + error.message : + fmt::format("{} failed with result {}", what, static_cast(result))); + } + return result == MOD_OK && mod.active; +} + +gamemode::GameMode::Callback wrap_callback( + LoadedMod& mod, GameModeCallback callback, void* userData, const char* what) { + return [&mod, callback, userData, what] { + return invoke_mod_callback( + mod, what, [callback, userData](ModError* error) { return callback(userData, error); }); + }; +} + +gamemode::GameMode::NewSaveSelectCallback wrap_new_save_select_callback( + LoadedMod& mod, GameModeNewSaveSelectCallback callback, void* userData, const char* what) { + return [&mod, callback, userData, what](GameModeNewSaveState* state) { + return invoke_mod_callback( + mod, what, [=](ModError* error) { return callback(userData, state, error); }); + }; +} + +std::string get_mod_game_mode_id(ModContext* ctx, const std::string& id) { + // Include the mod ID to prevent clashes and normalize to lowercase + std::string fullId = id + "_" + ctx->mod->metadata.id; + std::transform(fullId.begin(), fullId.end(), fullId.begin(), + [](unsigned char c) { return std::tolower(c); }); + return fullId; +} + +void game_mode_remove_mod(LoadedMod& mod) { + const auto it = s_gameModesByMod.find(mod.metadata.id); + if (it != s_gameModesByMod.end()) { + for (const auto& id : it->second) { + gamemode::getGameModeManager().unregisterGameMode(id); + } + s_gameModesByMod.erase(it); + } +} +} // namespace + +ModResult register_game_mode(ModContext* ctx, const GameModeDesc* desc) { + auto* owner = mod_from_context(ctx); + if (owner == nullptr || desc == nullptr || desc->struct_size < sizeof(GameModeDesc)) { + return MOD_INVALID_ARGUMENT; + } + + std::string id; + if (!desc->game_mode_id) { + Log.error("Attempted to register a game mode with a null ID"); + return MOD_ERROR; + } + id = desc->game_mode_id; + if (id.empty()) { + Log.error("Attempted to register a game mode with an empty ID"); + return MOD_ERROR; + } + id = get_mod_game_mode_id(ctx, id); + + std::string fullName; + if (!desc->full_name) { + Log.warn("Game mode {} has no display name; using its ID", id); + fullName = id; + } else { + fullName = desc->full_name; + if (fullName.empty()) { + Log.warn("Game mode {} has an empty display name; using its ID", id); + fullName = id; + } + } + + gamemode::GameMode mode{id, fullName, desc->save_name}; + if (desc->on_activated) { + mode.mOnActivatedFunction = wrap_callback( + *owner, desc->on_activated, desc->user_data, "game mode activation callback"); + } + if (desc->on_deactivated) { + mode.mOnDeactivatedFunction = wrap_callback( + *owner, desc->on_deactivated, desc->user_data, "game mode deactivation callback"); + } + if (desc->on_play) { + mode.mOnPlayFunction = + wrap_callback(*owner, desc->on_play, desc->user_data, "game mode play callback"); + } + if (desc->on_save_loaded) { + mode.mOnSaveLoadedFunction = wrap_callback( + *owner, desc->on_save_loaded, desc->user_data, "game mode save-loaded callback"); + } + if (desc->on_new_save) { + mode.mOnNewSaveFunction = wrap_callback( + *owner, desc->on_new_save, desc->user_data, "game mode new-save callback"); + } + if (desc->on_new_save_select) { + mode.mOnNewSaveSelectFunction = wrap_new_save_select_callback(*owner, + desc->on_new_save_select, desc->user_data, "game mode new-save selection callback"); + } + if (desc->on_game_reset) { + mode.mOnGameResetFunction = + wrap_callback(*owner, desc->on_game_reset, desc->user_data, "game mode reset callback"); + } + if (desc->on_tick) { + mode.mOnTickFunction = + wrap_callback(*owner, desc->on_tick, desc->user_data, "game mode tick callback"); + } + + gamemode::getGameModeManager().registerGameMode(mode); + s_gameModesByMod[ctx->mod->metadata.id].push_back(id); + return MOD_OK; +} + +ModResult unregister_game_mode(ModContext* ctx, const char* id) { + std::string fullId = get_mod_game_mode_id(ctx, id); + gamemode::getGameModeManager().unregisterGameMode(fullId); + + // Remove the game mode from the ownership map + auto it = s_gameModesByMod.find(ctx->mod->metadata.id); + if (it != s_gameModesByMod.end()) { + std::erase(it->second, fullId); + } + return MOD_OK; +} + +ModResult is_active(ModContext* ctx, const char* gameModeId, bool* out_active) { + *out_active = + gamemode::getGameModeManager().isCurrentGameMode(get_mod_game_mode_id(ctx, gameModeId)); + return MOD_OK; +} + +} // namespace dusk::mods::svc::game_mode_impl + +namespace dusk::mods::svc { +namespace { + +constexpr GameModeService s_gamemodeService{ + .header = SERVICE_HEADER(GameModeService, GAME_MODE_SERVICE_MAJOR, GAME_MODE_SERVICE_MINOR), + .register_game_mode = game_mode_impl::register_game_mode, + .unregister_game_mode = game_mode_impl::unregister_game_mode, + .is_active = game_mode_impl::is_active, +}; + +} // namespace + +constinit const ServiceModule g_gamemodeModule{ + .id = GAME_MODE_SERVICE_ID, + .majorVersion = GAME_MODE_SERVICE_MAJOR, + .minorVersion = GAME_MODE_SERVICE_MINOR, + .service = &s_gamemodeService, + .modDeactivating = game_mode_impl::game_mode_remove_mod, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/gfx.cpp b/src/dusk/mods/svc/gfx.cpp index 900640d4fb..098894007a 100644 --- a/src/dusk/mods/svc/gfx.cpp +++ b/src/dusk/mods/svc/gfx.cpp @@ -9,8 +9,10 @@ #include #include +#include #include +#include #include #include #include @@ -126,23 +128,34 @@ GfxSlot* resolve_owned_slot_locked(LoadedMod& mod, uint64_t handle, GfxSlotKind return &entry->value; } -void collect_mod_types_locked(LoadedMod& owner, std::vector& drawIds, +void take_mod_types_locked(LoadedMod& owner, std::vector& drawIds, std::vector& taskIds) { - s_slots.for_each([&](uint64_t, const auto& entry) { + std::vector drawHandles; + std::vector taskHandles; + s_slots.for_each([&](uint64_t handle, const auto& entry) { if (entry.owner != &owner) { return; } const auto& slot = entry.value; if (slot.kind == GfxSlotKind::DrawType && slot.auroraDrawId != aurora::gfx::InvalidDrawType) { - drawIds.push_back(slot.auroraDrawId); + drawHandles.push_back(handle); } else if ((slot.kind == GfxSlotKind::ComputeType || slot.kind == GfxSlotKind::PresentTarget) && slot.auroraTaskId != aurora::gfx::InvalidEncoderTask) { - taskIds.push_back(slot.auroraTaskId); + taskHandles.push_back(handle); } }); + for (const auto handle : drawHandles) { + auto* entry = s_slots.find(handle); + drawIds.push_back(std::exchange(entry->value.auroraDrawId, aurora::gfx::InvalidDrawType)); + } + for (const auto handle : taskHandles) { + auto* entry = s_slots.find(handle); + taskIds.push_back( + std::exchange(entry->value.auroraTaskId, aurora::gfx::InvalidEncoderTask)); + } } void unregister_aurora_types(const std::vector& drawIds, @@ -155,6 +168,49 @@ void unregister_aurora_types(const std::vector& drawIds } } +void gfx_mod_deactivating(LoadedMod& mod) { + std::vector drawIds; + std::vector taskIds; + { + std::lock_guard lock{s_mutex}; + take_mod_types_locked(mod, drawIds, taskIds); + } + unregister_aurora_types(drawIds, taskIds); + if (!drawIds.empty() || !taskIds.empty()) { + aurora::gfx::synchronize(); + } +} + +GfxAttachmentSemantic gfx_attachment_semantic(aurora::gfx::ColorAttachmentSemantic semantic) { + switch (semantic) { + case aurora::gfx::ColorAttachmentSemantic::SceneColor: + return GFX_ATTACHMENT_SCENE_COLOR; + case aurora::gfx::ColorAttachmentSemantic::Normal: + return GFX_ATTACHMENT_NORMAL; + case aurora::gfx::ColorAttachmentSemantic::Auxiliary: + return GFX_ATTACHMENT_AUXILIARY; + } + return GFX_ATTACHMENT_AUXILIARY; +} + +GfxRenderTargetLayout gfx_render_target_layout(const aurora::gfx::RenderTargetLayout& layout) { + GfxRenderTargetLayout result = GFX_RENDER_TARGET_LAYOUT_INIT; + result.key = layout.key; + result.color_attachment_count = + std::min(layout.colorAttachmentCount, GFX_MAX_COLOR_ATTACHMENTS); + for (uint32_t i = 0; i < result.color_attachment_count; ++i) { + result.color_attachments[i] = { + .semantic = gfx_attachment_semantic(layout.colorAttachments[i].semantic), + .format = static_cast(layout.colorAttachments[i].format), + .width = layout.colorAttachments[i].width, + .height = layout.colorAttachments[i].height, + }; + } + result.depth_stencil_format = static_cast(layout.depthStencilFormat); + result.sample_count = layout.sampleCount; + return result; +} + void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPassEncoder& pass, const void* payload, size_t payloadSize, void* userdata) { const auto handle = static_cast(reinterpret_cast(userdata)); @@ -184,12 +240,12 @@ void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPass .index_buffer = ctx.indexBuffer.Get(), .uniform_buffer = ctx.uniformBuffer.Get(), .storage_buffer = ctx.storageBuffer.Get(), - .color_format = static_cast(ctx.colorFormat), - .depth_format = static_cast(ctx.depthFormat), - .sample_count = ctx.sampleCount, - .target_width = ctx.targetWidth, - .target_height = ctx.targetHeight, + .color_format = static_cast( + ctx.layout.colorAttachments[GFX_SCENE_COLOR_ATTACHMENT_INDEX].format), + .depth_format = static_cast(ctx.layout.depthStencilFormat), + .sample_count = ctx.layout.sampleCount, .uses_reversed_z = aurora::gfx::uses_reversed_z(), + .layout = gfx_render_target_layout(ctx.layout), }; std::string failure; @@ -778,8 +834,10 @@ ModResult gfx_unregister_present_target(LoadedMod& mod, uint64_t handle) { auroraId = slot->auroraTaskId; } - aurora::gfx::unregister_encoder_task_type(auroraId); - aurora::gfx::synchronize(); + if (auroraId != aurora::gfx::InvalidEncoderTask) { + aurora::gfx::unregister_encoder_task_type(auroraId); + aurora::gfx::synchronize(); + } std::optional removed; { @@ -903,6 +961,7 @@ void gfx_run_stage( .game_viewport = gameViewport, }; + AuroraGXSync(); for (const auto& entry : entries) { { std::lock_guard lock{s_mutex}; @@ -924,6 +983,7 @@ void gfx_run_stage( fail_mod(*entry.owner, MOD_ERROR, "unknown exception in gfx stage callback"); } + AuroraGXSync(); if (aurora::gfx::is_offscreen() != wasOffscreen) { aurora::gfx::ResolvedTargets discarded; aurora::gfx::resolve_pass( @@ -935,17 +995,8 @@ void gfx_run_stage( } } -void gfx_remove_mod(LoadedMod& mod) { - std::vector drawIds; - std::vector taskIds; - { - std::lock_guard lock{s_mutex}; - collect_mod_types_locked(mod, drawIds, taskIds); - } - unregister_aurora_types(drawIds, taskIds); - if (!drawIds.empty() || !taskIds.empty()) { - aurora::gfx::synchronize(); - } +void gfx_mod_detached(LoadedMod& mod) { + gfx_mod_deactivating(mod); std::vector entries; { @@ -966,7 +1017,7 @@ void gfx_remove_mod(LoadedMod& mod) { } } -void gfx_drain_worker_failures() { +void gfx_frame_begin() { std::vector failures; { std::lock_guard lock{s_mutex}; @@ -979,7 +1030,7 @@ void gfx_drain_worker_failures() { for (const auto& failure : failures) { for (auto& mod : ModLoader::instance().mods()) { if (mod.metadata.id == failure.modId && mod.active) { - gfx_remove_mod(mod); + gfx_mod_detached(mod); fail_mod(mod, MOD_ERROR, failure.message); break; } @@ -1030,6 +1081,19 @@ ModResult gfx_get_device_info(ModContext* context, GfxDeviceInfo* outInfo) { return MOD_OK; } +ModResult gfx_get_scene_target_layout(ModContext* context, GfxRenderTargetLayout* outLayout) { + if (outLayout == nullptr || outLayout->struct_size < sizeof(GfxRenderTargetLayout) || + mod_from_context(context) == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + + const uint32_t structSize = outLayout->struct_size; + *outLayout = gfx_render_target_layout(aurora::gfx::scene_render_target_layout()); + outLayout->struct_size = structSize; + return MOD_OK; +} + void* gfx_get_proc_address(ModContext* context, const char* name) { if (mod_from_context(context) == nullptr || name == nullptr) { return nullptr; @@ -1327,6 +1391,7 @@ constexpr GfxService s_gfxService{ .resize_present_target = gfx_resize_present_target_impl, .unregister_present_target = gfx_unregister_present_target_impl, .push_present = gfx_push_present_impl, + .get_scene_target_layout = gfx_get_scene_target_layout, }; } // namespace @@ -1336,8 +1401,9 @@ constinit const ServiceModule g_gfxModule{ .majorVersion = GFX_SERVICE_MAJOR, .minorVersion = GFX_SERVICE_MINOR, .service = &s_gfxService, - .modDetached = gfx_remove_mod, - .frameBegin = gfx_drain_worker_failures, + .modDeactivating = gfx_mod_deactivating, + .modDetached = gfx_mod_detached, + .frameBegin = gfx_frame_begin, }; } // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/item.cpp b/src/dusk/mods/svc/item.cpp new file mode 100644 index 0000000000..f80fc16b9b --- /dev/null +++ b/src/dusk/mods/svc/item.cpp @@ -0,0 +1,151 @@ +#include "item.hpp" + +#include "registry.hpp" + +#include "dusk/mods/item.hpp" +#include "dusk/mods/loader/loader.hpp" + +#include "d/d_item_data.h" + +#include + +namespace dusk::mods::svc { +namespace { + +constexpr size_t kMaxCheckNameLength = 256; +constexpr uint32_t kGiveFlagMask = ITEM_GIVE_SILENT | ITEM_GIVE_RESOLVE; + +bool is_valid_check_name(const char* name) { + if (name == nullptr) { + return false; + } + const std::string_view view{name}; + return !view.empty() && view.size() <= kMaxCheckNameLength; +} + +ModResult item_set_check_override(ModContext* context, const char* name, uint8_t itemNo) { + auto* mod = mod_from_context(context); + if (mod == nullptr || !is_valid_check_name(name)) { + return MOD_INVALID_ARGUMENT; + } + return item_check_set_override(*mod, name, itemNo); +} + +ModResult item_clear_check_override(ModContext* context, const char* name) { + auto* mod = mod_from_context(context); + if (mod == nullptr || !is_valid_check_name(name)) { + return MOD_INVALID_ARGUMENT; + } + return item_check_clear_override(*mod, name); +} + +ModResult item_set_check_resolver(ModContext* context, const char* name, ItemCheckResolveFn fn, + void* userData, ItemCheckHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + + auto* mod = mod_from_context(context); + if (mod == nullptr || fn == nullptr || (name != nullptr && !is_valid_check_name(name))) { + return MOD_INVALID_ARGUMENT; + } + + ItemCheckHandle handle = 0; + const auto result = item_check_add_resolver(*mod, name, fn, userData, handle); + if (outHandle != nullptr) { + *outHandle = handle; + } + return result; +} + +ModResult item_clear_check_resolver(ModContext* context, ItemCheckHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return item_check_remove_resolver(*mod, handle); +} + +ModResult item_resolve_check( + ModContext* context, const char* name, uint8_t originalItemNo, uint8_t* outItem) { + if (mod_from_context(context) == nullptr || !is_valid_check_name(name) || outItem == nullptr) { + return MOD_INVALID_ARGUMENT; + } + *outItem = item_check(name, originalItemNo, nullptr); + return MOD_OK; +} + +ModResult item_give_item( + ModContext* context, const char* checkName, uint8_t itemNo, uint32_t flags) { + auto* mod = mod_from_context(context); + if (mod == nullptr || (checkName != nullptr && !is_valid_check_name(checkName)) || + (flags & ~kGiveFlagMask) != 0) + { + return MOD_INVALID_ARGUMENT; + } + if ((flags & ITEM_GIVE_RESOLVE) != 0) { + if (checkName == nullptr) { + return MOD_INVALID_ARGUMENT; + } + } else if (itemNo == dItemNo_NONE_e) { + return MOD_INVALID_ARGUMENT; + } + return item_give_enqueue(*mod, checkName, itemNo, flags); +} + +ModResult item_observe_gives( + ModContext* context, ItemGiveObserveFn fn, void* userData, ItemGiveHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + + auto* mod = mod_from_context(context); + if (mod == nullptr || fn == nullptr) { + return MOD_INVALID_ARGUMENT; + } + + ItemGiveHandle handle = 0; + const auto result = item_give_add_observer(*mod, fn, userData, handle); + if (outHandle != nullptr) { + *outHandle = handle; + } + return result; +} + +ModResult item_unobserve_gives(ModContext* context, ItemGiveHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return item_give_remove_observer(*mod, handle); +} + +constexpr ItemService s_itemService{ + .header = SERVICE_HEADER(ItemService, ITEM_SERVICE_MAJOR, ITEM_SERVICE_MINOR), + .set_check_override = item_set_check_override, + .clear_check_override = item_clear_check_override, + .set_check_resolver = item_set_check_resolver, + .clear_check_resolver = item_clear_check_resolver, + .resolve_check = item_resolve_check, + .give_item = item_give_item, + .observe_gives = item_observe_gives, + .unobserve_gives = item_unobserve_gives, +}; + +} // namespace + +constinit const ServiceModule g_itemModule{ + .id = ITEM_SERVICE_ID, + .majorVersion = ITEM_SERVICE_MAJOR, + .minorVersion = ITEM_SERVICE_MINOR, + .service = &s_itemService, + .modDetached = + [](LoadedMod& mod) { + item_checks_remove_mod(mod); + item_gives_remove_mod(mod); + }, + .frameEnd = item_gives_tick, + .shutdown = item_gives_clear, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/item.hpp b/src/dusk/mods/svc/item.hpp new file mode 100644 index 0000000000..355278a3af --- /dev/null +++ b/src/dusk/mods/svc/item.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "mods/svc/item.h" + +#include + +namespace dusk::mods { + +struct LoadedMod; + +namespace svc { + +ModResult item_check_set_override(LoadedMod& mod, const char* name, uint8_t itemNo); +ModResult item_check_clear_override(LoadedMod& mod, const char* name); +ModResult item_check_add_resolver(LoadedMod& mod, const char* name, ItemCheckResolveFn fn, + void* userData, ItemCheckHandle& outHandle); +ModResult item_check_remove_resolver(LoadedMod& mod, ItemCheckHandle handle); +void item_checks_remove_mod(LoadedMod& mod); + +ModResult item_give_enqueue(LoadedMod& mod, const char* checkName, uint8_t itemNo, uint32_t flags); +ModResult item_give_add_observer( + LoadedMod& mod, ItemGiveObserveFn fn, void* userData, ItemGiveHandle& outHandle); +ModResult item_give_remove_observer(LoadedMod& mod, ItemGiveHandle handle); +void item_gives_remove_mod(LoadedMod& mod); +void item_gives_tick(); +void item_gives_clear(); + +} // namespace svc +} // namespace dusk::mods diff --git a/src/dusk/mods/svc/overlay.cpp b/src/dusk/mods/svc/overlay.cpp index 0bd79caff3..f4ad2ff6a6 100644 --- a/src/dusk/mods/svc/overlay.cpp +++ b/src/dusk/mods/svc/overlay.cpp @@ -1,8 +1,9 @@ #include "registry.hpp" #include "slot_map.hpp" -#include "aurora/dvd.h" #include +#include "JSystem/JKernel/JKRArchive.h" +#include "aurora/dvd.h" #include "dusk/mods/loader/loader.hpp" #include "mods/svc/overlay.h" @@ -24,7 +25,7 @@ constexpr borealis::Log Log{"dusk::mods::overlay"}; struct OverlayFileData { std::string bundlePath; std::shared_ptr bundle; - std::shared_ptr > buffer; + std::shared_ptr> buffer; }; // Keyed by the id passed to Aurora as per-file userdata. Guarded by s_overlayMutex: Aurora may @@ -98,6 +99,7 @@ void append_runtime_overlays(std::vector& files, LoadedMod& m for (const auto* slot : slots) { const auto id = s_nextOverlayId++; + if (slot->buffer != nullptr) { s_overlayFiles.emplace(id, OverlayFileData{{}, nullptr, slot->buffer}); } else { @@ -110,7 +112,7 @@ void append_runtime_overlays(std::vector& files, LoadedMod& m struct OpenOverlayFile { std::vector ownedData; - std::shared_ptr > shared; + std::shared_ptr> shared; size_t pos = 0; [[nodiscard]] const std::vector& data() const { @@ -200,6 +202,7 @@ void overlay_sync_files() { Log.debug("Registering {} overlay file(s).", files.size()); aurora_dvd_overlay_files(files.data(), files.size(), nullptr); + JKRArchive::notifyOverlayFilesChanged(); for (const auto& file : files) { std::free(const_cast(file.fileName)); @@ -220,13 +223,13 @@ uint64_t overlay_add_file( uint64_t overlay_add_buffer(LoadedMod& mod, std::string discPath, std::vector data) { const auto size = data.size(); - const auto handle = s_runtimeOverlays.emplace(mod, - RuntimeOverlaySlot{ - .discPath = std::move(discPath), - .buffer = std::make_shared>(std::move(data)), - .size = size, - .order = s_nextRuntimeOrder++, - }); + const auto handle = s_runtimeOverlays.emplace( + mod, RuntimeOverlaySlot{ + .discPath = std::move(discPath), + .buffer = std::make_shared>(std::move(data)), + .size = size, + .order = s_nextRuntimeOrder++, + }); s_overlaysDirty = true; return handle; } @@ -275,13 +278,12 @@ ModResult overlay_add_file( try { size = mod->bundle->getFileSize(bundlePath); } catch (const std::exception& e) { - Log.error( - "[{}] overlay add_file '{}' failed: {}", mod->metadata.id, bundlePath, e.what()); + Log.error("[{}] overlay add_file '{}' failed: {}", mod->metadata.id, bundlePath, e.what()); return MOD_UNAVAILABLE; } if (size > kMaxOverlayFileSize) { - Log.error("[{}] overlay add_file '{}' failed: file too large ({} bytes)", - mod->metadata.id, bundlePath, size); + Log.error("[{}] overlay add_file '{}' failed: file too large ({} bytes)", mod->metadata.id, + bundlePath, size); return MOD_INVALID_ARGUMENT; } diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 34134c90c9..68eeb58771 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -150,6 +150,14 @@ ModResult register_module(const ServiceModule& module) { return MOD_OK; } +void modules_mod_deactivating(LoadedMod& mod) { + for (const auto* module : s_modules | std::views::reverse) { + if (module->modDeactivating != nullptr) { + module->modDeactivating(mod); + } + } +} + void modules_mod_detached(LoadedMod& mod) { for (const auto* module : s_modules | std::views::reverse) { if (module->modDetached != nullptr) { @@ -213,6 +221,10 @@ void ModLoader::init_services() { &svc::g_gfxModule, &svc::g_saveModule, &svc::g_stageModule, + &svc::g_itemModule, + &svc::g_flowModule, + &svc::g_messageModule, + &svc::g_gamemodeModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index df0ca7bdbb..ba693dd958 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -19,7 +19,7 @@ struct ServiceRecord { }; // A host service and its lifecycle hooks. Every hook is optional. Frame and lifecycle hooks run in -// registration order, modDetached in reverse registration order. +// registration order, teardown hooks in reverse registration order. struct ServiceModule { const char* id = nullptr; uint16_t majorVersion = 0; @@ -28,6 +28,9 @@ struct ServiceModule { // One-time setup, at registration (ModLoader::init_services). void (*initialize)() = nullptr; + // A mod is beginning deactivation: stop callbacks that may execute concurrently. Service state + // remains registered so mod_shutdown may release it normally. + void (*modDeactivating)(LoadedMod& mod) = nullptr; // A mod is going away (deactivation or failed activation): drop all state held for it. // Runs after the mod's mod_shutdown and before its library unloads, so pointers into // the mod are still valid but must not be called. @@ -55,6 +58,7 @@ const ServiceRecord* find_service( const ServiceRecord* find_service_record(const char* serviceId, uint16_t majorVersion); ModResult register_module(const ServiceModule& module); +void modules_mod_deactivating(LoadedMod& mod); void modules_mod_detached(LoadedMod& mod); void modules_lifecycle_applied(); void modules_frame_begin(); @@ -75,5 +79,9 @@ extern const ServiceModule g_windowModule; extern const ServiceModule g_gfxModule; extern const ServiceModule g_saveModule; extern const ServiceModule g_stageModule; +extern const ServiceModule g_itemModule; +extern const ServiceModule g_flowModule; +extern const ServiceModule g_messageModule; +extern const ServiceModule g_gamemodeModule; } // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/save.cpp b/src/dusk/mods/svc/save.cpp index 9930129be5..360cdf6dca 100644 --- a/src/dusk/mods/svc/save.cpp +++ b/src/dusk/mods/svc/save.cpp @@ -1,5 +1,6 @@ #include "save.hpp" +#include "item.hpp" #include "registry.hpp" #include "aurora/lib/logging.hpp" @@ -164,6 +165,7 @@ void save_slot_new(uint32_t slot) { store.mods.clear(); store.snapshotValid = false; s_currentSlot = static_cast(slot); + item_gives_clear(); Log.info("new save in slot {}; mod blob store cleared", slot); notify(slot, &SaveObserverRecord::onNewSave, "new-save"); } @@ -183,6 +185,7 @@ void save_slot_loaded(uint32_t slot, const void* slotData) { } } s_currentSlot = static_cast(slot); + item_gives_clear(); notify(slot, &SaveObserverRecord::onLoaded, "save-loaded"); } @@ -222,6 +225,7 @@ void save_slot_erased(uint32_t slot) { void save_no_slot() { s_currentSlot = -1; + item_gives_clear(); } namespace { diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index b47959a930..320d2549ae 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -29,6 +29,8 @@ #include #include +#include "SDL3/SDL_clipboard.h" + namespace dusk::mods::svc::ui_impl { namespace { @@ -424,7 +426,6 @@ public: } void close() { pop(); } - void force_close() { Document::hide(true); } private: std::function m_onDestroyed; @@ -929,7 +930,7 @@ void ui_sync_menu_tabs() { } s_menuTabsDirty = false; if (aurora::rmlui::is_initialized()) { - ui::MenuBar::rebuild(); + ui::MenuBar::refresh_tabs(); } } @@ -1017,14 +1018,14 @@ void ui_remove_mod(LoadedMod& mod) { case UiSlotKind::Window: { auto* window = static_cast(slot.document); if (window != nullptr) { - window->force_close(); + window->force_hide(true); } break; } case UiSlotKind::Dialog: { auto* dialog = static_cast(slot.document); if (dialog != nullptr) { - dialog->force_close(); + dialog->force_hide(true); } break; } @@ -1037,6 +1038,43 @@ void ui_remove_mod(LoadedMod& mod) { } } +ModResult ui_get_clipboard_text(LoadedMod& mod, char* buffer, size_t bufferSize, size_t* outLength) { + if (outLength != nullptr) { + *outLength = 0; + } + + std::string text; + if (SDL_HasClipboardText()) { + char* textPtr = SDL_GetClipboardText(); + text = textPtr != nullptr ? textPtr : ""; + SDL_free(textPtr); + if (text.empty()) { + return MOD_ERROR; + } + } + + if (outLength != nullptr) { + *outLength = text.size(); + } + + if (buffer == nullptr) { + return MOD_OK; + } + if (bufferSize < text.size() + 1) { + return MOD_INVALID_ARGUMENT; + } + + memcpy(buffer, text.c_str(), text.size() + 1); + return MOD_OK; +} + +ModResult ui_set_clipboard_text(LoadedMod& mod, const char* text) { + if (!SDL_SetClipboardText(text)) { + return MOD_ERROR; + } + return MOD_OK; +} + } // namespace dusk::mods::svc::ui_impl namespace dusk::mods::svc { @@ -1368,6 +1406,23 @@ ModResult ui_dialog_add_action( return ui_impl::ui_dialog_add_action(*mod, dialog, *action); } +ModResult ui_get_clipboard_text(ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || (buffer == nullptr && bufferSize != 0)) { + return MOD_INVALID_ARGUMENT; + } + + return ui_impl::ui_get_clipboard_text(*mod, buffer, bufferSize, outLength); +} + +ModResult ui_set_clipboard_text(ModContext* ctx, const char* text) { + auto* mod = mod_from_context(ctx); + if (mod == nullptr || text == nullptr) { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_set_clipboard_text(*mod, text); +} + constexpr UiService s_uiService{ .header = SERVICE_HEADER(UiService, UI_SERVICE_MAJOR, UI_SERVICE_MINOR), .register_mods_panel = ui_register_mods_panel, @@ -1394,6 +1449,8 @@ constexpr UiService s_uiService{ .register_menu_tab = ui_register_menu_tab, .unregister_menu_tab = ui_unregister_menu_tab, .push_toast = ui_push_toast, + .get_clipboard_text = ui_get_clipboard_text, + .set_clipboard_text = ui_set_clipboard_text, }; } // namespace diff --git a/src/dusk/presentation.cpp b/src/dusk/presentation.cpp index b55c11d93b..1443ab85f9 100644 --- a/src/dusk/presentation.cpp +++ b/src/dusk/presentation.cpp @@ -10,6 +10,10 @@ namespace dusk::presentation { namespace { float preferred_frame_rate() { + if (getTransientSettings().turboMode) { + return 0.0f; + } + switch (getSettings().game.enableFrameInterpolation.getValue()) { case FrameInterpMode::Off: return 30.0f; diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index da814c7b87..a6e01a68b2 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -1,6 +1,7 @@ #include "dusk/settings.h" -#include "dusk/config.hpp" #include +#include "dusk/config.hpp" +#include "dusk/game_mode.hpp" namespace dusk { @@ -51,6 +52,7 @@ UserSettings g_userSettings = { .sunsSong {"game.sunsSong", false}, .autoSave {"game.autoSave", false}, .enhancedMapMenus {"game.enhancedMapMenus", false}, + .aimingReticle {"game.aimingReticle", false}, // Preferences .enableMirrorMode {"game.enableMirrorMode", false}, @@ -75,6 +77,7 @@ UserSettings g_userSettings = { .resampler {"game.resampler", Resampler::Bilinear}, .enableMapBackground {"game.enableMapBackground", true}, .disableCutscenePillarboxing {"game.disableCutscenePillarboxing", false}, + .enableHighQualityMinimapTextures {"game.enableHighQualityMinimapTextures", true}, // Audio .noLowHpSound {"game.noLowHpSound", false}, @@ -156,7 +159,8 @@ UserSettings g_userSettings = { .showInputViewer {"game.showInputViewer", false}, .showInputViewerGyro {"game.showInputViewerGyro", false}, .enableMoveLinkCombo {"game.enableMoveLinkCombo", false}, - .enableTeleportCombo {"game.enableTeleportCombo", false} + .enableTeleportCombo {"game.enableTeleportCombo", false}, + .lastSelectedGameModeId {"game.lastSelectedGameModeId", gamemode::kVanillaGameModeId} }, .backend = { @@ -257,6 +261,7 @@ void registerSettings() { Register(g_userSettings.game.sunsSong); Register(g_userSettings.game.autoSave); Register(g_userSettings.game.enhancedMapMenus); + Register(g_userSettings.game.aimingReticle); Register(g_userSettings.game.enableMirrorMode); Register(g_userSettings.game.invertCameraXAxis); Register(g_userSettings.game.invertCameraYAxis); @@ -283,6 +288,7 @@ void registerSettings() { Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableMapBackground); Register(g_userSettings.game.disableCutscenePillarboxing); + Register(g_userSettings.game.enableHighQualityMinimapTextures); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.fastRoll); @@ -306,6 +312,7 @@ void registerSettings() { Register(g_userSettings.game.showInputViewerGyro); Register(g_userSettings.game.enableMoveLinkCombo); Register(g_userSettings.game.enableTeleportCombo); + Register(g_userSettings.game.lastSelectedGameModeId); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.infiniteHearts); Register(g_userSettings.game.infiniteArrows); @@ -411,7 +418,7 @@ static TransientSettings g_transientSettings = { .leevers = false, .opacity = 75.0f, }, - .skipFrameRateLimit = false + .turboMode = false, }; TransientSettings& getTransientSettings() { diff --git a/src/dusk/settings.h b/src/dusk/settings.h index 6cb9fa341a..0f6f61f36e 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -33,6 +33,7 @@ enum class GameLanguage : u8 { French = OS_LANGUAGE_FRENCH, Spanish = OS_LANGUAGE_SPANISH, Italian = OS_LANGUAGE_ITALIAN, + Japanese = 6, }; enum class DiscVerificationState : u8 { @@ -89,7 +90,7 @@ struct ConfigEnumRange { template <> struct ConfigEnumRange { static constexpr auto min = GameLanguage::English; - static constexpr auto max = GameLanguage::Italian; + static constexpr auto max = GameLanguage::Japanese; }; template <> @@ -183,6 +184,7 @@ struct UserSettings { ConfigVar sunsSong; ConfigVar autoSave; ConfigVar enhancedMapMenus; + ConfigVar aimingReticle; // Preferences ConfigVar enableMirrorMode; @@ -207,6 +209,7 @@ struct UserSettings { ConfigVar resampler; ConfigVar enableMapBackground; ConfigVar disableCutscenePillarboxing; + ConfigVar enableHighQualityMinimapTextures; // Audio ConfigVar noLowHpSound; @@ -284,6 +287,8 @@ struct UserSettings { ConfigVar showInputViewerGyro; ConfigVar enableMoveLinkCombo; ConfigVar enableTeleportCombo; + + ConfigVar lastSelectedGameModeId; } game; struct { @@ -344,7 +349,7 @@ struct TriggerViewSettings { struct TransientSettings { CollisionViewSettings collisionView; TriggerViewSettings triggerView; - bool skipFrameRateLimit; + bool turboMode; bool moveLinkActive; bool stateShareLoadActive; }; diff --git a/src/dusk/speedrun.cpp b/src/dusk/speedrun.cpp index 275a8de900..8527f9510a 100644 --- a/src/dusk/speedrun.cpp +++ b/src/dusk/speedrun.cpp @@ -1,12 +1,52 @@ #include "dusk/speedrun.h" -#include "dusk/settings.h" -#include "dusk/config.hpp" -#include "m_Do/m_Do_main.h" #include +#include "dusk/config.hpp" +#include "dusk/game_mode.hpp" +#include "dusk/livesplit.h" +#include "dusk/settings.h" +#include "m_Do/m_Do_main.h" -namespace dusk { +namespace dusk::speedrun { -SpeedrunInfo m_speedrunInfo; +SpeedrunInfo g_speedrunInfo; + +static void onSpeedrunModeActive() { + resetForSpeedrunMode(); +} + +static void onSpeedrunModeDeactive() { + restoreFromSpeedrunMode(); + if (getSettings().game.liveSplitEnabled) { + speedrun::disconnectLiveSplit(); + } +} + +void registerSpeedrunGameMode() { + dusk::gamemode::GameMode speedrunGameMode{ + kSpeedrunGameModeId, "Speedrun", "gczelda2-speedrun"}; + speedrunGameMode.mOnSaveLoadedFunction = [] { + dusk::speedrun::start(); + return true; + }; + speedrunGameMode.mOnActivatedFunction = [] { + onSpeedrunModeActive(); + return true; + }; + speedrunGameMode.mOnDeactivatedFunction = [] { + onSpeedrunModeDeactive(); + return true; + }; + speedrunGameMode.mOnTickFunction = [] { + dusk::speedrun::onGameFrame(); + return true; + }; + + dusk::gamemode::getGameModeManager().registerGameMode(speedrunGameMode); +} + +void unregisterSpeedrunGameMode() { + dusk::gamemode::getGameModeManager().unregisterGameMode(kSpeedrunGameModeId); +} void resetForSpeedrunMode() { mDoMain::developmentMode = -1; @@ -45,9 +85,7 @@ void resetForSpeedrunMode() { } static void clearSpeedrunOverrides() { - config::EnumerateRegistered([](config::ConfigVarBase& cvar) { - cvar.clearSpeedrunOverride(); - }); + config::EnumerateRegistered([](config::ConfigVarBase& cvar) { cvar.clearSpeedrunOverride(); }); } void restoreFromSpeedrunMode() { @@ -55,4 +93,4 @@ void restoreFromSpeedrunMode() { aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue()); } -} // namespace dusk +} // namespace dusk::speedrun diff --git a/src/dusk/speedrun.h b/src/dusk/speedrun.h index d9e43c3ed7..add396726f 100644 --- a/src/dusk/speedrun.h +++ b/src/dusk/speedrun.h @@ -1,23 +1,32 @@ #pragma once #include +#include "dusk/game_mode.hpp" -namespace dusk { +namespace dusk::speedrun { + +constexpr const char* kSpeedrunGameModeId = "vanilla_speedrun"; struct SpeedrunInfo { + void startRun() { m_isRunStarted = true; - m_startTimestamp = OSGetTime(); + m_rtaStartTimestamp = OSGetNativeTime(); + m_igtStartTimestamp = OSGetTime(); } void stopRun() { m_isRunStarted = false; - m_endTimestamp = OSGetTime() - m_startTimestamp; + m_rtaTimer = OSGetNativeTime() - m_rtaStartTimestamp; + if (!m_isPauseIGT) { + m_igtTimer = OSGetTime() - m_igtStartTimestamp - m_totalLoadTime; + } } void reset() { m_isRunStarted = false; - m_startTimestamp = 0; - m_endTimestamp = 0; + m_rtaStartTimestamp = 0; + m_rtaTimer = 0; + m_igtStartTimestamp = 0; m_isPauseIGT = false; m_loadStartTimestamp = 0; m_totalLoadTime = 0; @@ -25,8 +34,9 @@ struct SpeedrunInfo { } bool m_isRunStarted = false; - OSTime m_startTimestamp = 0; - OSTime m_endTimestamp = 0; + OSTime m_rtaStartTimestamp = 0; + OSTime m_rtaTimer = 0; + OSTime m_igtStartTimestamp = 0; bool m_isPauseIGT = false; OSTime m_loadStartTimestamp = 0; @@ -34,9 +44,15 @@ struct SpeedrunInfo { OSTime m_igtTimer = 0; }; -extern SpeedrunInfo m_speedrunInfo; +extern SpeedrunInfo g_speedrunInfo; +void registerSpeedrunGameMode(); +void unregisterSpeedrunGameMode(); void resetForSpeedrunMode(); void restoreFromSpeedrunMode(); +inline bool isActive() { + return dusk::gamemode::getGameModeManager().isCurrentGameMode(kSpeedrunGameModeId); +} + } // namespace dusk diff --git a/src/dusk/ui/document.cpp b/src/dusk/ui/document.cpp index 8b160a4ee6..8ad701d3ce 100644 --- a/src/dusk/ui/document.cpp +++ b/src/dusk/ui/document.cpp @@ -102,6 +102,15 @@ bool Document::focus() { return false; } +bool Document::has_focus() const { + if (mDocument == nullptr) { + return false; + } + auto* context = mDocument->GetContext(); + const auto* focused = context != nullptr ? context->GetFocusElement() : nullptr; + return focused != nullptr && focused->GetOwnerDocument() == mDocument; +} + bool Document::set_document_styles(const Rml::String& rcss) { if (rcss.empty()) { mDocumentStyleSheets = nullptr; diff --git a/src/dusk/ui/document.hpp b/src/dusk/ui/document.hpp index 147aa437bc..652b88006e 100644 --- a/src/dusk/ui/document.hpp +++ b/src/dusk/ui/document.hpp @@ -20,6 +20,7 @@ public: virtual void hide(bool close); virtual void update(); virtual bool focus(); + bool has_focus() const; virtual bool visible() const; virtual bool active() const; virtual bool obscures_game() const { return false; } @@ -64,6 +65,10 @@ public: hide(true); uncover_top_document(); } + void force_hide(bool close) { + hide(close); + Document::hide(close); + } bool pending_close() const { return mPendingClose; } bool closed() const { return mClosed; } diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index ba91e38940..d2bf8f8d9d 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -7,11 +7,13 @@ #include "achievements.hpp" #include "aurora/rmlui.hpp" +#include "dusk/game_mode.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/mods/svc/ui.hpp" #include "dusk/settings.h" #include "dusk/speedrun.h" +#include "dusk/ui/prelaunch.hpp" #include "editor.hpp" #include "f_op/f_op_scene_mng.h" #include "f_pc/f_pc_manager.h" @@ -55,6 +57,20 @@ MenuBar::MenuBar() }, .autoSelect = false, }); + + // Hide document after transition completion + listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { + if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && + Document::visible()) + { + Document::hide(mPendingClose); + } + }); + + build_tabs(); +} + +void MenuBar::build_tabs() { mTabBar->add_tab("Settings", [this] { push(std::make_unique()); }); if (getSettings().backend.enableAdvancedSettings) { @@ -62,7 +78,11 @@ MenuBar::MenuBar() mTabBar->add_tab("Editor", [this] { push(std::make_unique()); }); } - mTabBar->add_tab("Achievements", [this] { push(std::make_unique()); }); + // Only allow us to access achievements if we are playing on a game mode that uses them + if (dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::gamemode::kVanillaGameModeId) + || dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::speedrun::kSpeedrunGameModeId)) { + mTabBar->add_tab("Achievements", [this] { push(std::make_unique()); }); + } mTabBar->add_tab("Mods", [this] { push(std::make_unique()); }); for (auto& tab : mods::svc::ui_mod_menu_tabs()) { mTabBar->add_tab(tab.label, std::move(tab.onSelected)); @@ -94,9 +114,13 @@ MenuBar::MenuBar() dismiss(modal); return; } - JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; dismiss(modal); + if (gamemode::getGameModeManager().getRegisteredGameModes().size() > 1) { + // If game modes are registered, return to prelaunch on reset. + prelaunch_state().returnToPrelaunchOnReset = true; + } hide(false); + JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; }, }, }, @@ -135,11 +159,11 @@ MenuBar::MenuBar() })); }); - if (getSettings().game.speedrunMode) { + if (dusk::speedrun::isActive()) { mTabBar->add_tab("Reset Timer", [this] { mTabBar->set_active_tab(-1); mDoAud_seStartMenu(kSoundClick); - m_speedrunInfo.reset(); + dusk::speedrun::g_speedrunInfo.reset(); if (getSettings().game.liveSplitEnabled) { dusk::speedrun::reset(); } @@ -150,28 +174,19 @@ MenuBar::MenuBar() hide(false); }); } - - // Hide document after transition completion - listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { - if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && - Document::visible()) - { - Document::hide(mPendingClose); - } - }); } void MenuBar::show() { Document::show(); mRoot->SetAttribute("open", ""); mTabBar->set_active_tab(-1); - if (!mTabBar->focus_tab(mFocusedTabIndex)) { + if (!mTabBar->focus_tab(mFocusedTabTitle)) { mTabBar->focus(); } } void MenuBar::hide(bool close) { - mFocusedTabIndex = mTabBar->focused_tab_index(); + mFocusedTabTitle = mTabBar->focused_tab_title(); mRoot->RemoveAttribute("open"); if (close) { mPendingClose = true; @@ -241,19 +256,19 @@ bool MenuBar::focus() { return mTabBar->focus(); } -void MenuBar::rebuild() { - for (auto& doc : get_document_stack()) { - if (auto* menuBar = dynamic_cast(doc.get())) { - const bool wasVisible = menuBar->visible(); - auto next = std::make_unique(); - next->mFocusedTabIndex = menuBar->mFocusedTabIndex; - next->mWasVisible = menuBar->mWasVisible; - doc = std::move(next); - if (wasVisible) { - doc->show(); - } - break; - } +void MenuBar::refresh_tabs() { + auto* menuBar = static_cast(find_document(DocumentScope::MenuBar)); + if (menuBar == nullptr) { + return; + } + const auto focusedTitle = menuBar->mTabBar->focused_tab_title(); + if (!focusedTitle.empty()) { + menuBar->mFocusedTabTitle = focusedTitle; + } + menuBar->mTabBar->clear_tabs(); + menuBar->build_tabs(); + if (menuBar->visible() && !menuBar->mTabBar->focus_tab(menuBar->mFocusedTabTitle)) { + menuBar->mTabBar->focus(); } } diff --git a/src/dusk/ui/menu_bar.hpp b/src/dusk/ui/menu_bar.hpp index 29ce2199c5..44b4ee77cd 100644 --- a/src/dusk/ui/menu_bar.hpp +++ b/src/dusk/ui/menu_bar.hpp @@ -21,12 +21,13 @@ public: bool focus() override; bool visible() const override; - static void rebuild(); + static void refresh_tabs(); protected: bool handle_nav_command(Rml::Event& event, NavCommand cmd) override; private: + void build_tabs(); void update_safe_area() noexcept; Rml::Element* mRoot; @@ -34,7 +35,7 @@ private: std::unique_ptr