Skip to content

Fixed the issue where the smart block placer couldn't obtain blueprint previews in a server environment. 修复了26.1服务器环境下智能方块放置器获取不到蓝图预览的问题#3809

Merged
Gu-ZT merged 6 commits into
Anvil-Dev:dev/26.1/1.6from
PigeonNian:placerfix/26.1/1.6
Jun 22, 2026

Conversation

@PigeonNian

@PigeonNian PigeonNian commented Jun 22, 2026

Copy link
Copy Markdown
Contributor
  • 调整物品栏加载顺序,先加载磁盘物品确保结构加载不被误判清空
  • 在 loadFromTag 中重置无效结构标志,避免错误状态残留
  • 在载入NBT时同步结构缓存数据,支持客户端蓝图预览功能
  • 处理缓存结构UUID的解析,提升结构识别准确性
  • 保证hasStructureDisk和hasInvalidStructure状态正确设置
  • 修正了放置器蓝图模式下的预览
  • 添加了倒挂模型
  • 使磁盘预览兼容服务器环境

- 调整物品栏加载顺序,先加载磁盘物品确保结构加载不被误判清空
- 在 loadFromTag 中重置无效结构标志,避免错误状态残留
- 在载入NBT时同步结构缓存数据,支持客户端蓝图预览功能
- 处理缓存结构UUID的解析,提升结构识别准确性
- 保证hasStructureDisk和hasInvalidStructure状态正确设置
@PigeonNian PigeonNian changed the title Fixed the issue where the smart block placer couldn't obtain blueprint previews in a server environment. 修复了服务器环境下智能方块放置器获取不到蓝图预览的问题 Fixed the issue where the smart block placer couldn't obtain blueprint previews in a server environment. 修复了26.1服务器环境下智能方块放置器获取不到蓝图预览的问题 Jun 22, 2026
@anvil-craft

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

rm -rf /tmp/anvilcraft /tmp/pr3809.diff

Reason: delete in root path

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

@anvil-craft

Copy link
Copy Markdown

现在我有足够的上下文来完成审查。以下是我的分析:


PR #3809 代码审查

仓库: Anvil-Dev/AnvilCraft
文件: SmartBlockPlacerBlockEntity.java(49 行 diff,1 个文件)
结论: 评论(无阻塞性问题,2 条建议)


✅ 看起来不错

1. 物品栏加载顺序调整loadAdditional 方法)

-        this.loadFromTag(tag);
-        loadItemsFromTag(tag, this.diskInventory);
+        loadItemsFromTag(tag, this.diskInventory);
          this.lastDiskItem = this.diskInventory.getItem(0).copy();
          loadItemsFromTag(tag, this.bookInventory);
          loadItemsFromTag(tag, this.outputBookInventory);
+        this.loadFromTag(tag);

逻辑正确。原来 loadFromTag 先执行,其中的 tryLoadStructure 因磁盘物品尚未加载而将 cachedStructure 误判清空。调换顺序后,磁盘物品先就位,tryLoadStructure 能正确识别结构数据,这是干净的解耦。

2. 无效结构标志重置loadFromTag 方法)

this.hasStructureDisk = true;
+this.hasInvalidStructure = false;

在成功加载结构后重置错误状态,防止先前失败的状态残留误锁后续操作。与原有的 hasStructureDisk = true 对称,逻辑完整。

3. 客户端蓝图预览同步applySyncDataFromMenu 方法)
新增代码块从同步数据包中解析 cachedStructurecachedStructureNamecachedStructureUuid,并设置状态标志。这一步解决了"服务器环境下客户端拿不到蓝图预览"的核心问题。使用了与 loadFromTag 中一致的 UUIDUtil.CODEC.parse + orElse(null) 模式。

4. 无遗留调试语句、无密钥泄露、无合并冲突标记。
5. 无 \ No newline at end of file 问题。


💡 建议

建议 1(中等)— loadStructureData 调用缺少异常保护

this.loadedStructure = this.loadStructureData(tag.getCompoundOrEmpty("cachedStructure"));

loadStructureData 内部使用 StructureDiskData.CODEC.parse(...).getOrThrow() 如果没有 try-catch,当 cachedStructure 数据损坏或格式不匹配时会抛出未捕获异常。虽然 applySyncDataFromMenu 的数据来源于服务端自身(菜单同步包),但数据损坏场景下会导致客户端崩溃。建议:

try {
    this.loadedStructure = this.loadStructureData(tag.getCompoundOrEmpty("cachedStructure"));
    this.loadedStructureName = tag.getStringOr("cachedStructureName", "");
    // ... UUID 解析 ...
} catch (Exception e) {
    LOGGER.warn("Failed to load cached structure from sync data", e);
    this.loadedStructure = null;
    this.loadedStructureName = "";
    this.loadedStructureUuid = null;
}

建议 2(低)— loadedStructureUuid 的 null 值下游使用

this.loadedStructureUuid = UUIDUtil.CODEC.parse(
    NbtOps.INSTANCE, tag.getCompoundOrEmpty("cachedStructureUuid")
).result().orElse(null);

orElse(null) 意味着后续使用 loadedStructureUuid 的代码都需要做 null 检查。这是文件中已有的模式(loadFromTag 中有相同的写法),但建议确保所有读取 loadedStructureUuid 的地方都有 null 安全保护,避免 NPE。


📝 总结

PR 的 5 个变更目标全部正确实现,逻辑清晰、改动精准。修复了服务器环境下蓝图预览失效的问题,是高质量的热修复。仅有的建议涉及异常鲁棒性,非阻塞性问题。

@PigeonNian PigeonNian marked this pull request as draft June 22, 2026 10:35
@anvil-craft

Copy link
Copy Markdown

我现在已经掌握了完整的变更信息。以下是审查报告。


🔍 PR #3809 代码审查摘要

状态:评论(小的注意事项,不阻塞合并)

文件: SmartBlockPlacerBlockEntity.java — 1 个文件,+19 / -3 行


🟢 主要变更(正确)

1. 物品栏加载顺序调整loadAdditional(CompoundTag, HolderLookup.Provider)

之前:loadFromTag(tag) → loadItemsFromTag(...)
之后:loadItemsFromTag(...) → loadFromTag(tag)

✅ 这是此 PR 的核心修复。loadFromTag 内部调用 tryLoadStructure,它需要 diskInventory 中的物品来检查结构磁盘。原顺序下物品栏尚未加载,导致 tryLoadStructure 误判清空 cachedStructure。新顺序确保物品先可用,修复了服务端蓝图预览不可见的问题。

2. 重置失效结构标志loadFromTag 内)

this.hasStructureDisk = true;
this.hasInvalidStructure = false;  // 新增

✅ 当找到有效的结构磁盘时,显式清除失效标志。逻辑正确,防止状态残留。

3. 同步包中结构缓存数据加载applySyncDataFromMenu 内)

✅ 从菜单同步包中读取 cachedStructurecachedStructureNamecachedStructureUuid,用于客户端蓝图预览。同时设置 hasStructureDisk = truehasInvalidStructure = false,与服务端加载路径的状态保持一致。


⚠️ 注意事项(建议验证)

UUIDUtil.CODEC.parse 的 NBT 格式兼容性

UUIDUtil.CODEC.parse(
    NbtOps.INSTANCE, tag.getCompoundOrEmpty("cachedStructureUuid")
)

UUIDUtil.CODEC 基于 Codec.INT_STREAM,在 NBT 中序列化为 IntArrayTag[I;...]),但这里用 tag.getCompoundOrEmpty(...) 获取的是一个 CompoundTag。如果发送方将 UUID 也存为 CompoundTag,两者格式可能不匹配,导致 parse() 返回空。

建议: 请确认发送侧(menu sync 包的构建代码)写入 cachedStructureUuid 时使用的 NBT 类型。如果发送方存的是 IntArrayTag,这里应使用 tag.getIntArray("cachedStructureUuid") 或保留 tag.get("cachedStructureUuid") 作为通用 Tag 传入。如果发送方确实存了一个包含内部结构的 CompoundTag(如 {M: [I;...]}),那当前写法正确,但建议加注释说明格式约定。

loadAdditional(ValueInput input) 是否也需要调整?

上下文显示 loadAdditional(ValueInput input) 方法体很短(可能为空或委托传递)。请确认该方法是否最终也调用了 loadAdditional(CompoundTag, HolderLookup.Provider),以确保修复在所有加载路径上都生效。


✅ 看起来不错

  • 没有遗留的调试语句、TODO 或 FIXME
  • 文件末尾换行符正确,无 POSIX 问题
  • 没有新增 import,类型引用一致
  • 评论用中文描述了修复原因,便于团队理解
  • 变更范围小(单文件 22 行净增),定位精准

