/// <summary> /// Invoked when the game has been started. /// </summary> private void HandleStartGame(MessageReader message) { OnTalkingEnd?.Invoke(); _connection.SendReliableMessage(writer => { writer.StartMessage((byte)MMTags.GameData); writer.Write(_lobbyCode); writer.StartMessage((byte)GameDataTags.Ready); writer.WritePacked(_clientId); writer.EndMessage(); writer.EndMessage(); }); }
/// <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); } }
/// <summary> /// Connect to the specified ip:port endpoint for the matchmaker, then send the specified /// message. Returns a task that resolves to a tuple of the established connection and the /// first message received from the matchmaker in response to the sent message (usually /// the join/host confirmal message). Will throw if the connection closes prematurely or /// otherwise errors. Otherwise, the task itself is responsible for disposing of the /// connection once the server disconnects. /// </summary> private static async Task <(UdpClientConnection, MessageReader)> ConnectToMMAndSend(IPAddress address, ushort port, Action <MessageWriter> writeMessage) { var firstMessageTask = new TaskCompletionSource <MessageReader>(); var connection = new UdpClientConnection(new IPEndPoint(address, port)); connection.KeepAliveInterval = 1000; connection.DisconnectTimeout = 10000; connection.ResendPingMultiplier = 1.2f; // Set up an event handler to resolve the task on first non-reselect message received. Action <DataReceivedEventArgs> onDataReceived = null; onDataReceived = args => { try { var msg = args.Message.ReadMessage(); if (msg.Tag == (byte)MMTags.ReselectServer) { return; // not interested } firstMessageTask.TrySetResult(msg); connection.DataReceived -= onDataReceived; } finally { args.Message.Recycle(); } }; connection.DataReceived += onDataReceived; // Set up an event handler to set an exception for the task on early disconnect. connection.Disconnected += (sender, args) => { connection.Dispose(); firstMessageTask.TrySetException(new AUException("Connection to matchmaker prematurely exited")); }; // Connect to the endpoint. connection.ConnectAsync(HANDSHAKE); await connection.ConnectWaitLock.AsTask(); // Send the contents. connection.SendReliableMessage(writeMessage); // Wait for the response to arrive. var response = await firstMessageTask.Task; // If this is not a redirect, return the result. if (response.Tag != (byte)MMTags.Redirect) { return(connection, response); } // This is a redirect, so do this again but with the new data. var newIp = response.ReadUInt32(); var newPort = response.ReadUInt16(); // Reconnect to new host. return(await ConnectToMMAndSend(new IPAddress(newIp), newPort, writeMessage)); }