/// <inheritdoc/> public async Task<JoinResult> JoinAsync(GameClientInfo clientInfo, IGameClientStub clientStub) { using (await _clientsLock.LockAsync()) { if (_clients.Values.Any(conn => conn.ClientInfo.PlayerId == clientInfo.PlayerId.ToString())) return new JoinResult(false, Point.Zero, "Another client is already connected using the same player ID"); var connection = new ClientConnection(clientInfo, clientStub); var playerEntity = new PlayerEntity(clientInfo.PlayerId, Point.North); // Check if the player is playing this map for the first time if (Map.Metadata.Players.IsKnown(clientInfo.PlayerId)) { // Try to spawn player at its last known position. var playerInfo = Map.Metadata.Players[clientInfo.PlayerId]; var spawnTransaction = new TransactionWithMoveSupport(MoveInitiator.Empty); var spawnResult = await Map.SpawnAsync(playerEntity, playerInfo.Position, spawnTransaction); if (spawnResult.IsSuccessful) { // TODO: What if a player spawns within a level some others are currently trying to solve? await CommitAndBroadcastAsync(spawnResult.Transaction, spawnResult.FollowUpEvents, connection); _clients.Add(clientInfo.PlayerId, connection); Map.Emit(new PlayerJoinEvent(clientInfo)); return new JoinResult(true, playerInfo.Position); } else { // If that fails, // option 1: spawn at default spawn area // option 2: return false throw new NotImplementedException(); } } else { // Unknown player: Spawn player somewhere in default spawn area, add to list of known players var spawnPositions = await Map.GetCoherentPositionsAsync(Map.Metadata.Regions.DefaultRegion.SpawnPosition); var spawnTransaction = new TransactionWithMoveSupport(MoveInitiator.Empty); var spawnResult = await Map.SpawnAsync(playerEntity, spawnPositions, spawnTransaction); if (spawnResult.IsSuccessful) { await CommitAndBroadcastAsync(spawnResult.Transaction, spawnResult.FollowUpEvents, connection); var playerInfo = new PlayerInfo(clientInfo.PlayerId, clientInfo.PlayerDisplayName, spawnResult.SpawnPosition); Map.Metadata.Players.Add(playerInfo); _clients.Add(clientInfo.PlayerId, connection); Map.Emit(new PlayerJoinEvent(clientInfo)); return new JoinResult(true, spawnResult.SpawnPosition); } else { // TODO: What now? We could not spawn the player! throw new NotImplementedException(); } } } }
private async Task<int> CommitAndBroadcastAsync(ITransactionWithMoveSupport transaction, IEnumerable<FollowUpEvent> followUpEvents, ClientConnection excludedConnection = null) { var newVersion = -1; if (transaction.Changes.Any()) { // Get new version number for the tiles that changed newVersion = Map.Metadata.NextTileVersion(); // Apply changes to the server's map (using the version number above) await transaction.CommitAsync(Map, newVersion); // Send the updated tiles to clients that have loaded the corresponding map area await BroadcastChangesAsync(transaction, newVersion, excludedConnection); } // Schedule follow-up events that might have been created during e.g. a move _scheduledFollowUpEvents.AddRange(followUpEvents); return newVersion; }
/// <summary> /// Sends a subset of the changes and events of the specified transaction to the /// clients depending on which chunks each client has currently loaded. /// </summary> /// <param name="transaction">The transaction that contains the changes that are broadcasted</param> /// <param name="newVersion">The version to be used for the updates tiles</param> /// <param name="excludedConnection">An optional connection that is excluded and does not receive the tile updates</param> /// <returns></returns> private async Task BroadcastChangesAsync(ITransactionWithMoveSupport transaction, int newVersion, ClientConnection excludedConnection = null) { // Check which players have moved and update their positions in map metadata var playerMoveEvents = transaction.Events .OfType<EntityMoveEvent>() .Where(e => e.Source.Entity is PlayerEntity) .Select(e => new { PlayerId = (e.Source.Entity as PlayerEntity)?.PlayerId, NewPosition = e.TargetPosition }); foreach (var move in playerMoveEvents) Map.Metadata.Players[move.PlayerId] = Map.Metadata.Players[move.PlayerId].WithPosition(move.NewPosition); // Notify clients that have loaded the affected chunks foreach (var conn in _clients.Values) { if (conn == excludedConnection) continue; // Skip calling client var relevantChanges = transaction.Changes .Where(change => conn.LoadedChunks.Contains(change.Key / Chunk.Size)) .Select(change => new TileUpdate(change.Key, change.Value.WithVersion(newVersion))) .ToArray(); var relevantEvents = transaction.Events .OfType<ILocatedGameEvent>() .Where(ev => ev.GetPositions().Any(p => conn.LoadedChunks.Contains(p / Chunk.Size)) || ((ev as EntitySpawnEvent)?.Entity as PlayerEntity)?.PlayerId == conn.ClientInfo.PlayerId) .ToArray(); if (relevantChanges.Any() || relevantEvents.Any()) { // If there are any tile changes or any events that are of interest // to the client, push them to the client. var update = new ClientUpdate(relevantChanges, relevantEvents); await conn.ClientStub.OnUpdate(update); } } }
private void UnloadChunkCore(Point index, ClientConnection connection) { connection.LoadedChunks.Remove(index); var connectionsList = _connectionsByChunk.TryGetValue(index); if (connectionsList != null) { connectionsList.Remove(connection); if (!connectionsList.Any()) { // No more clients have loaded the chunk // TODO: Check if we can unload the chunk on the server // We can unload chunks which are no longer referenced by clients or by dependencies from other chunks // (Finding these chunks again involves checking dependencies and determining a topological order of checking) _connectionsByChunk.Remove(index); } } }