HawChow-CombatPanel icon

CombatPanel

Standalone GTFO combat panel: an in-level damage/share, friendly-fire and accuracy table, plus a post-run influence board scoring every player out of 100 on ten weighted metrics. Per-weapon attribution follows the gun that fired. BepInEx only.

CHANGELOG

Changelog

1.15.13

  • 主机→客机的队伍数据同步真的通了。 上一次失败的真因:SNet.SetupCustomData<A> 只认 IL2CPP 元数据里已经存在的类型,而模组自定义 struct 没有指针存储(Il2CppClassPointerStore1抛TypeInitializationException),所以一条也发不出去。现在改走游戏自己的**裸字节通道**——也就是 GTFO-API 0.5.1 的 NetworkAPI与 Hikaria Core 背后那套做法:发送用SNet.Core.SendBytes(字节, 质量, 频道, 目标);接收在 SNet_Replication.RecieveBytes 上挂一个 Harmony 前缀,前两字节是复制层分发的 key,本模组用一个没人占用的 key(0xFFFE)加魔数 CMPL` 认出自己的包并就地吞掉,别的包原样放行。依然只依赖 BepInEx,没有引入任何第三方库。每个包带协议版本(v2),版本不同的两端保持沉默,而不是互解字段。
  • 客机也把自己的精确数据回传主机。 主机看别人那一行只能估,客机对自己那一行是实测:现在客机把自己那一行发回去,谁的实测谁说话(Accepts 拒绝针对本机 lookup 的快照,本机行永远不会被覆盖)。
  • 主机没装模组时,客机给全队伤害一个近似值。 主机是唯一施加伤害的机器,客机只能看到敌人被同步后的新血量;于是客机把「看到的掉血」累加为全队总伤害,自己那一行的占比不再是 —,而是带 ~ 的近似值(~52.2%)。一旦装了模组的主机开口,它的实测总数立刻接管。血量是半精度相对值(SFloat16),按该敌人最大血量还原;掉血大于该敌人整条血量(重置/复活)不计入。
  • 回归断言 695 → 704(新增线路往返、包长与近似标记的断言)。

1.15.12

  • 评价下面多了一行说明,把称号背后的数字写出来。 此前「评价」只有四个字,看不出凭什么给的。现在紧贴「评价」下面新增一行「说明」,逐人写出这句称号是拿什么换来的,例如 伤害 95.4,占全队 52.2%、扫描 6 次,首次标记 2 只、打断敌人攻击 3 次、补给 1 次,有效 62.0%、阵亡 1 次;零分玩家写 本局没有任何计分贡献。这一行跟着称号走,而不是跟着权重最高的指标走,所以它永远不会解释一个此人并没有戴上的称号;量不到的项按本表约定写 —,估算项沿用 ~。整块因此由 24 行变 25 行(拆开击杀等级时 27 行)。
  • 看板出现的时机对齐页面自己的淡入。 失败页的看板一直是等自己的「任务失败」标题起来之后才渲染;成功页此前只等「量得到分区」,而对象一存在就能量到,于是表格仍在页面淡入过程中先出现。现在成功页也等「已清理分区」与它首个分区图标真正可见(已激活且不透明度到 1)才落位,与图标同时出现。等待有上限(4 秒);期间若这一块已停止变化(例如某个说明文字本来就是暗的),也按就绪处理,所以不会把看板拖住。
  • 看板出现后不再缩放。 结果页的轮询每 0.25 秒重刷一次,看板因此此前每帧都重新测量、重建网格,玩家正读着表格时字号会跟着页面动画跳。现在一个结算页只测量一次:位置与字号定下来就冻结,只有换页(成功↔失败)才重新测量。
  • 回归断言 695(本次新增 15 条:紧跟「评价」的那一行本身,以及它对伤害、扫描、硬直、三种击杀、补给、阵亡、零分与「量不到」各自写出的句子)。

1.15.11

  • 成功页看板不再抢在分区图标之前出现。 此前分区那几项还量不到时,看板会先按「历史比例」的兜底位置画出来,等图标加载完再跳到位——看起来就是表格先冒出来、页面随后才齐。现在量到分区之前先不画,量到的那一帧直接落在最终位置,与图标同时出现且全程不移动。等待是有上限的(2.5 秒):万一某个版本永远给不出可测量的分区,看板也会在片刻后按兜底位置出现,而不是干脆不显示。
  • 回归断言保持 678。

1.15.10

  • 结算看板用满剩余空间,字号不再偏小。 此前表格只按「能塞进那条带子」缩,从不长大,四周空着一大片。现在先在常规字号下建一次,若高度只占了带子的九成以下,就按剩余高度重建更大的字号(上限 2.5 倍),左右各留 24 px 边距;网格自身的缩放兜底仍在,所以「长大」永远不会把表格顶到图标或按钮上。
  • 回归断言保持 678。

1.15.9

  • 成功页落位:地标改量「连同子物体」。 1.15.8 的相机兜底让测量真的成了(实机 completion=373, lobbyButton=1309, boardTop=389),但 373 比肉眼所见浅了一百多像素——已清理分区 的图标下面的文字是子物体,只量本体取不到它的底边,看板于是仍然压住图标与那行字。现在 Bottom() 把该元素及其所有子渲染器都投影一遍取最深底边,子物体走同一套合理性校验(投到画面外、大得离谱的照样丢弃)。
  • 对局档案收进一个文件夹:…\plugins\GTFOCombatPanel\CombatPanel-results\CombatPanel-results-<时间戳>.json,不再直接散在插件目录里。
  • 回归断言保持 678。

1.15.8

  • 成功页地标测不到的根因修掉了:缺相机时的兜底。 1.15.7 已经让成功页那段测量真的跑起来了(实机日志终于出现 success region: …),但两个地标都报 unmeasurable —— 已清理分区 的分区白框/标题/四个分区图标是世界空间物体、上方没有 Canvas,而 CM_PageBase.Camera 在页面刚接上的那几帧还是 null,TryScreenRect 于是直接判「测不到」,看板退回屏高比例(boardTop=230),正好压住那排图标。现在相机缺失会去找:先 Camera.main,再取任意启用中的相机;仍然没有才放弃。矩形是否合理的校验一条没放宽,投到画面外或大得离谱的测量照旧被丢弃。
  • 回归断言保持 678。

1.15.7

  • 新增:每局结算数据自动存到本地,就在本模组 DLL 的同目录。 每张结算页(成功/失败)写一份 CombatPanel-results-<yyyyMMdd-HHmmss>.json:版本、写入时间、结果、每位玩家的名次/名字/分数,以及看板上逐行原样的内容(含 — 与 ~ 标记)——不重算、不筛选,免得过些时候翻文件时把「没测到」误当成 0。
  • 落盘目录取程序集真实加载路径:手装是 BepInEx\plugins\GTFOCombatPanel\,r2modman 管理下就是 …\HawChow-CombatPanel\GTFOCombatPanel\;都读不到才退回 BepInEx\plugins\<模组名>。目录与文件名各打一条日志。
  • 一张结算页只写一次(以页面实例为键,重复落位、重复绘制都不会重复写);写失败只打一条 warning,面板照常绘制。
  • 回归断言保持 678。

1.15.6

  • 成功结算页的看板落位修好了(根因,不是调坐标)。 NeedsAttach() 此前只看「页面在不在」,不区分成功页还是失败页;而成功页的位置只在准备阶段量一次,失败页却是每帧重算的。于是「先失败、后成功」时,看板带着失败页的坐标(实机日志 boardTop=506)画在成功页上,正好压住 已清理分区 的分区图标。现在两处一起改:换一种结果页必定重挂(记住上次落位是为哪种页面算的),并且成功页的可用带子每帧重新测量——页面本身会把完成分区与按钮动画淡入,某一帧量到的位置不该被带到另一帧。
  • 落位规则不变:看板顶边在完成分区实测底边之下、底边不超过送大厅按钮顶边,空间不够时缩字号,绝不向上钳制去压图标。
  • 回归断言保持 678。

1.15.5

  • 打容器锁、门锁这类环境物件不再算成命中(也不再算成伤害)。 实测:沙鹰的命中已经计上了,但 Dam_EnemyDamageBase 的子类里有一个 Dam_StaticEnemyDamage / Dam_StaticEnemyDamageLimb——容器锁、门锁这类静态可破坏物走的正是它,于是朝锁开枪被当成了「打中敌人」。现在两条同时判定:目标若是静态破坏物类型,或它的 EnemyAgent Owner 为空(场景物件身后没有敌人),这条记录整条跳过,伤害与命中一起不计。
  • 非模组武器的命中计算不受影响。 补命中那条路径的前提仍是「这次子弹伤害没有被任何射击窗口覆盖」——原版武器走 Fire → 射线 → 命中,永远有窗口,因此一次都不会进入新分支;它们仍旧只由射线路径计数。
  • 回归断言保持 678。

1.15.4

  • 沙鹰的命中补齐:普通命中与弱点命中分开。 弱点不靠算法猜——伤害钩子拿到的就是那一根肢体(Dam_EnemyDamageLimb 派生自 Dam_EnemyDamageBase),m_type == eLimbDamageType.Weakspot 直接给出答案,本模组的 OnEnemyLimbHit 早就在用同一字段。现在从伤害事件补命中时顺手读出这个标志,普通命中与弱点命中分别入账,折进下一次 accuracy 更新时各自按「这一次实际用掉的命中数」夹住,装不下的留到下一次。弱点命中永远不会多于命中数。
  • 回归断言保持 678。

1.15.3

  • 沙鹰的命中改从游戏自己的伤害管线取。 实测:EWC 公开的两条流(AccuracyAPI 三层、ProjectileAPI)对这把枪都不吐命中(shots=1 fired/0 hit, groups=1/0, full=1/0),所以任何只依赖 EWC 公开接口的做法都拿不到它,StatDisplay 的写法同理。但它的伤害本模组测得到(副武器·伤害 492.0),说明伤害走的是游戏自己的 ReceiveBulletDamage。现在:EWC 只负责「打了几发」(它对沙鹰的发射数是准的),命中由伤害事件补——没有被射击窗口覆盖的那次子弹伤害记一次命中,折进下一次 accuracy 更新;每次更新按 EWC 报的发射数夹住,命中永远不会多于发射。
  • 队伍同步这条通道干净关停。 实测拿到真因:SNet.SetupCustomData<A> 会为 A 构造 IL2CPP 指针存储,而托管 struct 没有——抛 TypeInitializationException: Il2CppClassPointerStore1`。即模组自定义类型根本不可能从这个入口发出(要另做裸字节通道)。既然做不到,就不再注册,也不再在日志与文档里留一句打不通的承诺;日志改为一行说明,所有行仍是本机观测。
  • 回归断言保持 678。

