/// <summary> /// We received some data from another client in the session. This method /// assumes all messages in the session are sent via our <c>Send()</c> /// methods, i.e. that only commands are sent. We try to parse these here, /// then forward them to the <c>HandleCommand()</c> method. /// /// <para> /// To take influence on how messages are sent and received (add another /// layer to the protocol), use the <c>WrapDataForSend()</c> and /// <c>UnwrapDataForReceive()</c> methods. /// </para> /// </summary> private void HandlePlayerData(object sender, SessionDataEventArgs e) { try { // Delegate unwrapping of the message, and if this yields a command object // try to handle it. var command = UnwrapDataForReceive(e); if (command != null) { HandleRemoteCommand(command); } } catch (PacketException ex) { Logger.WarnException("Failed parsing received packet.", ex); } catch (Exception ex) { Logger.WarnException("Failed deserializing data.", ex); } }
/// <summary>Takes care of server side TSS synchronization logic.</summary> protected override FrameCommand UnwrapDataForReceive(SessionDataEventArgs e) { var args = (ServerDataEventArgs)e; var type = (TssControllerMessage)args.Data.ReadByte(); switch (type) { case TssControllerMessage.Command: { // Normal command, forward it if it's valid. var command = base.UnwrapDataForReceive(e); // Validate player number (avoid command injection for // other players). if (command.PlayerNumber == args.Player.Number) { // All green. return(command); } Logger.Warn("Received invalid packet (player number mismatch)."); // Disconnect the player, as this might have been a // hacking attempt. Session.Disconnect(Session.GetPlayer(args.Player.Number)); // Ignore the command. return(null); } case TssControllerMessage.Synchronize: { // Client re-synchronizing. // Get the frame the client is at. var clientFrame = args.Data.ReadInt64(); // Get performance information of the client. _clientLoads[args.Player.Number] = args.Data.ReadSingle(); // Re-evaluate at what speed we want to run. AdjustSpeed(); // Send our reply. using (var packet = new Packet()) { packet // Message type. .Write((byte)TssControllerMessage.Synchronize) // For reference, the frame the client sent this message. .Write(clientFrame) // The current server frame, to allow the client to compute // the round trip time. .Write(Tss.CurrentFrame) // The current speed the game should run at. .Write(AdjustedSpeed); Session.SendTo(args.Player, packet); } break; } case TssControllerMessage.GameState: { // Client needs game state. var hasher = new Hasher(); hasher.Write(Tss.TrailingSimulation); using (var packet = new Packet()) { packet // Message type. .Write((byte)TssControllerMessage.GameState) // Hash value for validation. .Write(hasher.Value) // Actual game state, including the TSS wrapper. .Write(Tss); Session.SendTo(args.Player, packet); } break; } case TssControllerMessage.GameStateDump: { // Got a game state dump from a client due to hash check failure. var frame = args.Data.ReadInt64(); if (!_gameStates.ContainsKey(frame)) { Logger.Warn("Got a game state dump for a frame we don't have the local dump for anymore."); return(null); } // Get the id for the dump for easier file matching. var dumpId = args.Data.ReadString(); try { // Create actual game dump and write it to file. WriteGameState(frame, _gameStates[frame], dumpId + "_server.txt"); } catch (Exception ex) { Logger.ErrorException("Failed writing server desynchronization dump.", ex); } break; } } return(null); }
/// <summary> /// Takes care of client side TSS synchronization logic. /// </summary> protected override FrameCommand UnwrapDataForReceive(SessionDataEventArgs e) { // Ignore, we use the server's simulation for rendering. return(null); }
/// <summary> /// May be overridden to implement the other end of a protocol layer as /// added via <c>WrapDataForSend()</c>. You should follow the same pattern /// as there. /// </summary> /// <param name="args">the originally received network data.</param> /// <returns>the parsed command, or null, if the message /// was not a command (i.e. some other message type).</returns> protected virtual TCommand UnwrapDataForReceive(SessionDataEventArgs args) { // Parse the actual command. return(args.Data.ReadPacketizableWithTypeInfo <TCommand>()); }
/// <summary> /// Takes care of client side TSS synchronization logic. /// </summary> protected override FrameCommand UnwrapDataForReceive(SessionDataEventArgs e) { var args = (ClientDataEventArgs)e; var type = (TssControllerMessage)args.Data.ReadByte(); switch (type) { case TssControllerMessage.Command: { // Normal command, forward it. var command = base.UnwrapDataForReceive(e); // Test if we got the message from the server, to mark the command accordingly. command.IsAuthoritative = args.IsAuthoritative; // Return the deserialized command. return(command); } case TssControllerMessage.Synchronize: { // Answer to a synchronization request. // Only accept these when they come from the server, and disregard if // we're waiting for a snapshot of the simulation. if (args.IsAuthoritative && !Tss.WaitingForSynchronization) { // This calculation follows algorithm described here: // http://www.mine-control.com/zack/timesync/timesync.html var sentFrame = args.Data.ReadInt64(); var serverFrame = args.Data.ReadInt64(); // We also adjust the game speed to accommodate slow // machines. That's the speed we get in this step. AdjustedSpeed = args.Data.ReadSingle(); var latency = (Tss.CurrentFrame - sentFrame) / 2; var clientServerDelta = (serverFrame - Tss.CurrentFrame); var frameDelta = clientServerDelta + latency / 2; _frameDiff.Put((int)frameDelta); var median = _frameDiff.Median(); var stdDev = _frameDiff.StandardDeviation(); if (System.Math.Abs(frameDelta) > 1 && frameDelta < (int)(median + stdDev)) { Logger.Trace("Correcting for {0} frames.", frameDelta); // Adjust the current frame of the simulation. ScheduleFrameskip(frameDelta); } } break; } case TssControllerMessage.HashCheck: { // Only accept these when they come from the server. if (args.IsAuthoritative) { // Get the frame this hash data is for. var hashFrame = args.Data.ReadInt64(); Debug.Assert(hashFrame > _lastServerHashedFrame); _lastServerHashedFrame = hashFrame; // Read hash values. var hashValue = args.Data.ReadUInt32(); // And perform hash check. PerformHashCheck(hashValue, hashFrame, Tss.TrailingFrame < _lastServerHashedFrame); } break; } case TssControllerMessage.GameState: { // Got a simulation snap shot (normally after requesting it due to // our simulation going out of scope for an older event). // Only accept these when they come from the server. if (args.IsAuthoritative) { // Read data. var serverHash = args.Data.ReadUInt32(); args.Data.ReadPacketizableInto(Tss); // Validate the data we got. var hasher = new Hasher(); hasher.Write(Tss.TrailingSimulation); if (hasher.Value != serverHash) { Logger.Error("Hash mismatch after deserialization."); Session.Leave(); } // Run to current frame to avoid slow interpolation to current frame. // Take into account the time we need to get there. var delta = 0L; do { // Remember when we started, see below. var started = DateTime.UtcNow; // Do the actual update, run to where we want to be. Tss.RunToFrame(Tss.CurrentFrame + delta); // See how long we took for this update, and compute the number // of updates we'd otherwise have made in that time. delta = (long)((DateTime.UtcNow - started).TotalMilliseconds / TargetElapsedMilliseconds); // Continue until we're close enough with the *actual* current // frame to what we want to be at. } while (delta > 10); } break; } case TssControllerMessage.RemoveGameObject: { // Only accept these when they come from the server. if (args.IsAuthoritative) { var removeFrame = args.Data.ReadInt64(); var entityUid = args.Data.ReadInt32(); Tss.RemoveEntity(entityUid, removeFrame); } break; } } return(null); }