Exemple #1
0
 public AddCuboid(Position position1, Position position2, Block.BlockType blockType)
     : this()
 {
     Position1 = position1;
     Position2 = position2;
     BlockType = blockType;
 }
Exemple #2
0
 public AddBlock(ref Position position, Block.BlockType blockType)
     : this()
 {
     if (blockType == Block.BlockType.Air) throw new Exception("You can't place air, use RemoveBlock");
     Position = position;
     BlockType = blockType;
 }
 public AddBlockItem(ref Coords coords, ref Vector3 velocity, Block.BlockType blockType, int gameObjectId = -1)
     : this()
 {
     Coords = coords;
     Velocity = velocity;
     BlockType = blockType;
     GameObjectId = gameObjectId;
 }
Exemple #4
0
        internal Projectile(ref Coords coords, Block.BlockType blockType, bool allowBounce, Vector3? velocity = null, int id = -1)
            : base(ref coords, GameItemType.Projectile, allowBounce, velocity, id)
        {
            BlockType = blockType;

            //Stop += OnItemStop;
            Decay += OnItemDecay;
        }
 public AddProjectile(ref Coords coords, ref Vector3 velocity, Block.BlockType blockType, bool allowBounce, int gameObjectId = -1)
     : this()
 {
     Coords = coords;
     Velocity = velocity;
     BlockType = blockType;
     AllowBounce = allowBounce;
     GameObjectId = gameObjectId;
 }
Exemple #6
0
        internal BlockItem(ref Coords coords, Block.BlockType blockType, Vector3? velocity = null, int id = -1)
            : base(ref coords, GameItemType.BlockItem, true, velocity, id)
        {
            Coords.Xf = (float)Math.Floor(Coords.Xf) + Constants.HALF_BLOCK_SIZE; //spawn in the middle of the block
            Coords.Yf += Constants.HALF_BLOCK_SIZE;
            Coords.Zf = (float)Math.Floor(Coords.Zf) + Constants.HALF_BLOCK_SIZE;
            if (!Coords.IsValidItemLocation) throw new Exception(string.Format("Invalid BlockItem location: {0}", Coords));

            switch (blockType)
            {
                case Block.BlockType.Grass:
                case Block.BlockType.Snow:
                    BlockType = Block.BlockType.Dirt;
                    break;
                default:
                    BlockType = blockType;
                    break;
            }
        }
Exemple #7
0
 /// <summary>Check if key pressed is bound to an action button.</summary>
 public static bool IsActionKey(char keyBind, out Block.BlockType? blockType, out LightSourceType? lightSourceType)
 {
     foreach (var actionButton in _actionButtons.Where(actionButton => actionButton.KeyBind == keyBind))
     {
         if (actionButton.LightSourceType.HasValue)
         {
             lightSourceType = actionButton.LightSourceType.Value;
             blockType = null;
         }
         else
         {
             blockType = actionButton.BlockType;
             lightSourceType = null;
         }
         return true;
     }
     blockType = null;
     lightSourceType = null;
     return false;
 }
Exemple #8
0
 private static void AddButtonToPickerGrid(ref int col, ref int y, string name, int texture, Block.BlockType blockType = 0, LightSourceType? lightSourceType = null)
 {
     if (name.StartsWith("Placeholder")) return;
     Debug.Assert(!(blockType != 0 && lightSourceType.HasValue), "Cannot have block and light source types at the same time.");
     if (col % GRID_BUTTONS_PER_ROW == 0)
     {
         col = 1;
         y += BUTTON_SIZE;
     }
     var button = new Button(ButtonType.GridPicker, Settings.Game.Width / 2 - (GRID_BUTTONS_PER_ROW / 2 * BUTTON_SIZE) + (col * BUTTON_SIZE), y, texture)
         {
             BlockType = blockType,
             LightSourceType = lightSourceType
         };
     _blockPickerGridButtons.Add(button);
     col++;
 }