总结: 三个变更点逻辑连贯,共同修复了服务端环境下智能方块放置器蓝图预览的问题。物品栏加载顺序调整是根本修复,结构缓存同步是客户端预览的必要补充。建议确认 UUIDUtil.CODEC 的 NBT 格式匹配后即可合并。

@anvil-craft

Copy link
Copy Markdown

代码审查摘要 — PR #3809

仓库: Anvil-Dev/AnvilCraft
PR: Fixed the issue where the smart block placer couldn't obtain blueprint previews in a server environment
文件变更: 1 个文件,+18 / -2 行
结论: 评论(Approved,无阻塞性问题,仅建议)


🔍 变更总览

# 变更位置 影响
1 loadAdditional(CompoundTag, HolderLookup.Provider) → 加载顺序重排 修复服务端结构预览丢失的根因
2 loadFromTag() → 新增状态重置 防止 hasInvalidStructure 残留
3 applySyncDataFromMenu() → 同步缓存结构 支持客户端菜单中的蓝图预览

✅ Change 1:加载顺序修复(核心改动)

位置: loadAdditional(CompoundTag, HolderLookup.Provider) (原 §320-323)

旧逻辑:

loadFromTag(tag)        // 加载 cachedStructure → loadedStructure 被设置
// ... (loadFromTag 内部调用 tryLoadStructure())
// tryLoadStructure() 读取 diskInventory.getItem(0) → 空的!→ 判定无磁盘
// → loadedStructure = null, loadedStructureName = ""
loadItemsFromTag(...)   // 此时才真正加载物品栏 → 但为时已晚

新逻辑:

loadItemsFromTag(...)   // 先加载物品栏
this.lastDiskItem = ... // 缓存当前磁盘
loadFromTag(tag)        // loadFromTag 中加载 cachedStructure,
                        // 然后 tryLoadStructure 读取 diskInventory → 能找到磁盘!
                        // → 状态一致,不会误判清空

👍 正确且解释清晰。 这是服务端环境下面板预览丢失的根因——tryLoadStructure() 在 §548 行有状态跳跃检测:

if (nowHasDisk == this.hasStructureDisk && currentUuid != null
    && currentUuid.equals(this.loadedStructureUuid)) return;

nowHasDiskfalse(物品栏未加载)但 hasStructureDisktrue(刚被 cachedStructure 设为 false→true),条件不满足 → 进入清理路径,清空 loadedStructure


✅ Change 2:重置无效结构标志

位置: loadFromTag() (原 §358)

this.hasStructureDisk = true;
this.hasInvalidStructure = false;  // ← 新增

👍 合理的保险措施。如果 BE 在保存时 hasInvalidStructure = true(例如之前的磁盘加载失败),重新加载 NBT 时若 cachedStructure 有效,该标记会错误地保持 true,导致客户端认为结构无效而拒绝预览。


✅ Change 3:菜单同步数据中的结构缓存

位置: applySyncDataFromMenu() (新增 §3895-3907)

if (tag.contains("cachedStructure")) {
    this.loadedStructure = this.loadStructureData(...);
    this.loadedStructureName = ...;
    this.loadedStructureUuid = ...;
    this.hasStructureDisk = true;
    this.hasInvalidStructure = false;
}

👍 逻辑与 loadFromTag() 中已有的缓存结构加载路径(§351-357)完全一致——保持了代码一致性,复用同一个 loadStructureData 方法。

这个新增块是客户端预览的核心:当菜单打开(客户端侧),服务端发送的 sync data 中包含 cachedStructure,客户端加载后即可在 GUI 中渲染蓝图预览。


💡 建议

  1. getOrThrow() 的潜在 NPE 风险(非本 PR 引入,但可趁机修复)

    loadStructureData(CompoundTag tag) (约 §473):

    StructureDiskData.CODEC.parse(NbtOps.INSTANCE, tag.get("DiskData")).getOrThrow();

    如果 cachedStructure tag 中存在但缺失 DiskData 子 key,tag.get("DiskData") 返回 nullgetOrThrow() 会抛异常。新旧两条路径(§352 和 §3898)都存在此风险。建议用 .result().orElse(null) + null 检查替换 getOrThrow(),或至少用 try-catch 包裹。

    严重性: 低(生产中极少出现 cachedStructure 有值但缺 DiskData 的情况;如果真的发生,只会导致这一帧预览失败,不会崩服)

  2. applySyncDataFromMenu 不做清理

    当菜单 sync data 不包含 cachedStructure 时(例如网络包截断或版本不匹配),loadedStructure 不会被动清除。不过因为是客户端菜单 UI 层,影响范围仅限当前帧的渲染,属于边缘情况。


✅ 看起来不错

  • 没有遗留调试代码、TODOs 或密钥泄露
  • 没有 Objects.requireNonNull() 对可能为 null 的返回值使用
  • 没有合并冲突标记或 EOF 换行符问题
  • 更改高度聚焦,3 处修改目标明确,都在修复同一个问题链
  • 注释详细且准确描述了变更意图(中文)
  • tryLoadStructure 内部已有完善的 structureChanged 检测(§548),修改后不会导致不必要的 onChanged() 同步

📋 总结

类别 状态
正确性 ✅ 修复逻辑正确,三条修改形成完整的问题链闭环
安全性 ✅ 无安全问题
代码质量 ✅ 局部且可维护,与原代码风格一致
测试 ⚠️ 无测试覆盖(BE 状态同步难以单元测试,可接受)
性能 ✅ 仅有 3 次额外赋值 + 1 次条件判断,无影响
回归风险 ✅ 极低。改动不影响 ValueInput 路径(§310-320),仅影响 CompoundTag 路径

建议操作: 批准合并。

- 新增客户端向服务端请求结构预览数据的网络包 StructurePreviewRequestPacket
- 服务端读取结构文件,提取调色板和方块列表数据返回给客户端
- 客户端通过 StructurePreviewResponsePacket 接收并缓存原始NBT数据
- 在预览渲染时解析NBT数据为结构信息,构建并缓存LevelLike对象
- 实现本地文件加载及缓存淘汰策略,防止内存泄漏
- 优化缓存管理,避免重复请求,支持请求超时重试机制
- 增加路径和文件名校验功能,防止路径穿越攻击
@anvil-craft

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

# 检查常见问题
echo "=== Debug/TODO/FIXME/HACK ==="
grep -n "TODO\|FIXME\|HACK\|XXX\|debugger" /tmp/pr3809.diff | grep "^+" | grep -v "^+++" || echo "  无"

echo ""
echo "=== 密钥/凭据模式 ==="
grep -in "password\...

Reason: Security scan — [MEDIUM] Variation selector characters detected: Content contains Unicode variation selectors (VS1-256). These are commonly used in emoji sequences but may indicate steganographic encoding or obfuscation

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

@anvil-craft

Copy link
Copy Markdown

以下是 PR #3809 的完整代码审查。


代码审查摘要

结论:评论(3 条建议,无阻塞性问题)

概述

此 PR 修复了 26.1 服务器环境下智能方块放置器无法获取蓝图预览的核心问题——客户端无法直接访问服务端文件系统上的结构文件。架构上引入了 C2S 请求 → S2C 响应 的网络通信模式,配合三级缓存策略实现蓝图预览。

文件变更统计

文件 变更行 +行 / -行
SmartBlockPlacerBlockEntity.java 50 行 +18 / -2
StructureDiskPreviewSupport.java 330 行 +150 / -75
StructurePreviewRequestPacket.java 72 行 +65 / -0 (新文件)
StructurePreviewResponsePacket.java 50 行 +43 / -0 (新文件)
StructureLoadUtil.java 138 行 +58 / -14
StructureSaveUtil.java 38 行 +6 / -6

🟠 警告

1. @SuppressWarnings("all") 宽泛压制

  • 文件StructureDiskPreviewSupport.java:38
  • 问题:类级别使用 @SuppressWarnings("all") 会压制所有编译器警告(包括 unchecked 转换、rawtypes 等可能有用的提示),后续改动引入的类型安全问题可能被忽略。
  • 建议:缩小到具体需要压制的警告,如 @SuppressWarnings({"unchecked", "rawtypes"})

2. 服务端 loadPreviewData 对大结构可能发送大包

  • 文件StructureLoadUtil.java:loadPreviewData()
  • 问题loadPreviewData 从 NBT 文件中提取完整的 palette(调色板)和 blocks(方块列表)列表后原样发送。如果一个结构体包含数万个方块,这个 StructurePreviewResponsePacket 可能非常大(几十 KB 甚至上百 KB),可能触发 Minecraft 网络包大小限制或造成客户端卡顿。
  • 建议:在 loadPreviewData 中添加方块数量上限(如最多 4096 个方块),超限时返回截断数据或降级提示,而不是暴力全量发送。

