Skip to content

Support repeated projection of blueprints and improve runtime status and entity restoration 支持蓝图重复投影并完善运行状态与实体恢复 - #4971

Open
WhereisFff wants to merge 1 commit into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:dev/1.21/fix6
Open

WhereisFff wants to merge 1 commit into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:dev/1.21/fix6

Conversation

@WhereisFff

@WhereisFff WhereisFff commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

- 支持拖拽平铺蓝图投影、松开锁定并再次右键批量粘贴,统一预览与放置范围限制
- 完善选区距离锁定、方向调整和尺寸显示,同步更新操作提示与中英文翻译
- 统一结构扫描快照采集,保存并恢复方块运行进度、计划刻与时间基准,兼容 Litematica 计划刻导入
- 完善建造后的红石、流体与邻居更新,避免恢复结构时误触发侦测器
- 扩展生物、掉落物、投射物、移动方块及模组实体的蓝图建造,恢复运动状态、乘骑关系和实体引用
- 支持树脂封存生物供料,补全实体装备、库存、流体及特殊方块材料需求
- 增加磁铁、铁砧锤与点火工具需求校验,并显示不支持建造的具体对象
- 将实体纳入建造撤销,仅撤销状态未变化且允许修改的建造组
- 蓝图导出遇到同名文件时自动添加序号,并反馈实际导出文件名
Copilot AI lite review requested due to automatic review settings September 21, 2026 01:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -s "https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4971" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('changed_files',d['changed_files'])
print('additions',d['additions'],...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4971 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4971 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp; cat > extract.sh <<'EOF'
#!/bin/bash
# usage: extract.sh <file-substring> <diff> 
F="$1"; D="${2:-/tmp/pr4971.diff}"
H="${D}.headers"
grep -n "^diff --git" "$D" > "$H"
START=$(grep -n "^diff ...

Reason: shell execution via heredoc

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

I have everything I need. Writing the review now.

代码审查摘要 — PR #4971

操作: opened(dev/1.21/fix6dev/1.21/1.6,base 8aba6d835f
范围: 41 个文件(39 Java,9 新增,0 删除)/ 3307 行 diff,146 hunks(+1746 / -305
版本线: 1.21.x 常规功能 PR(非 26.1 迁移),跳过 API 迁移健康度检查

✅ 已核对的编译面(无问题)

用官方 1.21.1 client_mappings + 混淆客户端字节码核对:

引用 结论
AABB.of(BoundingBox)(BuildingRodRenderer) ✅ 存在(AABB.a(BoundingBox)
Level.neighborShapeChanged(Direction, BlockState, BlockPos, BlockPos, int, int)(BlueprintLevelMixin 注入目标) ✅ 签名与参数顺序完全对应
PistonMovingBlockEntity 持久化键 blockState/facing/progress/extending/source ✅ 全部存在,BlueprintRuntimeData/transform 的键名正确(字段名是 direction,NBT 键确是 facing
isProcessing()/getPhaseRemainingTicks()(BuildingCommit.activate) ✅ 目标分支已存在
StructureScannerFiles.handle 异常契约 ✅ 已捕获 IllegalArgumentExceptionflatten 抛「Too many blueprint passengers」不会炸服务端
en_us / en_ud 同步 ✅ 两侧 6 个键改动一致

📋 声称验证表

声称 状态 对应文件
拖拽平铺投影 / 松开锁定 / 右键批量粘贴 / 统一范围限制 BlueprintPlacement.tileBuildingRodService.blueprintsBuildingRodPacket.PLACE_BLUEPRINTSBuildingRodClient.blueprintPlacements
选区距离锁定、方向调整、尺寸显示、中英翻译 BuildingRodClient.applyControl/selectionBoundsBuildingRodLang、两个 lang json
统一快照采集 + 运行进度/计划刻/时间基准 + Litematica 计划刻 BlueprintCaptureBlueprintTicksBlueprintRuntimeDataLitematicaImporter
建造后红石/流体/邻居更新,避免误触发侦测器 BuildingCommit.activate/activatePlacedBlueprintLevelMixin.neighborShapeChanged 守卫
生物/掉落物/投射物/移动方块/模组实体,运动状态、乘骑、实体引用 ✅(见 🔴1) DynamicBuildingEntitiesBlueprintEntitiesisTransient 放宽
树脂封存生物供料,装备/库存/流体/特殊方块材料 ✅(提示文案见 💡8) BuildingMaterials.reserveCreatureEntityBuildAdapters.insertContents
磁铁/铁砧锤/点火工具校验 + 显示不支持对象 requiredTool/requiresHammer/ignitionsunsupported_type
实体纳入建造撤销,仅撤销未变化的建造组 BuildingRodUndo.recordEntitySavedEntity.matches
导出同名加序号 + 反馈真实文件名 ✅(见 💡9) StructureBlueprintFiles.writeStructureScannerFilesBlueprintClientFiles

🔴 关键

  • EntityBuildAdapters.isTransient 移除 Projectile 过滤后,任何“无适配器”的实体会中止整次建造**(不是跳过)**
    isTransient(EntityType) 删掉了 Projectile.class.isAssignableFrom(base)ItemEntity(前者有意为“支持投射物”),但 BuildingRodService.planBlueprintBuildingRodService.java:589-598)在找不到适配器时是 unsupported(...); return false;blueprints() 随即 return——整份蓝图(含全部平铺副本)一块都不放,只留一条客户端提示。
    箭/三叉戟靠 NBT 里的 item 键命中匹配器 ✅,但凋灵之首、末影龙火球、风弹、烟花火箭、羊驼唾沫、拴绳结等既不是 Mob/FallingBlockEntity,NBT 也没有 item/Health 键 → 旧版作为 transient 被静默忽略,现在会让整份蓝图无法建造。建造杆蓝图里混进一发风弹/烟花就会全盘失败,体验上很像 bug。
    建议:把 adapter == nullplan.unsupported() 改成 continue(跳过该实体),在结束时用一条消息汇总被跳过的对象;实在要中止,至少区分“方块不可建造”(中止)与“个别实体不可建造”(跳过)。

⚠️ 警告

  • BuildingEntityTransform.sanitize 由白名单改为「整份 NBT 去掉 UUID/Passengers」BuildingEntityTransform.java:120-140
    旧实现只放行 id/Pos/Rotation/CustomName + 各实体类型白名单字段,是一道明确的加固措施;现在蓝图文件里的实体数据可以整体注入世界(TagsInvulnerableNoGravityAttributes、超上限 HealthActiveEffects…)。StructureScannerFiles 的 IMPORT 分支允许客户端上传任意 .nbt/.litematic,所以“文件内容不可信”这条前提是成立的(op-only 实体与 FallingBlockEntity.TileEntityDatarequiresOperator 闸门做得不错,但它们只挡了方块实体载荷,没挡实体自身字段)。
    建议保留白名单思路,把 Motion/Health/HandItems/ArmorItems/ArmorItems/Inventory/Brain(已有 relocateMemories 处理)本次确实需要恢复的键显式加进去,而不是整体放开。另外 sanitize(Entity entity, …)entity 参数现在已无引用,可一并清理。

  • BlueprintTicks.capture 把接口强转成具体实现BlueprintTicks.java:81,86
    LevelChunk.getBlockTicks() 的声明返回类型是 TickContainerAccess<Block>(已用 mappings 核实),这里直接 (LevelChunkTicks<Block>) 向下转型。任何包装/替换 tick 容器的实现(部分性能类模组会这么做)都会在导出蓝图时抛 ClassCastException,而异常会一路冒到 StructureScannerFiles.handle 只留一条 warn。
    建议:if (chunk.getBlockTicks() instanceof LevelChunkTicks<Block> ticks) { … } 加守卫(拿不到就当作“无计划刻”,退化为旧行为),流体侧同理。

  • BlueprintRuntimeData.take 的调用时机使多处按类型清洗变成死代码,并让“设置值”绕过钳制
    take()BlueprintBlockConfiguration.take最前面执行(BlueprintBlockConfiguration.java:57),而 FIELDS 里包含 Cooldown / cd / Rolling / Faces / PreviousFaces / Output / RollStart / phase / progress / currentPlacementIndex / Powered / PoweredBefore …(对所有方块实体生效),随后 result.merge(runtime) 又把这些原值盖回结果。因此:

    • integer(source, result, "Cooldown", 0, 3)(ItemCollector/ExpCollector)、integer(..., "Cooldown") 的 BaseChute、smartPlacer(... "currentPlacementIndex","phase","progress")、红石骰子的 remove(source, "Rolling", …) 全部读不到键 → 钳制/清洗失效;
    • 结果里这些值由 runtime 原样注入(例如超范围的 Cooldown 会被直接还原),而「库存由材料系统供应、设置才走校验」的设计意图被削弱。
      建议:把 take 拆成“先按类型收集 settings,再取 runtime”,或在 merge(runtime) 时对需要钳制的键单独处理。
  • 服务端导出改为读取实时世界(BlueprintCapture),与客户端预览路径分叉
    StructureSaveUtil.buildSnapshotServerLevel 上直接 BlueprintCapture.capture(serverLevel, getScanBounds())scannedBlocks 参数被完全忽略;而客户端 StructureScannerScreen / DiskDisplaySupport 仍走旧的缓存路径(getLevel() 非 ServerLevel)。后果:

    1. 导出结果 = 当前世界状态,不再是“扫描时”的缓存结果;
    2. BlueprintTicks.capture 对范围内每个区块调用 level.getChunkAt(...),会**强制同步加载(必要时生成)**扫描区域内的区块——旧路径只读缓存,不触chunk 加载。扫描器区域一般不大,风险有限,但建议确认 rangeX/Y/Z 上限下的最坏区块数。
    3. 客户端预览与最终文件内容(计划刻、实体、方块实体数据)会不一致。
      顺带确认:新路径对 ScannerDiskNormalizer 固定传 Direction.NORTH, false 是正确的——因为捕获已是世界坐标系(旧路径是 世界→预览帧→normalize(facing) 两次旋转相消),这点我逐分支核对过,不是 bug。

💡 建议

  • DynamicBuildingEntities.spawnsliding.setMoveDirection(sliding.getMoveDirection()) 是空操作DynamicBuildingEntities.java:162-164)。看着像原本想恢复/重算方向或触发同步;如果只是残留,请删掉。
  • BuildingMaterials.missing() 会改状态reserveTool/reserveHammer 会置 retainedToolreserveIgnitions/reserveCreature 会递增 reserved。目前三个调用点都传 new BuildingMaterials(player) 才侥幸安全,一旦有人在“已预留”的实例上调用就会重复计数。建议拆出只读的 missingReport()
  • 无刷怪蛋生物的材料提示会误导missing()SpawnEggItem.byId(type) == null ? ModBlocks.RESIN_BLOCK.asStack() 给的是纯净树脂块,而 reserveCreature 要求带 SAVED_ENTITY 组件且实体类型匹配的树脂块。玩家照单拿来会继续失败,建议提示写“封存了 <实体> 的树脂方块”。
  • StructureBlueprintFiles.writefor (int index = 1; ; index++) 无上界(并发写入时理论上死循环),且为了拿到 FileAlreadyExistsException 放弃了 ATOMIC_MOVE。建议加个重试上限(如 1000 次后抛 IOException)并保留原子移动。
  • BuildingRodService.blueprints 只对 anchorlastwithinReach(..., 26) 校验,而平铺后最后一个副本的方块会超出 last 近一整份尺寸;单份蓝图本来也只校验锚点,属于既有宽松度,确认是否有意为之即可。

🟢 看起来不错

  • BlueprintCapture 用世界坐标 + NORTH 归一化与被替换掉的旧流水线等价,说明作者清楚两套坐标系的关系(这点最容易踩坑)。
  • BuildingCommit.activate 的顺序很讲究:先 clearArea 清掉残留计划刻 → 恢复快照计划刻 → onPlace/流体 tick → 统一邻居更新;且 activatequietly() 退出之后执行,因此 BlueprintTicksMixinschedule 拦截不会把恢复的计划刻一起吞掉。
  • isRestoredObserverUpdate 同时校验“目标格是本次恢复的侦测器 + 朝向匹配 + 源格也是本次恢复的同一状态”,只保护被恢复的侦测器,不会干扰世界里既存的侦测器。
  • 撤销侧 SavedEntity.matches()(存活 + 序列化快照全等)与 IdentityHashMap<Planned, PlacedGroup>Planned 实例同一性是正确的:BuildingMaterials.reserve 只做 addAll 复制引用,entities 列表与 placedGroups 里的对象是同一批,recordEntity 能命中。
  • BlueprintClientFiles.receive 对服务端回传文件名做了 isSafeName + .nbt 后缀双重校验,导出文件名回显路径是安全的。
  • tile()count * entries > MAX_BLOCKS 估算总量而非只数格子,避免了“1 格 × 999 份却每个 4000 方块”的绕过。
  • MAX_ENTITY_ENTRIES 现在在 capture/canonicalize 阶段就触发,比旧版“写得出、读不进”更早失败。

🧪 测试建议

被测目标 建议场景 优先级
BlueprintPlacement.tile 锚点=last(单份)、±X/Z 平铺、Y 向堆叠、count*entries 刚好越界、跨 4000 上限 🔴
planBlueprint 实体分支 范围内放风弹/烟花火箭/凋灵之首,确认是“跳过”还是“整盘失败”(当前实现验证 🔴1) 🔴
BlueprintEntities.flatten/link 三阶乘骑(船→马→玩家)、自引用/循环乘骑、>32 层嵌套、缺 UUID 的实体、同锚点重复粘贴的 UUID 复用 🔴
BlueprintTicks.capture/restore 边界跨区块的计划刻、delay=0、流体/方块双类、还原后立即 tick 的行为、被 cells 过滤掉的方块上的计划刻 🟡
BlueprintRuntimeData.restoreClock capturedAt=0(Litematica 导入)与有捕获时间的两种路径、RemainingWaitingTime>0、熔炉 BurnTime/CookTime 经“库存插入后”不被清零 🟡
BuildingCommit.activate 侦测器相邻放置不翻转、红石线网络恢复、含水方块不流水、计划刻不被重复调度 🟡
StructureBlueprintFiles.write 同名连续导出 3 次 → _1/_2 序号与回显文件名、超长名截断到 128、并发同名 🟢

结论: REQUEST_CHANGES

功能覆盖与 PR 声称一致,坐标系/时序这类最容易出错的点做得相当扎实。但 🔴1(个别不支持实体会让整份蓝图静默失败,且触发面因 isTransient 放宽而扩大)⚠️2(实体 NBT 白名单整体取消,而蓝图文件可来自客户端上传) 建议先处理;⚠️3、⚠️4 是明确的健壮性/语义回归风险,成本都不高。

PR 标题「Support repeated projection of blueprints and improve runtime status and entity restoration 支持蓝图重复投影并完善运行状态与实体恢复」与变更内容相符,无需修改。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4971

操作: edited(标题/分支变更 → 全量复审)
范围: 41 个文件(33 Java + 2 生成 lang + 6 其他)/ +1746 −305,新增 9 个文件(BlueprintCaptureBlueprintEntitiesBlueprintIgnitionBlueprintRuntimeDataBlueprintSpecialBlocksBlueprintTicksBlueprintBlockEntitiesDynamicBuildingEntitiesMagnetizedNodeBuildAdapter
版本线: dev/1.21/fix6dev/1.21/1.6(1.21.1 常规功能 PR,不适用 26.1 迁移指标;head=f363b43,与 git ls-remote 一致)

审查方式:patch-diff 全量 + 目标分支/头部树上 git show 交叉核对(未编译,无 issue 状态查询——gh token 已失效)。


🔴 关键

未发现确定性阻断缺陷(未发现必然崩溃/数据丢失的硬 bug)。

⚠️ 警告

  1. BuildingRodUndo.SavedEntity.matches() 让「实体组」实际上永远不可撤销,且连带其方块src/main/java/dev/dubhe/anvilcraft/building/BuildingRodUndo.java:113-118
    matches()entity.saveAsPassenger(tag) 与建造完成后立即抓取的快照做 NBT 全等比较,而 Entity.saveWithoutId 会写出 Pos/Motion/FallDistance/Air/PortalCooldown/HurtTime每 tick 变化的字段。生物/掉落物/下落方块/箭在被放下的 1~2 tick 内就会(重力、AI、Motion 归零)产生差异 → matches()==falseundo()continue 跳过整组(方块不还原、group.returned 不返还),玩家只看到 message.anvilcraft.building_rod.nothing_to_undo。PR 声称"仅撤销状态未变化的建造组"是设计意图,但实际效果是:含生物/实体的蓝图组连同其方块一起失去撤销。建议:把实体判据由"整组跳过"改为"只跳过该实体",或改比对白名单字段(类型/装备/位置取整等)。

  2. StructureScannerActionPacket 的 SAVE 调用点未捕获 IllegalArgumentException(兄弟路径都补了)network/StructureScannerActionPacket.java:120
    saveStructureToDisk 本身只 catch (IOException)StructureScannerSavePacket:50catch (IOException | IllegalArgumentException)StructureScannerBlockEntity:341 自动保存也补了 IAE catch,唯独这里没有(该文件全文无 try/catch)。本 PR 把 BlueprintCapture.capture 接进了 buildStructureNBT,新增了若干 IAE 源:BlueprintEntities.flatten"Too many blueprint passengers"(>4096 条/深度>32)、MagnetizedNodeBuildAdapter.support"Missing magnetized node support at …"(节点支撑方块不在快照内/已被破坏时必抛)、SlidingBlockSection.CODEC…getOrThrow()。请补齐与兄弟路径一致的 catch(或统一收口在 saveStructureToDisk)。

  3. 中文翻译未同步(PR 声称"同步更新操作提示与中英文翻译",实际只改了 en_us/en_ud + 生成器)src/main/resources/assets/anvilcraft/lang/zh_cn.json

    • 新增键缺失:message.anvilcraft.building_rod.unsupported_typescreen.anvilcraft.building_rod.selection_size(中文客户端回退英文);
    • 语义已变的键仍是旧中文:screen.anvilcraft.building_rod.distance(zh:2745 仍"固定蓝图")、.optimized(zh:2747 仍是"放置")、item.anvilcraft.building_rod.controls(zh:2712 仍是旧操作说明)、screen.anvilcraft.building_rod.traditional.hint
      占位符个数未变(distance 4 个、optimized 11 个、selection_size 3 个与 Component.translatable(..., xSpan, ySpan, zSpan) 一致 ✓),因此不是 Fix fluid port active draining and improve building-rod overlay text 修复流体端口主动排液并改进建筑杖文本显示 #4916 那种按键错位,只是漏翻 + 文案过期。
  4. 平铺上限估算偏低,且 commit 的 4000 上限对蓝图路径不生效building/BlueprintPlacement.java:126-131building/BuildingRodService.java:645
    entries = max(1, nonAirBlockCount + entities)快照条目数估算,但真正的 cell 来自 BlueprintMultiblocks.expand(门上下半、床、活塞头、多方块都会额外生成部件),实际格数可达估算的 2~4 倍;而 commit 里的 cells.size() > MAX_BLOCKS 检查带 !quiet 前提,蓝图路径恒为 quiet=true。于是"拖拽平铺"可能一次放下远超 4000 格(旧代码单次蓝图也绕过该检查,但平铺会成倍放大)。建议按 expand 后实际格数估算,或对平铺路径启用上限校验。

  5. 客户端预览与服务端保存的数据源不再一致util/StructureSaveUtil.java:154-158
    buildSnapshot 在服务端提前返回 BlueprintCapture.capture(serverLevel, getScanBounds())读实时世界),scannedBlocks 参数在服务端已成死参数;而客户端预览 StructureScannerScreen:1196、磁盘图标 DiskDisplaySupport:89 仍在 ClientLevel 上走旧路径(缓存 getScannedBlocks())。扫描后世界被改动时会出现"预览所见 ≠ 保存所得";另外 StructureScannerScreenif (!scannedBlocks.isEmpty()) 守卫与新的服务端逻辑也不再对称。请确认/注明或统一入口。

  6. 投射物等实体的语义变化:从"静默忽略"变成"整体拒绝建造"building/EntityBuildAdapters.java:56-72building/DynamicBuildingEntities.javabuilding/BuildingRodService.java:408-420
    isTransient(type) 删掉了 Projectile.class.isAssignableFrom(base)(以及 ItemEntity,改由 type == EntityType.ITEM 等显式列举替代),于是箭/三叉戟/烟花火箭/雪球等现在必须能算出材料并被适配器接管;一旦算不出(例如 AbstractArrow.getPickupItemStackOrigin() 为空 → Planned.skip()),Planned.skip()unsupported=true 会让整个投影unsupported_type: <对象名> 失败,而不是像以前那样跳过该实体。这与 PR 的"显示不支持建造的具体对象"一致,但会使用户既有的蓝图(含无材料投射物)直接建不出来——请确认是预期;若否,建议对"单个对象不可供料"只跳过该对象。

  7. (既有问题,本次未触及但正改同一函数)细雪桶返还仍按「首段」计数building/BuildingMaterials.java:257-262
    allocated.blockMaterials.put(entry.getKey(), taken.getFirst().placed()) 只保留第一个 Source 的片段,末尾 for (ItemStack stack : allocated.blockMaterials.values()) if (stack.is(POWDER_SNOW_BUCKET)) … 按该片段数量补空桶。单组期望数跨 ≥2 个 Source(背包每栈独立成 Source)时少还(Fix building wand material handling, equipment abilities and terminal item extraction logic 修复建筑杖材料处理、装备能力与终端取物逻辑 #4901 曾实测:40 个细雪返 32 桶,8 桶蒸发)。修法:遍历 allocated.materialsis(POWDER_SNOW_BUCKET) 的项求和。

💡 建议

  • BuildingCommit.activatePlaced 的邻居/形状扫描开销building/BuildingCommit.java:118-151):每格执行 onPlace + updateNeighbourShapes + updateIndirectNeighbourShapes + updateNeighborsAt + updateNeighbourForOutputSignal + 6×neighborChanged,4000 格时约 4 万次方块操作集中在一 tick,且平铺会更容易触顶。建议分帧/限流,或只在"该格有实际邻居语义"时补齐。
  • DynamicBuildingEntities.spawnif (entity instanceof SlidingBlockEntity sliding && sliding.getMoveDirection() != null) sliding.setMoveDirection(sliding.getMoveDirection()); 是自赋值空语句,若依赖 setter 副作用请加注释,否则删除。
  • AnimateAscendingBlockEntity 计划材料为 ItemStack.EMPTYDynamicBuildingEntities.java:174-176),且非 Mob 分支会 group.materials.add(EMPTY) 留下空栈;它其实是纯动画实体(animate()displayAnvilAnimation 配置门控、tick() 只上移后 discard(),不落地方块),零材料可接受,但与 SlidingBlockEntity 对内部方块逐个计费的写法不对称,请确认。
  • BuildingRodClient.tickclient/building/BuildingRodClient.java:185-190)在 first != null每 tick setOverlayMessage(...) 选区尺寸,会持续覆盖其他 actionbar 提示(含原版),建议仅在尺寸变化时刷新。
  • StructureSaveUtil.buildSnapshotscannedBlocks 参数在服务端分支已不使用,建议清理或注明"客户端预览专用"。

🟢 看起来不错

  • 平铺几何正确tile()bounds(size) 的 span 做步长(拷贝间不重叠)、signX/Y/Z 决定朝向、双重上限(单轴 ≤ MAX_BLOCKS 且 总量×条目 ≤ MAX_BLOCKS),blueprints() 同时校验 anchorlast 的 26 格触及范围;BuildingRodPacket.PLACE_BLUEPRINTS 与客户端 placement.anchor().offset(blueprintOffset) 对得上。
  • 工具/材料账目自洽(本轮改动)retainedTool 在所有 8 条失败路径上都与 reserved 一起由 restoreSources 回滚 ✓;reserveBlock/reserveItem/reserveContainer 一致地扣除被保留的工具位 ✓;reserveCreature 的树脂分支把 1~4 树脂写入 allocated.returned 且与撤销侧 group.returned 对称 ✓;missing() 的 4 个调用点全部是 new BuildingMaterials(player).missing(...)(新实例),探测期的 reserved/retainedTool 变更不会污染真正的预扣 ✓。
  • 计划刻/运行进度BlueprintTicks.capture 记录 subTickOrder 并按序回放、restore 前用 placed 集合 + 当前方块/流体类型一致性过滤;read 校验类型存在性、条目上限 32768;BlueprintRuntimeData.rebase/restoreClockcapturedAt>0 与 Litematica Time 两种来源都给了回退分支;BuildingCommit.activate 只对 quiet 路径调用 ✓。
  • 实体身份/乘骑链flatten 为缺 UUID 的条目补确定性 UUID → identities 按锚点派生新 UUID → scope/link 重映射 + startRidingrelocateMemories 丢弃越界记忆,sanitize 移除 UUID/Passengers 后由 link 重建 ✓ 设计自洽(HasMobBlockEntity:95entity.load(tag) 先例也证明该 API 会应用完整实体状态)。
  • 导出链路两端同步改造:服务端把实际文件名作为载荷(sendFile(..., name.getBytes(UTF_8))total=bytes.lengthoffset=0 与客户端校验一致)、客户端 isSafeName + .nbt 校验后 message("exported", exported) 回显真实名;Files.move 不带 REPLACE_EXISTING 以触发 FileAlreadyExistsException 重试、后缀基于原始 name 生成并做 128 长度截断 ✓。
  • BlueprintLevelMixin 新增的观测器抑制isRestoredObserverUpdate 同时校验 activation.level()、目标格仍是同一 ObserverBlock 状态实例、FACING 与更新方向一致、来源格也是蓝图状态,ACTIVATIONfinally 中原样复原 ✓。
  • en_ud 生成正确:4 处改动字符串逐字符翻转无误,新增键同步;selection_size%s × %s × %s 与翻译参数个数一致 ✓。

📋 声称验证表

声称 状态 对应实现
拖拽平铺投影、松开锁定再右键批量粘贴、统一预览与范围限制 BlueprintPlacement.tileBuildingRodClient.beginBlueprintSelection/confirmSelection/blueprintPlacementsBuildingRodPacket.PLACE_BLUEPRINTSBuildingRodService.blueprints
选区距离锁定、方向调整、尺寸显示 canAdjustDistance/controlAvailable/selectionOffsetselection_size 三参数文案
同步更新中英文翻译 ⚠️ en_us/en_ud/生成器 ✅;zh_cn 未更新(见 ⚠️3)
统一结构扫描快照采集 ⚠️ BlueprintCapture.capture 接入服务端保存;客户端预览仍走缓存(见 ⚠️5)
保存/恢复运行进度、计划刻、时间基准,兼容 Litematica 计划刻导入 BlueprintRuntimeDataBlueprintTicksLitematicaImporter(PendingBlockTicks/PendingFluidTicks)、StructureSnapshot.capturedAt
建造后红石/流体/邻居更新,避免误触发侦测器 ✅(性能见 💡) BuildingCommit.activate/activatePlacedBlueprintLevelMixin#neighborShapeChanged
扩展生物/掉落物/投射物/移动方块/模组实体建造,恢复运动状态/乘骑/引用 ⚠️ DynamicBuildingEntitiesBuildingEntityTransform(Motion/BlockPos/SlidingBlocks/记忆);语义变化见 ⚠️6
树脂封存生物供料,补全装备/库存/流体/特殊方块材料 reserveCreatureBlueprintSpecialBlocks、Mob 装备/InventoryCarrier 剥离与回填、FluidTankMinecartEntity 储罐
磁铁/铁砧锤/点火工具校验 + 显示不支持对象 requiredTool/requiresHammerreserveIgnitionsunsupported_type
实体纳入建造撤销(仅未变化且可修改的组) ⚠️ 已实现 recordEntity/SavedEntity,但状态门使实体组实际不可撤销(见 ⚠️1)
导出同名自动加序号并反馈实际文件名 StructureBlueprintFiles.write 返回候选名、BlueprintClientFiles.receive 校验并回显
resolved #4963/#4964/#4966、fixed #4968、resolved #4970 未校验 无 gh 凭据,无法查询 issue 状态与关闭链

结论: COMMENT(建议处理 ⚠️1/2/3/4 后再合并,⚠️5/6 请作者确认预期) —— 整体设计自洽、账目回滚与两端协议改造质量较好;上述问题集中在"撤销状态门过严导致整组失效"、"保存包路径 catch 不对称"、"中文漏翻"与"平铺上限估算"四点,均属可小范围修复。

🧪 测试建议

被测目标 推荐测试场景 优先级
BuildingRodUndo.SavedEntity.matches() 放置含生物/掉落物的蓝图 → 等 1 tick → 撤销;验证方块是否应还原、材料是否应返还 🔴
StructureSaveUtil.buildSnapshotBlueprintCapture 分支) 扫描区内含磁铁节点但支撑方块被破坏 → SAVE 各入口(GUI 保存 / 自动保存 / action 包)逐一验证不抛异常 🔴
BlueprintPlacement.tile × commit 上限 含门/床/活塞头的多方块蓝图拖拽平铺 → 统计实际放置格数与材料扣减是否一致 🟡
BlueprintTicks.capture/restore 含红石计划刻/流体计划刻的结构导出再投影,验证 tick 时刻与 priority 保持 🟡
BuildingMaterials.reserveCreature 背包树脂块 + 刷怪蛋混放、无蛋生物(仅树脂)、蛋跨两栈拆分 → 计量与撤销返还 🟡
BlueprintEntities.link 生物乘骑载具(含嵌套乘客)蓝图投影,验证乘骑关系与记忆引用指向新 UUID 🟡

标题

建议标题(本次 gh 未认证,未能自动修改):
Support tiled blueprint projection, block/entity runtime restoration and material requirements 支持蓝图平铺重复投影、方块/实体运行状态恢复与材料需求补全


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/blueprint-repetition-and-runtime-entity-restore-review.md in skill 'building-rod-blueprint-review' (1 replacement). · Patched SKILL.md in skill 'building-rod-blueprint-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'building-rod-blueprint-review' (1 replacement). · Patched SKILL.md in skill 'mixin-injection-verification' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

已完成审查。以下是审查意见(gh token 已失效,未能自动改标题,建议标题附在末尾)。


代码审查摘要 — PR #4971

操作: opened(synchronize 后 head f363b43f
范围: 41 个文件(39 Java,9 新增,0 删除)/ +1705 −264 行(api 与 diff 校验一致:changed_files=41,md5 d7b18df…
目标线: dev/1.21/fix6dev/1.21/1.6(1.21.1 线,非 26.1 迁移,跳过 API 迁移健康度检查)

🔴 关键

  1. BuildingEntityTransform.sanitize() 由白名单改为全量复制 → 实体 NBT 信任边界被显著放宽

    CompoundTag safe = tag.copy();  // 旧实现:id/Pos/Rotation/CustomName + 各实体类型专属安全键
    safe.remove("UUID"); safe.remove("Passengers");

    配合新的 DynamicBuildingEntities(Mob/投射物/掉落物路径),蓝图文件里几乎全部实体 NBT 会原样落到生成实体上:Attributes(任意 max_health/attack_damage)、ActiveEffects(任意层数/时长)、HealthInvulnerableFireTagsPersistenceRequiredBrain 记忆值、以及模组自定义数据。
    这不是纯理论:扫描仪的导入链路接受客户端字节——StructureScannerFilePacketStructureScannerFiles.importBlueprint()StructureScannerFiles.parseImportResult() → 快照实体 NBT,随后 planBlueprint()find(probe, transformed) + adapter.plan(...) 把这段 NBT 当作可放置实体。改前SpawnEggAdapter 只复制 id/Pos/Rotation,所以「蓝图里塞一只 1e9 生命的生物 / 无敌盔甲架」这条路是本次新开出来的(成本仅一枚刷怪蛋或树脂封存生物)。
    建议:保留一份黑名单Attributes/ActiveEffects/Health/Invulnerable/Fire/Tags/PersistenceRequired/ForgeData/Brain.memories 的非 pos 值),或对数值做钳制;同时把 planBlueprint() 里已对「受限方块」做的 requiresOperator() 校验延伸到实体侧。

  2. BlueprintEntities.flatten() 重复 UUID 分支缺 hasUUID 判断 → NPE

    CompoundTag existing = seen.putIfAbsent(tag.getUUID("UUID"), tag);
    if (existing != null) {
        if (tag.hasUUID(VEHICLE)) existing.putUUID(VEHICLE, tag.getUUID(VEHICLE));  // 条件在 if 里,但第二行无条件取
    }

    实际写法是 if (tag.hasUUID(VEHICLE)) existing.putUUID(VEHICLE, tag.getUUID(VEHICLE))——只在 hasUUID 为真时才取值,这里是安全的;但重复 UUID 且该条没有 anvilcraft:vehicle 时,不会崩溃也不会补链,而是静默丢弃这一条的载具信息(existing 已存在则直接 return)。真正的风险在另一处:CompoundTag.getUUID() 对缺失键返回 nullNbtUtils.loadUUID 返回 null),putUUID(key, null)UUIDUtil.uuidToIntArray(null)NPE。触发条件:手工/第三方 .nbt 里同一实体既出现在顶层、又出现在别的实体的 Passengers 中且顶层那条没有 vehicle 键(导入路径允许这种输入)。该 NPE 不属于 catch (ConstructionBlueprintException | IllegalArgumentException),会从包处理里逃逸 → 客户端掉线/日志刷屏。
    建议:改为 if (tag.hasUUID(VEHICLE)) { UUID v = tag.getUUID(VEHICLE); if (v != null) existing.putUUID(VEHICLE, v); }

⚠️ 警告

  1. BlueprintTicks.read()orElseThrow()NoSuchElementException,逃逸所有 catch
    NbtUtils.readBlockPos(entry, "pos").orElseThrow()(以及 StructureScannerFiles.handleBuildingRodService.blueprintsDiskDisplaySupportBuildingRodClientcatch (…| IllegalArgumentException))——NoSuchElementException 不是 IAE,导入一份 anvilcraft:scheduled_ticks 里缺 pos 的文件就能走到。同一函数里其它错误用 IAE、StructureSnapshotCodec 又用 warnings 收集,建议统一:要么显式抛 IAE,要么记 warning 跳过该条(后者更符合 codec 现有风格,也能避免「一条坏计划刻否决整个结构」)。

  2. MagnetizedNodeBuildAdapter.support() 匹配过严且直接抛异常 → 整个结构/扫描作废
    Math.abs(pos.getY() + height - entry.pos().y) < 1.0E-5heightstate.getCollisionShape(EmptyBlockGetter.INSTANCE, pos) 计算,而节点实体的 Y 是在真实 levelMagnetUtilgetCollisionShape(level, pos))里算出来的;对形状依赖世界上下文/邻居的方块,这两者可能不等 → throw new IllegalArgumentException("Missing magnetized node support at …") 会拒绝整个蓝图/整次扫描(StructureSaveUtil.saveStructureToDisk 的自动保存 tick 仅 catch (IllegalArgumentException) 记录警告,正常路径虽被捕获,但用户拿到的是「无法保存」而不是「跳过该节点」)。
    建议:找不到支撑时降级处理(用 entry.blockPos() 或同列最近候选),不要在采集路径上抛。

  3. StructureSaveUtil.buildSnapshot() 在服务端改为实时重采集,scannedBlocks 参数与「扫描所见」失效

    if (blockEntity.getLevel() instanceof ServerLevel serverLevel) return BlueprintCapture.capture(serverLevel, blockEntity.getScanBounds());

    集成服务器也是 ServerLevel,所以旧分支(用缓存 + scanner.getDirection()/isScannerUpsideDown())实际成为死代码,第二个参数在服务端永不使用。后果:导出内容 ≠ 玩家在扫描器界面看到的预览(扫描后改动过的方块、扫描后才出现的实体都会进文件)。我按四个朝向逐一验算了坐标:worldBlockToPreview ∘ normalize(scannerFacing) 的横轴系数恒为 +1(纯平移),因此新路径固定用 NORTH/false 不会引入朝向/镜像回归——这点没问题;但「实时 vs 缓存」的产品语义请确认,若是刻意为之,建议删掉 scannedBlocks 形参并同步 UI 提示。

  4. LitematicaImporterPendingBlockTicks.Time 直接当剩余延迟使用
    (int) Math.clamp(tick.getLong("Time"), 0, Integer.MAX_VALUE)。请确认 Litematica 该字段是剩余延迟还是绝对 triggerTick;若是后者,导入的计划刻会被推到约 2^31 tick 后(静默失效,而不是报错)。建议同时传入/减去一个基准(例如本地 capturedAt)。

  5. BuildingRodUndo.SavedEntity.matches()saveAsPassenger 全量等值比较 → 实体组几乎不可能被撤销
    实体 NBT 里 Pos/Motion/Air/HurtTime/PortalCooldown/TicksFrozen,掉落物的 Age/PickupDelayCauldronOutletEntity 新增持久化的 TargetPos/WasMoving 都会自然变化;而这些实体与「磁铁节点/坩埚出口」是挂在方块组里的(groups.get(core)),一旦实体 NBT 变动,整组(含方块)都不会被撤销。建议只比较稳定子集(如 blockState/id/BlockPos/CustomName + 结构性字段),或在 recordEntity 时快照一份「受控字段」用于比较。

  6. 平铺距离校验只覆盖 anchorlast
    blueprints() 只校验两个角点 withinReach(…, 26),而最后一份副本还会向 last 之外延伸最多一个蓝图尺寸(width-1),实际可放置到 ~26 + size 格外。建议把 count 夹到「最后一份副本的远角仍在 reach 内」,或对远角追加一次 withinReach

  7. BlueprintRuntimeData.FIELDS 是跨实体类型的全局字符串名单
    "Output"/"Powered"/"progress"/"phase"/"OutputSignal"/"Rolling" 等通用键对所有方块实体生效:take() 先把它们从 source 摘走,再在 afterContents 阶段合并回写。当前是自洽的(同名键不会丢,只是延后到内容插入之后再应用),但一旦将来某个 BE 把这些名字用作「必须在前一阶段生效的设置」,就会出现难查的时序 bug。建议按实体类型(或至少按注册的方块实体类型)限定字段集,而不是全局名单。

