feat(Yggdrasil): multiple content - #406
Conversation
…so generate visual effect.
… into Yggdrasil/newContent0
… into Yggdrasil/newContent0
…Everglow into Yggdrasil/newContent0
… into Yggdrasil/newContent0
…Everglow into Yggdrasil/newContent0
Almost basic features mentioned in the proposal were implemented.
CloudeaSoft
left a comment
There was a problem hiding this comment.
建议修改后再合并(Request changes)。
审查基于提交 5a9646b,相对基准 f222f93。本 review 汇总两轮审查的 24 项独立问题(6 项 P1、18 项 P2),包含此前单独发布的 6 条局部问题。
应优先处理客户端/服务端 Boss 初始化和死亡回调、箱体覆盖、熔炉商店越界索引以及藤蔓重复占用异常;其余意见覆盖内容与积分兼容、多人同步、房间切换、跨世界状态、武器行为和世界生成边界。每条行内意见均包含触发条件、实际影响、验证依据或限制,以及修复方向。
验证记录:
- 此提交此前执行 dotnet build /p:WarningLevel=0 成功:0 错误、7 条与 shader 输出文件占用相关的警告。
- dotnet test --no-build --no-restore --verbosity normal /p:WarningLevel=0:309/309 通过。
- 按仓库要求执行字节级 UTF-8 BOM 检查,通过。
- 使用已编译程序集建立最小内存场景,复现旧积分归零、Boss 接收端空引用和死亡回调异常、攻击初始化差异、错误脱战、形状覆盖/越界、藤蔓重复占用与跨世界残留,以及墙体和几何算法错误。
未启动完整专用服务器、双客户端会话、图形游戏循环或世界生成种子回放;静态调用链结论与方法复现已分别标注。现有单元测试通过不代表这些运行路径已通过验证。
| AICoroutine.Update(); | ||
| RealCenter += NPC.velocity; | ||
| BodyRope.Masses[0].Position = RealCenter; | ||
| BodyRope.ApplyForce_VelocityDecay(0.2f); |
There was a problem hiding this comment.
[P1] VampireMat 接收端首次 AI 会发生空引用
联机客户端收到 Boss 后,BodyRope 仍为 null:唯一赋值位于 OnSpawn,而该回调只在单机或服务端执行;AI 却直接访问 BodyRope.Masses。已使用本次构建的程序集创建接收端初始化状态,调用 AI 后在第 111 行得到 NullReferenceException。客户端需要在首次使用前初始化绳体,并单独同步战斗状态。
验证:编译后方法最小复现;未运行真实多人会话。回调语义见 tML ModNPC 文档。
| pWST.PlaceAtTileObjectDataOrigin(x, y); | ||
| Item.stack--; | ||
| return false; |
There was a problem hiding this comment.
[P1] 放置墩台可以覆盖装有物品的箱子
CanUseItem 在检查占用和锚点之前直接调用形状写入方法,再扣除物品并返回 false。该方法直接替换 TileType,不经过普通放置的保护。将鼠标对准箱子左上角时,13×8 形状的相应四个像素均为实心,会覆盖箱子四格,使内容无法通过箱子正常取出。应先验证整个形状的可放置性,再在成功放置后扣除物品。
验证:用真实形状 BMP 和已编译放置方法建立内存场景,箱子四格从类型 21 被替换为合成目标类型 0,Chest 记录及其中 7 个物品仍存在,但箱体已被覆盖。没有操作真实世界存档。
| if (CurrentSpecialShop >= 0 && Main.npcShop == 0) | ||
| { | ||
| Main.npcShop = 65536 + CurrentSpecialShop; |
There was a problem hiding this comment.
[P1] 与 NPC 交谈时打开熔炉商店会使用越界索引
与附近 NPC 保持对话、背包打开时再打开熔炉积分商店,会把 npcShop 设成 65536。CanInteract 没有排除对话状态,也没有关闭对话。本机 tML 的 DrawInventory 在 talkNPC != -1 时不会清掉该值,随后用它访问 shop[npcShop],造成 IndexOutOfRangeException。应使用明确的 UI 隐藏/替换逻辑,避免把无效下标传入原版绘制。
验证:已核对本机 tModLoader.dll 的 npcShop setter 及 DrawInventory IL:IL_33df 起的判断、IL_3623–IL_362d 的数组访问;未执行图形绘制。
| hTP.SwitchVineCoolTimer = 30; | ||
| hTP.Grasping = true; | ||
| RopeGraspingPlayer.Add(tilePos, player); |
There was a problem hiding this comment.
[P1] 第二名玩家抓住已占用藤蔓会触发重复键异常
第一名玩家抓住藤蔓后,第二名玩家在范围内按上,GraspHangingTile 仅排除第二名玩家已抓住其他藤蔓、以及藤蔓正在调节的情况,没有排除该藤蔓已被占用。此处对同一 tilePos 再次 Dictionary.Add 将抛出 ArgumentException,且此前已将玩家设为 Grasping。切换藤蔓的路径也缺少目标占用检查。应先通过 TryAdd 成功占用,再修改玩家状态。
验证:调用已编译的 AddPlayerToRope,两名玩家依次抓取同一坐标,第二次调用在第 397 行抛出 ArgumentException;占用表只有 1 项,两名玩家的 Grasping 却均为 True。未运行完整双人会话。
| if (i > Main.maxTilesX - TotalWidth || i < 0 || j > Main.maxTilesY || j - TotalHeight < 0) | ||
| { | ||
| return; |
There was a problem hiding this comment.
[P2] 形状放置的边界校验没有计入原点偏移
检查鼠标坐标后才减去 TileObjectData.Origin,检查的并非最终写入矩形。例如宽 13、原点 X=6 的墩台在 i=5 时通过检查,之后将访问负的 X;底边也只检查 j 是否超过地图,没有预留形状高度。放置道具绕过普通放置流程,因此可到达这些写入。应先算左上角,再检查完整矩形处于地图范围内。
验证:用宽 13、Origin.X=6 的真实放置方法,在 i=5 时于第 225 行复现 IndexOutOfRangeException;这是内存 Tilemap 的方法复现,未操作真实世界。
| bounds.Z = bounds.Z - bounds.Z % 16 - 8; | ||
| bounds.W = bounds.W - bounds.W % 16 - 8; |
There was a problem hiding this comment.
[P2] 最大边界漏掉最后一行和最后一列格子
最大 X/Y 向下对齐后再减 8,会把最右、最下的格子中心排除在循环上界外。传入 tile 顶点 (100,100)、(102,100)、(102,102)、(100,102),当前编译后函数只返回 X/Y 为 100 或 101 的 4 格;MathUtils.IsPointInPolygon 明确包含边界,应包含 9 格。开发工具多边形选择也直接调用这里。需要修正这两行对最大边界的对齐,使最后一行/列的格子中心能够进入包含判断。
| if (CurrentPlayer != null) | ||
| { | ||
| FurnacePlayer fPlayer = CurrentPlayer.GetModPlayer<FurnacePlayer>(); | ||
| CurrentScore = fPlayer.FurnaceScore; | ||
| CurrentScore = fPlayer.TotalFurnaceScore; |
There was a problem hiding this comment.
[P2] 熔炉保留上一角色的 Player 引用,后续积分归属错误
本 PR 删除了每帧把 CurrentPlayer 更新为 Main.LocalPlayer 的逻辑,却没有在世界卸载/加载时清理该静态引用。角色 A 站在地磅上退出,再用角色 B 进入且尚未踩秤时,CurrentPlayer 仍指向 A;地磅离开检测也只清理等于当前本地玩家的引用。MeltingButton.MeltDown 第 109–112 行将积分加给该旧对象,因此新角色的物品可被熔掉而积分未进入其存档。
验证:用两个内存 Player 执行系统 OnWorldUnload、OnWorldLoad、PostUpdateEverything,得到 OldPlayerRetained=True、NewPlayerIsLocal=True、DisplayedScore=25000。静态调用链确认积分写入目标;没有执行会消耗实际物品的完整熔炼流程。
修复方向:在世界生命周期清理 CurrentPlayer 及对应 UI 状态,熔炼前重新验证当前角色及有效地磅占用者。
| /// Be valid only when CanGrasp is true.<br></br> | ||
| /// 1 rope can be grasped by only 1 player. | ||
| /// </summary> | ||
| public static Dictionary<Point, Player> RopeGraspingPlayer = new Dictionary<Point, Player>(); |
There was a problem hiding this comment.
[P2] 抓藤蔓的静态占用状态跨世界残留
新增 RopeGraspingPlayer 保存世界坐标和 Player 引用,但 OnWorldUnload/OnWorldLoad 只清空各 tile 的绳体字典,没有清除占用表和 HangingTile_Player.Grasping。玩家抓住藤蔓时切换子世界并复用 Player 后,ContainsValue(Player) 会阻止其抓新藤蔓;旧世界的坐标键也可能与新世界绳体冲突。
验证:实际 AddPlayerToRope 后执行两个世界回调,结果 AfterWorldReloadEntries=1、P1Grasping=True。相关入口判断位于第 1377 行。此处复现的是清理回调缺失,未启动完整子世界过渡。
修复方向:把占用表、玩家抓取状态和世界绳体一起在切换时清理;注意模组卸载和玩家离开。
| } | ||
| } | ||
| } | ||
| List<int> noSolidMazeTile = WaterDeliveryHoleTiles; |
There was a problem hiding this comment.
[P2] 复制列表,避免把装饰物加入传送口集合
这里是引用赋值,后续对 noSolidMazeTile 的 16 次 Add() 也会修改 WaterDeliveryHoleTiles。后面 VampireMatCave() 用 WaterDeliveryHoleTiles.Contains(tile.TileType) 判断房间是否已有传送口,于是氧气罐、绿藻等装饰也会让 hasDeliveryHole 成为 true,跳过补建连接。这里应创建独立列表,例如 new List<int>(WaterDeliveryHoleTiles)。
| for (int i = 0; i < 200; i++) | ||
| { | ||
| Vector2 pos = new Vector2(Main.rand.Next(40, Main.maxTilesX / 2 + 50), Main.rand.Next(Main.maxTilesY - 700, Main.maxTilesY - 20)); | ||
| FillLiquid(pos); | ||
| FillLiquid(pos.ToTileCoordinates()); |
There was a problem hiding this comment.
[P2] 补水位置已经是 tile 坐标,不应再次除以 16
上一行直接按 Main.maxTilesX/maxTilesY 抽样,所以 pos 的数值已经是 tile 坐标。ToTileCoordinates() 又除以 16,例如 (1000,20500) 会变成 (62,1281),使这 200 次补水偏离底部矿道。当前 FillLiquid(Point) 直接使用传入点;原来的 FillLiquid(Vector2) 也是使用 center.ToPoint()。此处应改为 FillLiquid(pos.ToPoint())。
Many many new contents.