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 5ddeea4..49431df 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" @@ -21,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 @@ -136,12 +136,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; @@ -163,12 +163,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; @@ -197,7 +197,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); @@ -218,7 +218,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; } } @@ -242,7 +242,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]; @@ -279,7 +279,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; } } @@ -310,14 +310,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; } @@ -330,13 +330,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}; } @@ -351,21 +351,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]); } } @@ -378,37 +378,42 @@ 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; - (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). { - 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; 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; + 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; 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 c0c9b3b..935ed68 100644 --- a/Hook/AfkDisable.m +++ b/Hook/AfkDisable.m @@ -31,7 +31,8 @@ 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)", + @"[AFK] installed: orig=%p gate=KIOU_FEATURE_DISABLE_AFK", (void *)s_origGO_IsAfkEnabled]); } diff --git a/Hook/AiSpecialSupport.m b/Hook/AiSpecialSupport.m new file mode 100644 index 0000000..a18205a --- /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-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, + (void *)s_origMP_FreeRemaining, + (void *)s_origMP_PaidAvailable, + (int)KIOUEditorFeatureEnabled(KIOU_FEATURE_AI_SPECIAL_SUPPORT)]); +} diff --git a/Hook/AssistEnable.m b/Hook/AssistEnable.m index ea31a72..6b08a47 100644 --- a/Hook/AssistEnable.m +++ b/Hook/AssistEnable.m @@ -44,11 +44,13 @@ 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)", + @"[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 d450175..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,26 +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 *modelPath, void *settings); +typedef void (*BSEEvaluateAsync_t)(void *self, void *position, void *methodInfo); -typedef void (*BSECtor_t)(void *self, void *evalPath, void *settings); +static BSECtor_t s_origBSE_ctor = NULL; +static BSEEvaluateAsync_t s_origBSE_evaluateAsync = NULL; +static uintptr_t g_unityBaseForAssist = 0; + +#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 BSECtor_t s_origBSE_ctor = NULL; -static BSEEnsureInit_t s_origBSE_ensureInit = NULL; -static uintptr_t g_unityBaseForAssist = 0; +static BSEEnsureInit_t s_origBSE_ensureInit = NULL; +#endif -static void hook_BSE_ctor(void *self, void *evalPath, void *settings) { +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 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); @@ -58,14 +97,24 @@ 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]); +#endif } @catch (NSException *e) { IPALog([NSString stringWithFormat: - @"[ASSIST-TUNE] BSE ctor override exception: %@", e]); + @"[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 +// 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); @@ -78,22 +127,47 @@ 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] SetHashSize site unresolved — skipping"); + 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] 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]); + } +} +#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 +// 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) { + 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) { + s_origBSE_evaluateAsync(self, position, methodInfo); } } @@ -102,13 +176,33 @@ 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); +#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: BSE.ctor orig=%p EnsureInit orig=%p " - @"(depth=%d skill=%d hash=%d MB)", + @"[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, + (void *)s_origBSE_evaluateAsync, (int)KIOUEditorAssistDepth(), (int)KIOUEditorAssistSkillLevel(), - (int)KIOUEditorAssistHashMB()]); + (int)KIOUEditorAssistHashMB(), + (int)KIOUEditorFeatureEnabled(KIOU_FEATURE_INGAME_ANALYSIS)]); +#endif } diff --git a/Hook/Collection.m b/Hook/Collection.m index 23ea65f..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; } @@ -81,7 +82,8 @@ 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)", + @"[COLLECTION] installed: orig=%p mode=observationOnly", (void *)s_origCollectionPresetReply_merge]); } diff --git a/Hook/Common.h b/Hook/Common.h index d5e536b..b41b300 100644 --- a/Hook/Common.h +++ b/Hook/Common.h @@ -126,6 +126,14 @@ 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_KIFU_AUTOSAVE, // KiouEditor Kif/ pipeline — write .kif on OnMatchEndAsync + KIOU_FEATURE_SEAT_FILTER, // KiouEditor Hook/MatchingFilter — reject unwanted seat (default off) KIOU_FEATURE_COUNT, }; @@ -148,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); @@ -155,6 +169,43 @@ 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 +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. +// +// 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 @@ -173,3 +224,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/Hook/Common.m b/Hook/Common.m index c4baea9..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"); } // --------------------------------------------------------------------------- @@ -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/Hook/FriendUnhide.m b/Hook/FriendUnhide.m index 3764264..ad463aa 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,917 +19,46 @@ // +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 // GameObject's klass, then cached for subsequent ctor fires. // =========================================================================== -#define RVA_HOME_UTILITY_PRESENTER_CTOR 0x5A9F298 - #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)", + @"[HOME] fire: event=friendTap action=presentSettings self=%p go=%p", self, thisGo]); 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)", + @"[HOME] fire: event=cloneTap action=presentSettings self=%p go=%p", self, thisGo]); KIOUEditorPresentSettings(); return; @@ -932,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); @@ -945,62 +79,47 @@ 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); 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", + @"[HOME] resolved: subject=homeUtilityView view=%p 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); 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 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; } else { - IPALog(@"[HOME] friend gameObject lookup failed"); + IPALog(@"[HOME] skipped: reason=friendGameObjectMissing"); } } - // 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)", + @"[HOME] fire: at=presenterCtor mainThread=%d prevView=%p view=%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"); @@ -1013,21 +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]); - // 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. + @"[HOME] resolved: cloneGameObject=%p", cloneGo]); static bool s_textReconDone = false; if (!s_textReconDone) { void *cloneTfRecon = goTransformOf(cloneGo); @@ -1035,85 +140,67 @@ 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: - @"[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]); } } } - // 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"); + IPALog(@"[HOME] dumped: subject=cloneHierarchy phase=phase2c"); 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"); + 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]); - } -} - -// 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"); + IPALog([NSString stringWithFormat:@"[HOME] exception: error=%@", e]); } } +// --------------------------------------------------------------------------- +// 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); + 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.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..27b230e --- /dev/null +++ b/Hook/FriendUnhideBridge.m @@ -0,0 +1,439 @@ +#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] 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, + 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 +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 +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 +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 + +// 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; + +// 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: method=Component.get_gameObject ptr=%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: method=GameObject.SetActive ptr=%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: method=GameObject.get_transform ptr=%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: method=Transform.get_parent ptr=%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: 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] skipped: at=Tf.SetParent reason=methodPointerNull"); + return; + } + IPALog([NSString stringWithFormat: + @"[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); +} + +// 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: 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] skipped: at=Tf.GetSiblingIndex reason=methodPointerNull"); + 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: method=UnityEngine.Object.get_name ptr=%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] dumped: subject=hierarchyNode indent=\"%@\" 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). 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) { + 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; + 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)addr; + 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] skipped: reason=noImageComponent 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: 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] skipped: at=Image.set_sprite reason=methodPointerNull", tag]); + return false; + } + IPALog([NSString stringWithFormat: + @"[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; +} + +// 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] resolved: subject=cloneSprite ptr=%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..8af1e8c --- /dev/null +++ b/Hook/FriendUnhideBridgeUI.m @@ -0,0 +1,460 @@ +#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). +// 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; + 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); +} + +// 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: 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; + + 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] resolved: 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] 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; + 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: 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] fire: at=SetAllDirty"); +} + +// 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] resolved: target=%s component=\"%s\" ptr=%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] skipped: reason=noSprite source=%s", 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] fire: 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] skipped: reason=buttonPtrInvalid ptr=%p", tag, uiButton]); + return; + } + void *btnGo = gameObjectOf(uiButton); + void *btnTf = goTransformOf(btnGo); + IPALog([NSString stringWithFormat: + @"[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] dumped: subject=btnTf reason=noImageLeaf", + tag]); + dumpHierarchy(btnTf, 0, 3); + return; + } + void *imageGo = gameObjectOf(imageTf); + IPALog([NSString stringWithFormat: + @"[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] resolved: component=\"UnityEngine.UI.Image\" ptr=%p", + tag, imageComp]); + if (!ptrLooksValid(imageComp)) return; + + void *sprite = readPtr(imageComp, 0xD8); + IPALog([NSString stringWithFormat: + @"[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: subject=titleMenuSprite", + 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: 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] skipped: at=Tf.SetSiblingIndex reason=methodPointerNull"); + 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: method=Component.get_transform ptr=%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] skipped: phase=enumRecon reason=bridgeIncomplete"); + 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] fire: phase=enum subject=objectKlass 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] resolved: phase=enum name=%s argc=%u generic=%d method=%p", + name, argc, isGeneric, method]); + hits++; + } + IPALog([NSString stringWithFormat: + @"[HOME] resolved: phase=enum instantiateVariants=%d", 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: method=\"Instantiate(Object)\" generic=false ptr=%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: 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] skipped: at=instantiateDirect reason=methodPointerNull"); + return NULL; + } + IPALog([NSString stringWithFormat: + @"[HOME] fire: at=instantiateDirect 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] 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: method=\"Instantiate(Object,Transform)\" ptr=%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..af2f473 --- /dev/null +++ b/Hook/FriendUnhideBridge_Private.h @@ -0,0 +1,69 @@ +#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); + +// 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) diff --git a/Hook/GrpcLogging.m b/Hook/GrpcLogging.m index 2e0851b..39373d9 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" @@ -95,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]); } } } @@ -123,13 +124,19 @@ 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", + @"[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 c7e01a5..25c0be8 100644 --- a/Hook/MatchingPlayer.m +++ b/Hook/MatchingPlayer.m @@ -53,30 +53,62 @@ 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] skipped: reason=selfPtrInvalid"); + 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; + // 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] refresh: field=selfUserId reason=stale from=%@ to=%@", + configuredSelf, activeUserId]); + KIOUSetSelfUserId(activeUserId); + configuredSelf = activeUserId; + } else if (!configuredSelf && activeUserId.length > 0) { + IPALog([NSString stringWithFormat: + @"[MATCH] applied: field=selfUserId source=active value=%@", + 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); } 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; } @@ -87,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]); } @@ -98,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]); } } @@ -113,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]); } } @@ -126,9 +158,10 @@ 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=%@", + @"[MATCH] installed: orig=%p selfUserId=%@", (void *)s_origMatchingPlayer_merge, configured ?: @"(unset, using heuristic)"]); } diff --git a/Hook/PremiumUnlock.m b/Hook/PremiumUnlock.m index 2ee011a..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]); } } @@ -73,15 +73,18 @@ 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", + @"[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 c44aea8..d49ea35 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)) { @@ -59,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]); } } } @@ -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 { @@ -99,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]); } } @@ -117,12 +126,14 @@ 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", + @"[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 3bd34e3..2d7bbfd 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: + @"[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", + 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 { @@ -77,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; @@ -88,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]); @@ -103,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"); } } @@ -117,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; @@ -130,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]); @@ -152,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"); } } @@ -166,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++) { @@ -176,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; @@ -186,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"); } } @@ -207,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; } @@ -216,6 +225,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..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,13 +76,14 @@ 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. } 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..4684f96 100644 --- a/Hook/VoiceUnlock.m +++ b/Hook/VoiceUnlock.m @@ -65,11 +65,13 @@ 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", + @"[VOICE] installed: satisfiesRuleOrig=%p cellModelGetIsLockedOrig=%p", (void *)s_origCharacterVoicePlayer_SatisfiesRule, (void *)s_origVoiceCellModel_get_IsLocked]); } diff --git a/KIOUHook.h b/KIOUHook.h index 792c840..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,27 +36,49 @@ #endif // --------------------------------------------------------------------------- -// Cave geometry and dispatcher slot RVAs. +// Target selection. +// +// 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. +// +// 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 -// Single observer-dispatcher slot. Every CAVE_OBSERVER cave loads this -// 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). -// 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 +#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 -// 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 +// --------------------------------------------------------------------------- +// Cave geometry. +// +// 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 (matches recipes/common.py + per-version CAVE_REGION). -#define KIOU_HOOK_CAVE_REGION_START 0x826F5E8 +// 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) @@ -110,6 +133,38 @@ 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_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, + // 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_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, + // 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, }; @@ -148,66 +203,36 @@ 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_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_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, + // 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, + // 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, }; -// --------------------------------------------------------------------------- -// 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.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 - -// --- Direct-ABI helper RVAs (1.0.1) -------------------------------------- -// 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 - // --------------------------------------------------------------------------- // Dispatcher state — defined by the consumer's ChinlanDispatcher.m. // --------------------------------------------------------------------------- @@ -269,10 +294,45 @@ 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[]; -// Direct-ABI helper lookups (KiouEditor, 1.0.1). hook_id = -1 in the catalog. +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[]; +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[]; +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 — 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 b13749d..90673b4 100644 --- a/KIOUHook.m +++ b/KIOUHook.m @@ -50,10 +50,47 @@ 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"; -// Direct-ABI helpers (KiouEditor, 1.0.1). hook_id = -1 in the catalog. +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"; +// 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"; +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_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 — 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). @@ -100,11 +137,47 @@ { 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 }, + // 棋桜覚醒 (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 }, + // 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 }, + { 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_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 }, + { 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 }, }; @@ -125,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) { @@ -152,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 277ed36..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, 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", @@ -41,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 bf2561a..5349f6c 100644 --- a/recipes/common.py +++ b/recipes/common.py @@ -91,6 +91,44 @@ "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, + # 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, + # 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, + "KIOU_HOOK_ID_MATCH_START_D3_MOVENEXT": 43, + "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 + # (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, + # 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. @@ -126,10 +164,33 @@ "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, + # 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, + "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, + "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 = 29 -ENTRY_SLOT_CAPACITY = 32 # 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 @@ -266,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 ] @@ -282,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_1.py b/recipes/v1_0_1.py index 4981c3c..7f2c344 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 @@ -69,5 +69,23 @@ (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"), + + # --- 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"), + + # --- 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 bb3c326..a196dd4 100644 --- a/recipes/v1_0_2.py +++ b/recipes/v1_0_2.py @@ -8,14 +8,19 @@ 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 # 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. @@ -57,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 @@ -82,5 +87,89 @@ (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"), + + # --- 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"), + + # --- 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"), + # 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"), + + # --- 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, "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 + # 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 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())