1.15.2

  • 沙鹰(EWC 弹丸类武器)的命中终于有数了。 1.15.1 只在 EWC 的 AccuracyAPI 上取数,实测这把枪三层全为 0(shots=1 fired/0 hit, groups=1/0, full=1/0),说明 EWC 的精度系统没有覆盖它——Dinorush 的 StatDisplay 对这种情况另挂了 ProjectileAPI.OnProjectileHit。现在照做:EWC 精度不计的命中改从它自己的弹丸回调取,折进该玩家的下一次 accuracy 更新,按「这一次更新打出的弹丸数」夹住,装不下的留到下一次。数字仍是 EWC 自己的,不编造。
  • 队伍同步那条改成「失败必须说清原因」。 实测日志里一直有 squad sync is unavailable on this build … Exception has been thrown by the target of an invocation.,即 1.14.0 的客机同步从未生效(它按设计降级,不污染任何数字)。现在注册失败会把异常类型、消息与内部异常一并打进日志;查明之前这项能力仍是关闭的,DATA-AVAILABILITY.md 第六节已按此标注。
  • 回归断言保持 678。

1.15.1

  • 扫描行的名字改回「扫描效率」。 值仍然是「首次标记数/扫描次数」两个数(如 ~2/6),只是指标名按你的要求回到「扫描效率」。
  • EWC 武器的命中按 EWC 的三层计数取最宽那一层。 上一版只读了 FullShots(逐次射击)这一层,实测沙鹰仍是 9/0/0;EWC 实际同时维护逐弹丸 Shots、逐组 Groups、逐次 FullShots 三层,命中记在哪一层取决于武器怎么做的。现在按「哪层有内容就用哪层」取值,并把三层原始数字打一条日志([DIAG] EWC accuracy levels: …),下次现场能直接看出沙鹰的命中落在哪一层。
  • 回归断言保持 678。

