水体方块与扩散系统
基本信息
| 属性 | 值 |
|---|---|
| 方块基类 | 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!")。
关联
- 消费水体能量的机器见 水力坝
- 加压水(另一个流体)见 加压水与能量换算
- 网络包见 网络包与 GUI
- 地块保护集成见 MyTown2 集成(已失效)