Files
sfcraft/AGENTS.md
T
iceBear67andClaude Opus 5 2b70971b80 port to Minecraft 26.3
Source changes for 26.3 API breakage:
- block codec system removed upstream (Block has no MapCodec/codec()/
  simpleCodec anymore): drop CODEC + codec() from AmethystCauldronBlock
  and GravityCrystalBlock
- Entity.hurtMarked -> Entity.syncVelocity (pure rename, still public)
- LivingEntity.drop() now takes a Prediction; our drops are server-
  initiated and unpredicted, so they pass SERVER_ONLY
- canSimulateMovement() became final; the ravager/hoglin mount mixins
  now override getMoveSimulationType() and return
  AUTHORITATIVE_SIDE_AND_SERVER, which is the same "server always
  simulates" semantics the old !isClientSide branch had
- loot number providers split into providers.number.{ints,floats} and
  became Holder-based: UniformGenerator.between(1f,3f) ->
  ContextIntProviders.between(1,3)
- BonemealableBlock methods take a BonemealSource (INTERACTION here)
- clockManager().getTotalTicks(c) ->
  clockManager().getInstance(c).totalTicks()
- EnderMan -> Enderman (capitalisation only)

Build: MC 26.3, loader 0.19.5, fabric-api 0.160.5+26.3, loom 1.18.1,
gradle wrapper 9.6.0 -> 9.7.1 (loom 1.18.x requires Gradle >= 9.7).
Polymer is 0.18.0+26.3-rc-1: upstream has not published a 26.3-final
build yet, that rc build is the only one on the 26.3 line.

server-deploy: pack.toml -> 26.3 / loader 0.19.5; fabric-api, polymer
and simple-voice-chat are the only pack mods with 26.3 builds and were
updated (index hashes regenerated). The remaining third-party mods have
no 26.3 release yet and are deliberately left untouched. Dockerfile now
pins the justbackup and fabric-carpet forks to their 26.3 branches
instead of silently building whatever the default branch holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 15:54:54 +00:00

8.6 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 <类全名>

新增/修改玩法特性的套路

  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

  3. 编译验证,必要时 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<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.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 监听器按注册顺序执行