From 7527f91690f755a6e1b028abb97c9c21471fac16 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 02:22:06 +0000 Subject: [PATCH 01/20] feat: enable KiouEditor as a Chinlan consumer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small surface additions that unblock KiouEditor from consuming KIOU-Hook via vendor/KIOU-Hook + Chinlan: 1. Add KIOU_FEATURE_INGAME_ANALYSIS and KIOU_FEATURE_AI_SPECIAL_SUPPORT to the shared KiouFeature enum, with labels in the Common.m weak stub. The hook bodies that consume these flags live in the consumer tweak (KiouEditor) because the sites are tweak-local and not part of the shared catalog. Placing the enum values here lets KIOUEditorFeatureEnabled gate them uniformly with the shared 7. 2. Wrap KIOU_HOOK_CAVE_REGION_START in ifndef so consumers targeting a different KIOU binary version can override on the compiler command line. Default (0x826F5E8) still matches v1_0_2 / KiouForge; KiouEditor at 1.0.1 sets -DKIOU_HOOK_CAVE_REGION_START=0x8268024. No RVA additions, no slot table changes, no recipe cascade — the existing 11 KiouEditor-flavored shared hooks (Afk, AssistEnable, Collection, FriendUnhide, MatchingPlayer, PremiumUnlock, SelectCharacter, SyncItemList, Version, VoiceUnlock, plus the AssistTune ctor/EnsureInit pair) already sit in the 32-slot entry table with 1.0.1 RVAs live in the catalog. --- Hook/Common.h | 6 ++++++ Hook/Common.m | 2 ++ KIOUHook.h | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/Hook/Common.h b/Hook/Common.h index d5e536b..96baecc 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -126,6 +126,12 @@ typedef NS_ENUM(NSInteger, KiouFeature) { KIOU_FEATURE_VOICE_UNLOCK, // Hook/VoiceUnlock + SyncItemList intimacy pin KIOU_FEATURE_ASSIST_ENABLE, // Hook/AssistEnable force enabled + depth KIOU_FEATURE_DISABLE_AFK, // Hook/AfkDisable suppress AFK warning + auto-surrender + // Consumer-owned tweak-specific features. Defined here so the shared + // KIOUEditorFeatureEnabled surface can gate them, but the hook bodies + // that consume these flags live in the consumer tweak (KiouEditor) + // because the sites are tweak-local and not part of the shared catalog. + KIOU_FEATURE_INGAME_ANALYSIS, // KiouEditor Hook_AssistTune BSE.EvaluateAsync suppression + KIOU_FEATURE_AI_SPECIAL_SUPPORT, // KiouEditor Hook_AiSpecialSupport 棋桜覚醒 unlock (default off) KIOU_FEATURE_COUNT, }; diff --git a/Hook/Common.m b/Hook/Common.m index c4baea9..f4df483 100644 --- a/Hook/Common.m +++ b/Hook/Common.m @@ -220,6 +220,8 @@ void KIOUEditorSetFeatureEnabled(KiouFeature f, bool enabled) { case KIOU_FEATURE_VOICE_UNLOCK: return @"Voice Unlock"; case KIOU_FEATURE_ASSIST_ENABLE: return @"Assist Enable"; case KIOU_FEATURE_DISABLE_AFK: return @"Disable AFK"; + case KIOU_FEATURE_INGAME_ANALYSIS: return @"In-Game Analysis"; + case KIOU_FEATURE_AI_SPECIAL_SUPPORT: return @"AI Special Support"; default: return @"(unknown)"; } } diff --git a/KIOUHook.h b/KIOUHook.h index 792c840..0492eec 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -55,7 +55,16 @@ #define KIOU_HOOK_ENTRY_SLOT_BASE_RVA 0x091E91B8 // Cave payload region (matches recipes/common.py + per-version CAVE_REGION). +// +// Version note: the default here matches KIOU 1.0.2's CAVE_REGION[0] +// (recipes/v1_0_2.py). Consumers that target a different version must +// override the macro on the compiler command line — the 1.0.1 tree +// (recipes/v1_0_1.py) uses 0x8268024 for example. KiouForge targets +// 1.0.2 and can rely on the default; KiouEditor (1.0.1) sets +// -DKIOU_HOOK_CAVE_REGION_START=0x8268024 in its Makefile. +#ifndef KIOU_HOOK_CAVE_REGION_START #define KIOU_HOOK_CAVE_REGION_START 0x826F5E8 +#endif #define KIOU_HOOK_CAVE_SIZE 84 #define KIOU_HOOK_CAVE_BYPASS_OFFSET (KIOU_HOOK_CAVE_SIZE - 8) From d2a2352de558c45bbd0a341caf3ba1bad62ba948 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 02:32:56 +0000 Subject: [PATCH 02/20] feat: publish entry slots from KiouEditor hook installers Add KIOU_HOOK_PUBLISH_SLOT helper in Hook/Common.h and use it from all 11 KiouEditor-flavored installers so the shared hook bodies work under IPA_CHINLAN as well as JB/jailed. Prior to this, KIOUHookInstall's chinlan branch only returned the cave-bypass entry for orig-chain-back; the entry-slot table was never written, so any cave the recipe carved would BLR through NULL on the first call. Only KIOUInstallAfkSuppressHook (already in the tree) demonstrated the manual entry-slot write pattern. The macro is a one-line addition per site in each installer: s_orig = (fn_t)KIOUHookInstall(NAME, (void *)hook_fn, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, SLOT_ID, hook_fn); On JB / jailed the macro degenerates to (void) casts so the installer compiles unchanged. On chinlan it materialises the entry-slot address and stores the hook function pointer. KiouEditor at 1.0.1 can now consume these installers via vendor/KIOU-Hook + Chinlan without adding an editor-side ChinlanDispatcher publish for every shared site. --- Hook/AfkDisable.m | 1 + Hook/AssistEnable.m | 2 ++ Hook/AssistTune.m | 2 ++ Hook/Collection.m | 1 + Hook/Common.h | 25 +++++++++++++++++++++++++ Hook/FriendUnhide.m | 2 ++ Hook/MatchingPlayer.m | 1 + Hook/PremiumUnlock.m | 3 +++ Hook/SelectCharacter.m | 2 ++ Hook/SyncItemList.m | 1 + Hook/Version.m | 1 + Hook/VoiceUnlock.m | 2 ++ 12 files changed, 43 insertions(+) diff --git a/Hook/AfkDisable.m b/Hook/AfkDisable.m index c0c9b3b..9e3a09c 100644 --- a/Hook/AfkDisable.m +++ b/Hook/AfkDisable.m @@ -31,6 +31,7 @@ void KIOUEditorInstallAfkDisableHook(uintptr_t unityBase) { s_origGO_IsAfkEnabled = (GameOrchestratorIsAfkEnabled_t)KIOUHookInstall( KIOU_HOOK_NAME_GAME_ORCHESTRATOR_IS_AFK, (void *)hook_GO_IsAfkEnabled, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_GAME_ORCHESTRATOR_IS_AFK, hook_GO_IsAfkEnabled); IPALog([NSString stringWithFormat: @"[AFK] installed: orig=%p (toggled by KIOU_FEATURE_DISABLE_AFK)", (void *)s_origGO_IsAfkEnabled]); diff --git a/Hook/AssistEnable.m b/Hook/AssistEnable.m index ea31a72..950919d 100644 --- a/Hook/AssistEnable.m +++ b/Hook/AssistEnable.m @@ -44,9 +44,11 @@ void KIOUEditorInstallAssistEnableHook(uintptr_t unityBase) { s_origRBS_getEnabled = (BSupportGetBool_t)KIOUHookInstall( KIOU_HOOK_NAME_RBSUPPORT_GET_ENABLED, (void *)hook_RBS_getEnabled, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_RBSUPPORT_GET_ENABLED, hook_RBS_getEnabled); s_origRBS_getDepth = (BSupportGetI32_t)KIOUHookInstall( KIOU_HOOK_NAME_RBSUPPORT_GET_DEPTH, (void *)hook_RBS_getDepth, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_RBSUPPORT_GET_DEPTH, hook_RBS_getDepth); IPALog([NSString stringWithFormat: @"[ASSIST-EN] installed: get_Enabled orig=%p get_Depth orig=%p (depth=%d)", (void *)s_origRBS_getEnabled, (void *)s_origRBS_getDepth, diff --git a/Hook/AssistTune.m b/Hook/AssistTune.m index d450175..b31fc7a 100644 --- a/Hook/AssistTune.m +++ b/Hook/AssistTune.m @@ -102,9 +102,11 @@ void KIOUEditorInstallAssistTuneHook(uintptr_t unityBase) { s_origBSE_ctor = (BSECtor_t)KIOUHookInstall( KIOU_HOOK_NAME_BSE_CTOR, (void *)hook_BSE_ctor, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_CTOR, hook_BSE_ctor); s_origBSE_ensureInit = (BSEEnsureInit_t)KIOUHookInstall( KIOU_HOOK_NAME_BSE_ENSURE_INITIALIZED, (void *)hook_BSE_ensureInit, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_ENSURE_INITIALIZED, hook_BSE_ensureInit); IPALog([NSString stringWithFormat: @"[ASSIST-TUNE] installed: BSE.ctor orig=%p EnsureInit orig=%p " @"(depth=%d skill=%d hash=%d MB)", diff --git a/Hook/Collection.m b/Hook/Collection.m index 23ea65f..cacf1d7 100644 --- a/Hook/Collection.m +++ b/Hook/Collection.m @@ -81,6 +81,7 @@ void KIOUEditorInstallCollectionHook(uintptr_t unityBase) { s_origCollectionPresetReply_merge = (InternalMergeFrom_t)KIOUHookInstall( KIOU_HOOK_NAME_COLLECTION_PRESET_MERGE, (void *)hook_CollectionPresetReply_merge, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_COLLECTION_PRESET_MERGE, hook_CollectionPresetReply_merge); IPALog([NSString stringWithFormat: @"[COLLECTION] installed: orig=%p (observation only)", (void *)s_origCollectionPresetReply_merge]); diff --git a/Hook/Common.h b/Hook/Common.h index 96baecc..a395d91 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -162,6 +162,31 @@ int32_t KIOUEditorAssistHashIndex(void); void KIOUEditorSetAssistHashIndex(int32_t idx); int32_t KIOUEditorAssistHashMB(void); +// --------------------------------------------------------------------------- +// Chinlan slot publish helper. +// +// KIOUHookInstall() on the chinlan branch does NOT write the entry slot the +// cave will BLR through — it only returns the cave-bypass entry. Each hook +// installer must therefore ALSO write its slot before UnityFramework's +// first call reaches the cave. This macro keeps the pattern one-liner and +// no-ops on JB / jailed where MSHookFunction already rewrote the site. +// +// Usage inside an installer: +// s_orig = (fn_t)KIOUHookInstall(NAME, (void *)hook_fn, unityBase); +// KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_X, hook_fn); +// --------------------------------------------------------------------------- +#if IPA_CHINLAN +#define KIOU_HOOK_PUBLISH_SLOT(unityBase, slot_id, hook_fn) do { \ + void * volatile *_entrySlots = \ + (void * volatile *)((unityBase) + KIOU_HOOK_ENTRY_SLOT_BASE_RVA); \ + _entrySlots[(slot_id)] = (void *)(hook_fn); \ +} while (0) +#else +#define KIOU_HOOK_PUBLISH_SLOT(unityBase, slot_id, hook_fn) do { \ + (void)(unityBase); (void)(slot_id); (void)(hook_fn); \ +} while (0) +#endif + // --------------------------------------------------------------------------- // Per-module hook installers. Each takes the UnityFramework base and calls // KIOUHookInstall for every site it owns. Safe to call multiple times; diff --git a/Hook/FriendUnhide.m b/Hook/FriendUnhide.m index 3764264..e97f050 100644 --- a/Hook/FriendUnhide.m +++ b/Hook/FriendUnhide.m @@ -1109,9 +1109,11 @@ void KIOUEditorInstallFriendUnhideHook(uintptr_t unityBase) { orig_HUP_ctor = (HUP_ctor_t)KIOUHookInstall( KIOU_HOOK_NAME_HOME_UTILITY_PRESENTER_CTOR, (void *)hook_HUP_ctor, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_HOME_UTILITY_PRESENTER_CTOR, hook_HUP_ctor); orig_UIBtn_OnPointerClick = (UIBtn_OnPointerClick_t)KIOUHookInstall( KIOU_HOOK_NAME_UIBUTTONBASE_ONPOINTERCLICK, (void *)hook_UIBtn_OnPointerClick, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_UIBUTTONBASE_ONPOINTERCLICK, hook_UIBtn_OnPointerClick); IPALog([NSString stringWithFormat: @"[FRIEND] installed: HUP.ctor orig=%p UIBtn.OnPointerClick orig=%p", diff --git a/Hook/MatchingPlayer.m b/Hook/MatchingPlayer.m index c7e01a5..81dcbb1 100644 --- a/Hook/MatchingPlayer.m +++ b/Hook/MatchingPlayer.m @@ -126,6 +126,7 @@ void KIOUEditorInstallMatchingPlayerHook(uintptr_t unityBase) { s_origMatchingPlayer_merge = (ReplyMergeFrom_t)KIOUHookInstall( KIOU_HOOK_NAME_MATCHING_PLAYER_MERGE, (void *)hook_MatchingPlayer_merge, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_MATCHING_PLAYER_MERGE, hook_MatchingPlayer_merge); NSString *configured = KIOUSelfUserId(); IPALog([NSString stringWithFormat: @"[MATCH] installed: orig=%p self_user_id=%@", diff --git a/Hook/PremiumUnlock.m b/Hook/PremiumUnlock.m index 2ee011a..2af1fdf 100644 --- a/Hook/PremiumUnlock.m +++ b/Hook/PremiumUnlock.m @@ -73,12 +73,15 @@ void KIOUEditorInstallPremiumUnlockHook(uintptr_t unityBase) { s_origKifuDetailModel_IsPremiumUser = (IsPremiumUser_t)KIOUHookInstall( KIOU_HOOK_NAME_KIFU_DETAIL_IS_PREMIUM, (void *)hook_KifuDetailModel_IsPremiumUser, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_KIFU_DETAIL_IS_PREMIUM, hook_KifuDetailModel_IsPremiumUser); s_origHistoryDetailReply_merge = (ReplyMergeFrom_t)KIOUHookInstall( KIOU_HOOK_NAME_HISTORY_DETAIL_MERGE, (void *)hook_HistoryDetailReply_merge, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_HISTORY_DETAIL_MERGE, hook_HistoryDetailReply_merge); s_origHistoryDetailReply_IsPremiumUser = (IsPremiumUser_t)KIOUHookInstall( KIOU_HOOK_NAME_HISTORY_GET_PREMIUM, (void *)hook_HistoryDetailReply_IsPremiumUser, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_HISTORY_GET_PREMIUM, hook_HistoryDetailReply_IsPremiumUser); IPALog([NSString stringWithFormat: @"[PREMIUM] installed: KifuDetail.IsPremium orig=%p, " @"HistoryDetail.merge orig=%p, HistoryDetail.get_IsPremium orig=%p", diff --git a/Hook/SelectCharacter.m b/Hook/SelectCharacter.m index c44aea8..bb911a9 100644 --- a/Hook/SelectCharacter.m +++ b/Hook/SelectCharacter.m @@ -117,9 +117,11 @@ void KIOUEditorInstallSelectCharacterHook(uintptr_t unityBase) { s_origSelectCharacterAsync = (SelectCharacterAsync_t)KIOUHookInstall( KIOU_HOOK_NAME_SELECT_CHAR_ASYNC, (void *)hook_SelectCharacterAsync, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_SELECT_CHAR_ASYNC, hook_SelectCharacterAsync); s_origSelectCharacterReplyMerge = (ReplyMergeFrom_t)KIOUHookInstall( KIOU_HOOK_NAME_SELECT_CHAR_REPLY_MERGE, (void *)hook_SelectCharacterReplyMerge, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_SELECT_CHAR_REPLY_MERGE, hook_SelectCharacterReplyMerge); IPALog([NSString stringWithFormat: @"[SELECT] installed: Async orig=%p Reply.merge orig=%p " @"SAFE_ID=%d persisted=%d", diff --git a/Hook/SyncItemList.m b/Hook/SyncItemList.m index 3bd34e3..1966bc7 100644 --- a/Hook/SyncItemList.m +++ b/Hook/SyncItemList.m @@ -216,6 +216,7 @@ void KIOUEditorInstallSyncItemListHook(uintptr_t unityBase) { s_origSyncItemListReply_merge = (InternalMergeFrom_t)KIOUHookInstall( KIOU_HOOK_NAME_SYNC_ITEM_LIST_MERGE, (void *)hook_SyncItemListReply_merge, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_SYNC_ITEM_LIST_MERGE, hook_SyncItemListReply_merge); IPALog([NSString stringWithFormat: @"[SYNC-ITEM] installed: orig=%p", (void *)s_origSyncItemListReply_merge]); diff --git a/Hook/Version.m b/Hook/Version.m index 8d27413..122b4ac 100644 --- a/Hook/Version.m +++ b/Hook/Version.m @@ -83,6 +83,7 @@ void KIOUEditorInstallVersionHook(uintptr_t unityBase) { s_origTitleSceneMoveNext = (TitleSceneMoveNext_t)KIOUHookInstall( KIOU_HOOK_NAME_TITLE_SCENE_MOVENEXT, (void *)hook_TitleSceneMoveNext, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_TITLE_SCENE_MOVENEXT, hook_TitleSceneMoveNext); IPALog([NSString stringWithFormat: @"[VERSION] installed: orig=%p commit=%s", (void *)s_origTitleSceneMoveNext, KIOU_EDITOR_COMMIT]); diff --git a/Hook/VoiceUnlock.m b/Hook/VoiceUnlock.m index cc7fbdc..f33ce51 100644 --- a/Hook/VoiceUnlock.m +++ b/Hook/VoiceUnlock.m @@ -65,9 +65,11 @@ void KIOUEditorInstallVoiceUnlockHook(uintptr_t unityBase) { s_origCharacterVoicePlayer_SatisfiesRule = (SatisfiesRule_t)KIOUHookInstall( KIOU_HOOK_NAME_VOICE_PLAYER_SATISFIES, (void *)hook_CharacterVoicePlayer_SatisfiesRule, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_VOICE_PLAYER_SATISFIES, hook_CharacterVoicePlayer_SatisfiesRule); s_origVoiceCellModel_get_IsLocked = (GetIsLocked_t)KIOUHookInstall( KIOU_HOOK_NAME_VOICE_CELL_GET_IS_LOCKED, (void *)hook_VoiceCellModel_get_IsLocked, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_VOICE_CELL_GET_IS_LOCKED, hook_VoiceCellModel_get_IsLocked); IPALog([NSString stringWithFormat: @"[VOICE] installed: SatisfiesRule orig=%p CellModel.get_IsLocked orig=%p", (void *)s_origCharacterVoicePlayer_SatisfiesRule, From bbf2c61933f990e28389177c8b84f253ced85144 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 02:57:05 +0000 Subject: [PATCH 03/20] refactor: split Hook/FriendUnhide.m into hook body + bridge halves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hook/FriendUnhide.m was 1121 lines — well past the project's 600-line hard-split threshold. Extract the il2cpp + Unity bridging layer that takes up ~800 of those lines into two sibling files behind a shared private header, leaving Hook/FriendUnhide.m as the concise ~210-line hook-body + installer file it wants to be. Layout ------ * Hook/FriendUnhide.m (209 lines) — hook bodies for HomeUtilityPresenter.ctor + UIButtonBase.OnPointerClick, installer. * Hook/FriendUnhideBridge.h (public bridge surface) — the extern declarations the hook body uses: init, Component/GO/Transform helpers, hierarchy recon, hide-image, sprite recon, direct Instantiate, and the three shared state pointers (g_friendGo, g_cloneGo, g_lastClonedView). * Hook/FriendUnhideBridge.m (441 lines) — il2cpp bridge resolver, method-pointer caches, Component / GameObject / Transform helpers, hierarchy walker, sprite lookup + swap primitives, transformChildByName / componentByTypeName / findIconImageTransform. * Hook/FriendUnhideBridgeUI.m (457 lines) — sprite ops, image hide, text-component recon, Instantiate variants, and the two public strong-symbol implementations KIOUEditorReconButtonImage / KIOUEditorApplyTitleSpriteToClone. Also owns transformSetSiblingIndex + transformOf because their dependents live here. * Hook/FriendUnhideBridge_Private.h (61 lines) — declarations shared between the two Bridge .m files: il2cpp state, g_unityBaseAddr, a handful of internal helpers (invoke0, objectName, transformChildByName, componentByTypeName, transformChildCount, transformGetChild, findIconImageTransform, swapImageSpriteOnGo, g_titleMenuSprite). Private: not part of the public consumer API. Every file now sits comfortably under the 600-line threshold; only Bridge.m + BridgeUI.m sit in the 400-600 "consider splitting" band, where their single-responsibility grouping justifies the size. No behaviour change. The friendUnhide_initBridge symbol becomes FriendUnhideBridgeInit and is exported from Hook/FriendUnhideBridge.m so the hook installer in Hook/FriendUnhide.m can drive it before KIOUHookInstall wires either RVA. Consumers that only import Hook/Common.h continue to see just the two public entry points KIOUEditorReconButtonImage and KIOUEditorApplyTitleSpriteToClone. --- Hook/FriendUnhide.m | 980 ++---------------------------- Hook/FriendUnhideBridge.h | 103 ++++ Hook/FriendUnhideBridge.m | 441 ++++++++++++++ Hook/FriendUnhideBridgeUI.m | 457 ++++++++++++++ Hook/FriendUnhideBridge_Private.h | 61 ++ 5 files changed, 1096 insertions(+), 946 deletions(-) create mode 100644 Hook/FriendUnhideBridge.h create mode 100644 Hook/FriendUnhideBridge.m create mode 100644 Hook/FriendUnhideBridgeUI.m create mode 100644 Hook/FriendUnhideBridge_Private.h diff --git a/Hook/FriendUnhide.m b/Hook/FriendUnhide.m index e97f050..9478432 100644 --- a/Hook/FriendUnhide.m +++ b/Hook/FriendUnhide.m @@ -1,11 +1,16 @@ #import "Hook/Common.h" +#import "Hook/FriendUnhideBridge.h" #import "logging.h" -#import // =========================================================================== -// HOOK 10: HomeUtilityPresenter.ctor - unhide the friend button. -// RVA 0x5A9F298 from UnityFramework base. -// public void .ctor(IHomeUtilityView view) - x0=self, x1=view +// Hook/FriendUnhide.m — HomeUtilityPresenter.ctor unhides the friend button; +// UIButtonBase.OnPointerClick routes friend-button taps to KiouEditor's +// settings sheet. +// +// The il2cpp / Unity bridging layer this hook depends on lives in +// Hook/FriendUnhideBridge.m and Hook/FriendUnhideBridgeUI.m (behind +// Hook/FriendUnhideBridge.h) so the hook body stays close to the tamper +// logic itself. See FriendUnhideBridge.h for the exported bridge surface. // // HomeUtilityView (Project.Menu) layout from dump.cs: // +0x20 _menuButton (UIButtonBase) @@ -14,7 +19,7 @@ // +0x38 _friendButton (UIButtonBase) - hidden in the retail layout // // Phase 1a (recon) confirmed all three button pointers populate at ctor -// time. Phase 1b calls UnityEngine.Component.get_gameObject() on the friend +// time. Phase 1b calls UnityEngine.Component.get_gameObject on the friend // button and then UnityEngine.GameObject.SetActive(true) on the result via // il2cpp_runtime_invoke. Methods are resolved off the runtime object's own // klass (get_gameObject is inherited from Component) and the resulting @@ -22,897 +27,28 @@ // =========================================================================== #define RVA_HOME_UTILITY_PRESENTER_CTOR 0x5A9F298 +#define RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD1E08 #define OFF_HUV_MENU_BUTTON 0x20 #define OFF_HUV_GIFT_BUTTON 0x28 #define OFF_HUV_FRIEND_BUTTON 0x38 -// --------------------------------------------------------------------------- -// il2cpp runtime bridge (Phase 1a: resolved but unused; Phase 1b will call -// invoke + class_from_name + class_get_method_from_name for SetActive). -// --------------------------------------------------------------------------- - -typedef void *(*il2cpp_runtime_invoke_t)(void *method, void *obj, void **params, void **exc); -typedef void *(*il2cpp_class_from_name_t)(void *image, const char *ns, const char *name); -typedef void *(*il2cpp_string_new_t)(const char *s); -static il2cpp_string_new_t p_il2cpp_string_new = NULL; - -// UnityFramework load address - captured at install. Used to direct-call -// methods by RVA (GameObject.GetComponent(string) at 0x6BCA6AC). -static uintptr_t g_unityBaseAddr = 0; -typedef void *(*il2cpp_class_get_method_from_name_t)(void *klass, const char *name, int argc); -typedef void *(*il2cpp_object_get_class_t)(void *obj); -typedef void *(*il2cpp_class_get_parent_t)(void *klass); -typedef void *(*il2cpp_class_get_methods_t)(void *klass, void **iter); -typedef const char *(*il2cpp_method_get_name_t)(void *method); -typedef uint32_t (*il2cpp_method_get_param_count_t)(void *method); -typedef bool (*il2cpp_method_is_generic_t)(void *method); - -static il2cpp_runtime_invoke_t p_il2cpp_runtime_invoke = NULL; -static il2cpp_class_from_name_t p_il2cpp_class_from_name = NULL; -static il2cpp_class_get_method_from_name_t p_il2cpp_class_get_method_from_name = NULL; -static il2cpp_object_get_class_t p_il2cpp_object_get_class = NULL; -static il2cpp_class_get_parent_t p_il2cpp_class_get_parent = NULL; -static il2cpp_class_get_methods_t p_il2cpp_class_get_methods = NULL; -static il2cpp_method_get_name_t p_il2cpp_method_get_name = NULL; -static il2cpp_method_get_param_count_t p_il2cpp_method_get_param_count = NULL; -static il2cpp_method_is_generic_t p_il2cpp_method_is_generic = NULL; - -static void resolveIl2cppBridge(void) { - if (p_il2cpp_runtime_invoke) return; - p_il2cpp_runtime_invoke = (il2cpp_runtime_invoke_t)dlsym(RTLD_DEFAULT, "il2cpp_runtime_invoke"); - p_il2cpp_class_from_name = (il2cpp_class_from_name_t)dlsym(RTLD_DEFAULT, "il2cpp_class_from_name"); - p_il2cpp_class_get_method_from_name = (il2cpp_class_get_method_from_name_t)dlsym(RTLD_DEFAULT, "il2cpp_class_get_method_from_name"); - p_il2cpp_object_get_class = (il2cpp_object_get_class_t)dlsym(RTLD_DEFAULT, "il2cpp_object_get_class"); - p_il2cpp_class_get_parent = (il2cpp_class_get_parent_t)dlsym(RTLD_DEFAULT, "il2cpp_class_get_parent"); - p_il2cpp_class_get_methods = (il2cpp_class_get_methods_t)dlsym(RTLD_DEFAULT, "il2cpp_class_get_methods"); - p_il2cpp_method_get_name = (il2cpp_method_get_name_t)dlsym(RTLD_DEFAULT, "il2cpp_method_get_name"); - p_il2cpp_method_get_param_count = (il2cpp_method_get_param_count_t)dlsym(RTLD_DEFAULT, "il2cpp_method_get_param_count"); - p_il2cpp_method_is_generic = (il2cpp_method_is_generic_t)dlsym(RTLD_DEFAULT, "il2cpp_method_is_generic"); - IPALog([NSString stringWithFormat: - @"[HOME] il2cpp bridge: runtime_invoke=%p class_from_name=%p class_get_method_from_name=%p object_get_class=%p class_get_parent=%p class_get_methods=%p method_get_name=%p method_get_param_count=%p method_is_generic=%p", - p_il2cpp_runtime_invoke, - p_il2cpp_class_from_name, - p_il2cpp_class_get_method_from_name, - p_il2cpp_object_get_class, - p_il2cpp_class_get_parent, - p_il2cpp_class_get_methods, - p_il2cpp_method_get_name, - p_il2cpp_method_get_param_count, - p_il2cpp_method_is_generic]); -} - -// --------------------------------------------------------------------------- -// Cached method pointers - resolved from the live objects' klasses on the -// first ctor fire, then reused. The il2cpp method pointers are stable for -// the lifetime of the dylib so caching is safe. -// --------------------------------------------------------------------------- - -static void *g_method_get_gameObject = NULL; // Component.get_gameObject -static void *g_method_get_transform = NULL; // Component.get_transform (cached off Component-derived obj) -static void *g_method_GO_get_transform = NULL; // GameObject.get_transform -static void *g_method_SetActive = NULL; // GameObject.SetActive -static void *g_method_Instantiate2 = NULL; // UnityEngine.Object.Instantiate(Object, Transform) -static void *g_method_Instantiate1NonGen = NULL; // UnityEngine.Object.Instantiate(Object) non-generic -static void *g_method_Tf_get_parent = NULL; // Transform.get_parent -static void *g_method_Tf_SetParent = NULL; // Transform.SetParent(Transform,bool) -static void *g_method_Tf_GetSiblingIndex = NULL; // Transform.GetSiblingIndex -static void *g_method_Tf_SetSiblingIndex = NULL; // Transform.SetSiblingIndex(int) -static void *g_method_Tf_get_childCount = NULL; // Transform.get_childCount -static void *g_method_Tf_GetChild = NULL; // Transform.GetChild(int) -static void *g_method_Obj_get_name = NULL; // UnityEngine.Object.get_name - -// HomeUtilityView pointer the clone is currently parented under. Kept for -// historical reasons - the menu-button clone path is disabled in favor of -// repurposing the existing friend button as the settings entry point. -static void *g_lastClonedView = NULL; - -// GameObject pointer of the current menu-button clone (unused now that the -// clone code path is disabled). Preserved so the dead helpers in this file -// still compile. -static void *g_cloneGo = NULL; - -// Friend button GameObject. The retail friend button has no live wiring -// (taps trigger a "Coming soon" popup), so we redirect its OnPointerClick -// to the KiouEditor settings sheet instead. Captured every time the -// HomeUtilityPresenter ctor fires, so it stays current across scene -// re-entries. -static void *g_friendGo = NULL; - -// One-time guard for the Instantiate-method enumeration recon (Phase 2a -// debug). After the first fire we know which method handle is the -// non-generic Object.Instantiate so we do not need to re-walk every time. -static bool g_reconLogged = false; - -// Invoke instance method 0-arg returning a managed object pointer. -static void *invoke0(void *method, void *obj) { - if (!p_il2cpp_runtime_invoke || !method) return NULL; - return p_il2cpp_runtime_invoke(method, obj, NULL, NULL); -} - -// Invoke instance method that takes a single bool argument. -static void invokeSetActive(void *method, void *obj, bool value) { - if (!p_il2cpp_runtime_invoke || !method) return; - bool v = value; - void *params[1] = { &v }; - p_il2cpp_runtime_invoke(method, obj, params, NULL); -} - -static void *gameObjectOf(void *componentObj) { - if (!ptrLooksValid(componentObj)) return NULL; - if (!g_method_get_gameObject) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; - void *klass = p_il2cpp_object_get_class(componentObj); - if (!klass) return NULL; - g_method_get_gameObject = p_il2cpp_class_get_method_from_name(klass, "get_gameObject", 0); - IPALog([NSString stringWithFormat: - @"[HOME] cached get_gameObject method=%p (klass=%p)", - g_method_get_gameObject, klass]); - } - return invoke0(g_method_get_gameObject, componentObj); -} - -static void setActive(void *gameObject, bool value) { - if (!ptrLooksValid(gameObject)) return; - if (!g_method_SetActive) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; - void *klass = p_il2cpp_object_get_class(gameObject); - if (!klass) return; - g_method_SetActive = p_il2cpp_class_get_method_from_name(klass, "SetActive", 1); - IPALog([NSString stringWithFormat: - @"[HOME] cached SetActive method=%p (klass=%p)", - g_method_SetActive, klass]); - } - invokeSetActive(g_method_SetActive, gameObject, value); -} - -// GameObject.get_transform - returns the GameObject's transform. Separate -// from the Component.get_transform cache because they live on different -// klasses and the il2cpp method handles are not interchangeable. -static void *goTransformOf(void *gameObject) { - if (!ptrLooksValid(gameObject)) return NULL; - if (!g_method_GO_get_transform) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; - void *klass = p_il2cpp_object_get_class(gameObject); - if (!klass) return NULL; - g_method_GO_get_transform = p_il2cpp_class_get_method_from_name(klass, "get_transform", 0); - IPALog([NSString stringWithFormat: - @"[HOME] cached GameObject.get_transform method=%p (klass=%p)", - g_method_GO_get_transform, klass]); - } - return invoke0(g_method_GO_get_transform, gameObject); -} - -// Transform.get_parent - the Transform parent in the scene hierarchy. -static void *transformParentOf(void *transformObj) { - if (!ptrLooksValid(transformObj)) return NULL; - if (!g_method_Tf_get_parent) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; - void *klass = p_il2cpp_object_get_class(transformObj); - if (!klass) return NULL; - g_method_Tf_get_parent = p_il2cpp_class_get_method_from_name(klass, "get_parent", 0); - IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.get_parent method=%p (klass=%p)", - g_method_Tf_get_parent, klass]); - } - return invoke0(g_method_Tf_get_parent, transformObj); -} - -// Transform.SetParent(Transform parent, bool worldPositionStays). -// runtime_invoke hung the main thread on this method (same invoker_method -// problem the static Instantiate hit), so we go through methodPointer. -// IL2CPP instance-method ABI for this signature: -// void (Transform* this, Transform* parent, bool wps, MethodInfo* method) -typedef void (*Tf_SetParent_directABI_t)(void *thisTf, void *parent, bool wps, void *methodInfo); - -static void transformSetParent(void *transformObj, void *newParent, bool worldPositionStays) { - if (!ptrLooksValid(transformObj)) return; - if (!g_method_Tf_SetParent) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; - void *klass = p_il2cpp_object_get_class(transformObj); - if (!klass) return; - g_method_Tf_SetParent = p_il2cpp_class_get_method_from_name(klass, "SetParent", 2); - IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.SetParent(Tf,bool) method=%p (klass=%p)", - g_method_Tf_SetParent, klass]); - } - if (!g_method_Tf_SetParent) return; - void *methodPtr = *(void **)g_method_Tf_SetParent; - if (!methodPtr) { - IPALog(@"[HOME] Tf.SetParent direct: methodPointer NULL"); - return; - } - IPALog([NSString stringWithFormat: - @"[HOME] Tf.SetParent direct: methodPtr=%p this=%p parent=%p wps=%d", - methodPtr, transformObj, newParent, (int)worldPositionStays]); - ((Tf_SetParent_directABI_t)methodPtr)(transformObj, newParent, worldPositionStays, g_method_Tf_SetParent); -} - -// Transform.GetSiblingIndex -> Int32. Direct call instead of runtime_invoke -// for the same reason as above; this also dodges the boxed value-type -// return path entirely (the direct ABI just returns int32 by value). -typedef int32_t (*Tf_GetSiblingIndex_directABI_t)(void *thisTf, void *methodInfo); - -static int32_t transformGetSiblingIndex(void *transformObj) { - if (!ptrLooksValid(transformObj)) return -1; - if (!g_method_Tf_GetSiblingIndex) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return -1; - void *klass = p_il2cpp_object_get_class(transformObj); - if (!klass) return -1; - g_method_Tf_GetSiblingIndex = p_il2cpp_class_get_method_from_name(klass, "GetSiblingIndex", 0); - IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.GetSiblingIndex method=%p (klass=%p)", - g_method_Tf_GetSiblingIndex, klass]); - } - if (!g_method_Tf_GetSiblingIndex) return -1; - void *methodPtr = *(void **)g_method_Tf_GetSiblingIndex; - if (!methodPtr) { - IPALog(@"[HOME] Tf.GetSiblingIndex direct: methodPointer NULL"); - return -1; - } - return ((Tf_GetSiblingIndex_directABI_t)methodPtr)(transformObj, g_method_Tf_GetSiblingIndex); -} - -typedef int32_t (*Tf_get_childCount_directABI_t)(void *thisTf, void *methodInfo); -typedef void *(*Tf_GetChild_directABI_t)(void *thisTf, int32_t idx, void *methodInfo); -typedef void *(*Obj_get_name_directABI_t)(void *thisObj, void *methodInfo); - -static int32_t transformChildCount(void *transformObj) { - if (!ptrLooksValid(transformObj)) return 0; - if (!g_method_Tf_get_childCount) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return 0; - void *klass = p_il2cpp_object_get_class(transformObj); - if (!klass) return 0; - g_method_Tf_get_childCount = p_il2cpp_class_get_method_from_name(klass, "get_childCount", 0); - } - if (!g_method_Tf_get_childCount) return 0; - void *methodPtr = *(void **)g_method_Tf_get_childCount; - if (!methodPtr) return 0; - return ((Tf_get_childCount_directABI_t)methodPtr)(transformObj, g_method_Tf_get_childCount); -} - -static void *transformGetChild(void *transformObj, int32_t idx) { - if (!ptrLooksValid(transformObj)) return NULL; - if (!g_method_Tf_GetChild) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; - void *klass = p_il2cpp_object_get_class(transformObj); - if (!klass) return NULL; - g_method_Tf_GetChild = p_il2cpp_class_get_method_from_name(klass, "GetChild", 1); - } - if (!g_method_Tf_GetChild) return NULL; - void *methodPtr = *(void **)g_method_Tf_GetChild; - if (!methodPtr) return NULL; - return ((Tf_GetChild_directABI_t)methodPtr)(transformObj, idx, g_method_Tf_GetChild); -} - -// UnityEngine.Object.get_name -> System.String. Walks up the klass chain -// once on first hit since Transform's klass redeclares get_name only if -// overridden - but get_method_from_name searches parents too in IL2CPP. -static NSString *objectName(void *unityObj) { - if (!ptrLooksValid(unityObj)) return nil; - if (!g_method_Obj_get_name) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return nil; - void *klass = p_il2cpp_object_get_class(unityObj); - if (!klass) return nil; - g_method_Obj_get_name = p_il2cpp_class_get_method_from_name(klass, "get_name", 0); - IPALog([NSString stringWithFormat: - @"[HOME] cached Object.get_name method=%p (klass=%p)", - g_method_Obj_get_name, klass]); - } - if (!g_method_Obj_get_name) return nil; - void *methodPtr = *(void **)g_method_Obj_get_name; - if (!methodPtr) return nil; - void *strObj = ((Obj_get_name_directABI_t)methodPtr)(unityObj, g_method_Obj_get_name); - return il2cppStringToNSString(strObj); -} - -// Walk the Transform tree under `tfObj`, log each node's name with -// indentation. The clone is brand new so we cap depth to keep the log -// readable. Used purely as a recon pass for Phase 2c (label rewrite). -static void dumpHierarchy(void *tfObj, int depth, int maxDepth) { - if (!ptrLooksValid(tfObj)) return; - if (depth > maxDepth) return; - NSString *name = objectName(tfObj); - NSMutableString *indent = [NSMutableString string]; - for (int i = 0; i < depth; i++) [indent appendString:@" "]; - IPALog([NSString stringWithFormat: - @"[HOME] hier %@tf=%p name=%@", - indent, tfObj, name ?: @""]); - int32_t cc = transformChildCount(tfObj); - for (int32_t i = 0; i < cc; i++) { - void *child = transformGetChild(tfObj, i); - dumpHierarchy(child, depth + 1, maxDepth); - } -} - -// Find the immediate child Transform whose Object.get_name matches. -static void *transformChildByName(void *parentTf, const char *targetName) { - if (!ptrLooksValid(parentTf) || !targetName) return NULL; - int32_t cc = transformChildCount(parentTf); - NSString *needle = [NSString stringWithUTF8String:targetName]; - for (int32_t i = 0; i < cc; i++) { - void *child = transformGetChild(parentTf, i); - if (!ptrLooksValid(child)) continue; - NSString *name = objectName(child); - if ([name isEqualToString:needle]) return child; - } - return NULL; -} - -// GameObject.GetComponent(string type) at UnityFramework + 0x6BCA6AC. The -// codegen wrapper here is a thin FreeFunction marshal to native -// Scripting::GetScriptingWrapperOfComponentOfGameObject - probably ignores -// MethodInfo* so passing NULL is OK. If it crashes we'll revisit with proper -// klass-walked MethodInfo* resolution. -#define RVA_GO_GETCOMPONENT_STRING 0x6BCA6AC - -typedef void *(*GO_GetComponent_string_directABI_t)(void *thisGo, void *typeStr, void *methodInfo); - -static void *componentByTypeName(void *gameObject, const char *typeName) { - if (!ptrLooksValid(gameObject) || !typeName) return NULL; - if (!p_il2cpp_string_new || g_unityBaseAddr == 0) return NULL; - void *typeStr = p_il2cpp_string_new(typeName); - if (!typeStr) return NULL; - GO_GetComponent_string_directABI_t fn = - (GO_GetComponent_string_directABI_t)(g_unityBaseAddr + RVA_GO_GETCOMPONENT_STRING); - return fn(gameObject, typeStr, NULL); -} - -// Walks a UIButton-shaped hierarchy for the leaf that owns the icon sprite. -// HomeUtilityButton* puts the icon at Content/Image while TitleScene's -// _titleMenuButton uses Content/IconImage. Try both. -static void *findIconImageTransform(void *btnTf) { - if (!ptrLooksValid(btnTf)) return NULL; - void *contentTf = transformChildByName(btnTf, "Content"); - if (!ptrLooksValid(contentTf)) return NULL; - void *imageTf = transformChildByName(contentTf, "Image"); - if (!ptrLooksValid(imageTf)) { - imageTf = transformChildByName(contentTf, "IconImage"); - } - return imageTf; -} - -// Sprite captured from the TitleScene._titleMenuButton on the first title -// MoveNext fire. NULL until then (and during fresh launches that drop the -// user directly into a non-title screen). -static void *g_titleMenuSprite = NULL; - - -// UnityEngine.UI.Image.set_sprite resolved off the live Image component's -// klass once we have one; reused per clone Image swap. set_sprite has only -// one overload so class_get_method_from_name is unambiguous here. -typedef void (*Image_set_sprite_directABI_t)(void *thisImg, void *sprite, void *methodInfo); -static void *g_method_Image_set_sprite = NULL; - -static bool swapImageSpriteOnGo(void *imageHostGo, void *newSprite, const char *tag) { - if (!ptrLooksValid(imageHostGo) || !ptrLooksValid(newSprite)) return false; - void *imageComp = componentByTypeName(imageHostGo, "UnityEngine.UI.Image"); - if (!ptrLooksValid(imageComp)) { - IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] no Image component on go=%p", tag, imageHostGo]); - return false; - } - if (!g_method_Image_set_sprite) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return false; - void *klass = p_il2cpp_object_get_class(imageComp); - if (!klass) return false; - g_method_Image_set_sprite = - p_il2cpp_class_get_method_from_name(klass, "set_sprite", 1); - IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] cached Image.set_sprite method=%p (klass=%p)", - tag, g_method_Image_set_sprite, klass]); - } - if (!g_method_Image_set_sprite) return false; - void *methodPtr = *(void **)g_method_Image_set_sprite; - if (!methodPtr) { - IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] set_sprite methodPointer is NULL", tag]); - return false; - } - IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] applying sprite=%p to imageComp=%p (was m_Sprite=%p)", - tag, newSprite, imageComp, readPtr(imageComp, 0xD8)]); - ((Image_set_sprite_directABI_t)methodPtr)(imageComp, newSprite, g_method_Image_set_sprite); - return true; -} - -// Read the m_Sprite name on the clone's Image so we can tell whether the -// "メニュー" label is baked into the sprite (sprite name suggests a -// combined icon+text texture) or actually rendered separately somewhere. -static void reconSpriteName(void *cloneTf) { - if (!ptrLooksValid(cloneTf)) return; - void *contentTf = transformChildByName(cloneTf, "Content"); - if (!ptrLooksValid(contentTf)) return; - void *imageTf = transformChildByName(contentTf, "Image"); - if (!ptrLooksValid(imageTf)) imageTf = transformChildByName(contentTf, "IconImage"); - if (!ptrLooksValid(imageTf)) return; - void *imageGo = gameObjectOf(imageTf); - if (!ptrLooksValid(imageGo)) return; - void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); - if (!ptrLooksValid(imageComp)) return; - void *sprite = readPtr(imageComp, 0xD8); - if (!ptrLooksValid(sprite)) return; - NSString *name = objectName(sprite); - IPALog([NSString stringWithFormat: - @"[SPRITE-NAME] clone Image.m_Sprite=%p name=\"%@\"", - sprite, name ?: @""]); -} - -// Plain-C mirrors of Unity's value types. Vector3/Vector2 are HFAs on -// arm64 (3/2 contiguous floats) so they ride v0..v2 / v0..v1 on return, -// which clang matches when the struct is declared this way. -typedef struct { float x, y, z; } UVec3; -typedef struct { float x, y; } UVec2; - -typedef UVec3 (*Tf_get_position_HFA_t)(void *self, void *methodInfo); -typedef UVec2 (*Rt_get_sizeDelta_HFA_t)(void *self, void *methodInfo); - -static void *g_method_Tf_get_position = NULL; -static void *g_method_Rt_get_sizeDelta = NULL; - -// RectTransformUtility.WorldToScreenPoint(Camera cam, Vector3 worldPoint) -// at UnityFramework + 0x6F20040. Static, takes a null camera for -// ScreenSpaceOverlay canvases and returns the screen pixel position with -// bottom-left origin. Direct call with NULL methodInfo - same pattern as -// GameObject.GetComponent(string) which we proved out earlier. -#define RVA_RTU_WORLD_TO_SCREEN 0x6F20040 -typedef UVec2 (*RtU_WorldToScreenPoint_t)(void *cam, UVec3 worldPoint, void *methodInfo); - -static UVec2 unityWorldToScreen(UVec3 worldPoint) { - UVec2 zero = {0}; - if (g_unityBaseAddr == 0) return zero; - RtU_WorldToScreenPoint_t fn = - (RtU_WorldToScreenPoint_t)(g_unityBaseAddr + RVA_RTU_WORLD_TO_SCREEN); - return fn(NULL, worldPoint, NULL); -} - -// Resolve via class_get_method_from_name so we pass the real MethodInfo* -// trailing arg the codegen wrapper expects. Direct RVA + NULL methodInfo -// crashed inside the IL2CPP P/Invoke marshalling for the value-type -// returns, so we let il2cpp hand us the proper handle. -static bool readCloneScreenRect(void *cloneTf, - UVec3 *outPos, UVec2 *outSize) { - if (!ptrLooksValid(cloneTf)) return false; - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return false; - - if (!g_method_Tf_get_position || !g_method_Rt_get_sizeDelta) { - void *klass = p_il2cpp_object_get_class(cloneTf); - if (!klass) return false; - if (!g_method_Tf_get_position) { - g_method_Tf_get_position = - p_il2cpp_class_get_method_from_name(klass, "get_position", 0); - } - if (!g_method_Rt_get_sizeDelta) { - g_method_Rt_get_sizeDelta = - p_il2cpp_class_get_method_from_name(klass, "get_sizeDelta", 0); - } - IPALog([NSString stringWithFormat: - @"[CLONE-RECT] cached get_position=%p get_sizeDelta=%p (klass=%p)", - g_method_Tf_get_position, g_method_Rt_get_sizeDelta, klass]); - } - if (!g_method_Tf_get_position || !g_method_Rt_get_sizeDelta) return false; - - void *posPtr = *(void **)g_method_Tf_get_position; - void *sizePtr = *(void **)g_method_Rt_get_sizeDelta; - if (!posPtr || !sizePtr) return false; - - *outPos = ((Tf_get_position_HFA_t)posPtr)(cloneTf, g_method_Tf_get_position); - *outSize = ((Rt_get_sizeDelta_HFA_t)sizePtr)(cloneTf, g_method_Rt_get_sizeDelta); - IPALog([NSString stringWithFormat: - @"[CLONE-RECT] pos=(%g,%g,%g) sizeDelta=(%g,%g)", - outPos->x, outPos->y, outPos->z, outSize->x, outSize->y]); - return true; -} - -// Hide the clone's Image by zeroing its m_Color alpha and calling -// SetAllDirty so the canvas rebuild picks up the new color. Keeps the -// raycast target so the OnPointerClick hook still sees taps; the actual -// visual is rendered by a UIKit overlay above the Unity layer. -typedef void (*Graphic_SetAllDirty_t)(void *self, void *methodInfo); -static void *g_method_Graphic_SetAllDirty = NULL; - -static void hideCloneImage(void *cloneTf) { - if (!ptrLooksValid(cloneTf)) return; - void *contentTf = transformChildByName(cloneTf, "Content"); - if (!ptrLooksValid(contentTf)) return; - void *imageTf = transformChildByName(contentTf, "Image"); - if (!ptrLooksValid(imageTf)) imageTf = transformChildByName(contentTf, "IconImage"); - if (!ptrLooksValid(imageTf)) return; - void *imageGo = gameObjectOf(imageTf); - if (!ptrLooksValid(imageGo)) return; - void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); - if (!ptrLooksValid(imageComp)) return; - - // Graphic.m_Color @ 0x28 (Color = 4 floats RGBA). - float *color = (float *)((uint8_t *)imageComp + 0x28); - color[0] = 1.0f; - color[1] = 1.0f; - color[2] = 1.0f; - color[3] = 0.0f; - IPALog([NSString stringWithFormat: - @"[CLONE-HIDE] imageComp=%p m_Color set to (1,1,1,0)", imageComp]); - - if (!g_method_Graphic_SetAllDirty) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; - void *klass = p_il2cpp_object_get_class(imageComp); - if (!klass) return; - g_method_Graphic_SetAllDirty = p_il2cpp_class_get_method_from_name(klass, "SetAllDirty", 0); - IPALog([NSString stringWithFormat: - @"[CLONE-HIDE] cached Graphic.SetAllDirty method=%p (klass=%p)", - g_method_Graphic_SetAllDirty, klass]); - } - if (!g_method_Graphic_SetAllDirty) return; - void *methodPtr = *(void **)g_method_Graphic_SetAllDirty; - if (!methodPtr) return; - ((Graphic_SetAllDirty_t)methodPtr)(imageComp, g_method_Graphic_SetAllDirty); - IPALog(@"[CLONE-HIDE] SetAllDirty invoked"); -} - -// Probe each GameObject in the clone tree for a text component and log -// it. Helps us figure out where the inherited "メニュー" label lives so we -// can blank it on the clone. Recon-only, no mutations. -static void reconTextComponents(void *cloneTf) { - if (!ptrLooksValid(cloneTf)) return; - void *cloneGo = gameObjectOf(cloneTf); - void *contentTf = transformChildByName(cloneTf, "Content"); - void *contentGo = ptrLooksValid(contentTf) ? gameObjectOf(contentTf) : NULL; - void *imageTf = ptrLooksValid(contentTf) ? transformChildByName(contentTf, "Image") : NULL; - if (!ptrLooksValid(imageTf) && ptrLooksValid(contentTf)) { - imageTf = transformChildByName(contentTf, "IconImage"); - } - void *imageGo = ptrLooksValid(imageTf) ? gameObjectOf(imageTf) : NULL; - - void *grayTf = ptrLooksValid(imageTf) ? transformChildByName(imageTf, "GrayoutCover_Toggle") : NULL; - void *grayGo = ptrLooksValid(grayTf) ? gameObjectOf(grayTf) : NULL; - - const char *names[] = { - "TMPro.TextMeshProUGUI", - "UnityEngine.UI.Text", - "TMPro.TextMeshPro", - }; - struct { const char *tag; void *go; } pts[] = { - { "button-go", cloneGo }, - { "content-go", contentGo }, - { "image-go", imageGo }, - { "gray-go", grayGo }, - }; - for (int p = 0; p < 4; p++) { - if (!ptrLooksValid(pts[p].go)) continue; - for (int n = 0; n < 3; n++) { - void *c = componentByTypeName(pts[p].go, names[n]); - IPALog([NSString stringWithFormat: - @"[TEXT-RECON] %s GetComponent(\"%s\")=%p", - pts[p].tag, names[n], c]); - } - } -} - -// Read the m_Sprite (offset 0xD8) of the Image component on uiButton's -// Content/Image leaf. Used by callers that want to harvest a sprite handle -// from a sibling button without going through the full recon logger. -static void *spriteOfButton(void *uiButton) { - if (!ptrLooksValid(uiButton)) return NULL; - void *btnGo = gameObjectOf(uiButton); - if (!ptrLooksValid(btnGo)) return NULL; - void *btnTf = goTransformOf(btnGo); - void *imageTf = findIconImageTransform(btnTf); - if (!ptrLooksValid(imageTf)) return NULL; - void *imageGo = gameObjectOf(imageTf); - if (!ptrLooksValid(imageGo)) return NULL; - void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); - if (!ptrLooksValid(imageComp)) return NULL; - return readPtr(imageComp, 0xD8); -} - -// Apply a sibling sprite to a freshly cloned home utility button. The -// caller passes the gift / friend / menu button as a sprite source; this -// avoids the title atlas-unload trap until a permanent sprite source (a -// bundled PNG / SF Symbol generated Texture2D) is wired up. -static bool applySiblingSpriteToClone(void *cloneGo, void *sourceBtn, const char *sourceTag) { - if (!ptrLooksValid(cloneGo) || !ptrLooksValid(sourceBtn)) return false; - void *sprite = spriteOfButton(sourceBtn); - if (!ptrLooksValid(sprite)) { - IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP clone] no sprite on %s source", sourceTag]); - return false; - } - void *cloneTf = goTransformOf(cloneGo); - void *imageTf = findIconImageTransform(cloneTf); - if (!ptrLooksValid(imageTf)) return false; - void *imageGo = gameObjectOf(imageTf); - IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP clone] source=%s sprite=%p", sourceTag, sprite]); - return swapImageSpriteOnGo(imageGo, sprite, "clone"); -} - -// Phase 0 verification: apply the gift sprite to the clone. Gift sits on -// the same home strip as the clone, so the atlas is guaranteed loaded for -// the duration the clone is alive. If the clone renders gift icon visibly, -// set_sprite + canvas invalidation work; the white title-swap result was -// purely the title atlas getting unloaded post-scene-transition. -// -// Currently this also still tries the title sprite if no gift swap target -// was passed in - title path is left in place for direct comparison. -void KIOUEditorApplyTitleSpriteToClone(void *cloneGo) { - (void)cloneGo; - // Kept as a no-op placeholder so the call site in the clone path stays - // unchanged while we route through the new sibling sprite helper. - // The actual swap is now driven from hook_HUP_ctor via giftBtn. -} - -// Public recon entry. Walks uiButton -> btnGo -> btnTf -> "Content" -> -// "Image" -> GO -> GetComponent("UnityEngine.UI.Image") -> m_Sprite@+0xD8. -// Logs every step so we can see where it bails when something is missing. -void KIOUEditorReconButtonImage(void *uiButton, const char *tag) { - if (!ptrLooksValid(uiButton)) { - IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] button ptr invalid (%p)", tag, uiButton]); - return; - } - void *btnGo = gameObjectOf(uiButton); - void *btnTf = goTransformOf(btnGo); - IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] btn=%p go=%p tf=%p", - tag, uiButton, btnGo, btnTf]); - if (!ptrLooksValid(btnTf)) return; - - void *imageTf = findIconImageTransform(btnTf); - if (!ptrLooksValid(imageTf)) { - IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] no Image/IconImage leaf - dumping btnTf:", - tag]); - dumpHierarchy(btnTf, 0, 3); - return; - } - void *imageGo = gameObjectOf(imageTf); - IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] imageTf=%p imageGo=%p", - tag, imageTf, imageGo]); - if (!ptrLooksValid(imageGo)) return; - - void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); - IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] GetComponent(\"UnityEngine.UI.Image\")=%p", - tag, imageComp]); - if (!ptrLooksValid(imageComp)) return; - - void *sprite = readPtr(imageComp, 0xD8); - IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] m_Sprite=%p", tag, sprite]); - - // Title side: cache the sprite so the home clone hook can swap it in. - if (tag && strcmp(tag, "title-menu") == 0 && ptrLooksValid(sprite)) { - g_titleMenuSprite = sprite; - IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] cached title menu sprite for clone swap", - tag]); - } -} - -typedef void (*Tf_SetSiblingIndex_directABI_t)(void *thisTf, int32_t idx, void *methodInfo); - -static void transformSetSiblingIndex(void *transformObj, int32_t idx) { - if (!ptrLooksValid(transformObj)) return; - if (!g_method_Tf_SetSiblingIndex) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; - void *klass = p_il2cpp_object_get_class(transformObj); - if (!klass) return; - g_method_Tf_SetSiblingIndex = p_il2cpp_class_get_method_from_name(klass, "SetSiblingIndex", 1); - IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.SetSiblingIndex method=%p (klass=%p)", - g_method_Tf_SetSiblingIndex, klass]); - } - if (!g_method_Tf_SetSiblingIndex) return; - void *methodPtr = *(void **)g_method_Tf_SetSiblingIndex; - if (!methodPtr) { - IPALog(@"[HOME] Tf.SetSiblingIndex direct: methodPointer NULL"); - return; - } - ((Tf_SetSiblingIndex_directABI_t)methodPtr)(transformObj, idx, g_method_Tf_SetSiblingIndex); -} - -// Component.get_transform - returns this.transform. -static void *transformOf(void *componentObj) { - if (!ptrLooksValid(componentObj)) return NULL; - if (!g_method_get_transform) { - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; - void *klass = p_il2cpp_object_get_class(componentObj); - if (!klass) return NULL; - g_method_get_transform = p_il2cpp_class_get_method_from_name(klass, "get_transform", 0); - IPALog([NSString stringWithFormat: - @"[HOME] cached get_transform method=%p (klass=%p)", - g_method_get_transform, klass]); - } - return invoke0(g_method_get_transform, componentObj); -} - -// Recon: walk every method on UnityEngine.Object (resolved via parent klass -// of any GameObject we already hold) and log each "Instantiate" variant -// with (name, argc, is_generic, method_ptr). From the log we can pick the -// non-generic method handle directly and invoke it later without racing -// the generic ones via get_method_from_name. Pure logging - no invoke. -static void logInstantiateMethods(void *anyGo) { - if (!p_il2cpp_object_get_class - || !p_il2cpp_class_get_parent - || !p_il2cpp_class_get_methods - || !p_il2cpp_method_get_name - || !p_il2cpp_method_get_param_count) { - IPALog(@"[HOME] enum recon: bridge incomplete, skipping"); - return; - } - void *goKlass = p_il2cpp_object_get_class(anyGo); - if (!goKlass) return; - void *objKlass = p_il2cpp_class_get_parent(goKlass); - if (!objKlass) return; - IPALog([NSString stringWithFormat: - @"[HOME] enum: walking Object klass=%p", objKlass]); - void *iter = NULL; - void *method = NULL; - int hits = 0; - while ((method = p_il2cpp_class_get_methods(objKlass, &iter)) != NULL) { - const char *name = p_il2cpp_method_get_name(method); - if (!name) continue; - if (strstr(name, "nstantiate") == NULL) continue; - uint32_t argc = p_il2cpp_method_get_param_count(method); - int isGeneric = -1; - if (p_il2cpp_method_is_generic) { - isGeneric = (int)p_il2cpp_method_is_generic(method); - } - IPALog([NSString stringWithFormat: - @"[HOME] enum: %s argc=%u generic=%d method=%p", - name, argc, isGeneric, method]); - hits++; - } - IPALog([NSString stringWithFormat: - @"[HOME] enum: %d Instantiate variants found", hits]); -} - -// Walk a klass's methods and return the first one matching name + argc that -// is NOT generic. Hand-rolled because il2cpp_class_get_method_from_name has -// no generic filter and races the generic Instantiate descriptors first. -static void *findNonGenericMethod(void *klass, const char *targetName, uint32_t targetArgc) { - if (!klass) return NULL; - if (!p_il2cpp_class_get_methods - || !p_il2cpp_method_get_name - || !p_il2cpp_method_get_param_count - || !p_il2cpp_method_is_generic) return NULL; - void *iter = NULL; - void *method = NULL; - while ((method = p_il2cpp_class_get_methods(klass, &iter)) != NULL) { - const char *name = p_il2cpp_method_get_name(method); - if (!name) continue; - if (strcmp(name, targetName) != 0) continue; - if (p_il2cpp_method_get_param_count(method) != targetArgc) continue; - if (p_il2cpp_method_is_generic(method)) continue; - return method; - } - return NULL; -} - -// Object.Instantiate(Object original) - explicit non-generic match. -// Clone goes to root scene with null parent. Use SetParent in a later phase -// to slot it into the home layout. -static void *instantiateCloneNonGeneric(void *originalGo) { - if (!ptrLooksValid(originalGo)) return NULL; - if (!p_il2cpp_runtime_invoke - || !p_il2cpp_object_get_class - || !p_il2cpp_class_get_parent) return NULL; - if (!g_method_Instantiate1NonGen) { - void *goKlass = p_il2cpp_object_get_class(originalGo); - if (!goKlass) return NULL; - void *objKlass = p_il2cpp_class_get_parent(goKlass); - if (!objKlass) return NULL; - g_method_Instantiate1NonGen = findNonGenericMethod(objKlass, "Instantiate", 1); - IPALog([NSString stringWithFormat: - @"[HOME] cached non-generic Instantiate(Object) method=%p (objKlass=%p)", - g_method_Instantiate1NonGen, objKlass]); - } - if (!g_method_Instantiate1NonGen) return NULL; - void *originalRef = originalGo; - void *params[1] = { &originalRef }; - return p_il2cpp_runtime_invoke(g_method_Instantiate1NonGen, NULL, params, NULL); -} - -// Direct call into MethodInfo->methodPointer (offset 0 on Unity 6 IL2CPP), -// bypassing runtime_invoke entirely. IL2CPP appends a MethodInfo* slot to -// every method's native signature; the C ABI for the static one-arg -// Object.Instantiate(Object) is: -// Object* (Object* original, const MethodInfo* method) -// Tried because the runtime_invoke path crashes inside the invoker even -// after the recon confirmed we hold the non-generic method handle. The -// methodPointer is the actually-generated native function, no invoker -// trampoline involved. -typedef void *(*Instantiate1_directABI_t)(void *original, void *methodInfo); - -static void *instantiateCloneDirect(void *originalGo) { - if (!ptrLooksValid(originalGo)) return NULL; - if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_parent) return NULL; - if (!g_method_Instantiate1NonGen) { - void *goKlass = p_il2cpp_object_get_class(originalGo); - if (!goKlass) return NULL; - void *objKlass = p_il2cpp_class_get_parent(goKlass); - if (!objKlass) return NULL; - g_method_Instantiate1NonGen = findNonGenericMethod(objKlass, "Instantiate", 1); - IPALog([NSString stringWithFormat: - @"[HOME] cached non-generic Instantiate(Object) method=%p (objKlass=%p)", - g_method_Instantiate1NonGen, objKlass]); - } - if (!g_method_Instantiate1NonGen) return NULL; - void *methodPtr = *(void **)g_method_Instantiate1NonGen; - if (!methodPtr) { - IPALog(@"[HOME] direct: methodPointer at offset 0 is NULL"); - return NULL; - } - IPALog([NSString stringWithFormat: - @"[HOME] direct call: methodPtr=%p methodInfo=%p original=%p", - methodPtr, g_method_Instantiate1NonGen, originalGo]); - return ((Instantiate1_directABI_t)methodPtr)(originalGo, g_method_Instantiate1NonGen); -} - -// UnityEngine.Object.Instantiate(Object original, Transform parent) - static. -// 2-arg overload picked over the argc=1 version because the argc=1 path -// matched the generic Instantiate(T) descriptor and runtime_invoke -// crashed inside the un-inflated generic call. The 2-arg non-generic -// overload coexists with a generic counterpart too, so we still race the -// lookup; if this also crashes we will need to enumerate methods and -// filter by il2cpp_method_is_generic. -static void *instantiateCloneWithParent(void *originalGo, void *parentTransform) { - if (!ptrLooksValid(originalGo)) return NULL; - if (!p_il2cpp_runtime_invoke - || !p_il2cpp_object_get_class - || !p_il2cpp_class_get_parent - || !p_il2cpp_class_get_method_from_name) return NULL; - if (!g_method_Instantiate2) { - void *goKlass = p_il2cpp_object_get_class(originalGo); - if (!goKlass) return NULL; - void *objKlass = p_il2cpp_class_get_parent(goKlass); - if (!objKlass) { - IPALog(@"[HOME] Instantiate lookup: parent klass NULL"); - return NULL; - } - g_method_Instantiate2 = p_il2cpp_class_get_method_from_name(objKlass, "Instantiate", 2); - IPALog([NSString stringWithFormat: - @"[HOME] cached Instantiate(Obj,Tf) method=%p (goKlass=%p objKlass=%p)", - g_method_Instantiate2, goKlass, objKlass]); - } - if (!g_method_Instantiate2) return NULL; - void *originalRef = originalGo; - void *parentRef = parentTransform; - void *params[2] = { &originalRef, &parentRef }; - return p_il2cpp_runtime_invoke(g_method_Instantiate2, NULL, params, NULL); -} - -// --------------------------------------------------------------------------- -// Settings UI bridge - implemented in Hook_SettingsUI.m (Phase 2e). Called -// from the OnPointerClick hook when the clone is tapped. -// --------------------------------------------------------------------------- - -// --------------------------------------------------------------------------- -// Hook -// --------------------------------------------------------------------------- - -#define RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD1E08 - typedef void (*HUP_ctor_t)(void *self, void *view); typedef void (*UIBtn_OnPointerClick_t)(void *self, void *eventData, void *methodInfo); -static HUP_ctor_t orig_HUP_ctor = NULL; +static HUP_ctor_t orig_HUP_ctor = NULL; static UIBtn_OnPointerClick_t orig_UIBtn_OnPointerClick = NULL; -// UIButtonBase.IPointerClickHandler.OnPointerClick - fires for every +// UIButtonBase.IPointerClickHandler.OnPointerClick fires for every // UIButtonBase-derived button (including UIButton, since UIButton does not // override slot 17). We compare each call's `this.gameObject` against the -// menu-button clone we created in Phase 2a; on match, dispatch to the -// KiouEditor settings UI and skip orig (the clone's _onClick Subject has -// no subscribers anyway, so calling orig would be a no-op, but skipping it -// also avoids any future hidden subscribers). +// friend button GameObject captured at HUP.ctor time; on match, dispatch +// to the KiouEditor settings UI and skip orig (the retail friend button +// only shows a "Coming soon" popup, so bypassing it is the desired UX). static void hook_UIBtn_OnPointerClick(void *self, void *eventData, void *methodInfo) { @try { if (ptrLooksValid(self)) { void *thisGo = gameObjectOf(self); - // Friend button taps go straight to the KiouEditor settings - // sheet, bypassing the retail "Coming soon" popup orig would - // otherwise show. if (g_friendGo && thisGo == g_friendGo) { IPALog([NSString stringWithFormat: @"[HOME] friend tap -> settings (self=%p go=%p)", @@ -920,8 +56,9 @@ static void hook_UIBtn_OnPointerClick(void *self, void *eventData, void *methodI KIOUEditorPresentSettings(); return; } - // Legacy: the menu-button clone is still recognised in case - // the clone code path is re-enabled later for testing. + // Legacy: menu-button clone path is disabled (see hook_HUP_ctor + // below), kept only so the guard still passes if someone ever + // re-enables the branch for testing. if (g_cloneGo && thisGo == g_cloneGo) { IPALog([NSString stringWithFormat: @"[HOME] clone tap -> settings (self=%p go=%p)", @@ -951,17 +88,13 @@ static void hook_HUP_ctor(void *self, void *view) { void *menuBtn = readPtr(view, OFF_HUV_MENU_BUTTON); void *giftBtn = readPtr(view, OFF_HUV_GIFT_BUTTON); void *friendBtn = readPtr(view, OFF_HUV_FRIEND_BUTTON); + (void)menuBtn; (void)giftBtn; IPALog([NSString stringWithFormat: @"[HOME] HomeUtilityView@%p buttons: menu=%p gift=%p friend=%p", view, menuBtn, giftBtn, friendBtn]); - // FRIEND_UNHIDE only gates the SetActive on the friend button itself. - // The settings clone below is created unconditionally so the user - // can always re-toggle the flag from the UI - making the clone - // depend on this flag would lock the user out (no clone -> no - // settings -> no way to flip the flag back on). // Friend button is always SetActive(true) because it doubles as - // the settings entry. No feature flag - turning it off would lock + // the settings entry. No feature flag — turning it off would lock // the user out of the KiouEditor sheet. if (ptrLooksValid(friendBtn)) { void *friendGo = gameObjectOf(friendBtn); @@ -969,7 +102,7 @@ static void hook_HUP_ctor(void *self, void *view) { IPALog([NSString stringWithFormat: @"[HOME] friend gameObject=%p -> SetActive(true)", friendGo]); setActive(friendGo, true); - // Snapshot the friend GO so the OnPointerClick hook below + // Snapshot the friend GO so the OnPointerClick hook above // can recognise the tap and route to settings instead of // the "Coming soon" popup the orig handler shows. g_friendGo = friendGo; @@ -978,29 +111,18 @@ static void hook_HUP_ctor(void *self, void *view) { } } - // Phase 2a attempt 4: bypass runtime_invoke and call methodPointer - // directly. Thread diagnostic already confirmed main thread = 1, so - // the runtime_invoke crashes are from something inside the invoker - // trampoline rather than threading. methodPointer is the actual - // codegen'd function with the static IL2CPP ABI - // (Object* (Object* original, MethodInfo* method)). - (void)giftBtn; - (void)g_reconLogged; - (void)logInstantiateMethods; - (void)instantiateCloneNonGeneric; - (void)instantiateCloneWithParent; - // Clone creation disabled - friend button now doubles as the - // settings entry. Kept in source so the existing il2cpp bridge - // helpers compile and we can reactivate the path later if needed. - if (0 /* disabled */ + // Menu-button clone path is disabled — the friend button now doubles + // as the settings entry so the clone is no longer needed. Kept as + // dead code (guarded by `if (0)`) so the workflow can be + // reactivated for testing if the friend-button UX needs to move. + // Every helper it references still exists in FriendUnhideBridgeUI.m. + if (0 /* disabled: menu-button clone */ && view != g_lastClonedView && ptrLooksValid(menuBtn) && ptrLooksValid(friendBtn)) { IPALog([NSString stringWithFormat: @"[HOME] presenter.ctor on main thread=%d (view %p -> %p)", (int)[NSThread isMainThread], g_lastClonedView, view]); - // One-shot recon: walk the live menu button's Image to confirm - // the same path works on the home side. Logs once. static bool s_homeMenuReconDone = false; if (!s_homeMenuReconDone) { KIOUEditorReconButtonImage(menuBtn, "home-menu"); @@ -1014,20 +136,6 @@ static void hook_HUP_ctor(void *self, void *view) { g_cloneGo = cloneGo; IPALog([NSString stringWithFormat: @"[HOME] direct: clone gameObject=%p", cloneGo]); - // No sprite swap. Instantiate copies the menu button's - // Image component including its Sprite reference, which - // is the same atlas-backed img_ico_menu (no embedded - // text). Calling set_sprite to a foreign sprite was the - // source of the title-swap white render (cross-atlas - // unload). Leaving the inherited reference avoids it. - (void)applySiblingSpriteToClone; - (void)spriteOfButton; - (void)KIOUEditorApplyTitleSpriteToClone; - - // One-shot recon: find which GameObject (button GO, - // Content GO, Image GO) carries the "メニュー" Text - // component on the clone. Once located we can blank - // it instead of stripping the icon. static bool s_textReconDone = false; if (!s_textReconDone) { void *cloneTfRecon = goTransformOf(cloneGo); @@ -1035,14 +143,9 @@ static void hook_HUP_ctor(void *self, void *view) { reconTextComponents(cloneTfRecon); s_textReconDone = true; } - // Make the inherited "menu + text" sprite invisible so - // a UIKit overlay can render the real settings icon. void *cloneTfForLayout = goTransformOf(cloneGo); hideCloneImage(cloneTfForLayout); - (void)readCloneScreenRect; - // Phase 2b: slot the clone into the friend button's parent - // container, one position below the friend button. void *friendTf = transformOf(friendBtn); void *cloneTf = goTransformOf(cloneGo); IPALog([NSString stringWithFormat: @@ -1066,15 +169,8 @@ static void hook_HUP_ctor(void *self, void *view) { } } - // Phase 2c recon: dump the clone's transform subtree so - // we can spot the label node (TMP / Text) to overwrite. IPALog(@"[HOME] phase2c recon: dump clone hierarchy"); dumpHierarchy(cloneTf, 0, 6); - - // Compare against the live menu / friend buttons - if - // the clone tree looks too shallow it might be because - // children spawn lazily after ctor; the live buttons - // are fully populated by now. IPALog(@"[HOME] phase2c recon: dump menu (original) hierarchy"); void *menuTf = transformOf(menuBtn); dumpHierarchy(menuTf, 0, 6); @@ -1090,26 +186,18 @@ static void hook_HUP_ctor(void *self, void *view) { } } -// resolveIl2cppBridge() / g_unityBaseAddr / p_il2cpp_string_new initialisation -// is required by BOTH distribution modes — the hook bodies and the home-button -// clone path call into the il2cpp bridge regardless of how the install/publish -// side got wired. Pull it out so the JB installer and the binpatch publisher -// share one bootstrap helper. -static void friendUnhide_initBridge(uintptr_t unityBase) { - resolveIl2cppBridge(); - g_unityBaseAddr = unityBase; - if (!p_il2cpp_string_new) { - p_il2cpp_string_new = (il2cpp_string_new_t)dlsym(RTLD_DEFAULT, "il2cpp_string_new"); - } -} - +// --------------------------------------------------------------------------- +// Installer. FriendUnhideBridgeInit() must run before KIOUHookInstall so the +// il2cpp bridge is live for the hook bodies' first fire. +// --------------------------------------------------------------------------- void KIOUEditorInstallFriendUnhideHook(uintptr_t unityBase) { - friendUnhide_initBridge(unityBase); + FriendUnhideBridgeInit(unityBase); orig_HUP_ctor = (HUP_ctor_t)KIOUHookInstall( KIOU_HOOK_NAME_HOME_UTILITY_PRESENTER_CTOR, (void *)hook_HUP_ctor, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_HOME_UTILITY_PRESENTER_CTOR, hook_HUP_ctor); + orig_UIBtn_OnPointerClick = (UIBtn_OnPointerClick_t)KIOUHookInstall( KIOU_HOOK_NAME_UIBUTTONBASE_ONPOINTERCLICK, (void *)hook_UIBtn_OnPointerClick, unityBase); diff --git a/Hook/FriendUnhideBridge.h b/Hook/FriendUnhideBridge.h new file mode 100644 index 0000000..525b904 --- /dev/null +++ b/Hook/FriendUnhideBridge.h @@ -0,0 +1,103 @@ +#pragma once + +#import +#import +#import + +// =========================================================================== +// Hook/FriendUnhideBridge.h — private bridge surface for Hook/FriendUnhide.m. +// +// Everything below is consumed only by FriendUnhide.m — the shared header +// KIOU-Hook consumers include is Hook/Common.h, which already exposes the +// two truly public entry points (KIOUEditorReconButtonImage and +// KIOUEditorApplyTitleSpriteToClone). +// +// The split exists because FriendUnhide.m's il2cpp / Unity bridging layer +// runs to ~800 lines on its own. Keeping it in the same TU as the hook body +// pushed the file past 1100 lines, well over the project's 600-line +// hard-split threshold. The bridge lives here so the hook body stays the +// concise ~230-line file it wants to be. +// +// If you are a KIOU-Hook consumer (KiouEditor / KiouForge / ...) you should +// never need to import this header. It only ships with Hook/FriendUnhide.m +// as an internal implementation detail. +// =========================================================================== + +// --------------------------------------------------------------------------- +// One-shot bootstrap the hook installer runs before wiring the two RVAs. +// Resolves the il2cpp runtime symbols (runtime_invoke, class lookup, method +// lookup, string_new) via dlsym and stashes UnityFramework's base address +// so the direct-ABI helpers below can reach RVA-addressed functions. +// Safe to call more than once. +// --------------------------------------------------------------------------- +void FriendUnhideBridgeInit(uintptr_t unityBase); + +// --------------------------------------------------------------------------- +// Component / GameObject / Transform bridging. +// +// Each helper caches its il2cpp method pointer on first fire (per unique +// klass; the caches are keyed by "first klass this method was seen on") +// and hits a `dlsym`-resolved il2cpp_runtime_invoke or a direct-ABI call +// after that. NULL is returned when the bridge is not fully resolved. +// --------------------------------------------------------------------------- + +// Component.get_gameObject. +void *gameObjectOf(void *componentObj); +// Component.get_transform — cached on the Component klass side. +void *transformOf(void *componentObj); +// GameObject.get_transform — cached on the GameObject klass side (distinct +// method handle from Component.get_transform). +void *goTransformOf(void *gameObject); +// GameObject.SetActive(bool). +void setActive(void *gameObject, bool value); + +// Transform.get_parent. +void *transformParentOf(void *transformObj); +// Transform.SetParent(Transform parent, bool worldPositionStays) — direct +// ABI because runtime_invoke hangs on the invoker for this signature. +void transformSetParent(void *transformObj, void *newParent, bool worldPositionStays); +// Transform.GetSiblingIndex — direct ABI (int32 return). +int32_t transformGetSiblingIndex(void *transformObj); +// Transform.SetSiblingIndex(int) — direct ABI. +void transformSetSiblingIndex(void *transformObj, int32_t idx); + +// --------------------------------------------------------------------------- +// UI recon + clone bridging. Used both to log the live hierarchies and to +// mutate the freshly instantiated clone. +// --------------------------------------------------------------------------- + +// Walk the Transform tree under `tfObj`, log each node's name with +// indentation. Depth-capped for readability. +void dumpHierarchy(void *tfObj, int depth, int maxDepth); + +// Log Image.m_Sprite name on the clone. Recon-only. +void reconSpriteName(void *cloneTf); + +// Probe every GameObject in the clone tree for a TMPro / uGUI text +// component and log which ones carry one. Recon-only. +void reconTextComponents(void *cloneTf); + +// Zero out the clone's Image alpha + call SetAllDirty so the visual is +// invisible while the raycast target remains live (taps still fire). +void hideCloneImage(void *cloneTf); + +// Object.Instantiate(Object original) — direct call into MethodInfo-> +// methodPointer, bypassing runtime_invoke's crashy invoker path. +void *instantiateCloneDirect(void *originalObj); + +// --------------------------------------------------------------------------- +// State shared between the bridge and the hook body. +// +// - g_friendGo: the friend button's GameObject captured at HUP.ctor time. +// Compared against `this.gameObject` in the OnPointerClick hook so the +// friend tap can be routed to KIOUEditorPresentSettings instead of the +// retail "Coming soon" popup. +// +// - g_cloneGo / g_lastClonedView: legacy state for the menu-button clone +// path (currently disabled). Kept so the disabled branch in +// FriendUnhide.m still compiles. +// --------------------------------------------------------------------------- + +extern void *g_friendGo; +extern void *g_cloneGo; +extern void *g_lastClonedView; diff --git a/Hook/FriendUnhideBridge.m b/Hook/FriendUnhideBridge.m new file mode 100644 index 0000000..f79f79d --- /dev/null +++ b/Hook/FriendUnhideBridge.m @@ -0,0 +1,441 @@ +#import "Hook/Common.h" +#import "Hook/FriendUnhideBridge.h" +#import "logging.h" +#import +#import "Hook/FriendUnhideBridge_Private.h" + +// =========================================================================== +// Hook/FriendUnhideBridge.m — il2cpp + Unity bridging layer for the friend +// button unhide + settings redirect hook body in Hook/FriendUnhide.m. +// +// Split out of FriendUnhide.m to keep both files under the project's +// 600-line hard-split threshold. See Hook/FriendUnhideBridge.h for the +// tiny external surface; every other declaration inside this file stays +// TU-private (static). +// =========================================================================== + +// --------------------------------------------------------------------------- +// il2cpp runtime bridge (Phase 1a: resolved but unused; Phase 1b will call +// invoke + class_from_name + class_get_method_from_name for SetActive). +// --------------------------------------------------------------------------- + +typedef void *(*il2cpp_runtime_invoke_t)(void *method, void *obj, void **params, void **exc); +typedef void *(*il2cpp_class_from_name_t)(void *image, const char *ns, const char *name); +typedef void *(*il2cpp_string_new_t)(const char *s); +il2cpp_string_new_t p_il2cpp_string_new = NULL; + +// UnityFramework load address - captured at install. Used to direct-call +// methods by RVA (GameObject.GetComponent(string) at 0x6BCA6AC). +uintptr_t g_unityBaseAddr = 0; +typedef void *(*il2cpp_class_get_method_from_name_t)(void *klass, const char *name, int argc); +typedef void *(*il2cpp_object_get_class_t)(void *obj); +typedef void *(*il2cpp_class_get_parent_t)(void *klass); +typedef void *(*il2cpp_class_get_methods_t)(void *klass, void **iter); +typedef const char *(*il2cpp_method_get_name_t)(void *method); +typedef uint32_t (*il2cpp_method_get_param_count_t)(void *method); +typedef bool (*il2cpp_method_is_generic_t)(void *method); + +il2cpp_runtime_invoke_t p_il2cpp_runtime_invoke = NULL; +il2cpp_class_from_name_t p_il2cpp_class_from_name = NULL; +il2cpp_class_get_method_from_name_t p_il2cpp_class_get_method_from_name = NULL; +il2cpp_object_get_class_t p_il2cpp_object_get_class = NULL; +il2cpp_class_get_parent_t p_il2cpp_class_get_parent = NULL; +il2cpp_class_get_methods_t p_il2cpp_class_get_methods = NULL; +il2cpp_method_get_name_t p_il2cpp_method_get_name = NULL; +il2cpp_method_get_param_count_t p_il2cpp_method_get_param_count = NULL; +il2cpp_method_is_generic_t p_il2cpp_method_is_generic = NULL; + +static void resolveIl2cppBridge(void) { + if (p_il2cpp_runtime_invoke) return; + p_il2cpp_runtime_invoke = (il2cpp_runtime_invoke_t)dlsym(RTLD_DEFAULT, "il2cpp_runtime_invoke"); + p_il2cpp_class_from_name = (il2cpp_class_from_name_t)dlsym(RTLD_DEFAULT, "il2cpp_class_from_name"); + p_il2cpp_class_get_method_from_name = (il2cpp_class_get_method_from_name_t)dlsym(RTLD_DEFAULT, "il2cpp_class_get_method_from_name"); + p_il2cpp_object_get_class = (il2cpp_object_get_class_t)dlsym(RTLD_DEFAULT, "il2cpp_object_get_class"); + p_il2cpp_class_get_parent = (il2cpp_class_get_parent_t)dlsym(RTLD_DEFAULT, "il2cpp_class_get_parent"); + p_il2cpp_class_get_methods = (il2cpp_class_get_methods_t)dlsym(RTLD_DEFAULT, "il2cpp_class_get_methods"); + p_il2cpp_method_get_name = (il2cpp_method_get_name_t)dlsym(RTLD_DEFAULT, "il2cpp_method_get_name"); + p_il2cpp_method_get_param_count = (il2cpp_method_get_param_count_t)dlsym(RTLD_DEFAULT, "il2cpp_method_get_param_count"); + p_il2cpp_method_is_generic = (il2cpp_method_is_generic_t)dlsym(RTLD_DEFAULT, "il2cpp_method_is_generic"); + IPALog([NSString stringWithFormat: + @"[HOME] il2cpp bridge: runtime_invoke=%p class_from_name=%p class_get_method_from_name=%p object_get_class=%p class_get_parent=%p class_get_methods=%p method_get_name=%p method_get_param_count=%p method_is_generic=%p", + p_il2cpp_runtime_invoke, + p_il2cpp_class_from_name, + p_il2cpp_class_get_method_from_name, + p_il2cpp_object_get_class, + p_il2cpp_class_get_parent, + p_il2cpp_class_get_methods, + p_il2cpp_method_get_name, + p_il2cpp_method_get_param_count, + p_il2cpp_method_is_generic]); +} + +// --------------------------------------------------------------------------- +// Cached method pointers - resolved from the live objects' klasses on the +// first ctor fire, then reused. The il2cpp method pointers are stable for +// the lifetime of the dylib so caching is safe. +// --------------------------------------------------------------------------- + +static void *g_method_get_gameObject = NULL; // Component.get_gameObject +static void *g_method_get_transform = NULL; // Component.get_transform (cached off Component-derived obj) +static void *g_method_GO_get_transform = NULL; // GameObject.get_transform +static void *g_method_SetActive = NULL; // GameObject.SetActive +static void *g_method_Instantiate2 = NULL; // UnityEngine.Object.Instantiate(Object, Transform) +static void *g_method_Instantiate1NonGen = NULL; // UnityEngine.Object.Instantiate(Object) non-generic +static void *g_method_Tf_get_parent = NULL; // Transform.get_parent +static void *g_method_Tf_SetParent = NULL; // Transform.SetParent(Transform,bool) +static void *g_method_Tf_GetSiblingIndex = NULL; // Transform.GetSiblingIndex +static void *g_method_Tf_SetSiblingIndex = NULL; // Transform.SetSiblingIndex(int) +static void *g_method_Tf_get_childCount = NULL; // Transform.get_childCount +static void *g_method_Tf_GetChild = NULL; // Transform.GetChild(int) +static void *g_method_Obj_get_name = NULL; // UnityEngine.Object.get_name + +// HomeUtilityView pointer the clone is currently parented under. Kept for +// historical reasons - the menu-button clone path is disabled in favor of +// repurposing the existing friend button as the settings entry point. +void *g_lastClonedView = NULL; + +// GameObject pointer of the current menu-button clone (unused now that the +// clone code path is disabled). Preserved so the dead helpers in this file +// still compile. +void *g_cloneGo = NULL; + +// Friend button GameObject. The retail friend button has no live wiring +// (taps trigger a "Coming soon" popup), so we redirect its OnPointerClick +// to the KiouEditor settings sheet instead. Captured every time the +// HomeUtilityPresenter ctor fires, so it stays current across scene +// re-entries. +void *g_friendGo = NULL; + +// One-time guard for the Instantiate-method enumeration recon (Phase 2a +// debug). After the first fire we know which method handle is the +// non-generic Object.Instantiate so we do not need to re-walk every time. +static bool g_reconLogged = false; + +// Invoke instance method 0-arg returning a managed object pointer. +void *invoke0(void *method, void *obj) { + if (!p_il2cpp_runtime_invoke || !method) return NULL; + return p_il2cpp_runtime_invoke(method, obj, NULL, NULL); +} + +// Invoke instance method that takes a single bool argument. +static void invokeSetActive(void *method, void *obj, bool value) { + if (!p_il2cpp_runtime_invoke || !method) return; + bool v = value; + void *params[1] = { &v }; + p_il2cpp_runtime_invoke(method, obj, params, NULL); +} + +void *gameObjectOf(void *componentObj) { + if (!ptrLooksValid(componentObj)) return NULL; + if (!g_method_get_gameObject) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; + void *klass = p_il2cpp_object_get_class(componentObj); + if (!klass) return NULL; + g_method_get_gameObject = p_il2cpp_class_get_method_from_name(klass, "get_gameObject", 0); + IPALog([NSString stringWithFormat: + @"[HOME] cached get_gameObject method=%p (klass=%p)", + g_method_get_gameObject, klass]); + } + return invoke0(g_method_get_gameObject, componentObj); +} + +void setActive(void *gameObject, bool value) { + if (!ptrLooksValid(gameObject)) return; + if (!g_method_SetActive) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; + void *klass = p_il2cpp_object_get_class(gameObject); + if (!klass) return; + g_method_SetActive = p_il2cpp_class_get_method_from_name(klass, "SetActive", 1); + IPALog([NSString stringWithFormat: + @"[HOME] cached SetActive method=%p (klass=%p)", + g_method_SetActive, klass]); + } + invokeSetActive(g_method_SetActive, gameObject, value); +} + +// GameObject.get_transform - returns the GameObject's transform. Separate +// from the Component.get_transform cache because they live on different +// klasses and the il2cpp method handles are not interchangeable. +void *goTransformOf(void *gameObject) { + if (!ptrLooksValid(gameObject)) return NULL; + if (!g_method_GO_get_transform) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; + void *klass = p_il2cpp_object_get_class(gameObject); + if (!klass) return NULL; + g_method_GO_get_transform = p_il2cpp_class_get_method_from_name(klass, "get_transform", 0); + IPALog([NSString stringWithFormat: + @"[HOME] cached GameObject.get_transform method=%p (klass=%p)", + g_method_GO_get_transform, klass]); + } + return invoke0(g_method_GO_get_transform, gameObject); +} + +// Transform.get_parent - the Transform parent in the scene hierarchy. +void *transformParentOf(void *transformObj) { + if (!ptrLooksValid(transformObj)) return NULL; + if (!g_method_Tf_get_parent) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; + void *klass = p_il2cpp_object_get_class(transformObj); + if (!klass) return NULL; + g_method_Tf_get_parent = p_il2cpp_class_get_method_from_name(klass, "get_parent", 0); + IPALog([NSString stringWithFormat: + @"[HOME] cached Transform.get_parent method=%p (klass=%p)", + g_method_Tf_get_parent, klass]); + } + return invoke0(g_method_Tf_get_parent, transformObj); +} + +// Transform.SetParent(Transform parent, bool worldPositionStays). +// runtime_invoke hung the main thread on this method (same invoker_method +// problem the static Instantiate hit), so we go through methodPointer. +// IL2CPP instance-method ABI for this signature: +// void (Transform* this, Transform* parent, bool wps, MethodInfo* method) +typedef void (*Tf_SetParent_directABI_t)(void *thisTf, void *parent, bool wps, void *methodInfo); + +void transformSetParent(void *transformObj, void *newParent, bool worldPositionStays) { + if (!ptrLooksValid(transformObj)) return; + if (!g_method_Tf_SetParent) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; + void *klass = p_il2cpp_object_get_class(transformObj); + if (!klass) return; + g_method_Tf_SetParent = p_il2cpp_class_get_method_from_name(klass, "SetParent", 2); + IPALog([NSString stringWithFormat: + @"[HOME] cached Transform.SetParent(Tf,bool) method=%p (klass=%p)", + g_method_Tf_SetParent, klass]); + } + if (!g_method_Tf_SetParent) return; + void *methodPtr = *(void **)g_method_Tf_SetParent; + if (!methodPtr) { + IPALog(@"[HOME] Tf.SetParent direct: methodPointer NULL"); + return; + } + IPALog([NSString stringWithFormat: + @"[HOME] Tf.SetParent direct: methodPtr=%p this=%p parent=%p wps=%d", + methodPtr, transformObj, newParent, (int)worldPositionStays]); + ((Tf_SetParent_directABI_t)methodPtr)(transformObj, newParent, worldPositionStays, g_method_Tf_SetParent); +} + +// Transform.GetSiblingIndex -> Int32. Direct call instead of runtime_invoke +// for the same reason as above; this also dodges the boxed value-type +// return path entirely (the direct ABI just returns int32 by value). +typedef int32_t (*Tf_GetSiblingIndex_directABI_t)(void *thisTf, void *methodInfo); + +int32_t transformGetSiblingIndex(void *transformObj) { + if (!ptrLooksValid(transformObj)) return -1; + if (!g_method_Tf_GetSiblingIndex) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return -1; + void *klass = p_il2cpp_object_get_class(transformObj); + if (!klass) return -1; + g_method_Tf_GetSiblingIndex = p_il2cpp_class_get_method_from_name(klass, "GetSiblingIndex", 0); + IPALog([NSString stringWithFormat: + @"[HOME] cached Transform.GetSiblingIndex method=%p (klass=%p)", + g_method_Tf_GetSiblingIndex, klass]); + } + if (!g_method_Tf_GetSiblingIndex) return -1; + void *methodPtr = *(void **)g_method_Tf_GetSiblingIndex; + if (!methodPtr) { + IPALog(@"[HOME] Tf.GetSiblingIndex direct: methodPointer NULL"); + return -1; + } + return ((Tf_GetSiblingIndex_directABI_t)methodPtr)(transformObj, g_method_Tf_GetSiblingIndex); +} + +typedef int32_t (*Tf_get_childCount_directABI_t)(void *thisTf, void *methodInfo); +typedef void *(*Tf_GetChild_directABI_t)(void *thisTf, int32_t idx, void *methodInfo); +typedef void *(*Obj_get_name_directABI_t)(void *thisObj, void *methodInfo); + +int32_t transformChildCount(void *transformObj) { + if (!ptrLooksValid(transformObj)) return 0; + if (!g_method_Tf_get_childCount) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return 0; + void *klass = p_il2cpp_object_get_class(transformObj); + if (!klass) return 0; + g_method_Tf_get_childCount = p_il2cpp_class_get_method_from_name(klass, "get_childCount", 0); + } + if (!g_method_Tf_get_childCount) return 0; + void *methodPtr = *(void **)g_method_Tf_get_childCount; + if (!methodPtr) return 0; + return ((Tf_get_childCount_directABI_t)methodPtr)(transformObj, g_method_Tf_get_childCount); +} + +void *transformGetChild(void *transformObj, int32_t idx) { + if (!ptrLooksValid(transformObj)) return NULL; + if (!g_method_Tf_GetChild) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; + void *klass = p_il2cpp_object_get_class(transformObj); + if (!klass) return NULL; + g_method_Tf_GetChild = p_il2cpp_class_get_method_from_name(klass, "GetChild", 1); + } + if (!g_method_Tf_GetChild) return NULL; + void *methodPtr = *(void **)g_method_Tf_GetChild; + if (!methodPtr) return NULL; + return ((Tf_GetChild_directABI_t)methodPtr)(transformObj, idx, g_method_Tf_GetChild); +} + +// UnityEngine.Object.get_name -> System.String. Walks up the klass chain +// once on first hit since Transform's klass redeclares get_name only if +// overridden - but get_method_from_name searches parents too in IL2CPP. +NSString *objectName(void *unityObj) { + if (!ptrLooksValid(unityObj)) return nil; + if (!g_method_Obj_get_name) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return nil; + void *klass = p_il2cpp_object_get_class(unityObj); + if (!klass) return nil; + g_method_Obj_get_name = p_il2cpp_class_get_method_from_name(klass, "get_name", 0); + IPALog([NSString stringWithFormat: + @"[HOME] cached Object.get_name method=%p (klass=%p)", + g_method_Obj_get_name, klass]); + } + if (!g_method_Obj_get_name) return nil; + void *methodPtr = *(void **)g_method_Obj_get_name; + if (!methodPtr) return nil; + void *strObj = ((Obj_get_name_directABI_t)methodPtr)(unityObj, g_method_Obj_get_name); + return il2cppStringToNSString(strObj); +} + +// Walk the Transform tree under `tfObj`, log each node's name with +// indentation. The clone is brand new so we cap depth to keep the log +// readable. Used purely as a recon pass for Phase 2c (label rewrite). +void dumpHierarchy(void *tfObj, int depth, int maxDepth) { + if (!ptrLooksValid(tfObj)) return; + if (depth > maxDepth) return; + NSString *name = objectName(tfObj); + NSMutableString *indent = [NSMutableString string]; + for (int i = 0; i < depth; i++) [indent appendString:@" "]; + IPALog([NSString stringWithFormat: + @"[HOME] hier %@tf=%p name=%@", + indent, tfObj, name ?: @""]); + int32_t cc = transformChildCount(tfObj); + for (int32_t i = 0; i < cc; i++) { + void *child = transformGetChild(tfObj, i); + dumpHierarchy(child, depth + 1, maxDepth); + } +} + +// Find the immediate child Transform whose Object.get_name matches. +void *transformChildByName(void *parentTf, const char *targetName) { + if (!ptrLooksValid(parentTf) || !targetName) return NULL; + int32_t cc = transformChildCount(parentTf); + NSString *needle = [NSString stringWithUTF8String:targetName]; + for (int32_t i = 0; i < cc; i++) { + void *child = transformGetChild(parentTf, i); + if (!ptrLooksValid(child)) continue; + NSString *name = objectName(child); + if ([name isEqualToString:needle]) return child; + } + return NULL; +} + +// GameObject.GetComponent(string type) at UnityFramework + 0x6BCA6AC. The +// codegen wrapper here is a thin FreeFunction marshal to native +// Scripting::GetScriptingWrapperOfComponentOfGameObject - probably ignores +// MethodInfo* so passing NULL is OK. If it crashes we'll revisit with proper +// klass-walked MethodInfo* resolution. +#define RVA_GO_GETCOMPONENT_STRING 0x6BCA6AC + +typedef void *(*GO_GetComponent_string_directABI_t)(void *thisGo, void *typeStr, void *methodInfo); + +void *componentByTypeName(void *gameObject, const char *typeName) { + if (!ptrLooksValid(gameObject) || !typeName) return NULL; + if (!p_il2cpp_string_new || g_unityBaseAddr == 0) return NULL; + void *typeStr = p_il2cpp_string_new(typeName); + if (!typeStr) return NULL; + GO_GetComponent_string_directABI_t fn = + (GO_GetComponent_string_directABI_t)(g_unityBaseAddr + RVA_GO_GETCOMPONENT_STRING); + return fn(gameObject, typeStr, NULL); +} + +// Walks a UIButton-shaped hierarchy for the leaf that owns the icon sprite. +// HomeUtilityButton* puts the icon at Content/Image while TitleScene's +// _titleMenuButton uses Content/IconImage. Try both. +void *findIconImageTransform(void *btnTf) { + if (!ptrLooksValid(btnTf)) return NULL; + void *contentTf = transformChildByName(btnTf, "Content"); + if (!ptrLooksValid(contentTf)) return NULL; + void *imageTf = transformChildByName(contentTf, "Image"); + if (!ptrLooksValid(imageTf)) { + imageTf = transformChildByName(contentTf, "IconImage"); + } + return imageTf; +} + +// Sprite captured from the TitleScene._titleMenuButton on the first title +// MoveNext fire. NULL until then (and during fresh launches that drop the +// user directly into a non-title screen). +void *g_titleMenuSprite = NULL; + + +// UnityEngine.UI.Image.set_sprite resolved off the live Image component's +// klass once we have one; reused per clone Image swap. set_sprite has only +// one overload so class_get_method_from_name is unambiguous here. +typedef void (*Image_set_sprite_directABI_t)(void *thisImg, void *sprite, void *methodInfo); +static void *g_method_Image_set_sprite = NULL; + +bool swapImageSpriteOnGo(void *imageHostGo, void *newSprite, const char *tag) { + if (!ptrLooksValid(imageHostGo) || !ptrLooksValid(newSprite)) return false; + void *imageComp = componentByTypeName(imageHostGo, "UnityEngine.UI.Image"); + if (!ptrLooksValid(imageComp)) { + IPALog([NSString stringWithFormat: + @"[SPRITE-SWAP %s] no Image component on go=%p", tag, imageHostGo]); + return false; + } + if (!g_method_Image_set_sprite) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return false; + void *klass = p_il2cpp_object_get_class(imageComp); + if (!klass) return false; + g_method_Image_set_sprite = + p_il2cpp_class_get_method_from_name(klass, "set_sprite", 1); + IPALog([NSString stringWithFormat: + @"[SPRITE-SWAP %s] cached Image.set_sprite method=%p (klass=%p)", + tag, g_method_Image_set_sprite, klass]); + } + if (!g_method_Image_set_sprite) return false; + void *methodPtr = *(void **)g_method_Image_set_sprite; + if (!methodPtr) { + IPALog([NSString stringWithFormat: + @"[SPRITE-SWAP %s] set_sprite methodPointer is NULL", tag]); + return false; + } + IPALog([NSString stringWithFormat: + @"[SPRITE-SWAP %s] applying sprite=%p to imageComp=%p (was m_Sprite=%p)", + tag, newSprite, imageComp, readPtr(imageComp, 0xD8)]); + ((Image_set_sprite_directABI_t)methodPtr)(imageComp, newSprite, g_method_Image_set_sprite); + return true; +} + +// Read the m_Sprite name on the clone's Image so we can tell whether the +// "メニュー" label is baked into the sprite (sprite name suggests a +// combined icon+text texture) or actually rendered separately somewhere. +void reconSpriteName(void *cloneTf) { + if (!ptrLooksValid(cloneTf)) return; + void *contentTf = transformChildByName(cloneTf, "Content"); + if (!ptrLooksValid(contentTf)) return; + void *imageTf = transformChildByName(contentTf, "Image"); + if (!ptrLooksValid(imageTf)) imageTf = transformChildByName(contentTf, "IconImage"); + if (!ptrLooksValid(imageTf)) return; + void *imageGo = gameObjectOf(imageTf); + if (!ptrLooksValid(imageGo)) return; + void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); + if (!ptrLooksValid(imageComp)) return; + void *sprite = readPtr(imageComp, 0xD8); + if (!ptrLooksValid(sprite)) return; + NSString *name = objectName(sprite); + IPALog([NSString stringWithFormat: + @"[SPRITE-NAME] clone Image.m_Sprite=%p name=\"%@\"", + sprite, name ?: @""]); +} + +// --------------------------------------------------------------------------- +// FriendUnhideBridgeInit — one-shot bootstrap called from +// KIOUEditorInstallFriendUnhideHook before the two RVAs are installed. +// Resolves the il2cpp bridge and stashes UnityFramework's base address so +// the direct-ABI helpers in FriendUnhideBridgeUI.m have their inputs ready. +// Safe to call multiple times; each call is idempotent. +// --------------------------------------------------------------------------- +void FriendUnhideBridgeInit(uintptr_t unityBase) { + resolveIl2cppBridge(); + g_unityBaseAddr = unityBase; + if (!p_il2cpp_string_new) { + p_il2cpp_string_new = (il2cpp_string_new_t)dlsym(RTLD_DEFAULT, "il2cpp_string_new"); + } +} diff --git a/Hook/FriendUnhideBridgeUI.m b/Hook/FriendUnhideBridgeUI.m new file mode 100644 index 0000000..f78a6f3 --- /dev/null +++ b/Hook/FriendUnhideBridgeUI.m @@ -0,0 +1,457 @@ +#import "Hook/Common.h" +#import "Hook/FriendUnhideBridge.h" +#import "Hook/FriendUnhideBridge_Private.h" +#import "logging.h" + +// =========================================================================== +// Hook/FriendUnhideBridgeUI.m — sprite ops, hide-image, hierarchy recon, +// Instantiate variants, and the two public strong-symbol implementations +// KIOUEditorReconButtonImage / KIOUEditorApplyTitleSpriteToClone. +// +// Second half of the bridge whose first half sits in +// Hook/FriendUnhideBridge.m. The split exists purely to keep both files +// under the 600-line hard-split threshold; the shared internals cross +// through Hook/FriendUnhideBridge_Private.h. +// =========================================================================== + +typedef struct { float x, y, z; } UVec3; +typedef struct { float x, y; } UVec2; + +typedef UVec3 (*Tf_get_position_HFA_t)(void *self, void *methodInfo); +typedef UVec2 (*Rt_get_sizeDelta_HFA_t)(void *self, void *methodInfo); + +static void *g_method_Tf_get_position = NULL; +static void *g_method_Rt_get_sizeDelta = NULL; + +// RectTransformUtility.WorldToScreenPoint(Camera cam, Vector3 worldPoint) +// at UnityFramework + 0x6F20040. Static, takes a null camera for +// ScreenSpaceOverlay canvases and returns the screen pixel position with +// bottom-left origin. Direct call with NULL methodInfo - same pattern as +// GameObject.GetComponent(string) which we proved out earlier. +#define RVA_RTU_WORLD_TO_SCREEN 0x6F20040 +typedef UVec2 (*RtU_WorldToScreenPoint_t)(void *cam, UVec3 worldPoint, void *methodInfo); + +static UVec2 unityWorldToScreen(UVec3 worldPoint) { + UVec2 zero = {0}; + if (g_unityBaseAddr == 0) return zero; + RtU_WorldToScreenPoint_t fn = + (RtU_WorldToScreenPoint_t)(g_unityBaseAddr + RVA_RTU_WORLD_TO_SCREEN); + return fn(NULL, worldPoint, NULL); +} + +// Resolve via class_get_method_from_name so we pass the real MethodInfo* +// trailing arg the codegen wrapper expects. Direct RVA + NULL methodInfo +// crashed inside the IL2CPP P/Invoke marshalling for the value-type +// returns, so we let il2cpp hand us the proper handle. +static bool readCloneScreenRect(void *cloneTf, + UVec3 *outPos, UVec2 *outSize) { + if (!ptrLooksValid(cloneTf)) return false; + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return false; + + if (!g_method_Tf_get_position || !g_method_Rt_get_sizeDelta) { + void *klass = p_il2cpp_object_get_class(cloneTf); + if (!klass) return false; + if (!g_method_Tf_get_position) { + g_method_Tf_get_position = + p_il2cpp_class_get_method_from_name(klass, "get_position", 0); + } + if (!g_method_Rt_get_sizeDelta) { + g_method_Rt_get_sizeDelta = + p_il2cpp_class_get_method_from_name(klass, "get_sizeDelta", 0); + } + IPALog([NSString stringWithFormat: + @"[CLONE-RECT] cached get_position=%p get_sizeDelta=%p (klass=%p)", + g_method_Tf_get_position, g_method_Rt_get_sizeDelta, klass]); + } + if (!g_method_Tf_get_position || !g_method_Rt_get_sizeDelta) return false; + + void *posPtr = *(void **)g_method_Tf_get_position; + void *sizePtr = *(void **)g_method_Rt_get_sizeDelta; + if (!posPtr || !sizePtr) return false; + + *outPos = ((Tf_get_position_HFA_t)posPtr)(cloneTf, g_method_Tf_get_position); + *outSize = ((Rt_get_sizeDelta_HFA_t)sizePtr)(cloneTf, g_method_Rt_get_sizeDelta); + IPALog([NSString stringWithFormat: + @"[CLONE-RECT] pos=(%g,%g,%g) sizeDelta=(%g,%g)", + outPos->x, outPos->y, outPos->z, outSize->x, outSize->y]); + return true; +} + +// Hide the clone's Image by zeroing its m_Color alpha and calling +// SetAllDirty so the canvas rebuild picks up the new color. Keeps the +// raycast target so the OnPointerClick hook still sees taps; the actual +// visual is rendered by a UIKit overlay above the Unity layer. +typedef void (*Graphic_SetAllDirty_t)(void *self, void *methodInfo); +static void *g_method_Graphic_SetAllDirty = NULL; + +void hideCloneImage(void *cloneTf) { + if (!ptrLooksValid(cloneTf)) return; + void *contentTf = transformChildByName(cloneTf, "Content"); + if (!ptrLooksValid(contentTf)) return; + void *imageTf = transformChildByName(contentTf, "Image"); + if (!ptrLooksValid(imageTf)) imageTf = transformChildByName(contentTf, "IconImage"); + if (!ptrLooksValid(imageTf)) return; + void *imageGo = gameObjectOf(imageTf); + if (!ptrLooksValid(imageGo)) return; + void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); + if (!ptrLooksValid(imageComp)) return; + + // Graphic.m_Color @ 0x28 (Color = 4 floats RGBA). + float *color = (float *)((uint8_t *)imageComp + 0x28); + color[0] = 1.0f; + color[1] = 1.0f; + color[2] = 1.0f; + color[3] = 0.0f; + IPALog([NSString stringWithFormat: + @"[CLONE-HIDE] imageComp=%p m_Color set to (1,1,1,0)", imageComp]); + + if (!g_method_Graphic_SetAllDirty) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; + void *klass = p_il2cpp_object_get_class(imageComp); + if (!klass) return; + g_method_Graphic_SetAllDirty = p_il2cpp_class_get_method_from_name(klass, "SetAllDirty", 0); + IPALog([NSString stringWithFormat: + @"[CLONE-HIDE] cached Graphic.SetAllDirty method=%p (klass=%p)", + g_method_Graphic_SetAllDirty, klass]); + } + if (!g_method_Graphic_SetAllDirty) return; + void *methodPtr = *(void **)g_method_Graphic_SetAllDirty; + if (!methodPtr) return; + ((Graphic_SetAllDirty_t)methodPtr)(imageComp, g_method_Graphic_SetAllDirty); + IPALog(@"[CLONE-HIDE] SetAllDirty invoked"); +} + +// Probe each GameObject in the clone tree for a text component and log +// it. Helps us figure out where the inherited "メニュー" label lives so we +// can blank it on the clone. Recon-only, no mutations. +void reconTextComponents(void *cloneTf) { + if (!ptrLooksValid(cloneTf)) return; + void *cloneGo = gameObjectOf(cloneTf); + void *contentTf = transformChildByName(cloneTf, "Content"); + void *contentGo = ptrLooksValid(contentTf) ? gameObjectOf(contentTf) : NULL; + void *imageTf = ptrLooksValid(contentTf) ? transformChildByName(contentTf, "Image") : NULL; + if (!ptrLooksValid(imageTf) && ptrLooksValid(contentTf)) { + imageTf = transformChildByName(contentTf, "IconImage"); + } + void *imageGo = ptrLooksValid(imageTf) ? gameObjectOf(imageTf) : NULL; + + void *grayTf = ptrLooksValid(imageTf) ? transformChildByName(imageTf, "GrayoutCover_Toggle") : NULL; + void *grayGo = ptrLooksValid(grayTf) ? gameObjectOf(grayTf) : NULL; + + const char *names[] = { + "TMPro.TextMeshProUGUI", + "UnityEngine.UI.Text", + "TMPro.TextMeshPro", + }; + struct { const char *tag; void *go; } pts[] = { + { "button-go", cloneGo }, + { "content-go", contentGo }, + { "image-go", imageGo }, + { "gray-go", grayGo }, + }; + for (int p = 0; p < 4; p++) { + if (!ptrLooksValid(pts[p].go)) continue; + for (int n = 0; n < 3; n++) { + void *c = componentByTypeName(pts[p].go, names[n]); + IPALog([NSString stringWithFormat: + @"[TEXT-RECON] %s GetComponent(\"%s\")=%p", + pts[p].tag, names[n], c]); + } + } +} + +// Read the m_Sprite (offset 0xD8) of the Image component on uiButton's +// Content/Image leaf. Used by callers that want to harvest a sprite handle +// from a sibling button without going through the full recon logger. +static void *spriteOfButton(void *uiButton) { + if (!ptrLooksValid(uiButton)) return NULL; + void *btnGo = gameObjectOf(uiButton); + if (!ptrLooksValid(btnGo)) return NULL; + void *btnTf = goTransformOf(btnGo); + void *imageTf = findIconImageTransform(btnTf); + if (!ptrLooksValid(imageTf)) return NULL; + void *imageGo = gameObjectOf(imageTf); + if (!ptrLooksValid(imageGo)) return NULL; + void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); + if (!ptrLooksValid(imageComp)) return NULL; + return readPtr(imageComp, 0xD8); +} + +// Apply a sibling sprite to a freshly cloned home utility button. The +// caller passes the gift / friend / menu button as a sprite source; this +// avoids the title atlas-unload trap until a permanent sprite source (a +// bundled PNG / SF Symbol generated Texture2D) is wired up. +static bool applySiblingSpriteToClone(void *cloneGo, void *sourceBtn, const char *sourceTag) { + if (!ptrLooksValid(cloneGo) || !ptrLooksValid(sourceBtn)) return false; + void *sprite = spriteOfButton(sourceBtn); + if (!ptrLooksValid(sprite)) { + IPALog([NSString stringWithFormat: + @"[SPRITE-SWAP clone] no sprite on %s source", sourceTag]); + return false; + } + void *cloneTf = goTransformOf(cloneGo); + void *imageTf = findIconImageTransform(cloneTf); + if (!ptrLooksValid(imageTf)) return false; + void *imageGo = gameObjectOf(imageTf); + IPALog([NSString stringWithFormat: + @"[SPRITE-SWAP clone] source=%s sprite=%p", sourceTag, sprite]); + return swapImageSpriteOnGo(imageGo, sprite, "clone"); +} + +// Phase 0 verification: apply the gift sprite to the clone. Gift sits on +// the same home strip as the clone, so the atlas is guaranteed loaded for +// the duration the clone is alive. If the clone renders gift icon visibly, +// set_sprite + canvas invalidation work; the white title-swap result was +// purely the title atlas getting unloaded post-scene-transition. +// +// Currently this also still tries the title sprite if no gift swap target +// was passed in - title path is left in place for direct comparison. +void KIOUEditorApplyTitleSpriteToClone(void *cloneGo) { + (void)cloneGo; + // Kept as a no-op placeholder so the call site in the clone path stays + // unchanged while we route through the new sibling sprite helper. + // The actual swap is now driven from hook_HUP_ctor via giftBtn. +} + +// Public recon entry. Walks uiButton -> btnGo -> btnTf -> "Content" -> +// "Image" -> GO -> GetComponent("UnityEngine.UI.Image") -> m_Sprite@+0xD8. +// Logs every step so we can see where it bails when something is missing. +void KIOUEditorReconButtonImage(void *uiButton, const char *tag) { + if (!ptrLooksValid(uiButton)) { + IPALog([NSString stringWithFormat: + @"[SPRITE-RECON %s] button ptr invalid (%p)", tag, uiButton]); + return; + } + void *btnGo = gameObjectOf(uiButton); + void *btnTf = goTransformOf(btnGo); + IPALog([NSString stringWithFormat: + @"[SPRITE-RECON %s] btn=%p go=%p tf=%p", + tag, uiButton, btnGo, btnTf]); + if (!ptrLooksValid(btnTf)) return; + + void *imageTf = findIconImageTransform(btnTf); + if (!ptrLooksValid(imageTf)) { + IPALog([NSString stringWithFormat: + @"[SPRITE-RECON %s] no Image/IconImage leaf - dumping btnTf:", + tag]); + dumpHierarchy(btnTf, 0, 3); + return; + } + void *imageGo = gameObjectOf(imageTf); + IPALog([NSString stringWithFormat: + @"[SPRITE-RECON %s] imageTf=%p imageGo=%p", + tag, imageTf, imageGo]); + if (!ptrLooksValid(imageGo)) return; + + void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); + IPALog([NSString stringWithFormat: + @"[SPRITE-RECON %s] GetComponent(\"UnityEngine.UI.Image\")=%p", + tag, imageComp]); + if (!ptrLooksValid(imageComp)) return; + + void *sprite = readPtr(imageComp, 0xD8); + IPALog([NSString stringWithFormat: + @"[SPRITE-RECON %s] m_Sprite=%p", tag, sprite]); + + // Title side: cache the sprite so the home clone hook can swap it in. + if (tag && strcmp(tag, "title-menu") == 0 && ptrLooksValid(sprite)) { + g_titleMenuSprite = sprite; + IPALog([NSString stringWithFormat: + @"[SPRITE-RECON %s] cached title menu sprite for clone swap", + tag]); + } +} + +typedef void (*Tf_SetSiblingIndex_directABI_t)(void *thisTf, int32_t idx, void *methodInfo); + +void transformSetSiblingIndex(void *transformObj, int32_t idx) { + if (!ptrLooksValid(transformObj)) return; + if (!g_method_Tf_SetSiblingIndex) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; + void *klass = p_il2cpp_object_get_class(transformObj); + if (!klass) return; + g_method_Tf_SetSiblingIndex = p_il2cpp_class_get_method_from_name(klass, "SetSiblingIndex", 1); + IPALog([NSString stringWithFormat: + @"[HOME] cached Transform.SetSiblingIndex method=%p (klass=%p)", + g_method_Tf_SetSiblingIndex, klass]); + } + if (!g_method_Tf_SetSiblingIndex) return; + void *methodPtr = *(void **)g_method_Tf_SetSiblingIndex; + if (!methodPtr) { + IPALog(@"[HOME] Tf.SetSiblingIndex direct: methodPointer NULL"); + return; + } + ((Tf_SetSiblingIndex_directABI_t)methodPtr)(transformObj, idx, g_method_Tf_SetSiblingIndex); +} + +// Component.get_transform - returns this.transform. +void *transformOf(void *componentObj) { + if (!ptrLooksValid(componentObj)) return NULL; + if (!g_method_get_transform) { + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return NULL; + void *klass = p_il2cpp_object_get_class(componentObj); + if (!klass) return NULL; + g_method_get_transform = p_il2cpp_class_get_method_from_name(klass, "get_transform", 0); + IPALog([NSString stringWithFormat: + @"[HOME] cached get_transform method=%p (klass=%p)", + g_method_get_transform, klass]); + } + return invoke0(g_method_get_transform, componentObj); +} + +// Recon: walk every method on UnityEngine.Object (resolved via parent klass +// of any GameObject we already hold) and log each "Instantiate" variant +// with (name, argc, is_generic, method_ptr). From the log we can pick the +// non-generic method handle directly and invoke it later without racing +// the generic ones via get_method_from_name. Pure logging - no invoke. +static void logInstantiateMethods(void *anyGo) { + if (!p_il2cpp_object_get_class + || !p_il2cpp_class_get_parent + || !p_il2cpp_class_get_methods + || !p_il2cpp_method_get_name + || !p_il2cpp_method_get_param_count) { + IPALog(@"[HOME] enum recon: bridge incomplete, skipping"); + return; + } + void *goKlass = p_il2cpp_object_get_class(anyGo); + if (!goKlass) return; + void *objKlass = p_il2cpp_class_get_parent(goKlass); + if (!objKlass) return; + IPALog([NSString stringWithFormat: + @"[HOME] enum: walking Object klass=%p", objKlass]); + void *iter = NULL; + void *method = NULL; + int hits = 0; + while ((method = p_il2cpp_class_get_methods(objKlass, &iter)) != NULL) { + const char *name = p_il2cpp_method_get_name(method); + if (!name) continue; + if (strstr(name, "nstantiate") == NULL) continue; + uint32_t argc = p_il2cpp_method_get_param_count(method); + int isGeneric = -1; + if (p_il2cpp_method_is_generic) { + isGeneric = (int)p_il2cpp_method_is_generic(method); + } + IPALog([NSString stringWithFormat: + @"[HOME] enum: %s argc=%u generic=%d method=%p", + name, argc, isGeneric, method]); + hits++; + } + IPALog([NSString stringWithFormat: + @"[HOME] enum: %d Instantiate variants found", hits]); +} + +// Walk a klass's methods and return the first one matching name + argc that +// is NOT generic. Hand-rolled because il2cpp_class_get_method_from_name has +// no generic filter and races the generic Instantiate descriptors first. +static void *findNonGenericMethod(void *klass, const char *targetName, uint32_t targetArgc) { + if (!klass) return NULL; + if (!p_il2cpp_class_get_methods + || !p_il2cpp_method_get_name + || !p_il2cpp_method_get_param_count + || !p_il2cpp_method_is_generic) return NULL; + void *iter = NULL; + void *method = NULL; + while ((method = p_il2cpp_class_get_methods(klass, &iter)) != NULL) { + const char *name = p_il2cpp_method_get_name(method); + if (!name) continue; + if (strcmp(name, targetName) != 0) continue; + if (p_il2cpp_method_get_param_count(method) != targetArgc) continue; + if (p_il2cpp_method_is_generic(method)) continue; + return method; + } + return NULL; +} + +// Object.Instantiate(Object original) - explicit non-generic match. +// Clone goes to root scene with null parent. Use SetParent in a later phase +// to slot it into the home layout. +static void *instantiateCloneNonGeneric(void *originalGo) { + if (!ptrLooksValid(originalGo)) return NULL; + if (!p_il2cpp_runtime_invoke + || !p_il2cpp_object_get_class + || !p_il2cpp_class_get_parent) return NULL; + if (!g_method_Instantiate1NonGen) { + void *goKlass = p_il2cpp_object_get_class(originalGo); + if (!goKlass) return NULL; + void *objKlass = p_il2cpp_class_get_parent(goKlass); + if (!objKlass) return NULL; + g_method_Instantiate1NonGen = findNonGenericMethod(objKlass, "Instantiate", 1); + IPALog([NSString stringWithFormat: + @"[HOME] cached non-generic Instantiate(Object) method=%p (objKlass=%p)", + g_method_Instantiate1NonGen, objKlass]); + } + if (!g_method_Instantiate1NonGen) return NULL; + void *originalRef = originalGo; + void *params[1] = { &originalRef }; + return p_il2cpp_runtime_invoke(g_method_Instantiate1NonGen, NULL, params, NULL); +} + +// Direct call into MethodInfo->methodPointer (offset 0 on Unity 6 IL2CPP), +// bypassing runtime_invoke entirely. IL2CPP appends a MethodInfo* slot to +// every method's native signature; the C ABI for the static one-arg +// Object.Instantiate(Object) is: +// Object* (Object* original, const MethodInfo* method) +// Tried because the runtime_invoke path crashes inside the invoker even +// after the recon confirmed we hold the non-generic method handle. The +// methodPointer is the actually-generated native function, no invoker +// trampoline involved. +typedef void *(*Instantiate1_directABI_t)(void *original, void *methodInfo); + +void *instantiateCloneDirect(void *originalGo) { + if (!ptrLooksValid(originalGo)) return NULL; + if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_parent) return NULL; + if (!g_method_Instantiate1NonGen) { + void *goKlass = p_il2cpp_object_get_class(originalGo); + if (!goKlass) return NULL; + void *objKlass = p_il2cpp_class_get_parent(goKlass); + if (!objKlass) return NULL; + g_method_Instantiate1NonGen = findNonGenericMethod(objKlass, "Instantiate", 1); + IPALog([NSString stringWithFormat: + @"[HOME] cached non-generic Instantiate(Object) method=%p (objKlass=%p)", + g_method_Instantiate1NonGen, objKlass]); + } + if (!g_method_Instantiate1NonGen) return NULL; + void *methodPtr = *(void **)g_method_Instantiate1NonGen; + if (!methodPtr) { + IPALog(@"[HOME] direct: methodPointer at offset 0 is NULL"); + return NULL; + } + IPALog([NSString stringWithFormat: + @"[HOME] direct call: methodPtr=%p methodInfo=%p original=%p", + methodPtr, g_method_Instantiate1NonGen, originalGo]); + return ((Instantiate1_directABI_t)methodPtr)(originalGo, g_method_Instantiate1NonGen); +} + +// UnityEngine.Object.Instantiate(Object original, Transform parent) - static. +// 2-arg overload picked over the argc=1 version because the argc=1 path +// matched the generic Instantiate(T) descriptor and runtime_invoke +// crashed inside the un-inflated generic call. The 2-arg non-generic +// overload coexists with a generic counterpart too, so we still race the +// lookup; if this also crashes we will need to enumerate methods and +// filter by il2cpp_method_is_generic. +static void *instantiateCloneWithParent(void *originalGo, void *parentTransform) { + if (!ptrLooksValid(originalGo)) return NULL; + if (!p_il2cpp_runtime_invoke + || !p_il2cpp_object_get_class + || !p_il2cpp_class_get_parent + || !p_il2cpp_class_get_method_from_name) return NULL; + if (!g_method_Instantiate2) { + void *goKlass = p_il2cpp_object_get_class(originalGo); + if (!goKlass) return NULL; + void *objKlass = p_il2cpp_class_get_parent(goKlass); + if (!objKlass) { + IPALog(@"[HOME] Instantiate lookup: parent klass NULL"); + return NULL; + } + g_method_Instantiate2 = p_il2cpp_class_get_method_from_name(objKlass, "Instantiate", 2); + IPALog([NSString stringWithFormat: + @"[HOME] cached Instantiate(Obj,Tf) method=%p (goKlass=%p objKlass=%p)", + g_method_Instantiate2, goKlass, objKlass]); + } + if (!g_method_Instantiate2) return NULL; + void *originalRef = originalGo; + void *parentRef = parentTransform; + void *params[2] = { &originalRef, &parentRef }; + return p_il2cpp_runtime_invoke(g_method_Instantiate2, NULL, params, NULL); +} + diff --git a/Hook/FriendUnhideBridge_Private.h b/Hook/FriendUnhideBridge_Private.h new file mode 100644 index 0000000..2dee99b --- /dev/null +++ b/Hook/FriendUnhideBridge_Private.h @@ -0,0 +1,61 @@ +#pragma once + +#import +#import +#import + +// =========================================================================== +// Hook/FriendUnhideBridge_Private.h — declarations shared between +// Hook/FriendUnhideBridge.m and Hook/FriendUnhideBridgeUI.m. +// +// The two .m files were split out of the 1121-line Hook/FriendUnhide.m +// to keep every translation unit under the 600-line hard threshold. Both +// halves are logically one bridge; this header exists only so the split +// compiles. +// +// PRIVATE: not a public API of KIOU-Hook. Consumer tweaks continue to +// import Hook/Common.h + Hook/FriendUnhideBridge.h. +// =========================================================================== + +// il2cpp runtime bridge state — resolved via dlsym inside +// FriendUnhideBridgeInit() (Bridge.m). +typedef void *(*il2cpp_runtime_invoke_t)(void *method, void *obj, void **params, void **exc); +typedef void *(*il2cpp_class_from_name_t)(void *image, const char *ns, const char *name); +typedef void *(*il2cpp_string_new_t)(const char *s); +typedef void *(*il2cpp_class_get_method_from_name_t)(void *klass, const char *name, int argc); +typedef void *(*il2cpp_object_get_class_t)(void *obj); +typedef void *(*il2cpp_class_get_parent_t)(void *klass); +typedef void *(*il2cpp_class_get_methods_t)(void *klass, void **iter); +typedef const char *(*il2cpp_method_get_name_t)(void *method); +typedef uint32_t (*il2cpp_method_get_param_count_t)(void *method); +typedef bool (*il2cpp_method_is_generic_t)(void *method); + +extern il2cpp_string_new_t p_il2cpp_string_new; +extern il2cpp_runtime_invoke_t p_il2cpp_runtime_invoke; +extern il2cpp_class_from_name_t p_il2cpp_class_from_name; +extern il2cpp_class_get_method_from_name_t p_il2cpp_class_get_method_from_name; +extern il2cpp_object_get_class_t p_il2cpp_object_get_class; +extern il2cpp_class_get_parent_t p_il2cpp_class_get_parent; +extern il2cpp_class_get_methods_t p_il2cpp_class_get_methods; +extern il2cpp_method_get_name_t p_il2cpp_method_get_name; +extern il2cpp_method_get_param_count_t p_il2cpp_method_get_param_count; +extern il2cpp_method_is_generic_t p_il2cpp_method_is_generic; + +// UnityFramework base captured at install; used by direct-ABI callers. +extern uintptr_t g_unityBaseAddr; + +// Internal helpers shared across the split. Not part of the public bridge +// header (Hook/FriendUnhideBridge.h) — consumers never call these. +void *invoke0(void *method, void *obj); +NSString *objectName(void *unityObj); +void *transformChildByName(void *parentTf, const char *targetName); +void *componentByTypeName(void *gameObject, const char *typeName); +int32_t transformChildCount(void *transformObj); +void *transformGetChild(void *transformObj, int32_t idx); +void *findIconImageTransform(void *btnTf); + +// Sprite ops used across the split. `g_titleMenuSprite` is captured by +// KIOUEditorReconButtonImage (BridgeUI.m) on the title side and later +// swapped onto the home clone via swapImageSpriteOnGo (Bridge.m). +extern void *g_titleMenuSprite; +bool swapImageSpriteOnGo(void *imageHostGo, void *newSprite, const char *tag); From b1d4690890397dbe7c239e7c7a73fa02b00c28ab Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 16:20:33 +0000 Subject: [PATCH 04/20] =?UTF-8?q?fix(hook):=20unbreak=20FriendUnhideBridge?= =?UTF-8?q?=20split=20=E2=80=94=20expose=20shared=20globals=20via=20extern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 1121-line FriendUnhide.m split (bbf2c61) left four il2cpp method-handle caches — g_method_get_transform, g_method_Instantiate2, g_method_Instantiate1NonGen, g_method_Tf_SetSiblingIndex — defined `static` in Bridge.m while their only callers moved to BridgeUI.m. The result was a hard compile break: Bridge.m failed with -Wunused-variable on the four statics, and BridgeUI.m failed with "use of undeclared identifier" for each. Drop `static` on the four shared caches, add matching `extern` declarations in Hook/FriendUnhideBridge_Private.h (which is already imported from both halves for exactly this purpose), and delete g_reconLogged — the one-time debug guard from the pre-split recon path that no code references anymore. Co-Authored-By: Claude Opus 4.7 --- Hook/FriendUnhideBridge.m | 13 ++++--------- Hook/FriendUnhideBridge_Private.h | 8 ++++++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Hook/FriendUnhideBridge.m b/Hook/FriendUnhideBridge.m index f79f79d..d372a34 100644 --- a/Hook/FriendUnhideBridge.m +++ b/Hook/FriendUnhideBridge.m @@ -76,15 +76,15 @@ static void resolveIl2cppBridge(void) { // --------------------------------------------------------------------------- static void *g_method_get_gameObject = NULL; // Component.get_gameObject -static void *g_method_get_transform = NULL; // Component.get_transform (cached off Component-derived obj) +void *g_method_get_transform = NULL; // Component.get_transform — shared with BridgeUI.m static void *g_method_GO_get_transform = NULL; // GameObject.get_transform static void *g_method_SetActive = NULL; // GameObject.SetActive -static void *g_method_Instantiate2 = NULL; // UnityEngine.Object.Instantiate(Object, Transform) -static void *g_method_Instantiate1NonGen = NULL; // UnityEngine.Object.Instantiate(Object) non-generic +void *g_method_Instantiate2 = NULL; // UnityEngine.Object.Instantiate(Object, Transform) — shared with BridgeUI.m +void *g_method_Instantiate1NonGen = NULL; // UnityEngine.Object.Instantiate(Object) non-generic — shared with BridgeUI.m static void *g_method_Tf_get_parent = NULL; // Transform.get_parent static void *g_method_Tf_SetParent = NULL; // Transform.SetParent(Transform,bool) static void *g_method_Tf_GetSiblingIndex = NULL; // Transform.GetSiblingIndex -static void *g_method_Tf_SetSiblingIndex = NULL; // Transform.SetSiblingIndex(int) +void *g_method_Tf_SetSiblingIndex = NULL; // Transform.SetSiblingIndex(int) — shared with BridgeUI.m static void *g_method_Tf_get_childCount = NULL; // Transform.get_childCount static void *g_method_Tf_GetChild = NULL; // Transform.GetChild(int) static void *g_method_Obj_get_name = NULL; // UnityEngine.Object.get_name @@ -106,11 +106,6 @@ static void resolveIl2cppBridge(void) { // re-entries. void *g_friendGo = NULL; -// One-time guard for the Instantiate-method enumeration recon (Phase 2a -// debug). After the first fire we know which method handle is the -// non-generic Object.Instantiate so we do not need to re-walk every time. -static bool g_reconLogged = false; - // Invoke instance method 0-arg returning a managed object pointer. void *invoke0(void *method, void *obj) { if (!p_il2cpp_runtime_invoke || !method) return NULL; diff --git a/Hook/FriendUnhideBridge_Private.h b/Hook/FriendUnhideBridge_Private.h index 2dee99b..af2f473 100644 --- a/Hook/FriendUnhideBridge_Private.h +++ b/Hook/FriendUnhideBridge_Private.h @@ -59,3 +59,11 @@ void *findIconImageTransform(void *btnTf); // swapped onto the home clone via swapImageSpriteOnGo (Bridge.m). extern void *g_titleMenuSprite; bool swapImageSpriteOnGo(void *imageHostGo, void *newSprite, const char *tag); + +// il2cpp method-handle caches shared between the split. Defined in +// FriendUnhideBridge.m; used from FriendUnhideBridgeUI.m as well because +// the sibling / instantiate / get_transform paths span both files. +extern void *g_method_get_transform; // Component.get_transform +extern void *g_method_Instantiate2; // UnityEngine.Object.Instantiate(Object, Transform) +extern void *g_method_Instantiate1NonGen; // UnityEngine.Object.Instantiate(Object) non-generic +extern void *g_method_Tf_SetSiblingIndex; // Transform.SetSiblingIndex(int) From c8685f9a9f39ed0420859344b34b67f7c2a06236 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 16:21:08 +0000 Subject: [PATCH 05/20] feat(assist-tune): promote BSE.EvaluateAsync into the shared catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BeginnerSupportEvaluator.EvaluateAsync suppression that gates "In-Game Analysis" lived in KiouEditor as an MSHookFunction-only tweak-local supplement. On chinlan the site had no cave, so the toggle was silently a no-op on binary-patched builds — the exact flavor this consumer ships. Migrate it into the shared catalog so chinlan actually dispatches. The chinlan wiring mirrors hook_BSE_ctor / hook_BSE_ensureInit that already live in this file: KIOUHookInstall + KIOU_HOOK_PUBLISH_SLOT under the existing installer, so consumers call the same KIOUEditorInstallAssistTuneHook and get all three BSE hooks with one install call. Catalog additions (all appended before __COUNT so downstream hook IDs and slot indices stay pinned): - KIOUHook.h: KIOU_HOOK_ID_BSE_EVALUATE_ASYNC, KIOU_HOOK_SLOT_BSE_EVALUATE_ASYNC, KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC (1.0.1 pinned, matching the other KiouEditor sites in this header), and KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC. - KIOUHook.m: catalog string constant + catalog row. - recipes/common.py:HOOK_IDS[BSE_EVALUATE_ASYNC] = 34, ENTRY_SLOT_INDEX[BSE_EVALUATE_ASYNC] = 29, ENTRY_SLOT_COUNT bumped 29 -> 30 (still inside ENTRY_SLOT_CAPACITY = 32). - recipes/v1_0_1.py:new SITE (0x597B570, "ff8302d1", CAVE_ENTRY, "BeginnerSupportEvaluator.EvaluateAsync"). - recipes/v1_0_2.py:new SITE (0x5980304, "ff8302d1", CAVE_ENTRY, "BeginnerSupportEvaluator.EvaluateAsync"). The two RVAs are the BSE.EvaluateAsync entries in the respective dump.cs.index.json; the shared prologue "ff8302d1" (SUB SP, SP, #0xA0) is not PC-relative, so verbatim relocation into the cave tail is safe. Also included: fix(recipes): move v1_0_2 CAVE_REGION past __oslogstring. The old region (0x826F5E8, 0x8274000) had only 30 cave slots before running into __oslogstring at 0x8270000..0x8270023 (the "%{public}@" fragment), which broke SITE #30 and #31 when the 1.0.2 catalog crossed 30 sites. Shift the region to (0x8270024, 0x8274000) — 194 slots of verified zero-fill — so the pre-existing 34 sites patch cleanly and the new BSE.EvaluateAsync (SITE #35, cave @ 0x8270B4C) still lands inside the region. KiouForge's KIOU_HOOK_ID_ALLOW filter never hit this collision because it ships fewer than 30 sites, but adding EvaluateAsync would push any consumer over the edge. Build-verified on Kiou-1.0.2 (chinlan): all 35 SITEs patch, the recipe assertions pass, and the resulting IPA repacks. Not runtime-verified on device — the JB / jailed flavor paths are mechanically unchanged (KIOUHookInstall on JB still calls MSHookFunction), so behavior parity is preserved by construction. Co-Authored-By: Claude Opus 4.7 --- Hook/AssistTune.m | 32 +++++++++++++++++++++++++++----- KIOUHook.h | 4 ++++ KIOUHook.m | 2 ++ recipes/common.py | 4 +++- recipes/v1_0_1.py | 1 + recipes/v1_0_2.py | 8 +++++++- 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/Hook/AssistTune.m b/Hook/AssistTune.m index b31fc7a..73be67c 100644 --- a/Hook/AssistTune.m +++ b/Hook/AssistTune.m @@ -33,11 +33,13 @@ typedef void (*BSECtor_t)(void *self, void *evalPath, void *settings); typedef void (*BSEEnsureInit_t)(void *self); +typedef void (*BSEEvaluateAsync_t)(void *self, void *position, void *methodInfo); typedef void (*NSS_SetHashSize_directABI_t)(void *thisSession, int32_t mb, void *methodInfo); -static BSECtor_t s_origBSE_ctor = NULL; -static BSEEnsureInit_t s_origBSE_ensureInit = NULL; -static uintptr_t g_unityBaseForAssist = 0; +static BSECtor_t s_origBSE_ctor = NULL; +static BSEEnsureInit_t s_origBSE_ensureInit = NULL; +static BSEEvaluateAsync_t s_origBSE_evaluateAsync = NULL; +static uintptr_t g_unityBaseForAssist = 0; static void hook_BSE_ctor(void *self, void *evalPath, void *settings) { if (s_origBSE_ctor) { @@ -97,6 +99,19 @@ static void hook_BSE_ensureInit(void *self) { } } +// BSE.EvaluateAsync — drop the on-device NNUE evaluation entirely when the +// user has KIOU_FEATURE_INGAME_ANALYSIS off. The BSE object stays allocated +// so surrounding lifecycle code is unaffected; only the expensive search +// path is suppressed, which quiets the CPU and prevents device heating. +static void hook_BSE_evaluate_async(void *self, void *position, void *methodInfo) { + if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_INGAME_ANALYSIS)) { + return; + } + if (s_origBSE_evaluateAsync) { + s_origBSE_evaluateAsync(self, position, methodInfo); + } +} + void KIOUEditorInstallAssistTuneHook(uintptr_t unityBase) { g_unityBaseForAssist = unityBase; s_origBSE_ctor = (BSECtor_t)KIOUHookInstall( @@ -107,10 +122,17 @@ void KIOUEditorInstallAssistTuneHook(uintptr_t unityBase) { KIOU_HOOK_NAME_BSE_ENSURE_INITIALIZED, (void *)hook_BSE_ensureInit, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_ENSURE_INITIALIZED, hook_BSE_ensureInit); + s_origBSE_evaluateAsync = (BSEEvaluateAsync_t)KIOUHookInstall( + KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC, + (void *)hook_BSE_evaluate_async, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_EVALUATE_ASYNC, hook_BSE_evaluate_async); IPALog([NSString stringWithFormat: @"[ASSIST-TUNE] installed: BSE.ctor orig=%p EnsureInit orig=%p " - @"(depth=%d skill=%d hash=%d MB)", + @"EvaluateAsync orig=%p (depth=%d skill=%d hash=%d MB, " + @"INGAME_ANALYSIS gate=%d)", (void *)s_origBSE_ctor, (void *)s_origBSE_ensureInit, + (void *)s_origBSE_evaluateAsync, (int)KIOUEditorAssistDepth(), (int)KIOUEditorAssistSkillLevel(), - (int)KIOUEditorAssistHashMB()]); + (int)KIOUEditorAssistHashMB(), + (int)KIOUEditorFeatureEnabled(KIOU_FEATURE_INGAME_ANALYSIS)]); } diff --git a/KIOUHook.h b/KIOUHook.h index 0492eec..79b14e8 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -119,6 +119,7 @@ enum kiou_hook_id { KIOU_HOOK_ID_UIBUTTONBASE_ONPOINTERCLICK, KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT, KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK, + KIOU_HOOK_ID_BSE_EVALUATE_ASYNC, KIOU_HOOK_ID__COUNT, }; @@ -157,6 +158,7 @@ enum kiou_hook_slot_id { KIOU_HOOK_SLOT_UIBUTTONBASE_ONPOINTERCLICK, KIOU_HOOK_SLOT_TITLE_SCENE_MOVENEXT, KIOU_HOOK_SLOT_GAME_ORCHESTRATOR_IS_AFK, + KIOU_HOOK_SLOT_BSE_EVALUATE_ASYNC, KIOU_HOOK_SLOT__COUNT, }; @@ -206,6 +208,7 @@ enum kiou_hook_slot_id { #define KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD1E08 #define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x5DCC728 #define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x59455D4 +#define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x597B570 // --- Direct-ABI helper RVAs (1.0.1) -------------------------------------- // Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to @@ -278,6 +281,7 @@ extern const char KIOU_HOOK_NAME_HOME_UTILITY_PRESENTER_CTOR[]; extern const char KIOU_HOOK_NAME_UIBUTTONBASE_ONPOINTERCLICK[]; extern const char KIOU_HOOK_NAME_TITLE_SCENE_MOVENEXT[]; extern const char KIOU_HOOK_NAME_GAME_ORCHESTRATOR_IS_AFK[]; +extern const char KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC[]; // Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. extern const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[]; extern const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[]; diff --git a/KIOUHook.m b/KIOUHook.m index b13749d..5be598a 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -50,6 +50,7 @@ const char KIOU_HOOK_NAME_UIBUTTONBASE_ONPOINTERCLICK[] = "uibuttonbase_on_pointer_click"; const char KIOU_HOOK_NAME_TITLE_SCENE_MOVENEXT[] = "title_scene_movenext"; const char KIOU_HOOK_NAME_GAME_ORCHESTRATOR_IS_AFK[] = "game_orchestrator_is_afk"; +const char KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC[] = "bse_evaluate_async"; // Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[] = "nss_set_hash_size_direct"; const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[] = "game_object_get_component"; @@ -100,6 +101,7 @@ { KIOU_HOOK_NAME_UIBUTTONBASE_ONPOINTERCLICK, KIOU_HOOK_ID_UIBUTTONBASE_ONPOINTERCLICK, KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK }, { KIOU_HOOK_NAME_TITLE_SCENE_MOVENEXT, KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT, KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT }, { KIOU_HOOK_NAME_GAME_ORCHESTRATOR_IS_AFK, KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK, KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK }, + { KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC, KIOU_HOOK_ID_BSE_EVALUATE_ASYNC, KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC }, // Direct-call sites (no chinlan cave / no hook id): { KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, -1, KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC }, { KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT }, diff --git a/recipes/common.py b/recipes/common.py index bf2561a..5edfa48 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -91,6 +91,7 @@ "KIOU_HOOK_ID_UIBUTTONBASE_ONPOINTERCLICK": 31, "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT": 32, "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK": 33, + "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC": 34, } # Entry slot indices — one per CAVE_ENTRY row, must mirror KIOUHook.h. @@ -126,9 +127,10 @@ "KIOU_HOOK_ID_UIBUTTONBASE_ONPOINTERCLICK": 26, "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT": 27, "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK": 28, + "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC": 29, } -ENTRY_SLOT_COUNT = 29 +ENTRY_SLOT_COUNT = 30 ENTRY_SLOT_CAPACITY = 32 # reserved sibling room for future entry hooks # --------------------------------------------------------------------------- diff --git a/recipes/v1_0_1.py b/recipes/v1_0_1.py index 4981c3c..30aa87b 100644 --- a/recipes/v1_0_1.py +++ b/recipes/v1_0_1.py @@ -69,5 +69,6 @@ (0x5DD1E08, "f44fbea9", "KIOU_HOOK_ID_UIBUTTONBASE_ONPOINTERCLICK", CAVE_ENTRY, "UIButtonBase.OnPointerClick"), (0x5DCC728, "ff0303d1", "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT", CAVE_ENTRY, "TitleScene+d__10.MoveNext"), (0x59455D4, "f44fbea9", "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK", CAVE_ENTRY, "GameOrchestrator.IsAfkEnabled"), + (0x597B570, "ff8302d1", "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC", CAVE_ENTRY, "BeginnerSupportEvaluator.EvaluateAsync"), ] # fmt: on diff --git a/recipes/v1_0_2.py b/recipes/v1_0_2.py index bb3c326..5a56a28 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -8,7 +8,12 @@ BUILD = 12 # Cave payload region (zero-fill tail of UnityFramework __TEXT). -CAVE_REGION = (0x826F5E8, 0x8274000) +# Starts *after* __oslogstring (0x8270000..0x8270023) — the region between +# __eh_frame end (0x826F5E8) and 0x8270000 is only 0xA18 B (~30 caves) +# and the 30th and 31st caves straddled the __oslogstring bytes. The +# post-__oslogstring pad is 0x3FDC B (~194 caves), enough for all 34 +# SITES with zero collision. +CAVE_REGION = (0x8270024, 0x8274000) # Observer dispatcher slot — chinlan caves load this single 8-byte pointer. # Sits just past the entry-slot table inside __DATA.__common. The old @@ -82,5 +87,6 @@ (0x5DD7F54, "f44fbea9", "KIOU_HOOK_ID_UIBUTTONBASE_ONPOINTERCLICK", CAVE_ENTRY, "UIButtonBase.OnPointerClick"), (0x5DD2874, "ff0303d1", "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT", CAVE_ENTRY, "TitleScene+d__10.MoveNext"), (0x594A034, "f44fbea9", "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK", CAVE_ENTRY, "GameOrchestrator.IsAfkEnabled"), + (0x5980304, "ff8302d1", "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC", CAVE_ENTRY, "BeginnerSupportEvaluator.EvaluateAsync"), ] # fmt: on From d1c892ef74bc395721858ff7cd3f73b851d5db6d Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 16:34:26 +0000 Subject: [PATCH 06/20] =?UTF-8?q?feat(ai-special-support):=20promote=20?= =?UTF-8?q?=E6=A3=8B=E6=A1=9C=E8=A6=9A=E9=86=92=20UI-unlock=20into=20the?= =?UTF-8?q?=20shared=20catalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 5 ShogiMoveResultStatus / ShogiMatchingPlayerStatus getters that gate the 棋桜覚醒 button lived in KiouEditor as MSHookFunction-only supplements — a no-op on chinlan because the sites had no caves. The "paid-feature unlock stays local to the consumer" policy from KIOU-Hook's README predates the KIOU_FEATURE_AI_SPECIAL_SUPPORT toggle that already gates every one of these hooks: shared plumbing only publishes the site wiring; the user still has to opt in through KiouEditor's settings sheet. With the feature gate covering the risk, promote them so chinlan actually dispatches. Catalog layout — the 5 new CAVE_ENTRY IDs land after KIOU_HOOK_ID_BSE_EVALUATE_ASYNC (=34) at slots 30..34 / IDs 35..39: KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL (bool) KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING (int32) KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING (int32) KIOU_HOOK_ID_MP_FREE_REMAINING (int32) KIOU_HOOK_ID_MP_PAID_AVAILABLE (int32) That pushes ENTRY_SLOT_COUNT to 35, one past the previous ENTRY_SLOT_CAPACITY = 32. Bump the capacity to 40 (sibling room) and slide the single observer-dispatcher slot forward by 0x100: KIOU_HOOK_OBSERVER_SLOT_RVA: 0x091E92B8 -> 0x091E93B8 HOOK_SLOT_RVA (v1_0_1/v1_0_2):0x091E92B8 -> 0x091E93B8 The new observer slot sits inside __DATA.__common on both binaries (1.0.2 __common: 0x8F9D4C0..0x91F5978, headroom 0xC5B8 past the new slot), and both recipe assertions (ENTRY_SLOT_BASE + CAPACITY*8 <= ZERO_REGION_END, HOOK_SLOT_RVA + 8 <= ZERO_REGION_END) still pass. RVA / prologue capture: Site | 1.0.1 RVA | 1.0.2 RVA | prologue ------------------------------------------|------------|------------|--------- MoveResult.get_CanUseAiSpecialSupport | 0x5B4FE18 | 0x5B54F68 | 00204339 MoveResult.get_...RemainingFreeCount | 0x5B4FDE8 | 0x5B54F38 | 00bc40b9 MoveResult.get_...RemainingTicketCount | 0x5B4FDF8 | 0x5B54F48 | 00c040b9 MatchingPlayer.get_...FreeRemainingCount | 0x5B4BC54 | 0x5B50DA4 | 006040b9 MatchingPlayer.get_...PaidAvailableCount | 0x5B4BC64 | 0x5B50DB4 | 006440b9 All five prologues are pure LDR / LDRB immediate loads (no ADR / ADRP / B / BL), so verbatim relocation into the cave tail is safe. Verified against both assets/1.0.1/dump.cs.index.json and assets/1.0.2/dump.cs.index.json. New shared body — Hook/AiSpecialSupport.m — mirrors the KiouEditor original: feature-off => forward to orig, feature-on => CanUse -> true and the 4 counts pinned to KIOU_AI_SPECIAL_SUPPORT_MAX_COUNT (255). The single KIOUEditorInstallAiSpecialSupportHook installer wires all 5 sites through KIOUHookInstall + KIOU_HOOK_PUBLISH_SLOT. Build-verified for chinlan 1.0.2: all 40 SITEs patch, the assertions pass, the IPA repacks. Runtime blast radius worth flagging: the observer-slot move relocates the slot the 5 CAVE_OBSERVER kifu-end hooks ride. The prior 0x091E92B8 placement was device-tested; the new 0x091E93B8 is an in-section slide (same __common, same zero-fill, inside the declared boundary), so the extrapolation is sound — but a device re-verification of the kifu-autosave flow (AI / CPUStream / LocalPvP / OnlinePvP / RecordReplay OnMatchEndAsync) would be prudent before signing off. KiouForge picks up the new slot automatically on its next build (recipe-regenerated). Co-Authored-By: Claude Opus 4.7 --- Hook/AiSpecialSupport.m | 129 ++++++++++++++++++++++++++++++++++++++++ Hook/Common.h | 1 + KIOUHook.h | 31 +++++++++- KIOUHook.m | 12 ++++ recipes/common.py | 16 ++++- recipes/v1_0_1.py | 10 +++- recipes/v1_0_2.py | 10 +++- 7 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 Hook/AiSpecialSupport.m diff --git a/Hook/AiSpecialSupport.m b/Hook/AiSpecialSupport.m new file mode 100644 index 0000000..55d2921 --- /dev/null +++ b/Hook/AiSpecialSupport.m @@ -0,0 +1,129 @@ +#import "Hook/Common.h" +#import "logging.h" + +// =========================================================================== +// Hook/AiSpecialSupport.m — 棋桜覚醒 (AI Special Support) UI unlock. +// +// Migrated from KiouEditor's Sources/KiouEditor/Hook/AiSpecialSupport.m so +// the chinlan flavour can actually dispatch these caves. The historical +// "paid-feature stays out of the shared catalog" policy from the README +// was relaxed once KiouEditor picked up a KIOU_FEATURE_AI_SPECIAL_SUPPORT +// toggle (default off) that gates every one of these hooks — the shared +// catalog only publishes the site plumbing; the user still has to opt in +// through KiouEditor's settings sheet for anything to change. +// +// Five getters gate the button on the client: +// +// ShogiMoveResultStatus: +// get_CanUseAiSpecialSupport -> bool (server allow-flag) +// get_AiSpecialSupportRemainingFreeCount -> int32 +// get_AiSpecialSupportRemainingTicketCount-> int32 +// ShogiMatchingPlayerStatus: +// get_AiSpecialSupportFreeRemainingCount -> int32 +// get_AiSpecialSupportPaidAvailableCount -> int32 +// +// When the toggle is on we force CanUse -> true and pin the four +// count getters to KIOU_AI_SPECIAL_SUPPORT_MAX_COUNT (255). The server +// can still reject the request at the network layer — this is UI +// unlock only, no state or currency change. +// =========================================================================== + +#define KIOU_AI_SPECIAL_SUPPORT_MAX_COUNT 255 + +typedef bool (*GetBool_t)(void *self); +typedef int32_t (*GetI32_t)(void *self); + +static GetBool_t s_origMoveResult_CanUse = NULL; +static GetI32_t s_origMoveResult_FreeRemaining = NULL; +static GetI32_t s_origMoveResult_TicketRemaining = NULL; +static GetI32_t s_origMP_FreeRemaining = NULL; +static GetI32_t s_origMP_PaidAvailable = NULL; + +static bool hook_MoveResult_CanUse(void *self) { + if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_AI_SPECIAL_SUPPORT)) { + return s_origMoveResult_CanUse ? s_origMoveResult_CanUse(self) : false; + } + (void)self; + return true; +} + +static int32_t hook_MoveResult_FreeRemaining(void *self) { + if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_AI_SPECIAL_SUPPORT)) { + return s_origMoveResult_FreeRemaining + ? s_origMoveResult_FreeRemaining(self) : 0; + } + (void)self; + return KIOU_AI_SPECIAL_SUPPORT_MAX_COUNT; +} + +static int32_t hook_MoveResult_TicketRemaining(void *self) { + if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_AI_SPECIAL_SUPPORT)) { + return s_origMoveResult_TicketRemaining + ? s_origMoveResult_TicketRemaining(self) : 0; + } + (void)self; + return KIOU_AI_SPECIAL_SUPPORT_MAX_COUNT; +} + +static int32_t hook_MP_FreeRemaining(void *self) { + if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_AI_SPECIAL_SUPPORT)) { + return s_origMP_FreeRemaining ? s_origMP_FreeRemaining(self) : 0; + } + (void)self; + return KIOU_AI_SPECIAL_SUPPORT_MAX_COUNT; +} + +static int32_t hook_MP_PaidAvailable(void *self) { + if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_AI_SPECIAL_SUPPORT)) { + return s_origMP_PaidAvailable ? s_origMP_PaidAvailable(self) : 0; + } + (void)self; + return KIOU_AI_SPECIAL_SUPPORT_MAX_COUNT; +} + +void KIOUEditorInstallAiSpecialSupportHook(uintptr_t unityBase) { + s_origMoveResult_CanUse = (GetBool_t)KIOUHookInstall( + KIOU_HOOK_NAME_MOVE_RESULT_CAN_USE_SPECIAL, + (void *)hook_MoveResult_CanUse, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, + KIOU_HOOK_SLOT_MOVE_RESULT_CAN_USE_SPECIAL, + hook_MoveResult_CanUse); + + s_origMoveResult_FreeRemaining = (GetI32_t)KIOUHookInstall( + KIOU_HOOK_NAME_MOVE_RESULT_FREE_REMAINING, + (void *)hook_MoveResult_FreeRemaining, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, + KIOU_HOOK_SLOT_MOVE_RESULT_FREE_REMAINING, + hook_MoveResult_FreeRemaining); + + s_origMoveResult_TicketRemaining = (GetI32_t)KIOUHookInstall( + KIOU_HOOK_NAME_MOVE_RESULT_TICKET_REMAINING, + (void *)hook_MoveResult_TicketRemaining, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, + KIOU_HOOK_SLOT_MOVE_RESULT_TICKET_REMAINING, + hook_MoveResult_TicketRemaining); + + s_origMP_FreeRemaining = (GetI32_t)KIOUHookInstall( + KIOU_HOOK_NAME_MP_FREE_REMAINING, + (void *)hook_MP_FreeRemaining, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, + KIOU_HOOK_SLOT_MP_FREE_REMAINING, + hook_MP_FreeRemaining); + + s_origMP_PaidAvailable = (GetI32_t)KIOUHookInstall( + KIOU_HOOK_NAME_MP_PAID_AVAILABLE, + (void *)hook_MP_PaidAvailable, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, + KIOU_HOOK_SLOT_MP_PAID_AVAILABLE, + hook_MP_PaidAvailable); + + IPALog([NSString stringWithFormat: + @"[AI-SPECIAL] installed: CanUse orig=%p FreeRem=%p TicketRem=%p " + @"MP.Free=%p MP.Paid=%p (feature gate=%d)", + (void *)s_origMoveResult_CanUse, + (void *)s_origMoveResult_FreeRemaining, + (void *)s_origMoveResult_TicketRemaining, + (void *)s_origMP_FreeRemaining, + (void *)s_origMP_PaidAvailable, + (int)KIOUEditorFeatureEnabled(KIOU_FEATURE_AI_SPECIAL_SUPPORT)]); +} diff --git a/Hook/Common.h b/Hook/Common.h index a395d91..d740afa 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -204,3 +204,4 @@ void KIOUEditorInstallSyncItemListHook(uintptr_t unityBase); void KIOUEditorInstallVersionHook(uintptr_t unityBase); void KIOUEditorInstallVoiceUnlockHook(uintptr_t unityBase); void KIOUEditorInstallFriendUnhideHook(uintptr_t unityBase); +void KIOUEditorInstallAiSpecialSupportHook(uintptr_t unityBase); diff --git a/KIOUHook.h b/KIOUHook.h index 79b14e8..cd6de86 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -42,13 +42,19 @@ // pointer, BLRs it with W6 = hook_id, and routes through dispatch_one. // // Placement: sits inside __DATA.__common just past the entry-slot table -// capacity (ENTRY_SLOT_BASE_RVA + ENTRY_SLOT_CAPACITY * 8 = 0x091E92B8). +// capacity (ENTRY_SLOT_BASE_RVA + ENTRY_SLOT_CAPACITY * 8 = 0x091E93B8). // The old 0x8F90C80 landed in __DATA.__bss, which UnityRuntime / il2cpp // overwrites during lazy init — publishing dispatch_one there survived // startup but got clobbered before the first observer fire, so the cave // BLR X16 jumped to garbage and crashed with a PC alignment fault. See // recipes/v1_0_2.py for the __common vs __bss note. -#define KIOU_HOOK_OBSERVER_SLOT_RVA 0x091E92B8 +// +// History: this originally sat at 0x091E92B8 (ENTRY_SLOT_CAPACITY = 32). +// Adding the 5 AI-Special-Support caves pushed the count past 32, so +// capacity moved to 40 and the observer slot slid forward by 0x100 to +// 0x091E93B8 — still comfortably inside __common (which ends at +// 0x091F5978 on both 1.0.1 and 1.0.2). +#define KIOU_HOOK_OBSERVER_SLOT_RVA 0x091E93B8 // Entry-cave slot table. Slot N at +N*8 holds the function pointer the // CAVE_ENTRY cave BLRs directly (no dispatcher). @@ -120,6 +126,11 @@ enum kiou_hook_id { KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT, KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK, KIOU_HOOK_ID_BSE_EVALUATE_ASYNC, + KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL, + KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING, + KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING, + KIOU_HOOK_ID_MP_FREE_REMAINING, + KIOU_HOOK_ID_MP_PAID_AVAILABLE, KIOU_HOOK_ID__COUNT, }; @@ -159,6 +170,11 @@ enum kiou_hook_slot_id { KIOU_HOOK_SLOT_TITLE_SCENE_MOVENEXT, KIOU_HOOK_SLOT_GAME_ORCHESTRATOR_IS_AFK, KIOU_HOOK_SLOT_BSE_EVALUATE_ASYNC, + KIOU_HOOK_SLOT_MOVE_RESULT_CAN_USE_SPECIAL, + KIOU_HOOK_SLOT_MOVE_RESULT_FREE_REMAINING, + KIOU_HOOK_SLOT_MOVE_RESULT_TICKET_REMAINING, + KIOU_HOOK_SLOT_MP_FREE_REMAINING, + KIOU_HOOK_SLOT_MP_PAID_AVAILABLE, KIOU_HOOK_SLOT__COUNT, }; @@ -209,6 +225,12 @@ enum kiou_hook_slot_id { #define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x5DCC728 #define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x59455D4 #define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x597B570 +// 棋桜覚醒 (AI Special Support) UI-unlock caves. 1.0.1 RVAs. +#define KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL 0x5B4FE18 +#define KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING 0x5B4FDE8 +#define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x5B4FDF8 +#define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x5B4BC54 +#define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x5B4BC64 // --- Direct-ABI helper RVAs (1.0.1) -------------------------------------- // Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to @@ -282,6 +304,11 @@ extern const char KIOU_HOOK_NAME_UIBUTTONBASE_ONPOINTERCLICK[]; extern const char KIOU_HOOK_NAME_TITLE_SCENE_MOVENEXT[]; extern const char KIOU_HOOK_NAME_GAME_ORCHESTRATOR_IS_AFK[]; extern const char KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC[]; +extern const char KIOU_HOOK_NAME_MOVE_RESULT_CAN_USE_SPECIAL[]; +extern const char KIOU_HOOK_NAME_MOVE_RESULT_FREE_REMAINING[]; +extern const char KIOU_HOOK_NAME_MOVE_RESULT_TICKET_REMAINING[]; +extern const char KIOU_HOOK_NAME_MP_FREE_REMAINING[]; +extern const char KIOU_HOOK_NAME_MP_PAID_AVAILABLE[]; // Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. extern const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[]; extern const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[]; diff --git a/KIOUHook.m b/KIOUHook.m index 5be598a..34ea023 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -51,6 +51,12 @@ const char KIOU_HOOK_NAME_TITLE_SCENE_MOVENEXT[] = "title_scene_movenext"; const char KIOU_HOOK_NAME_GAME_ORCHESTRATOR_IS_AFK[] = "game_orchestrator_is_afk"; const char KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC[] = "bse_evaluate_async"; +// 棋桜覚醒 (AI Special Support) UI-unlock hook names. +const char KIOU_HOOK_NAME_MOVE_RESULT_CAN_USE_SPECIAL[] = "move_result_can_use_special"; +const char KIOU_HOOK_NAME_MOVE_RESULT_FREE_REMAINING[] = "move_result_free_remaining"; +const char KIOU_HOOK_NAME_MOVE_RESULT_TICKET_REMAINING[] = "move_result_ticket_remaining"; +const char KIOU_HOOK_NAME_MP_FREE_REMAINING[] = "mp_free_remaining"; +const char KIOU_HOOK_NAME_MP_PAID_AVAILABLE[] = "mp_paid_available"; // Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[] = "nss_set_hash_size_direct"; const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[] = "game_object_get_component"; @@ -102,6 +108,12 @@ { KIOU_HOOK_NAME_TITLE_SCENE_MOVENEXT, KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT, KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT }, { KIOU_HOOK_NAME_GAME_ORCHESTRATOR_IS_AFK, KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK, KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK }, { KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC, KIOU_HOOK_ID_BSE_EVALUATE_ASYNC, KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC }, + // 棋桜覚醒 (AI Special Support) UI-unlock catalog rows. + { KIOU_HOOK_NAME_MOVE_RESULT_CAN_USE_SPECIAL, KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL, KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL }, + { KIOU_HOOK_NAME_MOVE_RESULT_FREE_REMAINING, KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING, KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING }, + { KIOU_HOOK_NAME_MOVE_RESULT_TICKET_REMAINING,KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING,KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING}, + { KIOU_HOOK_NAME_MP_FREE_REMAINING, KIOU_HOOK_ID_MP_FREE_REMAINING, KIOU_HOOK_RVA_MP_FREE_REMAINING }, + { KIOU_HOOK_NAME_MP_PAID_AVAILABLE, KIOU_HOOK_ID_MP_PAID_AVAILABLE, KIOU_HOOK_RVA_MP_PAID_AVAILABLE }, // Direct-call sites (no chinlan cave / no hook id): { KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, -1, KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC }, { KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT }, diff --git a/recipes/common.py b/recipes/common.py index 5edfa48..92e4aa6 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -92,6 +92,12 @@ "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT": 32, "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK": 33, "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC": 34, + # KiouEditor CAVE_ENTRY hooks for 棋桜覚醒 (AI Special Support) UI unlock. + "KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL": 35, + "KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING": 36, + "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING": 37, + "KIOU_HOOK_ID_MP_FREE_REMAINING": 38, + "KIOU_HOOK_ID_MP_PAID_AVAILABLE": 39, } # Entry slot indices — one per CAVE_ENTRY row, must mirror KIOUHook.h. @@ -128,10 +134,16 @@ "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT": 27, "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK": 28, "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC": 29, + # KiouEditor CAVE_ENTRY slots for 棋桜覚醒 (AI Special Support) UI unlock. + "KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL": 30, + "KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING": 31, + "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING": 32, + "KIOU_HOOK_ID_MP_FREE_REMAINING": 33, + "KIOU_HOOK_ID_MP_PAID_AVAILABLE": 34, } -ENTRY_SLOT_COUNT = 30 -ENTRY_SLOT_CAPACITY = 32 # reserved sibling room for future entry hooks +ENTRY_SLOT_COUNT = 35 +ENTRY_SLOT_CAPACITY = 40 # reserved sibling room for future entry hooks # --------------------------------------------------------------------------- # Cave payload builders diff --git a/recipes/v1_0_1.py b/recipes/v1_0_1.py index 30aa87b..5b4d545 100644 --- a/recipes/v1_0_1.py +++ b/recipes/v1_0_1.py @@ -18,7 +18,7 @@ # See v1_0_2 for the rationale: 0x8F90C80 (__DATA.__bss) is unstable — # UnityRuntime overwrites it after KIOUChinlanPublish. Move to __common # right after the entry-slot table capacity, where publishes survive. -HOOK_SLOT_RVA = 0x091E92B8 +HOOK_SLOT_RVA = 0x091E93B8 PROBED_HOOK_SLOT_RVA = HOOK_SLOT_RVA INJECT_ENTRY_TABLE_RVA = 0x8F90C00 @@ -70,5 +70,13 @@ (0x5DCC728, "ff0303d1", "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT", CAVE_ENTRY, "TitleScene+d__10.MoveNext"), (0x59455D4, "f44fbea9", "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK", CAVE_ENTRY, "GameOrchestrator.IsAfkEnabled"), (0x597B570, "ff8302d1", "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC", CAVE_ENTRY, "BeginnerSupportEvaluator.EvaluateAsync"), + + # --- KiouEditor 棋桜覚醒 (AI Special Support) UI-unlock caves. ------------ + # Server-side reject on the network still applies; this is UI unlock only. + (0x5B4FE18, "00204339", "KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL", CAVE_ENTRY, "ShogiMoveResultStatus.get_CanUseAiSpecialSupport"), + (0x5B4FDE8, "00bc40b9", "KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingFreeCount"), + (0x5B4FDF8, "00c040b9", "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingTicketCount"), + (0x5B4BC54, "006040b9", "KIOU_HOOK_ID_MP_FREE_REMAINING", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportFreeRemainingCount"), + (0x5B4BC64, "006440b9", "KIOU_HOOK_ID_MP_PAID_AVAILABLE", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportPaidAvailableCount"), ] # fmt: on diff --git a/recipes/v1_0_2.py b/recipes/v1_0_2.py index 5a56a28..1212cb2 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -20,7 +20,7 @@ # 0x8F90C80 landed in __DATA.__bss, which il2cpp/UnityRuntime overwrites # during lazy init (verified crash: cave BLR X16 jumped to garbage after # publish). __common survives publish — entry slots (0x091E91B8..) do. -HOOK_SLOT_RVA = 0x091E92B8 +HOOK_SLOT_RVA = 0x091E93B8 PROBED_HOOK_SLOT_RVA = HOOK_SLOT_RVA # Entry-cave slot table — ENTRY_SLOT_BASE_RVA + idx*8 holds each hook fn ptr. @@ -88,5 +88,13 @@ (0x5DD2874, "ff0303d1", "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT", CAVE_ENTRY, "TitleScene+d__10.MoveNext"), (0x594A034, "f44fbea9", "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK", CAVE_ENTRY, "GameOrchestrator.IsAfkEnabled"), (0x5980304, "ff8302d1", "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC", CAVE_ENTRY, "BeginnerSupportEvaluator.EvaluateAsync"), + + # --- KiouEditor 棋桜覚醒 (AI Special Support) UI-unlock caves. ------------ + # Server-side reject on the network still applies; this is UI unlock only. + (0x5B54F68, "00204339", "KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL", CAVE_ENTRY, "ShogiMoveResultStatus.get_CanUseAiSpecialSupport"), + (0x5B54F38, "00bc40b9", "KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingFreeCount"), + (0x5B54F48, "00c040b9", "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingTicketCount"), + (0x5B50DA4, "006040b9", "KIOU_HOOK_ID_MP_FREE_REMAINING", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportFreeRemainingCount"), + (0x5B50DB4, "006440b9", "KIOU_HOOK_ID_MP_PAID_AVAILABLE", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportPaidAvailableCount"), ] # fmt: on From 564fe5a5edbcc385eb6f195f3e3cc30f52f48d61 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 16:53:31 +0000 Subject: [PATCH 07/20] feat(recipes): let consumers override DYLIB_PATH via KIOU_HOOK_DYLIB_PATH env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared recipe hard-codes DYLIB_PATH to @executable_path/Frameworks/KiouForge.dylib because KiouForge is the primary consumer. But every other consumer that reuses this recipe (KiouEditor being the current live example) ships its own dylib basename, and the fixed LC_LOAD_DYLIB target would fail to resolve at runtime — the injected load command would look for KiouForge.dylib in Frameworks while the tweak dropped e.g. KiouEditor.dylib there. Read KIOU_HOOK_DYLIB_PATH from the environment when finalizing the recipe. A value starting with @executable_path / @loader_path / @rpath is used verbatim; a bare filename gets the standard @executable_path/Frameworks/ prefix so consumers can't accidentally inject a path with no dyld resolver directive. KiouForge stays on the KiouForge.dylib default (unchanged behaviour); KiouEditor now sets KIOU_HOOK_DYLIB_PATH=@executable_path/Frameworks/KiouEditor.dylib in its Makefile before invoking build_patched_ipa.sh. Co-Authored-By: Claude Opus 4.7 --- recipes/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/recipes/__init__.py b/recipes/__init__.py index 277ed36..ea0a12a 100644 --- a/recipes/__init__.py +++ b/recipes/__init__.py @@ -11,7 +11,7 @@ import os from recipes.common import ( - DYLIB_PATH, + DYLIB_PATH as _DEFAULT_DYLIB_PATH, ENTRY_SLOT_CAPACITY, ENTRY_SLOT_COUNT, ENTRY_SLOT_INDEX, @@ -20,6 +20,21 @@ build_exports, ) +# Consumer override — the shared default points at KiouForge.dylib because +# KiouForge is the primary consumer, but other consumers (KiouEditor, …) +# ship their own dylib basename. Set KIOU_HOOK_DYLIB_PATH in the build +# environment to override the injected LC_LOAD_DYLIB target. Anything +# starting with @ (@executable_path / @loader_path / @rpath) is accepted +# verbatim; anything else is prefixed with the standard +# @executable_path/Frameworks/ layout to prevent typos. +_override = os.environ.get("KIOU_HOOK_DYLIB_PATH", "").strip() +if _override: + DYLIB_PATH = _override if _override.startswith("@") else ( + f"@executable_path/Frameworks/{_override}" + ) +else: + DYLIB_PATH = _DEFAULT_DYLIB_PATH + __all__ = [ "CAVE_PATCHES", "CAVE_REGION", From c244a07755cc7c19b2c43b825d2f83c7099ea0df Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 18:46:00 +0000 Subject: [PATCH 08/20] fix(kioueditor-rvas): repin KiouEditor RVA macros from 1.0.1 to 1.0.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The KiouEditor-block RVA macros in KIOUHook.h were pinned to 1.0.1 because the tweak historically shipped only for 1.0.1. The refactor branch retargeted KiouEditor at 1.0.2 (recipes/v1_0_2.py is now the authoritative site table), but the RVA macros were left at their 1.0.1 values — the header comment even called this out ("KiouEditor is 1.0.1-only"). Nobody caught it while wiring the chinlan pipeline because chinlan reads sites from the recipe, not from the macros. On JB / jailed, KIOUHookInstall goes through MSHookFunction(unityBase + KIOU_HOOK_RVA_*, replacement) — so the 1.0.1 addresses applied to a 1.0.2 UnityFramework land on unrelated methods. Verified on-device: with the 1.0.1 KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT (0x5DCC728), MSHookFunction overwrote the first bytes of TitleMenuPopupPresenter.<>c.<b__0_7 on 1.0.2 (which lives at 0x5DCC728 +0x20 on that binary). The next time the game showed a title-menu popup, control ran off into the corrupted region and died with EXC_BAD_ACCESS at a wild PC. Two crash reports (KIOU-2026-07-10-033358.ips and -033521.ips) captured this exact stack — TitleMenuPopupPresenter.MoveNext +0x140 crashing into imageIndex=? / imageOffset=0. Repin every KiouEditor entry from recipes/v1_0_2.py SITES: SYNC_ITEM_LIST_MERGE 0x5C37034 -> 0x5C3C29C COLLECTION_PRESET_MERGE 0x5C4065C -> 0x5C458C4 SELECT_CHAR_ASYNC 0x5CA7C90 -> 0x5CACEF8 SELECT_CHAR_REPLY_MERGE 0x5C26DCC -> 0x5C2C034 MATCHING_PLAYER_MERGE 0x5B4CAEC -> 0x5B51C3C HISTORY_DETAIL_MERGE 0x5C01328 -> 0x5C06590 HISTORY_GET_PREMIUM 0x5C00D88 -> 0x5C05FF0 KIFU_DETAIL_IS_PREMIUM 0x585B25C -> 0x585E000 VOICE_PLAYER_SATISFIES 0x582B88C -> 0x582E614 VOICE_CELL_GET_IS_LOCKED 0x584ADC0 -> 0x584DB64 BSE_CTOR 0x597A448 -> 0x597E608 BSE_ENSURE_INITIALIZED 0x597BAFC -> 0x5980890 RBSUPPORT_GET_ENABLED 0x593E630 -> 0x5942AA0 RBSUPPORT_GET_DEPTH 0x593E650 -> 0x5942AC0 HOME_UTILITY_PRESENTER_CTOR 0x5A9F298 -> 0x5AA4054 UIBUTTONBASE_ONPOINTERCLICK 0x5DD1E08 -> 0x5DD7F54 TITLE_SCENE_MOVENEXT 0x5DCC728 -> 0x5DD2874 GAME_ORCHESTRATOR_IS_AFK 0x59455D4 -> 0x594A034 BSE_EVALUATE_ASYNC 0x597B570 -> 0x5980304 MOVE_RESULT_CAN_USE_SPECIAL 0x5B4FE18 -> 0x5B54F68 MOVE_RESULT_FREE_REMAINING 0x5B4FDE8 -> 0x5B54F38 MOVE_RESULT_TICKET_REMAINING 0x5B4FDF8 -> 0x5B54F48 MP_FREE_REMAINING 0x5B4BC54 -> 0x5B50DA4 MP_PAID_AVAILABLE 0x5B4BC64 -> 0x5B50DB4 Also repin the direct-ABI helper block that Hook/AssistTune.m and Hook/FriendUnhideBridge.m look up via KIOUHookSiteAddr for verbatim direct calls (not hooks). Wrong RVAs here corrupt at call time in both chinlan and JB builds because the helpers are used from the shared hook bodies: NSS_SETHASHSIZE_DIRECT 0x5D320E0 -> 0x5D379DC GAMEOBJECT_GETCOMPONENT 0x6BCA6AC -> 0x6BD07F8 RTU_WORLDTOSCREENPOINT 0x6F20040 -> 0x6F2628C The NSS_SETHASHSIZE_DIRECT and NSS_SETHASHSIZE now resolve to the same underlying method on 1.0.2 (0x5D379DC). Keeping both catalog entries: NSS_SETHASHSIZE stays hook_id 1 (currently uninstalled by KiouEditor; still catalogued for KiouForge and future work), while NSS_SETHASHSIZE_DIRECT stays hook_id -1 so KIOUHookInstall on the DIRECT name is a no-op even under the JB pipeline. Update the comment header to reflect the 1.0.2 target and to record the on-device failure mode so the next person who touches this block doesn't repeat the drift. Co-Authored-By: Claude Opus 4.7 --- KIOUHook.h | 84 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/KIOUHook.h b/KIOUHook.h index cd6de86..06ea7f6 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -201,46 +201,54 @@ enum kiou_hook_slot_id { #define KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC 0x5CFC394 #define KIOU_HOOK_RVA_HEADER_PROVIDER_SET_OR_UPDATE_HEADER 0x5BD9EE8 -// --- KiouEditor hook sites (1.0.1 RVAs) ---------------------------------- -// These pin to KIOU 1.0.1 build 11 because KiouEditor is 1.0.1-only. If a -// future port maps the same semantic site to a 1.0.2 RVA, the macros above -// will need version-aware dispatch — for now KiouForge (1.0.2) callers -// don't reference these. -#define KIOU_HOOK_RVA_SYNC_ITEM_LIST_MERGE 0x5C37034 -#define KIOU_HOOK_RVA_COLLECTION_PRESET_MERGE 0x5C4065C -#define KIOU_HOOK_RVA_SELECT_CHAR_ASYNC 0x5CA7C90 -#define KIOU_HOOK_RVA_SELECT_CHAR_REPLY_MERGE 0x5C26DCC -#define KIOU_HOOK_RVA_MATCHING_PLAYER_MERGE 0x5B4CAEC -#define KIOU_HOOK_RVA_HISTORY_DETAIL_MERGE 0x5C01328 -#define KIOU_HOOK_RVA_HISTORY_GET_PREMIUM 0x5C00D88 -#define KIOU_HOOK_RVA_KIFU_DETAIL_IS_PREMIUM 0x585B25C -#define KIOU_HOOK_RVA_VOICE_PLAYER_SATISFIES 0x582B88C -#define KIOU_HOOK_RVA_VOICE_CELL_GET_IS_LOCKED 0x584ADC0 -#define KIOU_HOOK_RVA_BSE_CTOR 0x597A448 -#define KIOU_HOOK_RVA_BSE_ENSURE_INITIALIZED 0x597BAFC -#define KIOU_HOOK_RVA_RBSUPPORT_GET_ENABLED 0x593E630 -#define KIOU_HOOK_RVA_RBSUPPORT_GET_DEPTH 0x593E650 -#define KIOU_HOOK_RVA_HOME_UTILITY_PRESENTER_CTOR 0x5A9F298 -#define KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD1E08 -#define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x5DCC728 -#define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x59455D4 -#define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x597B570 -// 棋桜覚醒 (AI Special Support) UI-unlock caves. 1.0.1 RVAs. -#define KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL 0x5B4FE18 -#define KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING 0x5B4FDE8 -#define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x5B4FDF8 -#define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x5B4BC54 -#define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x5B4BC64 +// --- KiouEditor hook sites (1.0.2 RVAs) ---------------------------------- +// KiouEditor targets KIOU 1.0.2 build 12 now that the refactor branch +// realigned it with KiouForge. These values MUST mirror +// recipes/v1_0_2.py SITES — the chinlan pipeline reads that recipe +// directly, but the JB / jailed pipeline goes through +// KIOUHookInstall → MSHookFunction with the RVA below. If they drift, +// the JB / jailed hook lands on the wrong address and corrupts a +// neighbour method (verified on-device: 1.0.1 TITLE_SCENE_MOVENEXT +// = 0x5DCC728 lands inside TitleMenuPopupPresenter on 1.0.2, which +// then crashed with a wild PC on the first popup). +#define KIOU_HOOK_RVA_SYNC_ITEM_LIST_MERGE 0x5C3C29C +#define KIOU_HOOK_RVA_COLLECTION_PRESET_MERGE 0x5C458C4 +#define KIOU_HOOK_RVA_SELECT_CHAR_ASYNC 0x5CACEF8 +#define KIOU_HOOK_RVA_SELECT_CHAR_REPLY_MERGE 0x5C2C034 +#define KIOU_HOOK_RVA_MATCHING_PLAYER_MERGE 0x5B51C3C +#define KIOU_HOOK_RVA_HISTORY_DETAIL_MERGE 0x5C06590 +#define KIOU_HOOK_RVA_HISTORY_GET_PREMIUM 0x5C05FF0 +#define KIOU_HOOK_RVA_KIFU_DETAIL_IS_PREMIUM 0x585E000 +#define KIOU_HOOK_RVA_VOICE_PLAYER_SATISFIES 0x582E614 +#define KIOU_HOOK_RVA_VOICE_CELL_GET_IS_LOCKED 0x584DB64 +#define KIOU_HOOK_RVA_BSE_CTOR 0x597E608 +#define KIOU_HOOK_RVA_BSE_ENSURE_INITIALIZED 0x5980890 +#define KIOU_HOOK_RVA_RBSUPPORT_GET_ENABLED 0x5942AA0 +#define KIOU_HOOK_RVA_RBSUPPORT_GET_DEPTH 0x5942AC0 +#define KIOU_HOOK_RVA_HOME_UTILITY_PRESENTER_CTOR 0x5AA4054 +#define KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD7F54 +#define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x5DD2874 +#define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x594A034 +#define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x5980304 +// 棋桜覚醒 (AI Special Support) UI-unlock caves. 1.0.2 RVAs. +#define KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL 0x5B54F68 +#define KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING 0x5B54F38 +#define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x5B54F48 +#define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x5B50DA4 +#define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x5B50DB4 -// --- Direct-ABI helper RVAs (1.0.1) -------------------------------------- +// --- Direct-ABI helper RVAs (1.0.2) -------------------------------------- // Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to -// call the underlying functions directly. NSS_SETHASHSIZE_DIRECT is the -// 1.0.1 RVA of NativeSyncSession.SetHashSize — distinct from the -// NSS_SETHASHSIZE hook above whose macro carries the 1.0.2 RVA used by -// KiouForge. Catalog rows for these have hook_id = -1. -#define KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT 0x5D320E0 -#define KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT 0x6BCA6AC -#define KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT 0x6F20040 +// call the underlying functions directly. Now aligned with the 1.0.2 +// binary the KiouEditor tweak targets. NSS_SETHASHSIZE_DIRECT and +// NSS_SETHASHSIZE resolve to the same underlying method on 1.0.2 +// (0x5D379DC) — the DIRECT name is retained because Hook/AssistTune.m +// looks it up under that catalog entry to invoke SetHashSize +// verbatim (not as a redirected hook). Catalog rows for these still +// have hook_id = -1 so KIOUHookInstall won't try to MSHookFunction them. +#define KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT 0x5D379DC +#define KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT 0x6BD07F8 +#define KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT 0x6F2628C // --------------------------------------------------------------------------- // Dispatcher state — defined by the consumer's ChinlanDispatcher.m. From 8de810dd82bed4b9ba33a346d70847f85f422bc5 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 18:59:55 +0000 Subject: [PATCH 09/20] fix(friend-unhide): resolve direct-call RVAs via KIOUHookSiteAddr, not literals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two direct-ABI calls in the friend-unhide bridge — GameObject.GetComponent (string) and RectTransformUtility.WorldToScreenPoint — were computed as g_unityBaseAddr + . Both literals were 1.0.1 values (0x6BCA6AC / 0x6F20040), so on 1.0.2 they land inside unrelated methods. GetComponent(string) fell into what looks like a blocking runtime path (deterministic-hang on the first title-menu sprite recon, per on-device IPALog tail: the last line before freeze is always "[SPRITE-RECON title-menu] imageTf=%p imageGo=%p", i.e. the log immediately preceding the componentByTypeName call), and the app would spin forever without producing a crash report. Route both direct calls through KIOUHookSiteAddr against the version-appropriate catalog names (KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT and KIOU_HOOK_NAME_RTU_WORLDTOSCREENPOINT) so they pick up the same 1.0.2 RVAs KIOUHook.h just repinned. Belt-and-braces null-check on the resolved address before the cast — if the catalog name ever goes stale we degrade to a no-op instead of jumping to unityBase+0. Also drop the two long-dead RVA_HOME_UTILITY_PRESENTER_CTOR / RVA_UIBUTTONBASE_ONPOINTERCLICK #defines at the top of Hook/FriendUnhide.m — both were only kept as reference constants, both were 1.0.1 values, both never expanded anywhere. Removing them prevents the next audit from asking whether they were the freeze cause. Co-Authored-By: Claude Opus 4.7 --- Hook/FriendUnhide.m | 3 --- Hook/FriendUnhideBridge.m | 19 +++++++++++-------- Hook/FriendUnhideBridgeUI.m | 19 +++++++++++-------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Hook/FriendUnhide.m b/Hook/FriendUnhide.m index 9478432..a550731 100644 --- a/Hook/FriendUnhide.m +++ b/Hook/FriendUnhide.m @@ -26,9 +26,6 @@ // GameObject's klass, then cached for subsequent ctor fires. // =========================================================================== -#define RVA_HOME_UTILITY_PRESENTER_CTOR 0x5A9F298 -#define RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD1E08 - #define OFF_HUV_MENU_BUTTON 0x20 #define OFF_HUV_GIFT_BUTTON 0x28 #define OFF_HUV_FRIEND_BUTTON 0x38 diff --git a/Hook/FriendUnhideBridge.m b/Hook/FriendUnhideBridge.m index d372a34..6eb0321 100644 --- a/Hook/FriendUnhideBridge.m +++ b/Hook/FriendUnhideBridge.m @@ -321,13 +321,13 @@ void dumpHierarchy(void *tfObj, int depth, int maxDepth) { return NULL; } -// GameObject.GetComponent(string type) at UnityFramework + 0x6BCA6AC. The -// codegen wrapper here is a thin FreeFunction marshal to native -// Scripting::GetScriptingWrapperOfComponentOfGameObject - probably ignores -// MethodInfo* so passing NULL is OK. If it crashes we'll revisit with proper -// klass-walked MethodInfo* resolution. -#define RVA_GO_GETCOMPONENT_STRING 0x6BCA6AC - +// GameObject.GetComponent(string type). The codegen wrapper here is a thin +// FreeFunction marshal to native +// Scripting::GetScriptingWrapperOfComponentOfGameObject — probably ignores +// MethodInfo* so passing NULL is OK. The site address is resolved via +// KIOUHookSiteAddr against the version-appropriate catalog entry +// (KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT) rather than hard-coded, so this +// stays correct across 1.0.1 / 1.0.2 builds without a per-tweak repin. typedef void *(*GO_GetComponent_string_directABI_t)(void *thisGo, void *typeStr, void *methodInfo); void *componentByTypeName(void *gameObject, const char *typeName) { @@ -335,8 +335,11 @@ void dumpHierarchy(void *tfObj, int depth, int maxDepth) { if (!p_il2cpp_string_new || g_unityBaseAddr == 0) return NULL; void *typeStr = p_il2cpp_string_new(typeName); if (!typeStr) return NULL; + uintptr_t addr = KIOUHookSiteAddr( + KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT, g_unityBaseAddr); + if (addr == 0) return NULL; GO_GetComponent_string_directABI_t fn = - (GO_GetComponent_string_directABI_t)(g_unityBaseAddr + RVA_GO_GETCOMPONENT_STRING); + (GO_GetComponent_string_directABI_t)addr; return fn(gameObject, typeStr, NULL); } diff --git a/Hook/FriendUnhideBridgeUI.m b/Hook/FriendUnhideBridgeUI.m index f78a6f3..7320618 100644 --- a/Hook/FriendUnhideBridgeUI.m +++ b/Hook/FriendUnhideBridgeUI.m @@ -23,19 +23,22 @@ static void *g_method_Tf_get_position = NULL; static void *g_method_Rt_get_sizeDelta = NULL; -// RectTransformUtility.WorldToScreenPoint(Camera cam, Vector3 worldPoint) -// at UnityFramework + 0x6F20040. Static, takes a null camera for -// ScreenSpaceOverlay canvases and returns the screen pixel position with -// bottom-left origin. Direct call with NULL methodInfo - same pattern as -// GameObject.GetComponent(string) which we proved out earlier. -#define RVA_RTU_WORLD_TO_SCREEN 0x6F20040 +// RectTransformUtility.WorldToScreenPoint(Camera cam, Vector3 worldPoint). +// Static, takes a null camera for ScreenSpaceOverlay canvases and returns +// the screen pixel position with bottom-left origin. Direct call with +// NULL methodInfo — same pattern as GameObject.GetComponent(string). The +// site address is resolved via KIOUHookSiteAddr against the +// version-appropriate catalog entry (KIOU_HOOK_NAME_RTU_WORLDTOSCREENPOINT) +// rather than hard-coded, so it stays correct across 1.0.1 / 1.0.2. typedef UVec2 (*RtU_WorldToScreenPoint_t)(void *cam, UVec3 worldPoint, void *methodInfo); static UVec2 unityWorldToScreen(UVec3 worldPoint) { UVec2 zero = {0}; if (g_unityBaseAddr == 0) return zero; - RtU_WorldToScreenPoint_t fn = - (RtU_WorldToScreenPoint_t)(g_unityBaseAddr + RVA_RTU_WORLD_TO_SCREEN); + uintptr_t addr = KIOUHookSiteAddr( + KIOU_HOOK_NAME_RTU_WORLDTOSCREENPOINT, g_unityBaseAddr); + if (addr == 0) return zero; + RtU_WorldToScreenPoint_t fn = (RtU_WorldToScreenPoint_t)addr; return fn(NULL, worldPoint, NULL); } From f623cd8275ba4cbce0763efa35ce660b082a6f5a Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 19:17:59 +0000 Subject: [PATCH 10/20] feat(common): add KIOU_FEATURE_KIFU_AUTOSAVE to the shared feature enum KiouEditor is porting KiouForge's Kif/ pipeline. Both consumers use the same KIOUEditorFeatureEnabled surface, so the master toggle (the one users see labelled "Kifu Autosave" in the settings sheet) needs a KiouFeature slot. Slot it between AI_SPECIAL_SUPPORT and KIOU_FEATURE_COUNT so existing feature IDs stay pinned. Co-Authored-By: Claude Opus 4.7 --- Hook/Common.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Hook/Common.h b/Hook/Common.h index d740afa..6a51753 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -132,6 +132,7 @@ typedef NS_ENUM(NSInteger, KiouFeature) { // because the sites are tweak-local and not part of the shared catalog. KIOU_FEATURE_INGAME_ANALYSIS, // KiouEditor Hook_AssistTune BSE.EvaluateAsync suppression KIOU_FEATURE_AI_SPECIAL_SUPPORT, // KiouEditor Hook_AiSpecialSupport 棋桜覚醒 unlock (default off) + KIOU_FEATURE_KIFU_AUTOSAVE, // KiouEditor Kif/ pipeline — write .kif on OnMatchEndAsync KIOU_FEATURE_COUNT, }; From 69c5b02f76542b6ad8342c60892a53bde9758491 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Thu, 9 Jul 2026 19:41:40 +0000 Subject: [PATCH 11/20] feat(matching-filter): add 3 SITEs for KiouEditor preferred-seat filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported from KiouEngineBridge Hook_MatchingFilterObserve.m. The "First Player Only" filter lets KiouEditor reject a MatchFound whose IsFirstPlayer disagrees with the user's picked seat and ask the matching server to re-queue via ConnectionFailed. Three CAVE_ENTRY sites cover the flow: MatchingHandler.GetValidMatchFoundStatus (0x5D0A78C) Called once per MatchFound. The hook body reads IsFirstPlayer, compares to the seat pref, and — on mismatch — dispatches an IShogiMatchStreamArgs.Create + SendAsync pair. MatchingHandler+d__6.MoveNext (0x5D0C408) Fires as the async state machine resumes. The hook body reads the boxed state machine's stream field (approximate offset 0x40 = struct 0x30 + 0x10 il2cpp header) so hook 1's re-queue path has a live IStreamHandler to send on. IShogiMatchStreamArgs.Create (0x5BCF8CC) Observes every JoinQueue / LeaveQueue / ConnectionFailed the game sends. On JoinQueue the hook body caches matchType / rankRule / eventRule / mstEventMatchId / enableBeginnerSupport so the re-queue path can rebuild an args object with the same settings the user originally picked. All three prologues are non-PC-relative on 1.0.2 (SUB SP / STP) so verbatim relocation into the cave tail is safe. The three hooks are appended before KIOU_HOOK_ID__COUNT so all downstream IDs stay pinned. ENTRY_SLOT_COUNT bumps 35 -> 38; ENTRY_SLOT_CAPACITY bumps 40 -> 48 so the entry-slot table has sibling room for future matching / observer hooks without another observer-slot slide. The observer slot (0x091E93B8) stays where it is — 48 × 8 = 0x180 past ENTRY_SLOT_BASE = 0x091E9338 << 0x091E93B8 (0xC0 headroom). KIOU_FEATURE_SEAT_FILTER is appended to the shared feature enum so KiouEditor's Hook/MatchingFilter.m body can gate itself on the same KIOUEditorFeatureEnabled surface every other consumer uses. Default off — the filter is a competitive-match tampering surface and users should opt in explicitly. The v1_0_1 recipe intentionally does NOT get these sites — the KiouEngineBridge port assumes 1.0.2 addressing and KiouEditor no longer ships 1.0.1 by default. When someone actually needs the 1.0.1 path we can back-port the RVAs then. Co-Authored-By: Claude Opus 4.7 --- Hook/Common.h | 1 + KIOUHook.h | 14 ++++++++++++++ KIOUHook.m | 8 ++++++++ recipes/common.py | 11 +++++++++-- recipes/v1_0_2.py | 7 +++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Hook/Common.h b/Hook/Common.h index 6a51753..ddd6350 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -133,6 +133,7 @@ typedef NS_ENUM(NSInteger, KiouFeature) { KIOU_FEATURE_INGAME_ANALYSIS, // KiouEditor Hook_AssistTune BSE.EvaluateAsync suppression KIOU_FEATURE_AI_SPECIAL_SUPPORT, // KiouEditor Hook_AiSpecialSupport 棋桜覚醒 unlock (default off) KIOU_FEATURE_KIFU_AUTOSAVE, // KiouEditor Kif/ pipeline — write .kif on OnMatchEndAsync + KIOU_FEATURE_SEAT_FILTER, // KiouEditor Hook/MatchingFilter — reject unwanted seat (default off) KIOU_FEATURE_COUNT, }; diff --git a/KIOUHook.h b/KIOUHook.h index 06ea7f6..74b9069 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -131,6 +131,10 @@ enum kiou_hook_id { KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING, KIOU_HOOK_ID_MP_FREE_REMAINING, KIOU_HOOK_ID_MP_PAID_AVAILABLE, + // Matching-seat filter (ported from KiouEngineBridge Hook_MatchingFilterObserve). + KIOU_HOOK_ID_MATCH_GET_VALID_FOUND, + KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT, + KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_ID__COUNT, }; @@ -175,6 +179,9 @@ enum kiou_hook_slot_id { KIOU_HOOK_SLOT_MOVE_RESULT_TICKET_REMAINING, KIOU_HOOK_SLOT_MP_FREE_REMAINING, KIOU_HOOK_SLOT_MP_PAID_AVAILABLE, + KIOU_HOOK_SLOT_MATCH_GET_VALID_FOUND, + KIOU_HOOK_SLOT_MATCH_RECEIVE_TIMEOUT_MOVENEXT, + KIOU_HOOK_SLOT_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_SLOT__COUNT, }; @@ -236,6 +243,10 @@ enum kiou_hook_slot_id { #define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x5B54F48 #define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x5B50DA4 #define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x5B50DB4 +// Matching-seat filter (1.0.2 RVAs). Ported from KiouEngineBridge Hook_MatchingFilterObserve. +#define KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND 0x5D0A78C +#define KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT 0x5D0C408 +#define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x5BCF8CC // --- Direct-ABI helper RVAs (1.0.2) -------------------------------------- // Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to @@ -317,6 +328,9 @@ extern const char KIOU_HOOK_NAME_MOVE_RESULT_FREE_REMAINING[]; extern const char KIOU_HOOK_NAME_MOVE_RESULT_TICKET_REMAINING[]; extern const char KIOU_HOOK_NAME_MP_FREE_REMAINING[]; extern const char KIOU_HOOK_NAME_MP_PAID_AVAILABLE[]; +extern const char KIOU_HOOK_NAME_MATCH_GET_VALID_FOUND[]; +extern const char KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT[]; +extern const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[]; // Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. extern const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[]; extern const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[]; diff --git a/KIOUHook.m b/KIOUHook.m index 34ea023..8a79ee6 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -57,6 +57,10 @@ const char KIOU_HOOK_NAME_MOVE_RESULT_TICKET_REMAINING[] = "move_result_ticket_remaining"; const char KIOU_HOOK_NAME_MP_FREE_REMAINING[] = "mp_free_remaining"; const char KIOU_HOOK_NAME_MP_PAID_AVAILABLE[] = "mp_paid_available"; +// Matching-seat filter (KiouEditor, ported from KiouEngineBridge). +const char KIOU_HOOK_NAME_MATCH_GET_VALID_FOUND[] = "match_get_valid_found"; +const char KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT[] = "match_receive_timeout_movenext"; +const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[] = "match_stream_args_create"; // Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[] = "nss_set_hash_size_direct"; const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[] = "game_object_get_component"; @@ -114,6 +118,10 @@ { KIOU_HOOK_NAME_MOVE_RESULT_TICKET_REMAINING,KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING,KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING}, { KIOU_HOOK_NAME_MP_FREE_REMAINING, KIOU_HOOK_ID_MP_FREE_REMAINING, KIOU_HOOK_RVA_MP_FREE_REMAINING }, { KIOU_HOOK_NAME_MP_PAID_AVAILABLE, KIOU_HOOK_ID_MP_PAID_AVAILABLE, KIOU_HOOK_RVA_MP_PAID_AVAILABLE }, + // Matching-seat filter catalog rows. + { KIOU_HOOK_NAME_MATCH_GET_VALID_FOUND, KIOU_HOOK_ID_MATCH_GET_VALID_FOUND, KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND }, + { KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT }, + { KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE }, // Direct-call sites (no chinlan cave / no hook id): { KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, -1, KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC }, { KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT }, diff --git a/recipes/common.py b/recipes/common.py index 92e4aa6..8eefade 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -98,6 +98,10 @@ "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING": 37, "KIOU_HOOK_ID_MP_FREE_REMAINING": 38, "KIOU_HOOK_ID_MP_PAID_AVAILABLE": 39, + # Matching-seat filter (ported from KiouEngineBridge). + "KIOU_HOOK_ID_MATCH_GET_VALID_FOUND": 40, + "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT": 41, + "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 42, } # Entry slot indices — one per CAVE_ENTRY row, must mirror KIOUHook.h. @@ -140,10 +144,13 @@ "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING": 32, "KIOU_HOOK_ID_MP_FREE_REMAINING": 33, "KIOU_HOOK_ID_MP_PAID_AVAILABLE": 34, + "KIOU_HOOK_ID_MATCH_GET_VALID_FOUND": 35, + "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT": 36, + "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 37, } -ENTRY_SLOT_COUNT = 35 -ENTRY_SLOT_CAPACITY = 40 # reserved sibling room for future entry hooks +ENTRY_SLOT_COUNT = 38 +ENTRY_SLOT_CAPACITY = 48 # reserved sibling room for future entry hooks # --------------------------------------------------------------------------- # Cave payload builders diff --git a/recipes/v1_0_2.py b/recipes/v1_0_2.py index 1212cb2..069a225 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -96,5 +96,12 @@ (0x5B54F48, "00c040b9", "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingTicketCount"), (0x5B50DA4, "006040b9", "KIOU_HOOK_ID_MP_FREE_REMAINING", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportFreeRemainingCount"), (0x5B50DB4, "006440b9", "KIOU_HOOK_ID_MP_PAID_AVAILABLE", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportPaidAvailableCount"), + + # --- KiouEditor preferred-seat filter (ported from KiouEngineBridge). ---- + # Reject a MatchFound if it puts the user on the "wrong" seat, then + # send ConnectionFailed to the matching server so it re-queues. + (0x5D0A78C, "ff0301d1", "KIOU_HOOK_ID_MATCH_GET_VALID_FOUND", CAVE_ENTRY, "MatchingHandler.GetValidMatchFoundStatus"), + (0x5D0C408, "ff0303d1", "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT", CAVE_ENTRY, "MatchingHandler+d__6.MoveNext"), + (0x5BCF8CC, "fc6fbaa9", "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE", CAVE_ENTRY, "IShogiMatchStreamArgs.Create"), ] # fmt: on From 4e565998c9f5e34da6574583db874564d81a4be3 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 10 Jul 2026 11:00:50 +0000 Subject: [PATCH 12/20] fix(chinlan): publish entry slots for AccountObserve + GrpcLogging installers Both installers wired KIOUHookInstall for their hook names but never called KIOU_HOOK_PUBLISH_SLOT on chinlan, leaving cave entries for HTTPMSGINVOKER_ SEND_ASYNC, HEADER_PROVIDER_SET_OR_UPDATE_HEADER, ACCOUNT_EXISTS, LOGIN_ ARGS_CREATE, REGISTER_USER_ARGS_CREATE, RUN_LOGIN_SEQ_MOVENEXT, and GET_SELF_PROFILE_MOVENEXT pointing at a NULL slot. First call into any of them (HTTPMSGINVOKER fires at Unity boot; ACCOUNT_EXISTS during Steam token verify on the account-switch path) BLR X16=0 -> PC=0 -> EXC_BAD_ACCESS. Add the seven PUBLISH_SLOT calls right after each KIOUHookInstall, matching the pattern the KiouEditor-flavored installers already use. JB / jailed builds ignore the macro; only chinlan materialises the slot address. Co-Authored-By: Claude Opus 4.7 --- Hook/AccountObserve.m | 6 ++++++ Hook/GrpcLogging.m | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/Hook/AccountObserve.m b/Hook/AccountObserve.m index 5ddeea4..178f116 100644 --- a/Hook/AccountObserve.m +++ b/Hook/AccountObserve.m @@ -1,4 +1,5 @@ #import "KIOUHook.h" +#import "Hook/Common.h" #import "Account/Persistence.h" #import "il2cpp.h" #import "logging.h" @@ -378,14 +379,19 @@ void KIOUInstallAccountObserveHook(uintptr_t unityBase) { s_origAccountExists = (AccountExists_t) KIOUHookInstall(KIOU_HOOK_NAME_ACCOUNT_EXISTS, (void *)KIOUHookAccountExists, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_ACCOUNT_EXISTS, KIOUHookAccountExists); s_origLoginArgsCreate = (LoginArgsCreate_t) KIOUHookInstall(KIOU_HOOK_NAME_LOGIN_ARGS_CREATE, (void *)KIOUHookLoginArgsCreate, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_LOGIN_ARGS_CREATE, KIOUHookLoginArgsCreate); s_origRegisterUserArgsCreate = (RegisterUserArgsCreate_t) KIOUHookInstall(KIOU_HOOK_NAME_REGISTER_USER_ARGS_CREATE, (void *)KIOUHookRegisterUserArgsCreate, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_REGISTER_USER_ARGS_CREATE, KIOUHookRegisterUserArgsCreate); s_origRunLoginSeqMoveNext = (MoveNextVoid_t) KIOUHookInstall(KIOU_HOOK_NAME_RUN_LOGIN_SEQ_MOVENEXT, (void *)KIOUHookRunLoginSeqMoveNext, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_RUN_LOGIN_SEQ_MOVENEXT, KIOUHookRunLoginSeqMoveNext); s_origGetSelfProfileMoveNext = (MoveNextVoid_t) KIOUHookInstall(KIOU_HOOK_NAME_GET_SELF_PROFILE_MOVENEXT, (void *)KIOUHookGetSelfProfileMoveNext, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_GET_SELF_PROFILE_MOVENEXT, KIOUHookGetSelfProfileMoveNext); #if IPA_CHINLAN (void)KF_RVA_RUN_RESET_USER_DATA_SEQ; diff --git a/Hook/GrpcLogging.m b/Hook/GrpcLogging.m index 2e0851b..c724377 100644 --- a/Hook/GrpcLogging.m +++ b/Hook/GrpcLogging.m @@ -1,4 +1,5 @@ #import "KIOUHook.h" +#import "Hook/Common.h" #import "Account/Persistence.h" #import "il2cpp.h" #import "logging.h" @@ -123,10 +124,16 @@ void KIOUInstallGrpcLoggingHook(uintptr_t unityBase) { s_origHttpMsgInvokerSendAsync = (GenericSendAsync_t) KIOUHookInstall(KIOU_HOOK_NAME_HTTPMSGINVOKER_SEND_ASYNC, (void *)KIOUHookHttpMsgInvokerSendAsync, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, + KIOU_HOOK_SLOT_HTTPMSGINVOKER_SEND_ASYNC, + KIOUHookHttpMsgInvokerSendAsync); s_origHeaderProviderSetOrUpdate = (HeaderProviderSetOrUpdate_t) KIOUHookInstall(KIOU_HOOK_NAME_HEADER_PROVIDER_SET_OR_UPDATE_HEADER, (void *)KIOUHookHeaderProviderSetOrUpdate, unityBase); + KIOU_HOOK_PUBLISH_SLOT(unityBase, + KIOU_HOOK_SLOT_HEADER_PROVIDER_SET_OR_UPDATE_HEADER, + KIOUHookHeaderProviderSetOrUpdate); IPALog([NSString stringWithFormat: @"[GRPC] hook resolved: origSendAsync=%p origSetOrUpdate=%p strNew=%p", From 733f30d30af0d623fb454aa2199315b8908561c9 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 10 Jul 2026 11:01:00 +0000 Subject: [PATCH 13/20] chore(diag): expand fire-time logging in match/select/sync hook bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every hook body now IPALogs on entry with the state that gates its rewrite path, so the first line in the device log tells us whether the site is being hit and why the body took its branch: * MatchingPlayer.merge — fire log with userId + skin/char + persisted even before the cpu-sentinel / non-self early returns, so silent skips are visible. * SelectCharacter.Async / .ReplyMerge — fire log with feature-gate state + requested/persisted so pass-through cases are visible. * SyncItemListReply.merge — fire log with all three feature gates + persisted + reentrant guard state, so re-entrant skips are visible. Bodies are unchanged; this is diagnostic-only. Co-Authored-By: Claude Opus 4.7 --- Hook/MatchingPlayer.m | 16 +++++++++++++--- Hook/SelectCharacter.m | 13 +++++++++++-- Hook/SyncItemList.m | 9 +++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Hook/MatchingPlayer.m b/Hook/MatchingPlayer.m index 81dcbb1..1be5f08 100644 --- a/Hook/MatchingPlayer.m +++ b/Hook/MatchingPlayer.m @@ -53,16 +53,26 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { if (s_origMatchingPlayer_merge) { s_origMatchingPlayer_merge(self, parseContext); } - if (!ptrLooksValid(self)) return; + if (!ptrLooksValid(self)) { + IPALog(@"[MATCH] fire: self ptr invalid"); + return; + } @try { void *userIdStr = readPtr(self, OFF_MP_USER_ID); NSString *userId = il2cppStringToNSString(userIdStr); + int32_t dbgSkinId = readI32(self, OFF_MP_MST_SKIN_ID); + int32_t dbgCharId = readI32(self, OFF_MP_MST_CHAR_ID); + IPALog([NSString stringWithFormat: + @"[MATCH] fire: userId=%@ skin=%d char=%d persisted=%d", + userId.length ? userId : @"(empty)", + dbgSkinId, dbgCharId, KIOUEditorPersistedSelection()]); + if (userId.length == 0) return; if ([userId isEqualToString:kCpuUserIdSentinel]) return; - int32_t curSkinId = readI32(self, OFF_MP_MST_SKIN_ID); - int32_t curCharId = readI32(self, OFF_MP_MST_CHAR_ID); + int32_t curSkinId = dbgSkinId; + int32_t curCharId = dbgCharId; NSString *configuredSelf = KIOUSelfUserId(); BOOL isSelf; diff --git a/Hook/SelectCharacter.m b/Hook/SelectCharacter.m index bb911a9..3e4cfca 100644 --- a/Hook/SelectCharacter.m +++ b/Hook/SelectCharacter.m @@ -50,7 +50,12 @@ static void *hook_SelectCharacterAsync(void *self, void *args, void *opts, void *a3, void *a4, void *a5) { - if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_CHAR_BYPASS)) { + bool gate = KIOUEditorFeatureEnabled(KIOU_FEATURE_CHAR_BYPASS); + int32_t requestedRaw = ptrLooksValid(args) ? readI32(args, OFF_ARGS_SKIN_ID) : -1; + IPALog([NSString stringWithFormat: + @"[SELECT][REQ] fire: feature=%s requested=%d persisted=%d", + gate ? "on" : "off", requestedRaw, KIOUEditorPersistedSelection()]); + if (!gate) { return s_origSelectCharacterAsync(self, args, opts, a3, a4, a5); } if (ptrLooksValid(args)) { @@ -86,7 +91,11 @@ static void hook_SelectCharacterReplyMerge(void *self, void *parseContext) { if (s_origSelectCharacterReplyMerge) { s_origSelectCharacterReplyMerge(self, parseContext); } - if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_CHAR_BYPASS)) return; + bool gate = KIOUEditorFeatureEnabled(KIOU_FEATURE_CHAR_BYPASS); + IPALog([NSString stringWithFormat: + @"[SELECT][RESP] fire: feature=%s self=%p persisted=%d", + gate ? "on" : "off", self, KIOUEditorPersistedSelection()]); + if (!gate) return; if (!ptrLooksValid(self)) return; @try { diff --git a/Hook/SyncItemList.m b/Hook/SyncItemList.m index 1966bc7..90f2261 100644 --- a/Hook/SyncItemList.m +++ b/Hook/SyncItemList.m @@ -60,6 +60,15 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { s_origSyncItemListReply_merge(self, parseContext); } + IPALog([NSString stringWithFormat: + @"[SyncItemListReply] fire: self=%p item=%s voice=%s char=%s " + @"persisted=%d reentrant=%d", + self, + KIOUEditorFeatureEnabled(KIOU_FEATURE_ITEM_UNLOCK) ? "on" : "off", + KIOUEditorFeatureEnabled(KIOU_FEATURE_VOICE_UNLOCK) ? "on" : "off", + KIOUEditorFeatureEnabled(KIOU_FEATURE_CHAR_BYPASS) ? "on" : "off", + KIOUEditorPersistedSelection(), g_inHook]); + if (g_inHook) return; g_inHook = 1; @try { From ebbbb8bc6c9308309350e940ad218c43bf3be84e Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 10 Jul 2026 11:23:55 +0000 Subject: [PATCH 14/20] fix(match): refresh stale self_user_id from active account on merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hook_MatchingPlayer_merge locked in self_user_id via a one-shot heuristic (carry-the-SAFE_ID → this is us) and stored it in NSUserDefaults. Once captured, the value survived reinstalls and account switches, so a device that saw account A first would keep checking every subsequent match's userId against A even after the user signed in as B. Every incoming ShogiMatchingPlayerStatus for B → self_user_id mismatch → "[MATCH] skip non-self" → no beginner-support flip and no character rewrite. Reconcile against KIOUActiveAccountUserId (already refreshed on each AccountExists observation via AccountObserve.m): * active vs locked-in disagree → adopt active as the new self ([MATCH] self_user_id stale (X) -> refresh to active (Y)) * no locked-in but active known → adopt active as first-time capture ([MATCH] self_user_id adopted from active: X) * neither known → fall through to the existing skin==SAFE_ID heuristic Verified on iPad: previously every match log line was skip non-self for a stale 019f4b17-... UUID; after the fix the first match logs the stale → refresh transition, and subsequent matches take the self-rewrite path. Co-Authored-By: Claude Opus 4.7 --- Hook/MatchingPlayer.m | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Hook/MatchingPlayer.m b/Hook/MatchingPlayer.m index 1be5f08..7e127a5 100644 --- a/Hook/MatchingPlayer.m +++ b/Hook/MatchingPlayer.m @@ -74,13 +74,35 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { int32_t curSkinId = dbgSkinId; int32_t curCharId = dbgCharId; + // Prefer KIOUActiveAccountUserId (refreshed on every AccountExists + // observation) over the heuristic-captured KIOUSelfUserId. On + // account switch (or when a stale self_user_id was captured by a + // previous install), the locked-in value can disagree with the + // active account — reconcile by adopting active as the new self. + NSString *activeUserId = KIOUActiveAccountUserId(); NSString *configuredSelf = KIOUSelfUserId(); + if (activeUserId.length > 0 && + configuredSelf.length > 0 && + ![activeUserId isEqualToString:configuredSelf]) { + IPALog([NSString stringWithFormat: + @"[MATCH] self_user_id stale (%@) -> refresh to active (%@)", + configuredSelf, activeUserId]); + KIOUSetSelfUserId(activeUserId); + configuredSelf = activeUserId; + } else if (!configuredSelf && activeUserId.length > 0) { + IPALog([NSString stringWithFormat: + @"[MATCH] self_user_id adopted from active: %@", + activeUserId]); + KIOUSetSelfUserId(activeUserId); + configuredSelf = activeUserId; + } BOOL isSelf; if (configuredSelf) { isSelf = [userId isEqualToString:configuredSelf]; } else { - // No locked-in self: assume the player carrying SAFE_ID is us - // (SelectCharacter forces every outgoing select to SAFE_ID). + // No locked-in self and no known active: assume the player + // carrying SAFE_ID is us (SelectCharacter forces every outgoing + // select to SAFE_ID). isSelf = (curSkinId == KIOU_SAFE_SKIN_ID); } From cbfcf782ba3621b73d23dcc2a1ecc9a03f632ff2 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 10 Jul 2026 17:01:03 +0000 Subject: [PATCH 15/20] refactor(log): unify IPALog format + dedup no-op account saves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two orthogonal changes bundled because both touch Account/Persistence.m: * refactor(log): rewrite every IPALog format string to conform to a single grammar — `[TAG] verb: k=v k=v ...`. Verbs drawn from a small vocabulary (fire, installed, resolved, applied, saved, deleted, refresh, cached, published, skipped, evicted, updated, hooked, exception, swapped, armed, disarmed). Tags normalised to ALL-CAPS, hyphen-separated; nested `[TAG][SUB]` merged into `[TAG-SUB]`. Boot markers (`=== KiouEditor loaded ===` etc.) kept as-is. * fix(account): guard KIOUSaveAccount from re-emitting the `[ACCOUNT] saved: ...` line when the merged record is byte-identical to what's already stored. RunLoginSequence and AccountExists both fire for the same account within ~60 ms on boot; without the guard the log showed the same "saved" event twice back-to-back. Bodies unchanged apart from the noop-merge guard and the format strings. Zero behaviour change for the log format edits. Co-Authored-By: Claude Opus 4.7 --- Account/Persistence.m | 42 ++++++++++++++++++---------- Hook/AccountObserve.m | 40 +++++++++++++------------- Hook/AfkDisable.m | 2 +- Hook/AiSpecialSupport.m | 4 +-- Hook/AssistEnable.m | 2 +- Hook/AssistTune.m | 16 +++++------ Hook/Collection.m | 17 +++++------ Hook/Common.m | 14 +++++----- Hook/FriendUnhide.m | 38 ++++++++++++------------- Hook/FriendUnhideBridge.m | 34 +++++++++++----------- Hook/FriendUnhideBridgeUI.m | 56 ++++++++++++++++++------------------- Hook/GrpcLogging.m | 4 +-- Hook/MatchingPlayer.m | 18 ++++++------ Hook/PremiumUnlock.m | 8 +++--- Hook/SelectCharacter.m | 18 ++++++------ Hook/SyncItemList.m | 30 ++++++++++---------- Hook/Version.m | 6 ++-- Hook/VoiceUnlock.m | 2 +- 18 files changed, 182 insertions(+), 169 deletions(-) diff --git a/Account/Persistence.m b/Account/Persistence.m index eed2f38..4386ef7 100644 --- a/Account/Persistence.m +++ b/Account/Persistence.m @@ -42,7 +42,7 @@ void KIOUSaveAccount(NSString *uuid, NSString *userName, NSString *openId, NSString *userId, NSString *distinctId) { if (userId.length == 0) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] save skipped: missing userId (uuid=%@ userName=%@)", + @"[ACCOUNT] skipped: reason=missingUserId uuid=%@ userName=%@", uuid ?: @"", userName ?: @""]); return; } @@ -59,6 +59,7 @@ void KIOUSaveAccount(NSString *uuid, NSString *userName, NSString *openId, kFieldDistinctId: distinctId ?: @"", kFieldSavedAt: @((NSInteger)[[NSDate date] timeIntervalSince1970]), }; + BOOL noopMerge = NO; for (NSDictionary *e in existing) { NSString *eId = e[kFieldUserId]; if ([eId isKindOfClass:[NSString class]] && [eId isEqualToString:userId]) { @@ -67,6 +68,15 @@ void KIOUSaveAccount(NSString *uuid, NSString *userName, NSString *openId, if (userName.length == 0) merged[kFieldUserName] = e[kFieldUserName] ?: @""; if (openId.length == 0) merged[kFieldOpenId] = e[kFieldOpenId] ?: @""; if (distinctId.length == 0) merged[kFieldDistinctId] = e[kFieldDistinctId] ?: @""; + // Detect a no-op re-save: every non-timestamp field already + // matches. Common when RunLoginSequence and AccountExists both + // fire for the same account within a few dozen ms. + noopMerge = + [merged[kFieldUuid] isEqual:e[kFieldUuid]] && + [merged[kFieldUserName] isEqual:e[kFieldUserName]] && + [merged[kFieldOpenId] isEqual:e[kFieldOpenId]] && + [merged[kFieldUserId] isEqual:e[kFieldUserId]] && + [merged[kFieldDistinctId] isEqual:e[kFieldDistinctId]]; [next addObject:merged]; replaced = YES; } else { @@ -75,9 +85,11 @@ void KIOUSaveAccount(NSString *uuid, NSString *userName, NSString *openId, } if (!replaced) [next addObject:fresh]; [d setObject:next forKey:kKeyAccounts]; - IPALog([NSString stringWithFormat: - @"[ACCOUNT] saved userId=%@ userName=%@ uuid=%@ total=%lu", - userId, userName ?: @"", uuid ?: @"", (unsigned long)next.count]); + if (!noopMerge) { + IPALog([NSString stringWithFormat: + @"[ACCOUNT] saved: userId=%@ userName=%@ uuid=%@ total=%lu", + userId, userName ?: @"", uuid ?: @"", (unsigned long)next.count]); + } kfPostAccountStateChanged(); } @@ -104,7 +116,7 @@ void KIOUUpdateAccountProfile(NSString *userId, NSString *openId, if (!found) return; [d setObject:next forKey:kKeyAccounts]; IPALog([NSString stringWithFormat: - @"[ACCOUNT] profile updated userId=%@ openId=%@ ranks=%lu", + @"[ACCOUNT] updated: scope=profile userId=%@ openId=%@ ranks=%lu", userId, openId ?: @"", (unsigned long)ranks.count]); kfPostAccountStateChanged(); } @@ -122,7 +134,7 @@ void KIOUDeleteAccount(NSString *userId) { } [d setObject:next forKey:kKeyAccounts]; IPALog([NSString stringWithFormat: - @"[ACCOUNT] deleted userId=%@ remaining=%lu", + @"[ACCOUNT] deleted: userId=%@ remaining=%lu", userId, (unsigned long)next.count]); kfPostAccountStateChanged(); } @@ -139,7 +151,7 @@ void KIOUSetActiveAccountUserId(NSString *userId) { } else { [d setObject:userId forKey:kKeyActiveUserId]; } - IPALog([NSString stringWithFormat:@"[ACCOUNT] active_user_id=%@", + IPALog([NSString stringWithFormat:@"[ACCOUNT] applied: activeUserId=%@", userId.length > 0 ? userId : @"(none)"]); kfPostAccountStateChanged(); } @@ -156,7 +168,7 @@ void KIOUSetForceRegisterOnNextLaunch(bool enabled) { } else { [d removeObjectForKey:kKeyForceRegister]; } - IPALog([NSString stringWithFormat:@"[ACCOUNT] force_register=%s", + IPALog([NSString stringWithFormat:@"[ACCOUNT] applied: forceRegister=%s", enabled ? "true" : "false"]); } @@ -169,10 +181,10 @@ void KIOUSetPendingDeviceId(NSString *uuid) { NSUserDefaults *d = [NSUserDefaults standardUserDefaults]; if (uuid.length == 0) { [d removeObjectForKey:kKeyPendingDeviceId]; - IPALog(@"[ACCOUNT] pending_device_id cleared"); + IPALog(@"[ACCOUNT] deleted: field=pendingDeviceId"); } else { [d setObject:uuid forKey:kKeyPendingDeviceId]; - IPALog([NSString stringWithFormat:@"[ACCOUNT] pending_device_id=%@", uuid]); + IPALog([NSString stringWithFormat:@"[ACCOUNT] applied: pendingDeviceId=%@", uuid]); } kfPostAccountStateChanged(); } @@ -186,10 +198,10 @@ void KIOUSetPendingDistinctId(NSString *uuid) { NSUserDefaults *d = [NSUserDefaults standardUserDefaults]; if (uuid.length == 0) { [d removeObjectForKey:kKeyPendingDistinctId]; - IPALog(@"[ACCOUNT] pending_distinct_id cleared"); + IPALog(@"[ACCOUNT] deleted: field=pendingDistinctId"); } else { [d setObject:uuid forKey:kKeyPendingDistinctId]; - IPALog([NSString stringWithFormat:@"[ACCOUNT] pending_distinct_id=%@", uuid]); + IPALog([NSString stringWithFormat:@"[ACCOUNT] applied: pendingDistinctId=%@", uuid]); } kfPostAccountStateChanged(); } @@ -198,13 +210,13 @@ void KIOUSwitchAccount(NSString *uuid) { NSString *armedDistinct = KIOUPendingDistinctId(); if (armedDistinct.length > 0) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] KIOUSwitchAccount refused: Register flow in progress " - @"(pending_distinct_id=%@)", armedDistinct]); + @"[ACCOUNT] skipped: op=switchAccount reason=registerInProgress " + @"pendingDistinctId=%@", armedDistinct]); return; } KIOUSetPendingDeviceId(uuid); IPALog([NSString stringWithFormat: - @"[ACCOUNT] KIOUSwitchAccount armed pending_device_id=%@", uuid ?: @"(nil)"]); + @"[ACCOUNT] armed: op=switchAccount pendingDeviceId=%@", uuid ?: @"(nil)"]); } // --------------------------------------------------------------------------- diff --git a/Hook/AccountObserve.m b/Hook/AccountObserve.m index 178f116..44b2f1f 100644 --- a/Hook/AccountObserve.m +++ b/Hook/AccountObserve.m @@ -137,12 +137,12 @@ void *newStr = g_il2cpp_string_new(pending.UTF8String); if (newStr) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] RegisterUserArgs.Create distinctId → %@", pending]); + @"[ACCOUNT] applied: at=RegisterUserArgs.Create field=distinctId value=%@", pending]); return newStr; } } IPALog([NSString stringWithFormat: - @"[ACCOUNT] RegisterUserArgs.Create userName=%@ distinctId=%@", + @"[ACCOUNT] fire: at=RegisterUserArgs.Create userName=%@ distinctId=%@", readIl2CppStr(userName) ?: @"(nil)", readIl2CppStr(distinctId) ?: @"(nil)"]); return distinctId; @@ -164,12 +164,12 @@ void *newStr = g_il2cpp_string_new(pending.UTF8String); if (newStr) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] LoginArgs.Create deviceId → %@", pending]); + @"[ACCOUNT] applied: at=LoginArgs.Create field=deviceId value=%@", pending]); return newStr; } } IPALog([NSString stringWithFormat: - @"[ACCOUNT] LoginArgs.Create deviceId=%@ distinctId=%@", + @"[ACCOUNT] fire: at=LoginArgs.Create deviceId=%@ distinctId=%@", readIl2CppStr(deviceId) ?: @"(nil)", readIl2CppStr(distinctId) ?: @"(nil)"]); return deviceId; @@ -198,7 +198,7 @@ static void observeRunLoginSeqCompletion(void *self) { NSString *userName = readIl2CppStr(readPtr(candidate, OFF_LOGIN_REPLY_USER_NAME)); if (!userName && !deviceId) continue; IPALog([NSString stringWithFormat: - @"[ACCOUNT] LoginReply @0x%lx userName=%@ deviceId=%@", + @"[ACCOUNT] resolved: subject=loginReply offset=0x%lx userName=%@ deviceId=%@", (unsigned long)offsets[i], userName ?: @"(nil)", deviceId ?: @"(nil)"]); NSString *userId = extractJWTSub(accessToken); @@ -219,7 +219,7 @@ void KIOUHookRunLoginSeqMoveNext(void *self, void *mi) { @try { s_origRunLoginSeqMoveNext(self, mi); } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] RunLoginSeq.MoveNext orig threw: %@", e]); + @"[ACCOUNT] exception: at=RunLoginSeq.MoveNext error=%@", e]); return; } } @@ -243,7 +243,7 @@ static void observeGetSelfProfileCompletion(void *self) { if (userName.length == 0 && openUserId.length == 0) continue; IPALog([NSString stringWithFormat: - @"[ACCOUNT] SelfProfile @0x%lx userName=%@ openUserId=%@", + @"[ACCOUNT] resolved: subject=selfProfile offset=0x%lx userName=%@ openUserId=%@", (unsigned long)off, userName ?: @"(nil)", openUserId ?: @"(nil)"]); NSMutableArray *rankDicts = [NSMutableArray array]; @@ -280,7 +280,7 @@ void KIOUHookGetSelfProfileMoveNext(void *self, void *mi) { @try { s_origGetSelfProfileMoveNext(self, mi); } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] GetSelfProfile.MoveNext orig threw: %@", e]); + @"[ACCOUNT] exception: at=GetSelfProfile.MoveNext error=%@", e]); return; } } @@ -311,14 +311,14 @@ bool KIOUHookAccountExists(void *data, void *mi) { if (s_origAccountExists) { @try { origResult = s_origAccountExists(data, mi); } @catch (NSException *e) { - IPALog([NSString stringWithFormat:@"[ACCOUNT] AccountExists orig threw: %@", e]); + IPALog([NSString stringWithFormat:@"[ACCOUNT] exception: at=AccountExists error=%@", e]); } } observeAccountExistsData(data); bool forceRegister = KIOUForceRegisterOnNextLaunch(); bool result = forceRegister ? false : origResult; if (forceRegister) { - IPALog(@"[ACCOUNT] AccountExists overridden false (force_register)"); + IPALog(@"[ACCOUNT] applied: at=AccountExists override=false reason=forceRegister"); } return result; } @@ -331,13 +331,13 @@ KIOUUniTaskRet KIOUHookRunResetUserDataSeq(void *ct, void *mi) { KIOUSetPendingDistinctId(freshUuid); KIOUSetPendingDeviceId(freshUuid); IPALog([NSString stringWithFormat: - @"[ACCOUNT] RunResetUserDataSequenceAsync armed fresh_uuid=%@", freshUuid]); + @"[ACCOUNT] armed: at=RunResetUserDataSequenceAsync freshUuid=%@", freshUuid]); return s_origRunResetSeq ? s_origRunResetSeq(ct, mi) : (KIOUUniTaskRet){0, 0}; } KIOUUniTaskRet KIOUHookRunDeleteAccountSeq(void *ct, void *mi) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] RunDeleteAccountSequenceAsync (active=%@)", + @"[ACCOUNT] fire: at=RunDeleteAccountSequenceAsync active=%@", KIOUActiveAccountUserId() ?: @"(none)"]); return s_origRunDeleteAccountSeq ? s_origRunDeleteAccountSeq(ct, mi) : (KIOUUniTaskRet){0, 0}; } @@ -352,21 +352,21 @@ KIOUUniTaskRet KIOUHookRunDeleteAccountSeq(void *ct, void *mi) { void KIOUNavigateToTitleScene(void) { if (g_unityBase == 0) { - IPALog(@"[ACCOUNT] KIOUNavigateToTitleScene: unityBase not yet set"); + IPALog(@"[ACCOUNT] skipped: at=KIOUNavigateToTitleScene reason=unityBaseUnset"); return; } uintptr_t addr = KIOUHookSiteAddr(KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, g_unityBase); if (addr == 0) { - IPALog(@"[ACCOUNT] KIOUNavigateToTitleScene: site address unknown"); + IPALog(@"[ACCOUNT] skipped: at=KIOUNavigateToTitleScene reason=siteAddrUnknown"); return; } BackToTitleRunAsync_t fn = (BackToTitleRunAsync_t)addr; @try { (void)fn(NULL, NULL); - IPALog(@"[ACCOUNT] BackToTitleSequence.RunAsync invoked"); + IPALog(@"[ACCOUNT] fire: at=BackToTitleSequence.RunAsync"); } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[ACCOUNT] BackToTitleSequence.RunAsync threw: %@", e]); + @"[ACCOUNT] exception: at=BackToTitleSequence.RunAsync error=%@", e]); } } @@ -396,7 +396,7 @@ void KIOUInstallAccountObserveHook(uintptr_t unityBase) { #if IPA_CHINLAN (void)KF_RVA_RUN_RESET_USER_DATA_SEQ; (void)KF_RVA_RUN_DELETE_ACCOUNT_SEQ; - IPALog(@"[ACCOUNT] chinlan: catalog hooks resolved; raw-RVA hooks (Reset/Delete) skipped"); + IPALog(@"[ACCOUNT] resolved: variant=chinlan catalogHooks=yes rawRvaHooks=skipped"); #else // RunReset / RunDelete are JB-only sites (no chinlan cave for them). { @@ -405,7 +405,7 @@ void KIOUInstallAccountObserveHook(uintptr_t unityBase) { MSHookFunction((void *)addr, (void *)KIOUHookRunResetUserDataSeq, &orig); s_origRunResetSeq = (RunResetSeq_t)orig; IPALog([NSString stringWithFormat: - @"[ACCOUNT] hooked RunResetUserDataSeq @0x%lx", (unsigned long)addr]); + @"[ACCOUNT] hooked: target=RunResetUserDataSeq addr=0x%lx", (unsigned long)addr]); } { uintptr_t addr = unityBase + KF_RVA_RUN_DELETE_ACCOUNT_SEQ; @@ -413,8 +413,8 @@ void KIOUInstallAccountObserveHook(uintptr_t unityBase) { MSHookFunction((void *)addr, (void *)KIOUHookRunDeleteAccountSeq, &orig); s_origRunDeleteAccountSeq = (RunResetSeq_t)orig; IPALog([NSString stringWithFormat: - @"[ACCOUNT] hooked RunDeleteAccountSeq @0x%lx", (unsigned long)addr]); + @"[ACCOUNT] hooked: target=RunDeleteAccountSeq addr=0x%lx", (unsigned long)addr]); } - IPALog(@"[ACCOUNT] hooks installed"); + IPALog(@"[ACCOUNT] installed:"); #endif } diff --git a/Hook/AfkDisable.m b/Hook/AfkDisable.m index 9e3a09c..935ed68 100644 --- a/Hook/AfkDisable.m +++ b/Hook/AfkDisable.m @@ -33,6 +33,6 @@ void KIOUEditorInstallAfkDisableHook(uintptr_t unityBase) { (void *)hook_GO_IsAfkEnabled, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_GAME_ORCHESTRATOR_IS_AFK, hook_GO_IsAfkEnabled); IPALog([NSString stringWithFormat: - @"[AFK] installed: orig=%p (toggled by KIOU_FEATURE_DISABLE_AFK)", + @"[AFK] installed: orig=%p gate=KIOU_FEATURE_DISABLE_AFK", (void *)s_origGO_IsAfkEnabled]); } diff --git a/Hook/AiSpecialSupport.m b/Hook/AiSpecialSupport.m index 55d2921..a18205a 100644 --- a/Hook/AiSpecialSupport.m +++ b/Hook/AiSpecialSupport.m @@ -118,8 +118,8 @@ void KIOUEditorInstallAiSpecialSupportHook(uintptr_t unityBase) { hook_MP_PaidAvailable); IPALog([NSString stringWithFormat: - @"[AI-SPECIAL] installed: CanUse orig=%p FreeRem=%p TicketRem=%p " - @"MP.Free=%p MP.Paid=%p (feature gate=%d)", + @"[AI-SUPPORT] installed: canUse=%p freeRem=%p ticketRem=%p " + @"mpFree=%p mpPaid=%p featureGate=%d", (void *)s_origMoveResult_CanUse, (void *)s_origMoveResult_FreeRemaining, (void *)s_origMoveResult_TicketRemaining, diff --git a/Hook/AssistEnable.m b/Hook/AssistEnable.m index 950919d..6b08a47 100644 --- a/Hook/AssistEnable.m +++ b/Hook/AssistEnable.m @@ -50,7 +50,7 @@ void KIOUEditorInstallAssistEnableHook(uintptr_t unityBase) { (void *)hook_RBS_getDepth, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_RBSUPPORT_GET_DEPTH, hook_RBS_getDepth); IPALog([NSString stringWithFormat: - @"[ASSIST-EN] installed: get_Enabled orig=%p get_Depth orig=%p (depth=%d)", + @"[ASSIST-EN] installed: getEnabledOrig=%p getDepthOrig=%p depth=%d", (void *)s_origRBS_getEnabled, (void *)s_origRBS_getDepth, (int)KIOUEditorAssistDepth()]); } diff --git a/Hook/AssistTune.m b/Hook/AssistTune.m index 73be67c..297cf78 100644 --- a/Hook/AssistTune.m +++ b/Hook/AssistTune.m @@ -60,11 +60,11 @@ static void hook_BSE_ctor(void *self, void *evalPath, void *settings) { writeI32(self, OFF_BSE_ENGINE_SKILL_LEVEL, targetSkill); } IPALog([NSString stringWithFormat: - @"[ASSIST-TUNE] BSE tuned: depth %d -> %d, skillLevel %d -> %d", + @"[ASSIST-TUNE] applied: scope=bseCtor depth=%d->%d skillLevel=%d->%d", origDepth, targetDepth, origSkill, targetSkill]); } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[ASSIST-TUNE] BSE ctor override exception: %@", e]); + @"[ASSIST-TUNE] exception: scope=bseCtor error=%@", e]); } } @@ -83,7 +83,7 @@ static void hook_BSE_ensureInit(void *self) { uintptr_t setHashAddr = KIOUHookSiteAddr( KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, g_unityBaseForAssist); if (setHashAddr == 0) { - IPALog(@"[ASSIST-TUNE] SetHashSize site unresolved — skipping"); + IPALog(@"[ASSIST-TUNE] skipped: reason=setHashSizeSiteUnresolved"); return; } int32_t mb = KIOUEditorAssistHashMB(); @@ -91,11 +91,11 @@ static void hook_BSE_ensureInit(void *self) { (NSS_SetHashSize_directABI_t)setHashAddr; setHash(session, mb, NULL); IPALog([NSString stringWithFormat: - @"[ASSIST-TUNE] EnsureInitializedLocked: SetHashSize(%d) ok session=%p", + @"[ASSIST-TUNE] applied: scope=ensureInitializedLocked hashSizeMB=%d session=%p", mb, session]); } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[ASSIST-TUNE] EnsureInitializedLocked SetHashSize exception: %@", e]); + @"[ASSIST-TUNE] exception: scope=ensureInitializedLocked error=%@", e]); } } @@ -127,9 +127,9 @@ void KIOUEditorInstallAssistTuneHook(uintptr_t unityBase) { (void *)hook_BSE_evaluate_async, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_EVALUATE_ASYNC, hook_BSE_evaluate_async); IPALog([NSString stringWithFormat: - @"[ASSIST-TUNE] installed: BSE.ctor orig=%p EnsureInit orig=%p " - @"EvaluateAsync orig=%p (depth=%d skill=%d hash=%d MB, " - @"INGAME_ANALYSIS gate=%d)", + @"[ASSIST-TUNE] installed: bseCtorOrig=%p ensureInitOrig=%p " + @"evaluateAsyncOrig=%p depth=%d skill=%d hashMB=%d " + @"ingameAnalysisGate=%d", (void *)s_origBSE_ctor, (void *)s_origBSE_ensureInit, (void *)s_origBSE_evaluateAsync, (int)KIOUEditorAssistDepth(), (int)KIOUEditorAssistSkillLevel(), diff --git a/Hook/Collection.m b/Hook/Collection.m index cacf1d7..2f9216a 100644 --- a/Hook/Collection.m +++ b/Hook/Collection.m @@ -32,7 +32,7 @@ static void hook_CollectionPresetReply_merge(void *self, void *parseContext) { int32_t collCount = 0; if (readRepeatedField(self, 0x18, &collArr, &collCount)) { IPALog([NSString stringWithFormat: - @"[UpdateCollectionPresetReply] updatedUserCollectionList count=%d", + @"[UPDATE-COLLECTION-PRESET-REPLY] refresh: list=updatedUserCollectionList count=%d", collCount]); for (int32_t i = 0; i < collCount; i++) { void *coll = readArrayElem(collArr, i); @@ -41,12 +41,12 @@ static void hook_CollectionPresetReply_merge(void *self, void *parseContext) { int32_t presetCount = 0; if (!readRepeatedField(coll, 0x20, &presetArr, &presetCount)) { IPALog([NSString stringWithFormat: - @"[UpdateCollectionPresetReply] [%d] presetList unreadable/empty", + @"[UPDATE-COLLECTION-PRESET-REPLY] skipped: reason=unreadable list=presetList index=%d", i]); continue; } IPALog([NSString stringWithFormat: - @"[UpdateCollectionPresetReply] [%d] presetList count=%d", + @"[UPDATE-COLLECTION-PRESET-REPLY] refresh: list=presetList index=%d count=%d", i, presetCount]); for (int32_t j = 0; j < presetCount; j++) { void *preset = readArrayElem(presetArr, j); @@ -59,20 +59,21 @@ static void hook_CollectionPresetReply_merge(void *self, void *parseContext) { int32_t mstShogiBoardId = readI32(preset, 0x2C); int32_t mstShogiIngameBgmId = readI32(preset, 0x30); IPALog([NSString stringWithFormat: - @"[UpdateCollectionPresetReply] preset[%d] num=%d icon=%d " - @"frame=%d achievement=%d piece=%d board=%d bgm=%d", + @"[UPDATE-COLLECTION-PRESET-REPLY] refresh: presetIndex=%d presetNumber=%d " + @"mstIconId=%d mstIconFrameId=%d mstAchievementId=%d mstShogiPieceId=%d " + @"mstShogiBoardId=%d mstShogiIngameBgmId=%d", j, presetNumber, mstIconId, mstIconFrameId, mstAchievementId, mstShogiPieceId, mstShogiBoardId, mstShogiIngameBgmId]); } } } else { - IPALog(@"[UpdateCollectionPresetReply] updatedUserCollectionList unreadable/empty"); + IPALog(@"[UPDATE-COLLECTION-PRESET-REPLY] skipped: reason=unreadable list=updatedUserCollectionList"); } } } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[UpdateCollectionPresetReply] exception: %@", e]); + @"[UPDATE-COLLECTION-PRESET-REPLY] exception: error=%@", e]); } g_inHook = 0; } @@ -83,6 +84,6 @@ void KIOUEditorInstallCollectionHook(uintptr_t unityBase) { (void *)hook_CollectionPresetReply_merge, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_COLLECTION_PRESET_MERGE, hook_CollectionPresetReply_merge); IPALog([NSString stringWithFormat: - @"[COLLECTION] installed: orig=%p (observation only)", + @"[COLLECTION] installed: orig=%p mode=observationOnly", (void *)s_origCollectionPresetReply_merge]); } diff --git a/Hook/Common.m b/Hook/Common.m index f4df483..967f483 100644 --- a/Hook/Common.m +++ b/Hook/Common.m @@ -146,7 +146,7 @@ void KIOUEditorApplyPersistedSelectionToLists(void *charArr, int32_t charCount, if (flagMoves > 0 || idRewrites > 0) { IPALog([NSString stringWithFormat: - @"[SELECT] applied persisted skinId=%d (flag_moves=%d id_rewrites=%d)", + @"[SELECT] applied: scope=persistedSelection skinId=%d flagMoves=%d idRewrites=%d", target, flagMoves, idRewrites]); } } @@ -161,16 +161,16 @@ void KIOUEditorApplyPersistedSelectionToLists(void *charArr, int32_t charCount, void KIOUEditorReconButtonImage(void *uiButton, const char *tag) { (void)uiButton; IPALog([NSString stringWithFormat: - @"[COMMON] KIOUEditorReconButtonImage stub called (tag=%s) — " - @"FriendUnhide not yet ported", + @"[COMMON] fire: fn=KIOUEditorReconButtonImage note=\"stub\" " + @"reason=friendUnhideNotPorted tag=%s", tag ? tag : "(null)"]); } __attribute__((weak)) void KIOUEditorApplyTitleSpriteToClone(void *cloneGo) { (void)cloneGo; - IPALog(@"[COMMON] KIOUEditorApplyTitleSpriteToClone stub called — " - @"FriendUnhide not yet ported"); + IPALog(@"[COMMON] fire: fn=KIOUEditorApplyTitleSpriteToClone note=\"stub\" " + @"reason=friendUnhideNotPorted"); } // Consumer-provided UIKit settings presenter. Real definition lives in @@ -179,8 +179,8 @@ void KIOUEditorApplyTitleSpriteToClone(void *cloneGo) { // hasn't wired the UIKit side yet. __attribute__((weak)) void KIOUEditorPresentSettings(void) { - IPALog(@"[COMMON] KIOUEditorPresentSettings stub called — " - @"consumer tweak did not wire the UIKit settings surface"); + IPALog(@"[COMMON] fire: fn=KIOUEditorPresentSettings note=\"stub\" " + @"reason=consumerUIKitNotWired"); } // --------------------------------------------------------------------------- diff --git a/Hook/FriendUnhide.m b/Hook/FriendUnhide.m index a550731..ad463aa 100644 --- a/Hook/FriendUnhide.m +++ b/Hook/FriendUnhide.m @@ -48,7 +48,7 @@ static void hook_UIBtn_OnPointerClick(void *self, void *eventData, void *methodI void *thisGo = gameObjectOf(self); if (g_friendGo && thisGo == g_friendGo) { IPALog([NSString stringWithFormat: - @"[HOME] friend tap -> settings (self=%p go=%p)", + @"[HOME] fire: event=friendTap action=presentSettings self=%p go=%p", self, thisGo]); KIOUEditorPresentSettings(); return; @@ -58,7 +58,7 @@ static void hook_UIBtn_OnPointerClick(void *self, void *eventData, void *methodI // re-enables the branch for testing. if (g_cloneGo && thisGo == g_cloneGo) { IPALog([NSString stringWithFormat: - @"[HOME] clone tap -> settings (self=%p go=%p)", + @"[HOME] fire: event=cloneTap action=presentSettings self=%p go=%p", self, thisGo]); KIOUEditorPresentSettings(); return; @@ -66,7 +66,7 @@ static void hook_UIBtn_OnPointerClick(void *self, void *eventData, void *methodI } } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[HOME] OnPointerClick exc: %@", e]); + @"[HOME] exception: at=OnPointerClick error=%@", e]); } if (orig_UIBtn_OnPointerClick) { orig_UIBtn_OnPointerClick(self, eventData, methodInfo); @@ -79,7 +79,7 @@ static void hook_HUP_ctor(void *self, void *view) { } @try { if (!ptrLooksValid(view)) { - IPALog(@"[HOME] presenter.ctor: view ptr invalid"); + IPALog(@"[HOME] skipped: at=presenterCtor reason=viewPtrInvalid"); return; } void *menuBtn = readPtr(view, OFF_HUV_MENU_BUTTON); @@ -87,7 +87,7 @@ static void hook_HUP_ctor(void *self, void *view) { void *friendBtn = readPtr(view, OFF_HUV_FRIEND_BUTTON); (void)menuBtn; (void)giftBtn; IPALog([NSString stringWithFormat: - @"[HOME] HomeUtilityView@%p buttons: menu=%p gift=%p friend=%p", + @"[HOME] resolved: subject=homeUtilityView view=%p menu=%p gift=%p friend=%p", view, menuBtn, giftBtn, friendBtn]); // Friend button is always SetActive(true) because it doubles as @@ -97,14 +97,14 @@ static void hook_HUP_ctor(void *self, void *view) { void *friendGo = gameObjectOf(friendBtn); if (ptrLooksValid(friendGo)) { IPALog([NSString stringWithFormat: - @"[HOME] friend gameObject=%p -> SetActive(true)", friendGo]); + @"[HOME] applied: field=friendGameObject.active value=true go=%p", friendGo]); setActive(friendGo, true); // Snapshot the friend GO so the OnPointerClick hook above // can recognise the tap and route to settings instead of // the "Coming soon" popup the orig handler shows. g_friendGo = friendGo; } else { - IPALog(@"[HOME] friend gameObject lookup failed"); + IPALog(@"[HOME] skipped: reason=friendGameObjectMissing"); } } @@ -117,7 +117,7 @@ static void hook_HUP_ctor(void *self, void *view) { && view != g_lastClonedView && ptrLooksValid(menuBtn) && ptrLooksValid(friendBtn)) { IPALog([NSString stringWithFormat: - @"[HOME] presenter.ctor on main thread=%d (view %p -> %p)", + @"[HOME] fire: at=presenterCtor mainThread=%d prevView=%p view=%p", (int)[NSThread isMainThread], g_lastClonedView, view]); static bool s_homeMenuReconDone = false; @@ -132,7 +132,7 @@ static void hook_HUP_ctor(void *self, void *view) { g_lastClonedView = view; g_cloneGo = cloneGo; IPALog([NSString stringWithFormat: - @"[HOME] direct: clone gameObject=%p", cloneGo]); + @"[HOME] resolved: cloneGameObject=%p", cloneGo]); static bool s_textReconDone = false; if (!s_textReconDone) { void *cloneTfRecon = goTransformOf(cloneGo); @@ -146,40 +146,40 @@ static void hook_HUP_ctor(void *self, void *view) { void *friendTf = transformOf(friendBtn); void *cloneTf = goTransformOf(cloneGo); IPALog([NSString stringWithFormat: - @"[HOME] phase2b: friendTf=%p cloneTf=%p", + @"[HOME] resolved: phase=phase2b friendTf=%p cloneTf=%p", friendTf, cloneTf]); if (ptrLooksValid(friendTf) && ptrLooksValid(cloneTf)) { void *parentTf = transformParentOf(friendTf); IPALog([NSString stringWithFormat: - @"[HOME] phase2b: parentTf=%p", parentTf]); + @"[HOME] resolved: phase=phase2b parentTf=%p", parentTf]); if (ptrLooksValid(parentTf)) { transformSetParent(cloneTf, parentTf, false); int32_t friendIdx = transformGetSiblingIndex(friendTf); IPALog([NSString stringWithFormat: - @"[HOME] phase2b: friend siblingIndex=%d", friendIdx]); + @"[HOME] resolved: phase=phase2b friendSiblingIndex=%d", friendIdx]); if (friendIdx >= 0) { transformSetSiblingIndex(cloneTf, friendIdx + 1); IPALog([NSString stringWithFormat: - @"[HOME] phase2b: clone -> siblingIndex=%d", + @"[HOME] applied: phase=phase2b field=cloneSiblingIndex value=%d", friendIdx + 1]); } } } - IPALog(@"[HOME] phase2c recon: dump clone hierarchy"); + IPALog(@"[HOME] dumped: subject=cloneHierarchy phase=phase2c"); dumpHierarchy(cloneTf, 0, 6); - IPALog(@"[HOME] phase2c recon: dump menu (original) hierarchy"); + IPALog(@"[HOME] dumped: subject=menuHierarchy phase=phase2c"); void *menuTf = transformOf(menuBtn); dumpHierarchy(menuTf, 0, 6); - IPALog(@"[HOME] phase2c recon: dump friend (live) hierarchy"); + IPALog(@"[HOME] dumped: subject=friendHierarchy phase=phase2c"); dumpHierarchy(friendTf, 0, 6); } else { - IPALog(@"[HOME] direct: Instantiate returned NULL/invalid"); + IPALog(@"[HOME] skipped: reason=instantiateFailed"); } } } } @catch (NSException *e) { - IPALog([NSString stringWithFormat:@"[HOME] hook exception: %@", e]); + IPALog([NSString stringWithFormat:@"[HOME] exception: error=%@", e]); } } @@ -201,6 +201,6 @@ void KIOUEditorInstallFriendUnhideHook(uintptr_t unityBase) { KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_UIBUTTONBASE_ONPOINTERCLICK, hook_UIBtn_OnPointerClick); IPALog([NSString stringWithFormat: - @"[FRIEND] installed: HUP.ctor orig=%p UIBtn.OnPointerClick orig=%p", + @"[FRIEND] installed: hupCtor=%p uiBtnOnPointerClick=%p", (void *)orig_HUP_ctor, (void *)orig_UIBtn_OnPointerClick]); } diff --git a/Hook/FriendUnhideBridge.m b/Hook/FriendUnhideBridge.m index 6eb0321..27b230e 100644 --- a/Hook/FriendUnhideBridge.m +++ b/Hook/FriendUnhideBridge.m @@ -57,7 +57,7 @@ static void resolveIl2cppBridge(void) { p_il2cpp_method_get_param_count = (il2cpp_method_get_param_count_t)dlsym(RTLD_DEFAULT, "il2cpp_method_get_param_count"); p_il2cpp_method_is_generic = (il2cpp_method_is_generic_t)dlsym(RTLD_DEFAULT, "il2cpp_method_is_generic"); IPALog([NSString stringWithFormat: - @"[HOME] il2cpp bridge: runtime_invoke=%p class_from_name=%p class_get_method_from_name=%p object_get_class=%p class_get_parent=%p class_get_methods=%p method_get_name=%p method_get_param_count=%p method_is_generic=%p", + @"[HOME] resolved: subject=il2cppBridge runtimeInvoke=%p classFromName=%p classGetMethodFromName=%p objectGetClass=%p classGetParent=%p classGetMethods=%p methodGetName=%p methodGetParamCount=%p methodIsGeneric=%p", p_il2cpp_runtime_invoke, p_il2cpp_class_from_name, p_il2cpp_class_get_method_from_name, @@ -128,7 +128,7 @@ static void invokeSetActive(void *method, void *obj, bool value) { if (!klass) return NULL; g_method_get_gameObject = p_il2cpp_class_get_method_from_name(klass, "get_gameObject", 0); IPALog([NSString stringWithFormat: - @"[HOME] cached get_gameObject method=%p (klass=%p)", + @"[HOME] cached: method=Component.get_gameObject ptr=%p klass=%p", g_method_get_gameObject, klass]); } return invoke0(g_method_get_gameObject, componentObj); @@ -142,7 +142,7 @@ void setActive(void *gameObject, bool value) { if (!klass) return; g_method_SetActive = p_il2cpp_class_get_method_from_name(klass, "SetActive", 1); IPALog([NSString stringWithFormat: - @"[HOME] cached SetActive method=%p (klass=%p)", + @"[HOME] cached: method=GameObject.SetActive ptr=%p klass=%p", g_method_SetActive, klass]); } invokeSetActive(g_method_SetActive, gameObject, value); @@ -159,7 +159,7 @@ void setActive(void *gameObject, bool value) { if (!klass) return NULL; g_method_GO_get_transform = p_il2cpp_class_get_method_from_name(klass, "get_transform", 0); IPALog([NSString stringWithFormat: - @"[HOME] cached GameObject.get_transform method=%p (klass=%p)", + @"[HOME] cached: method=GameObject.get_transform ptr=%p klass=%p", g_method_GO_get_transform, klass]); } return invoke0(g_method_GO_get_transform, gameObject); @@ -174,7 +174,7 @@ void setActive(void *gameObject, bool value) { if (!klass) return NULL; g_method_Tf_get_parent = p_il2cpp_class_get_method_from_name(klass, "get_parent", 0); IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.get_parent method=%p (klass=%p)", + @"[HOME] cached: method=Transform.get_parent ptr=%p klass=%p", g_method_Tf_get_parent, klass]); } return invoke0(g_method_Tf_get_parent, transformObj); @@ -195,17 +195,17 @@ void transformSetParent(void *transformObj, void *newParent, bool worldPositionS if (!klass) return; g_method_Tf_SetParent = p_il2cpp_class_get_method_from_name(klass, "SetParent", 2); IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.SetParent(Tf,bool) method=%p (klass=%p)", + @"[HOME] cached: method=\"Transform.SetParent(Transform,bool)\" ptr=%p klass=%p", g_method_Tf_SetParent, klass]); } if (!g_method_Tf_SetParent) return; void *methodPtr = *(void **)g_method_Tf_SetParent; if (!methodPtr) { - IPALog(@"[HOME] Tf.SetParent direct: methodPointer NULL"); + IPALog(@"[HOME] skipped: at=Tf.SetParent reason=methodPointerNull"); return; } IPALog([NSString stringWithFormat: - @"[HOME] Tf.SetParent direct: methodPtr=%p this=%p parent=%p wps=%d", + @"[HOME] fire: at=Tf.SetParent methodPtr=%p this=%p parent=%p wps=%d", methodPtr, transformObj, newParent, (int)worldPositionStays]); ((Tf_SetParent_directABI_t)methodPtr)(transformObj, newParent, worldPositionStays, g_method_Tf_SetParent); } @@ -223,13 +223,13 @@ int32_t transformGetSiblingIndex(void *transformObj) { if (!klass) return -1; g_method_Tf_GetSiblingIndex = p_il2cpp_class_get_method_from_name(klass, "GetSiblingIndex", 0); IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.GetSiblingIndex method=%p (klass=%p)", + @"[HOME] cached: method=Transform.GetSiblingIndex ptr=%p klass=%p", g_method_Tf_GetSiblingIndex, klass]); } if (!g_method_Tf_GetSiblingIndex) return -1; void *methodPtr = *(void **)g_method_Tf_GetSiblingIndex; if (!methodPtr) { - IPALog(@"[HOME] Tf.GetSiblingIndex direct: methodPointer NULL"); + IPALog(@"[HOME] skipped: at=Tf.GetSiblingIndex reason=methodPointerNull"); return -1; } return ((Tf_GetSiblingIndex_directABI_t)methodPtr)(transformObj, g_method_Tf_GetSiblingIndex); @@ -278,7 +278,7 @@ int32_t transformChildCount(void *transformObj) { if (!klass) return nil; g_method_Obj_get_name = p_il2cpp_class_get_method_from_name(klass, "get_name", 0); IPALog([NSString stringWithFormat: - @"[HOME] cached Object.get_name method=%p (klass=%p)", + @"[HOME] cached: method=UnityEngine.Object.get_name ptr=%p klass=%p", g_method_Obj_get_name, klass]); } if (!g_method_Obj_get_name) return nil; @@ -298,7 +298,7 @@ void dumpHierarchy(void *tfObj, int depth, int maxDepth) { NSMutableString *indent = [NSMutableString string]; for (int i = 0; i < depth; i++) [indent appendString:@" "]; IPALog([NSString stringWithFormat: - @"[HOME] hier %@tf=%p name=%@", + @"[HOME] dumped: subject=hierarchyNode indent=\"%@\" tf=%p name=%@", indent, tfObj, name ?: @""]); int32_t cc = transformChildCount(tfObj); for (int32_t i = 0; i < cc; i++) { @@ -374,7 +374,7 @@ bool swapImageSpriteOnGo(void *imageHostGo, void *newSprite, const char *tag) { void *imageComp = componentByTypeName(imageHostGo, "UnityEngine.UI.Image"); if (!ptrLooksValid(imageComp)) { IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] no Image component on go=%p", tag, imageHostGo]); + @"[SPRITE-SWAP-%s] skipped: reason=noImageComponent go=%p", tag, imageHostGo]); return false; } if (!g_method_Image_set_sprite) { @@ -384,18 +384,18 @@ bool swapImageSpriteOnGo(void *imageHostGo, void *newSprite, const char *tag) { g_method_Image_set_sprite = p_il2cpp_class_get_method_from_name(klass, "set_sprite", 1); IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] cached Image.set_sprite method=%p (klass=%p)", + @"[SPRITE-SWAP-%s] cached: method=Image.set_sprite ptr=%p klass=%p", tag, g_method_Image_set_sprite, klass]); } if (!g_method_Image_set_sprite) return false; void *methodPtr = *(void **)g_method_Image_set_sprite; if (!methodPtr) { IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] set_sprite methodPointer is NULL", tag]); + @"[SPRITE-SWAP-%s] skipped: at=Image.set_sprite reason=methodPointerNull", tag]); return false; } IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP %s] applying sprite=%p to imageComp=%p (was m_Sprite=%p)", + @"[SPRITE-SWAP-%s] applied: sprite=%p imageComp=%p prevSprite=%p", tag, newSprite, imageComp, readPtr(imageComp, 0xD8)]); ((Image_set_sprite_directABI_t)methodPtr)(imageComp, newSprite, g_method_Image_set_sprite); return true; @@ -419,7 +419,7 @@ void reconSpriteName(void *cloneTf) { if (!ptrLooksValid(sprite)) return; NSString *name = objectName(sprite); IPALog([NSString stringWithFormat: - @"[SPRITE-NAME] clone Image.m_Sprite=%p name=\"%@\"", + @"[SPRITE-NAME] resolved: subject=cloneSprite ptr=%p name=\"%@\"", sprite, name ?: @""]); } diff --git a/Hook/FriendUnhideBridgeUI.m b/Hook/FriendUnhideBridgeUI.m index 7320618..8af1e8c 100644 --- a/Hook/FriendUnhideBridgeUI.m +++ b/Hook/FriendUnhideBridgeUI.m @@ -63,7 +63,7 @@ static bool readCloneScreenRect(void *cloneTf, p_il2cpp_class_get_method_from_name(klass, "get_sizeDelta", 0); } IPALog([NSString stringWithFormat: - @"[CLONE-RECT] cached get_position=%p get_sizeDelta=%p (klass=%p)", + @"[CLONE-RECT] cached: getPosition=%p getSizeDelta=%p klass=%p", g_method_Tf_get_position, g_method_Rt_get_sizeDelta, klass]); } if (!g_method_Tf_get_position || !g_method_Rt_get_sizeDelta) return false; @@ -75,7 +75,7 @@ static bool readCloneScreenRect(void *cloneTf, *outPos = ((Tf_get_position_HFA_t)posPtr)(cloneTf, g_method_Tf_get_position); *outSize = ((Rt_get_sizeDelta_HFA_t)sizePtr)(cloneTf, g_method_Rt_get_sizeDelta); IPALog([NSString stringWithFormat: - @"[CLONE-RECT] pos=(%g,%g,%g) sizeDelta=(%g,%g)", + @"[CLONE-RECT] resolved: pos=\"(%g,%g,%g)\" sizeDelta=\"(%g,%g)\"", outPos->x, outPos->y, outPos->z, outSize->x, outSize->y]); return true; } @@ -106,7 +106,7 @@ void hideCloneImage(void *cloneTf) { color[2] = 1.0f; color[3] = 0.0f; IPALog([NSString stringWithFormat: - @"[CLONE-HIDE] imageComp=%p m_Color set to (1,1,1,0)", imageComp]); + @"[CLONE-HIDE] applied: imageComp=%p field=mColor value=\"(1,1,1,0)\"", imageComp]); if (!g_method_Graphic_SetAllDirty) { if (!p_il2cpp_object_get_class || !p_il2cpp_class_get_method_from_name) return; @@ -114,14 +114,14 @@ void hideCloneImage(void *cloneTf) { if (!klass) return; g_method_Graphic_SetAllDirty = p_il2cpp_class_get_method_from_name(klass, "SetAllDirty", 0); IPALog([NSString stringWithFormat: - @"[CLONE-HIDE] cached Graphic.SetAllDirty method=%p (klass=%p)", + @"[CLONE-HIDE] cached: method=Graphic.SetAllDirty ptr=%p klass=%p", g_method_Graphic_SetAllDirty, klass]); } if (!g_method_Graphic_SetAllDirty) return; void *methodPtr = *(void **)g_method_Graphic_SetAllDirty; if (!methodPtr) return; ((Graphic_SetAllDirty_t)methodPtr)(imageComp, g_method_Graphic_SetAllDirty); - IPALog(@"[CLONE-HIDE] SetAllDirty invoked"); + IPALog(@"[CLONE-HIDE] fire: at=SetAllDirty"); } // Probe each GameObject in the clone tree for a text component and log @@ -157,7 +157,7 @@ void reconTextComponents(void *cloneTf) { for (int n = 0; n < 3; n++) { void *c = componentByTypeName(pts[p].go, names[n]); IPALog([NSString stringWithFormat: - @"[TEXT-RECON] %s GetComponent(\"%s\")=%p", + @"[TEXT-RECON] resolved: target=%s component=\"%s\" ptr=%p", pts[p].tag, names[n], c]); } } @@ -189,7 +189,7 @@ static bool applySiblingSpriteToClone(void *cloneGo, void *sourceBtn, const char void *sprite = spriteOfButton(sourceBtn); if (!ptrLooksValid(sprite)) { IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP clone] no sprite on %s source", sourceTag]); + @"[SPRITE-SWAP-CLONE] skipped: reason=noSprite source=%s", sourceTag]); return false; } void *cloneTf = goTransformOf(cloneGo); @@ -197,7 +197,7 @@ static bool applySiblingSpriteToClone(void *cloneGo, void *sourceBtn, const char if (!ptrLooksValid(imageTf)) return false; void *imageGo = gameObjectOf(imageTf); IPALog([NSString stringWithFormat: - @"[SPRITE-SWAP clone] source=%s sprite=%p", sourceTag, sprite]); + @"[SPRITE-SWAP-CLONE] fire: source=%s sprite=%p", sourceTag, sprite]); return swapImageSpriteOnGo(imageGo, sprite, "clone"); } @@ -222,45 +222,45 @@ void KIOUEditorApplyTitleSpriteToClone(void *cloneGo) { void KIOUEditorReconButtonImage(void *uiButton, const char *tag) { if (!ptrLooksValid(uiButton)) { IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] button ptr invalid (%p)", tag, uiButton]); + @"[SPRITE-RECON-%s] skipped: reason=buttonPtrInvalid ptr=%p", tag, uiButton]); return; } void *btnGo = gameObjectOf(uiButton); void *btnTf = goTransformOf(btnGo); IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] btn=%p go=%p tf=%p", + @"[SPRITE-RECON-%s] resolved: btn=%p go=%p tf=%p", tag, uiButton, btnGo, btnTf]); if (!ptrLooksValid(btnTf)) return; void *imageTf = findIconImageTransform(btnTf); if (!ptrLooksValid(imageTf)) { IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] no Image/IconImage leaf - dumping btnTf:", + @"[SPRITE-RECON-%s] dumped: subject=btnTf reason=noImageLeaf", tag]); dumpHierarchy(btnTf, 0, 3); return; } void *imageGo = gameObjectOf(imageTf); IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] imageTf=%p imageGo=%p", + @"[SPRITE-RECON-%s] resolved: imageTf=%p imageGo=%p", tag, imageTf, imageGo]); if (!ptrLooksValid(imageGo)) return; void *imageComp = componentByTypeName(imageGo, "UnityEngine.UI.Image"); IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] GetComponent(\"UnityEngine.UI.Image\")=%p", + @"[SPRITE-RECON-%s] resolved: component=\"UnityEngine.UI.Image\" ptr=%p", tag, imageComp]); if (!ptrLooksValid(imageComp)) return; void *sprite = readPtr(imageComp, 0xD8); IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] m_Sprite=%p", tag, sprite]); + @"[SPRITE-RECON-%s] resolved: sprite=%p", tag, sprite]); // Title side: cache the sprite so the home clone hook can swap it in. if (tag && strcmp(tag, "title-menu") == 0 && ptrLooksValid(sprite)) { g_titleMenuSprite = sprite; IPALog([NSString stringWithFormat: - @"[SPRITE-RECON %s] cached title menu sprite for clone swap", + @"[SPRITE-RECON-%s] cached: subject=titleMenuSprite", tag]); } } @@ -275,13 +275,13 @@ void transformSetSiblingIndex(void *transformObj, int32_t idx) { if (!klass) return; g_method_Tf_SetSiblingIndex = p_il2cpp_class_get_method_from_name(klass, "SetSiblingIndex", 1); IPALog([NSString stringWithFormat: - @"[HOME] cached Transform.SetSiblingIndex method=%p (klass=%p)", + @"[HOME] cached: method=Transform.SetSiblingIndex ptr=%p klass=%p", g_method_Tf_SetSiblingIndex, klass]); } if (!g_method_Tf_SetSiblingIndex) return; void *methodPtr = *(void **)g_method_Tf_SetSiblingIndex; if (!methodPtr) { - IPALog(@"[HOME] Tf.SetSiblingIndex direct: methodPointer NULL"); + IPALog(@"[HOME] skipped: at=Tf.SetSiblingIndex reason=methodPointerNull"); return; } ((Tf_SetSiblingIndex_directABI_t)methodPtr)(transformObj, idx, g_method_Tf_SetSiblingIndex); @@ -296,7 +296,7 @@ void transformSetSiblingIndex(void *transformObj, int32_t idx) { if (!klass) return NULL; g_method_get_transform = p_il2cpp_class_get_method_from_name(klass, "get_transform", 0); IPALog([NSString stringWithFormat: - @"[HOME] cached get_transform method=%p (klass=%p)", + @"[HOME] cached: method=Component.get_transform ptr=%p klass=%p", g_method_get_transform, klass]); } return invoke0(g_method_get_transform, componentObj); @@ -313,7 +313,7 @@ static void logInstantiateMethods(void *anyGo) { || !p_il2cpp_class_get_methods || !p_il2cpp_method_get_name || !p_il2cpp_method_get_param_count) { - IPALog(@"[HOME] enum recon: bridge incomplete, skipping"); + IPALog(@"[HOME] skipped: phase=enumRecon reason=bridgeIncomplete"); return; } void *goKlass = p_il2cpp_object_get_class(anyGo); @@ -321,7 +321,7 @@ static void logInstantiateMethods(void *anyGo) { void *objKlass = p_il2cpp_class_get_parent(goKlass); if (!objKlass) return; IPALog([NSString stringWithFormat: - @"[HOME] enum: walking Object klass=%p", objKlass]); + @"[HOME] fire: phase=enum subject=objectKlass klass=%p", objKlass]); void *iter = NULL; void *method = NULL; int hits = 0; @@ -335,12 +335,12 @@ static void logInstantiateMethods(void *anyGo) { isGeneric = (int)p_il2cpp_method_is_generic(method); } IPALog([NSString stringWithFormat: - @"[HOME] enum: %s argc=%u generic=%d method=%p", + @"[HOME] resolved: phase=enum name=%s argc=%u generic=%d method=%p", name, argc, isGeneric, method]); hits++; } IPALog([NSString stringWithFormat: - @"[HOME] enum: %d Instantiate variants found", hits]); + @"[HOME] resolved: phase=enum instantiateVariants=%d", hits]); } // Walk a klass's methods and return the first one matching name + argc that @@ -380,7 +380,7 @@ static void logInstantiateMethods(void *anyGo) { if (!objKlass) return NULL; g_method_Instantiate1NonGen = findNonGenericMethod(objKlass, "Instantiate", 1); IPALog([NSString stringWithFormat: - @"[HOME] cached non-generic Instantiate(Object) method=%p (objKlass=%p)", + @"[HOME] cached: method=\"Instantiate(Object)\" generic=false ptr=%p objKlass=%p", g_method_Instantiate1NonGen, objKlass]); } if (!g_method_Instantiate1NonGen) return NULL; @@ -410,17 +410,17 @@ static void logInstantiateMethods(void *anyGo) { if (!objKlass) return NULL; g_method_Instantiate1NonGen = findNonGenericMethod(objKlass, "Instantiate", 1); IPALog([NSString stringWithFormat: - @"[HOME] cached non-generic Instantiate(Object) method=%p (objKlass=%p)", + @"[HOME] cached: method=\"Instantiate(Object)\" generic=false ptr=%p objKlass=%p", g_method_Instantiate1NonGen, objKlass]); } if (!g_method_Instantiate1NonGen) return NULL; void *methodPtr = *(void **)g_method_Instantiate1NonGen; if (!methodPtr) { - IPALog(@"[HOME] direct: methodPointer at offset 0 is NULL"); + IPALog(@"[HOME] skipped: at=instantiateDirect reason=methodPointerNull"); return NULL; } IPALog([NSString stringWithFormat: - @"[HOME] direct call: methodPtr=%p methodInfo=%p original=%p", + @"[HOME] fire: at=instantiateDirect methodPtr=%p methodInfo=%p original=%p", methodPtr, g_method_Instantiate1NonGen, originalGo]); return ((Instantiate1_directABI_t)methodPtr)(originalGo, g_method_Instantiate1NonGen); } @@ -443,12 +443,12 @@ static void logInstantiateMethods(void *anyGo) { if (!goKlass) return NULL; void *objKlass = p_il2cpp_class_get_parent(goKlass); if (!objKlass) { - IPALog(@"[HOME] Instantiate lookup: parent klass NULL"); + IPALog(@"[HOME] skipped: at=instantiateLookup reason=parentKlassNull"); return NULL; } g_method_Instantiate2 = p_il2cpp_class_get_method_from_name(objKlass, "Instantiate", 2); IPALog([NSString stringWithFormat: - @"[HOME] cached Instantiate(Obj,Tf) method=%p (goKlass=%p objKlass=%p)", + @"[HOME] cached: method=\"Instantiate(Object,Transform)\" ptr=%p goKlass=%p objKlass=%p", g_method_Instantiate2, goKlass, objKlass]); } if (!g_method_Instantiate2) return NULL; diff --git a/Hook/GrpcLogging.m b/Hook/GrpcLogging.m index c724377..39373d9 100644 --- a/Hook/GrpcLogging.m +++ b/Hook/GrpcLogging.m @@ -96,7 +96,7 @@ void KIOUHookHeaderProviderSetOrUpdate(void *self, void *keyStr, void *valueStr, if (newValue) { valueStr = newValue; IPALog([NSString stringWithFormat: - @"[HEADER] x-user-id swapped → %@", target]); + @"[HEADER] swapped: header=x-user-id value=%@", target]); } } } @@ -136,7 +136,7 @@ void KIOUInstallGrpcLoggingHook(uintptr_t unityBase) { KIOUHookHeaderProviderSetOrUpdate); IPALog([NSString stringWithFormat: - @"[GRPC] hook resolved: origSendAsync=%p origSetOrUpdate=%p strNew=%p", + @"[GRPC] resolved: origSendAsync=%p origSetOrUpdate=%p strNew=%p", s_origHttpMsgInvokerSendAsync, s_origHeaderProviderSetOrUpdate, g_GrpcStringNew]); diff --git a/Hook/MatchingPlayer.m b/Hook/MatchingPlayer.m index 7e127a5..25c0be8 100644 --- a/Hook/MatchingPlayer.m +++ b/Hook/MatchingPlayer.m @@ -54,7 +54,7 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { s_origMatchingPlayer_merge(self, parseContext); } if (!ptrLooksValid(self)) { - IPALog(@"[MATCH] fire: self ptr invalid"); + IPALog(@"[MATCH] skipped: reason=selfPtrInvalid"); return; } @@ -85,13 +85,13 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { configuredSelf.length > 0 && ![activeUserId isEqualToString:configuredSelf]) { IPALog([NSString stringWithFormat: - @"[MATCH] self_user_id stale (%@) -> refresh to active (%@)", + @"[MATCH] refresh: field=selfUserId reason=stale from=%@ to=%@", configuredSelf, activeUserId]); KIOUSetSelfUserId(activeUserId); configuredSelf = activeUserId; } else if (!configuredSelf && activeUserId.length > 0) { IPALog([NSString stringWithFormat: - @"[MATCH] self_user_id adopted from active: %@", + @"[MATCH] applied: field=selfUserId source=active value=%@", activeUserId]); KIOUSetSelfUserId(activeUserId); configuredSelf = activeUserId; @@ -108,7 +108,7 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { if (!isSelf) { IPALog([NSString stringWithFormat: - @"[MATCH] skip non-self userId=%@ skin=%d char=%d", + @"[MATCH] skipped: reason=nonSelf userId=%@ skin=%d char=%d", userId, curSkinId, curCharId]); return; } @@ -119,7 +119,7 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { if (!configuredSelf) { KIOUSetSelfUserId(userId); IPALog([NSString stringWithFormat: - @"[MATCH] self_user_id captured: %@ (heuristic -> strict)", + @"[MATCH] applied: field=selfUserId source=heuristic userId=%@ next=strict", userId]); } @@ -130,7 +130,7 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { if (curBSE != 1) { writeU8(self, OFF_MP_ENABLE_BEGINNER_SUPPORT, 1); IPALog([NSString stringWithFormat: - @"[MATCH] enableBeginnerSupport %d -> 1 (self)", + @"[MATCH] applied: field=enableBeginnerSupport value=%d->1 scope=self", (int)curBSE]); } } @@ -145,12 +145,12 @@ static void hook_MatchingPlayer_merge(void *self, void *parseContext) { writeI32(self, OFF_MP_MST_CHAR_ID, target); // 1:1 mapping skin <-> char IPALog([NSString stringWithFormat: - @"[MATCH] self=%@ skin %d->%d char %d->%d (self_locked=%@)", + @"[MATCH] applied: userId=%@ skin=%d->%d char=%d->%d selfLocked=%@", userId, curSkinId, target, curCharId, target, configuredSelf ? @"YES" : @"NO->YES"]); } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[MATCH] exception: %@", e]); + @"[MATCH] exception: error=%@", e]); } } @@ -161,7 +161,7 @@ void KIOUEditorInstallMatchingPlayerHook(uintptr_t unityBase) { KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_MATCHING_PLAYER_MERGE, hook_MatchingPlayer_merge); NSString *configured = KIOUSelfUserId(); IPALog([NSString stringWithFormat: - @"[MATCH] installed: orig=%p self_user_id=%@", + @"[MATCH] installed: orig=%p selfUserId=%@", (void *)s_origMatchingPlayer_merge, configured ?: @"(unset, using heuristic)"]); } diff --git a/Hook/PremiumUnlock.m b/Hook/PremiumUnlock.m index 2af1fdf..a470d76 100644 --- a/Hook/PremiumUnlock.m +++ b/Hook/PremiumUnlock.m @@ -60,12 +60,12 @@ static void hook_HistoryDetailReply_merge(void *self, void *parseContext) { if (before != 1) { writeU8(self, OFF_SHOGI_HISTORY_DETAIL_REPLY_IS_PREMIUM_USER, 1); IPALog([NSString stringWithFormat: - @"[PREMIUM] HistoryDetailReply.isPremiumUser %d -> 1", + @"[PREMIUM] applied: field=historyDetailReply.isPremiumUser value=%d->1", (int)before]); } } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[PREMIUM] HistoryDetailReply merge exception: %@", e]); + @"[PREMIUM] exception: at=HistoryDetailReply.merge error=%@", e]); } } @@ -83,8 +83,8 @@ void KIOUEditorInstallPremiumUnlockHook(uintptr_t unityBase) { (void *)hook_HistoryDetailReply_IsPremiumUser, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_HISTORY_GET_PREMIUM, hook_HistoryDetailReply_IsPremiumUser); IPALog([NSString stringWithFormat: - @"[PREMIUM] installed: KifuDetail.IsPremium orig=%p, " - @"HistoryDetail.merge orig=%p, HistoryDetail.get_IsPremium orig=%p", + @"[PREMIUM] installed: kifuDetailIsPremium=%p " + @"historyDetailMerge=%p historyDetailGetIsPremium=%p", (void *)s_origKifuDetailModel_IsPremiumUser, (void *)s_origHistoryDetailReply_merge, (void *)s_origHistoryDetailReply_IsPremiumUser]); diff --git a/Hook/SelectCharacter.m b/Hook/SelectCharacter.m index 3e4cfca..d49ea35 100644 --- a/Hook/SelectCharacter.m +++ b/Hook/SelectCharacter.m @@ -53,7 +53,7 @@ bool gate = KIOUEditorFeatureEnabled(KIOU_FEATURE_CHAR_BYPASS); int32_t requestedRaw = ptrLooksValid(args) ? readI32(args, OFF_ARGS_SKIN_ID) : -1; IPALog([NSString stringWithFormat: - @"[SELECT][REQ] fire: feature=%s requested=%d persisted=%d", + @"[SELECT-REQ] fire: feature=%s requested=%d persisted=%d", gate ? "on" : "off", requestedRaw, KIOUEditorPersistedSelection()]); if (!gate) { return s_origSelectCharacterAsync(self, args, opts, a3, a4, a5); @@ -64,16 +64,16 @@ KIOUEditorSetPersistedSelection(requested); writeI32(args, OFF_ARGS_SKIN_ID, KIOU_SAFE_SKIN_ID); IPALog([NSString stringWithFormat: - @"[SELECT][REQ] user=%d -> server=%d (persisted)", + @"[SELECT-REQ] applied: user=%d server=%d persisted=true", requested, KIOU_SAFE_SKIN_ID]); } else if (requested == KIOU_SAFE_SKIN_ID) { // The user explicitly picked the safe skin. Drop any override. if (KIOUEditorPersistedSelection() != 0) { KIOUEditorSetPersistedSelection(0); - IPALog(@"[SELECT][REQ] user picked SAFE_ID; cleared persisted override"); + IPALog(@"[SELECT-REQ] deleted: field=persistedOverride reason=safeId"); } else { IPALog([NSString stringWithFormat: - @"[SELECT][REQ] passthrough skinId=%d", requested]); + @"[SELECT-REQ] skipped: reason=passthrough skinId=%d", requested]); } } } @@ -93,7 +93,7 @@ static void hook_SelectCharacterReplyMerge(void *self, void *parseContext) { } bool gate = KIOUEditorFeatureEnabled(KIOU_FEATURE_CHAR_BYPASS); IPALog([NSString stringWithFormat: - @"[SELECT][RESP] fire: feature=%s self=%p persisted=%d", + @"[SELECT-RESP] fire: feature=%s self=%p persisted=%d", gate ? "on" : "off", self, KIOUEditorPersistedSelection()]); if (!gate) return; if (!ptrLooksValid(self)) return; @@ -108,13 +108,13 @@ static void hook_SelectCharacterReplyMerge(void *self, void *parseContext) { readRepeatedField(self, OFF_REPLY_SKIN_LIST, &skinArr, &skinCount); IPALog([NSString stringWithFormat: - @"[SELECT][RESP] charCount=%d skinCount=%d persisted=%d", + @"[SELECT-RESP] resolved: charCount=%d skinCount=%d persisted=%d", charCount, skinCount, KIOUEditorPersistedSelection()]); KIOUEditorApplyPersistedSelectionToLists(charArr, charCount, skinArr, skinCount); } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[SELECT][RESP] exception: %@", e]); + @"[SELECT-RESP] exception: error=%@", e]); } } @@ -132,8 +132,8 @@ void KIOUEditorInstallSelectCharacterHook(uintptr_t unityBase) { (void *)hook_SelectCharacterReplyMerge, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_SELECT_CHAR_REPLY_MERGE, hook_SelectCharacterReplyMerge); IPALog([NSString stringWithFormat: - @"[SELECT] installed: Async orig=%p Reply.merge orig=%p " - @"SAFE_ID=%d persisted=%d", + @"[SELECT] installed: asyncOrig=%p replyMergeOrig=%p " + @"safeId=%d persisted=%d", (void *)s_origSelectCharacterAsync, (void *)s_origSelectCharacterReplyMerge, KIOU_SAFE_SKIN_ID, KIOUEditorPersistedSelection()]); diff --git a/Hook/SyncItemList.m b/Hook/SyncItemList.m index 90f2261..2d7bbfd 100644 --- a/Hook/SyncItemList.m +++ b/Hook/SyncItemList.m @@ -61,7 +61,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { } IPALog([NSString stringWithFormat: - @"[SyncItemListReply] fire: self=%p item=%s voice=%s char=%s " + @"[SYNC-ITEM-LIST-REPLY] fire: self=%p item=%s voice=%s char=%s " @"persisted=%d reentrant=%d", self, KIOUEditorFeatureEnabled(KIOU_FEATURE_ITEM_UNLOCK) ? "on" : "off", @@ -86,7 +86,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { int32_t count = 0; if (readRepeatedField(self, 0x20, &arr, &count)) { IPALog([NSString stringWithFormat: - @"[SyncItemListReply] updatedSupplyList count=%d", count]); + @"[SYNC-ITEM-LIST-REPLY] refresh: list=updatedSupplyList count=%d", count]); int32_t decoTotal = 0; int32_t flipped = 0; int32_t alreadyOwned = 0; @@ -97,7 +97,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { uint8_t isAcquired = readU8(elem, 0x30); int32_t acquiredCount = readI32(elem, 0x34); IPALog([NSString stringWithFormat: - @"[SyncItemListReply] [%d] mstSupplyId=%d (%s) isAcquired=%d acquiredCount=%d", + @"[SYNC-ITEM-LIST-REPLY] refresh: index=%d mstSupplyId=%d band=%s isAcquired=%d acquiredCount=%d", i, mstSupplyId, supplyBand(mstSupplyId), (int)isAcquired, acquiredCount]); @@ -112,10 +112,10 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { } } IPALog([NSString stringWithFormat: - @"[UNLOCK] decoration total=%d already_owned=%d unlocked=%d owned_after=%d", + @"[UNLOCK] updated: scope=decoration total=%d alreadyOwned=%d unlocked=%d ownedAfter=%d", decoTotal, alreadyOwned, flipped, alreadyOwned + flipped]); } else { - IPALog(@"[SyncItemListReply] updatedSupplyList unreadable/empty"); + IPALog(@"[SYNC-ITEM-LIST-REPLY] skipped: reason=unreadable list=updatedSupplyList"); } } @@ -126,7 +126,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { int32_t charCount = 0; if (readRepeatedField(self, 0x28, &charArr, &charCount)) { IPALog([NSString stringWithFormat: - @"[UNLOCK-CHAR] updatedCharacterList count=%d", charCount]); + @"[UNLOCK-CHAR] refresh: list=updatedCharacterList count=%d", charCount]); int32_t charTotal = 0; int32_t contractFlip = 0; int32_t acquiredFlip = 0; @@ -139,7 +139,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { uint8_t isContract = readU8(elem, 0x20); uint8_t isAcquired = readU8(elem, 0x30); IPALog([NSString stringWithFormat: - @"[UNLOCK-CHAR] [%d] mstCharacterId=%d intimacyLevel=%d " + @"[UNLOCK-CHAR] refresh: index=%d mstCharacterId=%d intimacyLevel=%d " @"isContract=%d isAcquired=%d", i, mstCharacterId, intimacyLevel, (int)isContract, (int)isAcquired]); @@ -161,11 +161,11 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { } } IPALog([NSString stringWithFormat: - @"[UNLOCK-CHAR] characters total=%d contract_unlocked=%d " - @"acquired_unlocked=%d intimacy_maxed=%d", + @"[UNLOCK-CHAR] updated: scope=characters total=%d contractUnlocked=%d " + @"acquiredUnlocked=%d intimacyMaxed=%d", charTotal, contractFlip, acquiredFlip, intimacyFlip]); } else { - IPALog(@"[UNLOCK-CHAR] updatedCharacterList unreadable/empty"); + IPALog(@"[UNLOCK-CHAR] skipped: reason=unreadable list=updatedCharacterList"); } } @@ -175,7 +175,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { int32_t skinCount = 0; if (readRepeatedField(self, 0x30, &skinArr, &skinCount)) { IPALog([NSString stringWithFormat: - @"[UNLOCK-CHAR] updatedCharacterSkinList count=%d", skinCount]); + @"[UNLOCK-CHAR] refresh: list=updatedCharacterSkinList count=%d", skinCount]); int32_t skinTotal = 0; int32_t skinFlip = 0; for (int32_t i = 0; i < skinCount; i++) { @@ -185,7 +185,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { int32_t mstCharId = readI32(elem, 0x1C); uint8_t isAcquired = readU8(elem, 0x20); IPALog([NSString stringWithFormat: - @"[UNLOCK-CHAR] skin[%d] mstSkinId=%d mstCharId=%d isAcquired=%d", + @"[UNLOCK-CHAR] refresh: skinIndex=%d mstSkinId=%d mstCharId=%d isAcquired=%d", i, mstSkinId, mstCharId, (int)isAcquired]); if (!isPlausibleMstId(mstSkinId)) continue; @@ -195,10 +195,10 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { if (readI32(elem, 0x30) <= 0) writeI32(elem, 0x30, 1); } IPALog([NSString stringWithFormat: - @"[UNLOCK-CHAR] skins total=%d unlocked=%d", + @"[UNLOCK-CHAR] updated: scope=skins total=%d unlocked=%d", skinTotal, skinFlip]); } else { - IPALog(@"[UNLOCK-CHAR] updatedCharacterSkinList unreadable/empty"); + IPALog(@"[UNLOCK-CHAR] skipped: reason=unreadable list=updatedCharacterSkinList"); } } @@ -216,7 +216,7 @@ static void hook_SyncItemListReply_merge(void *self, void *parseContext) { done:; } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[SyncItemListReply] exception: %@", e]); + @"[SYNC-ITEM-LIST-REPLY] exception: error=%@", e]); } g_inHook = 0; } diff --git a/Hook/Version.m b/Hook/Version.m index 122b4ac..558bccd 100644 --- a/Hook/Version.m +++ b/Hook/Version.m @@ -55,7 +55,7 @@ static void hook_TitleSceneMoveNext(void *sm) { if (ptrLooksValid(newStr)) { *(void *volatile *)((uint8_t *)titleScene + 0x40) = newStr; IPALog([NSString stringWithFormat: - @"[VERSION] _appVersionFormat: \"%@\" -> \"%@\"", + @"[VERSION] applied: field=appVersionFormat oldValue=\"%@\" newValue=\"%@\"", origFormat, newFormat]); } } @@ -63,7 +63,7 @@ static void hook_TitleSceneMoveNext(void *sm) { } } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[VERSION] format patch exception: %@", e]); + @"[VERSION] exception: scope=formatPatch error=%@", e]); } } @@ -76,7 +76,7 @@ void KIOUEditorInstallVersionHook(uintptr_t unityBase) { s_il2cpp_string_new = (il2cpp_string_new_t)dlsym(RTLD_DEFAULT, "il2cpp_string_new"); if (!s_il2cpp_string_new) { - IPALog(@"[VERSION] dlsym(il2cpp_string_new) failed — format patch will NOP"); + IPALog(@"[VERSION] skipped: reason=dlsymFailed symbol=il2cpp_string_new effect=formatPatchNoop"); // Continue and still publish the hook; the body NULL-guards on // s_il2cpp_string_new and falls through to orig. } diff --git a/Hook/VoiceUnlock.m b/Hook/VoiceUnlock.m index f33ce51..4684f96 100644 --- a/Hook/VoiceUnlock.m +++ b/Hook/VoiceUnlock.m @@ -71,7 +71,7 @@ void KIOUEditorInstallVoiceUnlockHook(uintptr_t unityBase) { (void *)hook_VoiceCellModel_get_IsLocked, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_VOICE_CELL_GET_IS_LOCKED, hook_VoiceCellModel_get_IsLocked); IPALog([NSString stringWithFormat: - @"[VOICE] installed: SatisfiesRule orig=%p CellModel.get_IsLocked orig=%p", + @"[VOICE] installed: satisfiesRuleOrig=%p cellModelGetIsLockedOrig=%p", (void *)s_origCharacterVoicePlayer_SatisfiesRule, (void *)s_origVoiceCellModel_get_IsLocked]); } From 68fc26066cb2cb4f63473b3c9af78428f197c011 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 10 Jul 2026 18:45:38 +0000 Subject: [PATCH 16/20] feat(common): expose KIOUEditorAssistNodesLimit accessors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three accessors so the KiouEditor-flavored FrameworkPassthrough hook body can read the user-picked NativeSyncSession NodesLimit preset without reaching directly into the Persistence NSUserDefaults key: * KIOUEditorAssistNodesIndex() — current preset index * KIOUEditorSetAssistNodesIndex(idx) * KIOUEditorAssistNodesLimit() — 0 when the preset is Off, otherwise the raw node ceiling The Persistence side (Sources/KiouEditor/Persistence.m) already ships the seven-slot preset table (Off / 1M / 2M / 3M / 5M / 7M / 10M) and the default (idx 4 → 5M) that matches the previously hardcoded clamp. Co-Authored-By: Claude Opus 4.7 --- Hook/Common.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Hook/Common.h b/Hook/Common.h index ddd6350..adc6403 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -163,6 +163,9 @@ void KIOUEditorSetAssistSkillLevel(int32_t v); int32_t KIOUEditorAssistHashIndex(void); void KIOUEditorSetAssistHashIndex(int32_t idx); int32_t KIOUEditorAssistHashMB(void); +int32_t KIOUEditorAssistNodesIndex(void); +void KIOUEditorSetAssistNodesIndex(int32_t idx); +int32_t KIOUEditorAssistNodesLimit(void); // 0 == disabled // --------------------------------------------------------------------------- // Chinlan slot publish helper. From 1684f12be2e31f8fb7b3c52f4237bccdd088c186 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 10 Jul 2026 20:57:11 +0000 Subject: [PATCH 17/20] feat(match): hook ShogiMatchStreamHandler.SendAsync at 0x5BD00E0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC (id 44, entry slot 39) so consumer tweaks can hook the game's every-3s Heartbeat / JoinQueue / ConnectionFailed / LeaveQueue writes on the matching stream. The immediate consumer is KiouEditor's seat filter, which uses the hook to snapshot the MethodInfo pointer il2cpp passes as arg 3 — its own reject branch calls SendAsync directly and needs a valid MethodInfo to avoid crashing inside the il2cpp method body. - Prologue bytes at 0x5BD00E0: f657bda9 (STP X22,X21,[SP,#-0x30]!). - Bumps ENTRY_SLOT_COUNT 39 -> 40 (well under ENTRY_SLOT_CAPACITY = 48). --- KIOUHook.h | 8 ++++++++ KIOUHook.m | 4 ++++ recipes/common.py | 6 +++++- recipes/v1_0_2.py | 13 +++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/KIOUHook.h b/KIOUHook.h index 74b9069..9e33782 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -135,6 +135,8 @@ enum kiou_hook_id { KIOU_HOOK_ID_MATCH_GET_VALID_FOUND, KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE, + KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT, + KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_ID__COUNT, }; @@ -182,6 +184,8 @@ enum kiou_hook_slot_id { KIOU_HOOK_SLOT_MATCH_GET_VALID_FOUND, KIOU_HOOK_SLOT_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_SLOT_MATCH_STREAM_ARGS_CREATE, + KIOU_HOOK_SLOT_MATCH_START_D3_MOVENEXT, + KIOU_HOOK_SLOT_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_SLOT__COUNT, }; @@ -247,6 +251,8 @@ enum kiou_hook_slot_id { #define KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND 0x5D0A78C #define KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT 0x5D0C408 #define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x5BCF8CC +#define KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT 0x5D0DA8C +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC 0x5BD00E0 // --- Direct-ABI helper RVAs (1.0.2) -------------------------------------- // Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to @@ -331,6 +337,8 @@ extern const char KIOU_HOOK_NAME_MP_PAID_AVAILABLE[]; extern const char KIOU_HOOK_NAME_MATCH_GET_VALID_FOUND[]; extern const char KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT[]; extern const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[]; +extern const char KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT[]; +extern const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC[]; // Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. extern const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[]; extern const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[]; diff --git a/KIOUHook.m b/KIOUHook.m index 8a79ee6..afd9e56 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -61,6 +61,8 @@ const char KIOU_HOOK_NAME_MATCH_GET_VALID_FOUND[] = "match_get_valid_found"; const char KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT[] = "match_receive_timeout_movenext"; const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[] = "match_stream_args_create"; +const char KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT[] = "match_start_d3_movenext"; +const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC[] = "match_stream_handler_send_async"; // Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[] = "nss_set_hash_size_direct"; const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[] = "game_object_get_component"; @@ -122,6 +124,8 @@ { KIOU_HOOK_NAME_MATCH_GET_VALID_FOUND, KIOU_HOOK_ID_MATCH_GET_VALID_FOUND, KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND }, { KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT }, { KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE }, + { KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT, KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT, KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT }, + { KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC }, // Direct-call sites (no chinlan cave / no hook id): { KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, -1, KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC }, { KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT }, diff --git a/recipes/common.py b/recipes/common.py index 8eefade..6d5afd0 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -102,6 +102,8 @@ "KIOU_HOOK_ID_MATCH_GET_VALID_FOUND": 40, "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT": 41, "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 42, + "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT": 43, + "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 44, } # Entry slot indices — one per CAVE_ENTRY row, must mirror KIOUHook.h. @@ -147,9 +149,11 @@ "KIOU_HOOK_ID_MATCH_GET_VALID_FOUND": 35, "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT": 36, "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 37, + "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT": 38, + "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 39, } -ENTRY_SLOT_COUNT = 38 +ENTRY_SLOT_COUNT = 40 ENTRY_SLOT_CAPACITY = 48 # reserved sibling room for future entry hooks # --------------------------------------------------------------------------- diff --git a/recipes/v1_0_2.py b/recipes/v1_0_2.py index 069a225..20d43ae 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -103,5 +103,18 @@ (0x5D0A78C, "ff0301d1", "KIOU_HOOK_ID_MATCH_GET_VALID_FOUND", CAVE_ENTRY, "MatchingHandler.GetValidMatchFoundStatus"), (0x5D0C408, "ff0303d1", "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT", CAVE_ENTRY, "MatchingHandler+d__6.MoveNext"), (0x5BCF8CC, "fc6fbaa9", "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE", CAVE_ENTRY, "IShogiMatchStreamArgs.Create"), + # d__3 wraps the caller state around StartMatchingAsyncInternal — we + # snapshot its <>8__1 (DisplayClass3_0) pointer so the seat-filter + # reject branch can Cancel() its matchingCts and let the game's own + # TryLeaveQueueAsync unwind the popup cleanly. + (0x5D0DA8C, "ffc305d1", "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT", CAVE_ENTRY, "MatchingHandler+d__3.MoveNext"), + # ShogiMatchStreamHandler.SendAsync — every outgoing frame the game + # writes to the matching stream (Heartbeat every 3 s, JoinQueue, + # LeaveQueue, ConnectionFailed) flows through this call. We hook the + # entry so we can (a) log every outbound frame and (b) capture the + # MethodInfo argument (x2) into a global so the seat-filter reject + # branch can call SendAsync directly with a valid MethodInfo instead + # of NULL (which crashes the il2cpp method body). + (0x5BD00E0, "f657bda9", "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC", CAVE_ENTRY, "ShogiMatchStreamHandler.SendAsync"), ] # fmt: on From 5a4c4ace8beafcf8438e6927e18b5b630b9821fd Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 10 Jul 2026 21:24:21 +0000 Subject: [PATCH 18/20] feat(observe): catalog universal gRPC wire-log sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add four Google.Protobuf serialize/parse bottlenecks so consumer tweaks can hook every gRPC request and reply from a single installer. Sites verified against IngameService.__Helper_SerializeMessage / DeserializeMessage BL disassembly (1.0.2): outbound: ToByteArray (0x52C071C) or WriteTo(IBufferWriter) (0x52C0DB8) inbound : MergeFrom(msg, ROSeq, bool, reg) (0x52C042C); MergeFrom(CIS) (0x52C1B18) as a residual-path backstop Adds IDs 45-48 + entry slots 40-43 (ENTRY_SLOT_COUNT 40->44), the four KIOU_HOOK_NAME_MSG_* string constants, RVA macros, and catalog rows. No hook body — consumers publish their own. --- KIOUHook.h | 22 ++++++++++++++++++++++ KIOUHook.m | 10 ++++++++++ recipes/common.py | 16 +++++++++++++++- recipes/v1_0_2.py | 24 ++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/KIOUHook.h b/KIOUHook.h index 9e33782..3884100 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -137,6 +137,13 @@ enum kiou_hook_id { KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT, KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC, + // Universal gRPC wire logger — every protobuf serialize/parse call + // in grpc-dotnet routes through one of these four bottlenecks + // (Google.Protobuf, verified against IngameService serializer BLs). + KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY, + KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER, + KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ, + KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED, KIOU_HOOK_ID__COUNT, }; @@ -186,6 +193,11 @@ enum kiou_hook_slot_id { KIOU_HOOK_SLOT_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_SLOT_MATCH_START_D3_MOVENEXT, KIOU_HOOK_SLOT_MATCH_STREAM_HANDLER_SEND_ASYNC, + // Universal gRPC wire logger slots (Google.Protobuf bottlenecks). + KIOU_HOOK_SLOT_MSG_EXT_TO_BYTE_ARRAY, + KIOU_HOOK_SLOT_MSG_EXT_WRITE_TO_BUFFER, + KIOU_HOOK_SLOT_MSG_EXT_MERGE_FROM_ROSEQ, + KIOU_HOOK_SLOT_MSG_PARSER_MERGE_FROM_CODED, KIOU_HOOK_SLOT__COUNT, }; @@ -253,6 +265,11 @@ enum kiou_hook_slot_id { #define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x5BCF8CC #define KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT 0x5D0DA8C #define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC 0x5BD00E0 +// Universal gRPC wire logger (Google.Protobuf bottlenecks, 1.0.2 RVAs). +#define KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY 0x52C071C +#define KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER 0x52C0DB8 +#define KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ 0x52C042C +#define KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED 0x52C1B18 // --- Direct-ABI helper RVAs (1.0.2) -------------------------------------- // Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to @@ -339,6 +356,11 @@ extern const char KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT[]; extern const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[]; extern const char KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT[]; extern const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC[]; +// Universal gRPC wire logger. +extern const char KIOU_HOOK_NAME_MSG_EXT_TO_BYTE_ARRAY[]; +extern const char KIOU_HOOK_NAME_MSG_EXT_WRITE_TO_BUFFER[]; +extern const char KIOU_HOOK_NAME_MSG_EXT_MERGE_FROM_ROSEQ[]; +extern const char KIOU_HOOK_NAME_MSG_PARSER_MERGE_FROM_CODED[]; // Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. extern const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[]; extern const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[]; diff --git a/KIOUHook.m b/KIOUHook.m index afd9e56..2312159 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -63,6 +63,11 @@ const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[] = "match_stream_args_create"; const char KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT[] = "match_start_d3_movenext"; const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC[] = "match_stream_handler_send_async"; +// Universal gRPC wire logger — Google.Protobuf serialize/parse bottlenecks. +const char KIOU_HOOK_NAME_MSG_EXT_TO_BYTE_ARRAY[] = "msg_ext_to_byte_array"; +const char KIOU_HOOK_NAME_MSG_EXT_WRITE_TO_BUFFER[] = "msg_ext_write_to_buffer"; +const char KIOU_HOOK_NAME_MSG_EXT_MERGE_FROM_ROSEQ[] = "msg_ext_merge_from_roseq"; +const char KIOU_HOOK_NAME_MSG_PARSER_MERGE_FROM_CODED[] = "msg_parser_merge_from_coded"; // Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[] = "nss_set_hash_size_direct"; const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[] = "game_object_get_component"; @@ -126,6 +131,11 @@ { KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE }, { KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT, KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT, KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT }, { KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC }, + // Universal gRPC wire logger — Google.Protobuf serialize/parse bottlenecks. + { KIOU_HOOK_NAME_MSG_EXT_TO_BYTE_ARRAY, KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY, KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY }, + { KIOU_HOOK_NAME_MSG_EXT_WRITE_TO_BUFFER, KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER, KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER }, + { KIOU_HOOK_NAME_MSG_EXT_MERGE_FROM_ROSEQ, KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ, KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ }, + { KIOU_HOOK_NAME_MSG_PARSER_MERGE_FROM_CODED, KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED, KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED }, // Direct-call sites (no chinlan cave / no hook id): { KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, -1, KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC }, { KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT }, diff --git a/recipes/common.py b/recipes/common.py index 6d5afd0..4e2ea0f 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -104,6 +104,16 @@ "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 42, "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT": 43, "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 44, + # Universal gRPC wire logger — every protobuf request/response passes + # through one of these four Google.Protobuf bottlenecks. Verified + # against IngameService.__Helper_SerializeMessage / DeserializeMessage + # (BL disassembly, 1.0.2): grpc-dotnet on this app uses ToByteArray OR + # WriteTo(IBufferWriter) outbound, and MergeFrom(msg, ROSeq, + # bool, reg) inbound. MergeFrom(CIS) covers other CIS-based paths. + "KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY": 45, + "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER": 46, + "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ": 47, + "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED": 48, } # Entry slot indices — one per CAVE_ENTRY row, must mirror KIOUHook.h. @@ -151,9 +161,13 @@ "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 37, "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT": 38, "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 39, + "KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY": 40, + "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER": 41, + "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ": 42, + "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED": 43, } -ENTRY_SLOT_COUNT = 40 +ENTRY_SLOT_COUNT = 44 ENTRY_SLOT_CAPACITY = 48 # reserved sibling room for future entry hooks # --------------------------------------------------------------------------- diff --git a/recipes/v1_0_2.py b/recipes/v1_0_2.py index 20d43ae..a03b911 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -116,5 +116,29 @@ # branch can call SendAsync directly with a valid MethodInfo instead # of NULL (which crashes the il2cpp method body). (0x5BD00E0, "f657bda9", "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC", CAVE_ENTRY, "ShogiMatchStreamHandler.SendAsync"), + + # --- Universal gRPC wire logger (protobuf serialize/parse bottlenecks) --- + # Sites verified against IngameService.__Helper_SerializeMessage + # (0x5C75918) and IngameService.__Helper_DeserializeMessage + # (0x2465FC4) BL disassembly on 1.0.2: + # + # Serialize path : ToByteArray (0x52C071C) OR + # WriteTo(IBufferWriter) (0x52C0DB8) + # Deserialize path : MessageParser.ParseFrom(ROSeq) + # → MessageExtensions.MergeFrom(msg, ROSeq, + # bool, ExtensionRegistry) (0x52C042C) + # + # The stream / byte[] variants (0x52C08E0, 0x52C185C) never fire on + # the KIOU gRPC path, so we skip them. MergeFrom(CIS) (0x52C1B18) + # covers any residual CIS-based path (nested submessage parses fall + # under it too, giving a coverage backstop). + # + # Prologues extracted from assets/1.0.2/Kiou-1.0.2.ipa UnityFramework + # on 2026-07-10; none are PC-relative so the first-4-byte relocation + # into the cave tail is safe. + (0x52C071C, "f657bda9", "KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY", CAVE_ENTRY, "MessageExtensions.ToByteArray"), + (0x52C0DB8, "ff0302d1", "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER", CAVE_ENTRY, "MessageExtensions.WriteTo(IBufferWriter)"), + (0x52C042C, "ff4304d1", "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ", CAVE_ENTRY, "MessageExtensions.MergeFrom(IMessage, ROSeq, bool, ExtensionRegistry)"), + (0x52C1B18, "ffc301d1", "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED", CAVE_ENTRY, "MessageParser.MergeFrom(CodedInputStream)"), ] # fmt: on From 79699c0adbf3e9cff178fa77c8a6f627ca17dc2d Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 31 Jul 2026 10:59:29 +0000 Subject: [PATCH 19/20] feat(hook): add NativeSyncSession Search* and SetOption hook sites BSE.EvaluateAsync fans out over legal candidates via SearchMulti / SearchMultiWithPV rather than the single-position SearchFull that FrameworkPassthrough already covers, so engine invocations were invisible to the logger. Adds 6 CAVE_ENTRY sites (Search, SearchMulti, SearchMultiPV, SearchMultiWithPV, SearchMultiPVWithPV, SetOption) plus ShogiMatchStreamHandler.DisposeAsync, with matching enum/name/catalog rows and recipe entries for 1.0.1 and 1.0.2. Also caches the (session, hashMB) tuple in hook_BSE_ensureInit so a 256 MB transposition table is not re-zeroed on every move, exposes a MultiPV cap accessor, and forwards the BSE evalPath to the consumer for USI option dumps. Co-Authored-By: Claude Opus 4.8 (1M context) --- Hook/AssistTune.m | 27 +++++++++++++++++++++++++-- Hook/Common.h | 9 +++++++++ KIOUHook.h | 39 +++++++++++++++++++++++++++++++++++++++ KIOUHook.m | 20 ++++++++++++++++++-- recipes/common.py | 30 ++++++++++++++++++++++++++---- recipes/v1_0_1.py | 9 +++++++++ recipes/v1_0_2.py | 31 +++++++++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 8 deletions(-) diff --git a/Hook/AssistTune.m b/Hook/AssistTune.m index 297cf78..0038cc9 100644 --- a/Hook/AssistTune.m +++ b/Hook/AssistTune.m @@ -45,6 +45,10 @@ static void hook_BSE_ctor(void *self, void *evalPath, void *settings) { if (s_origBSE_ctor) { s_origBSE_ctor(self, evalPath, settings); } + // Feed the consumer the raw evalPath il2cpp String* so it can spin up + // a diagnostic USI session against the same nn weights. Consumer-side + // guard ensures the dump only fires once per boot. + KIOUEditorNotifyBseEvalPath(evalPath); // Tune evaluator parameters regardless of ASSIST_ENABLE; the user // controls the engaged hint arrow via that flag in Hook/AssistEnable. if (!ptrLooksValid(self)) return; @@ -68,6 +72,14 @@ static void hook_BSE_ctor(void *self, void *evalPath, void *settings) { } } +// Cache the (session, mb) tuple we last programmed so EnsureInitializedLocked +// firing on every EvaluateAsync doesn't re-zero the transposition table each +// move. Reallocating 256 MB per move stalls the render loop far more than the +// search itself. When session ptr rolls over (new BSE instance) or the user +// changes the hashMB setting, we reprogram; otherwise skip. +static void * s_lastSizedSession = NULL; +static int32_t s_lastSizedMB = 0; + static void hook_BSE_ensureInit(void *self) { if (s_origBSE_ensureInit) { s_origBSE_ensureInit(self); @@ -80,16 +92,23 @@ static void hook_BSE_ensureInit(void *self) { // Nothing to size; let the next EvaluateAsync retry. return; } + int32_t mb = KIOUEditorAssistHashMB(); + if (session == s_lastSizedSession && mb == s_lastSizedMB) { + // Already programmed on this session at this MB — skip. Prevents + // per-move TT re-zero storms flagged in FrameworkPassthrough logs. + return; + } uintptr_t setHashAddr = KIOUHookSiteAddr( KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, g_unityBaseForAssist); if (setHashAddr == 0) { IPALog(@"[ASSIST-TUNE] skipped: reason=setHashSizeSiteUnresolved"); return; } - int32_t mb = KIOUEditorAssistHashMB(); NSS_SetHashSize_directABI_t setHash = (NSS_SetHashSize_directABI_t)setHashAddr; setHash(session, mb, NULL); + s_lastSizedSession = session; + s_lastSizedMB = mb; IPALog([NSString stringWithFormat: @"[ASSIST-TUNE] applied: scope=ensureInitializedLocked hashSizeMB=%d session=%p", mb, session]); @@ -104,7 +123,11 @@ static void hook_BSE_ensureInit(void *self) { // so surrounding lifecycle code is unaffected; only the expensive search // path is suppressed, which quiets the CPU and prevents device heating. static void hook_BSE_evaluate_async(void *self, void *position, void *methodInfo) { - if (!KIOUEditorFeatureEnabled(KIOU_FEATURE_INGAME_ANALYSIS)) { + BOOL gate = KIOUEditorFeatureEnabled(KIOU_FEATURE_INGAME_ANALYSIS); + IPALog([NSString stringWithFormat: + @"[BSE] evaluate: enter gate=%d self=%p position=%p origResolved=%d", + (int)gate, self, position, s_origBSE_evaluateAsync != NULL]); + if (!gate) { return; } if (s_origBSE_evaluateAsync) { diff --git a/Hook/Common.h b/Hook/Common.h index adc6403..a49dca9 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -166,6 +166,15 @@ int32_t KIOUEditorAssistHashMB(void); int32_t KIOUEditorAssistNodesIndex(void); void KIOUEditorSetAssistNodesIndex(int32_t idx); int32_t KIOUEditorAssistNodesLimit(void); // 0 == disabled +int32_t KIOUEditorAssistMultiPvCap(void); // 0 == pass-through, N>0 forces top-N +void KIOUEditorSetAssistMultiPvCap(int32_t v); + +// Called from Hook/AssistTune.m hook_BSE_ctor with the raw evalPath il2cpp +// String* the game passed to BSE. Consumer captures it, decodes to UTF-8, +// and (once per boot) kicks off an asynchronous `usi`-command diagnostic +// dump against Rshogi's USI C ABI so the built-in option defaults become +// observable in the log. +void KIOUEditorNotifyBseEvalPath(void *evalPathIl2cppStr); // --------------------------------------------------------------------------- // Chinlan slot publish helper. diff --git a/KIOUHook.h b/KIOUHook.h index 3884100..7ef4dff 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -144,6 +144,19 @@ enum kiou_hook_id { KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER, KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ, KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED, + // Appended after MSG_* so numeric IDs stay stable — must match + // HOOK_IDS in vendor/KIOU-Hook/recipes/common.py. + KIOU_HOOK_ID_MATCH_STREAM_HANDLER_DISPOSE_ASYNC, + // NativeSyncSession Search* variants — BSE's real search path fans + // out over legal candidates via SearchMulti / SearchMultiWithPV + // (SearchFull is only used for one-shot single-position queries). + // Appended at the end so numeric IDs stay stable. + KIOU_HOOK_ID_NSS_SEARCH, + KIOU_HOOK_ID_NSS_SEARCHMULTI, + KIOU_HOOK_ID_NSS_SEARCHMULTIPV, + KIOU_HOOK_ID_NSS_SEARCHMULTIWITHPV, + KIOU_HOOK_ID_NSS_SEARCHMULTIPVWITHPV, + KIOU_HOOK_ID_NSS_SETOPTION, KIOU_HOOK_ID__COUNT, }; @@ -198,6 +211,16 @@ enum kiou_hook_slot_id { KIOU_HOOK_SLOT_MSG_EXT_WRITE_TO_BUFFER, KIOU_HOOK_SLOT_MSG_EXT_MERGE_FROM_ROSEQ, KIOU_HOOK_SLOT_MSG_PARSER_MERGE_FROM_CODED, + // Appended after MSG_* so slot indices stay stable — must match + // ENTRY_SLOT_INDEX in vendor/KIOU-Hook/recipes/common.py. + KIOU_HOOK_SLOT_MATCH_STREAM_HANDLER_DISPOSE_ASYNC, + // NativeSyncSession Search* variants — see enum kiou_hook_id above. + KIOU_HOOK_SLOT_NSS_SEARCH, + KIOU_HOOK_SLOT_NSS_SEARCHMULTI, + KIOU_HOOK_SLOT_NSS_SEARCHMULTIPV, + KIOU_HOOK_SLOT_NSS_SEARCHMULTIWITHPV, + KIOU_HOOK_SLOT_NSS_SEARCHMULTIPVWITHPV, + KIOU_HOOK_SLOT_NSS_SETOPTION, KIOU_HOOK_SLOT__COUNT, }; @@ -265,11 +288,19 @@ enum kiou_hook_slot_id { #define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x5BCF8CC #define KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT 0x5D0DA8C #define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC 0x5BD00E0 +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_DISPOSE_ASYNC 0x5BCFEF4 // Universal gRPC wire logger (Google.Protobuf bottlenecks, 1.0.2 RVAs). #define KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY 0x52C071C #define KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER 0x52C0DB8 #define KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ 0x52C042C #define KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED 0x52C1B18 +// NativeSyncSession Search* variants (1.0.2 RVAs). +#define KIOU_HOOK_RVA_NSS_SEARCH 0x5D37A50 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTI 0x5D383A4 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPV 0x5D390A0 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIWITHPV 0x5D3960C +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPVWITHPV 0x5D3A38C +#define KIOU_HOOK_RVA_NSS_SETOPTION 0x5D37694 // --- Direct-ABI helper RVAs (1.0.2) -------------------------------------- // Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to @@ -356,11 +387,19 @@ extern const char KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT[]; extern const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[]; extern const char KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT[]; extern const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC[]; +extern const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_DISPOSE_ASYNC[]; // Universal gRPC wire logger. extern const char KIOU_HOOK_NAME_MSG_EXT_TO_BYTE_ARRAY[]; extern const char KIOU_HOOK_NAME_MSG_EXT_WRITE_TO_BUFFER[]; extern const char KIOU_HOOK_NAME_MSG_EXT_MERGE_FROM_ROSEQ[]; extern const char KIOU_HOOK_NAME_MSG_PARSER_MERGE_FROM_CODED[]; +// NativeSyncSession Search* variants. +extern const char KIOU_HOOK_NAME_NSS_SEARCH[]; +extern const char KIOU_HOOK_NAME_NSS_SEARCHMULTI[]; +extern const char KIOU_HOOK_NAME_NSS_SEARCHMULTIPV[]; +extern const char KIOU_HOOK_NAME_NSS_SEARCHMULTIWITHPV[]; +extern const char KIOU_HOOK_NAME_NSS_SEARCHMULTIPVWITHPV[]; +extern const char KIOU_HOOK_NAME_NSS_SETOPTION[]; // Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. extern const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[]; extern const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[]; diff --git a/KIOUHook.m b/KIOUHook.m index 2312159..b81bc10 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -62,12 +62,20 @@ const char KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT[] = "match_receive_timeout_movenext"; const char KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE[] = "match_stream_args_create"; const char KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT[] = "match_start_d3_movenext"; -const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC[] = "match_stream_handler_send_async"; +const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC[] = "match_stream_handler_send_async"; +const char KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_DISPOSE_ASYNC[] = "match_stream_handler_dispose_async"; // Universal gRPC wire logger — Google.Protobuf serialize/parse bottlenecks. const char KIOU_HOOK_NAME_MSG_EXT_TO_BYTE_ARRAY[] = "msg_ext_to_byte_array"; const char KIOU_HOOK_NAME_MSG_EXT_WRITE_TO_BUFFER[] = "msg_ext_write_to_buffer"; const char KIOU_HOOK_NAME_MSG_EXT_MERGE_FROM_ROSEQ[] = "msg_ext_merge_from_roseq"; const char KIOU_HOOK_NAME_MSG_PARSER_MERGE_FROM_CODED[] = "msg_parser_merge_from_coded"; +// NativeSyncSession Search* variants. +const char KIOU_HOOK_NAME_NSS_SEARCH[] = "nss_search"; +const char KIOU_HOOK_NAME_NSS_SEARCHMULTI[] = "nss_search_multi"; +const char KIOU_HOOK_NAME_NSS_SEARCHMULTIPV[] = "nss_search_multipv"; +const char KIOU_HOOK_NAME_NSS_SEARCHMULTIWITHPV[] = "nss_search_multi_with_pv"; +const char KIOU_HOOK_NAME_NSS_SEARCHMULTIPVWITHPV[] = "nss_search_multipv_with_pv"; +const char KIOU_HOOK_NAME_NSS_SETOPTION[] = "nss_set_option"; // Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[] = "nss_set_hash_size_direct"; const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[] = "game_object_get_component"; @@ -130,12 +138,20 @@ { KIOU_HOOK_NAME_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT, KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT }, { KIOU_HOOK_NAME_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE, KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE }, { KIOU_HOOK_NAME_MATCH_START_D3_MOVENEXT, KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT, KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT }, - { KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC }, + { KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC, KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC }, + { KIOU_HOOK_NAME_MATCH_STREAM_HANDLER_DISPOSE_ASYNC, KIOU_HOOK_ID_MATCH_STREAM_HANDLER_DISPOSE_ASYNC, KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_DISPOSE_ASYNC }, // Universal gRPC wire logger — Google.Protobuf serialize/parse bottlenecks. { KIOU_HOOK_NAME_MSG_EXT_TO_BYTE_ARRAY, KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY, KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY }, { KIOU_HOOK_NAME_MSG_EXT_WRITE_TO_BUFFER, KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER, KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER }, { KIOU_HOOK_NAME_MSG_EXT_MERGE_FROM_ROSEQ, KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ, KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ }, { KIOU_HOOK_NAME_MSG_PARSER_MERGE_FROM_CODED, KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED, KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED }, + // NativeSyncSession Search* variants: + { KIOU_HOOK_NAME_NSS_SEARCH, KIOU_HOOK_ID_NSS_SEARCH, KIOU_HOOK_RVA_NSS_SEARCH }, + { KIOU_HOOK_NAME_NSS_SEARCHMULTI, KIOU_HOOK_ID_NSS_SEARCHMULTI, KIOU_HOOK_RVA_NSS_SEARCHMULTI }, + { KIOU_HOOK_NAME_NSS_SEARCHMULTIPV, KIOU_HOOK_ID_NSS_SEARCHMULTIPV, KIOU_HOOK_RVA_NSS_SEARCHMULTIPV }, + { KIOU_HOOK_NAME_NSS_SEARCHMULTIWITHPV, KIOU_HOOK_ID_NSS_SEARCHMULTIWITHPV, KIOU_HOOK_RVA_NSS_SEARCHMULTIWITHPV }, + { KIOU_HOOK_NAME_NSS_SEARCHMULTIPVWITHPV, KIOU_HOOK_ID_NSS_SEARCHMULTIPVWITHPV, KIOU_HOOK_RVA_NSS_SEARCHMULTIPVWITHPV }, + { KIOU_HOOK_NAME_NSS_SETOPTION, KIOU_HOOK_ID_NSS_SETOPTION, KIOU_HOOK_RVA_NSS_SETOPTION }, // Direct-call sites (no chinlan cave / no hook id): { KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, -1, KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC }, { KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT }, diff --git a/recipes/common.py b/recipes/common.py index 4e2ea0f..863b519 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -103,7 +103,8 @@ "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT": 41, "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 42, "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT": 43, - "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 44, + "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 44, + "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_DISPOSE_ASYNC": 49, # Universal gRPC wire logger — every protobuf request/response passes # through one of these four Google.Protobuf bottlenecks. Verified # against IngameService.__Helper_SerializeMessage / DeserializeMessage @@ -114,6 +115,20 @@ "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER": 46, "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ": 47, "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED": 48, + # NativeSyncSession Search* variants (BSE's real search path — SearchFull + # is only the single-position API). SITES position must match HOOK_ID + # because ChinlanDispatcher.bypassEntryForHook(id) does cave_start + + # id*cave_size; new rows always go at the end. + "KIOU_HOOK_ID_NSS_SEARCH": 50, + "KIOU_HOOK_ID_NSS_SEARCHMULTI": 51, + "KIOU_HOOK_ID_NSS_SEARCHMULTIPV": 52, + "KIOU_HOOK_ID_NSS_SEARCHMULTIWITHPV": 53, + "KIOU_HOOK_ID_NSS_SEARCHMULTIPVWITHPV": 54, + # NSS.SetOption(name, value). Also called directly from FrameworkPassthrough + # via the raw RVA pointer, so those internal calls will re-enter the hook + # body and produce their own log line — that's intentional (lets us see + # both game-issued and tweak-issued option writes on one timeline). + "KIOU_HOOK_ID_NSS_SETOPTION": 55, } # Entry slot indices — one per CAVE_ENTRY row, must mirror KIOUHook.h. @@ -160,15 +175,22 @@ "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT": 36, "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE": 37, "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT": 38, - "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 39, + "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC": 39, + "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_DISPOSE_ASYNC": 44, "KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY": 40, "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER": 41, "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ": 42, "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED": 43, + "KIOU_HOOK_ID_NSS_SEARCH": 45, + "KIOU_HOOK_ID_NSS_SEARCHMULTI": 46, + "KIOU_HOOK_ID_NSS_SEARCHMULTIPV": 47, + "KIOU_HOOK_ID_NSS_SEARCHMULTIWITHPV": 48, + "KIOU_HOOK_ID_NSS_SEARCHMULTIPVWITHPV": 49, + "KIOU_HOOK_ID_NSS_SETOPTION": 50, } -ENTRY_SLOT_COUNT = 44 -ENTRY_SLOT_CAPACITY = 48 # reserved sibling room for future entry hooks +ENTRY_SLOT_COUNT = 51 +ENTRY_SLOT_CAPACITY = 56 # reserved sibling room for future entry hooks # --------------------------------------------------------------------------- # Cave payload builders diff --git a/recipes/v1_0_1.py b/recipes/v1_0_1.py index 5b4d545..7f2c344 100644 --- a/recipes/v1_0_1.py +++ b/recipes/v1_0_1.py @@ -78,5 +78,14 @@ (0x5B4FDF8, "00c040b9", "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingTicketCount"), (0x5B4BC54, "006040b9", "KIOU_HOOK_ID_MP_FREE_REMAINING", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportFreeRemainingCount"), (0x5B4BC64, "006440b9", "KIOU_HOOK_ID_MP_PAID_AVAILABLE", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportPaidAvailableCount"), + + # --- NativeSyncSession Search* variants (see v1_0_2 for rationale). ------ + # Prologues verified on 2026-07-12 against assets/1.0.1/Kiou-1.0.1.ipa. + (0x5D32154, "ffc300d1", "KIOU_HOOK_ID_NSS_SEARCH", CAVE_ENTRY, "NativeSyncSession.Search"), + (0x5D32AA8, "ffc302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTI", CAVE_ENTRY, "NativeSyncSession.SearchMulti"), + (0x5D337A4, "ff4302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiPV"), + (0x5D33D10, "ff4303d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIWITHPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiWithPV"), + (0x5D34A90, "ff8302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIPVWITHPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiPVWithPV"), + (0x5D31D98, "ff8301d1", "KIOU_HOOK_ID_NSS_SETOPTION", CAVE_ENTRY, "NativeSyncSession.SetOption"), ] # fmt: on diff --git a/recipes/v1_0_2.py b/recipes/v1_0_2.py index a03b911..422fc0b 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -140,5 +140,36 @@ (0x52C0DB8, "ff0302d1", "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER", CAVE_ENTRY, "MessageExtensions.WriteTo(IBufferWriter)"), (0x52C042C, "ff4304d1", "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ", CAVE_ENTRY, "MessageExtensions.MergeFrom(IMessage, ROSeq, bool, ExtensionRegistry)"), (0x52C1B18, "ffc301d1", "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED", CAVE_ENTRY, "MessageParser.MergeFrom(CodedInputStream)"), + + # ShogiMatchStreamHandler.DisposeAsync — appended AFTER the MSG_* rows so + # its position in SITES matches its HOOK_ID (49). ChinlanDispatcher's + # bypassEntryForHook(id) computes `cave_start + id * cave_size` and the + # patcher allocates cave memory in SITES order — the two must agree, so + # new hooks always go at the end. + # + # This is the full-teardown primitive for the matching stream. The + # server only marks the seat as gone when the underlying gRPC HTTP/2 + # duplex call is closed (LeaveQueue frames without a stream close are + # ignored — same match_room_id keeps getting served). We hook the entry + # to capture the MethodInfo so the seat-filter reject branch can invoke + # DisposeAsync directly on the cached handler self. + (0x5BCFEF4, "ff4302d1", "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_DISPOSE_ASYNC", CAVE_ENTRY, "ShogiMatchStreamHandler.DisposeAsync"), + + # --- NativeSyncSession Search* variants --- + # BSE.EvaluateAsync fans out over legal candidates via SearchMulti / + # SearchMultiWithPV, NOT the single-position SearchFull that + # FrameworkPassthrough already covers. Adding these 5 catches every + # engine invocation the game side can issue so the KiouEditor logger + # sees each search's per-move score + PV. + # + # Prologues verified on 2026-07-12 against assets/1.0.2/Kiou-1.0.2.ipa + # UnityFramework. All are `sub sp, sp, #imm` (PC-independent), safe to + # relocate verbatim into the cave tail. + (0x5D37A50, "ffc300d1", "KIOU_HOOK_ID_NSS_SEARCH", CAVE_ENTRY, "NativeSyncSession.Search"), + (0x5D383A4, "ffc302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTI", CAVE_ENTRY, "NativeSyncSession.SearchMulti"), + (0x5D390A0, "ff4302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiPV"), + (0x5D3960C, "ff4303d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIWITHPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiWithPV"), + (0x5D3A38C, "ff8302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIPVWITHPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiPVWithPV"), + (0x5D37694, "ff8301d1", "KIOU_HOOK_ID_NSS_SETOPTION", CAVE_ENTRY, "NativeSyncSession.SetOption"), ] # fmt: on From 863c5fe502ea3baaf779d14e69daef884a6a4729 Mon Sep 17 00:00:00 2001 From: tkgstrator Date: Fri, 31 Jul 2026 21:33:21 +0000 Subject: [PATCH 20/20] feat(recipes): add KIOU 1.1.0 target and per-version RVA headers Adds recipes/v1_1_0.py (56 sites, build 15) and moves the RVA catalog out of KIOUHook.h into generated rva/kiou_rva_.h headers, picked at compile time by KIOU_HOOK_TARGET_BUILD. gen_rva_header.py emits them from a recipe so the two can no longer drift apart by hand. 1.1.0 dropped BeginnerSupportEvaluator.EnsureInitializedLocked, TryBorrowSession and the NativeSyncSession field, and with them the hash-size knob AssistTune applied there. Its row stays as a site = None placeholder so every later hook keeps its cave index, and the hook body compiles out on build >= 15. The BSE ctor also reshaped: <= 1.0.2 carries _analysisDepth / _engineSkillLevel, while >= 1.1.0 carries _normalHintTopN / _firstHintTopN, so AssistTune now writes whichever pair the target build actually has. Co-Authored-By: Claude Opus 4.7 --- Hook/AccountObserve.m | 17 ++- Hook/AssistTune.m | 93 ++++++++++++---- Hook/Common.h | 6 ++ KIOUHook.h | 179 ++++++++++-------------------- KIOUHook.m | 32 +++++- recipes/__init__.py | 3 +- recipes/common.py | 17 ++- recipes/v1_0_2.py | 10 +- recipes/v1_1_0.py | 163 ++++++++++++++++++++++++++++ rva/kiou_rva_1_0_1.h | 91 ++++++++++++++++ rva/kiou_rva_1_0_2.h | 91 ++++++++++++++++ rva/kiou_rva_1_1_0.h | 91 ++++++++++++++++ tools/check_recipes.py | 117 ++++++++++++++++++-- tools/gen_rva_header.py | 233 ++++++++++++++++++++++++++++++++++++++++ 14 files changed, 968 insertions(+), 175 deletions(-) create mode 100644 recipes/v1_1_0.py create mode 100644 rva/kiou_rva_1_0_1.h create mode 100644 rva/kiou_rva_1_0_2.h create mode 100644 rva/kiou_rva_1_1_0.h create mode 100644 tools/gen_rva_header.py diff --git a/Hook/AccountObserve.m b/Hook/AccountObserve.m index 44b2f1f..49431df 100644 --- a/Hook/AccountObserve.m +++ b/Hook/AccountObserve.m @@ -22,11 +22,10 @@ // TitleMenuPopupPresenter.RunDeleteAccountSequenceAsync (raw site, not in catalog) // =========================================================================== -// RunReset / RunDelete are not in the catalog (not binpatched / not part -// of the cave system); keep their RVAs local so the hook installer can -// MSHookFunction them on JB. -#define KF_RVA_RUN_RESET_USER_DATA_SEQ 0x5DCC204 -#define KF_RVA_RUN_DELETE_ACCOUNT_SEQ 0x5DCC2B4 +// RunReset / RunDelete have no cave (they aren't binpatched), so they are +// JB-only MSHookFunction targets. Their addresses still come from the +// catalog — as hook_id = -1 rows — so they track the build's target +// version like everything else. // --------------------------------------------------------------------------- // Field offsets @@ -394,13 +393,12 @@ void KIOUInstallAccountObserveHook(uintptr_t unityBase) { KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_GET_SELF_PROFILE_MOVENEXT, KIOUHookGetSelfProfileMoveNext); #if IPA_CHINLAN - (void)KF_RVA_RUN_RESET_USER_DATA_SEQ; - (void)KF_RVA_RUN_DELETE_ACCOUNT_SEQ; IPALog(@"[ACCOUNT] resolved: variant=chinlan catalogHooks=yes rawRvaHooks=skipped"); #else // RunReset / RunDelete are JB-only sites (no chinlan cave for them). { - uintptr_t addr = unityBase + KF_RVA_RUN_RESET_USER_DATA_SEQ; + uintptr_t addr = KIOUHookSiteAddr( + KIOU_HOOK_NAME_RUN_RESET_USER_DATA_SEQ, unityBase); void *orig = NULL; MSHookFunction((void *)addr, (void *)KIOUHookRunResetUserDataSeq, &orig); s_origRunResetSeq = (RunResetSeq_t)orig; @@ -408,7 +406,8 @@ void KIOUInstallAccountObserveHook(uintptr_t unityBase) { @"[ACCOUNT] hooked: target=RunResetUserDataSeq addr=0x%lx", (unsigned long)addr]); } { - uintptr_t addr = unityBase + KF_RVA_RUN_DELETE_ACCOUNT_SEQ; + uintptr_t addr = KIOUHookSiteAddr( + KIOU_HOOK_NAME_RUN_DELETE_ACCOUNT_SEQ, unityBase); void *orig = NULL; MSHookFunction((void *)addr, (void *)KIOUHookRunDeleteAccountSeq, &orig); s_origRunDeleteAccountSeq = (RunResetSeq_t)orig; diff --git a/Hook/AssistTune.m b/Hook/AssistTune.m index 0038cc9..de76a42 100644 --- a/Hook/AssistTune.m +++ b/Hook/AssistTune.m @@ -6,20 +6,28 @@ // // Migrated from KiouEditor's Sources/KiouEditor/Hook_AssistTune.m. // -// Two hooks: +// The evaluator was rebuilt in KIOU 1.1.0 (build 15). Up to 1.0.2 it ran +// an Rshogi NNUE search, so the knobs were search depth, engine skill +// level, and transposition-table size. From 1.1.0 it runs a policy-model +// inference (PolicyEngine.PolicyTopK) and those three no longer exist — +// what is left is how many candidate moves the policy head returns. // -// A) BSE.ctor — public void .ctor(string evalPath, BeginnerSupportSettings) -// Let orig run (it allocates caches, captures eval path, reads the -// ScriptableObject), then overwrite: -// +0x18 _analysisDepth -> KIOUEditorAssistDepth() (default 16) -// +0x28 _engineSkillLevel -> KIOUEditorAssistSkillLevel() (default 20) +// Hooks, by target: // -// B) BSE.EnsureInitializedLocked — the lazy bring-up that allocates the -// Rshogi NativeSyncSession into _session (+0x38) on the first -// EvaluateAsync. Nothing in the retail path calls -// NativeSyncSession.SetHashSize, so Rshogi runs on its tiny default. -// Piggy-back here: once orig finishes and the session pointer is -// live, invoke SetHashSize(MB) via direct ABI. +// A) BSE.ctor — both eras. Let orig run (it allocates caches, captures +// the model/eval path and reads the ScriptableObject), then widen +// the assist: +// <= 1.0.2 +0x18 _analysisDepth -> KIOUEditorAssistDepth() +// +0x28 _engineSkillLevel -> KIOUEditorAssistSkillLevel() +// >= 1.1.0 +0x18 _normalHintTopN -> KIOUEditorAssistTopN() +// +0x1C _firstHintTopN -> KIOUEditorAssistTopN() +// +// B) BSE.EnsureInitializedLocked — <= 1.0.2 only. The lazy bring-up that +// allocates the NativeSyncSession into _session (+0x38) on the first +// EvaluateAsync. Nothing in the retail path calls SetHashSize, so +// Rshogi ran on its tiny default; we piggy-back here to size the TT +// once the session pointer is live. 1.1.0 removed the method (and the +// session), so there is nothing to hook and no hash size to set. // // The direct SetHashSize call site is looked up via // KIOUHookSiteAddr(KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT) — hook_id=-1 in @@ -27,32 +35,57 @@ // function pointer. // =========================================================================== +#define KIOU_ASSIST_POLICY_ERA (KIOU_HOOK_TARGET_BUILD >= 15) + +#if KIOU_ASSIST_POLICY_ERA +#define OFF_BSE_NORMAL_HINT_TOPN 0x18 +#define OFF_BSE_FIRST_HINT_TOPN 0x1C +#else #define OFF_BSE_ANALYSIS_DEPTH 0x18 #define OFF_BSE_ENGINE_SKILL_LEVEL 0x28 #define OFF_BSE_SESSION 0x38 +#endif -typedef void (*BSECtor_t)(void *self, void *evalPath, void *settings); -typedef void (*BSEEnsureInit_t)(void *self); +typedef void (*BSECtor_t)(void *self, void *modelPath, void *settings); typedef void (*BSEEvaluateAsync_t)(void *self, void *position, void *methodInfo); -typedef void (*NSS_SetHashSize_directABI_t)(void *thisSession, int32_t mb, void *methodInfo); static BSECtor_t s_origBSE_ctor = NULL; -static BSEEnsureInit_t s_origBSE_ensureInit = NULL; static BSEEvaluateAsync_t s_origBSE_evaluateAsync = NULL; static uintptr_t g_unityBaseForAssist = 0; -static void hook_BSE_ctor(void *self, void *evalPath, void *settings) { +#if !KIOU_ASSIST_POLICY_ERA +typedef void (*BSEEnsureInit_t)(void *self); +typedef void (*NSS_SetHashSize_directABI_t)(void *thisSession, int32_t mb, void *methodInfo); + +static BSEEnsureInit_t s_origBSE_ensureInit = NULL; +#endif + +static void hook_BSE_ctor(void *self, void *modelPath, void *settings) { if (s_origBSE_ctor) { - s_origBSE_ctor(self, evalPath, settings); + s_origBSE_ctor(self, modelPath, settings); } - // Feed the consumer the raw evalPath il2cpp String* so it can spin up - // a diagnostic USI session against the same nn weights. Consumer-side - // guard ensures the dump only fires once per boot. - KIOUEditorNotifyBseEvalPath(evalPath); + // Feed the consumer the raw path il2cpp String* so it can spin up a + // diagnostic USI session against the same weights. Consumer-side guard + // ensures the dump only fires once per boot. + KIOUEditorNotifyBseEvalPath(modelPath); // Tune evaluator parameters regardless of ASSIST_ENABLE; the user // controls the engaged hint arrow via that flag in Hook/AssistEnable. if (!ptrLooksValid(self)) return; @try { +#if KIOU_ASSIST_POLICY_ERA + int32_t targetTopN = KIOUEditorAssistTopN(); + int32_t origNormal = readI32(self, OFF_BSE_NORMAL_HINT_TOPN); + int32_t origFirst = readI32(self, OFF_BSE_FIRST_HINT_TOPN); + if (origNormal != targetTopN) { + writeI32(self, OFF_BSE_NORMAL_HINT_TOPN, targetTopN); + } + if (origFirst != targetTopN) { + writeI32(self, OFF_BSE_FIRST_HINT_TOPN, targetTopN); + } + IPALog([NSString stringWithFormat: + @"[ASSIST-TUNE] applied: scope=bseCtor normalTopN=%d->%d firstTopN=%d->%d", + origNormal, targetTopN, origFirst, targetTopN]); +#else int32_t targetDepth = KIOUEditorAssistDepth(); int32_t targetSkill = KIOUEditorAssistSkillLevel(); int32_t origDepth = readI32(self, OFF_BSE_ANALYSIS_DEPTH); @@ -66,12 +99,14 @@ static void hook_BSE_ctor(void *self, void *evalPath, void *settings) { IPALog([NSString stringWithFormat: @"[ASSIST-TUNE] applied: scope=bseCtor depth=%d->%d skillLevel=%d->%d", origDepth, targetDepth, origSkill, targetSkill]); +#endif } @catch (NSException *e) { IPALog([NSString stringWithFormat: @"[ASSIST-TUNE] exception: scope=bseCtor error=%@", e]); } } +#if !KIOU_ASSIST_POLICY_ERA // Cache the (session, mb) tuple we last programmed so EnsureInitializedLocked // firing on every EvaluateAsync doesn't re-zero the transposition table each // move. Reallocating 256 MB per move stalls the render loop far more than the @@ -117,6 +152,7 @@ static void hook_BSE_ensureInit(void *self) { @"[ASSIST-TUNE] exception: scope=ensureInitializedLocked error=%@", e]); } } +#endif // !KIOU_ASSIST_POLICY_ERA // BSE.EvaluateAsync — drop the on-device NNUE evaluation entirely when the // user has KIOU_FEATURE_INGAME_ANALYSIS off. The BSE object stays allocated @@ -141,16 +177,26 @@ void KIOUEditorInstallAssistTuneHook(uintptr_t unityBase) { KIOU_HOOK_NAME_BSE_CTOR, (void *)hook_BSE_ctor, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_CTOR, hook_BSE_ctor); +#if !KIOU_ASSIST_POLICY_ERA s_origBSE_ensureInit = (BSEEnsureInit_t)KIOUHookInstall( KIOU_HOOK_NAME_BSE_ENSURE_INITIALIZED, (void *)hook_BSE_ensureInit, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_ENSURE_INITIALIZED, hook_BSE_ensureInit); +#endif s_origBSE_evaluateAsync = (BSEEvaluateAsync_t)KIOUHookInstall( KIOU_HOOK_NAME_BSE_EVALUATE_ASYNC, (void *)hook_BSE_evaluate_async, unityBase); KIOU_HOOK_PUBLISH_SLOT(unityBase, KIOU_HOOK_SLOT_BSE_EVALUATE_ASYNC, hook_BSE_evaluate_async); +#if KIOU_ASSIST_POLICY_ERA + IPALog([NSString stringWithFormat: + @"[ASSIST-TUNE] installed: engine=policy bseCtorOrig=%p " + @"evaluateAsyncOrig=%p topN=%d ingameAnalysisGate=%d", + (void *)s_origBSE_ctor, (void *)s_origBSE_evaluateAsync, + (int)KIOUEditorAssistTopN(), + (int)KIOUEditorFeatureEnabled(KIOU_FEATURE_INGAME_ANALYSIS)]); +#else IPALog([NSString stringWithFormat: - @"[ASSIST-TUNE] installed: bseCtorOrig=%p ensureInitOrig=%p " + @"[ASSIST-TUNE] installed: engine=nnue bseCtorOrig=%p ensureInitOrig=%p " @"evaluateAsyncOrig=%p depth=%d skill=%d hashMB=%d " @"ingameAnalysisGate=%d", (void *)s_origBSE_ctor, (void *)s_origBSE_ensureInit, @@ -158,4 +204,5 @@ void KIOUEditorInstallAssistTuneHook(uintptr_t unityBase) { (int)KIOUEditorAssistDepth(), (int)KIOUEditorAssistSkillLevel(), (int)KIOUEditorAssistHashMB(), (int)KIOUEditorFeatureEnabled(KIOU_FEATURE_INGAME_ANALYSIS)]); +#endif } diff --git a/Hook/Common.h b/Hook/Common.h index a49dca9..b41b300 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -156,6 +156,12 @@ NSString *KIOUEditorFeatureLabel(KiouFeature f); // applied inside EnsureInitializedLocked once the native session is alive. // --------------------------------------------------------------------------- +// Depth / skill level / hash size drive the NNUE search that KIOU <= 1.0.2 +// used. From 1.1.0 the assist is a policy model with a single knob — how +// many candidate moves the policy head returns — so builds targeting +// build 15 and up use KIOUEditorAssistTopN() instead. +int32_t KIOUEditorAssistTopN(void); +void KIOUEditorSetAssistTopN(int32_t v); int32_t KIOUEditorAssistDepth(void); void KIOUEditorSetAssistDepth(int32_t v); int32_t KIOUEditorAssistSkillLevel(void); diff --git a/KIOUHook.h b/KIOUHook.h index 7ef4dff..a74a2e0 100644 --- a/KIOUHook.h +++ b/KIOUHook.h @@ -10,9 +10,10 @@ // import il2cpp.h / logging.h / chinlan.h explicitly in the .m files that // need them. // -// Pair-of-truth note: the `enum kiou_hook_id` and `KIOU_HOOK_RVA_*` -// macros mirror `recipes/common.py` HOOK_IDS and the per-version SITES -// tables. Update both sides together. +// Pair-of-truth note: `enum kiou_hook_id` mirrors `recipes/common.py` +// HOOK_IDS, and each `rva/kiou_rva_.h` mirrors that version's SITES +// table. Update the recipe first, then regenerate the rva/ header; +// tools/check_recipes.py fails the build when the two drift. // // Hook authors should NOT reference the enum or RVA macros directly. Use // the name-based API below (KIOUHookOrig / KIOUHookInstall / KIOUHookSiteAddr) @@ -35,42 +36,49 @@ #endif // --------------------------------------------------------------------------- -// Cave geometry and dispatcher slot RVAs. -// --------------------------------------------------------------------------- - -// Single observer-dispatcher slot. Every CAVE_OBSERVER cave loads this -// pointer, BLRs it with W6 = hook_id, and routes through dispatch_one. +// Target selection. // -// Placement: sits inside __DATA.__common just past the entry-slot table -// capacity (ENTRY_SLOT_BASE_RVA + ENTRY_SLOT_CAPACITY * 8 = 0x091E93B8). -// The old 0x8F90C80 landed in __DATA.__bss, which UnityRuntime / il2cpp -// overwrites during lazy init — publishing dispatch_one there survived -// startup but got clobbered before the first observer fire, so the cave -// BLR X16 jumped to garbage and crashed with a PC alignment fault. See -// recipes/v1_0_2.py for the __common vs __bss note. +// Every address in this catalog moves on each KIOU build, so the site RVAs +// and the two dispatcher-slot RVAs live in per-version headers under rva/, +// each generated from the matching recipes/v.py. Consumers select one +// by defining KIOU_HOOK_TARGET_BUILD (the target's CFBundleVersion) on the +// compiler command line; KiouEditor's Makefile derives it from +// TARGET_VERSION. The default matches the default recipe. // -// History: this originally sat at 0x091E92B8 (ENTRY_SLOT_CAPACITY = 32). -// Adding the 5 AI-Special-Support caves pushed the count past 32, so -// capacity moved to 40 and the observer slot slid forward by 0x100 to -// 0x091E93B8 — still comfortably inside __common (which ends at -// 0x091F5978 on both 1.0.1 and 1.0.2). -#define KIOU_HOOK_OBSERVER_SLOT_RVA 0x091E93B8 +// The JB / jailed builds resolve through these macros (KIOUHookInstall → +// MSHookFunction); the chinlan build reads the recipe directly. Both sides +// must agree, which is what tools/check_recipes.py enforces. +// --------------------------------------------------------------------------- +#ifndef KIOU_HOOK_TARGET_BUILD +#define KIOU_HOOK_TARGET_BUILD 12 +#endif -// Entry-cave slot table. Slot N at +N*8 holds the function pointer the -// CAVE_ENTRY cave BLRs directly (no dispatcher). -#define KIOU_HOOK_ENTRY_SLOT_BASE_RVA 0x091E91B8 +#if KIOU_HOOK_TARGET_BUILD == 11 +#import "rva/kiou_rva_1_0_1.h" +#elif KIOU_HOOK_TARGET_BUILD == 12 +#import "rva/kiou_rva_1_0_2.h" +#elif KIOU_HOOK_TARGET_BUILD == 15 +#import "rva/kiou_rva_1_1_0.h" +#else +#error "KIOU_HOOK_TARGET_BUILD must be 11 (1.0.1), 12 (1.0.2) or 15 (1.1.0)" +#endif -// Cave payload region (matches recipes/common.py + per-version CAVE_REGION). +// --------------------------------------------------------------------------- +// Cave geometry. // -// Version note: the default here matches KIOU 1.0.2's CAVE_REGION[0] -// (recipes/v1_0_2.py). Consumers that target a different version must -// override the macro on the compiler command line — the 1.0.1 tree -// (recipes/v1_0_1.py) uses 0x8268024 for example. KiouForge targets -// 1.0.2 and can rely on the default; KiouEditor (1.0.1) sets -// -DKIOU_HOOK_CAVE_REGION_START=0x8268024 in its Makefile. -#ifndef KIOU_HOOK_CAVE_REGION_START -#define KIOU_HOOK_CAVE_REGION_START 0x826F5E8 -#endif +// KIOU_HOOK_OBSERVER_SLOT_RVA (the single slot every CAVE_OBSERVER cave +// BLRs through) and KIOU_HOOK_ENTRY_SLOT_BASE_RVA (slot N at +N*8, BLRed +// directly by CAVE_ENTRY caves) come from the header selected above. Both +// deliberately sit in __DATA.__common: the original 1.0.2 placement was in +// __bss, which UnityRuntime / il2cpp overwrites during lazy init, so the +// published pointer survived startup but was clobbered before the first +// observer fire and the cave's BLR X16 jumped to garbage. +// --------------------------------------------------------------------------- + +// Cave payload region. KIOU_HOOK_CAVE_REGION_START defaults to the selected +// version's CAVE_REGION[0] (see the rva/ header); a consumer may still +// override it on the command line. The two must agree — a mismatch sends +// every orig-call trampoline into random code. #define KIOU_HOOK_CAVE_SIZE 84 #define KIOU_HOOK_CAVE_BYPASS_OFFSET (KIOU_HOOK_CAVE_SIZE - 8) @@ -225,96 +233,6 @@ enum kiou_hook_slot_id { KIOU_HOOK_SLOT__COUNT, }; -// --------------------------------------------------------------------------- -// Site RVAs — used by KIOUHookSiteAddr to compute target addresses. -// Hook authors: don't reference these directly. Use KIOUHookSiteAddr(name). -// --------------------------------------------------------------------------- -#define KIOU_HOOK_RVA_SET_TARGET_FRAMERATE 0x6B718A4 -#define KIOU_HOOK_RVA_NSS_SETHASHSIZE 0x5D379DC -#define KIOU_HOOK_RVA_NSS_SETSKILLEVEL 0x5D37968 -#define KIOU_HOOK_RVA_NSS_SEARCHFULL 0x5D37A74 -#define KIOU_HOOK_RVA_AI_END 0x59EA720 -#define KIOU_HOOK_RVA_CPUSTREAM_END 0x59F15D4 -#define KIOU_HOOK_RVA_LOCAL_END 0x5A046B4 -#define KIOU_HOOK_RVA_ONLINE_END 0x5A06158 -#define KIOU_HOOK_RVA_REPLAY_END 0x5A30320 -#define KIOU_HOOK_RVA_ACCOUNT_EXISTS 0x5922CD0 -#define KIOU_HOOK_RVA_LOGIN_ARGS_CREATE 0x5B9DC04 -#define KIOU_HOOK_RVA_REGISTER_USER_ARGS_CREATE 0x5B9DC94 -#define KIOU_HOOK_RVA_RUN_LOGIN_SEQ_MOVENEXT 0x58152BC -#define KIOU_HOOK_RVA_GET_SELF_PROFILE_MOVENEXT 0x5BB99DC -#define KIOU_HOOK_RVA_HTTPMSGINVOKER_SEND_ASYNC 0x6082AC0 -#define KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC 0x5CFC394 -#define KIOU_HOOK_RVA_HEADER_PROVIDER_SET_OR_UPDATE_HEADER 0x5BD9EE8 - -// --- KiouEditor hook sites (1.0.2 RVAs) ---------------------------------- -// KiouEditor targets KIOU 1.0.2 build 12 now that the refactor branch -// realigned it with KiouForge. These values MUST mirror -// recipes/v1_0_2.py SITES — the chinlan pipeline reads that recipe -// directly, but the JB / jailed pipeline goes through -// KIOUHookInstall → MSHookFunction with the RVA below. If they drift, -// the JB / jailed hook lands on the wrong address and corrupts a -// neighbour method (verified on-device: 1.0.1 TITLE_SCENE_MOVENEXT -// = 0x5DCC728 lands inside TitleMenuPopupPresenter on 1.0.2, which -// then crashed with a wild PC on the first popup). -#define KIOU_HOOK_RVA_SYNC_ITEM_LIST_MERGE 0x5C3C29C -#define KIOU_HOOK_RVA_COLLECTION_PRESET_MERGE 0x5C458C4 -#define KIOU_HOOK_RVA_SELECT_CHAR_ASYNC 0x5CACEF8 -#define KIOU_HOOK_RVA_SELECT_CHAR_REPLY_MERGE 0x5C2C034 -#define KIOU_HOOK_RVA_MATCHING_PLAYER_MERGE 0x5B51C3C -#define KIOU_HOOK_RVA_HISTORY_DETAIL_MERGE 0x5C06590 -#define KIOU_HOOK_RVA_HISTORY_GET_PREMIUM 0x5C05FF0 -#define KIOU_HOOK_RVA_KIFU_DETAIL_IS_PREMIUM 0x585E000 -#define KIOU_HOOK_RVA_VOICE_PLAYER_SATISFIES 0x582E614 -#define KIOU_HOOK_RVA_VOICE_CELL_GET_IS_LOCKED 0x584DB64 -#define KIOU_HOOK_RVA_BSE_CTOR 0x597E608 -#define KIOU_HOOK_RVA_BSE_ENSURE_INITIALIZED 0x5980890 -#define KIOU_HOOK_RVA_RBSUPPORT_GET_ENABLED 0x5942AA0 -#define KIOU_HOOK_RVA_RBSUPPORT_GET_DEPTH 0x5942AC0 -#define KIOU_HOOK_RVA_HOME_UTILITY_PRESENTER_CTOR 0x5AA4054 -#define KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD7F54 -#define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x5DD2874 -#define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x594A034 -#define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x5980304 -// 棋桜覚醒 (AI Special Support) UI-unlock caves. 1.0.2 RVAs. -#define KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL 0x5B54F68 -#define KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING 0x5B54F38 -#define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x5B54F48 -#define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x5B50DA4 -#define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x5B50DB4 -// Matching-seat filter (1.0.2 RVAs). Ported from KiouEngineBridge Hook_MatchingFilterObserve. -#define KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND 0x5D0A78C -#define KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT 0x5D0C408 -#define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x5BCF8CC -#define KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT 0x5D0DA8C -#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC 0x5BD00E0 -#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_DISPOSE_ASYNC 0x5BCFEF4 -// Universal gRPC wire logger (Google.Protobuf bottlenecks, 1.0.2 RVAs). -#define KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY 0x52C071C -#define KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER 0x52C0DB8 -#define KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ 0x52C042C -#define KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED 0x52C1B18 -// NativeSyncSession Search* variants (1.0.2 RVAs). -#define KIOU_HOOK_RVA_NSS_SEARCH 0x5D37A50 -#define KIOU_HOOK_RVA_NSS_SEARCHMULTI 0x5D383A4 -#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPV 0x5D390A0 -#define KIOU_HOOK_RVA_NSS_SEARCHMULTIWITHPV 0x5D3960C -#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPVWITHPV 0x5D3A38C -#define KIOU_HOOK_RVA_NSS_SETOPTION 0x5D37694 - -// --- Direct-ABI helper RVAs (1.0.2) -------------------------------------- -// Not hook sites; KiouEditor bodies look these up via KIOUHookSiteAddr to -// call the underlying functions directly. Now aligned with the 1.0.2 -// binary the KiouEditor tweak targets. NSS_SETHASHSIZE_DIRECT and -// NSS_SETHASHSIZE resolve to the same underlying method on 1.0.2 -// (0x5D379DC) — the DIRECT name is retained because Hook/AssistTune.m -// looks it up under that catalog entry to invoke SetHashSize -// verbatim (not as a redirected hook). Catalog rows for these still -// have hook_id = -1 so KIOUHookInstall won't try to MSHookFunction them. -#define KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT 0x5D379DC -#define KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT 0x6BD07F8 -#define KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT 0x6F2628C - // --------------------------------------------------------------------------- // Dispatcher state — defined by the consumer's ChinlanDispatcher.m. // --------------------------------------------------------------------------- @@ -400,10 +318,21 @@ extern const char KIOU_HOOK_NAME_NSS_SEARCHMULTIPV[]; extern const char KIOU_HOOK_NAME_NSS_SEARCHMULTIWITHPV[]; extern const char KIOU_HOOK_NAME_NSS_SEARCHMULTIPVWITHPV[]; extern const char KIOU_HOOK_NAME_NSS_SETOPTION[]; -// Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. +// Direct-ABI helper lookups — resolved via KIOUHookSiteAddr and called +// verbatim, never hooked. hook_id = -1 in the catalog. extern const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[]; +extern const char KIOU_HOOK_NAME_NSS_SETOPTION_DIRECT[]; extern const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[]; extern const char KIOU_HOOK_NAME_RTU_WORLDTOSCREENPOINT[]; +extern const char KIOU_HOOK_NAME_JSON_FORMATTER_GET_DEFAULT[]; +extern const char KIOU_HOOK_NAME_JSON_FORMATTER_FORMAT[]; +extern const char KIOU_HOOK_NAME_GAMECTRL_GET_USI_TEXT[]; +extern const char KIOU_HOOK_NAME_POSITION_TO_SFEN[]; +extern const char KIOU_HOOK_NAME_USIPARSER_PARSE_USI[]; +extern const char KIOU_HOOK_NAME_KIFWRITEOPTIONS_CTOR[]; +extern const char KIOU_HOOK_NAME_KIFWRITER_WRITE[]; +extern const char KIOU_HOOK_NAME_RUN_RESET_USER_DATA_SEQ[]; +extern const char KIOU_HOOK_NAME_RUN_DELETE_ACCOUNT_SEQ[]; // Resolve the orig function pointer for a hook by symbolic name. // diff --git a/KIOUHook.m b/KIOUHook.m index b81bc10..90673b4 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -76,10 +76,21 @@ const char KIOU_HOOK_NAME_NSS_SEARCHMULTIWITHPV[] = "nss_search_multi_with_pv"; const char KIOU_HOOK_NAME_NSS_SEARCHMULTIPVWITHPV[] = "nss_search_multipv_with_pv"; const char KIOU_HOOK_NAME_NSS_SETOPTION[] = "nss_set_option"; -// Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. +// Direct-ABI helpers — invoked verbatim via KIOUHookSiteAddr rather than +// hooked, so they carry hook_id = -1 in the catalog and have no recipe row. const char KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT[] = "nss_set_hash_size_direct"; +const char KIOU_HOOK_NAME_NSS_SETOPTION_DIRECT[] = "nss_set_option_direct"; const char KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT[] = "game_object_get_component"; const char KIOU_HOOK_NAME_RTU_WORLDTOSCREENPOINT[] = "rectxform_world_to_screen_point"; +const char KIOU_HOOK_NAME_JSON_FORMATTER_GET_DEFAULT[] = "json_formatter_get_default"; +const char KIOU_HOOK_NAME_JSON_FORMATTER_FORMAT[] = "json_formatter_format"; +const char KIOU_HOOK_NAME_GAMECTRL_GET_USI_TEXT[] = "gamectrl_get_usi_text"; +const char KIOU_HOOK_NAME_POSITION_TO_SFEN[] = "position_to_sfen"; +const char KIOU_HOOK_NAME_USIPARSER_PARSE_USI[] = "usiparser_parse_usi"; +const char KIOU_HOOK_NAME_KIFWRITEOPTIONS_CTOR[] = "kifwriteoptions_ctor"; +const char KIOU_HOOK_NAME_KIFWRITER_WRITE[] = "kifwriter_write"; +const char KIOU_HOOK_NAME_RUN_RESET_USER_DATA_SEQ[] = "run_reset_user_data_seq"; +const char KIOU_HOOK_NAME_RUN_DELETE_ACCOUNT_SEQ[] = "run_delete_account_seq"; // hook_id < 0 → not an entry hook (no g_inject_entry slot, e.g. observer // caves or a site that's only invoked directly via KIOUHookSiteAddr). @@ -155,8 +166,18 @@ // Direct-call sites (no chinlan cave / no hook id): { KIOU_HOOK_NAME_BACK_TO_TITLE_RUN_ASYNC, -1, KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC }, { KIOU_HOOK_NAME_NSS_SETHASHSIZE_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT }, + { KIOU_HOOK_NAME_NSS_SETOPTION_DIRECT, -1, KIOU_HOOK_RVA_NSS_SETOPTION_DIRECT }, { KIOU_HOOK_NAME_GAMEOBJECT_GETCOMPONENT, -1, KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT }, { KIOU_HOOK_NAME_RTU_WORLDTOSCREENPOINT, -1, KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT }, + { KIOU_HOOK_NAME_JSON_FORMATTER_GET_DEFAULT, -1, KIOU_HOOK_RVA_JSON_FORMATTER_GET_DEFAULT }, + { KIOU_HOOK_NAME_JSON_FORMATTER_FORMAT, -1, KIOU_HOOK_RVA_JSON_FORMATTER_FORMAT }, + { KIOU_HOOK_NAME_GAMECTRL_GET_USI_TEXT, -1, KIOU_HOOK_RVA_GAMECTRL_GET_USI_TEXT }, + { KIOU_HOOK_NAME_POSITION_TO_SFEN, -1, KIOU_HOOK_RVA_POSITION_TO_SFEN }, + { KIOU_HOOK_NAME_USIPARSER_PARSE_USI, -1, KIOU_HOOK_RVA_USIPARSER_PARSE_USI }, + { KIOU_HOOK_NAME_KIFWRITEOPTIONS_CTOR, -1, KIOU_HOOK_RVA_KIFWRITEOPTIONS_CTOR }, + { KIOU_HOOK_NAME_KIFWRITER_WRITE, -1, KIOU_HOOK_RVA_KIFWRITER_WRITE }, + { KIOU_HOOK_NAME_RUN_RESET_USER_DATA_SEQ, -1, KIOU_HOOK_RVA_RUN_RESET_USER_DATA_SEQ }, + { KIOU_HOOK_NAME_RUN_DELETE_ACCOUNT_SEQ, -1, KIOU_HOOK_RVA_RUN_DELETE_ACCOUNT_SEQ }, { NULL, 0, 0 }, }; @@ -177,7 +198,10 @@ uintptr_t KIOUHookSiteAddr(const char *name, uintptr_t unityBase) { const KIOUHookEntry *e = findEntry(name); - return e ? unityBase + e->site_rva : 0; + // site_rva 0 means the target build doesn't have this method; return 0 + // so callers see "unresolved" rather than a pointer to the image base. + if (!e || e->site_rva == 0) return 0; + return unityBase + e->site_rva; } void *KIOUHookOrig(const char *name) { @@ -204,6 +228,10 @@ uintptr_t KIOUHookSiteAddr(const char *name, uintptr_t unityBase) { return g_inject_entry[e->hook_id]; #else if (!replacement || e->hook_id < 0) return NULL; + // site_rva 0 marks a site the target build doesn't have (the recipe + // carries a placeholder row for it). Hooking would land on the image + // base, so refuse. + if (e->site_rva == 0) return NULL; uintptr_t addr = unityBase + e->site_rva; void *orig = NULL; MSHookFunction((void *)addr, replacement, &orig); diff --git a/recipes/__init__.py b/recipes/__init__.py index ea0a12a..022be7c 100644 --- a/recipes/__init__.py +++ b/recipes/__init__.py @@ -10,8 +10,8 @@ import importlib import os +from recipes.common import DYLIB_PATH as _DEFAULT_DYLIB_PATH from recipes.common import ( - DYLIB_PATH as _DEFAULT_DYLIB_PATH, ENTRY_SLOT_CAPACITY, ENTRY_SLOT_COUNT, ENTRY_SLOT_INDEX, @@ -56,6 +56,7 @@ _VERSIONS: dict[str, str | None] = { "1.0.1": "recipes.v1_0_1", "1.0.2": "recipes.v1_0_2", + "1.1.0": "recipes.v1_1_0", } _DEFAULT_VERSION = "1.0.2" diff --git a/recipes/common.py b/recipes/common.py index 863b519..5349f6c 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -327,15 +327,25 @@ def build_exports(sites, afk_site, afk_orig_8, hook_slot_rva, entry_slot_base_rv ) ) + # A row whose site is None is a placeholder: the method it patched no + # longer exists in this build. Its cave must still be reserved, because + # ChinlanDispatcher addresses a cave as cave_start + hook_id * cave_size + # and every later hook would otherwise shift down by one slot. The + # driver writes nothing for it and reserves len(expected) bytes, so + # `expected` carries one payload's worth of zeroes and the builder is + # never called. cave_patches = [ ( site, - bytes.fromhex(prologue_hex), - payload_for_site( + bytes(CAVE_PAYLOAD_SIZE) if site is None + else bytes.fromhex(prologue_hex), + None if site is None else payload_for_site( site, bytes.fromhex(prologue_hex), hook_id_name, kind, hook_slot_rva, entry_slot_base_rva, ), - f"{label}: route to KIOU-Hook {kind} cave ({hook_id_name})", + f"{label}: route to KIOU-Hook {kind} cave ({hook_id_name})" + if site is not None + else f"{label}: absent from this build ({hook_id_name})", ) for site, prologue_hex, hook_id_name, kind, label in sites ] @@ -343,6 +353,7 @@ def build_exports(sites, afk_site, afk_orig_8, hook_slot_rva, entry_slot_base_rv sites_index = [ (HOOK_IDS[hook_id_name], site_rva, prologue_hex, label) for site_rva, prologue_hex, hook_id_name, kind, label in sites + if site_rva is not None ] return patches, cave_patches, sites_index diff --git a/recipes/v1_0_2.py b/recipes/v1_0_2.py index 422fc0b..a196dd4 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -62,7 +62,7 @@ # Upstream site for x-user-id swap on account switch. Avoids the # HttpMessageInvoker.SendAsync / Yaha borrow path that crashes when # the request or HttpHeaders internal dictionary is touched. - (0x5BD9EE8, "f657bda9", "KIOU_HOOK_ID_HEADER_PROVIDER_SET_OR_UPDATE_HEADER", CAVE_ENTRY, "Project.Network.HeaderProvider.SetOrUpdateHeader"), + (0x5BD9EE8, "f657bda9", "KIOU_HOOK_ID_HEADER_PROVIDER_SET_OR_UPDATE_HEADER", CAVE_ENTRY, "Project.Network.HeaderProvider.SetOrUpdateHeader(string, string)"), # --- KiouEditor entry caves (CAVE_ENTRY, 1.0.2 port of the 1.0.1 sites) --- # RVAs verified against assets/1.0.2/dump.cs on 2026-07-01. Prologues @@ -136,10 +136,10 @@ # Prologues extracted from assets/1.0.2/Kiou-1.0.2.ipa UnityFramework # on 2026-07-10; none are PC-relative so the first-4-byte relocation # into the cave tail is safe. - (0x52C071C, "f657bda9", "KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY", CAVE_ENTRY, "MessageExtensions.ToByteArray"), - (0x52C0DB8, "ff0302d1", "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER", CAVE_ENTRY, "MessageExtensions.WriteTo(IBufferWriter)"), - (0x52C042C, "ff4304d1", "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ", CAVE_ENTRY, "MessageExtensions.MergeFrom(IMessage, ROSeq, bool, ExtensionRegistry)"), - (0x52C1B18, "ffc301d1", "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED", CAVE_ENTRY, "MessageParser.MergeFrom(CodedInputStream)"), + (0x52C071C, "f657bda9", "KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY", CAVE_ENTRY, "Google.Protobuf.MessageExtensions.ToByteArray(IMessage)"), + (0x52C0DB8, "ff0302d1", "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER", CAVE_ENTRY, "Google.Protobuf.MessageExtensions.WriteTo(IMessage, IBufferWriter)"), + (0x52C042C, "ff4304d1", "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ", CAVE_ENTRY, "Google.Protobuf.MessageExtensions.MergeFrom(IMessage, ReadOnlySequence, bool, ExtensionRegistry)"), + (0x52C1B18, "ffc301d1", "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED", CAVE_ENTRY, "Google.Protobuf.MessageParser.MergeFrom(IMessage, CodedInputStream)"), # ShogiMatchStreamHandler.DisposeAsync — appended AFTER the MSG_* rows so # its position in SITES matches its HOOK_ID (49). ChinlanDispatcher's diff --git a/recipes/v1_1_0.py b/recipes/v1_1_0.py new file mode 100644 index 0000000..3cef1a5 --- /dev/null +++ b/recipes/v1_1_0.py @@ -0,0 +1,163 @@ +"""KIOU patch constants for app version 1.1.0 (CFBundleVersion 15). + +Ported from v1_0_2.py with ``tools.port_recipe`` against +assets/1.1.0/dump.cs.index.json on 2026-07-31; every RVA and prologue +below was resolved by anchor name and read back from +assets/1.1.0/Kiou-1.1.0.ipa. __TEXT grew from 0x8274000 to 0x94BC000, so +no address carries over from 1.0.2. +""" + +from recipes.common import CAVE_ENTRY, CAVE_OBSERVER + +BUILD = 15 + +# Cave payload region (zero-fill tail of UnityFramework __TEXT). +# Same shape as 1.0.2: starts *after* __oslogstring +# (0x94B8000..0x94B8023) and runs to the end of __TEXT. Verified all-zero +# across its 0x3FDC B, which holds ~194 caves — comfortably more than the +# 56 SITES below. The __eh_frame..__oslogstring gap is only 0x1C50 B and +# is skipped for the same reason it was on 1.0.2. +CAVE_REGION = (0x94B8024, 0x94BC000) + +# Observer dispatcher slot — chinlan caves load this single 8-byte pointer. +# Placed at the same distance from the end of __DATA.__common as on 1.0.2 +# (end - 0xC5C0), keeping it clear of __bss, which il2cpp/UnityRuntime +# overwrites during lazy init. __common ends at 0xA522A28 here. +HOOK_SLOT_RVA = 0x0A516468 +PROBED_HOOK_SLOT_RVA = HOOK_SLOT_RVA + +# Entry-cave slot table — ENTRY_SLOT_BASE_RVA + idx*8 holds each hook fn ptr. +INJECT_ENTRY_TABLE_RVA = 0xA2BDBF8 +PROBED_INJECT_ENTRY_TABLE_RVA = 0xA2BDBF8 +ENTRY_SLOT_BASE_RVA = 0x0A516268 +ZERO_REGION_END_RVA = 0x0A522A28 + +# GameOrchestrator.IsAfkEnabled is handled via CAVE_ENTRY (see SITES +# below) on every supported version. Consumers that want the historic +# "always disabled" behaviour without wiring KIOUEditorFeatureEnabled +# can call KIOUInstallAfkSuppressHook(unityBase) — see KIOUHook.h. +AFK_SITE = None +AFK_ORIG_8 = "" + +# fmt: off +SITES = [ + # --- Entry caves (CAVE_ENTRY) --- + (0x7A9A020, "f44fbea9", "KIOU_HOOK_ID_SET_TARGET_FRAMERATE", CAVE_ENTRY, "Application.set_targetFrameRate"), + (0x6C58720, "ff0301d1", "KIOU_HOOK_ID_NSS_SETHASHSIZE", CAVE_ENTRY, "NativeSyncSession.SetHashSize"), + (0x6C5B64C, "ff0301d1", "KIOU_HOOK_ID_NSS_SETSKILLEVEL", CAVE_ENTRY, "NativeSyncSession.SetSkillLevel"), + (0x6C5B6E4, "ffc305d1", "KIOU_HOOK_ID_NSS_SEARCHFULL", CAVE_ENTRY, "NativeSyncSession.SearchFull"), + (0x682FF8C, "fd7bbfa9", "KIOU_HOOK_ID_ACCOUNT_EXISTS", CAVE_ENTRY, "UserSaveDataExtensions.AccountExists"), + (0x6AB29B0, "f85fbca9", "KIOU_HOOK_ID_LOGIN_ARGS_CREATE", CAVE_ENTRY, "ILoginArgs.Create"), + (0x6AB2A5C, "f657bda9", "KIOU_HOOK_ID_REGISTER_USER_ARGS_CREATE", CAVE_ENTRY, "IRegisterUserArgs.Create"), + (0x66CED7C, "ff8302d1", "KIOU_HOOK_ID_RUN_LOGIN_SEQ_MOVENEXT", CAVE_ENTRY, "AuthServiceExtensions+d__1.MoveNext"), + (0x6ACF014, "ff4302d1", "KIOU_HOOK_ID_GET_SELF_PROFILE_MOVENEXT", CAVE_ENTRY, "GameService+d__38.MoveNext"), + (0x6FA5DDC, "000840f9", "KIOU_HOOK_ID_HTTPMSGINVOKER_SEND_ASYNC", CAVE_ENTRY, "HttpMessageInvoker.SendAsync"), + + # --- Observer caves (CAVE_OBSERVER): IMatchMode.OnMatchEndAsync x 5 --- + (0x68F4988, "f657bda9", "KIOU_HOOK_ID_KIFU_AI_END", CAVE_OBSERVER, "AIMatchMode.OnMatchEndAsync"), + (0x68FCD78, "ff8301d1", "KIOU_HOOK_ID_KIFU_CPUSTREAM_END", CAVE_OBSERVER, "CPUStreamMode.OnMatchEndAsync"), + (0x69103D4, "f44fbea9", "KIOU_HOOK_ID_KIFU_LOCAL_END", CAVE_OBSERVER, "LocalPvPMode.OnMatchEndAsync"), + (0x6911E8C, "ff8301d1", "KIOU_HOOK_ID_KIFU_ONLINE_END", CAVE_OBSERVER, "OnlinePvPMode.OnMatchEndAsync"), + (0x693DE6C, "f85fbca9", "KIOU_HOOK_ID_KIFU_REPLAY_END", CAVE_OBSERVER, "RecordReplayMode.OnMatchEndAsync"), + + # --- Entry cave (CAVE_ENTRY): HeaderProvider.SetOrUpdateHeader --- + # Upstream site for x-user-id swap on account switch. Avoids the + # HttpMessageInvoker.SendAsync / Yaha borrow path that crashes when + # the request or HttpHeaders internal dictionary is touched. + (0x6AEF65C, "f657bda9", "KIOU_HOOK_ID_HEADER_PROVIDER_SET_OR_UPDATE_HEADER", CAVE_ENTRY, "Project.Network.HeaderProvider.SetOrUpdateHeader(string, string)"), + + # --- KiouEditor entry caves (CAVE_ENTRY) --- + (0x6B58E3C, "fc6fbaa9", "KIOU_HOOK_ID_SYNC_ITEM_LIST_MERGE", CAVE_ENTRY, "SyncItemListReply.InternalMergeFrom"), + (0x6B625EC, "fa67bba9", "KIOU_HOOK_ID_COLLECTION_PRESET_MERGE", CAVE_ENTRY, "UpdateCollectionPresetReply.InternalMergeFrom"), + (0x6BCBFA8, "ffc302d1", "KIOU_HOOK_ID_SELECT_CHAR_ASYNC", CAVE_ENTRY, "SelectCharacterAsync"), + (0x6B48BD4, "fc6fbaa9", "KIOU_HOOK_ID_SELECT_CHAR_REPLY_MERGE", CAVE_ENTRY, "SelectCharacterReply.InternalMergeFrom"), + (0x6A61C94, "fc6fbaa9", "KIOU_HOOK_ID_MATCHING_PLAYER_MERGE", CAVE_ENTRY, "ShogiMatchingPlayerStatus.InternalMergeFrom"), + (0x6B22EE0, "fc6fbaa9", "KIOU_HOOK_ID_HISTORY_DETAIL_MERGE", CAVE_ENTRY, "GetShogiHistoryDetailListReply.InternalMergeFrom"), + (0x6B22940, "00804039", "KIOU_HOOK_ID_HISTORY_GET_PREMIUM", CAVE_ENTRY, "GetShogiHistoryDetailListReply.get_IsPremiumUser"), + (0x6756674, "00004139", "KIOU_HOOK_ID_KIFU_DETAIL_IS_PREMIUM", CAVE_ENTRY, "KifuDetailModel.IsPremiumUser"), + (0x6722C4C, "e80300aa", "KIOU_HOOK_ID_VOICE_PLAYER_SATISFIES", CAVE_ENTRY, "CharacterVoicePlayer.SatisfiesRule"), + (0x6741F94, "00704039", "KIOU_HOOK_ID_VOICE_CELL_GET_IS_LOCKED", CAVE_ENTRY, "CharacterVoiceScrollerCellModel.get_IsLocked"), + (0x6888898, "f85fbca9", "KIOU_HOOK_ID_BSE_CTOR", CAVE_ENTRY, "BeginnerSupportEvaluator.ctor"), + # 1.1.0 replaced the NNUE evaluator with a policy model: + # EnsureInitializedLocked, TryBorrowSession and the NativeSyncSession + # field are all gone, and with them the hash-size knob this cave + # existed to apply. The row stays as a placeholder (site = None) so + # every later hook keeps its cave index — see Hook/AssistTune.m. + (None, "f657bda9", "KIOU_HOOK_ID_BSE_ENSURE_INITIALIZED", CAVE_ENTRY, "BeginnerSupportEvaluator.EnsureInitializedLocked"), + (0x684C374, "00404039", "KIOU_HOOK_ID_RBSUPPORT_GET_ENABLED", CAVE_ENTRY, "ResolvedBeginnerSupport.get_Enabled"), + (0x684C394, "002040b9", "KIOU_HOOK_ID_RBSUPPORT_GET_DEPTH", CAVE_ENTRY, "ResolvedBeginnerSupport.get_Depth"), + (0x69A5A70, "fc6fbaa9", "KIOU_HOOK_ID_HOME_UTILITY_PRESENTER_CTOR", CAVE_ENTRY, "HomeUtilityPresenter.ctor"), + (0x6CFB5C4, "f44fbea9", "KIOU_HOOK_ID_UIBUTTONBASE_ONPOINTERCLICK", CAVE_ENTRY, "UIButtonBase.OnPointerClick"), + (0x6CF5EF4, "ff0303d1", "KIOU_HOOK_ID_TITLE_SCENE_MOVENEXT", CAVE_ENTRY, "TitleScene+d__10.MoveNext"), + (0x6854064, "f44fbea9", "KIOU_HOOK_ID_GAME_ORCHESTRATOR_IS_AFK", CAVE_ENTRY, "GameOrchestrator.IsAfkEnabled"), + (0x6888A9C, "ff4302d1", "KIOU_HOOK_ID_BSE_EVALUATE_ASYNC", CAVE_ENTRY, "BeginnerSupportEvaluator.EvaluateAsync"), + + # --- KiouEditor 棋桜覚醒 (AI Special Support) UI-unlock caves. ------------ + # Server-side reject on the network still applies; this is UI unlock only. + (0x6A650DC, "00204339", "KIOU_HOOK_ID_MOVE_RESULT_CAN_USE_SPECIAL", CAVE_ENTRY, "ShogiMoveResultStatus.get_CanUseAiSpecialSupport"), + (0x6A650AC, "00bc40b9", "KIOU_HOOK_ID_MOVE_RESULT_FREE_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingFreeCount"), + (0x6A650BC, "00c040b9", "KIOU_HOOK_ID_MOVE_RESULT_TICKET_REMAINING", CAVE_ENTRY, "ShogiMoveResultStatus.get_AiSpecialSupportRemainingTicketCount"), + (0x6A60ECC, "006840b9", "KIOU_HOOK_ID_MP_FREE_REMAINING", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportFreeRemainingCount"), + (0x6A60EDC, "006c40b9", "KIOU_HOOK_ID_MP_PAID_AVAILABLE", CAVE_ENTRY, "ShogiMatchingPlayerStatus.get_AiSpecialSupportPaidAvailableCount"), + + # --- KiouEditor preferred-seat filter (ported from KiouEngineBridge). ---- + # Reject a MatchFound if it puts the user on the "wrong" seat, then + # send ConnectionFailed to the matching server so it re-queues. + (0x6C23BF0, "ff0301d1", "KIOU_HOOK_ID_MATCH_GET_VALID_FOUND", CAVE_ENTRY, "MatchingHandler.GetValidMatchFoundStatus"), + (0x6C2586C, "ff0303d1", "KIOU_HOOK_ID_MATCH_RECEIVE_TIMEOUT_MOVENEXT", CAVE_ENTRY, "MatchingHandler+d__6.MoveNext"), + (0x6AE500C, "fc6fbaa9", "KIOU_HOOK_ID_MATCH_STREAM_ARGS_CREATE", CAVE_ENTRY, "IShogiMatchStreamArgs.Create"), + # d__3 wraps the caller state around StartMatchingAsyncInternal — we + # snapshot its <>8__1 (DisplayClass3_0) pointer so the seat-filter + # reject branch can Cancel() its matchingCts and let the game's own + # TryLeaveQueueAsync unwind the popup cleanly. + (0x6C26EF0, "ffc305d1", "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT", CAVE_ENTRY, "MatchingHandler+d__3.MoveNext"), + # ShogiMatchStreamHandler.SendAsync — every outgoing frame the game + # writes to the matching stream (Heartbeat every 3 s, JoinQueue, + # LeaveQueue, ConnectionFailed) flows through this call. We hook the + # entry so we can (a) log every outbound frame and (b) capture the + # MethodInfo argument (x2) into a global so the seat-filter reject + # branch can call SendAsync directly with a valid MethodInfo instead + # of NULL (which crashes the il2cpp method body). + (0x6AE5820, "f657bda9", "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_SEND_ASYNC", CAVE_ENTRY, "ShogiMatchStreamHandler.SendAsync"), + + # --- Universal gRPC wire logger (protobuf serialize/parse bottlenecks) --- + # The serialize path reaches ToByteArray or WriteTo(IBufferWriter); + # the deserialize path reaches MessageParser.ParseFrom(ROSeq), which + # tail-calls MessageExtensions.MergeFrom(msg, ROSeq, bool, + # ExtensionRegistry). The stream / byte[] overloads never fire on the + # KIOU gRPC path, so they are skipped; MergeFrom(CodedInputStream) + # covers any residual CIS-based path (nested submessage parses fall + # under it too, giving a coverage backstop). + (0x617A650, "f657bda9", "KIOU_HOOK_ID_MSG_EXT_TO_BYTE_ARRAY", CAVE_ENTRY, "Google.Protobuf.MessageExtensions.ToByteArray(IMessage)"), + (0x617ACEC, "ff0302d1", "KIOU_HOOK_ID_MSG_EXT_WRITE_TO_BUFFER", CAVE_ENTRY, "Google.Protobuf.MessageExtensions.WriteTo(IMessage, IBufferWriter)"), + (0x617A360, "ff4304d1", "KIOU_HOOK_ID_MSG_EXT_MERGE_FROM_ROSEQ", CAVE_ENTRY, "Google.Protobuf.MessageExtensions.MergeFrom(IMessage, ReadOnlySequence, bool, ExtensionRegistry)"), + (0x617BA4C, "ffc301d1", "KIOU_HOOK_ID_MSG_PARSER_MERGE_FROM_CODED", CAVE_ENTRY, "Google.Protobuf.MessageParser.MergeFrom(IMessage, CodedInputStream)"), + + # ShogiMatchStreamHandler.DisposeAsync — appended AFTER the MSG_* rows so + # its position in SITES matches its HOOK_ID (49). ChinlanDispatcher's + # bypassEntryForHook(id) computes `cave_start + id * cave_size` and the + # patcher allocates cave memory in SITES order — the two must agree, so + # new hooks always go at the end. + # + # This is the full-teardown primitive for the matching stream. The + # server only marks the seat as gone when the underlying gRPC HTTP/2 + # duplex call is closed (LeaveQueue frames without a stream close are + # ignored — same match_room_id keeps getting served). We hook the entry + # to capture the MethodInfo so the seat-filter reject branch can invoke + # DisposeAsync directly on the cached handler self. + (0x6AE5634, "ff4302d1", "KIOU_HOOK_ID_MATCH_STREAM_HANDLER_DISPOSE_ASYNC", CAVE_ENTRY, "ShogiMatchStreamHandler.DisposeAsync"), + + # --- NativeSyncSession Search* variants --- + # BSE.EvaluateAsync fans out over legal candidates via SearchMulti / + # SearchMultiWithPV, NOT the single-position SearchFull that + # FrameworkPassthrough already covers. Adding these 5 catches every + # engine invocation the game side can issue so the KiouEditor logger + # sees each search's per-move score + PV. + (0x6C5B6C0, "ffc300d1", "KIOU_HOOK_ID_NSS_SEARCH", CAVE_ENTRY, "NativeSyncSession.Search"), + (0x6C5C090, "ffc302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTI", CAVE_ENTRY, "NativeSyncSession.SearchMulti"), + (0x6C5CD8C, "ff4302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiPV"), + (0x6C5D2F8, "ff4303d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIWITHPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiWithPV"), + (0x6C59AFC, "ff8302d1", "KIOU_HOOK_ID_NSS_SEARCHMULTIPVWITHPV", CAVE_ENTRY, "NativeSyncSession.SearchMultiPVWithPV"), + (0x6C57848, "ff8301d1", "KIOU_HOOK_ID_NSS_SETOPTION", CAVE_ENTRY, "NativeSyncSession.SetOption"), +] +# fmt: on diff --git a/rva/kiou_rva_1_0_1.h b/rva/kiou_rva_1_0_1.h new file mode 100644 index 0000000..d66d762 --- /dev/null +++ b/rva/kiou_rva_1_0_1.h @@ -0,0 +1,91 @@ +#pragma once + +// =========================================================================== +// kiou_rva_1_0_1.h — site addresses for KIOU 1.0.1 (CFBundleVersion 11). +// +// GENERATED by tools/gen_rva_header.py from recipes/v1_0_1.py and the +// direct-ABI anchors it carries. Do not hand-edit: fix the recipe (or the +// anchor) and regenerate. tools/check_recipes.py re-checks every hook-site +// value against the recipe on each run. +// =========================================================================== + +#define KIOU_HOOK_OBSERVER_SLOT_RVA 0x091E93B8 +#define KIOU_HOOK_ENTRY_SLOT_BASE_RVA 0x091E91B8 + +// CAVE_REGION[0] from recipes/v1_0_1.py — the consumer's Makefile may +// override this, but the two must agree. +#ifndef KIOU_HOOK_CAVE_REGION_START +#define KIOU_HOOK_CAVE_REGION_START 0x8268024 +#endif + + +#define KIOU_HOOK_RVA_SET_TARGET_FRAMERATE 0x6B6B758 +#define KIOU_HOOK_RVA_NSS_SETHASHSIZE 0x5D320E0 +#define KIOU_HOOK_RVA_NSS_SETSKILLEVEL 0x5D3206C +#define KIOU_HOOK_RVA_NSS_SEARCHFULL 0x5D32178 +#define KIOU_HOOK_RVA_ACCOUNT_EXISTS 0x591E860 +#define KIOU_HOOK_RVA_LOGIN_ARGS_CREATE 0x5B9899C +#define KIOU_HOOK_RVA_REGISTER_USER_ARGS_CREATE 0x5B98A2C +#define KIOU_HOOK_RVA_RUN_LOGIN_SEQ_MOVENEXT 0x5812534 +#define KIOU_HOOK_RVA_GET_SELF_PROFILE_MOVENEXT 0x5BB4774 +#define KIOU_HOOK_RVA_HTTPMSGINVOKER_SEND_ASYNC 0x607C974 +#define KIOU_HOOK_RVA_AI_END 0x59E5958 +#define KIOU_HOOK_RVA_CPUSTREAM_END 0x59EC818 +#define KIOU_HOOK_RVA_LOCAL_END 0x59FF8F8 +#define KIOU_HOOK_RVA_ONLINE_END 0x5A0139C +#define KIOU_HOOK_RVA_REPLAY_END 0x5A2B564 +#define KIOU_HOOK_RVA_HEADER_PROVIDER_SET_OR_UPDATE_HEADER 0x5BD4C80 +#define KIOU_HOOK_RVA_SYNC_ITEM_LIST_MERGE 0x5C37034 +#define KIOU_HOOK_RVA_COLLECTION_PRESET_MERGE 0x5C4065C +#define KIOU_HOOK_RVA_SELECT_CHAR_ASYNC 0x5CA7C90 +#define KIOU_HOOK_RVA_SELECT_CHAR_REPLY_MERGE 0x5C26DCC +#define KIOU_HOOK_RVA_MATCHING_PLAYER_MERGE 0x5B4CAEC +#define KIOU_HOOK_RVA_HISTORY_DETAIL_MERGE 0x5C01328 +#define KIOU_HOOK_RVA_HISTORY_GET_PREMIUM 0x5C00D88 +#define KIOU_HOOK_RVA_KIFU_DETAIL_IS_PREMIUM 0x585B25C +#define KIOU_HOOK_RVA_VOICE_PLAYER_SATISFIES 0x582B88C +#define KIOU_HOOK_RVA_VOICE_CELL_GET_IS_LOCKED 0x584ADC0 +#define KIOU_HOOK_RVA_BSE_CTOR 0x597A448 +#define KIOU_HOOK_RVA_BSE_ENSURE_INITIALIZED 0x597BAFC +#define KIOU_HOOK_RVA_RBSUPPORT_GET_ENABLED 0x593E630 +#define KIOU_HOOK_RVA_RBSUPPORT_GET_DEPTH 0x593E650 +#define KIOU_HOOK_RVA_HOME_UTILITY_PRESENTER_CTOR 0x5A9F298 +#define KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD1E08 +#define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x5DCC728 +#define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x59455D4 +#define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x597B570 +#define KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL 0x5B4FE18 +#define KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING 0x5B4FDE8 +#define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x5B4FDF8 +#define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x5B4BC54 +#define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x5B4BC64 +#define KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND 0x5D04E94 +#define KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT 0x5D06B10 +#define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x5BCA664 +#define KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT 0x5D08194 +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC 0x5BCAE78 +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_DISPOSE_ASYNC 0x5BCAC8C +#define KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY 0x52BDBEC +#define KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER 0x52BE288 +#define KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ 0x52BD8FC +#define KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED 0x52BEFE8 +#define KIOU_HOOK_RVA_NSS_SEARCH 0x5D32154 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTI 0x5D32AA8 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPV 0x5D337A4 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIWITHPV 0x5D33D10 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPVWITHPV 0x5D34A90 +#define KIOU_HOOK_RVA_NSS_SETOPTION 0x5D31D98 +#define KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC 0x5CF712C +#define KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT 0x5D320E0 +#define KIOU_HOOK_RVA_NSS_SETOPTION_DIRECT 0x5D31D98 +#define KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT 0x6BCA6AC +#define KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT 0x6F20040 +#define KIOU_HOOK_RVA_JSON_FORMATTER_GET_DEFAULT 0x52AA110 +#define KIOU_HOOK_RVA_JSON_FORMATTER_FORMAT 0x52ABCF8 +#define KIOU_HOOK_RVA_GAMECTRL_GET_USI_TEXT 0x5D44074 +#define KIOU_HOOK_RVA_POSITION_TO_SFEN 0x5D44374 +#define KIOU_HOOK_RVA_USIPARSER_PARSE_USI 0x5D572B4 +#define KIOU_HOOK_RVA_KIFWRITEOPTIONS_CTOR 0x5D53960 +#define KIOU_HOOK_RVA_KIFWRITER_WRITE 0x5D53968 +#define KIOU_HOOK_RVA_RUN_RESET_USER_DATA_SEQ 0x5DC6908 +#define KIOU_HOOK_RVA_RUN_DELETE_ACCOUNT_SEQ 0x5DC69B8 diff --git a/rva/kiou_rva_1_0_2.h b/rva/kiou_rva_1_0_2.h new file mode 100644 index 0000000..f205adc --- /dev/null +++ b/rva/kiou_rva_1_0_2.h @@ -0,0 +1,91 @@ +#pragma once + +// =========================================================================== +// kiou_rva_1_0_2.h — site addresses for KIOU 1.0.2 (CFBundleVersion 12). +// +// GENERATED by tools/gen_rva_header.py from recipes/v1_0_2.py and the +// direct-ABI anchors it carries. Do not hand-edit: fix the recipe (or the +// anchor) and regenerate. tools/check_recipes.py re-checks every hook-site +// value against the recipe on each run. +// =========================================================================== + +#define KIOU_HOOK_OBSERVER_SLOT_RVA 0x091E93B8 +#define KIOU_HOOK_ENTRY_SLOT_BASE_RVA 0x091E91B8 + +// CAVE_REGION[0] from recipes/v1_0_2.py — the consumer's Makefile may +// override this, but the two must agree. +#ifndef KIOU_HOOK_CAVE_REGION_START +#define KIOU_HOOK_CAVE_REGION_START 0x8270024 +#endif + + +#define KIOU_HOOK_RVA_SET_TARGET_FRAMERATE 0x6B718A4 +#define KIOU_HOOK_RVA_NSS_SETHASHSIZE 0x5D379DC +#define KIOU_HOOK_RVA_NSS_SETSKILLEVEL 0x5D37968 +#define KIOU_HOOK_RVA_NSS_SEARCHFULL 0x5D37A74 +#define KIOU_HOOK_RVA_ACCOUNT_EXISTS 0x5922CD0 +#define KIOU_HOOK_RVA_LOGIN_ARGS_CREATE 0x5B9DC04 +#define KIOU_HOOK_RVA_REGISTER_USER_ARGS_CREATE 0x5B9DC94 +#define KIOU_HOOK_RVA_RUN_LOGIN_SEQ_MOVENEXT 0x58152BC +#define KIOU_HOOK_RVA_GET_SELF_PROFILE_MOVENEXT 0x5BB99DC +#define KIOU_HOOK_RVA_HTTPMSGINVOKER_SEND_ASYNC 0x6082AC0 +#define KIOU_HOOK_RVA_AI_END 0x59EA720 +#define KIOU_HOOK_RVA_CPUSTREAM_END 0x59F15D4 +#define KIOU_HOOK_RVA_LOCAL_END 0x5A046B4 +#define KIOU_HOOK_RVA_ONLINE_END 0x5A06158 +#define KIOU_HOOK_RVA_REPLAY_END 0x5A30320 +#define KIOU_HOOK_RVA_HEADER_PROVIDER_SET_OR_UPDATE_HEADER 0x5BD9EE8 +#define KIOU_HOOK_RVA_SYNC_ITEM_LIST_MERGE 0x5C3C29C +#define KIOU_HOOK_RVA_COLLECTION_PRESET_MERGE 0x5C458C4 +#define KIOU_HOOK_RVA_SELECT_CHAR_ASYNC 0x5CACEF8 +#define KIOU_HOOK_RVA_SELECT_CHAR_REPLY_MERGE 0x5C2C034 +#define KIOU_HOOK_RVA_MATCHING_PLAYER_MERGE 0x5B51C3C +#define KIOU_HOOK_RVA_HISTORY_DETAIL_MERGE 0x5C06590 +#define KIOU_HOOK_RVA_HISTORY_GET_PREMIUM 0x5C05FF0 +#define KIOU_HOOK_RVA_KIFU_DETAIL_IS_PREMIUM 0x585E000 +#define KIOU_HOOK_RVA_VOICE_PLAYER_SATISFIES 0x582E614 +#define KIOU_HOOK_RVA_VOICE_CELL_GET_IS_LOCKED 0x584DB64 +#define KIOU_HOOK_RVA_BSE_CTOR 0x597E608 +#define KIOU_HOOK_RVA_BSE_ENSURE_INITIALIZED 0x5980890 +#define KIOU_HOOK_RVA_RBSUPPORT_GET_ENABLED 0x5942AA0 +#define KIOU_HOOK_RVA_RBSUPPORT_GET_DEPTH 0x5942AC0 +#define KIOU_HOOK_RVA_HOME_UTILITY_PRESENTER_CTOR 0x5AA4054 +#define KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK 0x5DD7F54 +#define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x5DD2874 +#define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x594A034 +#define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x5980304 +#define KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL 0x5B54F68 +#define KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING 0x5B54F38 +#define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x5B54F48 +#define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x5B50DA4 +#define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x5B50DB4 +#define KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND 0x5D0A78C +#define KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT 0x5D0C408 +#define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x5BCF8CC +#define KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT 0x5D0DA8C +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC 0x5BD00E0 +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_DISPOSE_ASYNC 0x5BCFEF4 +#define KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY 0x52C071C +#define KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER 0x52C0DB8 +#define KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ 0x52C042C +#define KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED 0x52C1B18 +#define KIOU_HOOK_RVA_NSS_SEARCH 0x5D37A50 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTI 0x5D383A4 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPV 0x5D390A0 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIWITHPV 0x5D3960C +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPVWITHPV 0x5D3A38C +#define KIOU_HOOK_RVA_NSS_SETOPTION 0x5D37694 +#define KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC 0x5CFC394 +#define KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT 0x5D379DC +#define KIOU_HOOK_RVA_NSS_SETOPTION_DIRECT 0x5D37694 +#define KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT 0x6BD07F8 +#define KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT 0x6F2628C +#define KIOU_HOOK_RVA_JSON_FORMATTER_GET_DEFAULT 0x52ACC40 +#define KIOU_HOOK_RVA_JSON_FORMATTER_FORMAT 0x52AE828 +#define KIOU_HOOK_RVA_GAMECTRL_GET_USI_TEXT 0x5D49970 +#define KIOU_HOOK_RVA_POSITION_TO_SFEN 0x5D49C70 +#define KIOU_HOOK_RVA_USIPARSER_PARSE_USI 0x5D5CBB0 +#define KIOU_HOOK_RVA_KIFWRITEOPTIONS_CTOR 0x5D5925C +#define KIOU_HOOK_RVA_KIFWRITER_WRITE 0x5D59264 +#define KIOU_HOOK_RVA_RUN_RESET_USER_DATA_SEQ 0x5DCC204 +#define KIOU_HOOK_RVA_RUN_DELETE_ACCOUNT_SEQ 0x5DCC2B4 diff --git a/rva/kiou_rva_1_1_0.h b/rva/kiou_rva_1_1_0.h new file mode 100644 index 0000000..919216d --- /dev/null +++ b/rva/kiou_rva_1_1_0.h @@ -0,0 +1,91 @@ +#pragma once + +// =========================================================================== +// kiou_rva_1_1_0.h — site addresses for KIOU 1.1.0 (CFBundleVersion 15). +// +// GENERATED by tools/gen_rva_header.py from recipes/v1_1_0.py and the +// direct-ABI anchors it carries. Do not hand-edit: fix the recipe (or the +// anchor) and regenerate. tools/check_recipes.py re-checks every hook-site +// value against the recipe on each run. +// =========================================================================== + +#define KIOU_HOOK_OBSERVER_SLOT_RVA 0x0A516468 +#define KIOU_HOOK_ENTRY_SLOT_BASE_RVA 0x0A516268 + +// CAVE_REGION[0] from recipes/v1_1_0.py — the consumer's Makefile may +// override this, but the two must agree. +#ifndef KIOU_HOOK_CAVE_REGION_START +#define KIOU_HOOK_CAVE_REGION_START 0x94B8024 +#endif + + +#define KIOU_HOOK_RVA_SET_TARGET_FRAMERATE 0x7A9A020 +#define KIOU_HOOK_RVA_NSS_SETHASHSIZE 0x6C58720 +#define KIOU_HOOK_RVA_NSS_SETSKILLEVEL 0x6C5B64C +#define KIOU_HOOK_RVA_NSS_SEARCHFULL 0x6C5B6E4 +#define KIOU_HOOK_RVA_ACCOUNT_EXISTS 0x682FF8C +#define KIOU_HOOK_RVA_LOGIN_ARGS_CREATE 0x6AB29B0 +#define KIOU_HOOK_RVA_REGISTER_USER_ARGS_CREATE 0x6AB2A5C +#define KIOU_HOOK_RVA_RUN_LOGIN_SEQ_MOVENEXT 0x66CED7C +#define KIOU_HOOK_RVA_GET_SELF_PROFILE_MOVENEXT 0x6ACF014 +#define KIOU_HOOK_RVA_HTTPMSGINVOKER_SEND_ASYNC 0x6FA5DDC +#define KIOU_HOOK_RVA_AI_END 0x68F4988 +#define KIOU_HOOK_RVA_CPUSTREAM_END 0x68FCD78 +#define KIOU_HOOK_RVA_LOCAL_END 0x69103D4 +#define KIOU_HOOK_RVA_ONLINE_END 0x6911E8C +#define KIOU_HOOK_RVA_REPLAY_END 0x693DE6C +#define KIOU_HOOK_RVA_HEADER_PROVIDER_SET_OR_UPDATE_HEADER 0x6AEF65C +#define KIOU_HOOK_RVA_SYNC_ITEM_LIST_MERGE 0x6B58E3C +#define KIOU_HOOK_RVA_COLLECTION_PRESET_MERGE 0x6B625EC +#define KIOU_HOOK_RVA_SELECT_CHAR_ASYNC 0x6BCBFA8 +#define KIOU_HOOK_RVA_SELECT_CHAR_REPLY_MERGE 0x6B48BD4 +#define KIOU_HOOK_RVA_MATCHING_PLAYER_MERGE 0x6A61C94 +#define KIOU_HOOK_RVA_HISTORY_DETAIL_MERGE 0x6B22EE0 +#define KIOU_HOOK_RVA_HISTORY_GET_PREMIUM 0x6B22940 +#define KIOU_HOOK_RVA_KIFU_DETAIL_IS_PREMIUM 0x6756674 +#define KIOU_HOOK_RVA_VOICE_PLAYER_SATISFIES 0x6722C4C +#define KIOU_HOOK_RVA_VOICE_CELL_GET_IS_LOCKED 0x6741F94 +#define KIOU_HOOK_RVA_BSE_CTOR 0x6888898 +#define KIOU_HOOK_RVA_BSE_ENSURE_INITIALIZED 0x0 +#define KIOU_HOOK_RVA_RBSUPPORT_GET_ENABLED 0x684C374 +#define KIOU_HOOK_RVA_RBSUPPORT_GET_DEPTH 0x684C394 +#define KIOU_HOOK_RVA_HOME_UTILITY_PRESENTER_CTOR 0x69A5A70 +#define KIOU_HOOK_RVA_UIBUTTONBASE_ONPOINTERCLICK 0x6CFB5C4 +#define KIOU_HOOK_RVA_TITLE_SCENE_MOVENEXT 0x6CF5EF4 +#define KIOU_HOOK_RVA_GAME_ORCHESTRATOR_IS_AFK 0x6854064 +#define KIOU_HOOK_RVA_BSE_EVALUATE_ASYNC 0x6888A9C +#define KIOU_HOOK_RVA_MOVE_RESULT_CAN_USE_SPECIAL 0x6A650DC +#define KIOU_HOOK_RVA_MOVE_RESULT_FREE_REMAINING 0x6A650AC +#define KIOU_HOOK_RVA_MOVE_RESULT_TICKET_REMAINING 0x6A650BC +#define KIOU_HOOK_RVA_MP_FREE_REMAINING 0x6A60ECC +#define KIOU_HOOK_RVA_MP_PAID_AVAILABLE 0x6A60EDC +#define KIOU_HOOK_RVA_MATCH_GET_VALID_FOUND 0x6C23BF0 +#define KIOU_HOOK_RVA_MATCH_RECEIVE_TIMEOUT_MOVENEXT 0x6C2586C +#define KIOU_HOOK_RVA_MATCH_STREAM_ARGS_CREATE 0x6AE500C +#define KIOU_HOOK_RVA_MATCH_START_D3_MOVENEXT 0x6C26EF0 +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_SEND_ASYNC 0x6AE5820 +#define KIOU_HOOK_RVA_MATCH_STREAM_HANDLER_DISPOSE_ASYNC 0x6AE5634 +#define KIOU_HOOK_RVA_MSG_EXT_TO_BYTE_ARRAY 0x617A650 +#define KIOU_HOOK_RVA_MSG_EXT_WRITE_TO_BUFFER 0x617ACEC +#define KIOU_HOOK_RVA_MSG_EXT_MERGE_FROM_ROSEQ 0x617A360 +#define KIOU_HOOK_RVA_MSG_PARSER_MERGE_FROM_CODED 0x617BA4C +#define KIOU_HOOK_RVA_NSS_SEARCH 0x6C5B6C0 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTI 0x6C5C090 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPV 0x6C5CD8C +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIWITHPV 0x6C5D2F8 +#define KIOU_HOOK_RVA_NSS_SEARCHMULTIPVWITHPV 0x6C59AFC +#define KIOU_HOOK_RVA_NSS_SETOPTION 0x6C57848 +#define KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC 0x6C1581C +#define KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT 0x6C58720 +#define KIOU_HOOK_RVA_NSS_SETOPTION_DIRECT 0x6C57848 +#define KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT 0x7AF8F74 +#define KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT 0x7E52670 +#define KIOU_HOOK_RVA_JSON_FORMATTER_GET_DEFAULT 0x6166B74 +#define KIOU_HOOK_RVA_JSON_FORMATTER_FORMAT 0x616875C +#define KIOU_HOOK_RVA_GAMECTRL_GET_USI_TEXT 0x6C6DA6C +#define KIOU_HOOK_RVA_POSITION_TO_SFEN 0x6C6DD6C +#define KIOU_HOOK_RVA_USIPARSER_PARSE_USI 0x6C84A50 +#define KIOU_HOOK_RVA_KIFWRITEOPTIONS_CTOR 0x6C81060 +#define KIOU_HOOK_RVA_KIFWRITER_WRITE 0x6C81068 +#define KIOU_HOOK_RVA_RUN_RESET_USER_DATA_SEQ 0x6CEF708 +#define KIOU_HOOK_RVA_RUN_DELETE_ACCOUNT_SEQ 0x6CEF7B8 diff --git a/tools/check_recipes.py b/tools/check_recipes.py index dde8a8c..204cf4a 100644 --- a/tools/check_recipes.py +++ b/tools/check_recipes.py @@ -22,10 +22,12 @@ import ast import pathlib +import re import sys REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent RECIPES_DIR = REPO_ROOT / "recipes" +RVA_DIR = REPO_ROOT / "rva" class RecipeError(Exception): @@ -75,7 +77,7 @@ def _find_int_assign(module: ast.Module, name: str) -> int: raise RecipeError(f"{name}: not found") -def _find_sites_assign(module: ast.Module) -> list[tuple[int, str, str, str, str]]: +def _find_sites_assign(module: ast.Module) -> list[tuple[int | None, str, str, str, str]]: """Extract the ``SITES`` list of tuples from a per-version recipe.""" for node in module.body: @@ -84,17 +86,22 @@ def _find_sites_assign(module: ast.Module) -> list[tuple[int, str, str, str, str if isinstance(target, ast.Name) and target.id == "SITES": if not isinstance(node.value, ast.List): raise RecipeError("SITES: expected list literal") - rows: list[tuple[int, str, str, str, str]] = [] + rows: list[tuple[int | None, str, str, str, str]] = [] for i, elt in enumerate(node.value.elts): if not isinstance(elt, ast.Tuple) or len(elt.elts) != 5: raise RecipeError( f"SITES[{i}]: expected 5-tuple (rva, prologue_hex, hook_id_name, kind, label)" ) rva_n, prologue_n, hook_id_n, kind_n, label_n = elt.elts - if not isinstance(rva_n, ast.Constant) or not isinstance( - rva_n.value, int - ): - raise RecipeError(f"SITES[{i}].rva: expected int constant") + # rva may be None: a placeholder row reserving the + # cave slot of a method this build no longer has. + if not isinstance(rva_n, ast.Constant) or isinstance( + rva_n.value, bool + ) or not isinstance(rva_n.value, (int, type(None))): + raise RecipeError( + f"SITES[{i}].rva: expected int constant or None" + ) + rva_val: int | None = rva_n.value if not isinstance(prologue_n, ast.Constant) or not isinstance( prologue_n.value, str ): @@ -127,12 +134,103 @@ def _find_sites_assign(module: ast.Module) -> list[tuple[int, str, str, str, str ): raise RecipeError(f"SITES[{i}].label: expected str") rows.append( - (rva_n.value, prologue_n.value, hook_id_val, kind_val, label_n.value) + (rva_val, prologue_n.value, hook_id_val, kind_val, label_n.value) ) return rows raise RecipeError("SITES: not found at module level") +# v1_0_1's SITES table predates the cave-index invariant and is sparse: it +# omits the 17 sites that only exist from 1.0.2 on, so every row after the +# first gap sits at a position below its hook id. Fixing it needs a +# placeholder-row mechanism in tools.caves, tracked separately; until then +# the check reports it as a warning so it can't mask a NEW recipe going +# sparse. +_KNOWN_SPARSE = {"v1_0_1.py"} + + +def _check_cave_index(name: str, sites, hook_ids: dict[str, int]) -> list[str]: + """Every SITES row's position must equal its hook id. + + ``tools.caves.apply_patches`` allocates cave payloads sequentially in + declaration order, while the consumer's ChinlanDispatcher computes a + cave's orig-call trampoline as ``cave_start + hook_id * cave_size``. + The two only agree when position == id, so a row inserted anywhere but + the end silently sends every later hook's orig call into the wrong + cave. + """ + sparse = [ + f"{name} SITES[{i}] ({label}): hook id {hook_id_name} = {hook_ids[hook_id_name]} " + f"!= row position {i}" + for i, (_rva, _prologue, hook_id_name, _kind, label) in enumerate(sites) + if hook_id_name in hook_ids and hook_ids[hook_id_name] != i + ] + if not sparse: + return [] + if name in _KNOWN_SPARSE: + print( + f"warning: {name} is sparse ({len(sparse)} row(s) whose position != hook id). " + "Its chinlan orig-call trampolines land in the wrong cave.", + file=sys.stderr, + ) + return [] + return [ + *sparse, + f"{name}: rows must stay dense and append-only — cave-bypass addresses " + "are computed as cave_start + hook_id * cave_size.", + ] + + +def _check_rva_header(recipe: pathlib.Path, sites) -> list[str]: + """Cross-check ``rva/kiou_rva_.h`` against the recipe's SITES. + + The JB / jailed builds hook through the header's ``KIOU_HOOK_RVA_*`` + macros while the chinlan build patches via the recipe, so a drift + between the two means the same hook lands on two different addresses + depending on the build flavour. + """ + header = RVA_DIR / f"kiou_rva_{recipe.stem[1:]}.h" + if not header.exists(): + return [f"{recipe.name}: no matching RVA header at {header.relative_to(REPO_ROOT)}"] + + text = header.read_text() + defines = { + m.group(1): int(m.group(2), 16) + for m in re.finditer(r"#define\s+(KIOU_HOOK_RVA_\w+)\s+(0x[0-9A-Fa-f]+)", text) + } + catalog = _catalog_macro_by_hook_id() + + errors = [] + for _rva, _prologue, hook_id_name, _kind, label in sites: + if _rva is None: + continue # placeholder row: no site, so nothing to cross-check + macro = catalog.get(hook_id_name) + if macro is None: + errors.append( + f"{recipe.name} ({label}): {hook_id_name} has no KIOUHook.m catalog row" + ) + continue + if macro not in defines: + errors.append(f"{header.name}: missing #define {macro}") + elif defines[macro] != _rva: + errors.append( + f"{header.name}: {macro} = 0x{defines[macro]:X} but " + f"{recipe.name} SITES has 0x{_rva:X} for {label}" + ) + return errors + + +def _catalog_macro_by_hook_id() -> dict[str, str]: + """Map ``KIOU_HOOK_ID_*`` to the ``KIOU_HOOK_RVA_*`` macro KIOUHook.m + resolves it through.""" + text = (REPO_ROOT / "KIOUHook.m").read_text() + rows = re.findall( + r"\{\s*KIOU_HOOK_NAME_\w+\s*,\s*(-1|KIOU_HOOK_ID_\w+)\s*,\s*(KIOU_HOOK_RVA_\w+)\s*\}", + text, + ) + return {hook_id: macro for hook_id, macro in rows if hook_id != "-1"} + + def check() -> None: common_path = RECIPES_DIR / "common.py" if not common_path.exists(): @@ -200,6 +298,8 @@ def check() -> None: errors.append( f"{where}: CAVE_OBSERVER row's hook_id {hook_id_name!r} unexpectedly in ENTRY_SLOT_INDEX" ) + if rva is None: + continue if rva in seen_rvas: errors.append( f"{where}: duplicate site RVA 0x{rva:X} (already at {seen_rvas[rva]})" @@ -207,6 +307,9 @@ def check() -> None: else: seen_rvas[rva] = label + errors.extend(_check_cave_index(recipe.name, sites, hook_ids)) + errors.extend(_check_rva_header(recipe, sites)) + if errors: raise RecipeError("\n ".join(["recipe check failed:", *errors])) diff --git a/tools/gen_rva_header.py b/tools/gen_rva_header.py new file mode 100644 index 0000000..30a779a --- /dev/null +++ b/tools/gen_rva_header.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""Generate ``rva/kiou_rva_.h`` from a recipe and the dump index. + +Every ``KIOU_HOOK_RVA_*`` macro the catalog in ``KIOUHook.m`` references +comes from one of two places: + + * a hook site — the address is already in ``recipes/v.py`` SITES, + keyed by hook id, so it is copied verbatim (``check_recipes`` then + enforces that the two never drift), or + * a direct-ABI helper — not a hook site, so there is no recipe row. Those + are resolved by the anchor names in ``DIRECT_ABI_ANCHORS`` below. + +Usage: + python3 -m tools.gen_rva_header --version 1.1.0 \\ + --index ../../assets/1.1.0/dump.cs.index.json + +Run it once per supported version after porting a recipe; the output is +committed. +""" + +from __future__ import annotations + +import argparse +import importlib +import os +import pathlib +import re +import sys + +from tools.check_recipes import ( + RECIPES_DIR, + REPO_ROOT, + RVA_DIR, + _catalog_macro_by_hook_id, + _find_sites_assign, + _load_module_ast, +) + +# Direct-ABI helpers: called straight through KIOUHookSiteAddr rather than +# hooked, so they carry hook_id = -1 in the catalog and never appear in a +# recipe. The anchor is the durable identity; the address is not. +DIRECT_ABI_ANCHORS: dict[str, str] = { + "KIOU_HOOK_RVA_BACK_TO_TITLE_RUN_ASYNC": + "Project.Orchestration.BackToTitleSequence.RunAsync(CancellationToken)", + "KIOU_HOOK_RVA_NSS_SETHASHSIZE_DIRECT": + "Rshogi.NativeSyncSession.SetHashSize(int)", + "KIOU_HOOK_RVA_NSS_SETOPTION_DIRECT": + "Rshogi.NativeSyncSession.SetOption(string, string)", + "KIOU_HOOK_RVA_GAMEOBJECT_GETCOMPONENT": + "UnityEngine.GameObject.GetComponent(string)", + "KIOU_HOOK_RVA_RTU_WORLDTOSCREENPOINT": + "UnityEngine.RectTransformUtility.WorldToScreenPoint(Camera, Vector3)", + "KIOU_HOOK_RVA_JSON_FORMATTER_GET_DEFAULT": + "Google.Protobuf.JsonFormatter.get_Default()", + "KIOU_HOOK_RVA_JSON_FORMATTER_FORMAT": + "Google.Protobuf.JsonFormatter.Format(IMessage)", + "KIOU_HOOK_RVA_GAMECTRL_GET_USI_TEXT": + "Project.ShogiCore.GameController.GetUSIText()", + "KIOU_HOOK_RVA_POSITION_TO_SFEN": + "Project.ShogiCore.Position.ToSFEN()", + "KIOU_HOOK_RVA_USIPARSER_PARSE_USI": + "Project.ShogiCore.USIParser.ParseUSI(string)", + "KIOU_HOOK_RVA_KIFWRITEOPTIONS_CTOR": + "Project.ShogiCore.KIFWriteOptions.ctor()", + "KIOU_HOOK_RVA_KIFWRITER_WRITE": + "Project.ShogiCore.KIFWriter.Write(RecordManager, KIFWriteOptions)", + "KIOU_HOOK_RVA_RUN_RESET_USER_DATA_SEQ": + "Project.Title.TitleMenuPopupPresenter.RunResetUserDataSequenceAsync(CancellationToken)", + "KIOU_HOOK_RVA_RUN_DELETE_ACCOUNT_SEQ": + "Project.Title.TitleMenuPopupPresenter.RunDeleteAccountSequenceAsync(CancellationToken)", +} + +HEADER = """\ +#pragma once + +// =========================================================================== +// kiou_rva_{stem}.h — site addresses for KIOU {version} (CFBundleVersion {build}). +// +// GENERATED by tools/gen_rva_header.py from recipes/{module}.py and the +// direct-ABI anchors it carries. Do not hand-edit: fix the recipe (or the +// anchor) and regenerate. tools/check_recipes.py re-checks every hook-site +// value against the recipe on each run. +// =========================================================================== + +#define KIOU_HOOK_OBSERVER_SLOT_RVA 0x{observer:08X} +#define KIOU_HOOK_ENTRY_SLOT_BASE_RVA 0x{entry:08X} + +// CAVE_REGION[0] from recipes/{module}.py — the consumer's Makefile may +// override this, but the two must agree. +#ifndef KIOU_HOOK_CAVE_REGION_START +#define KIOU_HOOK_CAVE_REGION_START 0x{cave:X} +#endif +""" + + +def _macro_order() -> list[str]: + """Macro emission order: hook sites in catalog order, then direct-ABI.""" + text = (REPO_ROOT / "KIOUHook.m").read_text() + rows = re.findall( + r"\{\s*KIOU_HOOK_NAME_\w+\s*,\s*(?:-1|KIOU_HOOK_ID_\w+)\s*,\s*(KIOU_HOOK_RVA_\w+)\s*\}", + text, + ) + seen: list[str] = [] + for macro in rows: + if macro not in seen: + seen.append(macro) + return seen + + +def generate(version: str, index_path: str, build: int) -> str: + from tools.verify_sites import ( + find_method, + load_dump_index, + split_label, + types_by_name, + ) + + module = "v" + version.replace(".", "_") + recipe = RECIPES_DIR / f"{module}.py" + if not recipe.exists(): + raise SystemExit(f"error: no recipe at {recipe}") + + sites = _find_sites_assign(_load_module_ast(recipe)) + # Placeholder rows (rva None) reserve a cave for a method this build no + # longer has; they fall through to the anchor lookup below, which will + # correctly report them as unresolvable if nothing matches. + by_hook_id = {row[2]: row[0] for row in sites if row[0] is not None} + placeholders = {row[2] for row in sites if row[0] is None} + catalog = _catalog_macro_by_hook_id() + macro_by_hook_id = {hook_id: macro for hook_id, macro in catalog.items()} + + by_name = types_by_name(load_dump_index(index_path)) + + values: dict[str, int] = {} + for hook_id, rva in by_hook_id.items(): + macro = macro_by_hook_id.get(hook_id) + if macro: + values[macro] = rva + + # Hook sites a version's recipe omits still need an address for the + # JB / jailed build, so fall back to resolving their anchor from the + # canonical label from any other recipe. Labels can themselves be + # version-specific (a compiler-generated state machine is d__36 on one + # build and d__38 on the next), so every known spelling is tried. + labels: dict[str, list[str]] = {} + for other in [recipe, *RECIPES_DIR.glob("v*.py")]: + for row in _find_sites_assign(_load_module_ast(other)): + known = labels.setdefault(row[2], []) + if row[4] not in known: + known.append(row[4]) + + unresolved: list[str] = [] + for hook_id, macro in macro_by_hook_id.items(): + if macro in values: + continue + candidates = labels.get(hook_id, []) + if not candidates: + unresolved.append(f"{macro} (no recipe row or label for {hook_id})") + continue + for label in candidates: + type_name, method_name, params = split_label(label) + hit = find_method(by_name, type_name, method_name, param_types=params) + if hit is not None: + values[macro] = int(hit[1]["rva"], 0) + break + else: + if hook_id in placeholders: + # The recipe already says this method is gone from the + # build. Emit 0 so KIOUHookInstall refuses to hook it + # rather than MSHookFunction-ing a stale address. + values[macro] = 0 + else: + unresolved.append(f"{macro} ({' | '.join(candidates)})") + + for macro, anchor in DIRECT_ABI_ANCHORS.items(): + type_name, method_name, params = split_label(anchor) + hit = find_method(by_name, type_name, method_name, param_types=params) + if hit is None: + unresolved.append(f"{macro} ({anchor})") + else: + values[macro] = int(hit[1]["rva"], 0) + + if unresolved: + raise SystemExit( + "error: could not resolve on " + + version + + ":\n " + + "\n ".join(unresolved) + ) + + sys.path.insert(0, str(REPO_ROOT)) + os.environ["TARGET_VERSION"] = version + v = importlib.import_module(f"recipes.{module}") + + order = [m for m in _macro_order() if m in values] + order += [m for m in DIRECT_ABI_ANCHORS if m not in order] + width = max(len(m) for m in order) + 1 + + body = HEADER.format( + stem=module[1:], + version=version, + build=build, + module=module, + observer=v.HOOK_SLOT_RVA, + entry=v.ENTRY_SLOT_BASE_RVA, + cave=v.CAVE_REGION[0], + ) + lines = [body, ""] + lines += [f"#define {m:<{width}} 0x{values[m]:X}" for m in order] + lines.append("") + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--version", required=True, help="Target version, e.g. 1.1.0") + parser.add_argument("--build", required=True, type=int, help="CFBundleVersion") + parser.add_argument("--index", required=True, help="dump.cs.index.json for it") + parser.add_argument("--out", help="Output path (default: rva/kiou_rva_.h)") + args = parser.parse_args() + + text = generate(args.version, args.index, args.build) + out = pathlib.Path( + args.out or RVA_DIR / f"kiou_rva_{args.version.replace('.', '_')}.h" + ) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(text) + print(f"wrote {out}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main())