Exemple #9
0
        /// <summary>Decide if this block face needs to be added to a VBO and rendered. XYZ are world relative coordinates.</summary>
        private void AddBlockFace(ref Block block, Face face, int x, int y, int z)
        {
            int adjacentX = x, adjacentY = y, adjacentZ = z;

            //check if surface is adjacent to another solid texture (if so dont draw it)
            switch (face)
            {
                case Face.Right: adjacentX++; break;
                case Face.Left: adjacentX--; break;
                case Face.Top: adjacentY++; break;
                case Face.Bottom: adjacentY--; break;
                case Face.Front: adjacentZ++; break;
                default: adjacentZ--; break; //back
            }

            //todo: possible optimization, get the Block from this.Blocks array when x/z are NOT on chunk edge, would result in not looking up chunk in World.GetBlock 99% of time, but tiny bit more checks for blocks on edges, try it out some other time
            //-could accept a bool param needToCheckOverChunkEdge, the DTL logic would always pass false for it because its alrdy figured it out, so both ways get the optimization
            #region todo: none of this would need to be checked for the DTL added faces, could grab the block straight out of this chunk instead, wouldnt need to check adjacent block either
            if (!WorldData.IsValidBlockLocation(adjacentX, adjacentY, adjacentZ)) return; //adjacent block is outside the world so theres no need to render this face
            var adjacentBlock = WorldData.GetBlock(adjacentX, adjacentY, adjacentZ);
            if (!adjacentBlock.IsTransparent) return; //no need to render this face because the adjacent face is not transparent

            //dont render inner faces of neighboring transparent blocks of same type, makes trees hollow and doesnt draw additional water faces under water
            //improves fps by limiting the number of faces needed and looks better on weak laptop gpu's as well (note: we know the adjacent face is transparent if we get this far)
            if (block.IsTransparent && block.Type == adjacentBlock.Type) return;
            #endregion

            var texture = Block.FaceTexture(block.Type, face);
            //check if there is an existing vbo for this chunk/texture to add this face to
            if (_chunkVbos[(int)texture] == null)
            {
                //vbo not created yet for this chunk/texture so we need to start one
                _chunkVbos[(int)texture] = new ChunkVbo(ref block, TextureLoader.GetBlockTexture(texture));
            }

            //we need position 4 no matter what, so retrieve it here, position 4 is used for the lighting values of all 4 vertices of this face when smooth lighting is off
            byte lightColor = WorldData.GetBlockLightColor(adjacentX, adjacentY, adjacentZ);

            _shortestFaceHeightTemp = Math.Min(_shortestFaceHeightTemp, y);

            if (block.Type == Block.BlockType.Leaves || block.Type == Block.BlockType.SnowLeaves)
            {
                //dont bother with smooth lighting for leaves, it would rarely matter anyway
                if (face == Face.Bottom) lightColor = (byte)(lightColor * 0.6); //make bottom leaves faces darker by giving them 60% of the light they would otherwise have
                BlockRender.AddBlockFaceToVbo(_chunkVbos[(int)texture], face, x, y, z, lightColor);
                return;
            }

            if (Config.SmoothLighting)
            {
                #region Smooth Lighting
                //SMOOTH LIGHTING MAP (block 4 is used by all 4 vertices, blocks 1,3,5,7 are used by 2 vertices each, blocks 0,2,6,8 are used by one vertex only)
                //	0  1  2
                //   v1 v0
                //	3  4  5
                //   v2 v3
                //	6  7  8
                var xyz = new Position[9];
                var smoothLighting = new byte[9];
                smoothLighting[4] = lightColor; //we already have the directly adjacent color which goes in position 4
                switch (face)
                {
                    case Face.Right: //X is constant
                        xyz[0] = new Position(adjacentX, adjacentY + 1, adjacentZ + 1);
                        xyz[1] = new Position(adjacentX, adjacentY + 1, adjacentZ);
                        xyz[2] = new Position(adjacentX, adjacentY + 1, adjacentZ - 1);
                        xyz[3] = new Position(adjacentX, adjacentY, adjacentZ + 1);
                        xyz[5] = new Position(adjacentX, adjacentY, adjacentZ - 1);
                        xyz[6] = new Position(adjacentX, adjacentY - 1, adjacentZ + 1);
                        xyz[7] = new Position(adjacentX, adjacentY - 1, adjacentZ);
                        xyz[8] = new Position(adjacentX, adjacentY - 1, adjacentZ - 1);
                        break;
                    case Face.Left: //X is constant
                        xyz[0] = new Position(adjacentX, adjacentY + 1, adjacentZ - 1);
                        xyz[1] = new Position(adjacentX, adjacentY + 1, adjacentZ);
                        xyz[2] = new Position(adjacentX, adjacentY + 1, adjacentZ + 1);
                        xyz[3] = new Position(adjacentX, adjacentY, adjacentZ - 1);
                        xyz[5] = new Position(adjacentX, adjacentY, adjacentZ + 1);
                        xyz[6] = new Position(adjacentX, adjacentY - 1, adjacentZ - 1);
                        xyz[7] = new Position(adjacentX, adjacentY - 1, adjacentZ);
                        xyz[8] = new Position(adjacentX, adjacentY - 1, adjacentZ + 1);
                        break;
                    case Face.Top: //Y is constant
                        xyz[0] = new Position(adjacentX - 1, adjacentY, adjacentZ - 1);
                        xyz[1] = new Position(adjacentX, adjacentY, adjacentZ - 1);
                        xyz[2] = new Position(adjacentX + 1, adjacentY, adjacentZ - 1);
                        xyz[3] = new Position(adjacentX - 1, adjacentY, adjacentZ);
                        xyz[5] = new Position(adjacentX + 1, adjacentY, adjacentZ);
                        xyz[6] = new Position(adjacentX - 1, adjacentY, adjacentZ + 1);
                        xyz[7] = new Position(adjacentX, adjacentY, adjacentZ + 1);
                        xyz[8] = new Position(adjacentX + 1, adjacentY, adjacentZ + 1);
                        break;
                    case Face.Bottom: //Y is constant
                        xyz[0] = new Position(adjacentX - 1, adjacentY, adjacentZ + 1);
                        xyz[1] = new Position(adjacentX, adjacentY, adjacentZ + 1);
                        xyz[2] = new Position(adjacentX + 1, adjacentY, adjacentZ + 1);
                        xyz[3] = new Position(adjacentX - 1, adjacentY, adjacentZ);
                        xyz[5] = new Position(adjacentX + 1, adjacentY, adjacentZ);
                        xyz[6] = new Position(adjacentX - 1, adjacentY, adjacentZ - 1);
                        xyz[7] = new Position(adjacentX, adjacentY, adjacentZ - 1);
                        xyz[8] = new Position(adjacentX + 1, adjacentY, adjacentZ - 1);
                        break;
                    case Face.Front: //Z is constant
                        xyz[0] = new Position(adjacentX - 1, adjacentY + 1, adjacentZ);
                        xyz[1] = new Position(adjacentX, adjacentY + 1, adjacentZ);
                        xyz[2] = new Position(adjacentX + 1, adjacentY + 1, adjacentZ);
                        xyz[3] = new Position(adjacentX - 1, adjacentY, adjacentZ);
                        xyz[5] = new Position(adjacentX + 1, adjacentY, adjacentZ);
                        xyz[6] = new Position(adjacentX - 1, adjacentY - 1, adjacentZ);
                        xyz[7] = new Position(adjacentX, adjacentY - 1, adjacentZ);
                        xyz[8] = new Position(adjacentX + 1, adjacentY - 1, adjacentZ);
                        break;
                    default: //Back; Z is constant
                        xyz[0] = new Position(adjacentX + 1, adjacentY + 1, adjacentZ);
                        xyz[1] = new Position(adjacentX, adjacentY + 1, adjacentZ);
                        xyz[2] = new Position(adjacentX - 1, adjacentY + 1, adjacentZ);
                        xyz[3] = new Position(adjacentX + 1, adjacentY, adjacentZ);
                        xyz[5] = new Position(adjacentX - 1, adjacentY, adjacentZ);
                        xyz[6] = new Position(adjacentX + 1, adjacentY - 1, adjacentZ);
                        xyz[7] = new Position(adjacentX, adjacentY - 1, adjacentZ);
                        xyz[8] = new Position(adjacentX - 1, adjacentY - 1, adjacentZ);
                        break;
                }

                //need to know if blocks on positions 1,3,5,7 are transparent, each value is used twice
                //-for example neither 1 or 5 are transparent, then vertex0 should not have any influence from light at position 2 and should use the darkest value instead for a dark corner
                //		-position 2 might be on the other side of a wall and then we get sunlight coming into ceiling corners etc
                //-if the coords of any of these positions are outside the world then consider them transparent
                bool transparent1 = !xyz[1].IsValidBlockLocation || xyz[1].GetBlock().IsTransparent;
                bool transparent3 = !xyz[3].IsValidBlockLocation || xyz[3].GetBlock().IsTransparent;
                bool transparent5 = !xyz[5].IsValidBlockLocation || xyz[5].GetBlock().IsTransparent;
                bool transparent7 = !xyz[7].IsValidBlockLocation || xyz[7].GetBlock().IsTransparent;

                smoothLighting[0] = transparent1 || transparent3 ? xyz[0].LightColor : Lighting.DARKEST_COLOR;
                smoothLighting[1] = xyz[1].LightColor;
                smoothLighting[2] = transparent1 || transparent5 ? xyz[2].LightColor : Lighting.DARKEST_COLOR;
                smoothLighting[3] = xyz[3].LightColor;
                smoothLighting[5] = xyz[5].LightColor;
                smoothLighting[6] = transparent3 || transparent7 ? xyz[6].LightColor : Lighting.DARKEST_COLOR;
                smoothLighting[7] = xyz[7].LightColor;
                smoothLighting[8] = transparent5 || transparent7 ? xyz[8].LightColor : Lighting.DARKEST_COLOR;

                //average 4 colors to get the color for each vertex
                var v0Color = (byte)((smoothLighting[1] + smoothLighting[2] + smoothLighting[4] + smoothLighting[5]) / 4);
                var v1Color = (byte)((smoothLighting[0] + smoothLighting[1] + smoothLighting[3] + smoothLighting[4]) / 4);
                var v2Color = (byte)((smoothLighting[3] + smoothLighting[4] + smoothLighting[6] + smoothLighting[7]) / 4);
                var v3Color = (byte)((smoothLighting[4] + smoothLighting[5] + smoothLighting[7] + smoothLighting[8]) / 4);

                BlockRender.AddBlockFaceToVbo(_chunkVbos[(int)texture], face, x, y, z, v0Color, v1Color, v2Color, v3Color);
                #endregion
            }
            else //use simple lighting
            {
                BlockRender.AddBlockFaceToVbo(_chunkVbos[(int)texture], face, x, y, z, lightColor);
            }
        }