1.15.0

  • 名字显示全名,不再截断到六个字符。 只剥掉游戏自己会解释的 TextMeshPro 标记(与 1.13.1 同一条规则),其余全部保留;列宽按实测文字自适应,长名字不再被切掉半截。
  • 扫描行改为「首次标记数/扫描次数」。 两个数一起显示,例如 5/2:斜杠前是这个人的扫描仪首次揭示的敌人数,斜杠后是他总共扫了几次。机器人扫描取不到归属时整对前面带 ~;一次都没扫过仍然显示 —。
  • 模组/EWC 武器的命中拿不到,按 Dinorush 的 StatDisplay 那套接好了。 原版武器每次开火的射线会经过 Weapon.CastWeaponRay,插件从那里逐弹丸计数;EWC 接管的武器不走这条路径,于是「发射数」靠弹药消耗兜底算了出来,「命中数」却因为没有被射线开出的弹丸编号而被整条丢弃(ShotSample.Contact 开头那句 if (_pellet < 0) return;),表现就是截图上的 37 / 0 / 0 → 0.0%。现在照 StatDisplay 的三步走:① 运行时用插件列表字符串判断 EWC 在不在(IL2CPPChainloader.Instance.Plugins.ContainsKey("Dinorush.ExtraWeaponCustomization"));② 用 weapon.GetComponent<CustomWeaponComponent>() != null 认出 EWC 武器,本模组主动不碰它们(BeginFire 直接返回),避免和 EWC 自己重复计数;③ 订阅 EWC 自己的 EWC.API.Accuracy.AccuracyAPI.OnAccuracyUpdate,它每次射击直接给出「打了几发弹丸 / 命中几个肢体 / 其中几个弱点」(WeaponDelta.FullShots),落到和原版武器同一行,精确、不带 ~。所有碰 EWC 类型的成员都标了 [MethodImpl(NoInlining)]:EWC 不在时它的程序集根本不会被解析,EwcBridge.cs 整个用 EWC_PRESENT 编译掉,行为与之前完全一致。运行时依赖仍只有 BepInEx。
  • 兜底仍然保留:万一某把枪的射线本机看不到而 EWC 又没接管它,改由射击窗口补开第一个弹丸(窗口本身证明这一枪打了),这一行会带 ~,不伪装成实测值。
  • [DIAG] Fire captured 现在带上武器名、武器类、射线数、命中数、弹药消耗,以及是否走了兜底估算,下次现场一眼可定位。
  • 回归断言 665 → 678。

