mirror of
https://git.chinosk6.cn/chinosk/gkms-localify-dmm.git
synced 2026-10-06 05:18:06 +07:00
feat: 未拥有技能卡皮肤时也播放该卡的 cut-in 演出
考试内所有皮肤表现都由 ExamCardData.ProduceCardSkinId 派生:
PrePlayCardAsync -> PlayCutInAsync(skinId) -> TryGetCutInParams
-> mov_{id}_skillcard.usm + cutInType,以及 ExamCardButtonImage
-> GetThumbnailLoopMovieName(skinId) 的手牌循环缩略图。
于是在该 getter 上,把没挂皮肤的卡按 produceCardId 补成主表里
CutInType 最高的皮肤 id,一个 hook 覆盖 cut-in 与手牌缩略图。
映射表从 ProduceCardSkinMaster 惰性构建一次。
跟随 dbgMode 生效,不额外加配置项和 GUI 开关。
已知代价:该 getter 另有 ExamDataSerializeData 与 ExamParameterModel
两个读取方,伪造的 skinId 会进考试状态序列化和 play log,
竞赛回放会随之上传。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
098a41459e
commit
5af7956318
1 file changed
+74
@@ -1724,6 +1724,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 +2549,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", {"*", "*", "*", "*", "*"}));
|
||||
|
||||
Reference in new issue
Block a user