@@ -12,6 +12,7 @@
# include <atomic>
# include <chrono>
# include <cmath>
# include <climits>
# include <ctime>
# include <cstring>
# include "camera/camera.hpp"
@@ -29,6 +30,7 @@
# ifdef GKMS_WINDOWS
# include "../windowsPlatform.hpp"
# include "GyroEmu.hpp"
# include "cpprest/details/http_helpers.h"
# include "../resourceUpdate/resourceUpdate.hpp"
# endif
@@ -212,6 +214,191 @@ namespace GakumasLocal::HookMain {
// 从游戏自己的调用里捕获真实对象指针:
// Camera.get_main / Component.get_transform 是带 MethodInfo* 尾参的 managed 方法,
// 用 hook(而不是 UnityResolve 的 managed Invoke)才能拿到可靠结果。
# ifdef GKMS_WINDOWS
// 陀螺仪模拟(PC 无陀螺仪),三层:
// 1. GyroIcall —— 替换 il2cpp icall 缓存指针提供假读数。不能 hook 原生 icall( Windows 上它们是 return 常量
// 的桩,被链接器 ICF 合并进 IsDebugBuild 等,实测 [Debug] 变 True、开场黑屏),也不能 hook
// 托管包装(PC 上被内联,实测一次不被调用)。
// 2. GyroForceInit —— PC 把 IsPanoramaGyroDisabledOnPlatform() 内联成常量 true,控制器根本不创建,LateUpdate 里自己建。
// 3. 两个方法体足够大、实测会被调用的托管 hook 放开 UI 闸门。
# define GYRO_LOG_ONCE(name) { static bool logged = false; if (!logged) { logged = true; Log::InfoFmt("[GyroEmu] %s called (emu=%d)", name, (int)Config::enableGyroEmu); } }
DEFINE_HOOK ( void , LiveScenePanoramaContentPresenter_Initialize , ( void * self , void * screenRoot , bool isGyroSupported , void * mtd ) ) {
GYRO_LOG_ONCE ( " PanoramaContentPresenter.Initialize " ) ;
LiveScenePanoramaContentPresenter_Initialize_Orig ( self , screenRoot , isGyroSupported | | Config : : enableGyroEmu , mtd ) ;
}
DEFINE_HOOK ( bool , Photography3DPresenter_get_IsGyroSupported , ( void * self , void * mtd ) ) {
GYRO_LOG_ONCE ( " Photography3DPresenter.IsGyroSupported " ) ;
if ( Config : : enableGyroEmu ) return true ;
return Photography3DPresenter_get_IsGyroSupported_Orig ( self , mtd ) ;
}
// PC 版把 LiveUtility.IsPanoramaGyroDisabledOnPlatform()(恒 true)内联成常量,CampusGyroController 的 InitializeAsync
// 被整段跳过(log 证实 IsGyroAvailable 从未被查询),IsGyroReady 永远 false。Live 与拍摄各持有一个控制器,
// 所以在构造函数 hook 里统一把它置成初始化完成态,再在两条链路各留一个“控制器为空就自己建”的兜底。
// 字段偏移来自 3.3.0 il2cpp.h( CampusGyroController / VLGyro / LivePanoramaCameraManager)。
namespace GyroForceInit {
using Quaternion = UnityResolve : : UnityType : : Quaternion ;
template < typename T > T & F ( void * obj , size_t off ) { return * reinterpret_cast < T * > ( reinterpret_cast < char * > ( obj ) + off ) ; }
struct Api {
UnityResolve : : Class * ctrlClass ; UnityResolve : : Class * gyroClass ;
void ( * ctrlCtor ) ( void * , void * ) ; void ( * gyroCtor ) ( void * , void * ) ; void ( * gyroReset ) ( void * , void * ) ;
Quaternion * ( * gyroRaw ) ( Quaternion * , void * , void * ) ;
} ;
static const Api * GetApi ( ) {
static Api api {
Il2cppUtils : : GetClass ( " campus-submodule.Runtime.dll " , " Campus.Common " , " CampusGyroController " ) ,
Il2cppUtils : : GetClass ( " vl-unity.Runtime.dll " , " VL " , " VLGyro " ) ,
reinterpret_cast < void ( * ) ( void * , void * ) > ( Il2cppUtils : : GetMethodPointer ( " campus-submodule.Runtime.dll " , " Campus.Common " , " CampusGyroController " , " .ctor " ) ) ,
reinterpret_cast < void ( * ) ( void * , void * ) > ( Il2cppUtils : : GetMethodPointer ( " vl-unity.Runtime.dll " , " VL " , " VLGyro " , " .ctor " ) ) ,
reinterpret_cast < void ( * ) ( void * , void * ) > ( Il2cppUtils : : GetMethodPointer ( " vl-unity.Runtime.dll " , " VL " , " VLGyro " , " ResetRotation " ) ) ,
reinterpret_cast < Quaternion * ( * ) ( Quaternion * , void * , void * ) > ( Il2cppUtils : : GetMethodPointer ( " vl-unity.Runtime.dll " , " VL " , " VLGyro " , " get_gyroRotationRaw " ) ) ,
} ;
static const bool ok = [ ] {
if ( api . ctrlClass & & api . gyroClass & & api . ctrlCtor & & api . gyroCtor & & api . gyroReset & & api . gyroRaw ) return true ;
Log : : ErrorFmt ( " [GyroEmu] force-init: missing class/method (%p %p %p %p %p %p) " , api . ctrlClass , api . gyroClass , api . ctrlCtor , api . gyroCtor , api . gyroReset , api . gyroRaw ) ;
return false ;
} ( ) ;
return ok ? & api : nullptr ;
}
// 把一个已构造的 CampusGyroController 直接置成“初始化完成”态(等价于它自己 InitializeAsync 成功后的状态)
void InitController ( void * ctrl ) {
auto api = GetApi ( ) ;
if ( ! api | | ! ctrl | | F < bool > ( ctrl , 0x18 ) ) return ; // _initialized
const Quaternion identity ( 0 , 0 , 0 , 1 ) ;
auto & gyro = F < void * > ( ctrl , 0x10 ) ; // _gyro
if ( ! gyro ) { gyro = api - > gyroClass - > New < void * > ( ) ; api - > gyroCtor ( gyro , nullptr ) ; }
F < int > ( gyro , 0x14 ) = 1 ; // _updateType = Gyro
F < bool > ( gyro , 0x29 ) = true ; // gyroSupported
F < bool > ( gyro , 0x2A ) = true ; // _defaultGyroEnable
for ( size_t off : { 0x2C , 0x3C , 0x4C , 0x5C } ) F < Quaternion > ( gyro , off ) = identity ;
F < bool > ( gyro , 0x6C ) = true ; // _initialized
F < bool > ( gyro , 0x6D ) = true ; // _firstUpdate
api - > gyroReset ( gyro , nullptr ) ;
Quaternion raw ; api - > gyroRaw ( & raw , gyro , nullptr ) ;
F < Quaternion > ( ctrl , 0x1C ) = Quaternion ( - raw . x , - raw . y , - raw . z , raw . w ) ; // _baseRotation = Inverse(raw)
F < int > ( ctrl , 0x2C ) = INT_MIN ; // _lastAccessFrame
F < Quaternion > ( ctrl , 0x30 ) = identity ; // _rotation
F < bool > ( ctrl , 0x18 ) = true ; // _initialized
Log : : InfoFmt ( " [GyroEmu] force-init done: controller %p gyro %p " , ctrl , gyro ) ;
}
void * NewController ( ) {
auto api = GetApi ( ) ;
if ( ! api ) return nullptr ;
auto ctrl = api - > ctrlClass - > New < void * > ( ) ;
api - > ctrlCtor ( ctrl , nullptr ) ; // 构造函数 hook 会顺手 InitController
InitController ( ctrl ) ;
return ctrl ;
}
}
// 所有 CampusGyroController 一出生就初始化完成,Live / 拍摄 / 其他持有者都不用再关心 PC 上被跳过的 InitializeAsync
DEFINE_HOOK ( void , CampusGyroController_ctor , ( void * self , void * mtd ) ) {
CampusGyroController_ctor_Orig ( self , mtd ) ;
if ( Config : : enableGyroEmu ) GyroForceInit : : InitController ( self ) ;
}
// Live: PC 版可能连控制器都没建,LateUpdate 里兜底
DEFINE_HOOK ( void , LivePanoramaCameraManager_LateUpdate , ( void * self , void * mtd ) ) {
if ( Config : : enableGyroEmu & & self ) {
auto & ctrl = GyroForceInit : : F < void * > ( self , 0x60 ) ; // _gyroController
if ( ! ctrl ) ctrl = GyroForceInit : : NewController ( ) ;
else GyroForceInit : : InitController ( ctrl ) ;
}
LivePanoramaCameraManager_LateUpdate_Orig ( self , mtd ) ;
}
// 拍摄:PhotographySceneManager 把控制器交给 PhotographyActivityController,这里是它进相机控制器的唯一入口
DEFINE_HOOK ( void , PhotographyActivityController_Initialize , ( void * self , void * volumeProfile , void * gyroController , float arg , void * mtd ) ) {
if ( Config : : enableGyroEmu ) {
if ( ! gyroController ) gyroController = GyroForceInit : : NewController ( ) ;
else GyroForceInit : : InitController ( gyroController ) ;
}
PhotographyActivityController_Initialize_Orig ( self , volumeProfile , gyroController , arg , mtd ) ;
}
// PC 版 MSVC 把 SystemInfo.supportsGyroscope / Gyroscope.* 这些托管包装内联进了调用方,hook 函数本体拦不到。
// il2cpp 的 icall 包装里有一个 static 缓存指针(_il2cpp_icall_func),内联后所有副本共用同一个变量,
// 直接把这个变量改成我们的函数即可,不碰任何机器码,也不受链接器 ICF 合并影响。
namespace GyroIcall {
using Quaternion = UnityResolve : : UnityType : : Quaternion ;
using Vector3 = UnityResolve : : UnityType : : Vector3 ;
bool ( * orig_IsGyroAvailable ) ( ) = nullptr ;
bool ( * orig_getEnabled ) ( int ) = nullptr ;
void ( * orig_setEnabled ) ( int , bool ) = nullptr ;
void ( * orig_attitude ) ( int , Quaternion * ) = nullptr ;
void ( * orig_gravity ) ( int , Vector3 * ) = nullptr ;
bool IsGyroAvailable ( ) {
GYRO_LOG_ONCE ( " icall SystemInfo.IsGyroAvailable " ) ;
if ( Config : : enableGyroEmu ) return true ;
return orig_IsGyroAvailable ? orig_IsGyroAvailable ( ) : false ;
}
bool getEnabled ( int idx ) {
GYRO_LOG_ONCE ( " icall Gyroscope.getEnabled " ) ;
if ( Config : : enableGyroEmu ) return true ;
return orig_getEnabled ? orig_getEnabled ( idx ) : false ;
}
void setEnabled ( int idx , bool v ) {
GYRO_LOG_ONCE ( " icall Gyroscope.setEnabled " ) ;
if ( Config : : enableGyroEmu ) return ; // Windows 无传感器对象,原生实现解空指针
if ( orig_setEnabled ) orig_setEnabled ( idx , v ) ;
}
void attitude ( int idx , Quaternion * ret ) {
GYRO_LOG_ONCE ( " icall Gyroscope.attitude " ) ;
if ( Config : : enableGyroEmu ) { * ret = GyroEmu : : Attitude ( ) ; return ; }
if ( orig_attitude ) orig_attitude ( idx , ret ) ; else * ret = Quaternion ( 0 , 0 , 0 , 1 ) ;
}
void gravity ( int idx , Vector3 * ret ) {
GYRO_LOG_ONCE ( " icall Gyroscope.gravity " ) ;
if ( Config : : enableGyroEmu ) { * ret = GyroEmu : : Gravity ( ) ; return ; }
if ( orig_gravity ) orig_gravity ( idx , ret ) ; else * ret = Vector3 ( 0 , 0 , 0 ) ;
}
// 在 icall 包装函数体里找 `mov rXX,[rip+disp]; test rXX,rXX` 定位缓存变量,写入替换函数
template < typename F >
void Patch ( const char * label , void * wrapper , F * * origOut , F * replacement , const char * icallName ) {
* origOut = reinterpret_cast < F * > ( Il2cppUtils : : il2cpp_resolve_icall ( icallName ) ) ;
if ( ! wrapper ) { Log : : ErrorFmt ( " [GyroEmu] %s: wrapper not found " , label ) ; return ; }
auto p = reinterpret_cast < uint8_t * > ( wrapper ) ;
for ( int i = 0 ; i < 0x80 ; i + + ) {
if ( p [ i ] ! = 0x48 | | p [ i + 1 ] ! = 0x8B | | ( p [ i + 2 ] & 0xC7 ) ! = 0x05 ) continue ;
const int reg = ( p [ i + 2 ] > > 3 ) & 7 ;
const uint8_t testModrm = 0xC0 | ( reg < < 3 ) | reg ;
bool ok = false ;
for ( int j = i + 7 ; j < = i + 7 + 8 ; j + + )
if ( p [ j ] = = 0x48 & & p [ j + 1 ] = = 0x85 & & p [ j + 2 ] = = testModrm ) { ok = true ; break ; }
if ( ! ok ) continue ;
auto cache = reinterpret_cast < void * * > ( p + i + 7 + * reinterpret_cast < int32_t * > ( p + i + 3 ) ) ;
MEMORY_BASIC_INFORMATION mbi { } ;
if ( ! VirtualQuery ( cache , & mbi , sizeof ( mbi ) ) | | ! ( mbi . Protect & ( PAGE_READWRITE | PAGE_WRITECOPY ) ) ) {
Log : : ErrorFmt ( " [GyroEmu] %s: cache %p not writable " , label , cache ) ; return ;
}
Log : : InfoFmt ( " [GyroEmu] %s: icall cache %p (was %p, orig %p) -> %p " , label , cache , * cache , * origOut , replacement ) ;
* cache = reinterpret_cast < void * > ( replacement ) ;
return ;
}
Log : : ErrorFmt ( " [GyroEmu] %s: icall cache pattern not found at %p " , label , wrapper ) ;
}
void Install ( ) {
Patch ( " IsGyroAvailable " , Il2cppUtils : : GetMethodPointer ( " UnityEngine.CoreModule.dll " , " UnityEngine " , " SystemInfo " , " IsGyroAvailable " ) ,
& orig_IsGyroAvailable , & IsGyroAvailable , " UnityEngine.SystemInfo::IsGyroAvailable() " ) ;
Patch ( " getEnabled_Internal " , Il2cppUtils : : GetMethodPointer ( " UnityEngine.InputLegacyModule.dll " , " UnityEngine " , " Gyroscope " , " getEnabled_Internal " ) ,
& orig_getEnabled , & getEnabled , " UnityEngine.Gyroscope::getEnabled_Internal(System.Int32) " ) ;
Patch ( " setEnabled_Internal " , Il2cppUtils : : GetMethodPointer ( " UnityEngine.InputLegacyModule.dll " , " UnityEngine " , " Gyroscope " , " setEnabled_Internal " ) ,
& orig_setEnabled , & setEnabled , " UnityEngine.Gyroscope::setEnabled_Internal(System.Int32,System.Boolean) " ) ;
Patch ( " attitude_Internal_Injected " , Il2cppUtils : : GetMethodPointer ( " UnityEngine.InputLegacyModule.dll " , " UnityEngine " , " Gyroscope " , " attitude_Internal_Injected " ) ,
& orig_attitude , & attitude , " UnityEngine.Gyroscope::attitude_Internal_Injected(System.Int32,UnityEngine.Quaternion&) " ) ;
Patch ( " gravity_Internal_Injected " , Il2cppUtils : : GetMethodPointer ( " UnityEngine.InputLegacyModule.dll " , " UnityEngine " , " Gyroscope " , " gravity_Internal_Injected " ) ,
& orig_gravity , & gravity , " UnityEngine.Gyroscope::gravity_Internal_Injected(System.Int32,UnityEngine.Vector3&) " ) ;
}
}
# endif
DEFINE_HOOK ( UnityResolve : : UnityType : : Camera * , Camera_get_main , ( void * mtd ) ) {
auto ret = Camera_get_main_Orig ( mtd ) ;
if ( Config : : enableFreeCamera & & ret & & ret ! = mainCameraCache ) {
@@ -869,7 +1056,16 @@ namespace GakumasLocal::HookMain {
}
# endif
std : : unordered_set < void * > updatedFontPtrs { } ;
// A managed object address can be reused after the original object is collected, and the
// replacement Font is dropped by Resources.UnloadUnusedAssets on scene loads. Pair the address
// with both InstanceIDs so a recycled address or a reloaded Font is set up again - and so the
// map is never cleared wholesale, which would re-initialize every live asset and reshuffle the
// atlas under text that is already on screen.
struct UpdatedFontState {
int fontAssetInstanceId ;
int sourceFontInstanceId ;
} ;
std : : unordered_map < void * , UpdatedFontState > updatedFontStates { } ;
void UpdateFont ( void * TMP_Textself ) {
if ( ! Config : : replaceFont ) return ;
static auto get_font = Il2cppUtils : : GetMethod ( " Unity.TextMeshPro.dll " ,
@@ -888,8 +1084,39 @@ namespace GakumasLocal::HookMain {
static auto set_sourceFontFile = Il2cppUtils : : GetMethod ( " Unity.TextMeshPro.dll " , " TMPro " ,
" TMP_FontAsset " , " set_sourceFontFile " ) ;
static auto UpdateFontAssetData = Il2cppUtils : : GetMethod ( " Unity.TextMeshPro.dll " , " TMPro " ,
" TMP_FontAsset " , " UpdateFontAssetData " ) ;
static auto set_atlasPopulationMode = Il2cppUtils : : GetMethod (
" Unity.TextMeshPro.dll " , " TMPro " , " TMP_FontAsset " , " set_atlasPopulationMode " ,
{ " TMPro.AtlasPopulationMode " } ) ;
static auto set_isMultiAtlasTexturesEnabled = Il2cppUtils : : GetMethod (
" Unity.TextMeshPro.dll " , " TMPro " , " TMP_FontAsset " , " set_isMultiAtlasTexturesEnabled " ,
{ " System.Boolean " } ) ;
static auto ClearFontAssetData = Il2cppUtils : : GetMethod (
" Unity.TextMeshPro.dll " , " TMPro " , " TMP_FontAsset " , " ClearFontAssetData " ,
{ " System.Boolean " } ) ;
static auto get_atlasWidth = Il2cppUtils : : GetMethod ( " Unity.TextMeshPro.dll " , " TMPro " ,
" TMP_FontAsset " , " get_atlasWidth " ) ;
static auto set_atlasWidth = Il2cppUtils : : GetMethod (
" Unity.TextMeshPro.dll " , " TMPro " , " TMP_FontAsset " , " set_atlasWidth " ,
{ " System.Int32 " } ) ;
static auto set_atlasHeight = Il2cppUtils : : GetMethod (
" Unity.TextMeshPro.dll " , " TMPro " , " TMP_FontAsset " , " set_atlasHeight " ,
{ " System.Int32 " } ) ;
static auto get_atlasTextureCount = Il2cppUtils : : GetMethod ( " Unity.TextMeshPro.dll " , " TMPro " ,
" TMP_FontAsset " , " get_atlasTextureCount " ) ;
static auto GetInstanceID = Il2cppUtils : : GetMethod (
" UnityEngine.CoreModule.dll " , " UnityEngine " , " Object " , " GetInstanceID " ) ;
// TMP's own "this font asset changed, re-layout everything using it" broadcast.
static auto ON_FONT_PROPERTY_CHANGED = Il2cppUtils : : GetMethod (
" Unity.TextMeshPro.dll " , " TMPro " , " TMPro_EventManager " , " ON_FONT_PROPERTY_CHANGED " ,
{ " System.Boolean " , " UnityEngine.Object " } ) ;
if ( ! get_font | | ! set_font | | ! get_name | | ! set_sourceFontFile | | ! set_atlasPopulationMode | |
! set_isMultiAtlasTexturesEnabled | | ! ClearFontAssetData | | ! get_atlasWidth | |
! set_atlasWidth | | ! set_atlasHeight | | ! get_atlasTextureCount | | ! GetInstanceID | |
! ON_FONT_PROPERTY_CHANGED ) {
Log : : Error ( " UpdateFont disabled: a TMP_FontAsset method could not be resolved " ) ;
return ;
}
auto fontAsset = get_font - > Invoke < void * > ( TMP_Textself ) ;
if ( ! fontAsset ) {
@@ -910,13 +1137,61 @@ namespace GakumasLocal::HookMain {
auto newFont = GetReplaceFont ( ) ;
if ( ! newFont ) return ;
set_sourceFontFile - > Invoke < void > ( fontAsset , newFont ) ;
if ( ! updatedFontPtrs . contains ( fontAsset ) ) {
updatedFontPtrs . emplace ( fontAsset ) ;
UpdateFontAssetData - > Invoke < void > ( fontAsset ) ;
const auto fontInstanceId = GetInstanceID - > Invoke < int > ( fontAsset ) ;
const auto sourceFontInstanceId = GetInstanceID - > Invoke < int > ( newFont ) ;
const auto known = updatedFontStates . find ( fontAsset ) ;
const bool isNewAsset = known = = updatedFontStates . end ( ) | |
known - > second . fontAssetInstanceId ! = fontInstanceId ;
const bool sourceFontReloaded = ! isNewAsset & &
known - > second . sourceFontInstanceId ! = sourceFontInstanceId ;
if ( isNewAsset | | sourceFontReloaded ) {
// Mark the asset before rebuilding it: TMP can re-enter one of the text hooks below
// while the rebuild is in progress.
updatedFontStates . insert_or_assign (
fontAsset , UpdatedFontState { fontInstanceId , sourceFontInstanceId } ) ;
set_sourceFontFile - > Invoke < void > ( fontAsset , newFont ) ;
}
if ( updatedFontPtrs . size ( ) > 200 ) updatedFontPtrs . clear ( ) ;
// Only a genuinely new asset is rebuilt. Resources.UnloadUnusedAssets drops the replacement
// Font on most scene loads, and re-clearing on every reload throws away glyphs that text
// already on screen is still pointing at - it keeps its stale atlas coordinates and renders
// the wrong characters, or nothing at all once the atlas is no longer refilled. The glyphs
// already in the atlas came from an identical font file, so a reloaded Font can simply be
// re-assigned without touching them.
if ( isNewAsset ) {
set_atlasPopulationMode - > Invoke < void > ( fontAsset , 1 ) ; // AtlasPopulationMode.Dynamic
set_isMultiAtlasTexturesEnabled - > Invoke < void > ( fontAsset , true ) ;
// Glyphs that do not fit on atlas page 0 get a second material, which puts them in
// textInfo.meshInfo[1]. The game's stylized text only submits meshInfo[0], so those
// characters vanish - correct advance, no quad - while ordinary UI still draws them.
// Measured from a frame capture: shelf pitch 79px, ~735 glyphs per 2048x2048 page, and
// page 0 was completely full, so the spill was guaranteed rather than occasional.
// 4096x4096 holds ~2900 glyphs (16MB of A8); 20 minutes of play reaches about a third.
// ponytail: still a ceiling. The atlasTextureCount check below reports when it is hit;
// the real fix is making the stylized text submit its sub meshes.
if ( get_atlasWidth - > Invoke < int > ( fontAsset ) < 4096 ) {
set_atlasWidth - > Invoke < void > ( fontAsset , 4096 ) ;
set_atlasHeight - > Invoke < void > ( fontAsset , 4096 ) ;
}
// Clear, not UpdateFontAssetData: the latter re-adds the asset's entire original
// character table first, spending page 0 on glyphs the translation never shows.
ClearFontAssetData - > Invoke < void > ( fontAsset , false ) ;
// Clearing invalidates the atlas coordinates baked into every mesh already generated
// from this asset. Without this broadcast those texts keep the stale coordinates: the
// game's ruby text objects, laid out before the rebuild, showed wrong glyphs when the
// old character table was re-added and nothing at all once it was not.
ON_FONT_PROPERTY_CHANGED - > Invoke < void > ( true , fontAsset ) ;
}
static int reportedAtlasPages = 1 ;
const auto atlasPages = get_atlasTextureCount - > Invoke < int > ( fontAsset ) ;
if ( atlasPages > reportedAtlasPages ) {
reportedAtlasPages = atlasPages ;
Log : : ErrorFmt ( " Font atlas spilled to page %d on [%s] - stylized text will start "
" dropping characters " ,
atlasPages - 1 , fontAssetName ? fontAssetName - > ToString ( ) . c_str ( ) : " ? " ) ;
}
set_font - > Invoke < void > ( TMP_Textself , fontAsset ) ;
// auto fontMaterial = get_material->Invoke<void*>(fontAsset);
@@ -1224,10 +1499,21 @@ namespace GakumasLocal::HookMain {
return ret;
}*/
// 名片编辑页的 P 偶像页签用 UserIdolCardSkinList.Exists(skin.Id) 过滤“拥有的皮肤”,随后按底卡去
// UserIdolCardList 查 UserIdolCard 取 IllustrationStage。下面 Exists 对所有 i_card-skin-* 返回 true 会把
// 没有底卡的皮肤也放进去,查到 null 直接 NRE,整个界面初始化中断(表现为卡死)。该模型构造期间放行真实结果。
static bool meishiModelConstructing = false ;
DEFINE_HOOK ( void , MeishiBasePIdolTabPageModel_ctor , ( void * self , void * mtd ) ) {
meishiModelConstructing = true ;
MeishiBasePIdolTabPageModel_ctor_Orig ( self , mtd ) ;
meishiModelConstructing = false ;
}
DEFINE_HOOK ( bool , UserIdolCardSkinCollection_Exists , ( void * self , Il2cppString * id , void * mtd ) ) { // Live默认选择
auto ret = UserIdolCardSkinCollection_Exists_Orig ( self , id , mtd ) ;
// Log::DebugFmt("UserIdolCardSkinCollection_Exists: %s, ret: %d", id->ToString().c_str(), ret);
if ( ! ( Config : : dbgMode & & Config : : unlockAllLive ) ) return ret ;
if ( meishiModelConstructing ) return ret ;
if ( id ) {
std : : string idStr = id - > ToString ( ) ;
@@ -1724,6 +2010,75 @@ namespace GakumasLocal::HookMain {
return AddPhotographyIdolSkinItems ( ret , characterId ? characterId - > ToString ( ) : " " , getItem , getId ) ;
}
// ---- 技能卡皮肤(ProduceCardSkin):没有专属皮也播专属皮的演出 ----
// 考试内所有皮肤表现都由 ExamCardData.ProduceCardSkinId 派生:
// PrePlayCardAsync -> PlayCutInAsync(skinId) -> TryGetCutInParams -> mov_{id}_skillcard.usm + cutInType
// ExamCardButtonImage.SetCard -> GetThumbnailLoopMovieName(skinId) -> 手牌循环缩略图
// 所以在这个 getter 上,把「没挂皮肤」的卡补成主表里同 ProduceCardId、CutInType 最高的专属皮即可。
// 注意:该 getter 另有 ExamDataSerializeData / ExamParameterModel 两个读取方,
// 伪造的 id 会进考试状态序列化和 play log(竞赛回放会上传),纯本地表现之外的副作用仅此。
std : : unordered_map < std : : string , std : : string > produceCardSkinCutInRemap { } ; // produceCardId -> 专属皮 id
bool produceCardSkinCutInRemapBuilt = false ;
void BuildProduceCardSkinCutInRemap ( ) {
if ( produceCardSkinCutInRemapBuilt ) return ;
static auto getMaster = Il2cppUtils : : GetMethod ( " Assembly-CSharp.dll " , " Campus.Common.Master " , " MasterManager " , " get_ProduceCardSkinMaster " ) ;
static auto masterKlass = Il2cppUtils : : GetClass ( " Assembly-CSharp.dll " , " Campus.Common.Master " , " ProduceCardSkinMaster " ) ;
static auto getId = Il2cppUtils : : GetMethod ( " Assembly-CSharp.dll " , " Campus.Common.Proto.Client.Master " , " ProduceCardSkin " , " get_Id " ) ;
static auto getCardId = Il2cppUtils : : GetMethod ( " Assembly-CSharp.dll " , " Campus.Common.Proto.Client.Master " , " ProduceCardSkin " , " get_ProduceCardId " ) ;
static auto getCutInType = Il2cppUtils : : GetMethod ( " Assembly-CSharp.dll " , " Campus.Common.Proto.Client.Master " , " ProduceCardSkin " , " get_CutInType " ) ;
if ( ! getMaster | | ! masterKlass | | ! getId | | ! getCardId | | ! getCutInType ) return ;
// 取 1 参重载 GetAllWithSortByKey(sortType),末尾补隐藏 MethodInfo*
static auto getAll_mtd = Il2cppUtils : : il2cpp_class_get_method_from_name ( masterKlass - > address , " GetAllWithSortByKey " , 1 ) ;
if ( ! getAll_mtd ) return ;
auto getAll = reinterpret_cast < UnityResolve : : UnityType : : List < void * > * ( * ) ( void * , int , void * ) > ( getAll_mtd - > methodPointer ) ;
auto master = getMaster - > Invoke < void * > ( nullptr ) ;
if ( ! master ) return ;
auto list = getAll ( master , 0 , ( void * ) getAll_mtd ) ;
if ( ! list ) return ;
// produceCardId -> (最高 cutInType, 皮肤 id)。没有 cut-in 的皮抄过来也没演出,直接跳过
std : : unordered_map < std : : string , std : : pair < int , std : : string > > best { } ;
int valid = 0 , withCutIn = 0 ;
for ( auto item : list - > ToArray ( ) - > ToVector ( ) ) {
if ( ! item ) continue ; // ToVector 含 List 容量内的 null 槽位
auto id = getId - > Invoke < Il2cppString * > ( item ) ;
auto cardId = getCardId - > Invoke < Il2cppString * > ( item ) ;
if ( ! id | | ! cardId ) continue ;
+ + valid ;
// ProduceCardSkinCutInType: Unknown=0 / Small=1 / Mid=2 / Grand=3
const int cutIn = getCutInType - > Invoke < int > ( item ) ;
if ( cutIn < = 0 ) continue ;
+ + withCutIn ;
auto & slot = best [ cardId - > ToString ( ) ] ;
if ( cutIn > slot . first ) slot = { cutIn , id - > ToString ( ) } ;
}
if ( ! valid ) return ; // 主表还没加载,下次再建
for ( const auto & [ cardId , slot ] : best ) produceCardSkinCutInRemap . emplace ( cardId , slot . second ) ;
produceCardSkinCutInRemapBuilt = true ;
Log : : InfoFmt ( " ProduceCardSkin cut-in remap built: %zu cards (rows=%d, withCutIn=%d) " ,
produceCardSkinCutInRemap . size ( ) , valid , withCutIn ) ;
}
// 卡本身没挂皮肤时,按 produceCardId 补一个带 cut-in 的皮肤 id;
// 已经挂了皮肤的走原样,不覆盖玩家自己的选择
DEFINE_HOOK ( Il2cppString * , ExamCardData_get_ProduceCardSkinId , ( void * self , void * mtd ) ) {
auto ret = ExamCardData_get_ProduceCardSkinId_Orig ( self , mtd ) ;
if ( ! Config : : dbgMode ) return ret ;
if ( ret & & ! ret - > ToString ( ) . empty ( ) ) return ret ;
static auto ExamCardData_get_Id = Il2cppUtils : : GetMethod ( " Assembly-CSharp.dll " , " Campus.InGame.Card " , " ExamCardData " , " get_Id " ) ;
if ( ! ExamCardData_get_Id ) return ret ;
auto cardId = ExamCardData_get_Id - > Invoke < Il2cppString * > ( self ) ;
if ( ! cardId ) return ret ;
BuildProduceCardSkinCutInRemap ( ) ;
auto it = produceCardSkinCutInRemap . find ( cardId - > ToString ( ) ) ;
return it = = produceCardSkinCutInRemap . end ( ) ? ret : Il2cppString : : New ( it - > second ) ;
}
void * getCompletedUniTask ( ) {
static auto unitask_klass = Il2cppUtils : : GetClass ( " UniTask.dll " , " Cysharp.Threading.Tasks " , " UniTask " ) ;
static auto CompletedTask_field = unitask_klass - > Get < UnityResolve : : Field > ( " CompletedTask " ) ;
@@ -2480,6 +2835,11 @@ namespace GakumasLocal::HookMain {
" ExchangeSpToNormal " ,
{ " Campus.Common.Proto.Client.Enums.ProduceStepType " } ) ) ;
ADD_HOOK ( ExamCardData_get_ProduceCardSkinId ,
Il2cppUtils : : GetMethodPointer (
" Assembly-CSharp.dll " , " Campus.InGame.Card " , " ExamCardData " ,
" get_ProduceCardSkinId " ) ) ;
/* // 此 block 为 MasterBase 相关的 hook,后来发现它们最后都会调用 MessageExtensions.MergeFrom 进行构造,遂停用。现留档以备用
// ADD_HOOK(MasterBase_GetAll, Il2cppUtils::GetMethodPointer("quaunity-master-manager.Runtime.dll", "Qua.Master",
// "MasterBase`2", "GetAll", {"*", "*", "*", "*", "*"}));
@@ -2533,6 +2893,8 @@ namespace GakumasLocal::HookMain {
if ( UserIdolCardSkinCollection_Exists_mtd ) {
ADD_HOOK ( UserIdolCardSkinCollection_Exists , UserIdolCardSkinCollection_Exists_mtd - > methodPointer ) ;
}
ADD_HOOK ( MeishiBasePIdolTabPageModel_ctor , Il2cppUtils : : GetMethodPointer ( " Assembly-CSharp.dll " , " Campus.OutGame " ,
" MeishiBasePIdolTabPageModel " , " .ctor " ) ) ;
auto UserCostumeCollection_klass = Il2cppUtils : : GetClass ( " Assembly-CSharp.dll " , " Campus.Common.User " ,
" UserCostumeCollection " ) ;
@@ -2728,6 +3090,18 @@ namespace GakumasLocal::HookMain {
" RenderPipeline " , " EndCameraRendering " ) ) ;
# ifdef GKMS_WINDOWS
ADD_HOOK ( LiveScenePanoramaContentPresenter_Initialize , Il2cppUtils : : GetMethodPointer ( " Assembly-CSharp.dll " , " Campus.Live " ,
" LiveScenePanoramaContentPresenter " , " Initialize " ) ) ;
ADD_HOOK ( Photography3DPresenter_get_IsGyroSupported , Il2cppUtils : : GetMethodPointer ( " Assembly-CSharp.dll " , " Campus.Photography " ,
" Photography3DPresenter " , " get_IsGyroSupported " ) ) ;
GyroIcall : : Install ( ) ;
ADD_HOOK ( LivePanoramaCameraManager_LateUpdate , Il2cppUtils : : GetMethodPointer ( " campus-submodule.Runtime.dll " , " Campus.Live " ,
" LivePanoramaCameraManager " , " LateUpdate " ) ) ;
ADD_HOOK ( CampusGyroController_ctor , Il2cppUtils : : GetMethodPointer ( " campus-submodule.Runtime.dll " , " Campus.Common " ,
" CampusGyroController " , " .ctor " ) ) ;
ADD_HOOK ( PhotographyActivityController_Initialize , Il2cppUtils : : GetMethodPointer ( " campus-submodule.Runtime.dll " , " Campus.Photography " ,
" PhotographyActivityController " , " Initialize " ) ) ;
ADD_HOOK ( WindowHandle_SetWindowLong , Il2cppUtils : : GetMethodPointer ( " Assembly-CSharp.dll " , " Campus.Common.StandAloneWindow " ,
" WindowHandle " , " SetWindowLong " ) ) ;
//ADD_HOOK(WindowHandle_SetWindowLong32, Il2cppUtils::GetMethodPointer("Assembly-CSharp.dll", "Campus.Common.StandAloneWindow",