1.14.0

  • 客机现在能看到全队的真实数据——前提是两端都装本模组。 主机是唯一同时看得到每个人开火、背包与伤害的机器,它把每位玩家的一份快照发给回应过握手的对端;客机照旧精确测自己那一行,并用主机发来的数字填满其余各行。只让主机单方面装模组做不到:快照是模组自定义数据,没装模组的一端既没有解析器也注册不了这个类型,收不到、也解不开。
  • 握手由主机发起、客机只应答。 主机每 2 秒给尚未表态的槽位各发一次 Hello(带协议版本号),应答过的对端才进入收件名单,那之后不会再被打扰。客机从不主动开口,所以主机没装模组时整个大厅一条包都不会发,客机行为与 1.13.1 完全一致。
  • 本机那一行永远不被覆盖。 收到关于自己的快照直接丢弃:这一行在本机是实测,在主机只是估算。
  • 快照是覆盖写入,不是累加。 主机那一份代表"这名玩家此刻的全部事实",累加会把两台机器都看到的伤害算两遍。未观测的项照旧落到 —,不会因为同步反而变成 0。
  • 全队伤害占比在客机上从 — 变成真实百分比:主机把全队总伤害随快照一起发出,客机不必再用自己那一列去除以全队。
  • 快照还带主/副武器的 archetype id 与名称(工具、近战两格只带名称),所以客机结算看板上的装备名与主机一致,而不是退化成 GearStandard 这类槽位名。
  • 降级与自检。 自定义数据类型注册失败、或对端协议版本对不上时,只打一条 warning 并退回 1.13.1 的行为;首次握手、首个对端、首次收发各有一条 [DIAG] squad sync … 日志,实机一眼可判。
  • 回归断言 633 → 665(新增 32 条:快照编解码、覆盖式写入不累加、未观测不留 0、本机行拒收、UTF-8 名称跨包与码点边界截断、capture→apply→capture 往返一致)。

