NoteTypeDefinition
这一页是签名与行为参考。从谱面解析到判定、渲染的完整回调面。直接做一遍:教程 · 添加新音符类型。
音符类型定义对象,是模组扩展音符的唯一入口:在 onPreInit 用 ctx.registries().noteTypes().register(key, def) 注册,身份由注册键承载,本接口不自带 id。谱面 JSON 的 type 字段按 serializationKey() 精确匹配(不带命名空间前缀),未命中时该音符回退到宿主的旧式类型比较逻辑。注册期会校验:serializationKey() 为空/空白、或与其它已注册类型(含内置五个)重复,均抛 IllegalArgumentException。相关类型:NoteData、NoteRenderContext、TimeoutStrategy。
方法速览
| 方法 | 用途 |
|---|---|
displayName() | 人类可读显示名 |
serializationKey() | 谱面 JSON 的 type 匹配键 |
deserialize(JsonObject, NoteParseContext) | 解析期:从音符 JSON 创建运行时音符 |
onCreateRuntime(NoteData) | 解析期:补充运行时字段(关联查找/随机值等) |
onJudge(JudgeInput, JudgeSink) | 判定等级修饰钩子(不决定是否命中) |
overridesJudgment() | 是否完全接管本类型判定 |
onJudgeTick(long, JudgeInputState, JudgeContext, JudgeSink) | 接管类型的每帧自主判定 |
timeoutStrategy() | 接管类型的超时兜底策略 |
modHandledTimeoutCapMs() | MOD_HANDLED 的兜底宽限上限(毫秒) |
render(RenderCanvas, NoteRenderContext) | 每帧渲染回调 |
defaultImage() | 皮肤无专属槽位时的模组兜底图(实验性) |
onJudged(NoteData, JudgeResult) | 音符被判定后的生命周期回调 |
onRemoved(NoteData) | 音符完全判定(含 MISS)后的收尾回调 |
displayName()
签名:
String displayName();行为:返回显示名称(如 "长按"、"星尘"),仅用于界面与日志;不参与谱面匹配,也不做非空校验。宿主每次需要时直接调用,实现应恒定返回同一值(不要做重计算)。
示例:
@Override
public String displayName() {
return "星尘";
}serializationKey()
签名:
String serializationKey();行为:返回谱面 JSON 中该音符的 type 字符串(如 "stardust")。注册期即被调用并建立全局索引:空/空白或与已有类型重复会抛 IllegalArgumentException,因此不要在注册之后才改变返回值。与 NoteData.typeKey() 对应。
示例:
@Override
public String serializationKey() {
return "stardust";
}deserialize(JsonObject json, NoteParseContext ctx)
签名:
NoteData deserialize(JsonObject json, NoteParseContext ctx);行为:谱面加载时对所有注册类型(内置与模组)统一派发。json 是该音符的完整 JSON 对象(time / lane / endTime 及自定义字段),ctx 提供轨道数、BPM 与内容缩放。返回的 NoteData 实例被宿主作为该音符的运行时对象保存(内置类型返回 null 表示"无运行时补充")。
边界与所有权:
- 返回
null时宿主跳过onCreateRuntime,判定/渲染仍会回调本定义对象。 - 反向映射按 JSON 数组下标与已解析音符一一对应;原始 JSON 解析失败或长度不一致时整批跳过
deserialize(限频日志),不会错位派发。 - 本回调抛异常由宿主捕获并记限频日志,该音符回退内置解析。
- 你在返回对象上写入的
time/lane不进入宿主管线:宿主的判定、渲染与轨道定位读的是谱面 JSON 解析出的内核音符值(见 NoteData 的写语义)。 ctx只在本回调与随后的onCreateRuntime期间有效,不要保存引用。
示例:
@Override
public NoteData deserialize(JsonObject json, NoteParseContext ctx) {
StardustNote note = new StardustNote();
note.setTime(json.get("time").getAsLong());
note.setLane(json.get("lane").getAsInt());
if (json.has("endTime") && !json.get("endTime").isJsonNull()) {
note.setEndTime(json.get("endTime").getAsLong());
}
note.setTypeKey(serializationKey());
return note;
}onCreateRuntime(NoteData note)
签名:
default void onCreateRuntime(NoteData note);行为:仅在 deserialize 返回非 null 后立即调用一次,用于补全运行时字段(关联查找、默认值、随机角度等)。note 就是 deserialize 返回的同一实例。默认空实现;抛异常由宿主捕获(限频日志)并跳过,音符仍会进入游玩。
示例:
@Override
public void onCreateRuntime(NoteData note) {
// 每个音符一份运行时状态,render 与 onJudgeTick 经 extensionData 取回同一实例
note.setExtensionData(new AimState(Math.toRadians(nextAngleDeg())));
}onJudge(JudgeInput input, JudgeSink sink)
签名:
void onJudge(JudgeInput input, JudgeSink sink);行为:宿主为一次判定构造结果时调用(按键命中、长按终点、超时 MISS、屏障摧毁等所有判定路径),用于声明该次判定使用哪个判定等级。
契约:
- 宿主只取本回调中第一次非
null的kind作为判定等级;sink.accept(kind, judgeTime)的judgeTime与三参版本的note参数在此路径被忽略。 - 本回调不决定是否命中、也不改变判定时间——命中/MISS 与时间由宿主判定引擎决定;不产出
kind时使用内置等级。 - 完全接管(
overridesJudgment() == true)的类型不参与按键候选匹配,但宿主兜底 MISS 仍会经过此回调;因此不要在这里做计分、播放音效等副作用,否则会与onJudgeTick的产出重复。 - 抛异常由宿主捕获(限频日志),回退内置判定等级,不影响游玩。
示例:
@Override
public void onJudge(JudgeInput input, JudgeSink sink) {
if (Math.abs(input.deltaMs()) <= 30) {
// 只是把这次判定的等级换成自定义等级;是否命中仍由宿主决定
sink.accept(myPerfectKind, input.judgeTime());
}
}overridesJudgment()
签名:
default boolean overridesJudgment();行为:返回 true 时声明完全接管本类型音符的判定:宿主不再把该类型音符放进按键候选匹配(普通/Hold 与无序路径都会跳过),改为每帧调用 onJudgeTick。默认 false。
边界:
- 超时兜底 MISS 仍由宿主负责:普通音符按
time + missWindowMs,长按语义按endTime + missWindowMs;接管者可用timeoutStrategy()申请宽限。 - 声明在注册后应保持恒定(宿主判定每帧读取,不缓存)。
示例:
@Override
public boolean overridesJudgment() {
return true;
}onJudgeTick(long timeMs, JudgeInputState state, JudgeContext ctx, JudgeSink sink)
签名:
default void onJudgeTick(long timeMs, JudgeInputState state, JudgeContext ctx, JudgeSink sink);行为:接管类型的每帧判定回调。调用时机固定在输入处理之后、超时兜底 MISS 之前(预览会话同样调用,此时输入快照为空)。宿主先做无分配守卫:本类型没有待判定音符时,本帧不构造快照、不回调。
产出与消费:
- 必须使用三参
sink.accept(kind, judgeTime, note);note必须是ctx.notes()返回的同一实例(宿主按引用身份定位音符),否则该条判定被丢弃。kind为null或被丢弃。 - 一次回调可以对多个音符分别产出;每条产出逐条消费:标记该音符已判定(长按语义只标记起点,终点仍由宿主长按路径结算)→ 统一计分 / 连击 / 连击中断 / 特效路径。打击音效需要模组自行播放(
AudioAccess能力)。 - 二参
sink.accept(kind, judgeTime)在此路径不生效(无法定位音符,被宿主忽略)。 judgeTime由你给出,建议传timeMs;它会计入准度分布。- 未产出的音符在之后的 MISS 检查中由宿主兜底(受
timeoutStrategy()影响)。 - 回调抛异常被宿主捕获(限频日志);异常之前已
accept的结果仍会被消费。 - 判定等级到内置枚举的映射(供依赖枚举的宿主逻辑):
seecmd:perfect/seecmd:great/seecmd:miss精确映射;其它等级按comboBehavior() == RESET映射为 MISS,否则 GREAT。 state/ctx/sink仅在本次回调内有效,不要跨帧持有。
示例:
@Override
public void onJudgeTick(long timeMs, JudgeInputState state, JudgeContext ctx, JudgeSink sink) {
List<NoteView> notes = ctx.notes(); // 每帧只取一次
for (Integer key : state.pressedKeys()) { // 本帧新按下的物理键
int lane = state.keyToLane(key);
if (lane < 0) {
continue;
}
for (NoteView note : notes) {
if (note.lane() == lane && Math.abs(timeMs - note.time()) <= 80) {
sink.accept(myPerfectKind, timeMs, note); // 三参版本才对指定音符生效
break;
}
}
}
}timeoutStrategy()
签名:
default TimeoutStrategy timeoutStrategy();行为:声明"超时后谁来兜底",默认 TimeoutStrategy.HOST_FALLBACK(超期即按普通期限补 MISS)。返回 MOD_HANDLED 时,宿主把兜底期限改为「普通期限 + modHandledTimeoutCapMs()」,宽限期内不强制 MISS,由接管者自行产出判定或保留音符。
边界:
- 仅对
overridesJudgment() == true的类型消费;非接管类型声明MOD_HANDLED不生效。 - 返回
null视为HOST_FALLBACK;读取抛异常按"不延长"处理(限频日志)。 - 宽限只作用于「尚未判定起点」的普通期限分支;长按起点已判后的终点期限(
endTime + missWindowMs)不叠加宽限;屏障与无序模式的路径不读取本声明。
示例:
@Override
public TimeoutStrategy timeoutStrategy() {
// 需要把音符保留到自定义条件满足时再判,声明由模组处理超时
return TimeoutStrategy.MOD_HANDLED;
}modHandledTimeoutCapMs()
签名:
default long modHandledTimeoutCapMs();行为:MOD_HANDLED 的兜底宽限上限(毫秒),默认 30000。宿主兜底期限 = 音符 time + 判定窗口 missWindowMs + 本值。≤ 0 视为不延长(等价 HOST_FALLBACK 期限);读取抛异常按 0 处理。仅当 overridesJudgment() 与 timeoutStrategy() == MOD_HANDLED 同时成立时被消费。
示例:
@Override
public long modHandledTimeoutCapMs() {
return 8_000L; // 最多多等 8 秒,之后宿主强制兜底 MISS
}render(RenderCanvas canvas, NoteRenderContext ctx)
签名:
void render(RenderCanvas canvas, NoteRenderContext ctx);行为:每个可见音符每帧调用一次,canvas 是模组唯一的绘制入口。
- 对模组自定义类型,回调前宿主已施加轨道变换(轨道透明度 / 旋转 / 缩放 / 位置式裁剪)与音符级变换(复合
L·N,与内置音符同层序),因此默认跟随轨道几何即可;轨道全默认时像素与旧行为一致。 ctx.x()/ctx.y()/ctx.judgeLineY()仍是未叠加轨道变换的布局(世界)坐标:真轨原点即世界原点,所以这些标量与画布缝坐标系一致。需要偏离轨道几何(如无视轨道旋转固定屏幕朝向)可经GameStateAccess查询圆心/变换后自行反向叠加。- 内置类型的
render只把绘制委托回宿主分支链(单次施加轨道 + 音符变换),模组不需要也不应模仿这条委托。 - 回调抛异常由宿主捕获(限频日志),随后按「皮肤专属槽位图 →
defaultImage()→ 内置行为分类」回退绘制。 - 宿主在调用本回调前后各做一次画布
save/restore,但模组内部的save/restore仍须自行配平。
示例:
@Override
public void render(RenderCanvas canvas, NoteRenderContext ctx) {
double size = Math.min(ctx.width(), 48);
canvas.save();
canvas.setColor(new Color(160, 120, 255));
canvas.fillRect(ctx.x() - size / 2, ctx.y() - size / 2, size, size);
canvas.restore();
}defaultImage()
签名:
default RenderImage defaultImage();行为(源码标注为实验性):当当前皮肤没有为本类型提供专属槽位(图片皮肤目录内的 <类型key>.png)时,宿主用本图绘制音符(宽度铺满轨道、高度按图片比例),优先级高于行为分类固有槽位回退。玩家皮肤提供专属槽位时玩家皮肤优先;返回 null(默认)表示不提供。
注意实现事实:宿主在每次渲染自定义音符前都会解析该图(皮肤槽位缺失时即调用本方法),并把它同时作为 ctx.skinImage() 传入 render——即使你的 render 随后自绘成功,本方法也已经被调用过。因此实现必须缓存(在 onInit 加载一次),不要在这里读文件/生成像素。
示例:
private RenderImage defaultImg; // onInit 中经 ctx.resources().loadImage(...) 预加载并缓存
@Override
public RenderImage defaultImage() {
return defaultImg; // 无图时返回 null,宿主落到内置分类渲染
}onJudged(NoteData note, JudgeResult result)
签名:
default void onJudged(NoteData note, JudgeResult result);行为:一次判定被宿主消费后调用,note 为该音符的运行时对象(内置类型为只读适配器),result 为最终判定结果(含判定等级、判定时间与 isHoldEnd())。长按起点与终点各触发一次;MISS 同样触发。默认空实现;异常被捕获并记限频日志。预览会话的静默事件模式下不投递本回调。
示例:
@Override
public void onJudged(NoteData note, JudgeResult result) {
if (result.isHoldEnd()) {
lastHoldEndMs = result.judgeTime();
}
}onRemoved(NoteData note)
签名:
default void onRemoved(NoteData note);行为:音符完全判定(命中、超时 MISS 都算)并经计分路径消费后调用一次。长按起点的判定(含起点超时 MISS)不触发——宿主按"非长按终点且为长按音符"识别起点;长按终点、普通音符的任何判定结局才触发。宿主按音符记录"已通知"标志,保证每音符至多一次。默认空实现;异常被捕获并记限频日志。
示例:
@Override
public void onRemoved(NoteData note) {
counts.remove(note.time() + ":" + note.lane()); // 清理本音符的连按计数
}完整签名
public interface NoteTypeDefinition {
String displayName();
String serializationKey();
NoteData deserialize(JsonObject json, NoteParseContext ctx);
default void onCreateRuntime(NoteData note);
void onJudge(JudgeInput input, JudgeSink sink);
default boolean overridesJudgment();
default void onJudgeTick(long timeMs, JudgeInputState state, JudgeContext ctx, JudgeSink sink);
default TimeoutStrategy timeoutStrategy();
default long modHandledTimeoutCapMs();
void render(RenderCanvas canvas, NoteRenderContext ctx);
default RenderImage defaultImage();
default void onJudged(NoteData note, JudgeResult result);
default void onRemoved(NoteData note);
}线程:源码未声明额外线程约束。宿主在谱面解析线程(deserialize / onCreateRuntime)与游玩更新/渲染线程(判定与渲染回调)同步调用,回调内不得阻塞。
生命周期阶段:onPreInit 注册;解析期回调 deserialize / onCreateRuntime,游玩期回调判定、渲染与生命周期钩子。
稳定性:1.x 内冻结承诺自 2.0 起对本接口兑现一次破坏:isHold() / isBarrier() / isExploit() / isPayload() 四个布尔旗标已删除,改为语义标记子接口(见 语义标记子接口);2.0 内恢复只增不破。timeoutStrategy() / modHandledTimeoutCapMs() 为 新增(只增不破);defaultImage() 在源码中标注为实验性。