# 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.` 建包:一个 `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.hurtMarked = true`。 ## 踩过的坑 **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`,`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.0+26.3-rc-1 —— 上游还没出 26.3 正式版构建,先用 rc-1 那版)**: - `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` 监听器按注册顺序执行