水体方块与扩散系统

基本信息

属性 值
方块基类 blocks/HEWater.java(118 行)→ BlockFluidBase
静止态子类 blocks/HEWaterStill.java(71 行)
注册数量 HEConfig.maxDams 个(默认 16,可配 1–16),每个 waterId 一个独立方块
注册位置 hooks/HEHooksShared.java:77-83(preInit)
方块 ID 由 Block.blockRegistry.getIDForObject(...) 运行时取得,存进 HE.waterBlockIds[]
NEI 全部 API.hideItem(...) 隐藏(hooks/HEHooksShared.java:82)
for (int waterId = 0; waterId < HEConfig.maxDams; waterId++) {
    HE.waterBlocks[waterId] = new HEWaterStill(waterId);
    GameRegistry.registerBlock(HE.waterBlocks[waterId], HE.waterBlocks[waterId].getUnlocalizedName());
    HE.waterBlockIds[waterId] = Block.blockRegistry.getIDForObject(HE.waterBlocks[waterId]);
    API.hideItem(new ItemStack(HE.waterBlocks[waterId]));
}

(hooks/HEHooksShared.java:77-83)

这是本 mod 唯一「数量随配置变化」的注册物: HE.waterBlocks = new HEWaterStill[HEConfig.maxDams](HE.java), 静态数组在类加载时按当时的 maxDams 定长度。

源码提前打了一条提示(hooks/HEHooksShared.java:75): The subsequent N liquid errors are intendend. Please ignore... ——注册多个同类 BlockFluidBase 会产生 Forge 的液体重复警告,属预期。

方块构造(HEWater.java:27-34)

public HEWater(int waterId) {
    super(FluidRegistry.WATER, Material.water);
    this.waterId = waterId;
    setHardness(100.0F);
    setLightOpacity(0);
    setBlockName("water");
    setBlockTextureName("minecraft:water_still");
}
属性 值 说明
硬度 100.0F 极难破坏
初始不透光 setLightOpacity(0),但被 getLightOpacity() 覆写 见下
贴图 minecraft:water_still 直接借用原版水贴图
未本地化名 water + waterId getUnlocalizedName() 追加数字(HEWater.java:104-107)→ tile.water0.name … tile.water15.name

关键覆写

方法 行 行为
drain(...) 36-39 恒返回 null —— 桶 / 机器抽不走水
canDrain(...) 41-44 恒 false
getFlowVector(...) 53-56 恒 (0, 0, 0) —— 不流动,水平面完全平整
canCollideCheck(...) 58-61 恒 false —— 无碰撞箱
getMaxRenderHeightMeta() 72-75 返回 0
getLightOpacity() 63-70 客户端 = HE.waterOpacity(3);服务端 = 0
getQuantaValue(...) 46-51 Math.round(clamp(水面高度 - blockY, 0, 1) * 8) —— 8 quanta = 1 满格

双端光不透光性不同(客户端 3 / 服务端 0)是刻意设计: 服务端不需为渲染付出光照计算开销。 HE.underWaterSkylightDepth = (int) Math.ceil(16f / 3) = 6 (HE.java),即一个 16 格高的子区块要穿透 6 个子区块的水。

材质判定(两套阈值)

public Material getMaterial(int blockY) {
    return (Math.floor(getWaterLevel() - HEConfig.clippingOffset)) < blockY ? Material.air : Material.water;
}
public Material getMaterial(double blockY) {
    // This constant is so magic I'm gonna die!
    // Without this constant there is a gap between rendered water and all underwater effects
    return (getWaterLevel() + 0.120f) < blockY ? Material.air : Material.water;
}

(HEWater.java:109-117)

重载 阈值 用途
getMaterial(int) floor(水位 − clippingOffset) 物理/逻辑(clippingOffset 默认 0.05 格)
getMaterial(double) 水位 + 0.120 渲染/水下效果——硬编码的 0.120f 魔数,源码注释直言「This constant is so magic I’m gonna die!」

clippingOffset 同时由服务端下发并全客户端同步(配置注释明确说明, 见 配置项),用途是消除「水太窄时的深度缓冲精度问题」。

