跳转至

2.5 侧信道扩展

上下文的六个核心字段(source、baseDamage、hitVelocity、hitPoint、hitNormal、penetration)覆盖了最通用的弹道信息。但在实际的模组协作场景中,武器侧和护甲侧可能需要交换更多自定义数据——弹药类型、炸药装药量、引信延迟、特殊效应标记等。如果将这些数据全部塞入核心字段,BFDamageContext 的字段列表会不断膨胀,且每个模组关心的子集不同,难以统一。

扩展机制(BFDamageExtensions + BFDamageExtensionKey)正是为此设计的——它是一个类型安全的、基于泛型 key 的键值存储,挂在每个上下文实例上,允许武器侧携带任意结构化数据,护甲侧按需读取。

为什么叫"侧信道"

扩展数据被协议核心管线完全忽略。五步管线(getRHAmodifyPenetrationresolvePenetrationcalculateFinalDamagehurt)不读取扩展容器中的任何内容,不基于扩展值做任何判定。扩展数据的消费者永远是护甲侧(在管线方法的覆写中读取)或武器侧(在回调中读取已写入的值用于后续效果)。

这意味着扩展是纯侧信道——协议的运行不依赖它们,但它们为武器侧和护甲侧提供了一个标准化的"留言板"。武器的设计者可以在这个留言板上写下弹药的元信息,护甲的实现者可以读取这些信息来做更精细的判定(例如爆反对 HEAT 弹和 APFSDS 弹的区别对待)。

扩展容器的基本操作

BFDamageExtensions 提供了两个核心方法:set(key, value)get(key)

BFDamageExtensions exts = new BFDamageExtensions();

// 写入
exts.set(BFDamageExtensions.FUSE_DELAY, 0.05f);   // 引信延迟 50ms
exts.set(BFDamageExtensions.CALIBER, 0.12f);       // 120mm 口径

// 读取
float fuseDelay = exts.get(BFDamageExtensions.FUSE_DELAY);   // 0.05
float caliber   = exts.get(BFDamageExtensions.CALIBER);       // 0.12
float mass      = exts.get(BFDamageExtensions.MASS);          // 10.0(未设置,退回默认值)

set 要求 key 和 value 均不能为 null——写入 null 值没有意义,想表示"未设置"就不要调用 set。get 总是返回非 null——如果之前没有通过 set 写入,则返回 key 注册时指定的默认值。这个保证使得护甲侧在读取扩展时永远不需要判空,简化了管线覆写中的代码。

如果多次命中共享同一组基础扩展数据,可以使用 copy() 创建浅拷贝,再写入本次命中的特定字段:

BFDamageExtensions base = new BFDamageExtensions();
base.set(BFDamageExtensions.CALIBER, 0.12f);
base.set(BFDamageExtensions.MASS, 8.5f);

BFDamageExtensions perHit = base.copy();
perHit.set(BFDamageExtensions.FUSE_DELAY, 0.05f);

也可以使用拷贝构造器:new BFDamageExtensions(base)。两种方式都会复制容器内部的 key-value 映射,但不会深拷贝扩展值对象本身。因此如果扩展值是可变对象,多个容器可能仍然共享同一个值实例;跨模组扩展字段建议使用不可变值。

协议预定义的扩展 Key

协议自身定义了三个标准扩展 key,均为 Float 类型,已注册在 ballistics_framework 命名空间下:

常量 默认值 含义
BFDamageExtensions.FUSE_DELAY 0f 引信延迟(秒)。0 表示瞬发引信(命中即炸);正值表示延迟引爆(如穿甲后在内部爆炸)
BFDamageExtensions.CALIBER 0.1f 弹体口径(米)。默认 0.1(100mm),典型坦克炮口径
BFDamageExtensions.MASS 10f 弹体质量(千克)。默认 10kg,典型坦克炮穿甲弹质量

FUSE_DELAY 是最典型的"侧信道"扩展——协议核心不关心什么时间引爆,但武器侧在回调中可以根据此值决定何时(或是否)触发爆炸。护甲侧也可以通过读取此值来判断是否为延迟引信弹药,从而决定爆反的拦截时机。

CALIBERMASS 则为护甲侧提供了弹药的基础物理属性。例如,间隙装甲的衰减系数可能取决于弹丸口径——口径越小,间隙衰减越明显。护甲侧在 modifyPenetration 中读取 CALIBER 来做这种判定。

注册自定义扩展 Key

如果你的模组需要携带协议预定义之外的扩展数据,可以通过 BFDamageExtensions.register() 注册自己的 key。注册操作建议放在模组初始化阶段(@Mod 构造器或静态初始化块),并保存为 public static final 常量,供武器侧和护甲侧共享:

// 在你的 @Mod 类中
public static final BFDamageExtensionKey<String> AMMO_TYPE =
    BFDamageExtensions.register(
        ResourceLocation.fromNamespaceAndPath("your_mod_id", "ammo_type"),
        String.class,
        () -> "unknown"    // 默认值
    );

public static final BFDamageExtensionKey<Integer> PROPELLANT_CHARGE =
    BFDamageExtensions.register(
        ResourceLocation.fromNamespaceAndPath("your_mod_id", "propellant_charge"),
        Integer.class,
        () -> 0
    );

注册 key 的三个参数分别是:唯一标识符(ResourceLocation,全局唯一,不允许重名)、值类型(Class<T>,用于编译期类型检查)、默认值工厂(Supplier<T>,每次 get 未命中时调用)。

