本文档为在现有代码库中注册新内容的快速参考。
如果你要创建一个独立的扩展模组(新 Gradle 项目),请先阅读 docs/创建模组.md。
如果你要新增或修改职业(角色),请先阅读 docs/角色开发指南.md(职业注册、组件、技能、事件、文案、商店速查)。
在开始开发前,确保项目可以正常编译和运行:
# 编译并打包(产物在 build/libs/starrailexpress.jar)
./gradlew build
# 启动测试客户端
./gradlew runClient
# 启动测试服务端
./gradlew runServer
# 清理构建缓存
./gradlew clean环境要求: JDK 21,并能访问
maven.fabricmc.net、maven.terraformersmc.com等 Maven 仓库。依赖已在本机缓存时,加
--offline更快(./gradlew compileJava --offline)。若GRADLE_USER_HOME=E:\gradle_home报Plugin [id: 'fabric-loom'] was not found,说明该目录的缓存不全 —— 用默认 gradle home(~/.gradle)即可。
动手前先看
docs/AI创建新职业攻略.md:可直接抄的角色/物品/商店/技能/网络包/GUI 模板、翻译键位置对照表与踩坑清单(核心原则:职业相关逻辑集中在SRERole子类与RoleData里)。
普通职业写在 org/agmas/noellesroles/role/ModRoles.java,彩蛋职业写在 org/agmas/noellesroles/role/bouns/BounsRoles.java(复杂职业单独建类放 role/bouns/roles/XxxRole.java)。
- 添加自定义角色id(
ResourceLocation,注意路径不能与已有角色重复,重复会抛IllegalArgumentException) - 注册公有静态角色:
TMMRoles.registerRole(new NormalRole(...)),可以在初始化函数中初始化或直接在声明时初始化 - 补翻译键:
announcement.star.role.<路径>/announcement.star.goals.<路径>/info.screen.roleid.<路径>(缺了游戏里会显示错误文本)
本模组使用 RoleData 接口及其实现类来管理玩家职业的持久化数据,写职业逻辑时不再需要自己碰 Cardinal Components API(CCA)。下文将介绍如何为您的职业创建数据类,并将其注册到职业系统中。
-
RoleData是玩家职业数据的顶层接口,定义了数据的生命周期(init、clear)、同步(writeToSyncNbt、readFromSyncNbt)以及客户端/服务端 Tick 回调。 -
SimpleRoleData是一个基础抽象实现,提供了 NBT 读写辅助方法,推荐直接继承该类以简化开发。 -
每个职业可以拥有独立的
RoleData实例,当玩家切换职业时,旧实例会被丢弃,新职业会创建新的数据对象。
您需要创建一个类,实现 RoleData 接口(或继承 SimpleRoleData),并实现所有抽象方法。
import io.wifi.starrailexpress.api.impl.SimpleRoleData;
import io.wifi.starrailexpress.api.data.RoleDataContext;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.core.HolderLookup;
public class YourRoleDataClass extends SimpleRoleData {
private int customValue;
public YourRoleDataClass(RoleDataContext context) {
super(context);
// 初始化自定义字段
this.customValue = 0;
}
@Override
public void writeToSyncNbt(CompoundTag tag, HolderLookup.Provider registryLookup) {
// 写入数据
tag.putInt("CustomValue", customValue);
}
@Override
public void readFromSyncNbt(CompoundTag tag, HolderLookup.Provider registryLookup) {
// 读取数据
this.customValue = tag.getInt("CustomValue");
}
// 可选:重写 init/clear/clientTick/serverTick
@Override
public void init() {
// 职业赋予时触发
}
@Override
public void clear() {
// 职业移除时触发
}
// 自定义业务方法
public int getCustomValue() {
return customValue;
}
public void setCustomValue(int value) {
this.customValue = value;
}
}注意:构造方法必须接受一个
RoleDataContext参数,用于获取玩家对象及其他上下文信息。
在定义职业(Role 实现类)时,通过 setRoleData 方法绑定数据工厂。该方法接受一个函数式接口,用于在玩家获得该职业时创建 RoleData 实例。
如果您的数据类构造方法与 RoleDataContext 匹配,可以直接使用方法引用:
role.setRoleData(YourRoleDataClass::new);如果您需要在创建时执行额外操作,可以使用 Lambda 表达式:
role.setRoleData((ctx) -> {
// 可以在这里根据上下文做额外处理
YourRoleDataClass data = new YourRoleDataClass(ctx);
data.setCustomValue(100); // 初始值
return data;
});说明:
setRoleData会在玩家被赋予该职业时调用,返回的RoleData实例将被关联到玩家的组件中。
在游戏逻辑中,您可以使用 RoleData 提供的静态工具方法获取当前玩家的职业数据。
// 返回 Optional<RoleData>
Optional<RoleData> optional = RoleData.getOptional(player);
optional.ifPresent(data -> {
// 处理数据
});// 返回 Optional<YourRoleDataClass>
Optional<YourRoleDataClass> opt = RoleData.getOptional(YourRoleDataClass.class, player);
opt.ifPresent(data -> {
int val = data.getCustomValue();
});若数据不存在或类型不匹配,会自动创建新实例(但不会自动绑定到玩家,需自行处理):
YourRoleDataClass data = RoleData.getOrCreate(YourRoleDataClass.class, player);
if (data != null) {
// 使用数据
}- 同步:
writeToSyncNbt和readFromSyncNbt用于客户端-服务端数据同步,系统会在合适时机调用。 - 不支持持久化
| 方法 | 触发时机 |
|---|---|
init() |
玩家被赋予该职业时(仅服务端) |
clear() |
玩家离开该职业时(仅服务端) |
clientTick() |
客户端每 tick 调用 |
serverTick() |
服务端每 tick 调用 |
您可以根据需要重写这些方法。
-
每个玩家同时只能拥有一个职业的
RoleData实例,切换职业时会自动替换。 -
数据类必须提供
RoleDataContext构造器,否则RoleData.create()会失败。 -
若使用
SimpleRoleData,您可以直接使用其提供的getXxxTag辅助方法简化 NBT 操作。 -
包名核对(写错直接编译失败):
RoleData=io.wifi.starrailexpress.api.data.RoleData;RoleDataContext=io.wifi.starrailexpress.api.data.RoleDataContext;SimpleRoleData=io.wifi.starrailexpress.api.impl.SimpleRoleData(不是api.data);NBT 辅助方法(getIntTag/getStringTag/getBooleanTag/getLongTag…)是RoleData接口上的 default 方法。
// 1. 定义数据类
public class WarriorData extends SimpleRoleData {
private int rage;
public WarriorData(RoleDataContext ctx) { super(ctx); }
@Override
public void writeToSyncNbt(CompoundTag tag, HolderLookup.Provider provider) {
tag.putInt("Rage", rage);
}
@Override
public void readFromSyncNbt(CompoundTag tag, HolderLookup.Provider provider) {
this.rage = tag.getInt("Rage");
}
public void addRage(int amount) { this.rage += amount; }
}
// 2. 在职业注册处绑定(角色类型是 io.wifi.starrailexpress.api.SRERole / NormalRole / EggRole)
SRERole warriorRole = TMMRoles.registerRole(new NormalRole(...));
warriorRole.setRoleData(WarriorData::new);
// 3. 在逻辑中获取
Optional<WarriorData> data = RoleData.getOptional(WarriorData.class, player);
data.ifPresent(d -> d.addRage(10));如果您之前使用了 Cardinal Components API,现在只需:
- 移除对
Component和ComponentKey的依赖。 - 将原有组件类改为实现
RoleData(或继承SimpleRoleData)。 - 使用
role.setRoleData(factory)替代原有的setComponentKey(KEY)。 - 使用
RoleData.getOptional(clazz, player)替代原有的KEY.get(player)。
如此即可平滑迁移。
- 玩家职业会变更,需要持续化的数据。
- 非特定职业玩家的数据。
- 保存到世界的全局数据。
创建物品Java类继承Item类并实现功能
在ModItems.java文件中添加公有静态物品常量对象并赋予id
创建实体类并注册,创建实体渲染器类并为实体注册
实体类创建java文件后在ModEntities.java件中进行注册
再在client.renderer中创建EntityRender.java渲染器类用于客户端渲染,
并在 org.agmas.noellesroles.client.RicesRoleRhapsodyClient 的 registerEntityRenderers(...) 里注册渲染器
(NoellesrolesClient.onInitializeClient 会调用它;在 NoellesrolesClient 里搜不到这个方法名是正常的)。
集中在 org/agmas/noellesroles/init/RoleShopHandler.java(由 Noellesroles 初始化时调用 RoleShopHandler.shopRegister()):
- 在
shopRegister()里声明并填充ArrayList<ShopEntry>(或专用的initializeXxxShop()方法) - 用
ShopContent.customEntries.put(roleId, 列表)为角色注册商店;更推荐直接在职业类里覆写SRERole.getShopEntries()(逻辑跟着职业走,见docs/AI创建新职业攻略.md§5)
在packet下创建___C2Packets.java(或S2C)类继承CustomPacketPayload作为网络包包含:
- 网络包的唯一标识符ResourcesLocatiom
- 网络包类型标识符
- 序列化/反序列化编解码
- 定义编解码器写入读取方法
- 需要传输的内容等
对于C2S网络包
- 在模组初始化调用registerPackets函数中注册网络包
- 并且对该网络包进行处理
对于S2C网络包
- 在
PayloadTypeRegistry.playS2C().register(ID, CODEC)里注册;统一入口是io.wifi.starrailexpress.register.PayloadBootstrap.registerAll()(它会依次调用SREPayloadRegister/ModPackets/RicePacketTypeRegister等),所以通常写进org.agmas.noellesroles.init.ModPackets#registerPackets()就够了 - 在客户端用
ClientPlayNetworking.registerGlobalReceiver(ID, (payload, ctx) -> ctx.client().execute(() -> ...))处理(位置:client/NoellesrolesClient.onInitializeClient)
client下创建___Screen.java类继承Screen作为新GUI
重写init函数对GUI进行初始化:注意布局以及添加到渲染列表中
- 在mixin文件夹内创建java类,使用
@Mixin注解进行混合 - 在
src/main/resources/noellesroles.mixins.json(noellesroles 侧;主模组是starrailexpress.mixins.json)中添加混合配置
在en_us.json 和 zh_cn.json中添加注册的id对应的汉化(根据已有汉化即可)
完整位置对照(职业文案是重点,缺了会显示错误文本):职业名/目标在
assets/noellesroles/lang/{zh_cn,zh_tw,en_us}.json(键announcement.star.role.<路径>/announcement.star.goals.<路径>,<路径>是identifier().getPath()而不是完整 id);职业详细介绍在assets/role_modifier_intro/lang/*(info.screen.roleid.<路径>);物品名/<id>.tooltip在assets/noellesroles/lang/*;物品介绍item.<命名空间>.<id>.desc在assets/item_intro/lang/*。