Theseus 模组文档
API 参考 / registry 包:注册表与资源标识

Registry


想先跑起来?

这一页是签名与行为参考。注册表的通用读写语义,冻结规则见 onPreInit。直接做一遍:教程 · 第一个模组。

冻结式注册表:只读视图 + 启动期一次性写入。模组在注册窗口(onPreInit)内通过 ctx.registries() 拿到各领域注册表的实例并写入自有内容;宿主在全部模组的 onPreInit 结束后立即冻结所有注册表,运行期只做只读查询。同一个接口服务全部内容类型(音符类型、游戏模式、渲染滤镜、收集品……),所以注册写法永远只有一种:register(key, value)。

适用场景:注册自有内容(onPreInit 内);在运行期按键/按资源标识查询已注册内容(get / contains / keys)。

方法速览

方法用途
register(key, value)注册一项内容,返回传入的键
get(key)按注册键查询(带类型收敛)
get(location)按资源标识查询
contains(location)该资源标识是否已注册
keys()全部已注册标识(不可变视图,注册顺序)
keysByNamespace(namespace)按来源命名空间过滤的只读查询
isFrozen()注册表是否已冻结

完整签名

java
public interface Registry<T> {

    <V extends T> RegistryKey<V> register(RegistryKey<V> key, V value);

    <V extends T> Optional<V> get(RegistryKey<V> key);

    Optional<T> get(ResourceLocation location);

    boolean contains(ResourceLocation location);

    Set<ResourceLocation> keys();

    default Set<ResourceLocation> keysByNamespace(String namespace);

    boolean isFrozen();
}

register(key, value)

签名:

java
<V extends T> RegistryKey<V> register(RegistryKey<V> key, V value);

行为:写入一项内容并返回传入的键(便于链式书写)。校验顺序以宿主实现为准:

  1. key 为 null → IllegalArgumentException;
  2. value 为 null → IllegalArgumentException;
  3. key.type() 必须是本注册表元素类型 T 的子类型,否则 IllegalArgumentException(编译期泛型拦不住时由运行期兜底);
  4. value 必须是 key.type() 的实例,否则 IllegalArgumentException(防止「键声明 NoteTypeDefinition、值却是别的类型」);
  5. 注册表已冻结 → RegistryFrozenException;
  6. 同一 ResourceLocation 已注册 → IllegalArgumentException(明确失败,不静默忽略)。

注意判重口径是资源标识而不是 RegistryKey.equals:同一 location 用不同 type 再注册一次同样算重复。

经 ModContext.registries() 取得的注册表是 per-mod 视图,另有前置校验:注册键的命名空间必须等于本模组 id,否则 IllegalArgumentException(归属错配会让宿主把内容记到别的模组名下、本模组收不到回调,因此注册期就拒绝)。该归属校验发生在冻结检查之前。

特殊注册表的额外约束:noteTypes() 还要求 NoteTypeDefinition.serializationKey() 非空白且全局唯一,冲突会抛 IllegalArgumentException(谱面 JSON 的 "type" 用它选类型,两者共用同一 key 将无法区分)。

示例:

java
@Override
public void onPreInit(ModContext ctx) {
    RegistryKey<RenderFilter> key =
            RegistryKey.of("mymod", "bloom", RenderFilter.class);
    ctx.registries().renderFilters().register(key, new BloomFilter());
}

get(key)

签名:

java
<V extends T> Optional<V> get(RegistryKey<V> key);

行为:按 key.location() 查表,再用 key.type() 做一次类型收敛:未注册、key 为 null、或已注册值不是 key.type() 的实例时返回空 Optional(不抛异常)。key.type() 比实际注册类型更具体时会得到空结果,而不是 ClassCastException。

示例:

java
Optional<NoteTypeDefinition> normal = ctx.registries().noteTypes()
        .get(RegistryKey.of("seecmd", "normal", NoteTypeDefinition.class));
if (normal.isPresent()) {
    String ser = normal.get().serializationKey();
}

get(location)

签名:

java
Optional<T> get(ResourceLocation location);

行为:按资源标识查询,不关心值类型;未注册或 location 为 null 时返回空 Optional。适合「已知 id,想拿到内容」的运行期查询。

示例:

java
Optional<RenderFilter> filter =
        ctx.registries().renderFilters().get(ResourceLocation.parse("mymod:bloom"));

contains(location)

签名:

java
boolean contains(ResourceLocation location);

行为:等价于 get(location).isPresent();location 为 null 时返回 false。注册前可用于冲突自查(但真正防重复靠 register 抛 IllegalArgumentException,不要只靠 contains 做「先查再写」)。

示例:

java
if (!ctx.registries().skins().contains(ResourceLocation.of("mymod", "neon"))) {
    // 不冲突,可以注册
}

keys()

签名:

java
Set<ResourceLocation> keys();

行为:返回全部已注册的资源标识,保持注册顺序(宿主内置内容先于模组内容,同批次按注册先后)。返回不可变视图:冻结后是冻结快照的键集,冻结前是当前内容的副本;两种情况下调用方都不能修改,也不能指望它反映之后的写入。

示例:

java
for (ResourceLocation id : ctx.registries().gameModes().keys()) {
    ctx.getLogger().info("game mode: " + id);
}

keysByNamespace(namespace)

签名:

java
default Set<ResourceLocation> keysByNamespace(String namespace);

行为:返回该命名空间下全部已注册标识,保持注册顺序;namespace 为 null 或没有匹配时返回空集合。自 起 per-mod 视图强制「注册键命名空间 = 本模组 id」,因此这个查询就是「本模组(或指定来源)注册了哪些」的精确答案,不必依赖「命名空间 = 模组 id」的猜测。

实现细节(以宿主为准):默认实现遍历 keys() 逐个过滤,返回的是新建的可变 LinkedHashSet——接口 javadoc 称「不可变视图」,但实际可以修改且不会影响注册表;它是调用时刻的快照,不随后续注册更新。宿主实现未覆写该方法。

示例:

java
for (ResourceLocation id : ctx.registries().renderFilters().keysByNamespace("mymod")) {
    ctx.getLogger().info("my filter: " + id);
}

isFrozen()

签名:

java
boolean isFrozen();

行为:返回注册表是否已冻结。宿主在全部模组的 onPreInit 结束后调用一次冻结(幂等);此后写入抛 RegistryFrozenException,读取路径切到无锁快照。不能靠它判断「现在是否在注册窗口」来做注册:请把注册写在 onPreInit 内。

示例:

java
boolean canRegister = !ctx.registries().collectibles().isFrozen();

线程:读操作(get / contains / keys / keysByNamespace / isFrozen)任意线程安全;写操作(register)仅允许在注册窗口内的宿主线程。

生命周期阶段:写入仅限注册窗口(onPreInit);运行期可读。注册表在进程生命周期内只冻结一次,因此模组停用/启用需要重启才生效;接口不暴露删除,已注册内容不会消失。

稳定性:2.0 起冻结:2.x 内只增不破(禁止删签名 / 改签名 / 改语义)。