Key 注册后是全局单例——整个 JVM 中同一个 id 只能注册一次。如果两个模组意外注册了相同 id 的 key,第二个注册会抛出异常。因此建议 key 的命名空间使用你自己的 mod id,避免与其他模组冲突。

注册的时机与生命周期

扩展 key 的注册时机必须早于任何对 get()set() 的调用。建议在以下位置注册:

  • @Mod 主类的构造函数中:NeoForge 保证在模组初始化阶段调用,此时其他模组尚未开始游戏逻辑。
  • 静态初始化块中:类的 <clinit> 在第一次使用时触发,同样早于游戏逻辑。

不要在游戏运行中动态注册 key——不仅因为注册表会检查重名,更因为动态注册的 key 在并发场景下可能尚未传播到其他线程。

如果你的 key 只在你的模组内部使用(武器侧写入,自己的护甲侧读取),注册在你的模组类中即可。如果你希望其他模组也能读取你的扩展数据,你需要将 key 常量公开——通常是暴露为 public static final 字段,其他模组在编译期引用。

护甲侧读取扩展

护甲侧在管线方法内通过 ctx.extensions().get(key) 读取扩展数据。由于 get 保证非 null 返回,读取代码可以写得很简洁:

@Override
public float modifyPenetration(BFDamageContext ctx) {
    float base = ctx.penetration();

    // 读取弹药口径
    float caliber = ctx.extensions().get(BFDamageExtensions.CALIBER);

    // 口径小于 75mm 的全口径弹对间隙装甲衰减明显
    if (caliber < 0.075f) {
        return base * 0.7f;
    }
    return base;
}

如果护甲侧需要读取其他模组注册的自定义 key,需要先将该 key 的常量引入自己的编译依赖。这也意味着扩展 key 的常量定义类必须是一个稳定的公共 API——如果武器模组改变了 key 的 id 或默认值,依赖它的护甲模组也需要重新编译。

扩展与核心字段的区别

核心字段(penetrationbaseDamage 等)和扩展字段的设计哲学有本质区别。核心字段由协议设计师定义,是整个协议的"语言"——管线的每个步骤都依赖它们,所有参与协议的模组都必须理解它们的语义。扩展字段由各模组自行定义,是各方之间的"方言"——只有声明了 key 的模组和使用该 key 的模组才需要理解它,协议核心不读取也不依赖扩展。

因此,当你设计弹药的数据模型时,一个判断标准是:如果这个数据参与击穿判定(影响 resolvePenetration 的返回值),它应该是武器侧在构造上下文前完成的计算——结果反映在 penetration 的值中,而不是作为一个扩展 key 让每个护甲模组分别读取和计算。如果这个数据用于非判定性的反馈(音效类型、粒子颜色、后续效果),它天然适合作为扩展 key——武器侧写入,回调中读取,护甲侧可选忽略。

完整实例:携带弹药元信息的上下文

以下是一个从弹药配置到上下文构造的完整流程:

// === 弹药配置(JSON 或代码定义) ===
public class AmmoConfig {
    public static final BFDamageExtensionKey<String> AMMO_ID = BFDamageExtensions.register(
        ResourceLocation.fromNamespaceAndPath("my_gun_mod", "ammo_id"),
        String.class, () -> "unknown"
    );
    public static final BFDamageExtensionKey<Float> EXPLOSIVE_MASS = BFDamageExtensions.register(
        ResourceLocation.fromNamespaceAndPath("my_gun_mod", "explosive_mass"),
        Float.class, () -> 0f
    );
}

// === 武器侧:构造上下文 ===
public class APFSDSBullet {
    public BFDamageContext buildContext(DamageSource source, Vec3 hitVelocity,
                                         Vec3 hitPoint, Vec3 hitNormal, float rawPen) {
        BFDamageExtensions exts = new BFDamageExtensions();
        exts.set(BFDamageExtensions.FUSE_DELAY, 0f);
        exts.set(BFDamageExtensions.CALIBER, 0.025f);
        exts.set(BFDamageExtensions.MASS, 4.5f);
        exts.set(AmmoConfig.AMMO_ID, "apfsds_m829a1");
        exts.set(AmmoConfig.EXPLOSIVE_MASS, 0f);

        float effectivePen = rawPen / (float)Math.abs(hitVelocity.normalize().dot(hitNormal));
        return BFDamageContext.builder()
            .source(source)
            .baseDamage(50f)
            .hitVelocity(hitVelocity)
            .hitPoint(hitPoint)
            .hitNormal(hitNormal)
            .penetration(effectivePen)
            .extensions(exts)
            .build();
    }
}

// === 护甲侧:读取扩展做精细判定 ===
@Override
public float modifyPenetration(BFDamageContext ctx) {
    float base = ctx.penetration();
    String ammoId = ctx.extensions().get(AmmoConfig.AMMO_ID);

    // 特定弹药对此装甲有额外穿透加成
    if ("apfsds_m829a1".equals(ammoId)) {
        return base * 1.1f;  // 10% 加成——弹药专门优化过
    }

    // HEAT 弹药被爆反拦截
    float explosiveMass = ctx.extensions().get(AmmoConfig.EXPLOSIVE_MASS);
    if (explosiveMass > 0) {
        return base * 0.3f;  // 爆反拦截化学能射流
    }

    return base;
}

这个实例展示了扩展机制的典型使用模式:武器侧负责将弹药的全部元信息打包进扩展容器;护甲侧选择性地读取其中感兴趣的 key,做出精细判定;协议核心完全不知情,只使用核心字段做比较。