Exemple #10
0
        /// <summary>Updates the heightmap following a block placement. Usually a lot quicker then re-building the heightmap.</summary>
        internal void UpdateHeightMap(ref Block block, int chunkRelativeX, int yLevel, int chunkRelativeZ)
        {
            var currentHeight = HeightMap[chunkRelativeX, chunkRelativeZ];
            if (block.IsTransparent) //transparent block
            {
                //update height map
                if (yLevel == currentHeight)
                {
                    //transparent block being placed at the previous heightmap level, most likely removing a block (which places Air), so we need to find the next non transparent block for the heightmap
                    for (var y = currentHeight - 1; y >= 0; y--) //start looking down from the previous heightmap level
                    {
                        if (y > 0 && Blocks[chunkRelativeX, y, chunkRelativeZ].IsTransparent) continue;
                        //found the next non transparent block, update the heightmap and exit
                        HeightMap[chunkRelativeX, chunkRelativeZ] = y;
                        break;
                    }
                }

                //update deepest transparent level
                if (yLevel < DeepestTransparentLevel) DeepestTransparentLevel = yLevel;
            }
            else //non transparent block
            {
                //update height map
                //when placing a non transparent block, check if its above the current heightmap value and if so update the heightmap
                if (yLevel > currentHeight) HeightMap[chunkRelativeX, chunkRelativeZ] = yLevel;

                //update deepest transparent level
                if (yLevel == DeepestTransparentLevel)
                {
                    //this block is being set at the DeepestTransparentLevel of this chunk
                    //we will need to calc if this is still the deepest level (because theres another transparent block at this depth) or what the new level is
                    //the easiest way to do that is just rebuild the height map, even though all we really need to do is the portion that updates the deepest level
                    BuildHeightMap();
                    return; //no need to continue on to check anything else when doing a full heightmap rebuild
                }
            }

            //update HighestNonAirLevel property
            //1. if placing air (removing block), is it at same level as previous HighestNonAir?, just rebuild HeightMap in this case, otherwise do nothing
            //2. if placing anything other then air, simply check if its > HighestNonAirLevel and set it
            if (block.Type == Block.BlockType.Air) //removing a block
            {
                if (yLevel == HighestNonAirLevel) BuildHeightMap();
            }
            else //adding a block
            {
                if (yLevel > HighestNonAirLevel) HighestNonAirLevel = yLevel;
            }
        }
        public static void BuildCastle(Position center, int radius, int height, Block.BlockType blockType, Facing frontFace)
        {
            //make sure diagonal positions are valid
            if (!WorldData.IsValidBlockLocation(center.X - radius, center.Y, center.Z - radius)) return;
            if (!WorldData.IsValidBlockLocation(center.X + radius, center.Y, center.Z + radius)) return;

            //clear area with air
            WorldData.PlaceCuboid(new Position(center.X - radius, center.Y, center.Z - radius), new Position(center.X + radius, center.Y + height - 1, center.Z + radius), Block.BlockType.Air, true);
            //front wall
            WorldData.PlaceCuboid(new Position(center.X - radius, center.Y, center.Z + radius), new Position(center.X + radius, center.Y + height - 1, center.Z + radius), blockType, true);
            //back wall
            WorldData.PlaceCuboid(new Position(center.X - radius, center.Y, center.Z - radius), new Position(center.X + radius, center.Y + height - 1, center.Z - radius), blockType, true);
            //left wall
            WorldData.PlaceCuboid(new Position(center.X - radius, center.Y, center.Z - radius), new Position(center.X - radius, center.Y + height - 1, center.Z + radius), blockType, true);
            //right wall
            WorldData.PlaceCuboid(new Position(center.X + radius, center.Y, center.Z - radius), new Position(center.X + radius, center.Y + height - 1, center.Z + radius), blockType, true);

            //front and back tops
            for (var x = center.X - radius; x <= center.X + radius; x += 2)
            {
                WorldData.PlaceBlock(new Position(x, center.Y + height, center.Z + radius), blockType, true);
                WorldData.PlaceBlock(new Position(x, center.Y + height, center.Z - radius), blockType, true);
            }
            //side tops
            for (var z = center.Z - radius; z <= center.Z + radius; z += 2)
            {
                WorldData.PlaceBlock(new Position(center.X + radius, center.Y + height, z), blockType, true);
                WorldData.PlaceBlock(new Position(center.X - radius, center.Y + height, z), blockType, true);
            }

            //top wood floor
            WorldData.PlaceCuboid(new Position(center.X - radius + 1, center.Y + height - 2, center.Z - radius + 1), new Position(center.X + radius - 1, center.Y + height - 2, center.Z + radius - 1), Block.BlockType.WoodTile2, true);

            //door/lights
            Position doorBottom = center;
            Position outsideLight1;
            Position outsideLight2;
            switch (frontFace)
            {
                case Facing.North:
                    doorBottom.Z -= radius;
                    outsideLight1 = doorBottom;
                    outsideLight1.Z--;
                    outsideLight2 = outsideLight1;
                    outsideLight1.X--;
                    outsideLight2.X++;
                    break;
                case Facing.East:
                    doorBottom.X += radius;
                    outsideLight1 = doorBottom;
                    outsideLight1.X++;
                    outsideLight2 = outsideLight1;
                    outsideLight1.Z--;
                    outsideLight2.Z++;
                    break;
                case Facing.South:
                    doorBottom.Z += radius;
                    outsideLight1 = doorBottom;
                    outsideLight1.Z++;
                    outsideLight2 = outsideLight1;
                    outsideLight1.X--;
                    outsideLight2.X++;
                    break;
                default:
                    doorBottom.X -= radius;
                    outsideLight1 = doorBottom;
                    outsideLight1.X--;
                    outsideLight2 = outsideLight1;
                    outsideLight1.Z--;
                    outsideLight2.Z++;
                    break;
            }
            outsideLight1.Y += 2;
            outsideLight2.Y += 2;
            WorldData.PlaceCuboid(doorBottom, new Position(doorBottom.X, doorBottom.Y + 1, doorBottom.Z), Block.BlockType.Air, true); //clear space for door

            //add lights
            //todo: downside to this is its causing extra chunk queues and lightbox rebuilds, only for structure builds so not a huge deal for now
            //-clients are independently adding the lightsources, so id's might not match with server, for now theres no reason requiring them to match
            //-another quirk is if the location ends up not being valid, the client will get an error msg and not know why if this came from the server
            //-eventually may want a way for the AddStructure packet to include them, structures might get built containing many static items...
            if (WorldData.IsValidStaticItemPosition(outsideLight1))
            {
                var ls1 = new LightSource(ref outsideLight1, LightSourceType.Lantern, frontFace.ToFace().ToOpposite());
                new LightSource(ref ls1.Coords, ls1.Type, ls1.AttachedToFace, ls1.Id);
            }
            if (WorldData.IsValidStaticItemPosition(outsideLight2))
            {
                var ls2 = new LightSource(ref outsideLight2, LightSourceType.Lantern, frontFace.ToFace().ToOpposite());
                new LightSource(ref ls2.Coords, ls2.Type, ls2.AttachedToFace, ls2.Id);
            }
        }