3. 新文件缺少服务端 loadPreviewData 调用处的 NPE 防御

  • 文件StructurePreviewRequestPacket.java:54
  • 问题StructureLoadUtil.loadPreviewData() 在文件不存在或格式错误时返回 null;但 handleOnServer 中仅返回而不做进一步处理(正确)。不过,若 previewData 不为 null 但内部 palette/blocks 为空列表时,客户端 parsePreviewNbt 仅返回 null,不会触发重试——这意味着服务端已确认结构存在但数据为空,客户端不会再请求,用户看到空预览。
  • 建议:在服务端额外校验 previewData 是否非空(paletteblocks 都不为空),若空则返回一个带提示的空预览包,避免客户端永久等待。

💡 建议

1. 加载顺序重构 —— 正确但要注意 lastDiskItem 状态同步

  • 文件SmartBlockPlacerBlockEntity.java:loadAdditional(CompoundTag, HolderLookup.Provider)
  • 分析:将 loadItemsFromTag(diskInventory) 移到 loadFromTag() 之前,确保 tryLoadStructure 访问磁盘物品时缓存已就位。这是正确的修复方向。
  • 注意this.lastDiskItem = this.diskInventory.getItem(0).copy() 现在在 loadFromTag 之前执行。如果 loadFromTag 内部修改了磁盘物品(比如清空或替换),lastDiskItem 将过时。不过从 diff 看 loadFromTag 不修改物品栏,只读取它,因此无实际风险。

2. parsePreviewNbtposTag.getInt().orElse(0) 可简化

  • 文件StructureDiskPreviewSupport.java:parsePreviewNbt()
  • 分析listTag.getInt(0).orElse(0) 语法正确但略显冗余。由于上方已检查 posTag.size() < 3continueposTag.getInt(0) 必然有值,可以直接用 posTag.getInt(0)posTag.getInt(0).orElseThrow() 更明确表达"不可能为 null"的语义。

3. 缓存键从 String 改为 UUID 时未兼容旧版缓存

  • 文件StructureDiskPreviewSupport.java
  • 分析PREVIEW_CACHE 的键从 String(原 cacheKey 支持 uuid.toString()"hash_" + hashCode 两种回退)改为 UUID。这意味着旧版缓存在此更新后全部失效(第一次进入时需重新请求/加载),影响可忽略(仅首次),无需改动。

✅ 看起来不错

  • 核心架构设计合理:C2S 请求 → S2C 响应的网络模式是解决服务端结构预览问题的正确方案。客户端通过网络获取 palette + blocks,本地构造 LevelLike 渲染 3D 预览,不暴露服务端文件系统路径。
  • 请求去重机制PENDING_REQUESTS + REQUEST_TIMESTAMPS + 30s 超时,有效防止同一 UUID 重复请求导致的网络洪泛。
  • LRU 淘汰机制evictIfNeeded() 在缓存超限时按创建时间淘汰最旧条目,防止内存泄漏。
  • hasInvalidStructure = false 重置SmartBlockPlacerBlockEntity.loadFromTag() 中在成功解析结构后重置无效状态,防止脏标志残留。
  • 负逻辑重构isValidStructureFile → isInvalidStructureFileisPathWithinBaseDirectory → isPathOutsideBaseDirectory 让调用处的语义更直观(if (isInvalid) return nullif (!isValid) return null 更易读)。
  • BedBlock/DoorBlock switch 模式变量清理:将未使用的命名模式变量 bedBlock/doorBlock 改为 _,兼容 Java 21+ 语法,消除编译器警告。
  • 无遗留调试代码:未发现 TODOFIXMEHACK、合并冲突标记或密钥泄露。
  • 文件末尾换行符规范:所有变更文件末尾均有换行符,无 POSIX 兼容性问题。

@PigeonNian PigeonNian marked this pull request as ready for review June 22, 2026 11:28
@anvil-craft

Copy link
Copy Markdown

所有检查完成。以下是完整的审查意见:


代码审查摘要

结论:建议修改(2 个警告,2 条建议)


🟢 主要变更概述

该 PR 通过引入客户端-服务端网络预览协议修复了 26.1 服务器环境下智能方块放置器无法获取蓝图预览的问题:

文件 行数 变更
SmartBlockPlacerBlockEntity.java +19/-3 修复加载顺序、重置无效结构标识、同步缓存结构数据
StructureDiskPreviewSupport.java +151/-76 缓存策略重写 + 网络预览加载
StructurePreviewRequestPacket.java +66/-1 (新文件) C2S 预览请求包
StructurePreviewResponsePacket.java +44/-1 (新文件) S2C 预览响应包
StructureLoadUtil.java +59/-15 新增 loadPreviewData() + 校验方法反转重构
StructureSaveUtil.java +7/-7 同步校验方法反转重构

⚠️ 警告

1. @SuppressWarnings("all") 范围过大

文件: StructureDiskPreviewSupport.java:16

@SuppressWarnings("all")
public class StructureDiskPreviewSupport {

整个类使用了最宽泛的抑制注解,会隐藏所有编译器警告,包括:

  • 弃用 API 使用警告
  • 未使用的导入或字段警告
  • 未经检查的类型转换警告

建议: 将其缩小到实际触发警告的特定项,例如 @SuppressWarnings("unused")@SuppressWarnings("rawtypes")


2. StructurePreviewRequestPacket 无速率限制 / 权限检查

文件: StructurePreviewRequestPacket.java:49-63

public void handleOnServer(Player player) {
    ServerPlayer serverPlayer = Util.cast(player);
    ServerLevel serverLevel = serverPlayer.level();
    if (this.structureFile.isEmpty()) return;
    CompoundTag previewData = StructureLoadUtil.loadPreviewData(serverLevel, this.structureFile);
    // ... send response
}

虽然路径遍历防护已做(isInvalidStructureFile + isPathOutsideBaseDirectory),但每次请求都会触发一次 NbtIo.readCompressed 磁盘 I/O。恶意客户端可以高频发送请求导致服务端 IO 压力。

建议:handleOnServer 入口处加入简易速率限制(如 Player.getCooldowns() 配合冷却标记),或对同一 UUID 的请求做去重缓存。


💡 建议

3. loadPreviewData 中空列表的检测可更精确

文件: StructureLoadUtil.java:561

return previewTag.isEmpty() ? null : previewTag;

CompoundTag.isEmpty()paletteblocks 均为 ListTag 但为空列表(size=0)时返回 false,因为 key 是存在的。这种情况下返回的 tag 在后续 parsePreviewNbt 中会被 paletteTag.isEmpty() 拦截返回 null,但这次无效的网络往返本可以避免。

建议: 改为检查实际列表大小:

ListTag palette = fullTag.getListOrEmpty("palette");
ListTag blocks = fullTag.getListOrEmpty("blocks");
if (palette.isEmpty() || blocks.isEmpty()) return null;
// 然后 put

4. isInvalidStructureFile 语义陷阱

文件: StructureLoadUtil.java:569-583

方法名从 isValidStructureFile(正向语义)改为 isInvalidStructureFile(反向语义),访问级别从 private 改为 public。调用点和 StructureSaveUtil 中的对应方法已全部更新 ✅,但将其暴露为 public 后,未来其他维护者可能误以为存在同名的 isValidStructureFile() 方法,或在调用时混淆布尔值的含义。

建议: 可以考虑保留正向语义方法名作为委托,或为方法添加更清晰的 Javadoc 注释。


✅ 看起来不错

  • 加载顺序修复(BlockEntity.java:326-331):先加载物品栏再 loadFromTag(),从根本解决了 tryLoadStructure 因找不到磁盘物品而误判清空缓存的问题。这是正确的修复方向。
  • hasInvalidStructure = false 重置(BlockEntity.java:355):在 loadFromTag 中确定结构有效时重置无效标识,避免错误状态残留。很好的边界情况处理。
  • 三级缓存降级策略(PreviewSupport.java:213-271):完整缓存 → Pending NBT → 本地文件 → 网络请求。层次清晰,降级合理。
  • UUID 去重 + 30s 超时(PreviewSupport.java:275-283):防止重复请求的并发问题。
  • 路径穿越防护重构(LoadUtil.java ➕ SaveUtil.java):isPathWithinBaseDirectoryisPathOutsideBaseDirectory 的反转逻辑在所有调用点保持一致 ✅
  • 预览数据裁剪loadPreviewData):只提取 palette + blocks,不包含实体/TE/NBT 数据,减少了网络传输量且避免了序列化问题。

总体评价

这是一次结构清晰、理由充分的功能修复。核心改动(加载顺序调整 + 网络预览协议)精确定位了服务器环境下的根因。缓存策略从定时过期改为了会话级永不过期 + LRU 淘汰,更适合预览场景。网络协议最小化地只传输调色板和方块列表,减少了带宽压力。

建议采纳,同时处理上述 2 个警告(@SuppressWarnings 范围、速率限制)后再合并。

