diff --git a/Freeserf.Core/Network/IServer.cs b/Freeserf.Core/Network/IServer.cs index e74fba35..1e38fecb 100644 --- a/Freeserf.Core/Network/IServer.cs +++ b/Freeserf.Core/Network/IServer.cs @@ -57,6 +57,7 @@ public interface IServer public delegate void ClientJoinedHandler(ILocalServer server, IRemoteClient client); public delegate void ClientLeftHandler(ILocalServer server, IRemoteClient client); public delegate bool GameReadyHandler(bool ready); + public delegate void ClientChangedFaceHandler(ILocalServer server, IRemoteClient client, PlayerFace face); public interface ILocalServer : IServer, INetworkDataHandler { @@ -70,10 +71,12 @@ public interface ILocalServer : IServer, INetworkDataHandler void DisconnectClient(IRemoteClient client, bool sendNotificationToClient); void BroadcastHeartbeat(); void BroadcastDisconnect(); + void BroadcastLobbyData(); event ClientJoinedHandler ClientJoined; event ClientLeftHandler ClientLeft; event GameReadyHandler GameReady; + event ClientChangedFaceHandler ClientChangedFace; /// /// List of all connected clients. diff --git a/Freeserf.Core/Network/RequestData.cs b/Freeserf.Core/Network/RequestData.cs index 8c3b8d53..0e109c8c 100644 --- a/Freeserf.Core/Network/RequestData.cs +++ b/Freeserf.Core/Network/RequestData.cs @@ -43,7 +43,7 @@ public partial class Global public const byte SpontaneousMessage = 0xff; static byte CurrentMessageIndex = 0; - static object MessageIndexLock = new object(); + static readonly object MessageIndexLock = new object(); public static byte GetNextMessageIndex() { diff --git a/Freeserf.Core/Network/UserActionData.cs b/Freeserf.Core/Network/UserActionData.cs index 5183f2e6..bb93577c 100644 --- a/Freeserf.Core/Network/UserActionData.cs +++ b/Freeserf.Core/Network/UserActionData.cs @@ -182,6 +182,11 @@ public enum UserAction /// Unknown, /// + /// Change player face (and color). In lobby only. + /// Byte 0: Face index + /// + ChangeFace, + /// /// Change a game-relevant settings. /// Byte 0: The setting (see ) /// See for additional bytes/parameters. @@ -370,6 +375,11 @@ public void Send(IRemote destination) destination.Send(rawData.ToArray()); } + internal static UserActionData CreateChangeFaceUserAction(byte number, PlayerFace face) + { + return new UserActionData(number, 0u, UserAction.ChangeFace, new byte[1] { (byte)face }); + } + internal static UserActionData CreateChangeSettingUserAction(byte number, Game game, UserActionGameSetting setting, params byte[] values) { byte[] parameters = new byte[values.Length + 1]; diff --git a/Freeserf.Core/UI/GameInitBox.cs b/Freeserf.Core/UI/GameInitBox.cs index db60299d..61c09d6f 100644 --- a/Freeserf.Core/UI/GameInitBox.cs +++ b/Freeserf.Core/UI/GameInitBox.cs @@ -934,6 +934,7 @@ public void HandleAction(Action action) Server.Init(checkBoxSameValues.Checked, checkBoxServerValues.Checked, ServerGameInfo.MapSize, randomInput.Text, ServerGameInfo.Players); Server.ClientJoined += Server_ClientJoined; Server.ClientLeft += Server_ClientLeft; + Server.ClientChangedFace += Server_ClientChangedFace; break; case Action.StartGame: { @@ -1027,6 +1028,7 @@ public void HandleAction(Action action) { Server.ClientJoined -= Server_ClientJoined; Server.ClientLeft -= Server_ClientLeft; + Server.ClientChangedFace -= Server_ClientChangedFace; GameManager.Instance.CloseGame(); interf = interf.Viewer.ChangeTo(Viewer.Type.Server).MainInterface; @@ -1252,6 +1254,39 @@ public void HandleAction(Action action) } } + private void Server_ClientChangedFace(ILocalServer server, IRemoteClient client, PlayerFace face) + { + if (client == null || !playerClientMapping.Values.Contains(client) || client.PlayerIndex > ServerGameInfo.PlayerCount) + return; + + if (face >= PlayerFace.You && face <= PlayerFace.FriendYellow) + { + bool failed = false; + + lock (ServerGameInfo) + { + for (int i = 0; i < ServerGameInfo.PlayerCount; ++i) + { + if (i != client.PlayerIndex && CompareFace(ServerGameInfo.Players[i].Face, face)) + { + failed = true; + break; + } + + } + + if (!failed) + { + ServerGameInfo.Players[(int)client.PlayerIndex].SetCharacter(face); + ServerUpdate(); + SetRedraw(); + } + } + } + + server.BroadcastLobbyData(); + } + private void Client_GameStarted(object sender, System.EventArgs e) { // TODO: remote spectator @@ -1480,6 +1515,37 @@ PlayerInfo GetRandomPlayerInfo(uint playerIndex) return playerInfo; } + static readonly PlayerFace[] MultiplayerFaceOrder = + { + PlayerFace.You, PlayerFace.YouRed, PlayerFace.YouMagenta, PlayerFace.YouYellow, + PlayerFace.FriendBlue, PlayerFace.Friend, PlayerFace.FriendMagenta, PlayerFace.FriendYellow + }; + + static bool CompareFace(PlayerFace face1, PlayerFace face2) + { + if (face1 == face2) + return true; + + // In multiplayer games the same color can't be taken twice. + if (face1 >= PlayerFace.You && face2 >= PlayerFace.You) + { + return face1 switch + { + PlayerFace.You => face2 == PlayerFace.FriendBlue, + PlayerFace.YouRed => face2 == PlayerFace.Friend, + PlayerFace.YouMagenta => face2 == PlayerFace.FriendMagenta, + PlayerFace.YouYellow => face2 == PlayerFace.FriendYellow, + PlayerFace.FriendBlue => face2 == PlayerFace.You, + PlayerFace.Friend => face2 == PlayerFace.YouRed, + PlayerFace.FriendMagenta => face2 == PlayerFace.YouMagenta, + PlayerFace.FriendYellow => face2 == PlayerFace.YouYellow, + _ => false + }; + } + + return false; + } + bool HandlePlayerClick(uint playerIndex, int cx, int cy) { if (cx < 8 || cx > 8 + 64 || cy < 8 || cy > 76) @@ -1553,11 +1619,19 @@ bool HandlePlayerClick(uint playerIndex, int cx, int cy) if (cx < 8 + 32 && cy < 72) // click on face { bool canNotChange = (playerIndex == 0 && gameType != GameType.AIvsAI) || - gameType == GameType.MultiplayerJoined || // TODO: maybe later choose between some special faces gameType == GameType.Mission || gameType == GameType.Tutorial || gameType == GameType.Load; + if (gameType == GameType.MultiplayerServer) + { + canNotChange = playerIndex != 0 && player.Face >= PlayerFace.You; + } + else if (gameType == GameType.MultiplayerJoined) + { + canNotChange = playerIndex != Client.PlayerIndex; + } + if (!canNotChange) { // Face @@ -1565,10 +1639,19 @@ bool HandlePlayerClick(uint playerIndex, int cx, int cy) do { - uint next = ((uint)player.Face + 1) % 11; // Note: Use 12 here to also allow the last enemy as a custom game player - next = Math.Max(1u, next); + PlayerFace next; + + if (gameType == GameType.MultiplayerServer || gameType == GameType.MultiplayerJoined) + { + int index = (MultiplayerFaceOrder.ToList().IndexOf(player.Face) + 1) % 8; + next = MultiplayerFaceOrder[index]; + } + else + { + next = (PlayerFace)(((int)player.Face + 1) % 11); // Note: Use 12 here to also allow the last enemy as a custom game player + } - player.SetCharacter((PlayerFace)next); + player.SetCharacter(next); // Check that face is not already in use by another player inUse = false; @@ -1576,7 +1659,7 @@ bool HandlePlayerClick(uint playerIndex, int cx, int cy) for (uint i = 0; i < ServerGameInfo.PlayerCount; ++i) { if (playerIndex != i && - ServerGameInfo.GetPlayer(i).Face == (PlayerFace)next) + CompareFace(ServerGameInfo.GetPlayer(i).Face, next)) { inUse = true; break; @@ -1587,6 +1670,8 @@ bool HandlePlayerClick(uint playerIndex, int cx, int cy) if (gameType == GameType.MultiplayerServer) ServerUpdate(); + else if (gameType == GameType.MultiplayerJoined) + Client.SendUserAction(UserActionData.CreateChangeFaceUserAction(Network.Global.SpontaneousMessage, player.Face)); } } else if (cx >= 8 + 32 && cx < 8 + 32 + 8 && cy >= 8 && cy < 24) // click on copy values button diff --git a/Freeserf.Network/Server.cs b/Freeserf.Network/Server.cs index 0a20d74f..1c6a55f2 100644 --- a/Freeserf.Network/Server.cs +++ b/Freeserf.Network/Server.cs @@ -170,6 +170,7 @@ public GameInfo GameInfo public event ClientJoinedHandler ClientJoined; public event ClientLeftHandler ClientLeft; public event GameReadyHandler GameReady; + public event ClientChangedFaceHandler ClientChangedFace; public void Run(bool useServerValues, bool useSameValues, uint mapSize, string mapSeed, IEnumerable players, CancellationToken cancellationToken) @@ -517,9 +518,7 @@ void HandleData(RemoteClient client, byte[] data) { case ServerState.Lobby: { - // TODO allow user actions in lobby? can clients do something in lobby? - - if (networkData.Type != NetworkDataType.Request) + if (networkData.Type != NetworkDataType.Request && networkData.Type != NetworkDataType.UserActionData) { client.SendResponse(networkData.MessageIndex, ResponseType.BadState); throw new ExceptionFreeserf("Request expected."); @@ -597,10 +596,19 @@ void ProcessData(IRemoteClient client, INetworkData networkData, ResponseHandler { case ServerState.Lobby: { - // TODO: assert that it is a request (checked before in HandleData) - var request = networkData as RequestData; + if (networkData is RequestData request) + HandleLobbyRequest(client, request.MessageIndex, request.Request, responseHandler); + else if (networkData is UserActionData userAction) + { + if (userAction.UserAction != UserAction.ChangeFace) + { + Log.Error.Write(ErrorSystemType.Network, $"Received user action {userAction.UserAction} in lobby."); + responseHandler?.Invoke(ResponseType.BadState); + return; + } - HandleLobbyRequest(client, request.MessageIndex, request.Request, responseHandler); + ClientChangedFace?.Invoke(this, client, (PlayerFace)userAction.Parameters[0]); + } break; } @@ -705,10 +713,10 @@ void HandleLobbyRequest(IRemoteClient client, byte messageIndex, Request request case Request.LobbyData: // TODO: check if the client just requested it (bruteforce attacks should be avoided) lock (lobbyServerInfo) - lock (lobbyPlayerInfo) - { - client.SendLobbyDataUpdate(messageIndex, lobbyServerInfo, lobbyPlayerInfo); - } + lock (lobbyPlayerInfo) + { + client.SendLobbyDataUpdate(messageIndex, lobbyServerInfo, lobbyPlayerInfo); + } break; default: responseHandler?.Invoke(ResponseType.BadRequest); @@ -955,17 +963,17 @@ private void BroadcastResumeRequest() Broadcast((client) => new RequestData(Global.SpontaneousMessage, Request.Resume).Send(client)); } - private void BroadcastLobbyData() + public void BroadcastLobbyData() { Log.Verbose.Write(ErrorSystemType.Network, $"Broadcast lobby data to {clients.Count} clients."); Broadcast((client) => { lock (lobbyServerInfo) - lock (lobbyPlayerInfo) - { - client.SendLobbyDataUpdate(Global.SpontaneousMessage, lobbyServerInfo, lobbyPlayerInfo); - } + lock (lobbyPlayerInfo) + { + client.SendLobbyDataUpdate(Global.SpontaneousMessage, lobbyServerInfo, lobbyPlayerInfo); + } }); } diff --git a/Issues.md b/Issues.md index 16d0cf25..af6a18f8 100644 --- a/Issues.md +++ b/Issues.md @@ -10,6 +10,7 @@ - After saving the quit confirm will not ask for saving even if the game progressed a good amount of time - After closing a popup the delayed click handler will be raised when the box is already closed and therefore will count as a map click. This should be avoided somehow. +- New faces represent colors which should be adjusted accordingly in multiplayer games. ## Rendering