水的来源:HEDam(server/HEDam.java)

每个 waterId 对应一个 HEDam 实例,服务端持有。

放置控制器时的默认限高(HEDam.java:217-239)

limitEast  = blockX + 20;
limitWest  = blockX - 20;
limitUp    = blockY + 10;
limitDown  = blockY;
limitSouth = blockZ + 20;
limitNorth = blockZ - 20;
waterLevel = blockY;
blocksPerY = new int[256];

默认范围是 41 × 11 × 41 格(含控制器所在层),向上只有 10 格。

同时初始化 waterLevel = blockY(空坝)并 mode = HE.DamMode.DRAIN(不扩散)。

⚠️ 方向命名的反直觉之处

HEServer.isBlockOutOfBounds(server/HEServer.java:152-159):

return blockX > dam.limitEast || blockX < dam.limitWest
        || blockY > dam.limitUp
        || blockY < dam.limitDown
        || blockZ > dam.limitSouth
        || blockZ < dam.limitNorth;

即 South 是 Z 的上界(max),North 是下界(min)—— 与 X 轴(East = max,West = min)不对称。 这是源码事实,不是笔误断言,但读 GUI 时容易搞反方向。

客户端限高请求会被服务端夹紧(HEDam.java:277-310)

// Clap change requests to server limits before processing
limitWest  = blockX - HEUtil.clamp(blockX - limitWest, 0, HEConfig.maxWaterSpreadWest);
limitDown  = blockY - HEUtil.clamp(blockY - limitDown, 0, HEConfig.maxWaterSpreadDown);
limitNorth = blockZ - HEUtil.clamp(blockZ - limitNorth, 0, HEConfig.maxWaterSpreadNorth);
limitEast  = blockX + HEUtil.clamp(limitEast - blockX, 0, HEConfig.maxWaterSpreadEast);
limitUp    = blockY + HEUtil.clamp(limitUp - blockY, 0, HEConfig.maxWaterSpreadUp);
limitSouth = blockZ + HEUtil.clamp(limitSouth - blockZ, 0, HEConfig.maxWaterSpreadSouth);

6 个方向各自有全局上限(默认值见 配置项), 客户端无法突破。注释里 Clap 是 Clamp 的拼写错误。

变更后立即 sendConfigUpdate() + HEBlockQueue.enqueueBlock(...) 触发重算。

三种坝模式 HE.DamMode(HE.java)

模式 getValue() 含义
DRAIN 1 排水。canSpread() 返回 false
DEBUG 2 调试
SPREAD 3 扩散
public boolean canSpread() {
    return mode != HE.DamMode.DRAIN && isPlaced;
}

(HEDam.java:312-314)

getMode(int) 的 switch 所有 case 都有 default(HE.java 的 DamMode.getMode), 任何未知数值都映射到 DRAIN(安全默认)。

模式与 drainState 的持久化转换(HEDam.java:63-67):

if (!isPlaced || drainState) {
    mode = HE.DamMode.DRAIN;
} else {
    mode = HE.DamMode.SPREAD;
}

⚠️ 存档只存一个 drainState 布尔(writeToNBTFull 里 setBoolean(HETags.drainState, mode == HE.DamMode.DRAIN),HEDam.java:72)。 因此 DEBUG 模式无法持久化——重载存档后会变成 SPREAD。

能量模型:水压 = 高度

HEDam.getEuCapacity()(HEDam.java:320-329):

long euCapacity = 0;
for (int blockY = this.blockY; blockY < HE.numChunksY * HE.chunkHeight; blockY++) {
    euCapacity += blocksPerY[blockY] * HE.bucketToMilliBucket
            * HEConfig.euPerMilliBucket
            * (blockY - this.blockY + 1);
    euCapacityUpToY[blockY] = euCapacity;
}
return euCapacity;
因子 值 说明
blocksPerY[blockY] 运行时统计 该 Y 层的水方块数,由 onWaterPlaced / onWaterRemoved 增减(HEDam.java:241-247)
HE.bucketToMilliBucket 1000 桶 → 毫桶
HEConfig.euPerMilliBucket 默认 1.0 见 配置项
(blockY − 控制器Y + 1) 静水压深度因子 越深能量越多,线性递增

