// Spludlow Software // Copyright © Samuel P. Ludlow 2020 All Rights Reserved // Distributed under the terms of the GNU General Public License version 3 // Distributed WITHOUT ANY WARRANTY; without implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE // https://www.spludlow.co.uk/LICENCE.TXT // The Spludlow logo is a registered trademark of Samuel P. Ludlow and may not be used without permission // v1.14 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; namespace Spludlow.Tetris { /// /// Thread for listening for new connections /// Each player has a thread for recieving from the client /// Another thread provides the down tick /// public class TetrisServer { public static Encoding Encoding = Encoding.UTF8; private TetrisPlayers _TetrisPlayers = new TetrisPlayers(); private System.Threading.EventWaitHandle _WaitFinished = null; public enum ServerModes { Creative, Standard, Battle, Levels }; private ServerModes _ServerMode = ServerModes.Standard; //private bool CreativeMode = false; // By using [ThreadStatic] every thread will get it's own Random object automatically [ThreadStatic] private static Random _Random; private static int RandomNext(int maxValue) { if (_Random == null) { _Random = new Random(); Spludlow.Log.Info("new Random"); } return _Random.Next(maxValue); } private TetrisShapes _TetrisShapes = new TetrisShapes(); public int _Width = 0; public int _Height = 0; public void Stop() { if (_WaitFinished != null) _WaitFinished.Set(); Spludlow.Log.Info("Tetris Server, Asked to Stop"); } /// /// The main tetris server thread /// Starts the down tick timer /// Listens for incoming connections /// Uses async Accept so can also wait for stop signal with clean exit /// Initilises the player and runs a client thread for each connection /// This method will block (not exit) while the server is running /// public void Run(string hostNameOrAddressAndPortNumber, int width, int height, int milliseconds) { try { _Width = width; _Height = height; if (milliseconds == 0) this._ServerMode = ServerModes.Creative; _WaitFinished = new EventWaitHandle(false, EventResetMode.ManualReset); IPEndPoint serverEndPoint = CreateEndpoint(hostNameOrAddressAndPortNumber); // Create the main server socket, used only or accepting incoming connections using (Socket listener = new Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)) { // Start using the socket on specified address listener.Bind(serverEndPoint); // Start listeding for clients listener.Listen(256); // Run the tick thread if (this._ServerMode != ServerModes.Creative) { Thread tickThread = new Thread(() => this.TickThread(milliseconds)); tickThread.Start(); } Spludlow.Log.Report("Tetris Server, Listening"); // This loop will efectivly sleep until a client connects while (true) { // begin an async Accept IAsyncResult aSyncResult = listener.BeginAccept(null, null); // Wait for the finished flag being set or the Accept int index = WaitHandle.WaitAny(new WaitHandle[] { _WaitFinished, aSyncResult.AsyncWaitHandle }); // Clean exit if it was the finish flag if (index == 0) break; // end the async Accept Socket clientSocket = null; clientSocket = listener.EndAccept(aSyncResult); string ipAddress = ((IPEndPoint)clientSocket.RemoteEndPoint).Address.ToString(); // Set up a player object TetrisPlayer player = new TetrisPlayer(); player.ClientSocket = clientSocket; player.Score = new TetrisPlayerScore(); player.Score.TimeConnect = DateTime.Now; player.Score.TimeGameStart = DateTime.Now; // Initilise player ResetPlayer(player, true); _TetrisPlayers.Add(player); // Start the client thread Thread clientThread = new Thread(() => this.ClientThread(player)); clientThread.Start(); Spludlow.Log.Report("Tetris Server, Accepted Connection: " + player.ClientId + ", " + ipAddress); } Spludlow.Log.Info("Tetris Server, Stopping"); foreach (TetrisPlayer player in this._TetrisPlayers.CurrentPlayers()) { player.ClientSocket.Dispose(); Spludlow.Log.Info("Tetris Server, Disposed Client Socket: " + player.ClientId); } } Spludlow.Log.Finish("Tetris Server, Stopped"); } catch (Exception ee) { Spludlow.Log.Error("Tetris Server, Fatal Error", ee); throw ee; } } /// /// Server's client thread one for each player /// Recieves Tetris messages from the clients and act acordingly, like perform client moves /// public void ClientThread(TetrisPlayer player) { try { while (player.Removed == false) { TetrisMessage message; try { message = TetrisMessage.Receive(player.ClientSocket, _WaitFinished); if (message == null) { Spludlow.Log.Finish("Tetris Server, Client Thread End, Asked to stop: " + player.ClientId); return; } } catch (SocketException ee) { if (ee.ErrorCode == 10053 || ee.ErrorCode == 10054 || ee.ErrorCode == 10060) { this.RemovePlayer(player.ClientId); Spludlow.Log.Finish("Tetris Server, Client Thread End, Lost connection: " + player.ClientId); return; } Spludlow.Log.Error("Tetris Server, Client Thread, Receive Socket Error: " + ee.ErrorCode + ", " + player.ClientId, ee); throw ee; } switch ((TetrisMessage.BodyTypes)message.BodyType) { case TetrisMessage.BodyTypes.Text: string clientText = TetrisServer.Encoding.GetString(message.Body); Spludlow.Log.Info("Tetris Server, Client Thread: Text from client: " + clientText); if (clientText.StartsWith("@") == true) { switch (clientText) { case "@BR": this.ToggleBattle(); break; } } break; case TetrisMessage.BodyTypes.Move: Move(player, message.Body[0]); break; case TetrisMessage.BodyTypes.Name: player.DisplayName = TetrisServer.Encoding.GetString(message.Body); Spludlow.Log.Info("Tetris Server, Client Thread: Display Name: " + player.ClientId + ", " + player.DisplayName); this.SendText(player, "Server V " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()); this.SendText("\"" + player.DisplayName + "\" has joined, id:" + player.ClientId); this.SendSound("OPEN"); break; } } Spludlow.Log.Finish("Tetris Server, Client Thread End, Player Removed Flag set: " + player.ClientId); } catch (Exception ee) { Spludlow.Log.Error("Tetris Server, ClientThread, Fatal Error", ee); this.RemovePlayer(player.ClientId); //throw ee; } } public void ToggleBattle() { if (this._ServerMode != ServerModes.Battle) { this._ServerMode = ServerModes.Battle; foreach (TetrisPlayer player in this._TetrisPlayers.CurrentPlayers()) this.ResetPlayer(player, true); this.SendText("BATTLE ROYALE STARTED"); } } public void SocketSend(TetrisPlayer player, TetrisMessage message) { if (player.Removed == true) return; try { if (message.SocketSend(player.ClientSocket) == false) this.RemovePlayer(player.ClientId); } catch (ObjectDisposedException) { Spludlow.Log.Warning("Tetris TetrisServer, SocketSend: ObjectDisposedException"); return; } } private void RemovePlayer(int clientId) { TetrisPlayer player = this._TetrisPlayers.Remove(clientId); if (player != null) { player.Removed = true; this.SendText("\"" + player.DisplayName + "\" has left"); this.SendSound("CLOSE"); player.ClientSocket.Dispose(); Spludlow.Log.Info("Tetris Server, RemovePlayer: " + clientId); } } /// /// The .net timers did not work well under heavy testing. So rolled own timer thread /// private void TickThread(int milliseconds) { int waitTime = milliseconds; int warn = (int)((decimal)milliseconds * 0.5M); while (this._WaitFinished.WaitOne(waitTime) == false) { DateTime start = DateTime.Now; foreach (TetrisPlayer player in this._TetrisPlayers.CurrentPlayers()) this.Move(player, 1); this.SendSound("TICK"); int processingTime = (int)(DateTime.Now - start).TotalMilliseconds; waitTime = milliseconds - processingTime; if (waitTime < warn) Spludlow.Log.Warning("Tetris Server, Server Overload, Tick took : " + processingTime + " / " + milliseconds); if (waitTime < 0) waitTime = 0; } Spludlow.Log.Finish("Tetris Server, Tick Thread finished"); } public void Move(TetrisPlayer player, byte command) { try { DateTime start = DateTime.Now; lock (player._PlayerLock) { this.MoveWork(player, command); } } catch (Exception ee) { Spludlow.Log.Error("Tetris Server, Move Method: ", ee); } } public void MoveWork(TetrisPlayer player, byte command) { bool moved = false; switch (command) { case 0: // U if (this._ServerMode != ServerModes.Creative) { while (this.PositionValid(player, 0, 1, 0, true) == true) { } this.Merge(player, true); } else { moved = this.PositionValid(player, 0, -1, 0, true); } break; case 1: // D moved = this.PositionValid(player, 0, 1, 0, true); if (moved == false) this.Merge(player, true); break; case 2: // L moved = this.PositionValid(player, -1, 0, 0, true); break; case 3: // R moved = this.PositionValid(player, 1, 0, 0, true); break; case 4: // CW moved = Rotate(player, 1); if (moved == true) this.SendSound(player, "ROTATE"); this.SendNextPeiceBoard(player); break; case 5: // CCW moved = Rotate(player, -1); if (moved == true) this.SendSound(player, "ROTATE"); this.SendNextPeiceBoard(player); break; case 6: // Target Next player.TargetClientId = _TetrisPlayers.TargetNext(player.TargetClientId); this.SendInfo(player); this.SendTargetBoard(player); break; } if (moved == true) this.SendBoard(player); } /// /// Can shape be placed /// public bool PositionValid(TetrisPlayer player, int xMove, int yMove, int rotMove, bool performMove) { int x = player.X + xMove; int y = player.Y + yMove; int rot = player.Rotate + rotMove; if (rot > 3) rot = 0; if (rot < 0) rot = 3; TetrisBoard shape = _TetrisShapes.ShapeBoards[(player.Shape - 1) * 4 + rot]; for (int shapeY = 0; shapeY < 4; ++shapeY) { for (int shapeX = 0; shapeX < 4; ++shapeX) { byte value = shape.Peek(shapeX, shapeY); if (value == 0) continue; int boardX = x + shapeX; int boardY = y + shapeY; if (boardX < 0 || boardY < 0) return false; if (boardX >= player.Board.Width || boardY >= player.Board.Height) return false; if (player.Board.Peek(boardX, boardY) != 0) return false; } } if (performMove == true) { player.X = x; player.Y = y; player.Rotate = rot; } return true; } /// /// Copy board, place current shape if required /// public TetrisBoard Merge(TetrisPlayer player, bool place) { TetrisBoard shape = _TetrisShapes.ShapeBoards[(player.Shape - 1) * 4 + player.Rotate]; TetrisBoard board = TetrisBoard.Merge(player.Board, player.X, player.Y, shape, !place); if (place == true) { player.Board = board; int lines = 0; if (this._ServerMode != ServerModes.Creative) lines = FindLines(player); int sendLines = lines - 1; bool sendSound = false; if (sendLines > 0) { if (player.TargetClientId > 0) { TetrisPlayer targetPlayer = this._TetrisPlayers.Get(player.TargetClientId); if (targetPlayer != null) { targetPlayer.IncomingRows += sendLines; this.SendText("\"" + player.DisplayName + "\" > " + new string('X', sendLines) + " > \"" + targetPlayer.DisplayName + "\""); for (int count = 0; count < sendLines; ++count) this.SendSound("SEND*"); sendSound = true; } else { player.TargetClientId = 0; } } } if (lines > 0 && sendSound == false) { for (int count = 0; count < lines; ++count) this.SendSound(player, "LINE*"); } this.SendSound(player, "DROP"); ResetPlayer(player, false); } return board; } public void ResetPlayer(TetrisPlayer player, bool newBoard) { if (newBoard == true) { player.Board = new TetrisBoard(_Width, _Height); player.Level = 0; } if (player.NextShape == 0) player.NextShape = RandomNext(7) + 1; player.Shape = player.NextShape; player.NextShape = RandomNext(7) + 1; player.X = (player.Board.Width / 2) - 2; player.Y = 0; player.Rotate = 0; if (player.IncomingRows > 0) { for (int count = 0; count < player.IncomingRows; ++count) this.SendSound("RECEIVE*"); this.InsertLines(player); } if (this.PositionValid(player, 0, 0, 0, false) == false) // Game over { player.Board = new TetrisBoard(_Width, _Height); ++player.Score.CountGameOver; this.GameOverText(player); this.SendSound("OVER"); player.Score.TimeGameStart = DateTime.Now; } this.SendInfo(player); this.SendNextPeiceBoard(player); this.SendBoard(player); } private void GameOverText(TetrisPlayer player) { StringBuilder text = new StringBuilder(); string mainLine = "## R.I.P. \"" + player.DisplayName + "\" ##"; string sepLine = new string('#', mainLine.Length); text.AppendLine(sepLine); text.AppendLine(mainLine); text.AppendLine(sepLine); text.AppendLine(" Alive for: " + Spludlow.Text.TimeTook(player.Score.TimeGameStart)); text.AppendLine(" Died " + player.Score.CountGameOver + " times."); text.Append(sepLine); this.SendText(text.ToString()); } public void SendTargetBoard(TetrisPlayer player) { TetrisBoard board = new TetrisBoard(this._Width, this._Height); board.BoardKey = Int32.MaxValue; board.ReDraw = true; if (player.TargetClientId > 0) { TetrisPlayer testPlayer = this._TetrisPlayers.Get(player.TargetClientId); if (testPlayer != null) { board = this.Merge(testPlayer, false); board.BoardKey = player.TargetClientId; board.ReDraw = true; } else { // player gone } } TetrisMessage message = new TetrisMessage(TetrisMessage.BodyTypes.Board, board.Serialize()); SocketSend(player, message); } public void SendBoard(TetrisPlayer player) { List players = new List(); players.Add(player); if (player.ClientId != 0) { foreach (TetrisPlayer testPlayer in this._TetrisPlayers.CurrentPlayers()) { if (testPlayer.TargetClientId == player.ClientId) players.Add(testPlayer); } } TetrisBoard board = this.Merge(player, false); board.BoardKey = 0; for (int index = 0; index < players.Count; ++index) { TetrisPlayer sendPlayer = players[index]; if (index == 1) board.BoardKey = player.ClientId; TetrisMessage message = new TetrisMessage(TetrisMessage.BodyTypes.Board, board.Serialize()); this.SocketSend(sendPlayer, message); } } public void SendNextPeiceBoard(TetrisPlayer player) { TetrisBoard nextShapeBoard = _TetrisShapes.ShapeBoards[(player.NextShape - 1) * 4 + player.Rotate].Copy(); nextShapeBoard.BoardKey = -1; TetrisMessage message = new TetrisMessage(TetrisMessage.BodyTypes.Board, nextShapeBoard.Serialize()); this.SocketSend(player, message); } public void SendSound(string sound) { sound = "!" + sound; TetrisPlayer[] players = this._TetrisPlayers.CurrentPlayers(); foreach (TetrisPlayer player in players) this.SendSound(player, sound); } public void SendSound(TetrisPlayer player, string sound) { TetrisMessage message = new TetrisMessage(TetrisMessage.BodyTypes.Sound, TetrisServer.Encoding.GetBytes(sound)); this.SocketSend(player, message); } public void SendText(string text) { TetrisPlayer[] players = this._TetrisPlayers.CurrentPlayers(); foreach (TetrisPlayer player in players) this.SendText(player, text); } public void SendText(TetrisPlayer player, string text) { TetrisMessage message = new TetrisMessage(TetrisMessage.BodyTypes.Text, TetrisServer.Encoding.GetBytes(text)); this.SocketSend(player, message); } public void SendInfo(TetrisPlayer player) { TetrisClientInfo info = new TetrisClientInfo(); info.ClientId = player.ClientId; info.DisplayName = player.DisplayName; info.Width = this._Width; info.Height = this._Height; if (player.TargetClientId > 0) { TetrisPlayer targetPlayer = this._TetrisPlayers.Get(player.TargetClientId); if (targetPlayer != null) { info.TargetClientId = player.TargetClientId; info.TargetDisplayName = targetPlayer.DisplayName; } else { // Gone } } if (info.DisplayName == null) info.DisplayName = ""; if (info.TargetDisplayName == null) info.TargetDisplayName = ""; TetrisMessage message = new TetrisMessage(TetrisMessage.BodyTypes.Info, info.Serialize()); this.SocketSend(player, message); } // Logic private bool Rotate(TetrisPlayer player, int rotateDir) { bool moved = this.PositionValid(player, 0, 0, rotateDir, true); if (moved == true) return true; for (int move = 1; move < 3; ++move) { foreach (int dir in new int[] { -1, 1 }) { moved = this.PositionValid(player, move * dir, 0, rotateDir, true); if (moved == true) return true; } } return false; } public int FindLines(TetrisPlayer player) { int count = 0; for (int y = 0; y < player.Board.Height; ++y) { int lineCount = 0; for (int x = 0; x < player.Board.Width; ++x) { if (player.Board.Peek(x, y) != 0) ++lineCount; } if (lineCount == player.Board.Width) { RemoveLine(player, y); ++count; } } return count; } public void RemoveLine(TetrisPlayer player, int yIndex) { for (int y = yIndex; y >= 0; --y) { for (int x = 0; x < player.Board.Width; ++x) { if (y == 0) player.Board.Poke(x, y, 0); else player.Board.Poke(x, y, player.Board.Peek(x, y - 1)); } } } public void InsertLines(TetrisPlayer player) { int count = player.IncomingRows; if (count >= player.Board.Height) count = player.Board.Height - 1; int top = player.Board.Height - count; // Move Current Board Up for (int y = 0; y < top; ++y) { for (int x = 0; x < player.Board.Width; ++x) player.Board.Poke(x, y, player.Board.Peek(x, y + count)); } int missX = RandomNext(player.Board.Width); // Grey out below for (int y = top; y < player.Board.Height; ++y) { for (int x = 0; x < player.Board.Width; ++x) { if (missX == x) player.Board.Poke(x, y, 0); else player.Board.Poke(x, y, 8); } } player.IncomingRows -= count; } public static IPEndPoint CreateEndpoint(string hostNameOrAddressAndPortNumber) { int portNumber = 32199; string hostNameOrAddress = hostNameOrAddressAndPortNumber; int index = hostNameOrAddressAndPortNumber.IndexOf(":"); if (index != -1) { hostNameOrAddress = hostNameOrAddressAndPortNumber.Substring(0, index).Trim(); portNumber = Int32.Parse(hostNameOrAddressAndPortNumber.Substring(index + 1).Trim()); } if (hostNameOrAddress == "*") return new IPEndPoint(IPAddress.Any, portNumber); IPAddress useAddress = null; if (IPAddress.TryParse(hostNameOrAddress, out useAddress) == false) { IPHostEntry hostEntry = System.Net.Dns.GetHostEntry(hostNameOrAddress); foreach (IPAddress address in hostEntry.AddressList) { if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue; if (useAddress == null) useAddress = address; else throw new ApplicationException("More than 1 IPv4 Address found (try using the IP not the hostname): " + hostNameOrAddress); } if (useAddress == null) throw new ApplicationException("No IPv4 Address found: " + hostNameOrAddress); } return new IPEndPoint(useAddress, portNumber); } } }