- 删除了 StructureDiskPreviewSupport 中的@SuppressWarnings注解及多余空检查
- 结构数据接收逻辑中移除空检查,避免重复判断
- StructurePreviewRequestPacket 中校验 previewData 的 palette 和 blocks 非空,避免解析失败重试
- StructureLoadUtil 中添加最大预览方块限制(4096),超出时截断并打印警告日志
- 优化结构文件加载时 palette 和 blocks 的复制逻辑及异常处理
@anvil-craft

Copy link
Copy Markdown

好的,我已经完整分析了 PR #3809 的所有 6 个文件的变更。以下是审查。


代码审查摘要

结论:评论(1 个警告,3 条建议,无阻塞性问题)

🟢 主要改动概述

该 PR 修复了智能方块放置器在服务器环境下无法获取蓝图预览的问题。核心思路是引入 C2S/S2C 网络协议,让客户端在无法直接读取服务端结构文件时,通过请求-响应机制获取预览数据。涉及 6 个文件,±386 行净变更。

文件 变更 角色
SmartBlockPlacerBlockEntity.java +18/-2 调整加载顺序 + 同步菜单数据
StructureDiskPreviewSupport.java +147/-75 缓存策略重写(网络感知 + 请求去重)
StructurePreviewRequestPacket.java +77/-0 C2S 预览请求包
StructurePreviewResponsePacket.java +43/-0 S2C 预览响应包
StructureLoadUtil.java +78/-14 新增 loadPreviewData + 否定重命名
StructureSaveUtil.java +6/-6 否定重命名对齐

⚠️ 警告

1. NbtAccounter.unlimitedHeap() — 潜在 OOM 风险

文件: StructureLoadUtil.javaloadPreviewData() 方法

CompoundTag fullTag = NbtIo.readCompressed(structureFile, NbtAccounter.unlimitedHeap());

使用 unlimitedHeap() 绕过了 NeoForge 的 NBT 大小限制。如果结构文件被篡改(包含超大 NBT 数据),可能导致服务端内存溢出。虽然文件名已经过 VALID_STRUCTURE_FILE 正则校验,但文件内容未经大小约束。

建议: 改用有上限的 accounter,例如 NbtAccounter.create(512L * 1024 * 1024)(512MB 上限),或在读取后验证 fullTag 的预估内存大小。


💡 建议

2. 线程安全性:预览缓存存在潜在竞态条件

文件: StructureDiskPreviewSupport.java

PREVIEW_CACHE, PENDING_PREVIEW_DATA, PENDING_REQUESTS, REQUEST_TIMESTAMPS 这四个 static 集合被以下两个路径访问:

  • 网络线程(或主线程的包处理队列)→ receiveStructureData() 写入 PENDING_PREVIEW_DATA
  • 渲染线程getOrCreateCache() 读取 PENDING_PREVIEW_DATA

在 NeoForge 中,客户端包处理通常在 Netty worker 线程上反序列化,然后回调传回主线程执行。如果 receiveStructureDatagetOrCreateCache 都在主线程执行(前者通过包队列,后者通过 tooltip 渲染),则没有竞态问题。但如果包反序列化直接在 Netty 线程上写入了 PENDING_PREVIEW_DATA,而此时渲染线程正在迭代 getOrCreateCache,可能导致 ConcurrentModificationException 或可见性问题。

建议: 在访问这些 static 集合时加 synchronized 块,或者使用 ConcurrentHashMap + ConcurrentHashMap.newKeySet(). 如果确认两者都在主线程执行,添加注释说明线程安全假设。


3. hasInvalidStructure 复位可能过于宽泛

文件: SmartBlockPlacerBlockEntity.java:359

this.hasStructureDisk = true;
this.hasInvalidStructure = false;

这两行在 loadFromTag 中无条件设置(不在 if (uuid != null) 块内)。这意味着即使磁盘 UUID 解析失败或结构文件加载失败,hasStructureDisk 仍为 truehasInvalidStructurefalse。虽然这修复了服务端环境下状态残留导致预览不可用的问题,但也掩盖了真正的加载错误。

建议: 考虑将 hasInvalidStructure = false 放在更精确的位置——仅在结构成功加载后复位。如果不是阻塞性问题,建议添加注释说明"即使加载失败也标记为有磁盘,以允许客户端重试"的设计意图。


4. 否定重命名(isValidStructureFileisInvalidStructureFile)的兼容性

文件: StructureLoadUtil.java, StructureSaveUtil.java

方法名从"正向判断"改为"负向判断"是合理的重构(减少调用处 ! 取反),使 if (isInvalidStructureFile(...)) { error; } 更自然。但需要注意:

  • 这两个类之外的任何第三方调用者(如果有)会编译失败
  • isInvalidStructureFile 的访问修饰符从 private 改为 public,但 loadPreviewData 在同一类中,无需改为 public. 检查是否确实有外部调用。

经检查,isInvalidStructureFile 改为 public 可能是为了其他包的使用预留。建议确认是否有外部调用,否则保持 private 更安全。


✅ 看起来不错

  • 网络协议设计清晰: C2S 请求 + S2C 响应模式,服务端做完整校验(空文件、空调色板/方块列表),日志警告覆盖了所有失败路径。
  • 客户端缓存策略合理: 三层缓存(完整缓存 → 待处理 NBT → 本地文件回退),会话级永不过期避免了旧方案 5 秒过期的 flicker 问题。请求去重 + 30 秒超时防止了短时间内重复请求风暴。
  • 路径穿越防护正确: 两次调用 isPathOutsideBaseDirectoryloadFromDiskloadPreviewData)确保了安全一致性。
  • parsePreviewNbt 的防御性编程:blocksTag 中每个方块的 pos/state 做了边界检查(posTag.size() < 3continuestateIndex >= 0 && < palette.size()),没有数组越界风险。
  • applySyncDataFromMenu 中的结构同步: 通过菜单同步数据包传递 cachedStructure, cachedStructureName, cachedStructureUuid,实现客户端实时预览更新,方案巧妙。
  • 文件末尾换行符: 所有文件均正常以换行符结尾 ✅
  • 无遗留调试语句或硬编码凭证

总体评价: 这是一个高质量的服务器兼容性修复 PR。架构设计合理——通过网络包传递结构预览数据而非尝试在服务端进行本地文件读取,是多人在线环境下的正确做法。unlimitedHeap() 是唯一让我犹豫的地方,建议修复后合并。

- 调整NbtAccounter内存限制为128MB,避免无限堆内存消耗
- 改进结构文件读取的稳定性和性能
- 防止因内存无限制占用导致的潜在崩溃风险
@anvil-craft

Copy link
Copy Markdown

现在我已掌握所有更改的全貌。以下是审查结果:


代码审查摘要 — PR #3809

结论:评论(无阻塞性问题,有一些建议)

范围: 6 个文件,+369/−97 行。修复 26.1 服务端环境下智能方块放置器的蓝图预览缺失问题。


🔵 主要逻辑分析

1. SmartBlockPlacerBlockEntity.java — 物品栏加载顺序修正(行 320-362)

loadAdditional(CompoundTag, HolderLookup.Provider) 中将 loadItemsFromTag 移至 loadFromTag 之前,是本次 PR 的核心修复。服务器环境中,NBT deserialize 调用 loadAdditional 时,若物品栏先被加载,loadFromTag 内的 tryLoadStructure 就能正确读取到 diskInventory 中的物品,不会被误判为空而清空 loadedStructure

✅ 正确性:修复逻辑正确。旧代码先调 loadFromTag(其中调 tryLoadStructure),在物品还未加载时会将空物品栏视为"磁盘已移除",清空结构缓存。调换顺序后,物品先就位,结构加载正常运作。

2. SmartBlockPlacerBlockEntity.javahasInvalidStructure = false 复位(行 358)

this.hasStructureDisk = true;
this.hasInvalidStructure = false;  // 新增

✅ 在 loadFromTag 中成功加载结构后显式重置无效标志,防止旧会话残留的状态在 reload 后继续生效。

3. applySyncDataFromMenu — 客户端结构缓存同步(行 3891-3910)

菜单同步包中包含 cachedStructure 时,客户端解析并缓存结构数据以供预览渲染。

⚠️ 注意: 当菜单同步包包含 cachedStructure 时,无条件设置 hasStructureDisk = true。如果存在某种竞态条件(磁盘在菜单序列化后、反序列化前被移除),客户端可能会短暂保有错误的 hasStructureDisk 状态。不过菜单描述通常只存在于打开菜单的瞬间,实际影响极低。

4. StructureLoadUtil.javaloadPreviewData() 新方法(行 229-299)

服务端读取结构 .nbt 文件,仅提取 paletteblocks 两个关键字段,通过网络发送给客户端。块数量上限 4096,超过时截断并记录警告。