1.13.1

  • 名字里的富文本标记不再被当成名字打印。 玩家名允许带 TextMeshPro 标记,实机日志里就有一个 <#fa0>"<#0ff>A1pha<#f00>"。表格绘制时 richText 是关的,于是「取前六个文本元素」的截断把标记切出一截前缀,结算看板第三名此前显示的就是裸的 <#fa0>。现在先剥掉可识别的标记(<#rrggbb>、<color=…>/</color>、<b>、<i>、<size=…>、<sprite …>、<br> 等),再按可见文本元素截到六个:同一个名字现在是 "A1pha。不成对的 < 仍按字面保留;名字全部由标记组成时输出空串,不会再让 SubstringByTextElements 抛异常。
  • 回归断言 627 → 633(新增六条名字用例,覆盖实机那名玩家、成对颜色标记、<br> 不能拆行、只由标记组成的名字、字面尖括号与未识别标记)。

1.13.0

  • 结算看板的安全区域改成量出来的。 成功页不再用「屏高 × 0.16」这个固定分数,而是读页面自己的两块地标:完成分区(分区底板、分区标题、四个分区图示)与送回大厅按钮,看板落在两者之间那条带里。两侧都读不到时才退回原来的分数,并且仍然只让开页面顶部,不会盖在量出来的元素上。字号与行距随这条带的高度缩放(渲染网格最多缩到 0.45 倍),因此 1920×1440、2560×1440、1280×720 下四个人的数据都在,不靠裁掉第三/第四名来腾地方。日志 [DIAG] success region: completion=…, lobbyButton=…, boardTop=…, room=…。
  • 战斗数据按真正开火的那把枪归属。 游戏给每一条子弹与近战伤害记录都带上开火武器的 gearCategoryId,插件现在用它把伤害、命中与友伤落到那把枪上,而不是落到「此刻手里拿着什么」。改枪之后才落地的穿透弹与延迟弹丸因此不会再算到新换的武器上。副作用是命中率与友伤也一起归位,因为三者走同一条伤害入口。
  • 模组武器(含 EWC)不依赖任何第三方模组即可归属。 EWC(Dinorush.ExtraWeaponCustomization)的武器可能不暴露自己的 archetype id,插件改为用 ItemEquippable.GearCategoryData.persistentID 与伤害记录里的 gear category 配对;一个 category 在本机对应多把枪时不做猜测,退回「手里拿的那把」。这样运行时依赖仍然只有 BepInEx 本体。原先预留的 PelletHitLedger 已删除:它需要一个原版数据里不存在的「子发射稳定 ID」,在只依赖 BepInEx 的前提下没有任何数据能喂给它。
  • 评分重新校准。 救援队友权重 1.5 → 1.0,弱点率 0.7 → 0.55,命中率 0.6 → 0.35,其余不变,顺序与团队贡献优先的设计不变。仅救援 1 人的玩家因此不会再拿到最高名次,也拿不到「妙手回春」;救援次数达到门限(默认 2)后照常计分并在数据行里如实显示。
  • 命中率带上置信度。 命中率按「已发射弹丸数 ÷ 20」折算,最多 1 倍:3 发全中不再压过 100 发 80%。同一发射量下命中率越高得分仍不倒退(50%、60%、70%、80%、90% 五个档位已断言单调)。
  • 旧默认权重自动迁移。 配置文件里还留着 1.12.1 那组默认值(1.1,1.2,0.9,1.5,0.7,0.6,1.4,1.3,0.9,0.7)时,加载时自动换成新的默认值并打一条日志;任何手改过的权重原样保留。
  • 扫描行改名「首次标记」并按次数计分。 结算看板该行此前显示「首次揭示数 ÷ 扫描次数」的比值,现在直接显示首次揭示的敌人数(尝试次数只作展示),因为它比比值更直白也更难被重复扫描稀释。
  • 未观察到的数据不再显示成 0。 命中率、弱点率、伤害、友伤、弹丸数在没有任何观测时一律显示 —;只有「确实观测到、结果就是 0」才显示 0。新增 DATA-AVAILABILITY.md,逐项写明姓名、装备、命中率、敌人伤害、友伤、队友统计与全队伤害占比在「主机装了本模组 / 主机没装」两种情况下的来源、准确度与限制。
  • Thunderstore 图标换成 1254×1254 粉色角色图。
  • 回归断言 454 → 627,Release 构建 0 警告 0 错误。仍未实战验证:成功页安全区域在真实转场中的落点、EWC 武器在实战中的 gear category 归属、客机面板内容、失败页淡入透明度同步。

