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