Skip to content

第十五部分 · 15.4 Buddy 外观反作弊 — 会话级重算与配置一致性

导航← 15.3 Buddy · 15.5 深度规划 →


学习目标

完成本节学习后,你应该能够:

  1. 解释 为何纯客户端彩蛋仍需要「反作弊」:防止用户通过编辑本地配置获得非种子一致的外观组合。
  2. 描述 每次会话重算外观的策略:在会话初始化阶段基于权威输入(如 userId、服务端/密钥链、会话盐)重新派生眼睛/帽子/闪光位。
  3. 关联 15.3 中的组件规模:6 眼睛8 帽子(Common 无帽)、物种与稀有度。
  4. 对比 「持久化缓存」与「会话派生」在 UX 与完整性上的权衡。

生活类比:游乐园手环 versus 自制贴纸

把 Buddy 外观想象成游乐园入场手环颜色

  • 手环颜色由售票系统根据身份证哈希决定,不是你进场后自己贴一张彩色贴纸就算数。
  • 每次入园(新会话),闸机重新验证并重新发放当日有效手环——防止你昨晚把贴纸撕下来换给别人用。
  • Common 无帽像「基础票不发遮阳帽」:不是歧视,是稀有度分层的一部分。

威胁模型(教学简化)

攻击意图手段示例防御思路
改本地 JSON手动把 hat 改成传奇款会话重算覆盖
换皮资源替换 png/svg 文件哈希校验或打包签名(若实现)
内存改值运行时 patch 状态周期性 reconcile 或只信派生函数
共享配置拷贝「他人宠物档案」种子绑定 userId,拷贝无效

Mermaid:会话重算时序


Mermaid:外观派生状态机


源码片段:派生与覆盖(示意)

typescript
// appearance.ts(示意)
export type BuddyAppearance = {
  eyeVariant: number; // 0..5
  hatVariant: number | null; // 0..7 or null if common without hat
  shiny: boolean;
};

export function deriveAppearance(input: {
  userId: string;
  sessionId: string;
  rarity: Rarity;
  species: string;
}): BuddyAppearance {
  const rng = mulberry32(
    hashCompositeSeed(input.userId, input.sessionId, input.species)
  );
  const eyeVariant = Math.floor(rng() * 6);
  const shiny = rng() < 0.01;
  const hatVariant =
    input.rarity === 'common' ? null : Math.floor(rng() * 8);
  return { eyeVariant, hatVariant, shiny };
}
typescript
// session-bootstrap.ts(示意)
export function bootstrapBuddyUiState(ctx: RuntimeContext) {
  const rolled = rollBuddy(ctx.userId); // 15.3
  const canonical = deriveAppearance({
    userId: ctx.userId,
    sessionId: ctx.sessionId,
    rarity: rolled.rarity,
    species: rolled.species,
  });
  // 本地文件若存在篡改字段,一律以 canonical 为准
  ctx.ui.buddy = { ...rolled, appearance: canonical };
}
typescript
// integrity-check.ts(示意)
export function reconcileStoredBuddyPrefs(
  disk: Partial<BuddyAppearance> | undefined,
  canonical: BuddyAppearance
): BuddyAppearance {
  if (!disk) return canonical;
  const mismatch =
    disk.eyeVariant !== canonical.eyeVariant ||
    disk.hatVariant !== canonical.hatVariant ||
    disk.shiny !== canonical.shiny;
  if (mismatch) {
    telemetryDebug('buddy_prefs_reset', { reason: 'tamper' });
    return canonical;
  }
  return canonical;
}

眼睛 × 帽子 × 稀有度(约束表)

稀有度帽子眼睛闪光
Commonnull6 选 1独立 1%
Uncommon8 选 16 选 1独立 1%
Rare8 选 16 选 1独立 1%
Epic8 选 16 选 1独立 1%
Legendary8 选 16 选 1独立 1%

与「仅种子_roll」方案的区别

方案优点缺点
仅 userId 种子极简、可预测换会话无法轮换会话盐;篡改本地更易「粘住」错误状态
userId + sessionId防简单拷贝配置、鼓励每会话对齐同用户每天外观可能变化(若实现如此)
userId 唯一权威最强「身份一致」需在文档声明 session 盐是否参与

本指南教学口径:强调「每次会话重算」——实现可选用 session 盐或仅校验和覆盖,但核心不以磁盘为最终真相源


遥测与隐私

事件是否应含 PII说明
buddy_prefs_reset用匿名会话 ID
外观哈希仅用于崩溃聚合

测试用例矩阵(节选)

用例输入期望
T1删配置文件回退 canonical
T2hat 改非法索引重置为派生值
T3common + 非 null 帽强制 null
T4连续两次 SessionStart若含 session 盐则 eye/hat 可变;否则稳定

与 Feature Flag 的交互

Flag影响
BUDDY off跳过派生与 UI,防无谓计算
调试 Flag(若存在)可能固定 rng 种子便于截图

常见问题 FAQ

问题回答方向
这是安全机制吗?主要是体验一致性,不是金融级安全。
会影响性能吗?派生为 O(1) 计算,可忽略相对 LLM 延迟。
mod 社区呢?第三方皮肤与官方 integrity 可能冲突,属预期外。

小结

  • 反作弊在此语境 = 配置篡改无效化,通过会话级 canonical 重算实现。
  • 6 眼 / 8 帽 / Common 无帽 必须在派生函数中硬编码约束,而非仅信 UI。
  • 与 15.3 的 PRNG 配套:物种稀有可长期稳定,外观可会话刷新——二者并不矛盾,取决于复合种子设计。

课后自测

  1. 写出一个「磁盘偏好」结构体,并标注哪些字段是「只读展示」、哪些是「可写用户设置」(如静音彩蛋)。
  2. 解释为何「会话重算」能阻断「复制他人配置文件」攻击路径。
  3. 绘制测试矩阵中 T3 的断言伪代码。

上一节15.3 Buddy 宠物
下一节15.5 Deep Planning Mode

本项目仅用于教育学习目的。Claude Code 源码版权归 Anthropic, PBC 所有。