Exemple #12
0
 /// <summary>
 /// Update the applicable light box and queue the chunks that were affected. Skip light update if the changing block is transparent as it would have no effect on light.
 /// Light box update is done in a task which queues the affected chunks only after the light box update is finished.
 /// </summary>
 /// <param name="position">Position the light change is originating at (the block that is changing).</param>
 /// <param name="changeInTransparency">Lighting only needs to be recalculated if we are replacing a solid with a transparent or vice versa</param>
 /// <param name="blockType">New block type being placed. Will be Air for block remove.</param>
 private static void ModifyLightAndQueueChunksForBlockChange(Position position, bool changeInTransparency, Block.BlockType blockType)
 {
     if (!changeInTransparency) //transparency did not change; no effect on lighting
     {
         if (position.IsOnChunkBorder)
         {
             var chunks = new Queue<Chunk>();
             chunks.Enqueue(Chunks[position]); //queue this chunk first
             foreach (Chunk chunk in position.BorderChunks) chunks.Enqueue(chunk); //now queue 1 adjacent chunk as well when on chunk border or 2 adjacent chunks when on chunk corner
             Debug.WriteLine("Growth change on chunk border {0}:{1}; Lighting not affected; Queueing {2} chunks", blockType, position, chunks.Count);
             QueueAffectedChunks(chunks);
         }
         else //not on a chunk border, queue this chunk only
         {
             Debug.WriteLine("Growth change {0}:{1}; Lighting not affected; Queue local chunk only", blockType, position);
             Chunks[position].QueueImmediate();
         }
     }
     else
     {
         Task<Queue<Chunk>>.Factory.StartNew(() => Lighting.UpdateLightBox(ref position, null, true, blockType == Block.BlockType.Air)).ContinueWith(task => QueueAffectedChunks(task.Result));
     }
 }