💡 建议

  • BuildingEntityTransform.sanitize(Entity entity, CompoundTag tag)entity 形参已完全无用,可删(否则误导以为仍按类型过滤)。
  • SlidingBlockEntity.setMoveDirection(sliding.getMoveDirection()) 是"自赋值",实际靠 setter 内的 SlidingEntitySyncPacket 副作用做重同步——建议加注释或抽成 resync(),否则极易被后来者当死代码删掉。
  • BuildingCommit.set()serverLevel.onBlockStateChange(...) / state.onBlockStateChange(...) 被放在静默阶段(邻居尚未更新)调用,而 NeoForge 契约(IBlockExtension#onBlockStateChange)是「状态变更且邻居更新之后」。本仓库无覆写者,但第三方方块可能依赖该顺序,建议注释说明或移到 activate() 里。
  • planBlueprint() 每次副本都重算 BlueprintIgnition.portalCores(declared)declared 随副本增长)→ 最坏 O(copies × declared)。可把结果缓存在副本循环外、只增量处理新位置。
  • group.ignitions = 1赋值不是自增,正确性依赖「每个点火方块自成一组」(BlueprintIgnition.portalCores 让整扇传送门共组 → 1 个点火工具,是刻意设计);若将来 BlueprintMultiblocks.core() 的归组变化会漏算数量,建议写成 Math.max(1, …) 语义的显式注释。
  • BuildingMaterials.missing()reserveTool()/reserveHammer() 当纯谓词用,但它们会写 Source.retainedTool;目前只因每次 new BuildingMaterials(player) 一次性使用才安全,建议拆出无副作用的 hasTool()
  • 单份蓝图(count == 1)不校验 snapshot.nonAirBlockCount() 上限,而平铺路径校验 count*entries ≤ MAX_BLOCKS(quiet 路径也不校验 cells 总数)——两条路径的上限语义请对齐或在文档里说明。
  • VoxelShape.max(Axis.Y, 0.5, 0.5) + EmptyBlockGetter.INSTANCE 与运行时形状可能不同(同第 4 条),建议抽出公共 helper 供 MagnetUtil/MagnetizedNodeBuildAdapter/BuildingEntityTransform 共用。
  • 客户端 updateTarget() 把原来的 player.pick(...)OUTLINE)换成了 ClipContext.Block.COLLIDERitem.anvilcraft.building_rod.controls 已写成 "drag from a collidable block",看得出是刻意的;但副作用是瞄准无碰撞但有轮廓的方块(草/花/红石粉/火把等)时会直接穿过去命中后方方块,与旧行为不同,建议在 PR 描述里标注这是一次交互行为变更。

