Exemple #1
0
        public async Task<IReadOnlyCollection<FollowUpEvent>> UpdateTilesAsync(IEnumerable<Point> positions, ITransactionWithMoveSupport transaction)
        {
            var initialPoints = new HashSet<Point>(positions);
            var followUpEvents = new List<FollowUpEvent>();
            var now = DateTimeOffset.Now;

            // Notify the initial tiles and tiles that are directly or indirectly
            // (transitively) dependent on them in topological order
            await Dependencies.DoAsyncWorkFollowingDependenciesAsync(initialPoints, async p =>
            {
                // Try to get from transaction first; if that fails get from map
                var tileInfo = transaction.Changes.TryGetValue(p);
                if (tileInfo == TileInfo.Empty) tileInfo = await GetAsync(p);

                followUpEvents.Add(new FollowUpEvent(p, transaction.Initiator, now + tileInfo.Tile.FollowUpDelay));

                var completionArgs = new GameplayArgs(transaction, this);
                await tileInfo.Tile.OnEntityMoveTransactionCompletedAsync(completionArgs);
                completionArgs.ValidateResult();

                if (transaction.IsSealed)
                    return null; // Null terminates the loop

                var newTileInfo = tileInfo.WithTile(completionArgs.Result);
                
                if (transaction.Set(p, tileInfo, newTileInfo))
                {
                    // TODO: Test and check whether this is sufficient
                    // (Initial idea:
                    // Somehow modify events that have been emitted during the move.
                    // E.g. when a balloon is moved onto an InkTile the move event's target
                    // still contains the balloon with its old color.
                    // Here, after the InkTile has changed the balloons color, we have to
                    // change that event to reflect the changes of InkTile and balloon.
                    // We probably have to emit spawn/despawn events as well.)
                    if (tileInfo.Tile.Entity != Entity.None)
                    {
                        if (newTileInfo.Tile.Entity == Entity.None)
                            transaction.Emit(new EntityDespawnEvent(p, tileInfo.Tile.Entity));
                        else
                            transaction.Emit(new EntityChangeEvent(p, newTileInfo.Tile.Entity));
                    }
                    else
                    {
                        if (newTileInfo.Tile.Entity != Entity.None)
                            transaction.Emit(new EntitySpawnEvent(p, newTileInfo.Tile.Entity));
                    }
                    return true;
                }

                return initialPoints.Contains(p);
            });

            return followUpEvents;
        }
Exemple #2
0
        /// <inheritdoc/>
        public async Task<ResetResult> ResetRegionAsync(Point position, ITransactionWithMoveSupport transaction)
        {
            // TOOD: Do not modify map directly, collect changes in a transaction
            // instead of 'bool' return 'ResetResult' containing the transaction

            var tileMeta = await GetMetadataAsync(position);
            var region = Metadata.Regions[tileMeta.RegionId];
            var regionPoints = await this.GetCoherentPositionsAsync(position);
            var playersInRegion = new List<PlayerEntity>();

            // Despawn all players within region
            foreach (var p in regionPoints)
            {
                var tileInfo = await GetAsync(p);
                var player = tileInfo.Tile.Entity as PlayerEntity;

                if (player != null)
                {
                    playersInRegion.Add(player);
                    var despawnResult = await DespawnAsync(p, transaction);
                    if (!despawnResult.IsSuccessful)
                        return new ResetResult(transaction, false, Enumerable.Empty<FollowUpEvent>()); // If we can't despawn a player in the region, we can't reset
                }
            }

            // Replace tiles with their templates
            foreach (var p in regionPoints)
            {
                var meta = await GetMetadataAsync(p);
                var oldTileInfo = await GetAsync(p);
                var newTileInfo = new TileInfo(meta.TileTemplate, oldTileInfo.Version); // Version will be set during commit

                var hasChanged = transaction.Set(p, oldTileInfo, newTileInfo);

                if (hasChanged && oldTileInfo.Tile.Entity != Entity.None)
                    transaction.Emit(new EntityDespawnEvent(p, oldTileInfo.Tile.Entity));

                if (hasChanged && newTileInfo.Tile.Entity != Entity.None)
                    transaction.Emit(new EntitySpawnEvent(p, newTileInfo.Tile.Entity));
            }

            // Respawn players at spawn position
            var spawnPoints = await this.GetCoherentPositionsAsync(region.SpawnPosition);
            var availableSpawnPoints = spawnPoints.Shuffle().ToQueue();

            if (availableSpawnPoints.Count < playersInRegion.Count)
                return new ResetResult(transaction, false, Enumerable.Empty<FollowUpEvent>()); // Spawn region is too small to respawn all players

            foreach (var player in playersInRegion)
            {
                var success = false;
                var spawnPosition = Point.Zero;

                while (!success && availableSpawnPoints.Any())
                {
                    transaction.IsSealed = false; // If the last spawn failed the transaction is sealed which would prevent any other spawns
                    spawnPosition = availableSpawnPoints.Dequeue();
                    var spawnResult = await SpawnAsync(player, spawnPosition, transaction);
                    success = spawnResult.IsSuccessful;
                }

                if (success)
                {
                    // Update player position in map metadata
                    Metadata.Players[player.PlayerId] = Metadata.Players[player.PlayerId].WithPosition(spawnPosition);
                }
                else
                {
                    // We ran out of available spawn positions and could not spawn the player
                    // => we can't fully reset the region
                    return new ResetResult(transaction, false, Enumerable.Empty<FollowUpEvent>());
                }
            }

            // Notify changed tiles and tiles that directly or indirectly depend on changed tiles
            var affectedTiles = transaction.Changes.Select(kvp => kvp.Key);
            var followUpEvents = await UpdateTilesAsync(affectedTiles, transaction);
            return new ResetResult(transaction, true, followUpEvents);
        }