1.12.1

  • 机器人扫描的归属改成精确取值。 1.12.0 靠“标记中心一米内唯一携带追踪仪的机器人”猜归属,而四人局里三个机器人都带追踪仪,实机必然失败(现场日志:bot tag observed but no unique nearby bot bio-tracker owner resolved,四人扫描效率全为 —)。反编译确认 EnemyScanner.BotTag 是静态方法,全游戏只有一个调用者:机器人自己的 Player.PlayerBotActionUseEnemyScanner.UpdateStateScan() 实例方法,它的 m_agent 就是该机器人。插件现在钩这个实例方法,在它执行期间发布 owner、返回即清除,静态 BotTag 因此拿到确定的那一个机器人;只有读不到时才退回原来的近距离规则。未归属时的诊断也会打印候选数与最近距离,成功时打印归属来源。
  • 失败页的“单人存活”不再偷偷计分。 这一行在失败页显示 —,但评分此前仍把它的权重算进总分;现在失败页显示与评分一起关闭,口径一致,不会再出现“看不见的加分”。
  • 补给链路补上可观测性。 弹药给予、弹药包回调、回血三条入口首次触发各打一条日志;此前一次补给都没发生时日志完全安静,无法区分“没人补给”和“读不到数值被跳过”。
  • 454 条回归断言通过,Release 构建 0 警告 0 错误。真实对局仍需确认新的归属钩子在实战中命中(日志 [DIAG] bot scan action observed 与 [DIAG] bot tag credited)。

1.12.0

  • 扫描效率只认首次揭示。 全队共享一本揭示账本:某个敌人第一次被扫上生物追踪仪的那一次扫描记一分,之后无论谁再扫同一个敌人,都只增加扫描次数、不再增加揭示数。重复标记别人的目标不再刷分,扫描效率(首次揭示数 ÷ 扫描次数)只会被重复扫描拉低。
  • 近战推击打断攻击计入硬直。 推击在游戏里走自己的链路(PushDamage → ReceivePushDamage),从不经过近战伤害入口,也不造成伤害,因此钩子改在推击入口、在游戏处理推击之前读取敌人当时的攻击状态:打断正在进行或正在蓄力的攻击就记一次硬直。同一帧内同一敌人的重复上报只记一次;致死反应与无攻击状态一律不计。
  • 硬直按“被打断的攻击次数”计,不再累加伤害数值。
  • 补给效率改成每次使用的有效量 ÷ 应有量的平均值,记在使用者名下;自己给自己用同样计入,拾取不计入分母,溢出按 0% 计。旧的“相对量 ÷ 拾取容量”口径及其补丁已删除。
  • 零分玩家固定“战地游客”。 显示分正好是 0.0 的玩家拿到固定且可重复的负面称号;其他并列玩家仍然各自拿到不同称号。
  • 击杀分类用完整文字标注:小怪击杀 / 精英击杀 / BOSS击杀,结算表始终分行。
  • 默认权重改为团队贡献优先:总伤害 1.1、硬直 1.2、击杀 0.9、救援队友 1.5、弱点命中率 0.7、命中率 0.6、补给效率 1.4、扫描效率 1.3、单人存活 0.9、阵亡 0.7(越少越好)。配置文件里仍可改成任意十个非负数。
  • 结算底板更透明:底板 0.50、行带 0.020、分隔线 0.12。
  • 450 条回归断言通过,Release 构建 0 警告 0 错误。真实对局的验证边界:推击在实战中的实际触发、跨玩家重复标记的去重、原生字体创建、失败页淡入同步仍需进游戏确认;仓库里的模型预览不是游戏截图。

1.11.1

  • Fix the silent failure-board suppression path: unavailable title geometry now uses a logged screen-space fallback rather than skipping the entire table. Known hidden headings still wait, and known heading alpha is preserved. Add stage diagnostics for visibility, measurement, missing counters and fit failures.
  • Bypass Font.CreateDynamicFontFromOSFont, whose current local Interop helper calls a missing Font constructor. Create the Font through its existing default constructor and invoke Internal_CreateDynamicFont directly.
  • 414 regression assertions pass. Local installation hashes are checked separately from packaging. Actual rendered results and font creation remain gameplay checks.