🟢 看起来不错

  • 朝向归一化等价性(我做了逐项验算)BlueprintCapture.capture() 固定 ScannerDiskNormalizer.normalize(raw, NORTH, false) 不会造成朝向回归——旧路径 worldBlockToPreview(scannerFacing)normalize(scannerFacing, upsideDown) 的复合在 SOUTH/WEST/EAST/NORTH 四个朝向下横轴系数恒为 +1(含 Y 轴翻转抵消),净效果与新的「世界坐标 → 平移」完全一致。
  • 侦测器抑制isRestoredObserverUpdate()neighborShapeChanged 首参语义核对正确(经 RedstoneWireBlock/GiantAnvilBlock/MultiblockConversionRecipe 三处现有调用确认「首参 = 从被更新方块指向变化源的方向」),guard 同时校验 level、observer FACING、declared 状态与实时状态引用,且只在 activate() 的 ThreadLocal 窗口内生效 ✅。
  • 位置/尺度计算BlueprintCaptureorigin = BlockPos.containing(min) / end = max-1getScanBounds()rangeX/Y/Z 一致;tile()floor(offset/size)+1 + count*entries ≤ MAX_BLOCKS 保证副本不重叠、总量受控,客户端与服务端共用同一实现,连红框合法色都用 blueprintPlacements().isEmpty() 对齐。
  • 计划刻BlueprintTicks 做了注册表存在性校验 + 32768 条上限 + capturedAt == 0(老文件/Litematica)回退到 tick.delay()restore 前用 clearArea 清空再按 order 排序恢复、并以 placed 集合过滤,语义完整。
  • 撤销映射entityGroupsIdentityHashMap(正确地规避了 record 值相等导致的合并),且撤销前要求「实体仍存活 + NBT 未变 + 可修改」。
  • 导出重名:循环内每轮都重算 target 并保留 resolve() 路径穿越校验,name 已被 StructureFileTransfer.isSafeName 二次校验;exported 名称回传链路(StructureBlueprintFiles.writeStructureScannerFiles.handleFileResultPacketBlueprintClientFiles.receive)的 total/offset/isSafeName/.nbt 校验与服务端 sendFile 的语义一致。
  • 中英 en_us/en_udBuildingRodLang 三处同步,%s 占位符个数一致,en_ud 行序反转正确。