Exemple #3
0
        private async Task<bool> MoveCoreAsync(Point sourcePosition, Point targetPosition, ITransactionWithMoveSupport transaction)
        {
            var source = await GetAsync(sourcePosition);
            var target = await GetAsync(targetPosition);

            var oldSource = source;
            var oldTarget = target;

            source.Tile.EnsureNotNull();
            target.Tile.EnsureNotNull();

            if (source.Tile.Entity == Entity.None)
            {
                // Cancel if there's no entity to move
                transaction.IsSealed = true;
                return false;
            }

            var move = new EntityMoveInfo(source.Tile.Entity, sourcePosition, targetPosition);
            transaction.Moves.Push(move);

            IBeginMoveArgs beginMoveArgs = new GameplayArgs(transaction, this);
            IDetachArgs detachArgs = new GameplayArgs(transaction, this);
            IAttachArgs attachArgs = new GameplayArgs(transaction, this);

            // OnBeginMove: Notify entity that a move has been initiated
            await source.Tile.Entity.BeginMoveAsync(beginMoveArgs);
            beginMoveArgs.ValidateResult();
            if (transaction.IsSealed) return false;

            var newSource = source.WithTile(Tile.Compose(source.Tile, beginMoveArgs.ResultingEntity));
            if (transaction.Set(sourcePosition, source, newSource))
                source = newSource;

            move.Entity = beginMoveArgs.ResultingEntity;

            // Detach: Remove the entity from the source tile
            await source.Tile.DetachEntityAsync(detachArgs);
            detachArgs.ValidateResult();
            if (transaction.IsSealed) return false;

            // Attach: Add the entity to the target tile
            await target.Tile.AttachEntityAsync(attachArgs);
            attachArgs.ValidateResult();
            if (transaction.IsSealed) return false;

            // Detach from old tile, attach to new tile have succeeded
            // => Apply changes to transaction
            var newSource2 = source.WithTile(detachArgs.Result);
            if (transaction.Set(sourcePosition, source, newSource2))
                source = newSource2;

            var newTarget = target.WithTile(attachArgs.Result);
            if (transaction.Set(targetPosition, target, newTarget))
                target = newTarget;

            // Emit events
            var moveEvent = new EntityMoveEvent(sourcePosition, targetPosition, oldSource.Tile, target.Tile);
            transaction.Emit(moveEvent);

            if (source.Tile.Entity != Entity.None)
            {
                // A new entity has been created at source position
                var spawnEvent = new EntitySpawnEvent(sourcePosition, source.Tile.Entity);
                transaction.Emit(spawnEvent);
            }

            if (oldTarget.Tile.Entity != Entity.None)
            {
                // The entity at target position has either been replaced
                // or it has been moved/collected/... during a nested move
                var oldTargetEntityOverwritten =
                    !(transaction.Events.OfType<EntityDespawnEvent>().Any(ev => ev.Position == targetPosition) ||
                    transaction.Events.OfType<EntityMoveEvent>().Any(ev => ev.SourcePosition == targetPosition));

                if (oldTargetEntityOverwritten)
                {
                    // If replaced, emit despawn event
                    var despawnEvent = new EntityDespawnEvent(targetPosition, oldTarget.Tile.Entity);
                    transaction.Emit(despawnEvent);
                }
            }

            transaction.Moves.Pop();
            return true;
        }
Exemple #4
0
        /// <inheritdoc/>
        public async Task<MoveResult> MoveAsync(Point sourcePosition, Point targetPosition, ITransactionWithMoveSupport transaction)
        {
            // Execute the actual move.
            // Any changes to the map are "recorded" in the transaction
            // and can later be applied to the map via a commit.
            var isSuccessful = await MoveCoreAsync(sourcePosition, targetPosition, transaction);

            // At the end of the recursion...
            if (transaction.Moves.Count == 0)
            {
                // Notify changed tiles and tiles that directly or indirectly depend on changed tiles
                var affectedTiles = transaction.Changes.Select(kvp => kvp.Key);
                var followUpEvents = await UpdateTilesAsync(affectedTiles, transaction);
                return new MoveResult(transaction, isSuccessful, followUpEvents);
            }

            return new MoveResult(transaction, isSuccessful, Enumerable.Empty<FollowUpEvent>());
        }
Exemple #5
0
        /// <inheritdoc/>
        public async Task<MoveResult> DespawnAsync(Point position, ITransactionWithMoveSupport transaction)
        {
            var tileInfo = await GetAsync(position);
            tileInfo.Tile.EnsureNotNull();

            // No entity => nothing to despawn
            if (tileInfo.Tile.Entity == Entity.None)
                return new MoveResult(transaction, true, Enumerable.Empty<FollowUpEvent>());

            transaction.Moves.Push(new EntityMoveInfo(tileInfo.Tile.Entity, position, position));

            // Try to detach entity
            var detachArgs = new GameplayArgs(transaction, this);
            await tileInfo.Tile.DetachEntityAsync(detachArgs);
            detachArgs.ValidateResult();

            if (transaction.IsSealed)
                return new MoveResult(transaction, false, Enumerable.Empty<FollowUpEvent>());

            var newTileInfo = tileInfo.WithTile(detachArgs.Result);
            transaction.Set(position, tileInfo, newTileInfo);
            transaction.Emit(new EntityDespawnEvent(position, tileInfo.Tile.Entity));

            // Update tile
            var followUpEvents = await UpdateTilesAsync(new[] { position }, transaction);

            return new MoveResult(transaction, true, followUpEvents);
        }
 public GameplayArgs(ITransactionWithMoveSupport transaction, IMap map)
 {
     _transaction = transaction;
     _map = map;
 }
 private GameplayArgs(ITransactionWithMoveSupport transaction, IGameplayMap map, Tile tile, Point suggestedPushDirection)
     : this(transaction, map)
 {
     Tile = tile;
     SuggestedPushDirection = suggestedPushDirection;
 }
        /// <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 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;
        }
 public ResetResult(ITransactionWithMoveSupport transaction, bool isSuccessful, IEnumerable<FollowUpEvent> followUpEvents)
     : base(transaction, isSuccessful, followUpEvents)
 {
 }
 public AreaSpawnResult(ITransactionWithMoveSupport transaction, bool isSuccessful, IEnumerable<FollowUpEvent> followUpEvents, Point spawnPosition)
     : base(transaction, isSuccessful, followUpEvents)
 {
     SpawnPosition = spawnPosition;
 }
 public MoveResult(ITransactionWithMoveSupport transaction, bool isSuccessful, IEnumerable<FollowUpEvent> followUpEvents)
 {
     Transaction = transaction;
     IsSuccessful = isSuccessful;
     FollowUpEvents = followUpEvents?.ToArray() ?? new FollowUpEvent[0];
 }