✅ 设计合理——不发送 entitiesbiomessize 等客户端预览不需要的数据,减少网络负载。
✅ 路径安全验证与主加载路径一致。

5. StructureDiskPreviewSupport.java — 大幅重写(+147/−75)

将缓存架构从"5秒过期"改为会话级永不过期(仅条目超过 100 时淘汰最旧),并新增三阶段缓存策略:

  • PREVIEW_CACHE — 已解析的 LevelLike(永不过期)
  • PENDING_PREVIEW_DATA — 服务端返回的原始 NBT,等待 tooltip 渲染时解析
  • PENDING_REQUESTS + REQUEST_TIMESTAMPS — 请求去重,30秒超时后允许重试

获取预览的新降级链:① 本地缓存 → ② 服务端待处理数据 → ③ 本地文件加载 → ④ 向服务端发网络请求

✅ 去重机制防止大量 StructurePreviewRequestPacket 垃圾请求。
✅ 缓存键从 String 改为 UUID,类型更安全。

6. StructurePreviewRequestPacket.java + StructurePreviewResponsePacket.java(新增)

标准的 C2S/S2C 请求-响应模式。服务端收到请求后调用 loadPreviewData,校验 palette 和 blocks 非空后回复。客户端收到后存入 PENDING_PREVIEW_DATA

✅ 使用标准的 NeoForge StreamCodec 编码。
✅ 服务端校验 palette 和 blocks 非空,避免发送空数据浪费带宽。


⚠️ 警告

无严重安全或正确性问题。


💡 建议

1. applySyncDataFromMenu 中的 UUID 解析——使用 getUUID 替代 NBT 路径

// 当前:
this.loadedStructureUuid = UUIDUtil.CODEC.parse(
    NbtOps.INSTANCE, tag.getCompoundOrEmpty("cachedStructureUuid")
).result().orElse(null);

// 建议(如果 tag 存储为 UUID 格式而非 compound):
this.loadedStructureUuid = tag.getUUID("cachedStructureUuid");

如果 cachedStructureUuid 在 NBT 中以 UUID 原生格式存储,tag.getUUID() 更简洁且不易出错。但如果写入时就用了 UUIDUtil.CODEC.encode 输出为 IntArrayTag,则当前写法是正确的。建议确认序列化/反序列化的一致性。

2. StructureLoadUtil.loadPreviewDataloadStructureFromDisk 的路径验证重复

两者都包含相同的路径安全逻辑(isInvalidStructureFile + isPathOutsideBaseDirectory 检查 + getStructureDirectory)。如果后续还需要类似的方法(如批量预览),建议提取一个 resolveStructureFile 辅助方法减少重复。

3. MAX_PREVIEW_BLOCKS = 4096 截断后的预览可能令人困惑

当结构超过 4096 方块时,客户端预览只显示前 4096 个方块。用户可能会看到一个"不完整"的残缺结构而不知道原因。考虑在客户端侧也发出警告(例如在 tooltip 上显示 "预览已截断" 提示),或提供一个指示器。

4. StructureDiskPreviewSupportPREVIEW_CACHE 永不过期策略的风险

缓存永不过期(仅 size 淘汰)在长会话中可能占用较多内存。当前 MAX_CACHE_SIZE = 100 个预览,每个 PreviewCache 含一个 LevelLike 对象,内存占用可控。但如果未来有用户频繁查看 100 种不同的结构磁盘,建议监控实际内存开销并考虑添加一个较长的软超时(如 30 分钟)。