📋 声称验证表

声称 状态 对应文件
拖拽平铺投影 / 松开锁定 / 再右键批量粘贴 BlueprintPlacement.tile, BuildingRodClientblueprintOffset/press/release/confirm/PLACE_BLUEPRINTS), BuildingRodPacket
统一预览与放置范围限制 ✅(⚠️ 第 8 条:距离只校验两角) BlueprintPlacement.tile ← 客户端/服务端共用, BuildingRodRenderer
选区距离锁定 / 方向调整 / 尺寸显示 BuildingRodClient.selectionBounds, applyControl(4 方向), handleKeyboardInput, selection_size
统一结构扫描快照采集 ✅(⚠️ 第 5 条:实时重采集语义 + 死形参) StructureSaveUtil.buildSnapshotBlueprintCapture
保存/恢复运行进度、计划刻、时间基准 ✅(⚠️ 第 3、9 条) BlueprintRuntimeData, BlueprintTicks, StructureSnapshot(+Codec), BlueprintBlockConfiguration
Litematica 计划刻导入 ⚠️(第 6 条:Time 语义待确认) LitematicaImporter
建造后红石/流体/邻居更新,避免误触侦测器 BuildingCommit.activate/activatePlaced/isRestoredObserverUpdate, BlueprintLevelMixin
生物/掉落物/投射物/移动方块/模组实体建造 ✅(🔴 第 1 条信任边界) DynamicBuildingEntities, BlueprintEntities, MagnetizedNodeBuildAdapter, EntityBuildAdapters, BuildingEntityTransform
恢复运动状态/乘骑/实体引用 BuildingEntityTransform(Motion 旋转/SlidingBlocks/Relative*), BlueprintEntities.link
树脂封存生物供料,补全装备/库存/流体/特殊方块材料 BuildingMaterials.reserveCreature, DynamicBuildingEntities(contents), VanillaBuildingEntities.VehicleAdapter(fluids), BlueprintSpecialBlocks
磁铁/铁砧锤/点火工具需求校验 + 显示不支持对象 EntityBuildAdapter.requiredTool/requiresHammer, BuildingMaterials.reserveTool/reserveHammer/reserveIgnitions, unsupported_type
实体纳入撤销(仅状态未变且可修改的组) ✅(⚠️ 第 7 条:比较过严) BuildingRodUndo
导出同名自动编号 + 反馈实际文件名 StructureBlueprintFiles.write, BlueprintClientFiles.receive

