2015-02-21 10:47:39 -07:00

323 lines
14 KiB
C#

using System;
using TrueCraft.API;
using TrueCraft.API.World;
using TrueCraft.API.Server;
using TrueCraft.API.Logic;
using TrueCraft.API.Networking;
using System.Collections.Generic;
using System.Linq;
namespace TrueCraft.Core.Logic.Blocks
{
public abstract class FluidBlock : BlockProvider
{
// Fluids in Minecraft propegate according to a set of rules as cellular automata.
// Source blocks start at zero and each block progressively further from the source
// is one greater than the largest value nearby. When they reach
// MaximumFluidDepletion, the fluid stops propgetating.
public override abstract byte ID { get; }
public override BoundingBox? BoundingBox
{
get
{
return null;
}
}
protected override ItemStack[] GetDrop(BlockDescriptor descriptor)
{
return new ItemStack[0];
}
protected abstract double SecondsBetweenUpdates { get; }
protected abstract byte MaximumFluidDepletion { get; }
protected abstract byte FlowingID { get; }
protected abstract byte StillID { get; }
protected virtual bool AllowSourceCreation { get { return true; } }
private static readonly Coordinates3D[] Neighbors =
{
Coordinates3D.North,
Coordinates3D.South,
Coordinates3D.East,
Coordinates3D.West
};
/// <summary>
/// Represents a block that the currently updating fluid block is able to flow outwards into.
/// </summary>
protected struct LiquidFlow
{
public LiquidFlow(Coordinates3D targetBlock, byte level)
{
TargetBlock = targetBlock;
Level = level;
}
/// <summary>
/// The block to be filled with fluid.
/// </summary>
public Coordinates3D TargetBlock;
/// <summary>
/// The fluid level to fill the target block with.
/// </summary>
public byte Level;
}
public void ScheduleNextEvent(Coordinates3D coords, IWorld world, IMultiplayerServer server)
{
server.Scheduler.ScheduleEvent(DateTime.Now.AddSeconds(SecondsBetweenUpdates), (_server) =>
AutomataUpdate(_server, world, coords));
}
public override void BlockPlaced(BlockDescriptor descriptor, BlockFace face, IWorld world, IRemoteClient user)
{
if (ID == FlowingID)
ScheduleNextEvent(descriptor.Coordinates, world, user.Server);
}
public override void BlockUpdate(BlockDescriptor descriptor, BlockDescriptor source, IMultiplayerServer server, IWorld world)
{
if (ID == StillID)
{
var outward = DetermineOutwardFlow(world, descriptor.Coordinates);
var inward = DetermineInwardFlow(world, descriptor.Coordinates);
if (outward.Length != 0 || inward != descriptor.Metadata)
{
world.SetBlockID(descriptor.Coordinates, FlowingID);
ScheduleNextEvent(descriptor.Coordinates, world, server);
}
}
}
private void AutomataUpdate(IMultiplayerServer server, IWorld world, Coordinates3D coords)
{
if (world.GetBlockID(coords) != FlowingID && world.GetBlockID(coords) != StillID)
return;
server.BlockUpdatesEnabled = false;
var again = DoAutomata(server, world, coords);
server.BlockUpdatesEnabled = true;
if (again)
{
server.Scheduler.ScheduleEvent(DateTime.Now.AddSeconds(SecondsBetweenUpdates), (_server) =>
AutomataUpdate(_server, world, coords));
}
}
public bool DoAutomata(IMultiplayerServer server, IWorld world, Coordinates3D coords)
{
var previousLevel = world.GetMetadata(coords);
var inward = DetermineInwardFlow(world, coords);
var outward = DetermineOutwardFlow(world, coords);
if (outward.Length == 1 && outward[0].TargetBlock == coords + Coordinates3D.Down)
{
// Exit early if we have placed a fluid block beneath us (and we aren't a source block)
FlowOutward(world, outward[0], server);
if (previousLevel != 0)
return true;
}
// Process inward flow
if (inward > MaximumFluidDepletion)
{
world.SetBlockID(coords, 0);
return true;
}
world.SetMetadata(coords, inward);
if (inward == 0 && previousLevel != 0)
{
// Exit early if we have become a source block
return true;
}
// Process outward flow
for (int i = 0; i < outward.Length; i++)
FlowOutward(world, outward[i], server);
// Set our block to still fluid if we are done spreading.
if (outward.Length == 0 && inward == previousLevel)
{
world.SetBlockID(coords, StillID);
return false;
}
return true;
}
private void FlowOutward(IWorld world, LiquidFlow target, IMultiplayerServer server)
{
// For each block we can flow into, generate an item entity if appropriate
var provider = world.BlockRepository.GetBlockProvider(world.GetBlockID(target.TargetBlock));
provider.GenerateDropEntity(new BlockDescriptor { Coordinates = target.TargetBlock, ID = provider.ID }, world, server);
// And overwrite the block with a new fluid block.
world.SetBlockID(target.TargetBlock, FlowingID);
world.SetMetadata(target.TargetBlock, target.Level);
server.Scheduler.ScheduleEvent(DateTime.Now.AddSeconds(SecondsBetweenUpdates), s => AutomataUpdate(s, world, target.TargetBlock));
}
/// <summary>
/// Examines neighboring blocks and determines the new fluid level that this block should adopt.
/// </summary>
protected byte DetermineInwardFlow(IWorld world, Coordinates3D coords)
{
var currentLevel = world.GetMetadata(coords);
var up = world.GetBlockID(coords + Coordinates3D.Up);
if (up == FlowingID || up == StillID) // Check for fluid above us
return currentLevel;
else
{
if (currentLevel != 0)
{
byte highestNeighboringFluid = 15;
int neighboringSourceBlocks = 0;
for (int i = 0; i < Neighbors.Length; i++)
{
var nId = world.GetBlockID(coords + Neighbors[i]);
if (nId == FlowingID || nId == StillID)
{
var neighborLevel = world.GetMetadata(coords + Neighbors[i]);
if (neighborLevel < highestNeighboringFluid)
highestNeighboringFluid = neighborLevel;
if (neighborLevel == 0)
neighboringSourceBlocks++;
}
}
if (neighboringSourceBlocks >= 2 && AllowSourceCreation)
currentLevel = 0;
if (highestNeighboringFluid > 0)
currentLevel = (byte)(highestNeighboringFluid + 1);
}
}
return currentLevel;
}
/// <summary>
/// Produces a list of outward flow targets that this block may flow towards.
/// </summary>
protected LiquidFlow[] DetermineOutwardFlow(IWorld world, Coordinates3D coords)
{
// The maximum distance we will search for lower ground to flow towards
const int dropCheckDistance = 5;
var outwardFlow = new List<LiquidFlow>(5);
var currentLevel = world.GetMetadata(coords);
var blockBelow = world.BlockRepository.GetBlockProvider(world.GetBlockID(coords + Coordinates3D.Down));
if (blockBelow.Hardness == 0 && blockBelow.ID != FlowingID && blockBelow.ID != StillID)
{
outwardFlow.Add(new LiquidFlow(coords + Coordinates3D.Down, 1));
if (currentLevel != 0)
return outwardFlow.ToArray();
}
if (currentLevel < MaximumFluidDepletion)
{
// This code is responsible for seeking out candidates for flowing towards.
// Fluid in Minecraft will flow in the direction of the nearest drop-off where
// there is at least one block removed on the Y axis.
// It will flow towards several equally strong candidates at once.
var candidateFlowPoints = new List<Coordinates3D>(4);
var furthestPossibleCandidate = new Coordinates3D(x: dropCheckDistance + 1, z: dropCheckDistance + 1) + Coordinates3D.Down;
var nearestCandidate = furthestPossibleCandidate;
for (int x = -dropCheckDistance; x < dropCheckDistance; x++)
{
for (int z = -dropCheckDistance; z < dropCheckDistance; z++)
{
if (Math.Abs(z) + Math.Abs(x) > dropCheckDistance)
continue;
var check = new Coordinates3D(x: x, z: z) + Coordinates3D.Down;
var c = world.BlockRepository.GetBlockProvider(world.GetBlockID(check + coords));
if (c.Hardness == 0)
{
if (!LineOfSight(world, check + coords, coords))
continue;
if (coords.DistanceTo(check + coords) == coords.DistanceTo(nearestCandidate + coords))
candidateFlowPoints.Add(check);
if (coords.DistanceTo(check + coords) < coords.DistanceTo(nearestCandidate + coords))
{
candidateFlowPoints.Clear();
nearestCandidate = check;
}
}
}
}
if (nearestCandidate == furthestPossibleCandidate)
{
candidateFlowPoints.Add(new Coordinates3D(x: -dropCheckDistance - 1, z: dropCheckDistance + 1) + Coordinates3D.Down);
candidateFlowPoints.Add(new Coordinates3D(x: dropCheckDistance + 1, z: -dropCheckDistance - 1) + Coordinates3D.Down);
candidateFlowPoints.Add(new Coordinates3D(x: -dropCheckDistance - 1, z: -dropCheckDistance - 1) + Coordinates3D.Down);
}
candidateFlowPoints.Add(nearestCandidate);
// For each candidate, determine if we are actually capable of flowing towards it.
// We are able to flow through blocks with a hardness of zero, but no others. We are
// not able to flow through established fluid blocks.
for (int i = 0; i < candidateFlowPoints.Count; i++)
{
var location = candidateFlowPoints[i];
location.Clamp(1);
var xCoordinateCheck = new Coordinates3D(x: location.X) + coords;
var zCoordinateCheck = new Coordinates3D(z: location.Z) + coords;
var xID = world.BlockRepository.GetBlockProvider(world.GetBlockID(xCoordinateCheck));
var zID = world.BlockRepository.GetBlockProvider(world.GetBlockID(zCoordinateCheck));
if (xID.Hardness == 0 && xID.ID != FlowingID && xID.ID != StillID)
{
if (outwardFlow.All(f => f.TargetBlock != xCoordinateCheck))
outwardFlow.Add(new LiquidFlow(xCoordinateCheck, (byte)(currentLevel + 1)));
}
if (zID.Hardness == 0 && zID.ID != FlowingID && zID.ID != StillID)
{
if (outwardFlow.All(f => f.TargetBlock != zCoordinateCheck))
outwardFlow.Add(new LiquidFlow(zCoordinateCheck, (byte)(currentLevel + 1)));
}
}
// Occasionally, there are scenarios where the nearest candidate hole is not acceptable, but
// there is space immediately next to the block. We should fill that space.
if (outwardFlow.Count == 0 && blockBelow.ID != FlowingID && blockBelow.ID != StillID)
{
for (int i = 0; i < Neighbors.Length; i++)
{
var b = world.BlockRepository.GetBlockProvider(world.GetBlockID(coords + Neighbors[i]));
if (b.Hardness == 0 && b.ID != StillID && b.ID != FlowingID)
outwardFlow.Add(new LiquidFlow(Neighbors[i] + coords, (byte)(currentLevel + 1)));
}
}
}
return outwardFlow.ToArray();
}
/// <summary>
/// Returns true if the given candidate coordinate has a line-of-sight to the given target coordinate.
/// </summary>
private bool LineOfSight(IWorld world, Coordinates3D candidate, Coordinates3D target)
{
candidate += Coordinates3D.Up;
var direction = target - candidate;
direction.Clamp(1);
do
{
int z = candidate.Z;
do
{
var p = world.BlockRepository.GetBlockProvider(world.GetBlockID(candidate));
if (p.Hardness != 0)
return false;
candidate.Z += direction.Z;
} while (target.Z != candidate.Z);
candidate.Z = z;
candidate.X += direction.X;
} while (target.X != candidate.X);
return true;
}
}
}