/// <summary> /// Handles a spawn game packet, updating the local player data state. /// </summary> private void HandleSpawn(MessageReader reader) { var spawnId = (SpawnableObjects)reader.ReadPackedUInt32(); var owner = reader.ReadPackedUInt32(); reader.ReadByte(); // flags reader.ReadPackedInt32(); // component length if (spawnId == SpawnableObjects.GameData) { reader.ReadPackedInt32(); // game data net id var gameData = reader.ReadMessage(); var numPlayers = gameData.ReadPackedInt32(); for (var i = 0; i < numPlayers; i++) { var playerId = gameData.ReadByte(); UpdateOrCreatePlayerData(gameData, playerId); } } else if (spawnId == SpawnableObjects.PlayerControl) { var netId = reader.ReadPackedInt32(); // player control net id var controlData = reader.ReadMessage(); controlData.ReadByte(); // unk, seems to be 1 if us, else 0 var playerId = controlData.ReadByte(); // this is us, we got to ignore us _playerControlNetIdToPlayerId[netId] = playerId; // If this is us (only for the brief initial connect), ignore it. if (owner == _clientId) { return; } // Either join an existing entry or create a new one. var existing = _playerData.Find(x => x.id == playerId); if (existing != null) { existing.clientId = owner; } else { _playerData.Add(new PlayerData(owner, playerId)); } // Update if this is not the initial data gathering state. if (_hasPlayerData) { OnPlayerDataUpdate?.Invoke(_playerData); } // Check if we have the data on everyone, and if yes disconnect and reconnect. if (!_hasPlayerData && _playerData.All(x => x.clientId != 0)) { _hasPlayerData = true; DisconnectAndReconnect(); } } }
/// <summary> /// Invoked when the game has ended. Attempts to rejoin the same lobby. /// </summary> private void HandleEndGame(MessageReader message) { _playerData.ForEach(x => x.tasks.Clear()); // nobody has tasks any more OnPlayerDataUpdate?.Invoke(_playerData); OnGameEnd?.Invoke(); // Simply rejoin the same lobby. _connection.SendReliableMessage(JoinGame); }
/// <summary> /// Handles an RPC game packet, dispatching the results when appropriate. /// </summary> private void HandleRPC(MessageReader reader) { reader.ReadPackedInt32(); // rpc target var action = (RPCCalls)reader.ReadByte(); if (action == RPCCalls.Close) { OnTalkingEnd?.Invoke(); } else if (action == RPCCalls.StartMeeting) { OnTalkingStart?.Invoke(); } else if (action == RPCCalls.UpdateGameData) { if (!_hasReconnectedAfterPlayerData) { return; // don't handle UpdateGameData earlier. } foreach (var dataEntry in reader.Messages()) { UpdateOrCreatePlayerData(dataEntry, dataEntry.Tag); } OnPlayerDataUpdate?.Invoke(_playerData); } else if (action == RPCCalls.MurderPlayer) { var victim = reader.ReadPackedInt32(); var victimPlayerId = _playerControlNetIdToPlayerId[victim]; _playerData.Find(x => x.id == victimPlayerId).statusBitField |= 4; // dead OnPlayerDataUpdate?.Invoke(_playerData); } else if (action == RPCCalls.VotingComplete) { reader.ReadBytesAndSize(); // voting data var victim = reader.ReadByte(); if (victim != 0xFF) { _playerData.Find(x => x.id == victim).statusBitField |= 4; // dead OnPlayerDataUpdate?.Invoke(_playerData); } } }
/// <summary> /// Invoked when a player left the lobby. Handles situations where we /// end up becoming the host. /// </summary> private async void HandleRemovePlayer(MessageReader reader) { reader.ReadInt32(); // room code var idThatLeft = reader.ReadInt32(); // id that left var newHost = reader.ReadUInt32(); reader.ReadByte(); // disconnect reason // Update the game data by removing the player that left. _hostId = newHost; _playerData = _playerData.Where(x => x.clientId != idThatLeft).ToList(); OnPlayerDataUpdate?.Invoke(_playerData); // If we're the host now, leave and attempt to rejoin to make someone else host. if (newHost == _clientId) { await DisconnectAndReconnect(); } }
/// <summary> /// Initializes this client by connecting to the specified host and attempting /// to join the specified lobby code. Will throw if connection fails, else will /// start servicing messages in the background. The caller is responsible for /// ensuring that the application stays running as long as the client is active. /// </summary> public async Task Connect(IPAddress address, string lobbyName, ushort port = MATCHMAKER_PORT) { _address = address; _lobbyName = lobbyName; _lobbyCode = GameCode.GameNameToIntV2(lobbyName); var(connection, response) = await ConnectToMMAndSend(address, port, JoinGame); _port = (ushort)connection.EndPoint.Port; _connection = connection; _connection.DataReceived += OnMessageReceived; _connection.Disconnected += (sender, args) => { OnDisconnect?.Invoke(); }; HandleJoinGameResult(response); if (!_hasPlayerData) { // If we don't have user data, send a SceneChange so that we receive a spawn. _connection.SendReliableMessage(writer => { writer.StartMessage((byte)MMTags.GameData); writer.Write(_lobbyCode); writer.StartMessage((byte)GameDataTags.SceneChange); writer.WritePacked(_clientId); // note: must be _clientId since localplayer is not set yet writer.Write("OnlineGame"); writer.EndMessage(); writer.EndMessage(); }); } else { // We have user data, invoke listeners. _hasReconnectedAfterPlayerData = true; OnConnect?.Invoke(); OnPlayerDataUpdate?.Invoke(_playerData); } }