结论: REQUEST_CHANGES(建议先处理 🔴 第 1、2 条,⚠️ 第 3、4 条建议同批修) — 功能映射完整、坐标与状态机语义经得起推敲、客户端/服务端约束对齐得不错;主要顾虑集中在两处:实体 NBT 白名单被移除后的信任边界(配合客户端可提交原始 NBT 的导入链路),以及「解析/采集失败」路径上混用异常类型导致的部分逃逸与整体作废。

🧪 测试建议

被测目标 推荐场景 优先级
BlueprintPlacement.tile() offset 恰好 = / 略小于 / 为 0 / 为负;count*entries 刚好越界;MAX_BLOCKS 边界 🔴
BuildingRodService.blueprints() + planBlueprint() 平铺多份时的 declared/portalCores 归属;第二份失败时不得留下半成品 🔴
BlueprintTicks.read/restore pos/未知 type/>32768 条 → 期望可捕获的 IAE 而非 NSEE;capturedAt=0 回退分支;RESTORE 时 placed 过滤 🔴
BuildingEntityTransform.sanitize() 恶意 NBT(1e9 Attributes/ActiveEffects/Invulnerable)不得生效;Passengers/UUID 必须被剥离 🔴
BuildingRodUndo 实体 NBT 自然漂移(Age/Motion)后撤销行为;节点/出口实体与方块同组时的跳过语义 🟡
BuildingMaterials 工具/生物/点火预留 铁砧锤被 retainedTool 占用后不能当材料消耗;树脂封存生物匹配;火焰计数(多扇传送门/多处火) 🟡
BuildingCommit.activate() 侦测器朝向不被邻居更新改写、且恢复后仍能被真实邻居触发;计划刻/流体刻数量与蓝图一致 🟡
StructureSaveUtil.buildSnapshot() / ScannerDiskNormalizer 扫描区域被改动后导出内容;magnetized_node 支撑缺失时不得整体失败 🟡
StructureBlueprintFiles.write() 目录内已存在 a.nbta_1.nbt、…;名称长度接近 128 的截断 🟢

建议标题gh auth status 显示 token 已失效,故未自动修改):
Support blueprint tiling, runtime-tick restoration and entity building 支持蓝图平铺投影、运行状态/计划刻恢复与实体建造


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'building-rod-blueprint-review' (1 replacement). · Patched references/shell-only-diff-navigation.md in skill 'github-webhook-pr-review' (1 replacement). · Patched references/pr-class-routing.md in skill 'github-webhook-pr-review' (1 replacement).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants