- polymer 0.18.0+26.3-rc-1 -> 0.18.2+26.3 (first 26.3 final build) - fabric-api 0.160.5 -> 0.161.0, loom 1.18.1 -> 1.18.2 - pack: move squaremap, syncmatica, farmers-delight (+polymer patch), jei, worldedit, lithium to their 26.3 builds; bump fabric-api, polymer - lithium now resolved from Modrinth instead of CurseForge (same mod, same author) - docs: fix stale hurtMarked reference, drop carpet snapshot warning Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
102 lines
8.5 KiB
Markdown
102 lines
8.5 KiB
Markdown
# sfcraft Agent 工作指南
|
|
|
|
Fabric **纯服务端** mod,Minecraft **26.3**,Java 25,官方 Mojang 映射,自定义内容经 [Polymer](https://polymer.pb4.eu) 伪装成原版内容。整体结构见 [ARCHITECTURE.md](./ARCHITECTURE.md),先读它再动代码。
|
|
|
|
## ⚠️ 工作范围(最高优先级规则)
|
|
|
|
Agent 的主要工作范围**只有**这两个包:
|
|
|
|
- `io.ib67.sfcraft.module.game` — 游戏玩法模块
|
|
- `io.ib67.sfcraft.mixin.gameplay` — 玩法相关 mixin
|
|
|
|
**其它任何位置在修改之前必须先向用户说明并获得明确许可**,包括但不限于:
|
|
|
|
- `io.ib67.sfcraft` 其余包(基础设施、DI、callback、registry、其他 module、其他 mixin)
|
|
- `SFCraftInitializer`(即使只是给新模块加一行 `registerFeature`)
|
|
- `sfcraft.mixins.json`(即使只是加一条 gameplay mixin 条目)
|
|
- `src/main/resources/` 下的 assets/data(lang、配方、战利品表、tag…)
|
|
- 构建脚本、accesswidener、fabric.mod.json、文档
|
|
|
|
新特性通常必然涉及 `registerFeature` 注册行、mixins.json 条目、lang 键、配方 JSON 这些"例行越界"——请在给出方案时一并列出这些改动点,**打包征得一次同意**后再动手,不要做完了才说。
|
|
|
|
## 工具使用
|
|
|
|
- 探索、读写代码**优先使用 IDE 的 MCP 工具**:`list_directory_tree`(代替 `ls`/`find`)、`read_file`(代替 `cat`/`sed`)、`search_text`/`search_regex`/`search_symbol`(代替 `rg`/`grep`)、`get_symbol_info`、`analyze_calls`;编辑用 `apply_patch`/`create_new_file`。
|
|
- 查 26.3 原版类签名**优先用 `read_file` 直接读 loom 缓存 jar 内的类**(会反编译,可读上下文):`~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.3/minecraft-merged-deobf-26.3.jar!/net/minecraft/.../SomeClass.class`,`javap` 仅作回退。
|
|
- shell 只留给 MCP 做不了的事:gradle 编译/运行、git、文件权限操作等。
|
|
|
|
## 构建与验证
|
|
|
|
仓库自带 gradlew wrapper(**9.7.1** —— loom 1.18.x 要求 Gradle ≥ 9.7,Dockerfile 也走 `./gradlew`):
|
|
|
|
```bash
|
|
./gradlew compileJava --console=plain # 编译(mixin AP 会校验注入目标)
|
|
./gradlew runServer --console=plain # dev 服务器(run/ 目录,EULA 已接受)
|
|
```
|
|
|
|
查 26.3 真实签名(解决 cannot find symbol 的最快办法):
|
|
|
|
```bash
|
|
javap -cp ~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.3/minecraft-merged-deobf-26.3.jar <类全名>
|
|
```
|
|
|
|
## 新增/修改玩法特性的套路
|
|
|
|
1. 在 `module.game.<feature>` 建包:一个 `ServerModule` 子类 + 该特性的方块/物品/实体类。
|
|
2. 注册表内容在模块 `onInitialize()` 里用 `RegistryHelper` 注册(此阶段注册表未冻结、`MinecraftServer` 不可用),实例存模块字段(`@Getter`)。**不要写 `public static void register()` 或静态注册表持有类。**
|
|
3. 依赖一律 `@Inject`(如 `GameConfigService`、`CauldronRecipeRegistry`);方块实体/实体拿不到注入,由模块在注册时用工厂闭包把依赖塞进构造器(参考 `CauldronModule`)。
|
|
4. mixin 放 `mixin.gameplay`,保持轻薄:逻辑写在模块的公开方法里,mixin 用 `SFCraft.getInjector().getInstance(XxxModule.class)` 一行委托(参考 `MobCommanderMixin`);高频调用路径可参考 `MountLogic` 的惰性缓存。能用 Fabric API 事件或 `SFCallbacks` 回调就不要写 mixin。
|
|
5. 需要注册模块时在 `SFCraftInitializer#registerGameFeatures` 加一行(⚠️ 越界,需许可)。**`GameExtensionModule` 必须保持在所有 game 模块之前**——它加载 gameplay.json,后续模块注册物品时会读配置。
|
|
|
|
另外,如果你注册了新的物品,确保通过依赖注入把他注册到 ItemGroupService 中。
|
|
|
|
如何区分 gameplay 和一般特性:
|
|
1. 通常来说,和游戏中的内容(物品,方块,实体)直接挂钩的是 gameplay. 而类似聊天,权限以及主要是辅助工具性质的不属于 gameplay
|
|
2. gameplay 通常直接影响游戏逻辑,如果修改并不能让玩家体验到新的世界交互形式,那么不属于 gameplay
|
|
|
|
6. 编译验证,必要时 runServer 冒烟。
|
|
|
|
## 编码约定
|
|
|
|
- **配置**:数值参数进 `GameConfig`(`io.ib67.sfcraft.config`)对应特性的嵌套类,运行期通过注入的 `GameConfigService#get()` **每次现读,不要缓存**(否则 `/sfcraft reload` 不生效)。注册时烘焙进组件的值(如耐久)天然不可热重载,属已知限制。需要在重载时重建的东西(如炼药锅配方)监听 `callback.SFConfigReload`。
|
|
- **i18n**:玩家可见文本一律 `Component.translatable`,键加进 `assets/sfcraft/lang/zh_cn.json` **和** `en_us.json`(⚠️ 越界,需许可)。**禁止**硬编码中文/英文 literal。不要引入 server-translations 依赖——玩家装服务器材质包,lang 由客户端解析。物品显示名 = `ITEM_NAME` 组件挂 translatable。
|
|
- **纯服务端**:假设代码总在服务端运行;玩家移动是客户端权威,服务端改玩家速度矢量要置 `entity.syncVelocity = true`(26.2 及以前叫 `hurtMarked`)。
|
|
|
|
## 踩过的坑
|
|
|
|
**26.3 变更**(相对 26.2,本次移植踩到的;写代码前先 javap 确认):
|
|
|
|
- **方块 codec 体系整个删了**:`Block` 不再有 `MapCodec CODEC` / `codec()`,`simpleCodec()` 也没了 —— 自定义方块直接删掉这两段即可(原版 `LayeredCauldronBlock` 同样已无 codec)
|
|
- `Entity.hurtMarked` 字段 → **`Entity.syncVelocity`**(仍是 public 字段,纯改名);`markHurt()` 照旧 protected
|
|
- `LivingEntity.drop(ItemStack, boolean)` → **`drop(ItemStack, boolean, Prediction)`**(`net.minecraft.util.Prediction`,枚举 `PREDICTED`/`SERVER_ONLY`)。服务端逻辑自己发起的掉落用 `SERVER_ONLY`,客户端没预测过
|
|
- `Entity.canSimulateMovement()` 变 **final**,改写点移到 **`getMoveSimulationType()`**(返回 `MoveSimulationType`,默认 `AUTHORITATIVE_SIDE`)。想让服务端始终模拟(纯服务端坐骑)返回 `AUTHORITATIVE_SIDE_AND_SERVER`
|
|
- 战利品数字提供器拆成 `providers.number.ints` / `.floats` 两个子包,且全部 `Holder` 化:`UniformGenerator.between(1.0F, 3.0F)` → **`ContextIntProviders.between(1, 3)`**(返回 `Holder<ContextIntProvider>`,`SetItemCountFunction.setCount` 现在收的就是它)
|
|
- `BonemealableBlock` 三个方法都多了一个 **`BonemealSource`** 参数(`INTERACTION`/`MOB`)
|
|
- `MinecraftServer.clockManager()` 的 `getTotalTicks(clock)` 没了 → **`clockManager().getInstance(clock).totalTicks()`**(`ClockInstance` 接口:`totalTicks/partialTick/rate/isPaused`);`addTicks`/`setTotalTicks` 等写操作仍在 manager 上
|
|
- `EnderMan` → **`Enderman`**(只是大小写,包没动)
|
|
|
|
**26.2 Mojmap 重命名**(相对 1.21.x,写代码前先 javap 确认):
|
|
|
|
- `ResourceLocation` → `net.minecraft.resources.Identifier`(`Identifier.fromNamespaceAndPath`)
|
|
- EntityType 常量在 `net.minecraft.world.entity.EntityTypes`(`EntityTypes.SNOWBALL`)
|
|
- `Level.isClientSide` 字段私有化 → 用 `isClientSide()`;`Level.random` → `getRandom()`
|
|
- BE 存档:`saveAdditional(ValueOutput)` / `loadAdditional(ValueInput)`,列表用 `out.list(name, Codec)` / `in.listOrEmpty(...)`
|
|
- `BlockBehaviour.onRemove` 已删 → 覆写 `BlockEntity.preRemoveSideEffects`
|
|
- 类移包:`Zombie` → `entity.monster.zombie`,`ThrowableItemProjectile` → `entity.projectile.throwableitemprojectile`,`VillagerProfession` → `entity.npc.villager`
|
|
- `MobEffects.SLOWNESS`(不是 MOVEMENT_SLOWDOWN);`SimpleContainer` 没有 addListener 了(覆写 `setChanged()` 持久化)
|
|
- `GameProfile` 是 record(`name()`/`id()`);`sendOverlayMessage`/`sendSystemMessage` 取代 `displayClientMessage`;命令权限 `Commands.LEVEL_GAMEMASTERS.check(source.permissions())`
|
|
|
|
**Polymer(0.18.2+26.3)**:
|
|
|
|
- `PacketContext` 在 `net.fabricmc.fabric.api.networking.v1.context.PacketContext`
|
|
- BE 类型必须 `PolymerBlockUtils.registerBlockEntity`,实体类型必须 `PolymerEntityUtils.registerType`(`RegistryHelper` 已封装)
|
|
- 伪装物品尽量选**非方块、无自定义 use 行为**的原版物品:方块物品有放置鬼影;有 use 覆写的物品会吃掉客户端 consumable 预测(进食动画不显示)
|
|
- 要让客户端拿到额外组件(如 `food`/`consumable`),覆写 `getPolymerItemStack` 显式复制
|
|
- 自定义 MobEffect 必须实现 `PolymerMobEffect` 并返回原版替身(静态非同步注册表)
|
|
|
|
**加载时序**:
|
|
|
|
- 初始数据包加载**早于物品组件初始化**,自定义 Codec 里不能用 `ItemStack.CODEC`(报 "Item ... does not have components yet")——只存物品 id(`BuiltInRegistries.ITEM.holderByNameCodec()`),用时再 `new ItemStack(holder)`
|
|
- 注册必须在 `onInitialize()`(ModInit);`onEnable` 阶段注册表已冻结
|
|
- 模块按 `registerFeature` 顺序初始化;`SFConfigReload` 监听器按注册顺序执行
|