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 _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(); } public BlockFluid (BlockFluid bl) { _blockset = bl._blockset; _xdim = bl._xdim; _ydim = bl._ydim; _zdim = bl._zdim; _chunks = new Dictionary(); } public void ResetWater (ByteArray blocks, NibbleArray 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 (ByteArray blocks, NibbleArray 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.StationaryWater.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 buildQueue = new List(); 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 buildQueue = new List(); 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 TileOutflow (BlockKey key, int reach = 5) { Queue searchQueue = new Queue(); Queue> traceQueue = new Queue>(); Dictionary markTable = new Dictionary(); 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 ret = new List(); ret.Add(new BlockKey(branch.x, branch.y - 1, branch.z)); return ret; } reach = distance; traceQueue.Enqueue(new KeyValuePair(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 outflow = new List(); 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 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(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 flowQueue = new Queue(); BlockKey prikey = new BlockKey(x, y, z); flowQueue.Enqueue(prikey); List 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 flowQueue = new Queue(); BlockKey prikey = new BlockKey(x, y, z); flowQueue.Enqueue(prikey); List 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); } } } } }