✅ 看起来不错

  • 新的网络包结构清晰,StreamCodec 使用规范
  • 请求去重机制合理(30秒超时防止无限重试)
  • 路径遍历安全验证在 loadPreviewData 中保持了一致性(isInvalidStructureFile + isPathOutsideBaseDirectory
  • BedBlock _ / DoorBlock _ 的 pattern match unused variable 修复(消除编译器警告)
  • 无遗留调试语句、TODO、合并冲突
  • 日志记录完善——失败时记录 UUID 和文件名便于排查
  • 三个关键状态正确设置:hasStructureDiskhasInvalidStructureloadedStructureUuid

总结: 本次 PR 正确地解决了服务器环境下智能方块放置器蓝图预览不可用的问题。核心修复(物品栏加载顺序调换 + 无效状态复位)逻辑正确,网络层新增的 C2S/S2C 预览请求-响应机制设计合理。建议多为一个小型优化(UUID 解析方式确认)和一个体验提示(截断预览的可见反馈),均可后续迭代处理。无阻塞性问题。

由 Hermes Agent 审查

@PigeonNian PigeonNian marked this pull request as draft June 22, 2026 11:47
@anvil-craft

Copy link
Copy Markdown

好的,所有变更已看完,以下是完整的审查意见。


代码审查摘要

PR #3809 — Fixed server environment blueprint preview for SmartBlockPlacer

修复了 26.1 服务器环境下智能方块放置器获取不到蓝图预览的问题

🟢 主要更改

1. SmartBlockPlacerBlockEntity.java — 加载顺序修复(+18/-2)

  • loadItemsFromTag 移到 loadFromTag 之前,确保 tryLoadStructure 执行时磁盘物品已就位,避免 cachedStructure 被误判清空 ✅ 这是问题的根本修复,方向正确
  • loadFromTag 中设置 this.hasStructureDisk = true 后新增 this.hasInvalidStructure = false 重置——防止服务端错误状态残留导致其他逻辑误判 ✅
  • applySyncDataFromMenu 中新增 cachedStructure/cachedStructureName/cachedStructureUuid 的反序列化,使客户端能从菜单同步包中获得结构缓存数据,从而实现蓝图预览 ✅

2. StructureDiskPreviewSupport.java — 缓存策略重构(+147/-75)

预览系统从单人模式扩展为 CS 架构:

  • 三阶段缓存模型:完整缓存 PREVIEW_CACHE(UUID→LevelLike,会话级永不过期)→ 待处理缓存 PENDING_PREVIEW_DATA(服务端返回的原始 NBT)→ 请求去重 PENDING_REQUESTS(30s 超时后允许重试)
  • 去掉原来的时间过期策略(CACHE_EXPIRY_MS=5000),改为会话级缓存 + 上限淘汰(MAX_CACHE_SIZE=100),更适合服务端场景 ✅
  • 四级回落逻辑:完整缓存命中 → 待处理 NBT 解析 → 本地文件加载(单人模式)→ 向服务端发送网络请求(多人模式)✅ 架构清晰
  • 新增 receiveStructureData() 方法,由 StructurePreviewResponsePacket 调用接收服务端数据 ✅
  • 新增 parsePreviewNbt() 方法,从 NBT 解析 palette + blocks 列表构建预览数据 ✅

3. 新网络包:StructurePreviewRequestPacket(+77,C2S)

  • 标准 AnvilCraft 数据包模式,StreamCodec 编解码 ✅
  • 服务端处理:校验 structureFile 非空 → 调用 StructureLoadUtil.loadPreviewData() 读取结构 → 校验 palette 和 blocks 非空 → 发送 StructurePreviewResponsePacket
  • 对空 palette 或 blocks 的情况有 LOGGER.warn 记录 ✅

4. 新网络包:StructurePreviewResponsePacket(+43,S2C)

  • 简单的数据容器包,服务端 → 客户端传递 UUID + 结构 NBT ✅
  • handleOnClient 直接调用 StructureDiskPreviewSupport.receiveStructureData() 缓存数据 ✅

5. StructureLoadUtil.java — 新增预览数据加载(+78/-14)

  • 新增 loadPreviewData() 方法:校验文件名 → 路径遍历检查 → 读取 .nbt 文件 → 提取 palette + blocks(超 4096 截断并警告)→ 返回 CompoundTag
  • 安全相关的 isValidStructureFile/isPathWithinBaseDirectoryisInvalidStructureFile/isPathOutsideBaseDirectory 逻辑反转重命名,返回值语义更直观 ✅
  • check all call sites 确认反转正确:
    • loadStructureFromDisk: if (!isValidStructureFile)if (isInvalidStructureFile)
    • loadPreviewData: 新调用 if (isInvalidStructureFile)
    • isPathWithinBaseDirectoryisPathOutsideBaseDirectory: 所有 if (!isPathWithin...)if (isPathOutside...)
  • BedBlock bedBlockBedBlock _ / DoorBlock doorBlockDoorBlock _:Java 21 pattern variable 风格清理 ✅

6. StructureSaveUtil.java — 一致性调整(+6/-6)

  • 对应的 isPathWithinBaseDirectoryisPathOutsideBaseDirectory 改名 ✅
  • ⚠️ 注意:StructureSaveUtil 中存在一份与 StructureLoadUtil 重复的 isPathOutsideBaseDirectory 方法,建议后续抽取为共享工具方法

💡 建议(非阻塞性)

文件 建议
StructureDiskPreviewSupport.java parsePreviewNbt stateIndex 为 -1 或越界时,当前仅静默跳过该方块。建议添加 LOGGER.warn 记录,便于排查结构数据损坏问题。
StructureLoadUtil.java loadPreviewData MAX_PREVIEW_BLOCKS = 4096 的截断策略合理,但截断后客户端显示的将是不完整的预览。建议在客户端渲染预览时添加"部分显示"标记(如半透明覆盖或文字提示),使用户知道预览被截断。
StructureSaveUtil.java 全局 isPathOutsideBaseDirectoryStructureLoadUtil 中代码重复。考虑到两个工具类的职责(保存 vs 加载)确实分离,但路径校验逻辑完全一致,建议抽取到一个共享的安全工具类。

✅ 看起来不错

  • 代码注释详尽(中文),新方法/字段都有 javadoc,设计意图清晰
  • 所有新网络包遵循 anvilcraft-libIPacket/StreamCodec 模式,与现有代码一致 ✅
  • loadPreviewData 的路径遍历防护具有双重保障:文件名正则校验(VALID_STRUCTURE_FILE)+ resolved path safety check ✅
  • 请求去重机制 PENDING_REQUESTS + 30s 超时有效防止客户端的重复请求风暴 ✅
  • 缓存淘汰策略使用 LinkedHashMap-style 的 streams 排序淘汰最旧条目,防止内存泄漏 ✅
  • 无 TODO/FIXME/HACK 遗留,无凭据泄露,无合并冲突标记 ✅
  • isInvalidStructureFile 的可见性改为 public 以便被其他包调用,合理 ✅
  • \ No newline at end of file 检查:0 处 ✅

结论

建议:评论(Comment)— 无阻塞问题,可以合并。

该 PR 正确地解决了服务器环境下智能方块放置器无法获取蓝图预览的核心问题。关键修复(加载顺序调整)简单有效,同时通过新增 C2S/S2C 网络包构建了完整的客户端-服务端预览数据同步体系。缓存架构重构干净,安全措施到位,代码质量良好。以上建议均为可选的优化方向,不阻塞合并。


由 Hermes Agent 审查

Pigeon_Nian added 2 commits June 22, 2026 19:56
- 调整UUID解析的代码换行,提升可读性
- 保持原有逻辑不变
- 符合代码风格规范
- 使用 PropertyDispatch 优化 SmartBlockPlacerBlock 和 StructureScannerBlock 的方块状态渲染
- 增加倒挂(upside_down)属性,完善模型旋转逻辑,支持水平方向和上下翻转
- 修正 SmartBlockPlacerBlock 中倒挂状态的碰撞盒形状计算
- 优化 SmartBlockPlacerBlockEntity 的前置扫描逻辑,停止模式下缺少物品时停止扫描
- SmartBlockPlacerScreen 中预览结构时添加倒挂方向渲染及结构数据翻转处理
- 更新 smart_block_placer.json 和 structure_scanner.json 增加倒挂相关方块状态配置
@PigeonNian PigeonNian marked this pull request as ready for review June 22, 2026 13:05
@anvil-craft

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

# Check for common issues
echo "=== 调试语句/遗留代码 ===" && grep -n "print(\|console\.log\|TODO\|FIXME\|HACK\|XXX\|debugger" /tmp/pr3809.diff | grep "^+" | head -10
echo ""
echo "=== 密钥/凭据模式 ===" && grep -i...

Reason: Security scan — [MEDIUM] Variation selector characters detected: Content contains Unicode variation selectors (VS1-256). These are commonly used in emoji sequences but may indicate steganographic encoding or obfuscation

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

@anvil-craft

Copy link
Copy Markdown

代码审查摘要

结论:评论(发现若干建议,无阻塞性问题)


📋 变更总览

本 PR 修复了 26.1 服务端环境下智能方块放置器无法获取蓝图预览 的问题,涉及 11 个文件1,108 行差异。核心变更:

类别 文件 说明
🎯 核心修复 SmartBlockPlacerBlockEntity.java 物品栏加载顺序调整、无效结构标志重置、菜单数据包同步结构缓存
🌐 网络通道 StructurePreviewRequestPacket.java (新) + StructurePreviewResponsePacket.java (新) C2S 请求 + S2C 响应的结构预览数据传输
🖼️ 客户端预览 StructureDiskPreviewSupport.java 缓存策略重构:从 TTL 过期改为会话级永不过期 + 请求去重 + 服务端优先
🖥️ GUI SmartBlockPlacerScreen.java 倒挂模式蓝图预览坐标修正、方块状态旋转
🔧 碰撞箱 SmartBlockPlacerBlock.java NORTH_UPSIDE 与 SOUTH_UPSIDE 的 Shape 基座互换
🏗️ 数据生成 ModBlocks.java SmartBlockPlacer + StructureScanner 的 blockstate 生成逻辑增加 upside_down 属性支持
🔒 安全重构 StructureLoadUtil.java + StructureSaveUtil.java 校验方法语义反转(isValidisInvalid),逻辑不变

💡 建议

1. 蓝图模式"停止"行为变更可能影响用户体验

SmartBlockPlacerBlockEntity.javaprepareBlueprintModeHeldBlock() 方法新增了 3 处 !isSkipMissingMode 检查。当停止模式下所需方块缺失时,行为从原来的 continue(跳过当前层继续前扫)变为 setEmpty + return(停止全部前扫)。这是一个有意为之的行为变更,但:

  • PR 描述和代码注释未解释为什么需要这个调整。如果这是修复"服务端预览"所必需的附随改动,请补充注释说明其与预览问题的关联。
  • 已有用户可能依赖旧的"继续前扫"行为。建议在此方法头部的 JavaDoc 中明确记录 stop 模式的语义。

2. 预览数据截断缺乏客户端感知

StructureLoadUtil.loadPreviewData() 在方块 >4096 时截断并记录服务端日志,但客户端收到的截断数据与完整数据无异,无法区分。考虑在响应包中增加 truncated: boolean 字段,以便客户端在预览时显示提示(如"预览仅显示前 4096 个方块")。

这属于优化建议——当前实现对预览功能无功能性问题,只是大结构用户可能感到困惑。

3. 请求去重超时期间无 loading 状态

StructureDiskPreviewSupport.shouldSendRequest() 使用 30s 超时去重。在网络延迟较高或丢包场景下,用户在这 30s 内 hover 磁盘不会看到任何预览,也无加载指示。建议:

  • PENDING_REQUESTS 状态期间返回一个特殊标记,让渲染层显示"加载中..."提示而非空白

4. 生成 JSON 缺少末尾换行符

两个 src/generated/resources/ 下的 blockstate JSON 文件在 diff 末尾标记了 \ No newline at end of file。这通常是无害的(且可能是数据生成器的默认行为),但如果构建系统/CI 工具检查 POSIX 兼容性,建议确认数据生成器是否自动补全末尾换行。


✅ 值得肯定的设计

方面 说明
网络通道设计 预览请求/响应分离为独立 Packet(StructurePreviewRequestPacket + StructurePreviewResponsePacket),职责清晰,符合 NeoForge 网络最佳实践。服务端 handleOnServer 中增加了空 palette 和 blocks 的校验。
多级缓存策略 客户端 getOrCreateCache() 的 4 级 fallback 链(缓存 → 待处理数据 → 本地文件 → 网络请求)设计合理,既能服务端离线时(单人模式)由本地文件加载,也支持联机时通过服务端获取。
安全方法重构 isValidStructureFile() 改为 isInvalidStructureFile()isPathWithinBaseDirectory() 改为 isPathOutsideBaseDirectory(),否定语义更直观,调用处 if (!isValid)if (isInvalid) 消除了易错的逻辑取反。
Switch 模式清理 BedBlock bedBlockBedBlock _ 消除了未使用变量的编译警告。
倒挂渲染修正 SmartBlockPlacerScreen.java 中的坐标变换(sizeX - 1 - bp.x()bp.state().rotate(CLOCKWISE_180) + flipHalfPropertyStatic)经过仔细的代数计算,修正了之前预览与实际放置不一致的问题。
BlockState 数据生成 PropertyDispatch.initial(4个属性) + lambda 生成器替代之前的 PropertyDispatchWrap + ROTATION_HORIZONTAL_FACING,原生支持了 upside_down 的 X 轴旋转和 Y 轴补偿。

📊 文件变更统计

文件 类型 +行 -行 说明
StructureDiskPreviewSupport.java 重写 +147 -75 缓存策略 + 网络预览管线
StructureLoadUtil.java 增强 +78 -14 新增 loadPreviewData() + 安全方法重构
ModBlocks.java 修改 +38 -8 blockstate 生成增加 upside_down
SmartBlockPlacerBlockEntity.java 核心修复 +37 -5 加载顺序 + 缓存同步 + stop模式
smart_block_placer.json (gen) 自动生成 +92 -16 含 upside_down 的新 variants
SmartBlockPlacerScreen.java 修改 +14 -8 倒挂预览坐标修复
StructurePreviewRequestPacket.java 新文件 +77 -0 C2S 预览请求
StructurePreviewResponsePacket.java 新文件 +43 -0 S2C 预览响应
StructureSaveUtil.java 重构 +6 -6 安全方法重命名
SmartBlockPlacerBlock.java 修复 +2 -2 Shape 南北互换
structure_scanner.json (gen) 自动生成 +23 -4 含 upside_down 的新 variants

由 Hermes Agent 审查

@anvil-craft

Copy link
Copy Markdown

⚠️ Dangerous command requires approval:

# 检查 EOF 换行符
grep -c 'No newline at end of file' /tmp/pr3809.diff
echo "---"
# 检查 TODO/FIXME/HACK
grep -n "TODO\|FIXME\|HACK\|XXX\|debugger" /tmp/pr3809.diff | grep -v "^[0-9]*:.*import\|^[0-9]*:.*//....

Reason: Security scan — [MEDIUM] Variation selector characters detected: Content contains Unicode variation selectors (VS1-256). These are commonly used in emoji sequences but may indicate steganographic encoding or obfuscation

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

@anvil-craft

Copy link
Copy Markdown

代码审查摘要

结论:评论(观察到 2 条建议,无阻塞性问题)

PR 概览: 新增 C2S/S2C 网络包体系,解决服务端环境下智能方块放置器的蓝图预览无法获取的问题。核心变更包括:新增 StructurePreviewRequestPacket / StructurePreviewResponsePacket、重写 StructureDiskPreviewSupport 缓存策略、NBT 加载顺序调整、结构预览数据服务端加载方法 loadPreviewData,以及方块状态模型数据生成重构。共 11 个文件变更(1108 行 diff)。


📋 变更总览

类别 文件 变更量 说明
核心架构 StructurePreviewRequestPacket +78/-1 (新增) C2S 结构预览请求包
核心架构 StructurePreviewResponsePacket +44/-1 (新增) S2C 预览数据响应包
缓存重写 StructureDiskPreviewSupport +148/-76 TTL过期 → 会话级永不过期 + LRU淘汰 + 网络请求去重
服务端加载 StructureLoadUtil +79/-15 新增 loadPreviewData() + 安全方法命名反转
NBT修复 SmartBlockPlacerBlockEntity +38/-6 加载顺序调整 + 状态重置 + 停止模式逻辑
数据生成 ModBlocks +39/-9 方块状态生成器全面重构(新增 upside_down + facing)
生成资源 smart_block_placer.json +93/-17 重新生成的方块状态(含 upside_down 变体)
生成资源 structure_scanner.json +24/-5 重新生成
预览渲染 SmartBlockPlacerScreen +15/-9 倒挂状态下结构预览位置/旋转修正
形状修正 SmartBlockPlacerBlock +3/-3 NORTH↔SOUTH 倒挂形状互换
安全重构 StructureSaveUtil +7/-7 方法命名反转(纯重构)

💡 建议

1. ⚠️ 缓存集合的线程安全性StructureDiskPreviewSupport

PREVIEW_CACHEPENDING_PREVIEW_DATAPENDING_REQUESTSHashMap/HashSet,但 getOrCreateCache() 在渲染线程调用,receiveStructureData() 在网络线程调用。两者均可并发修改这些集合。

虽然 Minecraft 客户端主线程架构下极少出现真正的并行问题,但在以下场景可能触发 ConcurrentModificationException

  • 服务端快速连续返回多个 StructurePreviewResponsePacket,同时 GuiGraphicsExtractor 触发 tooltip 渲染进入 evictIfNeeded()
  • evictIfNeeded().stream().sorted().forEach() 不修改源 Map 的流操作,但 PREVIEW_CACHE.remove() 在并发写入时可能出问题

建议: 考虑使用 ConcurrentHashMap 替代 HashMap,或至少对 evictIfNeeded()synchronized 保护。推荐更轻量的方案:改用 ConcurrentHashMap + ConcurrentHashMap.newKeySet()

2. ⚠️ 蓝图停止模式的语义变化SmartBlockPlacerBlockEntity.prepareBlueprintModeHeldBlock

// 新增代码(三处):
if (!this.isSkipMissingMode) {
    this.currentHeldBlock = ItemStack.EMPTY;
    return;
}

旧行为:所有缺失方块场景均 continue 扫描下一个方块。
新行为:当 isSkipMissingMode == false(停止模式)时,遇到任一缺失方块即停止扫描并设置 currentHeldBlock = EMPTY

这是一个用户可见的行为变化,可能影响依赖旧"自动跳过缺失方块"行为的玩家。建议确认:

  • 这是否为有意的设计决定,还是与修复目标无关的附带变更;
  • 停止模式下一旦第一个缺失方块出现就停止扫描,而不是检查完所有候选后依然未找到才停止,是否符合用户预期。

3. ⚠️ 生成资源文件缺失 EOF 换行符

s/(.*)smart_block_placer.jsons/(.*)structure_scanner.json 末尾标记了 \ No newline at end of file

这是生成文件常见问题,POSIX 兼容性影响较小(JSON 解析器不受影响),建议下次运行数据生成时确认生成器是否配置了末尾换行。

4. ℹ️ 网络包注册需确认

StructurePreviewRequestPacketStructurePreviewResponsePacket 定义了 TYPESTREAM_CODEC,但此 PR diff 中未看到包注册代码(通常位于某个网络初始化类中)。请确认这些包已在 NeoForge 的 PacketDistributor/PayloadRegistrar 中注册,否则客户端收到响应包时会因未注册 payload type 而静默丢弃。


🔍 详细分析

核心修复方案评估:✅ 正确

旧方案:
  StructureDiskPreviewSupport → 本地读取结构 NBT 文件
                              → 服务端无本地文件 → 返回 null → 无预览

新方案:
  Client → StructurePreviewRequestPacket (C2S) → Server
       Server → StructureLoadUtil.loadPreviewData() → 读取文件
       Server → StructurePreviewResponsePacket (S2C) → 返回 palette + blocks
  Client → receiveStructureData() → PENDING_PREVIEW_DATA
         → 下次渲染时 parsePreviewNbt → LevelLike → PREVIEW_CACHE

这是正确的架构方案。服务端拥有结构文件访问权限,通过网络同步调色板和方块列表给客户端,客户端无需直接访问服务端文件系统。

缓存策略评估:✅ 正确且有提升

项目 旧版 新版
缓存键 String (UUID 或 hash) UUID (直接从 diskData)
过期策略 TTL 5秒 + 间隔清理 会话级永不过期 + LRU淘汰
请求去重 UUID去重 + 30秒超时重试
对象 PreviewCache (class) PreviewCache (record)
  • UUID 作为缓存键消除了 hash 碰撞风险
  • 会话级永不过期避免了 tooltip 渲染时的重复请求
  • MAX_CACHE_SIZE = 100 防内存泄漏
  • 请求去重 + 超时机制合理

安全方法重构评估:✅ 逻辑正确

isValidStructureFileisInvalidStructureFileisPathWithinBaseDirectoryisPathOutsideBaseDirectory,所有调用点的取反关系已正确更新:

旧: if (!isValidStructureFile(name)) → error
新: if (isInvalidStructureFile(name)) → error
    ✓ 等价

旧: if (!isPathWithinBaseDirectory(path, base)) → error
新: if (isPathOutsideBaseDirectory(path, base)) → error
    ✓ 等价

StructureSaveUtil 中也同步更新了同样的命名转换。

SmartBlockPlacerBlock 形状修正:✅ 正确

旧: SHAPE_NORTH_UPSIDE = rotate(X, 180, SHAPE_NORTH)  ❌
    SHAPE_SOUTH_UPSIDE = rotate(X, 180, SHAPE_SOUTH)   ❌
新: SHAPE_NORTH_UPSIDE = rotate(X, 180, SHAPE_SOUTH)   ✅
    SHAPE_SOUTH_UPSIDE = rotate(X, 180, SHAPE_NORTH)    ✅

绕 X 轴旋转 180 度时,NORTH ↔ SOUTH 自动互换,旧值导致 NORTH_UPSIDE 和 SOUTH_UPSIDE 形状交换了位置。

NBT 加载顺序修复:✅ 正确

旧: loadFromTag(tag) → loadItemsFromTag(...) 
    → loadFromTag 中 tryLoadStructure 可能在物品尚未加载时
      误判"磁盘槽位为空"而清空 cachedStructure

新: loadItemsFromTag(...) → loadFromTag(tag)
    → 先加载物品确保 tryLoadStructure 能正确识别磁盘物品

同时 loadFromTag 中增加了 this.hasInvalidStructure = false; 的显式重置,防止旧状态残留。

applySyncDataFromMenu 新增结构缓存同步:✅ 正确

菜单同步包现在携带 cachedStructure/cachedStructureName/cachedStructureUuid,客户端收到后更新到结构缓存中,使蓝图预览能直接从本地缓存命中,减少网络请求。

SmartBlockPlacerScreen 预览渲染改进:✅

  • 倒挂状态下 X/Z/Y 坐标正确翻转
  • 方块状态 rotate(Rotation.CLOCKWISE_180) + flipHalfPropertyStatic
  • placer 自身不再设置 UPSIDE_DOWN 属性(预览渲染仅在结构旋转层面处理正倒挂,更简洁)

新网络包 StreamCodec 设计:✅

StructurePreviewRequestPacket 使用 UUIDUtil.STREAM_CODEC + ByteBufCodecs.STRING_UTF8StructurePreviewResponsePacket 使用 ByteBufCodecs.COMPOUND_TAG。编解码简洁高效。服务端对空结构文件和空 palette/blocks 的日志警告策略合理。

loadPreviewData 安全截断:✅

MAX_PREVIEW_BLOCKS = 4096 限制了单个预览包的大小,超过时截断并记录日志。这防止了超大结构导致网络包超过 Minecraft 的 2MB 包大小限制。

Java 21 未命名模式变量:✅

case BedBlock _ ->   // 替代旧的 case BedBlock bedBlock ->
case DoorBlock _ ->  // 替代旧的 case DoorBlock doorBlock ->

JEP 443 (JDK 21 final) 语法,清理未使用变量警告,无行为变化。


🔎 审查清单检查结果

类别 结果
✅ 正确性 NBT加载顺序修复方案合理;缓存策略正确;网络包设计合理
✅ 安全性 路径遍历防护已保留(仅重命名),文件名白名单验证未放宽
✅ 代码质量 结构清晰,注释充分(中/双语),命名改善
⚠️ 性能 缓存策略大幅提升(去除了 TTL 重计算),但并发映射可优化
⚠️ 测试 diff 中未见新增测试代码(模组项目常见,建议确认手动测试覆盖)
✅ 文档 JavaDoc 注释完整,缓存策略用 ul 标签详细说明
✅ 向后兼容 NBT 加载兼容旧路径(保留 loadFromTag 调用)

✨ 总结亮点

  • 核心问题定位准确:服务端无本地文件 → 网络同步是唯一正确方案
  • 缓存策略设计合理:会话级永不过期 + 去重 + 超时重试的三层保障
  • 变更范围控制良好:仅涉及预览相关逻辑,未引入不必要的重构
  • 安全防护保留:路径遍历和文件名验证在原语义下完整保留

建议: 确认网络包在 NeoForge PayloadRegistrar 中的注册代码是否存在,并评估 HashMap 是否需要升级为 ConcurrentHashMap


由 Hermes Agent 审查 · PR #3809 · synchronize · 26.1 服务器环境蓝图预览修复

@anvil-craft

Copy link
Copy Markdown

现在我已经审查了所有文件,以下是完整的审查意见。


代码审查摘要 — PR #3809

结论:评论(无阻塞性问题,2 条建议)

PR 范围: 修复智能方块放置器在 26.1 服务器环境下无法获取蓝图预览的问题 + 添加倒挂模型支持


🟢 主要变更架构

该 PR 引入了 C2S/S2C 网络协议 来解决服务器环境下磁盘预览的问题:

客户端 (tooltip 渲染)
  └─ 缓存未命中 → StructurePreviewRequestPacket (UUID + fileName)
      └─ 服务端 → StructureLoadUtil.loadPreviewData() 读取结构文件
          └─ 提取 palette (调色板) + blocks (方块列表, 上限 4096)
              └─ StructurePreviewResponsePacket (NBT)
                  └─ 客户端 parsePreviewNbt() → LevelLike → 缓存 → 渲染

三层缓存策略:

  1. PREVIEW_CACHE (UUID → LevelLike) — 会话级,永不超时,LRU 淘汰 (上限 100)
  2. PENDING_PREVIEW_DATA (UUID → CompoundTag) — 服务端 NBT 等待解析
  3. PENDING_REQUESTS (UUID → timestamp) — 去重,30s 超时可重试

核心修复:

  • loadAdditional(): 物品栏加载顺序调整 — 先加载磁盘物品,再 loadFromTag(),防止 tryLoadStructure 因磁盘物品不可用而误判清空缓存
  • loadFromTag(): 加载结构成功后重置 hasInvalidStructure = false,避免错误状态残留
  • applySyncDataFromMenu(): 同步 cachedStructure 数据到客户端,使蓝图预览在服务器环境下也能工作
  • prepareBlueprintModeHeldBlock(): 停止模式 (!isSkipMissingMode) 下遇到缺失方块直接停止,不继续前扫

✅ 代码质量

  • record 模式参数重构BedBlock doorBlockBedBlock _ (Java 21 未命名模式变量),干净
  • 安全逻辑反转正确isValidStructureFileisInvalidStructureFileisPathWithinBaseDirectoryisPathOutsideBaseDirectory,所有调用点统一更新,验证逻辑等价
  • 预览数据上限 MAX_PREVIEW_BLOCKS = 4096,超限时日志警告 + 截断,防止大结构导致网络/内存问题
  • 网络包 StreamCodec 实现正确,使用 IPacket.type() 注册类型

⚠️ 警告

1. 生成资源文件缺少 EOF 换行符
两个 generated blockstate JSON 文件末尾无换行符 (\ No newline at end of file):

  • smart_block_placer.json
  • structure_scanner.json

虽然这些是数据生成器输出,但 POSIX 兼容性问题建议修复。查看 ModBlocks.java 中数据生成器代码,generate() lambda 的 BlockModelGenerators.plainVariant(model).with(...) 输出直接拼接到 blockStateOutput.accept(...),请确认数据生成器框架是否自动添加尾随换行。若否,建议在生成器后追加 \n


💡 建议

1. 服务端预览请求速率限制
StructurePreviewRequestPacket.handleOnServer() 未对客户端的请求频率做限制。一个恶意客户端(或模组发包器)可以高频发送大量预览请求,触发服务端重复读取压缩 NBT 文件 (128MB + accounter)。建议:

// 在服务端添加玩家级请求冷却
private static final Map<UUID, Long> LAST_REQUEST_TIME = new HashMap<>();
private static final long REQUEST_COOLDOWN_MS = 1000;

// handleOnServer 开头:
Long lastTime = LAST_REQUEST_TIME.get(serverPlayer.getUUID());
if (lastTime != null && System.currentTimeMillis() - lastTime < REQUEST_COOLDOWN_MS) {
    return; // 冷却期内忽略
}
LAST_REQUEST_TIME.put(serverPlayer.getUUID(), System.currentTimeMillis());

2. loadStructureData() 空返回值处理
applySyncDataFromMenu() 中新增了 this.loadedStructure = this.loadStructureData(...),如果 loadStructureData() 返回 null(例如 NBT 格式损坏),loadedStructure 被赋值为 null。请确认下游使用 loadedStructure 的地方(如 tryLoadStructuregetStructureRenderer() 等)能够安全处理 null 值。同样的问题存在于原 loadFromTag() 中,但新路径 applySyncDataFromMenu 尚未经过实际运行验证。


✅ 看起来不错

  • 倒挂模型旋转逻辑正确SHAPE_NORTH_UPSIDESHAPE_SOUTH 旋转 180°(NORTH→SOUTH 翻转),SHAPE_SOUTH_UPSIDESHAPE_NORTH,对称正确
  • ModBlocks 数据生成器:正确处理了 UPSIDE_DOWN + FACING + OVERLOAD + POWERED 四个属性的组合,x 轴旋转 180° + y 轴旋转与 base 相反方向,视觉上正确实现倒挂
  • UI 蓝图预览SmartBlockPlacerScreen.buildPreviewLevelLike()upsideDown 模式的坐标映射 (sizeX - 1 - x, sizeZ - z, 2 - y) 以及方块状态旋转 (Rotation.CLOCKWISE_180 + flipHalfPropertyStatic) 实现完整
  • 跨语言翻译同步:本次 PR 未修改语言文件,无翻译相关问题
  • 无遗留调试语句、无合并冲突标记、无硬编码凭据

由 Hermes Agent 审查

@Gu-ZT Gu-ZT merged commit 2cd7012 into Anvil-Dev:dev/26.1/1.6 Jun 22, 2026
2 checks passed
@anvil-craft

Copy link
Copy Markdown

PR #3809 已被关闭(action: closed)。

根据 github-code-review 技能的明确指引:

若 action 为 "closed" 或 "labeled",请在此处停止。

因此我不会下载 diff 或执行任何审查。PR 已合并/关闭,无需进一步操作。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants