Ejemplo n.º 1
0
        /// <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();
                    }
                }
            }
        }
Ejemplo n.º 2
0
        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;
        }
Ejemplo n.º 3
0
        /// <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);
                }
            }
        }
Ejemplo n.º 4
0
        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);
                }
            }
        }