循环上界 HE.numChunksY * HE.chunkHeight = 16 × 16 = 256(HE.java)。 euCapacityUpToY[256] 是前缀和缓存。

⚠️ 调用顺序有硬依赖,源码两处都写了警告注释:

  • // This method must be called after getEuCapacity (cause euCapacityUpToY[]) (HEDam.java:331、336)

getEuCapacityAt(int) 与 setWaterLevel(long) 都依赖 getEuCapacity() 刚填好的 euCapacityUpToY[]。若单独调用会读到上一次的缓存。 水力坝 的 onRunningTick 正是先调 getEuCapacity() 再调 getEuCapacityAt()(HEHydroDamTileEntity.java:151-154)才正确。

能量 → 水位反算(HEDam.java:337-348)

for (int blockY = this.blockY; blockY < HE.numChunksY * HE.chunkHeight; blockY++) {
    if (euStored < euCapacityUpToY[blockY]) {
        float energyCapacityAtY = blocksPerY[blockY] * HE.bucketToMilliBucket
                * HEConfig.euPerMilliBucket * (blockY - this.blockY + 1);
        float decimals = 1.0f + ((float) euStored - (float) euCapacityUpToY[blockY]) / energyCapacityAtY;
        setWaterLevel(blockY + decimals);
        return;
    }
}

在首个未装满的 Y 层做线性插值,得到小数水位。

⚠️ 若 blocksPerY[blockY] == 0(空层), energyCapacityAtY 为 0 → 除零得到 Infinity / NaN。 循环用的是 euStored < euCapacityUpToY[blockY],而空层的前缀和与前一层相同, 是否触发取决于 euStored 恰好落在哪。源码无防护。

雨天发电取样(HEDam.java:350-357)

public int getRainedOnBlocks() {
    for (int blockY = 255; blockY >= 0; blockY--) {
        if (blocksPerY[blockY] > 0) {
            return blocksPerY[blockY];
        }
    }
    return 0;
}

从 y=255 向下找第一个非空层,返回该层的方块数—— 即「水面上被雨淋到的方块数」。

NBT 键(HETags.java:14-37,全部是短键名)

键常量 字符串 类型
waterLevel walv float
drainState drai boolean
isPlaced isPl boolean
limitUp limU int
limitDown limD int
limitEast limE int
limitWest limW int
limitSouth limS int
limitNorth limN int
blocksPerY BlPY int[256]
blockX / blockY / blockZ bloX / bloY / bloZ int
dimensionId dimI int
waterBlockX/Y/Z watX / watY / watZ int
waterId waId int
waterStored / waterCapacity wSto / wCap —
pressure pres float(流体 NBT)
dam dam —
ownerName ownN String
mainStructure / structurePieceMain 都是 main 结构 ID

⚠️ mainStructure 与 structurePieceMain 是两个不同常量,值都是 "main"(HETags.java:37、39)。 泵/涡轮用前者,坝用后者。值相同属巧合式冗余,不是同一个常量。

同步节流

public boolean setWaterLevel(float waterLevel) {
    this.waterLevel = waterLevel;
    long timestamp = System.currentTimeMillis();
    if (timestamp - timestampLastUpdate >= HEConfig.minimalWaterUpdateInterval) {
        timestampLastUpdate = timestamp;
        sendWaterUpdate();
        return true;
    }
    return false;
}

(HEDam.java:102-111)

服务端 → 全客户端的水位广播最快每 minimalWaterUpdateInterval 毫秒一次(默认 1000 ms)。 sendConfigUpdate()(HEDam.java:196-210)无节流,配置一变立刻全服广播。

客户端 / 服务端配置不一致会触发 HE.WARN_clientConfigMissmatchDetected ("HydroEnergy: Configuration mismatch to the server found! This might crash somewhat randomly. Please talk to your server admin!")。

关联