373 lines
56 KiB
Python
373 lines
56 KiB
Python
from pathlib import Path
|
||
import csv, json, re, hashlib, shutil, io
|
||
from collections import Counter, defaultdict
|
||
|
||
ROOT = Path(__file__).resolve().parents[3]
|
||
OUT = ROOT / 'HEROLL_OUTGAME_2.0.1'
|
||
CORE = ROOT / 'HEROLL_APK_REVERSE_ONLY/01_core_reverse_archive'
|
||
SRC = CORE / 'resource-tables/extracted/csv'
|
||
AUDIT = OUT / '99-审计与证据'
|
||
|
||
# 编号、模块、表清单、职责、配置关系、需要原生/实机补证的边界、元数据检索词。
|
||
SPECS = '''01|账号大厅与功能解锁|tutorial newbieguide modesortingorder localpush communitydata permanentkeyremoveschedule|负责大厅入口、新手引导、功能开放和持久化键清理。|tutorial 用 unlockContentKey 描述解锁内容;newbieguide 同时记录 clearStage、跳转目标和奖励;modesortingorder 决定模式展示排序。gameconfig 中的 UnlockStage、UnlockDay、Block 是入口检查候选配置,阶段索引不能直接改成玩家看到的关卡编号。|关卡门槛使用已通关还是当前关卡、离线保存和失败踢出实际分支、邮件服务端领取状态尚未闭合。|Lobby,UserData,Save,Mail,Tutorial,Unlock,Account
|
||
02|资源背包与体力|item itemgroup itemsource lobbyticket outgamereward|统一道具类型、礼包内容、资源来源以及大厅到局内的资源映射。|item 用 ERewardType/refStringId 描述奖励语义,itemgroup 存放并列 itemIds/itemCounts/weights;itemsource 记录来源跳转与 unlockKey。lobbyticket 将 ELobbyAsset 映射到 EAsset,并区分 defaultValue/boostValue。|itemgroup 是否有放回、容量溢出补偿和资源扣除顺序必须查消费者;负值、空值、0均保留,不能自行当成默认1。chargeEnergy 数组的单位和索引需调用点确认。|Asset,Item,Reward,Inventory,Energy,Ticket
|
||
03|永久装备与合成|equipment equipmentdata equipmentdefaultstatdata equipmentugradelevel equipmenttype equipskill mainequipmentscore|负责局外装备身份、装备位、品质星级、升级消耗和基础属性配置。|equipment 是装备目录;equipmentdata 用 level/starCount/grade 表达品质星级阶段,记录 maxUpgradeLevel 和合成材料要求;equipmentugradelevel 给出强化级别的 needMaterial/needGold;equipmentdefaultstatdata 区分普通与特殊装备基础值和升级增量。原表拼写 ugrade 保留。|不能把局外装备 level 与局内武器等级合并;基础值和升级增量如何组合、材料是否包含目标本体、升级返还和替换顺序仍需原生逻辑。equipmentdata 的材料备注明确带星级者不可作为材料。|Equipment,EquipMeta,EquipmentMerge,EquipmentUpgrade
|
||
04|槽位强化与永久属性|slotupgrade slotupgradekey bonusstatus|管理装备槽位强化及永久属性升级。|slotupgrade 按 slotNum/level 分档,独立列出 needItemID/needCount/needGold、upgradeValue 和 specialKey/specialValue;slotupgradekey 提供属性与主属性槽映射;bonusstatus 单独给出 maxLevel、upgradeValue、isPercent。|不同属性增量不可统一套一个倍率;槽位加成作用于基础属性还是最终值、升级开关与强化重置未闭合。|SlotUpgrade,BonusStatus
|
||
05|遗物与合成|relic relicdata|负责遗物目录、装备槽、属性技能和品质星级合成。|relic 保存 slotNum/baseStatus/statusValue/isPercent 以及 skill/skillValue/extraValue;relicdata 分离等级星级与 materialLevel/materialCount/sameGradeAnyRelicCount。普通、稀有、史诗行的 materialCount 为2。|数字2仅是字段原值,不能直接宣称总共消耗2件或3件;技能叠加、材料校验和解锁数组 relicContentsUnlockStage 的索引解释需要消费者。|Relic
|
||
06|宠物与成长|pet petskill petgrowthdata petgrowthevent petgrowthlevel|负责宠物获取、重复转化、碎片升级、技能开放与限时成长活动。|pet 给出 needPiece/needGold 数组、解锁价格、重复转化字段和战斗基础/增量参数;petskill 按 petId 与 skillUnlockLevel 配置技能;成长活动用 eventKey 关联事件权重、经验和等级奖励。pet 的 needPiece 注释指出数组首项为未使用值。|不得从数组长度直接推断可用等级数;ifOwnAutoConvert、满级碎片转钻、技能生效和选择概率的实际执行顺序待确认。|Pet,PetGrowth
|
||
07|坐骑与觉醒|mount mountlevel mountawaken mountskilldesc mountskilleffectgroup mounteventcount mounteventseasonitem|管理坐骑解锁、升级、觉醒、技能和活动材料兑换。|mount 同时记录 unlockPiece/unlockDia/unlockProductId 与溢出碎片返还;mountlevel 用 isOwnEffect 区分持有效果;mountawaken 按 grade/level 配置碎片与技能等级;mountskilleffectgroup 定义触发事件、概率、持续和叠加上限。|解锁路径是并列选项还是组合条件需要界面/调用验证;持有与出战效果叠加方式、觉醒是否重置和活动材料回收触发时间待确认。|Mount
|
||
08|命运属性|destinystat|管理命运词条的启用、品质、属性、特殊效果和候选权重。|destinystat 每行同时带 normalWeight/rareWeight 两套权重、isEnabled、isPercent、addValue 和 EDestinySpecialEffect。gameconfig 保存 destinyUnlockStages 与 destinySummonNeedCost 两个数组。|两组权重对应按钮和消费资源尚未映射;保留停用词条但不计作必定可抽取;锁定、替换及属性合成公式待查。|Destiny
|
||
09|收藏与图鉴|collection|按装备组合条件提供收藏属性。|collection 给出并列 equipmentIds/equipmentLevels/equipmentGrades,目标属性由 EMonoStatus/value 指定,isEnabled 控制配置启用。CollectionUnlockStage 原值为6。|条件是否要求同时持有、装备消耗与自动激活、value 的百分比单位不能只根据列名确认。|Collection
|
||
10|外观头像与称号|skin profile profileframe prefixtitle|管理皮肤、头像、头像框和称号展示及部分属性加成。|skin 包含 additionalStatus/additionalValue 与 maxSetCount/setAdditionalValueList,表明皮肤并非只有展示配置;profile/profileframe 用 unlockKey 表达解锁键;prefixtitle 描述展示顺序和颜色。|穿戴还是拥有即生效、套装阈值索引和称号获取来源需调用补证;未提取的本地化文本不翻译成虚构名称。|Skin,Profile,PrefixTitle
|
||
11|领地地块与研究|outgametile outgametileskill outgametileresearch outgametileresearchexp outgametileresearchstatus outgametileafkconfig|负责局外地块收集配置、地块技能、研究和地块包挂机。|outgametile 经 refNewTileId 连接局内事件,包含 defaultEquip、unlockStage、needPiece 和 skillUnlockLevel;研究用 totalUpgradeCount 映射 researchLevel/weaponPlusLevel,研究属性按 needResearchLevel/index 配置 needCost。地块包使用独立 afkTime/maxPackCount 及里程奖励。|研究经验如何累计、装备容量与更换约束、概率池过滤和里程重置需执行证据;地块包挂机不能与巡逻挂机共用计时参数。|OutGameTile,OutgameTile,TileResearch
|
||
12|巡逻挂机|afkconfig afkreward|按已通关进度配置离线巡逻与快速巡逻奖励。|afkreward 以 lastClearStage 区分 goldPerHour、rewards、amountPerCheckTime、maxAmount、dropPercent;afkconfig 注释明确时间单位为分钟,并分离普通巡逻、快速巡逻和广告次数。|不足一个检查周期如何舍入、超时结算、随机倍数是否作用于所有奖励、每日重置时区未闭合。|Afk,AFK,Patrol
|
||
13|抽卡与保底|gachatype gacha gachaceildata|统一抽卡按钮、消费、候选奖励、权重和保底配置。|gachatype 按 type/buttonNum 定义 gachaCount、免费或广告、消耗和替代道具;gacha 同 type 行组成配置候选池;gachaceildata 按 type 配 ceilCount/isResetBeforeCeil/ceilRewardString。rewardID=-1 注释表示指定类型中的全池。|prob 是未归一化字段,真实概率依赖运行时筛选和保底优先级;券抵扣、十连计数、跨池累计及动态池内容需实机或原生验证。|Gacha
|
||
14|商店礼包与广告|shopproductlist dailyshoplist relaypackagedata d3package pigbank dailypurchasereward groupbmshop groupexchangeshop promotioninfo triggerinfo interstitialexeptionregion|管理常驻商品、每日商店、连续礼包、存钱罐、兑换和触发式促销。|shopproductlist 分离 BuyPrice/Dollar、平台产品ID、ECostType、次数重置和每日发放;triggerinfo 给出触发条件、时段、重复开启及开关;dailyshoplist 用 Prob 描述刷新候选。|包内价格是历史配置,不代表当前支付平台报价;触发优先级、订单校验、服务端限购和广告可用性无法由静态表证明。|Shop,Purchase,Product,Package,PigBank,Advert,Ads,Promotion
|
||
15|任务与成就|questdata groupquestlist achievement|统一任务语义、任务组、累计条件、领取方式和成就前置链。|questdata 定义 questType/questKey/subValue 与跳转;groupquestlist 通过 questDataStringId 关联目标数量、锁定时是否计数、EClaimType 和奖励;achievement 包含 prerequisiteId、isPerGameReset、isHidden 和 goalCount。|事件实际计数时机、前置链激活、隐藏成就曝光以及服务端重复领取防护待确认。|Quest,Achievement
|
||
16|签到新人与回归|attendance attendanceconfig comebackconfig newbiebuffconfig|配置签到日历、补签、新人加成和回归福利。|attendance 用 EAttendanceType/day 组织奖励与补签方式价格;attendanceconfig 保留普通与 prod 日期字段;comebackconfig 注释以分钟定义离线门槛与buff时长,另外给出奖励/加速次数。|签到漏天处理、补签可用日期、时区和回归状态重置未闭合;历史排期不是当前在线活动。|Attendance,Comeback,NewbieBuff
|
||
17|通行证与赛季|passdata passquestlist passrewarddata passseasondata|负责通行证经验、任务、奖励轨道和赛季开放。|passdata 区分日常/每周/挑战任务数量、dailyXP、playMaxXP 和 primiumPlayMaxXP;passrewarddata 区分普通、高级及高级Plus奖励;passseasondata 连接商品组、兑换组、任务组、抽卡和排行榜。|经验上限重置、补领奖励、重复等级、付费生效和赛季切换执行顺序仍需确认。不能把不同 passID 的参数合并。|Pass,SeasonPass
|
||
18|主线进度与星级挑战|stageinfo stagerewardinfo loopreward starchallenge starchallengereward|管理局外选关、进度奖励和星级挑战;关内战斗只保留衔接配置。|stageinfo 的 stage/EPlayMode/isEnabled 定义关卡目录;stagerewardinfo 以 stage/loop/isClear 等字段分支发奖;starchallenge 描述挑战值,starchallengereward 按 starCount 配置奖励档。|首通与重复通关判定、星级条件评估和奖励领取幂等性需消费者;本模块保留整张stageinfo以防丢失入口字段,不重写局内战斗。|StageSelect,StarChallenge,StageReward,ClearReward
|
||
19|每日副本与探索副本|dayoftheweekdungeon dayoftheweekdungeonconfig weekdungeonboard idledungeon idledungeonconfig idledungeondrop idledungeonequipmentsalvage idledungeonequipmentstatus idledungeonplayerdefaultstat idledungeonstageinfo|管理曜日副本以及 IdleDungeon 探索玩法的入口、票券、成长与掉落回收。|曜日表按 day/difficulty 给奖励;IdleDungeon 单独定义票券恢复、背包容量、关卡、掉落权重与装备分解表。保留战斗衔接表以保证子系统数据完整。|两个模式并非同一个挂机系统;星期枚举、票券单位和满票停止恢复、掉落保护算法以及分解是否受等级约束待查。|WeekDungeon,DayOfWeek,IdleDungeon
|
||
20|无限模式|infiniteconfig infiniteseason infiniteboard infinitemodefevertiles|管理无限模式入场成本、赛季、奖励限制和独立难度配置。|infiniteconfig 给出 playAssetId/playAssetCount、excludeReward、maxGold、shopIncrease;infiniteseason 记录赛季Boss;infiniteboard 保留难度/层数成长;fever地块表带 isEnabled/weight。|maxGold 的限制周期、shopIncrease 计算公式、赛季切换与结算扣费时间待查。|Infinite,Infinity
|
||
21|Raid讨伐|raidconfig raidtier raidreward raidhighscorereward raidseasonpetlist raidseasonpreset raidboard raidaugmentoverride|管理讨伐门票、赛季预设、试用宠物、分数奖励与最高分档。|raidconfig 独立定义初始票、上限、恢复时间和入场成本;raidreward 按 tierId/season/minScore/maxScore 给出奖励;raidhighscorereward 为独立最高分门槛;seasonpetlist 用 deckIndex/trialLevel 配阵容。|分数端点包含关系、票券消耗时点和最高分奖励是否仅一次待查;不能把重复结算表与最高分表合并。|Raid
|
||
22|药水竞技与段位|battlepotion battleplayerdefaultstat potionbattleconfig potionbattlebonus potionbattlebonuskey rankbasedata rankmmradjust ranktier|管理竞技药水养成、匹配门票、段位MMR和竞技加成。|battlepotion 定义重量/冷却/效果基础值与增量、needPiece/needCost;potionbattleconfig 定义匹配范围与门票;ranktier 给出MMR区间和阶梯;rankmmradjust 存储多段修正参数,rankbasedata 关联通行证和赛季重置调整。|匹配服务器算法、MMR修正顺序、药水重量组装规则以及旧gameconfig票券字段与专表的优先级待查。|PotionBattle,RankTier,Matchmaking,BattlePotion,MMR
|
||
23|排行榜与赛季奖励|rankingdata rankingreferencedata rankingrewarddata|配置排行分组、环境排期、业务引用和名次奖励。|rankingdata 分别记录QA/Dev/Prod分组和赛季字段;rankingreferencedata 通过 RankKey/ERefType/referenceKey 关联业务;rankingrewarddata 以 minRank/maxRank 关联奖励。|名次区间端点、同分排序、跨服分组、结算时刻和发奖服务端逻辑未闭合。|Ranking,Leaderboard
|
||
24|赛季活动与超级球|seasonevent seasoneventroulette seasonquestlist superballschedule|管理活动排期、活动任务、兑换时段、转盘及超级球奖池。|seasonevent 独立记录活动结束与兑换结束以及prod日期;seasonquestlist 连接任务/通行证经验;seasoneventroulette 存储weight与奖励;superballschedule 给出参与人数相关奖励字段、赛季组、开奖计时和gachaType。|排期仅包内历史快照;不能根据字段名断言超级球奖池计算公式,真实参与人数与结算由运行时/服务器决定。|SeasonEvent,SuperBall
|
||
25|活动小游戏与奖励池|seasoneventminigamedata bubbleshooterminigame cardunpackingeventgame clawcraneminigame dicebeteventgame dicemachine herobodyminigame plinko plinkoad plinkolevel roulettedata spitoeventdailyreward spitoeventreward|归档活动小游戏入场资源、玩法模板、概率奖励和广告增益。|seasoneventminigamedata 描述 gamePackIndex 与 playItemUseCount;Plinko 独立配置球上限/恢复和广告buff;抓娃娃、拆卡包、刮奖、转盘等分别保留自己的概率字段和模板,不能混成统一抽卡池。|部分小游戏也可能出现在局内事件,作为交叉模块完整保留;胜负判定、概率归一化范围、广告叠加与奖励结算顺序待查。|Minigame,MiniGame,Plinko,Roulette,Spito,ClawCrane,CardUnpacking,DiceMachine
|
||
26|NPC好感与特殊养成商店|deoksun deoksunattendance deokjashop|保留NPC好感档位、签到轮次和特殊属性商店。|deoksun 按 likePoint 配置 slotRarity、goldUP、diaUP、服饰对话与 ingameDiscount;deoksunattendance 以 groupKey/round/day 发奖;deokjashop 包含属性、权重、基础与增量、maxLevel。|deokjashop 的金币字段原注释标注不使用,不能据其数值写实际价格公式;NPC中文正式名称、好感获取和商店解锁消费逻辑尚待本地化/消费者补证。|Deoksun,Deokja
|
||
'''
|
||
MODULES = [dict(zip(['id','name','tables','role','flow','unknown','terms'], l.split('|'))) for l in SPECS.strip().splitlines()]
|
||
for m in MODULES:
|
||
m['folder'] = m['id']+'-'+m['name']
|
||
m['tables'] = m['tables'].split()
|
||
|
||
# 原作功能还原口径:只保留影响玩法解释的待考证项。
|
||
DETAILS = {
|
||
'01':'解锁阶段使用已通关阶段还是当前阶段、开放日序从0还是1起算,现有配置尚不能确定。',
|
||
'14':'商品触发条件的先后关系、每日商店的候选筛选与权重计算方式尚需补充。价格按原作包内配置保留。',
|
||
'15':'任务事件的计数时机、前置成就完成后的激活时机、隐藏成就的展示条件尚需补充。',
|
||
'22':'MMR多段修正顺序、药水重量组合规则以及全局配置与竞技专表参数的使用关系尚需补充。',
|
||
'23':'名次区间端点是否包含、同分排序和奖励结算时刻尚需补充。',
|
||
'24':'超级球参与人数奖励的累计公式尚未确定;保留初始奖池、每人增加范围、名次分配与统计时间原值。'
|
||
}
|
||
FLOWS = {
|
||
'01':'达到进度条件 → 开放对应大厅入口 → 按引导进入功能 → 领取引导奖励',
|
||
'02':'获得道具或资源 → 查看用途与来源 → 使用、开启道具组或兑换 → 形成奖励或带入局内的资源',
|
||
'03':'获得装备 → 选择对应槽位 → 强化提升属性 → 按品质星级材料要求合成 → 提高强化上限',
|
||
'04':'选择装备槽位或永久属性 → 查看当前等级与下级消耗 → 升级 → 获得属性增量或特殊效果',
|
||
'05':'获得遗物 → 选择遗物槽位 → 按品质星级配置合成 → 获得属性和技能收益',
|
||
'06':'解锁宠物 → 收集碎片与金币 → 升级 → 开放等级技能;成长活动另按事件获得经验和阶段奖励',
|
||
'07':'解锁坐骑 → 消耗养成材料升级 → 使用碎片觉醒 → 提升持有效果、属性或被动技能等级',
|
||
'08':'开放命运功能 → 从对应权重组取得词条 → 形成属性或特殊效果;保留两组权重和召唤成本配置',
|
||
'09':'查看收藏组合 → 满足指定装备、等级、品质要求 → 获得对应收藏属性',
|
||
'10':'解锁外观 → 选择皮肤、头像、头像框或称号 → 展示外观;皮肤另有单件属性与套装协同配置',
|
||
'11':'获得地块包与碎片 → 解锁或升级地块 → 配置地块与开放技能 → 累积研究进度 → 提升研究属性',
|
||
'12':'积累巡逻时长 → 按通关进度确定奖励档 → 领取巡逻收益;快速巡逻按固定时长、体力与次数配置提供收益',
|
||
'13':'选择抽卡池与按钮 → 按抽数及资源配置消费 → 从奖励池取得奖励 → 按对应池的保底配置处理保底',
|
||
'14':'展示满足条件的商品 → 查看成本、限购与奖励 → 购买或兑换 → 获得即时奖励或每日奖励;部分礼包按顺序开放',
|
||
'15':'开放任务或成就 → 按条件累计目标进度 → 满足目标数量 → 按领取方式取得奖励 → 连接后续任务或前置链',
|
||
'16':'进入签到或满足新人、回归条件 → 按日序和状态开放福利 → 领取道具或限时加成 → 按次数与时长结束加成',
|
||
'17':'开放通行证赛季 → 通过任务与游玩积累经验 → 提升通行证等级 → 领取所属奖励轨道的等级奖励',
|
||
'18':'选择已开放关卡或挑战 → 进行局内玩法 → 更新通关或星级进度 → 领取对应关卡、层数或星数奖励',
|
||
'19':'选择曜日或探索副本 → 按模式使用入场资源 → 推进关卡 → 获取模式奖励;探索装备另按品质等级分解回收',
|
||
'20':'选择无限模式 → 消耗入场资源 → 推进对应难度层数 → 按模式的奖励排除与金币上限配置取得收益',
|
||
'21':'取得讨伐门票 → 进入赛季预设与宠物阵容 → 击败目标累积分数 → 获得分数档奖励并追求最高分奖励',
|
||
'22':'养成竞技药水 → 配置竞技效果与重量 → 使用门票参加竞技 → 按MMR与段位配置形成竞技进度和奖励',
|
||
'23':'玩法成绩关联对应排行 → 形成名次 → 按名次区间确定奖励 → 随关联赛季进入下一周期',
|
||
'24':'进入排期活动 → 完成活动任务或参与转盘、超级球 → 获得活动奖励 → 在兑换期使用活动资源',
|
||
'25':'进入对应活动小游戏 → 消耗指定资源或等待恢复 → 按游戏模板参与 → 获得对应奖励池收益;部分功能可获得广告增益',
|
||
'26':'提升NPC好感 → 变化对话、服饰与收益档位 → 参与签到或特殊商店养成 → 获得道具或属性增益'
|
||
}
|
||
for m in MODULES:
|
||
m['unknown'] = DETAILS.get(m['id'], m['unknown'])
|
||
|
||
EDITORIAL = {
|
||
'不采用《一骰登天》MVP主动改造值。覆盖全部提取CSV的范围判定;“配置已收录”不表示运行时规则全部闭合。未运行游戏、未查询服务器、未重新反编译原生方法体。':'以原作功能设计、玩法规则和完整数值为主线。设计文档说明功能关系,规则文档解释条件与消耗,表格保存全量等级、奖励和参数。',
|
||
'原作配置快照,不混入MVP数值。配置和元数据检索已归档;未恢复的执行规则逐模块明确列出。':'以原作HEROLL 2.0.1为唯一还原对象。每个模块按功能设计、规则说明和数值表组织,来源索引集中保留在审计目录。',
|
||
'运行时筛选、服务端状态、动态内容、数组索引和单位等证据不足。':'少量规则的筛选顺序、数组索引或单位尚待原作资料补充。',
|
||
'out_of_scope:新项目MVP改造、在线服务器请求、原APK执行、重新原生逆向。':'交付口径:还原原作功能、规则及数值,来源与检索过程作为附录。',
|
||
'本次“所有”指现有提取表的全量检索与范围分配,不意味着服务端规则或全部方法实现可由静态数据恢复。':'全部提取表均已进行范围分配。无法确定的原作玩法细节简短列入补充项。',
|
||
'没有读取原生方法实现、没有动态运行或服务器访问;不能据此声称所有局外运行规则已经还原。':'本包依据现有配置、注释和元数据整理;无法确定的原作玩法细节保留补充标记。'
|
||
}
|
||
|
||
def write(path, text):
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
for old,new in EDITORIAL.items(): text=text.replace(old,new)
|
||
path.write_text(text, encoding='utf-8')
|
||
|
||
def csvwrite(path, rows, fields=None):
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
if fields is None: fields = list(rows[0]) if rows else ['说明']
|
||
with path.open('w', encoding='utf-8-sig', newline='') as f:
|
||
w=csv.DictWriter(f, fields); w.writeheader(); w.writerows(rows)
|
||
|
||
def readtable(path):
|
||
with path.open(encoding='utf-8-sig', newline='') as f: raw=list(csv.reader(f))
|
||
hi=next(i for i,r in enumerate(raw) if 'Id' in r and 'StringId' in r)
|
||
headers=raw[hi]; types=raw[hi+1]; comments=raw[hi-1] if hi else []
|
||
records=[]
|
||
for ri,r in enumerate(raw[hi+2:], hi+3):
|
||
if not any(r): continue
|
||
assert len(r)==len(headers), (path.name,ri,len(r),len(headers))
|
||
records.append((ri,dict(zip(headers,r))))
|
||
return headers,types,comments,records
|
||
|
||
TABLES={p.stem:readtable(p) for p in sorted(SRC.glob('*.csv'))}
|
||
owner={}
|
||
for m in MODULES:
|
||
for t in m['tables']:
|
||
assert t in TABLES,t
|
||
assert t not in owner,t
|
||
owner[t]=m['folder']
|
||
metadata=json.loads((CORE/'metadata/assembly-csharp.json').read_text(encoding='utf-8'))
|
||
gconfig=TABLES['gameconfig'][3]
|
||
|
||
def escape(v): return str(v).replace('|','\\|').replace('\n','<br>').replace('\r','')
|
||
def table(headers, rows):
|
||
return '| '+' | '.join(headers)+' |\n| '+' | '.join(['---']*len(headers))+' |\n'+''.join('| '+' | '.join(escape(v) for v in r)+' |\n' for r in rows)
|
||
def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest()
|
||
|
||
# 总纲先于模块生成,保持阅读及生成顺序一致。
|
||
overview='# HEROLL 2.0.1 局外系统模块总纲\n\n版本:原版APK逆向数据整理 v1;整理日期:2026-09-06。\n\n本包检索现有原版逆向归档,恢复配置规则与结构证据,不采用《一骰登天》MVP主动改造值。覆盖全部提取CSV的范围判定;“配置已收录”不表示运行时规则全部闭合。未运行游戏、未查询服务器、未重新反编译原生方法体。\n\n## 模块总纲\n\n'
|
||
overview+=table(['模块','职责','数据表数'],[(f"[{m['folder']}](./{m['folder']}/01-模块设计文档.md)",m['role'],len(m['tables'])) for m in MODULES])
|
||
overview+='\n## 模块交付结构\n\n每个模块含 `01-模块设计文档.md`、`02-规则说明.md`、`03-表格/`。表格中保留原始CSV(含注释和类型)、规范化CSV(标准首行字段名)、字段字典、数值范围、元数据符号与规则证据。原始表的全部行均保留,包括停用和历史内容。\n\n## 系统关系\n\n账号与进度 → 功能解锁 → 资源获取 → 抽卡/商店/奖励 → 装备、宠物、坐骑、遗物、地块等长期养成 → 局内初始属性及模式配置 → 通关与活动奖励 → 长期养成。此关系是按配置用途整理的设计阅读路径,不是已经恢复的原生调用栈。\n\n## 证据等级\n\n- A 配置事实:原始CSV值、类型及注释。行定位使用CSV记录序号(含注释/表头/类型行),不等同含多行单元格文件的物理行号。\n- B 结构事实:Assembly-CSharp元数据的类型、字段、方法名称;可能含混淆或损坏名称,不证明方法实现。\n- C 条件推导:只在所列前提成立时有效,不能作为已确认运行规则。\n- U 未闭合:运行时筛选、服务端状态、动态内容、数组索引和单位等证据不足。\n\n## 审计入口\n\n[全表范围清单](./99-审计与证据/全表范围清单.csv) · [源文件哈希](./99-审计与证据/源文件清单.csv) · [全局配置](./00-共享配置/gameconfig.csv) · [检索与验证报告](./99-审计与证据/2026-09-06_逆向-HEROLL局外系统-report.md)\n'
|
||
write(OUT/'00-局外系统模块总纲.md',overview)
|
||
write(OUT/'README.md','# HEROLL 2.0.1 局外系统规则与数值\n\n先读[模块总纲](./00-局外系统模块总纲.md),再按26个模块阅读设计、规则和完整CSV表。\n\n原作配置快照,不混入MVP数值。配置和元数据检索已归档;未恢复的执行规则逐模块明确列出。\n')
|
||
|
||
GROUTES = {
|
||
'01':r'Unlock|Block|Save|autoUnlock', '02':r'Energy|dailyIngameDiaLimit|ingameDiaAdj|vip',
|
||
'03':r'calcCp|autoEquip', '04':r'Rune', '05':r'relic', '06':r'pet', '07':r'mount',
|
||
'08':r'destiny', '09':r'Collection', '10':r'skin|profile', '11':r'outgameTile|BaseTile|MultiplyTile',
|
||
'12':r'afk', '13':r'Gacha', '14':r'Shop|vip', '15':r'quest', '16':r'newbie|comeback',
|
||
'17':r'pass', '18':r'starChallenge|needEnergy', '19':r'weekDungeon|dayOfWeek|idleDungeon',
|
||
'20':r'infinite|infinity', '21':r'raid', '22':r'potionBattle|pvp', '23':r'rank',
|
||
'24':r'SeasonEvent|Summer|SuperBall', '25':r'Roulette|Spito', '26':r'Deokja|Deoksun'
|
||
}
|
||
FIELD_ZH={
|
||
'Id':'数值主键','StringId':'字符串主键','isEnabled':'配置启用标志','IsUse':'使用标志','isUse':'使用标志',
|
||
'level':'等级/阶段索引(具体语义按表)','grade':'品质枚举','starCount':'星数','maxUpgradeLevel':'强化等级上限',
|
||
'materialLevel':'合成材料等级','materialCount':'合成材料数量字段','sameGradeAnyEquipmentCount':'同品质任意装备材料数量',
|
||
'sameGradeAnyRelicCount':'同品质任意遗物材料数量','needMaterial':'所需材料数量','needGold':'所需金币(数组按原索引)',
|
||
'needPiece':'所需碎片数组','slotNum':'槽位编号','maxLevel':'等级上限(负值语义需消费者)',
|
||
'needItemID':'消耗道具ID','needCount':'消耗数量','itemID':'道具ID','itemId':'道具ID','itemCount':'道具数量',
|
||
'rewardId':'奖励ID(按奖励类型解析)','rewardID':'奖励ID(按奖励类型解析)','rewardCount':'奖励数量',
|
||
'prob':'概率/权重原值(未归一化)','Prob':'概率/权重原值(未归一化)','weight':'权重原值','weights':'权重数组',
|
||
'isPercent':'是否百分比标记(存储缩放需消费者)','isOwnEffect':'持有效果标记','isFree':'免费标记',
|
||
'isAD':'广告按钮标记','gachaCount':'抽取次数','ceilCount':'保底次数','isResetBeforeCeil':'保底前命中是否重置标记',
|
||
'ceilRewardString':'保底奖励Gacha字符串键','ADcooldown':'广告重置周期枚举','alterCount':'单个替代道具抵扣量',
|
||
'alterItemID':'替代道具ID','isAlterBtn':'替代按钮标记','type':'类型/分组','buttonNum':'按钮编号',
|
||
'checkTime':'巡逻检查间隔(分钟,原注释)','maxAfkTime':'巡逻累计上限(分钟)',
|
||
'quickAfkTime':'快速巡逻奖励时长(分钟)','dailyMaxQuickAfkCount':'每日快速巡逻上限',
|
||
'quickAfkEnergy':'快速巡逻体力消耗','dailyMaxQuickAfkAdCount':'每日广告快速巡逻上限',
|
||
'multipleValue':'奖励倍数数组','multipleWeight':'奖励倍数权重数组','lastClearStage':'最后通关阶段',
|
||
'goldPerHour':'每小时金币','amountPerCheckTime':'每检查周期奖励数量','maxAmount':'奖励数量上限',
|
||
'dropPercent':'掉落百分数字段(缩放待消费者)','afkTime':'每包累计时间(分钟)','maxPackCount':'最多累计包数',
|
||
'startAddPack':'内容解锁初始包数','needMileage':'里程奖励需求','getMileage':'每类包里程数组',
|
||
'mileageRewardId':'里程奖励道具ID','mileageRewardCount':'里程奖励数量','packList':'实际地块包ID数组',
|
||
'packAfkList':'挂机奖励用包ID数组','probList':'概率/权重列表原值','totalUpgradeCount':'总升级次数',
|
||
'researchLevel':'研究等级','weaponPlusLevel':'武器额外等级','needResearchLevel':'研究等级需求',
|
||
'needCost':'消耗字段(资源类别看消费者)','refNewTileId':'关联局内newtile主键','unlockStage':'解锁阶段',
|
||
'defaultEquip':'默认装备标记','skillUnlockLevel':'技能解锁等级','baseAdditionalStatus':'基础附加属性',
|
||
'plusAdditionalStatus':'附加属性增量','unlockPiece':'解锁碎片数量','unlockDia':'解锁钻石字段',
|
||
'unlockProductId':'解锁商品键','refundItemId':'溢出返还道具ID','refundItemCount':'溢出返还数量',
|
||
'normalWeight':'普通权重','rareWeight':'稀有权重','addValue':'增加值','value':'数值(单位按表)',
|
||
'baseValue':'基础值','upgradeValue':'升级增量字段','specialValue':'特殊数值',
|
||
'maxTicketCount':'门票上限','startTicketCount':'初始门票数','ticketChargeTime':'门票恢复时间(单位按表注释)',
|
||
'playAssetId':'入场资源ID','playAssetCount':'入场消耗数量','goalCount':'目标次数',
|
||
'isCanCountWhenLock':'锁定期间是否计数','prerequisiteId':'前置成就ID','isHidden':'隐藏标记',
|
||
'isPerGameReset':'每局重置标记','day':'日序/星期枚举(按表)','price':'价格字段',
|
||
'comebackTime':'回归离线门槛(分钟)','buffTime':'加成持续分钟(以注释为准)',
|
||
'rewardBuffPercent':'额外奖励百分数','rewardBuffCount':'奖励加成次数','playSpeedBuffCount':'加速次数',
|
||
'passID':'通行证ID','needXP':'经验需求','dailyXP':'每日经验','playMaxXP':'每日游玩经验上限',
|
||
'primiumPlayMaxXP':'高级通行证游玩经验上限(原拼写)','loopClearXP':'每层通关经验',
|
||
'season':'赛季编号','seasonStartDay':'赛季起日','seasonEndDay':'赛季止日','exchangeEndDay':'兑换截止日',
|
||
'minRank':'名次区间下界','maxRank':'名次区间上界','minScore':'分数下界','maxScore':'分数上界',
|
||
'minMMR':'MMR下界','maxMMR':'MMR上界','stepMMR':'MMR步长','BuyPrice':'包内购买价格字段',
|
||
'Dollar':'包内美元价格字段','ECostType':'消费类型枚举','buyCount':'限购数量','buyResetCooldown':'限购重置周期',
|
||
'dailyObtainDay':'每日奖励持续天数','isSpecial':'特殊标记','isMaterial':'材料标记',
|
||
'baseCoolTime':'基础冷却','plusCoolTime':'冷却增量','baseDamageValue':'基础伤害字段',
|
||
'plusDamageValue':'伤害增量','baseExtraValue':'基础额外值','plusExtraVaue':'额外值增量(原拼写)',
|
||
'EMonoStatus':'属性枚举','ERewardType':'奖励类型枚举','EPlayMode':'玩法模式枚举',
|
||
'Values':'全局配置原数组(单位/索引不得自行推定)'
|
||
}
|
||
|
||
# 经原注释和记录核对的中文规则。值从源行读取,避免手工转录漂移。
|
||
FACTS = {
|
||
'01': [('gameconfig','modeSelectUnlockStage','模式选择解锁门槛配置','Values'),('gameconfig','IsOfflineSaveBlock','离线保存阻止开关原值;不等同已确认运行行为','Values'),('gameconfig','mountUnlockDay','坐骑开放日序配置;首日索引未闭合','Values')],
|
||
'02': [('gameconfig','needEnergy','入场体力需求配置','Values'),('gameconfig','maxEnergy','基础体力上限配置','Values'),('gameconfig','chargeEnergy','体力恢复配置数组;计时单位及数组位置需消费函数','Values'),('gameconfig','vipAddMaxEnergy','VIP额外体力上限配置','Values')],
|
||
'03': [('equipmentdata','equipment_NORMAL_0','普通0星:品质阶段与强化上限、合成字段','level starCount maxUpgradeLevel materialLevel materialCount'),('equipmentdata','equipment_LEGEND_2','传说2星:强化上限及材料字段','level starCount maxUpgradeLevel materialLevel materialCount'),('equipmentdata','equipment_ETERNITY_3','永恒3星:表中最高阶段记录,空材料不自动代表可继续合成','level starCount maxUpgradeLevel materialLevel materialCount sameGradeAnyEquipmentCount')],
|
||
'04': [('slotupgrade','slot0_lv_0','槽0当前level=0到下一级的消耗(原注释明确“到下一级”)','level needItemID needCount needGold upgradeValue'),('slotupgrade','slot0_lv_1','槽0当前level=1到下一级的消耗','level needItemID needCount needGold upgradeValue'),('bonusstatus','ATTACK_POWER','永久攻击属性配置','isPercent upgradeValue maxLevel')],
|
||
'05': [('relicdata','relic_NORMAL_0','普通遗物合成材料字段','level starCount grade materialLevel materialCount'),('relicdata','relic_LEGEND_0','传说遗物材料分支','level starCount grade sameGradeAnyRelicCount'),('gameconfig','relicContentsUnlockStage','遗物开放条件原数组','Values')],
|
||
'06': [('pet','pet01','pet01升级、重复转化与冷却原值;needPiece首项为未使用值','needPiece needGold ifOwnAutoConvert convertItemId convertCount baseCoolTime plusCoolTime'),('gameconfig','petMaxConvertDia','宠物满级转钻配置数组;品质映射待查','Values')],
|
||
'07': [('mountlevel','mount_level_1','坐骑等级1升阶材料字段;原注释exp为坐骑粉末,不直接称角色经验','level exp isOwnEffect'),('mountlevel','mount_level_2','坐骑等级2升阶材料字段','level exp isOwnEffect'),('mountawaken','mount_awaken_RARE_0','稀有觉醒0阶段的碎片、持有效果和技能等级','grade level piece isOwnEffect baseStatus0 value0 passiveSkill1Level')],
|
||
'08': [('destinystat','attack_power_s','S攻击词条启用、数值与两种权重','isEnabled isPercent addValue normalWeight rareWeight'),('destinystat','aim_s','S命中词条明确停用且两种权重为0','isEnabled addValue normalWeight rareWeight'),('gameconfig','destinySummonNeedCost','命运召唤消费原数组;各项资源尚未映射','Values')],
|
||
'09': [('collection','collection_001','收藏001的装备组合、级别品质与属性值','equipmentIds equipmentLevels equipmentGrades EMonoStatus value'),('gameconfig','CollectionUnlockStage','收藏开放阶段配置','Values')],
|
||
'10': [('skin','sword_pumpkin','南瓜剑皮肤:攻击附加值5、套装上限4,1/2/3/4件协同数组按原注释为0/50/100/150;数值缩放待消费者','isEnabled EEquipmentSlot additionalStatus additionalValue maxSetCount setAdditionalValueList')],
|
||
'11': [('outgametileafkconfig','DefaultConfig','地块包独立挂机:每包分钟数、容量、初始包和里程','afkTime maxPackCount startAddPack probList getMileage needMileage mileageRewardId mileageRewardCount'),('outgametileresearch','101','总升级次数24对应研究1和鞋类武器加级配置','totalUpgradeCount researchLevel weaponPlusLevel EMonoWeapon'),('outgametileresearchstatus','ATTACK_POWER_0_0','研究0的首条攻击属性与消耗','needResearchLevel index EMonoStatus value needCost')],
|
||
'12': [('afkconfig','DefaultConfig','巡逻分钟、快速巡逻消耗和次数上限;普通倍数使用独立权重','checkTime maxAfkTime quickAfkTime dailyMaxQuickAfkCount quickAfkEnergy dailyMaxQuickAfkAdCount multipleValue multipleWeight')],
|
||
'13': [('gachatype','shopGacha_1_1','shopGacha单抽:道具101消耗30,存在替代道具204','gachaCount needItemID needCount isAlterBtn alterItemID alterCount'),('gachatype','shopGacha_2_10','shopGacha十连:道具101消耗300;抵扣值并非已确认券消耗数','gachaCount needItemID needCount alterItemID alterCount'),('gachaceildata','advancedEquipmentGacha20','高级装备保底次数及提前命中重置标记','type ceilCount isResetBeforeCeil ceilRewardString'),('gachaceildata','enhancedEquipmentGacha50','增强装备保底次数及提前命中重置标记','type ceilCount isResetBeforeCeil ceilRewardString'),('gachaceildata','mount_00_Gacha50','坐骑保底必须读取ceilCount=400;StringId尾部50不是实际次数','type ceilCount isResetBeforeCeil ceilRewardString')],
|
||
'14': [('pigbank','pigbank_1','存钱罐档1起止值、胜败增加和购买窗口分钟数','startValue endValue winUpValue loseUpValue purchaseAvailableMinutes cooldownMinutes'),('gameconfig','dailyShopRerollCount','每日商店重掷次数配置','Values')],
|
||
'15': [('achievement','ingameEquip4Normal','普通装备收集成就:目标4、每局开始重置进度标记True,奖励ITEM 52000×1','conditionType goalCount isPerGameReset rewardType rewardId rewardValue')],
|
||
'16': [('comebackconfig','DefaultConfig','回归:离线20160分钟(14天),buff4320分钟(3天),加成和次数另列','comebackTime buffTime rewardBuffPercent rewardBuffCount playSpeedBuffCount'),('newbiebuffconfig','DefaultConfig','新人:buff4320分钟,加速50、奖励20;次数-1的哨兵语义未闭合','buffTime additionalSpeed rewardBuffPercent playSpeedBuffCount rewardBuffCount')],
|
||
'17': [('passdata','normalPass','普通通行证:日任务6、周任务9、挑战3;普通/高级每日游玩经验上限200/400','dailyQuestCount weeklyQuestCount challengeQuestCount playMaxXP primiumPlayMaxXP dailyQuestRerollCount loopClearXP')],
|
||
'18': [('gameconfig','starChallengeUnlockStage','星级挑战开放阶段配置','Values'),('starchallengereward','0','星数1对应道具101×50;其他奖励ID=-1保留哨兵值','starCount rewardID1 value1 rewardID2 rewardID3')],
|
||
'19': [('idledungeonconfig','DefaultConfig','探索副本票券每14400秒(4小时)恢复,初始6、上限12;背包30不含已装备物','ticketChargeTime maxTicket startTicket maxInvSize'),('gameconfig','dayOfWeekDungeonUnlockStage','曜日副本解锁阶段配置','Values'),('gameconfig','weekDungeonADRewardPercent','曜日广告奖励百分数配置','Values')],
|
||
'20': [('infiniteconfig','DefaultConfig','无限入场消耗道具109×5;按难度金币上限100000/150000;每层商店涨价百分数1/2,最终价格小数舍去(原注释)','playAssetId playAssetCount excludeReward maxGold shopIncrease')],
|
||
'21': [('raidconfig','DefaultConfig','讨伐门票初始2、上限10,每43200秒(12小时)恢复,入场道具155×1','startTicketCount maxTicketCount ticketChargeTime playAssetId playAssetCount'),('gameconfig','raidADRewardPercent','讨伐广告奖励百分数配置','Values')],
|
||
'22': [('potionbattleconfig','setting2','竞技setting2配置:10张上限、3600秒恢复;限定targetSeason列表','targetSeason searchMmrRange searchRange maxTicketCount ticketChargeTime isRelayProduct'),('rankbasedata','potionBattle','竞技跟随potionBattlePass赛季;重置调整字段=-5,不直接解释为MMR减少5','followingPassId mmrKey resetSeasonAdjustTier')],
|
||
'23': [('rankingrewarddata','halloween2025Event1-1_rankReward','历史万圣节活动第1名档:道具101×100、100×10000;奖励数组须按位置配对','rankingDataKey minRank maxRank rewardId rewardCount')],
|
||
'24': [('superballschedule','2026-001-SuperBall','历史超级球赛季1:初始奖励10000、名次比例30/30/40;日期注释为UTC零点,统计时间30分钟','rewardPerParticipant startReward RewardPercent superBallStartDay superBallEndDay countingTime')],
|
||
'25': [('plinkolevel','level0','Plinko初始球容量15、恢复900秒;level=-1保留原值','maxBallCount chargeSecond adKey level'),('plinkoad','adGoldPin','Plinko广告金钉:每日3次、buff180秒、升级效果数组','maxADPerDay buffSecond values')],
|
||
'26': [('deoksun','likePoint1','NPC好感1:金币倍率字段1.1、钻石获得概率字段5;缩放及生效时机仍需消费者','likePoint goldUP diaUP slotRarity ingameDiscount'),('deokjashop','attackUpgrade','特殊商店攻击升级:权重30、基础与增量1;maxLevel=-1不自动解释无限','isEnabled probabilityWeight baseAdditionalValue additionalValue maxLevel')]
|
||
}
|
||
TABLE_ZH = {'equipment':'永久装备目录','equipmentdata':'装备品质星级与合成','equipmentdefaultstatdata':'装备基础和强化属性','equipmentugradelevel':'装备强化成本','slotupgrade':'槽位逐级成本与增益','bonusstatus':'永久属性上限','relic':'遗物目录','relicdata':'遗物品质合成','pet':'宠物养成参数','petskill':'宠物技能','mount':'坐骑目录','mountlevel':'坐骑逐级材料与增益','mountawaken':'坐骑觉醒','destinystat':'命运候选词条','collection':'收藏组合与收益','outgametile':'地块收集养成','outgametileresearch':'地块研究进度','outgametileresearchstatus':'研究属性与成本','afkconfig':'巡逻参数','afkreward':'巡逻进度奖励','gachatype':'抽卡按钮与成本','gacha':'抽卡奖励权重','gachaceildata':'抽卡保底','shopproductlist':'商品价格与发放','triggerinfo':'商品触发与入口','questdata':'任务条件定义','groupquestlist':'任务组目标与奖励','achievement':'成就条件奖励','attendance':'签到逐日奖励','passrewarddata':'通行证逐级奖励','ranktier':'竞技段位区间','rankingrewarddata':'排行榜名次奖励'}
|
||
|
||
catalog=[]; manifest=[]; allrules=[]; allsymbols=[]; dictionary=[]; invalid=[]
|
||
for t,(headers,types,comments,records) in TABLES.items():
|
||
disposition='局外模块' if t in owner else ('跨模块共享' if t=='gameconfig' else '局内或展示共享支持表')
|
||
catalog.append(dict(表=t,模块=owner.get(t,'00-共享配置' if t=='gameconfig' else '99-审计与证据/范围补充'),范围判定=disposition,记录数=len(records),说明='完整保留,含未启用行'))
|
||
manifest.append(dict(证据ID='E-'+t,源文件=str((SRC/(t+'.csv')).relative_to(ROOT)).replace('\\','/'),SHA256=sha(SRC/(t+'.csv')),记录数=len(records)))
|
||
ids=Counter(r.get('Id') for _,r in records)
|
||
invalid += [dict(表=t,问题='重复Id',值=k) for k,v in ids.items() if v>1]
|
||
for i,h in enumerate(headers):
|
||
dictionary.append(dict(表=t,字段=h,类型=types[i] if i<len(types) else '',中文含义=FIELD_ZH.get(h,'原字段;结合原注释与模块规则阅读,精确定义未翻译确认'),原始注释=comments[i] if i<len(comments) else '',空值条数=sum(r[h]=='' for _,r in records)))
|
||
|
||
def copytable(t, directory):
|
||
headers,types,comments,records=TABLES[t]
|
||
raw=directory/'原始表'/(t+'.csv'); raw.parent.mkdir(parents=True,exist_ok=True)
|
||
shutil.copyfile(SRC/(t+'.csv'),raw)
|
||
csvwrite(directory/(t+'.csv'),[r for _,r in records],headers)
|
||
|
||
write(AUDIT/'scope.md','# 范围\n\nauth:用户要求从已有逆向数据检索所有局外系统规则数值。\nnetwork_profile:offline。\nin_scope:HEROLL_APK_REVERSE_ONLY 的提取CSV、Assembly-CSharp元数据与直接派生商业化表。\nout_of_scope:新项目MVP改造、在线服务器请求、原APK执行、重新原生逆向。\n\n本次“所有”指现有提取表的全量检索与范围分配,不意味着服务端规则或全部方法实现可由静态数据恢复。\n')
|
||
write(AUDIT/'timeline.md','# 工作记录\n\n2026-09-06:定位原作净化归档,核对局内包结构。\n2026-09-06:全量扫描提取CSV,先输出26模块总纲。\n2026-09-06:逐模块导出设计、规则、全量数据、字段与符号证据。\n2026-09-06:检查原始文件哈希、规范化逐单元格一致性、范围覆盖及文档相对链接。\n')
|
||
|
||
for m in MODULES:
|
||
d=OUT/m['folder']; td=d/'03-表格'; td.mkdir(parents=True,exist_ok=True)
|
||
rules=[]; summaries=[]; numeric=[]; facts=[]; arrays=[]
|
||
for t,key,desc,fields in FACTS[m['id']]:
|
||
matches=[(ri,r) for ri,r in TABLES[t][3] if r['StringId']==key]
|
||
assert len(matches)==1,(t,key)
|
||
ri,r=matches[0]
|
||
vals=';'.join(h+'='+r[h] for h in fields.split())
|
||
facts.append((desc,vals,f'E-{t} / StringId={key} / CSV记录{ri}'))
|
||
rules.append(dict(规则ID=f"R-{m['id']}-{len(rules)+1:03}",等级='A',规则=desc+';'+vals,证据='E-'+t,定位=f'StringId={key};CSV记录{ri}',边界='字段原值及原注释;尚未验证运行时消费者'))
|
||
for t in m['tables']:
|
||
copytable(t,td)
|
||
headers,types,comments,records=TABLES[t]
|
||
rules.append(dict(规则ID=f"R-{m['id']}-{len(rules)+1:03}",等级='A',规则=f'{t} 配置含 {len(records)} 条数据;字段及所有原值见同名表',证据='E-'+t,定位='全表',边界='表存在不证明运行时全部启用'))
|
||
for i,h in enumerate(headers):
|
||
typ=types[i] if i<len(types) else ''
|
||
if typ.endswith('[]'):
|
||
for ri,r in records:
|
||
if not r[h]: continue
|
||
for ai,value in enumerate(r[h].split(',')):
|
||
arrays.append(dict(表=t,Id=r['Id'],StringId=r['StringId'],字段=h,零基数组索引=ai,原值=value,类型=typ,来源记录=ri,说明='按逗号展开原序列;不是玩家等级,保留空格和哨兵'))
|
||
if typ not in ('int','float','double','long'): continue
|
||
vals=[]
|
||
for _,r in records:
|
||
try: vals.append(float(r[h]))
|
||
except ValueError: pass
|
||
if vals: numeric.append(dict(表=t,字段=h,类型=typ,有效数值数=len(vals),最小值=min(vals),最大值=max(vals),说明='全配置范围,含停用/历史行;不是玩家可达区间'))
|
||
flags=[h for h in headers if h in ('isEnabled','isUse','IsUse','isEnable')]
|
||
flagtxt=';'.join(h+'='+str(dict(Counter(r[h] for _,r in records))) for h in flags) or '无统一启用字段'
|
||
examplekeys=[h for h in headers if h not in ('Id','StringId') and not re.search(r'icon|prefab|title|desc|animation|sound|feedback|text|color',h,re.I)]
|
||
samples=[]
|
||
for ri,r in records[:3]:
|
||
samples.append((r['Id'],r.get('StringId',''),'; '.join(h+'='+r[h] for h in examplekeys[:10]),'E-'+t+f' / CSV记录{ri}'))
|
||
summaries.append(f"### {TABLE_ZH.get(t,t)}({t})\n\n数据条数:{len(records)};{flagtxt}。完整数据:[标准CSV](./03-表格/{t}.csv);[原始表](./03-表格/原始表/{t}.csv)。\n\n"+table(['Id','StringId','配置原值示例(非完整字段)','来源'],samples))
|
||
config=[dict(证据='E-gameconfig',记录序号=ri,**r) for ri,r in gconfig if re.search(GROUTES[m['id']],r['StringId'],re.I)]
|
||
csvwrite(td/'关联全局配置.csv',config,['证据','记录序号','Id','StringId','Values'])
|
||
symbols=[]
|
||
terms=m['terms'].split(',')
|
||
for ty in metadata['types']:
|
||
if not any(term.lower() in ty['name'].lower() for term in terms): continue
|
||
for kind in ['fields','methods']:
|
||
for member in ty.get(kind) or []:
|
||
symbols.append(dict(类型索引=ty['index'],命名空间=ty.get('namespace',''),类型=ty['name'],成员类别=kind,成员索引=member['index'],成员=member['name'],证据='E-metadata',说明='名称检索命中,仅结构证据;非原生实现'))
|
||
csvwrite(td/'元数据符号.csv',symbols,['类型索引','命名空间','类型','成员类别','成员索引','成员','证据','说明'])
|
||
allsymbols += [dict(模块=m['folder'],**s) for s in symbols]
|
||
csvwrite(td/'字段字典.csv',[r for r in dictionary if r['表'] in m['tables']])
|
||
csvwrite(td/'数值范围.csv',numeric,['表','字段','类型','有效数值数','最小值','最大值','说明'])
|
||
csvwrite(td/'规则证据.csv',rules)
|
||
csvwrite(td/'数组逐项明细.csv',arrays,['表','Id','StringId','字段','零基数组索引','原值','类型','来源记录','说明'])
|
||
allrules += [dict(模块=m['folder'],**r) for r in rules]
|
||
design=f"# {m['id']} {m['name']}:原作模块设计\n\n## 功能定位\n\n{m['role']}\n\n## 功能流程\n\n{FLOWS[m['id']]}。\n\n以上按原作配置关系整理功能步骤,具体条件与数值见下文及规则说明。\n\n## 功能构成与规则关系\n\n{m['flow']}\n\n"
|
||
if facts: design+='## 关键规则与数值\n\n'+table(['原作规则','数值','来源'],facts)+'\n'
|
||
design+='## 完整数值表\n\n'+table(['配置表','记录数','完整数据'],[(t,len(TABLES[t][3]),f'[CSV](./03-表格/{t}.csv)') for t in m['tables']])
|
||
design+=f"\n[规则说明](./02-规则说明.md) · [字段字典](./03-表格/字段字典.csv) · [数组逐项明细](./03-表格/数组逐项明细.csv)\n\n[返回总纲](../00-局外系统模块总纲.md)\n"
|
||
write(d/'01-模块设计文档.md',design)
|
||
rule=f"# {m['id']} {m['name']}:规则与数值\n\n## 已恢复的配置关系\n\n{m['flow']}\n\n证据等级A仅确认字段原值;下列示例不是全量表,所有等级、成本、奖励、参数见所链接CSV。\n\n"
|
||
if facts: rule+='## 核心规则核对\n\n'+table(['规则解读','源字段原值','证据定位'],facts)+'\n'
|
||
rule+='## 表级规则及原值\n\n'+'\n'.join(summaries)
|
||
rule+='\n## 关联全局配置\n\n'
|
||
rule+=table(['键','原值','CSV记录序号'],[(r['StringId'],r['Values'],r['记录序号']) for r in config]) if config else '未按本模块关键字命中gameconfig;不代表没有硬编码参数。\n'
|
||
rule+=f"\n[完整全局配置](../00-共享配置/gameconfig.csv) · [规则来源](./03-表格/规则证据.csv) · [原作符号索引](./03-表格/元数据符号.csv)\n\n## 尚待补充的原作细节\n\n{m['unknown']}\n"
|
||
write(d/'02-规则说明.md',rule)
|
||
write(d/'README.md',f"# {m['folder']}\n\n{m['role']}\n\n[模块设计](./01-模块设计文档.md) · [规则说明](./02-规则说明.md) · [字段字典](./03-表格/字段字典.csv)\n")
|
||
|
||
# 未分入局外主模块的表仍完整保留,并给出明确范围,防止检索遗漏。
|
||
for t in TABLES:
|
||
if t not in owner:
|
||
copytable(t,OUT/'00-共享配置' if t=='gameconfig' else AUDIT/'范围补充')
|
||
csvwrite(OUT/'00-共享配置/字段字典.csv',[r for r in dictionary if r['表']=='gameconfig'])
|
||
csvwrite(AUDIT/'全表范围清单.csv',catalog)
|
||
manifest.append(dict(证据ID='E-metadata',源文件=str((CORE/'metadata/assembly-csharp.json').relative_to(ROOT)).replace('\\','/'),SHA256=sha(CORE/'metadata/assembly-csharp.json'),记录数=len(metadata['types'])))
|
||
csvwrite(AUDIT/'源文件清单.csv',manifest)
|
||
csvwrite(AUDIT/'规则索引.csv',allrules)
|
||
csvwrite(AUDIT/'字段总字典.csv',dictionary)
|
||
csvwrite(AUDIT/'数据异常.csv',invalid,['表','问题','值'])
|
||
csvwrite(AUDIT/'未闭合规则.csv',[dict(模块=m['folder'],等级='U',问题=m['unknown'],下一步='定位元数据对应原生消费者,或用原版实机记录操作前后状态') for m in MODULES])
|
||
|
||
# 按池计算条件概率,明确不是运行时真实概率。
|
||
pool=defaultdict(list)
|
||
for ri,r in TABLES['gacha'][3]: pool[r['type']].append((ri,r))
|
||
probabilities=[]
|
||
for key, rows in pool.items():
|
||
total=sum(float(r['prob']) for _,r in rows)
|
||
for ri,r in rows:
|
||
probabilities.append(dict(池=key,Id=r['Id'],StringId=r['StringId'],权重=r['prob'],池权重和=total,配置归一化百分比=float(r['prob'])/total*100 if total else '',奖励类型=r['rewardType'],奖励ID=r['rewardID'],奖励数量=r['rewardCount'],等级='C',前提='同type所有行均参与且按prob线性加权;未计保底和运行时过滤',证据=f'E-gacha/CSV记录{ri}'))
|
||
csvwrite(OUT/'13-抽卡与保底/03-表格/条件概率推导.csv',probabilities)
|
||
|
||
# 显式Id引用的可核验映射,只解析明确道具字段;奖励类型ID不一概当道具。
|
||
items={r['Id']:r for _,r in TABLES['item'][3]}
|
||
refs=[]
|
||
for t in owner:
|
||
for ri,r in TABLES[t][3]:
|
||
for h in ('needItemID','alterItemID','convertItemId','refundItemId','exchangeItemId','playAssetId','useItemId'):
|
||
if h not in r or not r[h]: continue
|
||
target=items.get(r[h])
|
||
refs.append(dict(表=t,Id=r['Id'],字段=h,值=r[h],道具StringId=target['StringId'] if target else '',匹配='item.Id匹配' if target else '未匹配;可能为枚举/占位/跨类型ID,需查消费者',来源记录=ri))
|
||
csvwrite(AUDIT/'消耗道具关联.csv',refs)
|
||
|
||
from organize_module_docs import organize
|
||
document_counts = organize(globals())
|
||
|
||
# 验证:全量原表字节哈希、规范表逐单元格、文档相对链接。
|
||
checks=[]
|
||
for t in TABLES:
|
||
d=(OUT/owner[t]/'03-表格') if t in owner else (OUT/'00-共享配置' if t=='gameconfig' else AUDIT/'范围补充')
|
||
assert sha(SRC/(t+'.csv'))==sha(d/'原始表'/(t+'.csv')),t
|
||
with (d/(t+'.csv')).open(encoding='utf-8-sig',newline='') as f: got=list(csv.DictReader(f))
|
||
assert got==[r for _,r in TABLES[t][3]],t
|
||
checks.append('全部源CSV哈希与原始副本一致;全部规范化CSV逐单元格一致')
|
||
for p in OUT.rglob('*.md'):
|
||
for link in re.findall(r'\]\(([^)]+)\)',p.read_text(encoding='utf-8')):
|
||
if link.startswith(('http','#')): continue
|
||
target=(p.parent/link.split('#')[0])
|
||
if target.name=='2026-09-06_逆向-HEROLL局外系统-report.md': continue # 下方统计汇总后生成
|
||
assert target.exists(),(p,link)
|
||
checks.append('Markdown相对链接全部可解析')
|
||
summary=dict(模块数=len(MODULES),源表数=len(TABLES),局外主模块表数=len(owner),共享和补充表数=len(TABLES)-len(owner),全表数据行数=sum(len(v[3]) for v in TABLES.values()),局外主模块数据行数=sum(len(TABLES[t][3]) for t in owner),字段数=len(dictionary),元数据命中记录数=len(allsymbols),抽卡配置池数=len(pool),重复主键问题数=len(invalid),验证=checks)
|
||
summary.update(document_counts)
|
||
write(AUDIT/'验证结果.json',json.dumps(summary,ensure_ascii=False,indent=2))
|
||
write(AUDIT/'2026-09-06_逆向-HEROLL局外系统-report.md','# HEROLL局外系统检索与验证报告\n\n## 范围和结果\n\n见[scope](./scope.md)和[timeline](./timeline.md)。原版HEROLL 2.0.1配置与元数据归档已分为26模块。\n\n'+table(['指标','结果'],[(k,v) for k,v in summary.items() if k!='验证'])+'\n## Evidence → Finding → Path\n\nEvidence:E-表名对应[源文件SHA256清单](./源文件清单.csv),E-metadata为Assembly-CSharp元数据。\n\nFinding:[规则索引](./规则索引.csv)记录模块配置事实,等级A;元数据名称为B;抽卡条件归一化为C;执行缺口列入[未闭合规则](./未闭合规则.csv),等级U,severity=n/a_re。状态:配置与结构整理完成,原生/动态规则未闭合。\n\nPath P-001(solve/资料复现):原始CSV → 识别Id/StringId表头 → 排除注释和类型记录 → 保留所有数据行与空值 → 模块映射 → 文档和CSV → 哈希及逐单元格校验。\n\n## 验证\n\n'+'\n'.join('- '+x for x in checks)+'\n\n## 复现\n\n在项目根目录用Python 3执行 `HEROLL_OUTGAME_2.0.1/99-审计与证据/工具/build_outgame.py`。仅需标准库,重建本包,不改动源归档。生成器会覆盖本包中的同名自动生成文件。\n\n## 限制\n\n没有读取原生方法实现、没有动态运行或服务器访问;不能据此声称所有局外运行规则已经还原。源数据韩文注释原样保留;未明确翻译的字段直接标明未知。表中排期、价格和开关只代表包内快照。\n\n## 工作流自检\n\n- Triage:核对原作净化归档、表目录与局内结构。\n- Static:完成所有CSV扫描与元数据名称检索。\n- Dynamic:本次已有数据检索范围不执行;缺口保持U,不冒称验证。\n- Synthesis:设计、规则、完整表、证据、复现与校验已输出。\n- 工具:使用本机PowerShell、rg和已发现的捆绑Python;技能tool-index.md缺失,未依赖虚构工具路径。\n')
|
||
print(json.dumps(summary,ensure_ascii=False,indent=2))
|
||
|
||
# 报告写完后重新核验所有链接,不豁免任何目标。
|
||
for p in OUT.rglob('*.md'):
|
||
for link in re.findall(r'\]\(([^)]+)\)',p.read_text(encoding='utf-8')):
|
||
if not link.startswith(('http','#')):
|
||
assert (p.parent/link.split('#')[0]).exists(),(p,link)
|
||
files=[dict(文件=str(p.relative_to(OUT)).replace('\\','/'),字节数=p.stat().st_size,SHA256=sha(p)) for p in sorted(OUT.rglob('*')) if p.is_file() and p.name!='文件索引.csv']
|
||
csvwrite(OUT/'文件索引.csv',files)
|