Exemple #13
0
 /// <summary>Place multiple blocks in the world of the same type.</summary>
 /// <param name="startPosition">start placing blocks at</param>
 /// <param name="endPosition">stop placing blocks at</param>
 /// <param name="type">type of block to place</param>
 /// <param name="isMultipleCuboidPlacement">Use this when placing multiple cuboids at once so lighting and chunk queueing only happens once.</param>
 internal static void PlaceCuboid(Position startPosition, Position endPosition, Block.BlockType type, bool isMultipleCuboidPlacement = false)
 {
     for (var x = Math.Min(startPosition.X, endPosition.X); x <= Math.Max(startPosition.X, endPosition.X); x++)
     {
         for (var y = Math.Min(startPosition.Y, endPosition.Y); y <= Math.Max(startPosition.Y, endPosition.Y); y++)
         {
             for (var z = Math.Min(startPosition.Z, endPosition.Z); z <= Math.Max(startPosition.Z, endPosition.Z); z++)
             {
                 PlaceBlock(new Position(x, y, z), type, true);
             }
         }
     }
     if (!Config.IsServer && !isMultipleCuboidPlacement) ModifyLightAndQueueChunksForCuboidChange(startPosition, endPosition);
 }
Exemple #14
0
        /// <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>Send message to the server to add or remove a block. Block will be removed if the block type is Air.</summary>
        /// <param name="position">position to add the block</param>
        /// <param name="blockType">block type to add</param>
        public static void SendAddOrRemoveBlock(Position position, Block.BlockType blockType)
        {
            if (!position.IsValidBlockLocation) return;
            if (blockType == Block.BlockType.Air) //remove block
            {
                if (position.Y == 0) { Game.UiHost.AddChatMessage(new ChatMessage(ChatMessageType.Error, "Cannot remove a block at the base of the world. Block cancelled.")); return; }
                new RemoveBlock(ref position).Send();
            }
            else //add block
            {
                var head = Game.Player.CoordsHead;
                if (!Config.CreativeMode && Game.Player.Inventory[(int)blockType] <= 0) { Game.UiHost.AddChatMessage(new ChatMessage(ChatMessageType.Error, string.Format("No {0} in inventory.", blockType))); return; }
                if (Block.IsBlockTypeSolid(blockType) && (position.IsOnBlock(ref Game.Player.Coords) || position.IsOnBlock(ref head))) { Game.UiHost.AddChatMessage(new ChatMessage(ChatMessageType.Error, "Attempted to build solid block on self. Not smart. Block cancelled.")); return; }

                foreach (var player in Players.Values)
                {
                    head = player.CoordsHead;
                    if (!Block.IsBlockTypeSolid(blockType) || (!position.IsOnBlock(ref player.Coords) && !position.IsOnBlock(ref head))) continue;
                    Game.UiHost.AddChatMessage(new ChatMessage(ChatMessageType.Error, "Attempted to build solid block on other player. Not nice. Block cancelled.")); return;
                }
                new AddBlock(ref position, blockType).Send();
                if (!Config.CreativeMode) Game.Player.Inventory[(int)blockType]--;
            }
        }