1.11.0

  • Reworked settlement readability: a dark plate, alternating rows, section rules, stronger names and scores, left-aligned descriptive cells and a shared right edge for numeric cells. Multi-value cells use measured / separators; the HUD retains field slots.
  • Scores use podium colours only, with no gold/silver/bronze words. Kill tiers are labelled in both merged and split modes; fresh configurations default to split tiers. Existing configuration is preserved.
  • Failure placement uses the menu camera and visible TextMeshPro heading. The board follows heading alpha and stays below its rendered bounds; negative offsets cannot move it over the heading. Insufficient room is not resolved by covering the heading.
  • Human bio-tracker accounting uses one tag-attempt edge ledger and one enemy-instance identity set, sampled around UpdateTagProgress and by the frame observer. Passive radar growth is not a scan attempt.
  • Bot scanning listens to BotTag only, not BotGetScanResult. It accepts only a unique nearby bot carrying a tracker and groups callback bursts using tag duration. Attribution/timing are still estimates, shown with ~; unresolved events remain unavailable.
  • 404 regression assertions pass. Actual rendered transitions and the one-human/three-bot scan case remain unverified in gameplay. Model preview is not a game screenshot.

1.10.0

  • The top three rows of the board are the player's place and name, the score and the sentence. The old layout wrote the name twice: once on a rank strip and once on a row of its own. The duplicate is gone, the score moved up from the bottom of the block to the second row and the sentence to the third, so the four gear slots and the two weapons' figures follow straight after and the descriptive block is one row shorter (thirteen fixed rows, a separator and the ten metrics: 24 rows, 26 with the kill tiers split).
  • No two players wear the same sentence. The evaluation now runs from the top of the board down: a player takes the sentence of the metric they lead the squad in outright that they contributed most to, and when somebody above them already wears it they step down their own list to the next one still free. A player who leads nothing ends up with a sentence of their own rather than sharing the neutral title with everyone else.
  • The result pages' own blocks are really gone this time. Switching a block off is not enough: both pages animate their content in over several seconds and switch it back on as they go, which is why the artifact inventory kept coming back. The blocks are therefore also moved right out of the page, and the whole thing is re-applied on every frame the page is up rather than once per poll. Results/HideVanillaSuccessBlocks and Results/HideVanillaFailBlocks still put them back, and this is the same problem and the same workaround that StatDisplay documents.
  • The failure board keeps clear of the failure message. The message is measured through the camera that renders the page and the board is placed in whichever of the two gaps it leaves is the larger, so the board no longer lands on top of it. If it cannot be measured the board falls back to the place it has always used, and Results/OffsetY still nudges it either way.
  • A stagger is an interrupt and only an interrupt. The row now needs the enemy's attack to be under way and not yet finished, and it will not count a kill: the hit reacts that report a death are rejected, and so is any blow that takes the last of the enemy's health, because a corpse that stopped moving was not interrupted. When the enemy's own attack state cannot be read the row stays empty rather than counting hits that interrupted nothing. Scoring/StaggerNeedsAttack turns the attack requirement back off.

