/// <summary>Place a single block in the world. This will mark the block as dirty.</summary> /// <param name="position">position to place the block at</param> /// <param name="type">type of block to place</param> /// <param name="isMultipleBlockPlacement">Use this when placing multiple blocks at once so lighting and chunk queueing only happens once.</param> internal static void PlaceBlock(Position position, Block.BlockType type, bool isMultipleBlockPlacement = false) { if (!position.IsValidBlockLocation || position.Y <= 0) { return; } //this was a multiple block placement, prevent placing blocks on yourself and getting stuck; used to be able to place cuboids on yourself and get stuck //only check in single player for now because in multiplayer this could allow the blocks on different clients to get out of sync and placements of multiple blocks in multiplayer will be rare if (Config.IsSinglePlayer && isMultipleBlockPlacement && (position.IsOnBlock(ref Game.Player.Coords) || position == Game.Player.CoordsHead.ToPosition())) { return; } if (type == Block.BlockType.Air) { //if destroying a block under water, fill with water instead of air if (position.Y + 1 < Chunk.CHUNK_HEIGHT && GetBlock(position.X, position.Y + 1, position.Z).Type == Block.BlockType.Water) { type = Block.BlockType.Water; } } var chunk = Chunks[position]; var block = position.GetBlock(); var oldType = block.Type; block.Type = type; //assign the new type var isTransparentBlock = Block.IsBlockTypeTransparent(type); var isTransparentOldBlock = Block.IsBlockTypeTransparent(oldType); block.BlockData = (ushort)(block.BlockData | 0x8000); //mark the block as dirty for the save file "diff" logic chunk.Blocks[position] = block; //insert the new block chunk.UpdateHeightMap(ref block, position.X % Chunk.CHUNK_SIZE, position.Y, position.Z % Chunk.CHUNK_SIZE); if (!isTransparentBlock || type == Block.BlockType.Water) { var below = position; below.Y--; if (below.Y > 0) { if (below.GetBlock().Type == Block.BlockType.Grass || below.GetBlock().Type == Block.BlockType.Snow) { PlaceBlock(below, Block.BlockType.Dirt, true); //dont queue with this dirt block change, the source block changing takes care of it, prevents double queueing the chunk and playing sound twice } } } if (!chunk.WaterExpanding) //water is not expanding, check if this change should make it start { switch (type) { case Block.BlockType.Water: chunk.WaterExpanding = true; break; case Block.BlockType.Air: for (var q = 0; q < 5; q++) { Position adjacent; switch (q) { case 0: adjacent = new Position(position.X + 1, position.Y, position.Z); break; case 1: adjacent = new Position(position.X - 1, position.Y, position.Z); break; case 2: adjacent = new Position(position.X, position.Y + 1, position.Z); break; case 3: adjacent = new Position(position.X, position.Y, position.Z + 1); break; default: adjacent = new Position(position.X, position.Y, position.Z - 1); break; } if (adjacent.IsValidBlockLocation && adjacent.GetBlock().Type == Block.BlockType.Water) { Chunks[adjacent].WaterExpanding = true; } } break; } } //its easier to just set .GrassGrowing on all affected chunks to true, and then let that logic figure it out and turn it off, this way the logic is contained in one spot //and also the logic here doesnt need to run every time a block gets placed. ie: someone is building a house, its running through this logic for every block placement; //now it can only check once on the grass grow interval and turn it back off //gm: an additional optimization, grass could never start growing unless this is an air block and its replacing a non transparent block //OR this is a non transparent block filling in a previously transparent block to cause grass to die if (!isTransparentBlock || (type == Block.BlockType.Air && !isTransparentOldBlock)) { chunk.GrassGrowing = true; if (position.IsOnChunkBorder) { foreach (var adjacentChunk in position.BorderChunks) { adjacentChunk.GrassGrowing = true; } } } //determine if any static game items need to be removed as a result of this block placement if (type != Block.BlockType.Air) { lock (chunk.Clutters) //lock because clutter is stored in a HashSet { //if theres clutter on this block then destroy it to place the block (FirstOrDefault returns null if no match is found) var clutterToRemove = chunk.Clutters.FirstOrDefault(clutter => position.IsOnBlock(ref clutter.Coords)); if (clutterToRemove != null) { chunk.Clutters.Remove(clutterToRemove); } } var lightSourceToRemove = chunk.LightSources.FirstOrDefault(lightSource => position.IsOnBlock(ref lightSource.Value.Coords)); if (lightSourceToRemove.Value != null) { LightSource temp; chunk.LightSources.TryRemove(lightSourceToRemove.Key, out temp); } } else //destroying block { lock (chunk.Clutters) //lock because clutter is stored in a HashSet { //if theres clutter on top of this block then remove it as well (FirstOrDefault returns null if no match is found) var clutterToRemove = chunk.Clutters.FirstOrDefault(clutter => clutter.Coords.Xblock == position.X && clutter.Coords.Yblock == position.Y + 1 && clutter.Coords.Zblock == position.Z); //add one to Y to look on the block above if (clutterToRemove != null) { chunk.Clutters.Remove(clutterToRemove); } } //look on ALL 6 adjacent blocks for static items, and those only get destroyed if its on the matching opposite attached to face var adjacentPositions = position.AdjacentPositionFaces; foreach (var tuple in adjacentPositions) { var adjBlock = tuple.Item1.GetBlock(); if (adjBlock.Type != Block.BlockType.Air) { continue; //position cannot contain an item if the block is not air } var adjChunk = tuple.Item2 == Face.Top || tuple.Item2 == Face.Bottom ? chunk : Chunks[tuple.Item1]; //get the chunk in case the adjacent position crosses a chunk boundary var lightSourceToRemove = adjChunk.LightSources.FirstOrDefault(lightSource => tuple.Item1.IsOnBlock(ref lightSource.Value.Coords)); if (lightSourceToRemove.Value != null && lightSourceToRemove.Value.AttachedToFace == tuple.Item2.ToOpposite()) //remove the light source { LightSource temp; chunk.LightSources.TryRemove(lightSourceToRemove.Key, out temp); } } //if theres a dynamic item on top of this block then let it fall foreach (var item in chunk.GameItems.Values) { if (!item.IsMoving && item.Coords.Xblock == position.X && item.Coords.Yblock == position.Y + 1 && item.Coords.Zblock == position.Z) { item.IsMoving = true; } } } if (!Config.IsServer && !isMultipleBlockPlacement) { Debug.WriteLineIf(type == Block.BlockType.Ice && oldType == Block.BlockType.Water, "Growth change Water->Ice; Multiple lightbox updates and chunk queues are possible"); ModifyLightAndQueueChunksForBlockChange(position, isTransparentOldBlock != isTransparentBlock, type); //sounds dont play for multi/cuboid placements; they are responsible for their own sounds; prevents sound spam switch (type) { case Block.BlockType.Air: switch (oldType) { case Block.BlockType.Water: //remove water Sounds.Audio.PlaySound(Sounds.SoundType.JumpOutOfWater, ref position); break; default: //remove another type Sounds.Audio.PlaySound(Sounds.SoundType.RemoveBlock, ref position); break; } break; case Block.BlockType.Water: //place water Sounds.Audio.PlaySound(Sounds.SoundType.JumpOutOfWater, ref position); break; default: //only play the add block sound if the old type was air; the only way blocks can change otherwise are the auto changes in chunks for grass/snow/dirt/etc. if (oldType == Block.BlockType.Air) { Sounds.Audio.PlaySound(Sounds.SoundType.AddBlock, ref position); } break; } } }
/// <summary>Only called for SinglePlayer and Servers.</summary> private void WaterExpand() { Debug.WriteLine("Water expanding in chunk {0}...", Coords); var newWater = new List<Position>(); for (var i = 0; i < CHUNK_SIZE; i++) { for (var j = 0; j < CHUNK_HEIGHT; j++) { for (var k = 0; k < CHUNK_SIZE; k++) { if (Blocks[i, j, k].Type != Block.BlockType.Water) continue; var belowCurrent = new Position(); for (var q = 0; q < 5; q++) { Position adjacent; switch (q) { case 0: adjacent = new Position(Coords.WorldCoordsX + i, j - 1, Coords.WorldCoordsZ + k); belowCurrent = adjacent; break; case 1: adjacent = new Position(Coords.WorldCoordsX + i + 1, j, Coords.WorldCoordsZ + k); break; case 2: adjacent = new Position(Coords.WorldCoordsX + i - 1, j, Coords.WorldCoordsZ + k); break; case 3: adjacent = new Position(Coords.WorldCoordsX + i, j, Coords.WorldCoordsZ + k + 1); break; default: adjacent = new Position(Coords.WorldCoordsX + i, j, Coords.WorldCoordsZ + k - 1); break; } if (newWater.Contains(adjacent)) continue; //if there's air or water below the current block, don't spread sideways if (q != 0 && belowCurrent.IsValidBlockLocation && (Blocks[belowCurrent].Type == Block.BlockType.Air || Blocks[belowCurrent].Type == Block.BlockType.Water)) continue; if (adjacent.IsValidBlockLocation && adjacent.GetBlock().Type == Block.BlockType.Air) newWater.Add(adjacent); } } } } if (newWater.Count == 0) { WaterExpanding = false; Debug.WriteLine("Water finished expanding in chunk {0}", Coords); return; } var addBlocks = new List<AddBlock>(); Settings.ChunkUpdatesDisabled = true; //change blocks while updates are disabled so chunk is only rebuilt once foreach (var newWaterPosition in newWater.Where(newWaterCoords => newWaterCoords.GetBlock().Type != Block.BlockType.Water)) { WorldData.PlaceBlock(newWaterPosition, Block.BlockType.Water); if (Config.IsServer) { var temp = newWaterPosition; addBlocks.Add(new AddBlock(ref temp, Block.BlockType.Water)); } } Settings.ChunkUpdatesDisabled = false; if (Config.IsServer && addBlocks.Count > 0) { foreach (var player in Server.Controller.Players.Values) { var addBlockMulti = new AddBlockMulti {ConnectedPlayer = player}; addBlockMulti.Blocks.AddRange(addBlocks); addBlockMulti.Send(); } } }
/// <summary>Place a single block in the world. This will mark the block as dirty.</summary> /// <param name="position">position to place the block at</param> /// <param name="type">type of block to place</param> /// <param name="isMultipleBlockPlacement">Use this when placing multiple blocks at once so lighting and chunk queueing only happens once.</param> internal static void PlaceBlock(Position position, Block.BlockType type, bool isMultipleBlockPlacement = false) { if (!position.IsValidBlockLocation || position.Y <= 0) return; //this was a multiple block placement, prevent placing blocks on yourself and getting stuck; used to be able to place cuboids on yourself and get stuck //only check in single player for now because in multiplayer this could allow the blocks on different clients to get out of sync and placements of multiple blocks in multiplayer will be rare if (Config.IsSinglePlayer && isMultipleBlockPlacement && (position.IsOnBlock(ref Game.Player.Coords) || position == Game.Player.CoordsHead.ToPosition())) return; if (type == Block.BlockType.Air) { //if destroying a block under water, fill with water instead of air if (position.Y + 1 < Chunk.CHUNK_HEIGHT && GetBlock(position.X, position.Y + 1, position.Z).Type == Block.BlockType.Water) type = Block.BlockType.Water; } var chunk = Chunks[position]; var block = position.GetBlock(); var oldType = block.Type; block.Type = type; //assign the new type var isTransparentBlock = Block.IsBlockTypeTransparent(type); var isTransparentOldBlock = Block.IsBlockTypeTransparent(oldType); block.BlockData = (ushort)(block.BlockData | 0x8000); //mark the block as dirty for the save file "diff" logic chunk.Blocks[position] = block; //insert the new block chunk.UpdateHeightMap(ref block, position.X % Chunk.CHUNK_SIZE, position.Y, position.Z % Chunk.CHUNK_SIZE); if (!isTransparentBlock || type == Block.BlockType.Water) { var below = position; below.Y--; if (below.Y > 0) { if (below.GetBlock().Type == Block.BlockType.Grass || below.GetBlock().Type == Block.BlockType.Snow) { PlaceBlock(below, Block.BlockType.Dirt, true); //dont queue with this dirt block change, the source block changing takes care of it, prevents double queueing the chunk and playing sound twice } } } if (!chunk.WaterExpanding) //water is not expanding, check if this change should make it start { switch (type) { case Block.BlockType.Water: chunk.WaterExpanding = true; break; case Block.BlockType.Air: for (var q = 0; q < 5; q++) { Position adjacent; switch (q) { case 0: adjacent = new Position(position.X + 1, position.Y, position.Z); break; case 1: adjacent = new Position(position.X - 1, position.Y, position.Z); break; case 2: adjacent = new Position(position.X, position.Y + 1, position.Z); break; case 3: adjacent = new Position(position.X, position.Y, position.Z + 1); break; default: adjacent = new Position(position.X, position.Y, position.Z - 1); break; } if (adjacent.IsValidBlockLocation && adjacent.GetBlock().Type == Block.BlockType.Water) { Chunks[adjacent].WaterExpanding = true; } } break; } } //its easier to just set .GrassGrowing on all affected chunks to true, and then let that logic figure it out and turn it off, this way the logic is contained in one spot //and also the logic here doesnt need to run every time a block gets placed. ie: someone is building a house, its running through this logic for every block placement; //now it can only check once on the grass grow interval and turn it back off //gm: an additional optimization, grass could never start growing unless this is an air block and its replacing a non transparent block //OR this is a non transparent block filling in a previously transparent block to cause grass to die if (!isTransparentBlock || (type == Block.BlockType.Air && !isTransparentOldBlock)) { chunk.GrassGrowing = true; if (position.IsOnChunkBorder) { foreach (var adjacentChunk in position.BorderChunks) adjacentChunk.GrassGrowing = true; } } //determine if any static game items need to be removed as a result of this block placement if (type != Block.BlockType.Air) { lock (chunk.Clutters) //lock because clutter is stored in a HashSet { //if theres clutter on this block then destroy it to place the block (FirstOrDefault returns null if no match is found) var clutterToRemove = chunk.Clutters.FirstOrDefault(clutter => position.IsOnBlock(ref clutter.Coords)); if (clutterToRemove != null) chunk.Clutters.Remove(clutterToRemove); } var lightSourceToRemove = chunk.LightSources.FirstOrDefault(lightSource => position.IsOnBlock(ref lightSource.Value.Coords)); if (lightSourceToRemove.Value != null) { LightSource temp; chunk.LightSources.TryRemove(lightSourceToRemove.Key, out temp); } } else //destroying block { lock (chunk.Clutters) //lock because clutter is stored in a HashSet { //if theres clutter on top of this block then remove it as well (FirstOrDefault returns null if no match is found) var clutterToRemove = chunk.Clutters.FirstOrDefault(clutter => clutter.Coords.Xblock == position.X && clutter.Coords.Yblock == position.Y + 1 && clutter.Coords.Zblock == position.Z); //add one to Y to look on the block above if (clutterToRemove != null) chunk.Clutters.Remove(clutterToRemove); } //look on ALL 6 adjacent blocks for static items, and those only get destroyed if its on the matching opposite attached to face var adjacentPositions = position.AdjacentPositionFaces; foreach (var tuple in adjacentPositions) { var adjBlock = tuple.Item1.GetBlock(); if (adjBlock.Type != Block.BlockType.Air) continue; //position cannot contain an item if the block is not air var adjChunk = tuple.Item2 == Face.Top || tuple.Item2 == Face.Bottom ? chunk : Chunks[tuple.Item1]; //get the chunk in case the adjacent position crosses a chunk boundary var lightSourceToRemove = adjChunk.LightSources.FirstOrDefault(lightSource => tuple.Item1.IsOnBlock(ref lightSource.Value.Coords)); if (lightSourceToRemove.Value != null && lightSourceToRemove.Value.AttachedToFace == tuple.Item2.ToOpposite()) //remove the light source { LightSource temp; chunk.LightSources.TryRemove(lightSourceToRemove.Key, out temp); } } //if theres a dynamic item on top of this block then let it fall foreach (var item in chunk.GameItems.Values) { if (!item.IsMoving && item.Coords.Xblock == position.X && item.Coords.Yblock == position.Y + 1 && item.Coords.Zblock == position.Z) { item.IsMoving = true; } } } if (!Config.IsServer && !isMultipleBlockPlacement) { Debug.WriteLineIf(type == Block.BlockType.Ice && oldType == Block.BlockType.Water, "Growth change Water->Ice; Multiple lightbox updates and chunk queues are possible"); ModifyLightAndQueueChunksForBlockChange(position, isTransparentOldBlock != isTransparentBlock, type); //sounds dont play for multi/cuboid placements; they are responsible for their own sounds; prevents sound spam switch (type) { case Block.BlockType.Air: switch (oldType) { case Block.BlockType.Water: //remove water Sounds.Audio.PlaySound(Sounds.SoundType.JumpOutOfWater, ref position); break; default: //remove another type Sounds.Audio.PlaySound(Sounds.SoundType.RemoveBlock, ref position); break; } break; case Block.BlockType.Water: //place water Sounds.Audio.PlaySound(Sounds.SoundType.JumpOutOfWater, ref position); break; default: //only play the add block sound if the old type was air; the only way blocks can change otherwise are the auto changes in chunks for grass/snow/dirt/etc. if (oldType == Block.BlockType.Air) Sounds.Audio.PlaySound(Sounds.SoundType.AddBlock, ref position); break; } } }