NBTExplorer/SubstrateCS/Source/Core/BlockFluid.cs

565 lines
21 KiB
C#

using System;
using System.Collections.Generic;
namespace Substrate.Core
{
// Rules:
// - Water must be calculated in steps breadth-first
// - If there are any "holes" within 5 steps (manhattan distance) of a water tile, only the edges
// that can be part of a shortest path to the closest hole(s) are part of the outflow.
// - Any blocks in the water tile's outflow are added to the queue
// - A water source's strength is calculated as strongest inflow - 1.
public class BlockFluid
{
private IBoundedDataBlockCollection _blockset;
private readonly int _xdim;
private readonly int _ydim;
private readonly int _zdim;
private Dictionary<ChunkKey, IBoundedDataBlockCollection> _chunks;
public delegate IBoundedDataBlockCollection NeighborLookupHandler (int relx, int rely, int relz);
public event NeighborLookupHandler ResolveNeighbor;
internal class BlockCoord
{
internal IBoundedDataBlockCollection chunk;
internal int lx;
internal int ly;
internal int lz;
internal BlockCoord (IBoundedDataBlockCollection _chunk, int _lx, int _ly, int _lz)
{
chunk = _chunk;
lx = _lx;
ly = _ly;
lz = _lz;
}
}
public BlockFluid (IBoundedDataBlockCollection blockset)
{
_blockset = blockset;
_xdim = _blockset.XDim;
_ydim = _blockset.YDim;
_zdim = _blockset.ZDim;
_chunks = new Dictionary<ChunkKey,IBoundedDataBlockCollection>();
}
public BlockFluid (BlockFluid bl)
{
_blockset = bl._blockset;
_xdim = bl._xdim;
_ydim = bl._ydim;
_zdim = bl._zdim;
_chunks = new Dictionary<ChunkKey, IBoundedDataBlockCollection>();
}
public void ResetWater (IDataArray blocks, IDataArray data)
{
for (int i = 0; i < blocks.Length; i++) {
if ((blocks[i] == BlockInfo.StationaryWater.ID || blocks[i] == BlockInfo.Water.ID) && data[i] != 0) {
blocks[i] = (byte)BlockInfo.Air.ID;
data[i] = 0;
}
else if (blocks[i] == BlockInfo.Water.ID) {
blocks[i] = (byte)BlockInfo.StationaryWater.ID;
}
}
}
public void ResetLava (IDataArray blocks, IDataArray data)
{
for (int i = 0; i < blocks.Length; i++) {
if ((blocks[i] == BlockInfo.StationaryLava.ID || blocks[i] == BlockInfo.Lava.ID) && data[i] != 0) {
blocks[i] = (byte)BlockInfo.Air.ID;
data[i] = 0;
}
else if (blocks[i] == BlockInfo.Lava.ID) {
blocks[i] = (byte)BlockInfo.StationaryLava.ID;
}
}
}
public void UpdateWater (int x, int y, int z)
{
DoWater(x, y, z);
_chunks.Clear();
}
public void UpdateLava (int x, int y, int z)
{
DoLava(x, y, z);
_chunks.Clear();
}
public void RebuildWater ()
{
int xdim = _xdim;
int ydim = _ydim;
int zdim = _zdim;
List<BlockKey> buildQueue = new List<BlockKey>();
for (int x = 0; x < xdim; x++) {
for (int z = 0; z < zdim; z++) {
for (int y = 0; y < ydim; y++) {
BlockInfo info = _blockset.GetInfo(x, y, z);
if (info.ID == BlockInfo.StationaryWater.ID && _blockset.GetData(x, y, z) == 0) {
buildQueue.Add(new BlockKey(x, y, z));
}
}
}
}
foreach (BlockKey key in buildQueue) {
DoWater(key.x, key.y, key.z);
}
_chunks.Clear();
}
public void RebuildLava ()
{
int xdim = _xdim;
int ydim = _ydim;
int zdim = _zdim;
List<BlockKey> buildQueue = new List<BlockKey>();
for (int x = 0; x < xdim; x++) {
for (int z = 0; z < zdim; z++) {
for (int y = 0; y < ydim; y++) {
BlockInfo info = _blockset.GetInfo(x, y, z);
if (info.ID == BlockInfo.StationaryLava.ID && _blockset.GetData(x, y, z) == 0) {
buildQueue.Add(new BlockKey(x, y, z));
}
}
}
}
foreach (BlockKey key in buildQueue) {
DoLava(key.x, key.y, key.z);
}
_chunks.Clear();
}
private BlockCoord TranslateCoord (int x, int y, int z)
{
IBoundedDataBlockCollection chunk = GetChunk(x, z);
int lx = ((x % _xdim) + _xdim) % _xdim;
int lz = ((z % _zdim) + _zdim) % _zdim;
return new BlockCoord(chunk, lx, y, lz);
}
private IBoundedDataBlockCollection GetChunk (int x, int z)
{
int cx = x / _xdim + (x >> 31);
int cz = z / _zdim + (z >> 31);
ChunkKey key = new ChunkKey(cx, cz);
IBoundedDataBlockCollection chunk;
if (!_chunks.TryGetValue(key, out chunk)) {
chunk = OnResolveNeighbor(cx, 0, cz);
_chunks[key] = chunk;
}
return chunk;
}
private IBoundedDataBlockCollection OnResolveNeighbor (int relX, int relY, int relZ)
{
if (ResolveNeighbor != null) {
IBoundedDataBlockCollection n = ResolveNeighbor(relX, relY, relZ);
if (n == null) {
return null;
}
if (n.XDim != _xdim ||
n.YDim != _ydim ||
n.ZDim != _zdim) {
throw new InvalidOperationException("Subscriber returned incompatible IDataBlockCollection");
}
return n;
}
return null;
}
// -----
private List<BlockKey> TileOutflow (BlockKey key, int reach = 5)
{
Queue<BlockKey> searchQueue = new Queue<BlockKey>();
Queue<KeyValuePair<BlockKey, int>> traceQueue = new Queue<KeyValuePair<BlockKey, int>>();
Dictionary<BlockKey, int> markTable = new Dictionary<BlockKey,int>();
searchQueue.Enqueue(key);
markTable.Add(key, 0);
// Identify sinks
while (searchQueue.Count > 0) {
BlockKey branch = searchQueue.Dequeue();
int distance = markTable[branch];
// Ignore blocks out of range
if (distance > reach) {
continue;
}
// Ignore invalid blocks
BlockCoord branchHigh = TranslateCoord(branch.x, branch.y, branch.z);
if (branchHigh.chunk == null || branch.y == 0) {
markTable.Remove(branch);
continue;
}
// If we're not the magical source block...
if (distance > 0) {
// Ignore blocks that block fluid (and thus could not become a fluid)
BlockInfo branchHighInfo = branchHigh.chunk.GetInfo(branchHigh.lx, branchHigh.ly, branchHigh.lz);
if (branchHighInfo.BlocksFluid) {
markTable.Remove(branch);
continue;
}
}
// If we found a hole, add as a sink, mark the sink distance.
BlockCoord branchLow = TranslateCoord(branch.x, branch.y - 1, branch.z);
BlockInfo branchLowInfo = branchLow.chunk.GetInfo(branchLow.lx, branchLow.ly, branchLow.lz);
if (!branchLowInfo.BlocksFluid) {
// If we are our own sink, return the only legal outflow
if (key == branch) {
List<BlockKey> ret = new List<BlockKey>();
ret.Add(new BlockKey(branch.x, branch.y - 1, branch.z));
return ret;
}
reach = distance;
traceQueue.Enqueue(new KeyValuePair<BlockKey, int>(branch, distance));
continue;
}
// Expand to neighbors
if (distance < reach) {
BlockKey[] keys = {
new BlockKey(branch.x - 1, branch.y, branch.z),
new BlockKey(branch.x + 1, branch.y, branch.z),
new BlockKey(branch.x, branch.y, branch.z - 1),
new BlockKey(branch.x, branch.y, branch.z + 1),
};
for (int i = 0; i < 4; i++) {
if (!markTable.ContainsKey(keys[i])) {
searchQueue.Enqueue(keys[i]);
markTable.Add(keys[i], distance + 1);
}
}
}
}
// Candidate outflows are marked
BlockKey[] neighbors = {
new BlockKey(key.x - 1, key.y, key.z),
new BlockKey(key.x + 1, key.y, key.z),
new BlockKey(key.x, key.y, key.z - 1),
new BlockKey(key.x, key.y, key.z + 1),
};
List<BlockKey> outflow = new List<BlockKey>();
foreach (BlockKey n in neighbors) {
if (markTable.ContainsKey(n)) {
outflow.Add(n);
}
}
// If there's no sinks, all neighbors are valid outflows
if (traceQueue.Count == 0) {
return outflow;
}
// Trace back from each sink eliminating shortest path marks
while (traceQueue.Count > 0) {
KeyValuePair<BlockKey, int> tilekv = traceQueue.Dequeue();
BlockKey tile = tilekv.Key;
int distance = tilekv.Value;
markTable.Remove(tile);
BlockKey[] keys = {
new BlockKey(tile.x - 1, tile.y, tile.z),
new BlockKey(tile.x + 1, tile.y, tile.z),
new BlockKey(tile.x, tile.y, tile.z - 1),
new BlockKey(tile.x, tile.y, tile.z + 1),
};
for (int i = 0; i < 4; i++) {
int nval;
if (!markTable.TryGetValue(keys[i], out nval)) {
continue;
}
if (nval < distance) {
markTable.Remove(keys[i]);
traceQueue.Enqueue(new KeyValuePair<BlockKey, int>(keys[i], nval));
}
}
}
// Remove any candidates that are still marked
foreach (BlockKey n in neighbors) {
if (markTable.ContainsKey(n)) {
outflow.Remove(n);
}
}
return outflow;
}
private int TileInflow (BlockKey key)
{
// Check if water is falling on us
if (key.y < _ydim - 1) {
BlockCoord up = TranslateCoord(key.x, key.y + 1, key.z);
BlockInfo upInfo = up.chunk.GetInfo(up.lx, up.ly, up.lz);
if (upInfo.State == BlockState.FLUID) {
return up.chunk.GetData(up.lx, up.ly, up.lz) | (int)LiquidState.FALLING;
}
}
// Otherwise return the min inflow of our neighbors + step
BlockKey[] keys = {
new BlockKey(key.x - 1, key.y, key.z),
new BlockKey(key.x + 1, key.y, key.z),
new BlockKey(key.x, key.y, key.z - 1),
new BlockKey(key.x, key.y, key.z + 1),
};
int minFlow = 16;
// XXX: Might have different neighboring fluids
for (int i = 0; i < 4; i++) {
BlockCoord neighbor = TranslateCoord(keys[i].x, keys[i].y, keys[i].z);
if (neighbor.chunk == null) {
continue;
}
BlockInfo neighborInfo = neighbor.chunk.GetInfo(neighbor.lx, neighbor.ly, neighbor.lz);
if (neighborInfo.State == BlockState.FLUID) {
int flow = neighbor.chunk.GetData(neighbor.lx, neighbor.ly, neighbor.lz);
bool flowFall = (flow & (int)LiquidState.FALLING) != 0;
if (flowFall) {
if (keys[i].y == 0) {
continue;
}
BlockCoord low = TranslateCoord(keys[i].x, keys[i].y - 1, keys[i].z);
BlockInfo lowinfo = low.chunk.GetInfo(low.lx, low.ly, low.lz);
if (lowinfo.BlocksFluid) {
return 0;
}
continue;
}
if (flow < minFlow) {
minFlow = flow;
}
}
}
return minFlow;
}
private void DoWater (int x, int y, int z)
{
Queue<BlockKey> flowQueue = new Queue<BlockKey>();
BlockKey prikey = new BlockKey(x, y, z);
flowQueue.Enqueue(prikey);
List<BlockKey> outflow = TileOutflow(prikey);
foreach (BlockKey outkey in outflow) {
flowQueue.Enqueue(outkey);
}
while (flowQueue.Count > 0) {
BlockKey key = flowQueue.Dequeue();
int curflow = 16;
int inflow = TileInflow(key);
BlockCoord tile = TranslateCoord(key.x, key.y, key.z);
BlockInfo tileInfo = tile.chunk.GetInfo(tile.lx, tile.ly, tile.lz);
if (tileInfo.ID == BlockInfo.StationaryWater.ID || tileInfo.ID == BlockInfo.Water.ID) {
curflow = tile.chunk.GetData(tile.lx, tile.ly, tile.lz);
}
else if (tileInfo.BlocksFluid) {
continue;
}
bool curFall = (curflow & (int)LiquidState.FALLING) != 0;
bool inFall = (inflow & (int)LiquidState.FALLING) != 0;
// We won't update from the following states
if (curflow == 0 || curflow == inflow || curFall) {
continue;
}
int newflow = curflow;
// Update from inflow if necessary
if (inFall) {
newflow = inflow;
}
else if (inflow >= 7) {
newflow = 16;
}
else {
newflow = inflow + 1;
}
// If we haven't changed the flow, don't propagate
if (newflow == curflow) {
continue;
}
// Update flow, add or remove water tile as necessary
if (newflow < 16 && curflow == 16) {
// If we're overwriting lava, replace with appropriate stone type and abort propagation
if (tileInfo.ID == BlockInfo.StationaryLava.ID || tileInfo.ID == BlockInfo.Lava.ID) {
if ((newflow & (int)LiquidState.FALLING) != 0) {
int odata = tile.chunk.GetData(tile.lx, tile.ly, tile.lz);
if (odata == 0) {
tile.chunk.SetID(tile.lx, tile.ly, tile.lz, BlockInfo.Obsidian.ID);
}
else {
tile.chunk.SetID(tile.lx, tile.ly, tile.lz, BlockInfo.Cobblestone.ID);
}
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, 0);
continue;
}
}
// Otherwise replace the tile with our water flow
tile.chunk.SetID(tile.lx, tile.ly, tile.lz, BlockInfo.StationaryWater.ID);
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, newflow);
}
else if (newflow == 16) {
tile.chunk.SetID(tile.lx, tile.ly, tile.lz, BlockInfo.Air.ID);
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, 0);
}
else {
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, newflow);
}
// Process outflows
outflow = TileOutflow(key);
foreach (BlockKey nkey in outflow) {
flowQueue.Enqueue(nkey);
}
}
}
private void DoLava (int x, int y, int z)
{
Queue<BlockKey> flowQueue = new Queue<BlockKey>();
BlockKey prikey = new BlockKey(x, y, z);
flowQueue.Enqueue(prikey);
List<BlockKey> outflow = TileOutflow(prikey);
foreach (BlockKey outkey in outflow) {
flowQueue.Enqueue(outkey);
}
while (flowQueue.Count > 0) {
BlockKey key = flowQueue.Dequeue();
int curflow = 16;
int inflow = TileInflow(key);
BlockCoord tile = TranslateCoord(key.x, key.y, key.z);
BlockInfo tileInfo = tile.chunk.GetInfo(tile.lx, tile.ly, tile.lz);
if (tileInfo.ID == BlockInfo.StationaryLava.ID || tileInfo.ID == BlockInfo.Lava.ID) {
curflow = tile.chunk.GetData(tile.lx, tile.ly, tile.lz);
}
else if (tileInfo.BlocksFluid) {
continue;
}
bool curFall = (curflow & (int)LiquidState.FALLING) != 0;
bool inFall = (inflow & (int)LiquidState.FALLING) != 0;
// We won't update from the following states
if (curflow == 0 || curflow == inflow || curFall) {
continue;
}
int newflow = curflow;
// Update from inflow if necessary
if (inFall) {
newflow = inflow;
}
else if (inflow >= 6) {
newflow = 16;
}
else {
newflow = inflow + 2;
}
// If we haven't changed the flow, don't propagate
if (newflow == curflow) {
continue;
}
// Update flow, add or remove lava tile as necessary
if (newflow < 16 && curflow == 16) {
// If we're overwriting water, replace with appropriate stone type and abort propagation
if (tileInfo.ID == BlockInfo.StationaryWater.ID || tileInfo.ID == BlockInfo.Water.ID) {
if ((newflow & (int)LiquidState.FALLING) == 0) {
tile.chunk.SetID(tile.lx, tile.ly, tile.lz, BlockInfo.Cobblestone.ID);
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, 0);
continue;
}
}
tile.chunk.SetID(tile.lx, tile.ly, tile.lz, BlockInfo.StationaryLava.ID);
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, newflow);
}
else if (newflow == 16) {
tile.chunk.SetID(tile.lx, tile.ly, tile.lz, BlockInfo.Air.ID);
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, 0);
}
else {
tile.chunk.SetData(tile.lx, tile.ly, tile.lz, newflow);
}
// Process outflows
outflow = TileOutflow(key);
foreach (BlockKey nkey in outflow) {
flowQueue.Enqueue(nkey);
}
}
}
}
}