- 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>
8.5 KiB
8.5 KiB
sfcraft Agent 工作指南
Fabric 纯服务端 mod,Minecraft 26.3,Java 25,官方 Mojang 映射,自定义内容经 Polymer 伪装成原版内容。整体结构见 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):
./gradlew compileJava --console=plain # 编译(mixin AP 会校验注入目标)
./gradlew runServer --console=plain # dev 服务器(run/ 目录,EULA 已接受)
查 26.3 真实签名(解决 cannot find symbol 的最快办法):
javap -cp ~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.3/minecraft-merged-deobf-26.3.jar <类全名>
新增/修改玩法特性的套路
- 在
module.game.<feature>建包:一个ServerModule子类 + 该特性的方块/物品/实体类。 - 注册表内容在模块
onInitialize()里用RegistryHelper注册(此阶段注册表未冻结、MinecraftServer不可用),实例存模块字段(@Getter)。不要写public static void register()或静态注册表持有类。 - 依赖一律
@Inject(如GameConfigService、CauldronRecipeRegistry);方块实体/实体拿不到注入,由模块在注册时用工厂闭包把依赖塞进构造器(参考CauldronModule)。 - mixin 放
mixin.gameplay,保持轻薄:逻辑写在模块的公开方法里,mixin 用SFCraft.getInjector().getInstance(XxxModule.class)一行委托(参考MobCommanderMixin);高频调用路径可参考MountLogic的惰性缓存。能用 Fabric API 事件或SFCallbacks回调就不要写 mixin。 - 需要注册模块时在
SFCraftInitializer#registerGameFeatures加一行(⚠️ 越界,需许可)。GameExtensionModule必须保持在所有 game 模块之前——它加载 gameplay.json,后续模块注册物品时会读配置。
另外,如果你注册了新的物品,确保通过依赖注入把他注册到 ItemGroupService 中。
如何区分 gameplay 和一般特性:
-
通常来说,和游戏中的内容(物品,方块,实体)直接挂钩的是 gameplay. 而类似聊天,权限以及主要是辅助工具性质的不属于 gameplay
-
gameplay 通常直接影响游戏逻辑,如果修改并不能让玩家体验到新的世界交互形式,那么不属于 gameplay
-
编译验证,必要时 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()照旧 protectedLivingEntity.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监听器按注册顺序执行