1.9.0

  • The result screens are now an influence board. Success and failure no longer print the plain per-weapon table; they print a board whose rows are fields and whose columns are players, ordered by a composite score out of 100. Each player gets a place, a sentence, the four gear slots they carried, both weapons' damage/share, hit/weakspot and pellet figures, and then the ten scored metrics below.
  • Ten metrics, each with a weight and a gold star. Total damage (1.5), stagger interrupts (1.4), kills (1.3), revives (1.2), weakspot rate (1.0), hit rate (0.8), supply efficiency (0.9), scan efficiency (0.9), lone survivor (1.0) and deaths (0.8, reverse). The squad's outright leader in a metric has its cell tinted gold; a tie tints nobody. The score row is coloured gold, silver, copper, then plain white.
  • Kills are graded from the enemy's own maximum health, so a modded enemy is graded by what it is rather than by a name list that would go stale: below Scoring/KillMinorMaxHealth (120) is a minor kill, below Scoring/KillEliteMaxHealth (1000) an elite, above it a boss. A boss is worth three minors and an elite two, and Scoring/SplitKillTiers prints the three classes on their own rows instead of one merged row.
  • A sentence per player, taken from the metric they contributed most to and only when they lead the squad in it outright; a player who leads nothing is the squad's 团队中坚.
  • Support kits are lifted. A player whose class tool matches Scoring/SupportToolNames (bio tracker, glue gun, lock melter, C-foam) gets Scoring/SupportRoleFirepowerBonus (1.25) on the fighting metrics and Scoring/SupportRoleUtilityBonus (1.25) on the support ones. Set either to 1.0 to switch it off.
  • The vanilla blocks the board replaces are switched off in place, one at a time and each in its own guard: the artifact inventory, the four prisoner report cards and the checkpoint usage on the success screen, the artifact inventory on the failure screen. Results/HideVanillaSuccessBlocks and Results/HideVanillaFailBlocks put them back without recompiling. Nothing inside a report card is ever repositioned and no vanilla text object is ever written into.
  • Six new Harmony hooks feed the metrics the damage and accuracy patches could not already reach: ProcessReceivedDamage (stagger), InstantDead and EnemyAgent.OnDead (kills, both feeding one de-duplicated ledger), Interact_Revive.TriggerInteractionAction (revives), PlayerAgent.GiveAmmoRel (supply handed out) and ResourcePackPickup.OnInteractionPickUp (the capacity it came from). Who is still standing, who is the last one standing and what the bio tracker has revealed are polled from the per-frame state observer instead, so no patch is spent on a signal that only ever needs comparing with its own previous value.
  • Every hook logs the first time it fires, and every one that cannot read its input logs once and skips itself rather than throwing into the game. The two assumptions that only the real game can settle -- whether the bio tracker's blip array clears between sweeps, and whether a resource pack's capacity is the same unit as the relative amounts a player hands out -- are guarded so a build where they do not hold shows a dash instead of a wrong score.
  • The in-level HUD table is untouched: same columns, same headers, same share arithmetic, same name colouring and right alignment, same — placeholder, same position under the flashlight icon.

1.8.2

  • Narrower columns. The space surrounding every cell was cut by more than a third: the whitespace between two neighbouring columns went from 1.067em to 0.625em. That figure is the padding of both cells plus the gap between them (PaddingFactor 0.45 → 0.27, GapFactor 0.55 → 0.30), and the whole table follows it at every density.
  • The four column floors whose header is the widest thing they hold came down with it: friendly fire 3.4 → 2.65em, accuracy and weakspot 4.6 → 3.05em, damage/share 7.0 → 6.0em (3.5 → 3.0em per field). A floor is only a minimum — a column still grows to whatever its widest cell measures — so a lower floor can never truncate a value; it only stops the column reserving room its header never used.
  • The 总弹丸 命中 弱点 triplet keeps the spacing that was called "about right": its field gap factor is still exactly 0.60 and its column floor is still 6.8em, and the suite asserts both of its distances are identical to 1.8.1 to within 1e-4.
  • Nothing else moved: same columns, same headers, same share arithmetic, same name colouring and right alignment, same results layout, same — placeholder.

1.8.1

  • No slashes in the panel. The merged columns are no longer held apart by a drawn /: the fields sit side by side, separated by a gap that is a fraction of the glyph size. 伤害/占比 and 总弹丸/命中/弱点 became 伤害 占比 and 总弹丸 命中 弱点, so both merged columns are narrower and the table takes less of the screen.
  • The result blocks follow, so no / appears anywhere in the mod's text any more.

1.8.0

  • Merged columns are split into measured field slots: the damage/share figures and the pellet triple each keep one right edge per field from row to row, instead of the whole group being pushed around by the widest value.
  • The pellet header qualifies "total" only, which is two characters narrower.

1.7.0

  • Placed against the native flashlight icon: its screen rectangle is measured through the HUD's own camera until it holds still, then frozen, so the table sits next to the gear column and stops swaying with the view.

1.6.0

  • Default position moved to a right-edge anchored fraction.

1.5.0

  • The table moved to IMGUI in window pixels: rendered, immobile, and always fully on screen.

1.4.0

  • Placed inside the game's HUD hierarchy. It rendered, but inherited the HUD's parallax and its right-hand columns were clipped by the screen edge.

1.3.0

  • A canvas owned by the mod. It was sized correctly but produced no pixels at all.

1.2.0

  • Parented into the HUD hierarchy and positioned through a screen-to-local conversion on the wrong camera. The table was displaced and skewed.

1.1.0

  • Standalone build: player damage, friendly-fire loss, projectile accuracy and weakspot rate, per player and per weapon.

1.0.0

  • Initial local build.