public async Task<bool> MoveAsync(Point sourcePosition, Point targetPosition) { var source = await Map.GetAsync(sourcePosition); var target = await Map.GetAsync(targetPosition); var initiator = new MoveInitiator(source.Tile.Entity, sourcePosition); var transaction = new TransactionWithMoveSupport(initiator); var result = await Map.MoveAsync(sourcePosition, targetPosition, transaction); var request = new RemoteMoveRequest( new VersionedPoint(sourcePosition, source.Version), new VersionedPoint(targetPosition, target.Version), result.Transaction.Changes.Select(c => new VersionedPoint(c.Key, c.Value.Version))); var remoteMoveResult = await _serverStub.MoveAsync(request, PlayerInfo.PlayerId); if (remoteMoveResult.IsSuccessful) { // Commit transaction await transaction.CommitAsync(Map, remoteMoveResult.NewVersion); AnalyzeEvents(transaction.Events); return true; } else { // Our map was not up to date => apply updated chunks foreach (var kvp in remoteMoveResult.ChunkUpdates) await ApplyChunkAsync(kvp.Value, kvp.Key); } return false; }
/// <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 void OnChunkLoaded(KeyValuePair<Point, IChunk> kvp) { // TODO: Decide to what extent chunk loading should be // handled in Map and in GameServer. // Properly initialize the chunk. // Example scenarios: On chunk loading... // - a button should be activated immediately if there's an entity pressing it // - a piston should extend immediately if its trigger is on // - a balloon should be popped immediately if it is on a pin var chunkPoints = new Rectangle(kvp.Key.X * Chunk.Size, kvp.Key.Y * Chunk.Size, Chunk.Size - 1, Chunk.Size - 1); var transaction = new TransactionWithMoveSupport(MoveInitiator.Empty); var followUpEvents = await ((Map)Map).UpdateTilesAsync(chunkPoints, transaction); using (await _clientsLock.LockAsync()) await CommitAndBroadcastAsync(transaction, followUpEvents); }
private async void OnChunkLoaded(KeyValuePair<Point, IChunk> kvp) { for (var y = 0; y < Chunk.Size; y++) { for (var x = 0; x < Chunk.Size; x++) { var localPosition = new Point(x, y); var globalPosition = kvp.Key * Chunk.Size + localPosition; var tileInfo = kvp.Value[localPosition]; Dependencies.AddDependenciesOf(tileInfo.Tile, globalPosition); // For entities emit a spawn event if (tileInfo.Tile.Entity != Entity.None) { var spawnEvent = new EntitySpawnEvent(globalPosition, tileInfo.Tile.Entity); _eventSource.OnNext(spawnEvent); } } } // Properly initialize the chunk. // Example scenarios: On chunk loading... // - a button should be activated immediately if there's an entity pressing it // - a balloon should be popped immediately if it is on a pin var chunkPoints = new Rectangle(kvp.Key.X * Chunk.Size, kvp.Key.Y * Chunk.Size, Chunk.Size - 1, Chunk.Size - 1); var transaction = new TransactionWithMoveSupport(MoveInitiator.Empty); await UpdateTilesAsync(chunkPoints, transaction); _eventSource.OnNext(new ChunkAddedEvent(kvp.Value)); // TODO: This is problematic! A commit is done directly on the map // bypassing the GameServer. We might have to forward the new chunk // to some of the players. // On client side we can't just invent some new version numbers. await transaction.CommitAsync(this, Metadata?.NextTileVersion() ?? 0); }
private async Task RunAsync() { // 4 updates per second var targetElapsedTime = TimeSpan.FromSeconds(.25); while (true) { var startTime = DateTimeOffset.Now; // Run all scheduled moves that should have run until now while (_scheduledFollowUpEvents.Any() && _scheduledFollowUpEvents.First().ExecutionTime <= startTime) { // Get the "oldest" follow-up event var followUpEvent = _scheduledFollowUpEvents.OrderBy(e => e.ExecutionTime).First(); _scheduledFollowUpEvents.Remove(followUpEvent); var tileInfo = await Map.GetAsync(followUpEvent.Position); // Trigger follow-up transactions // e.g. a teleporter might use this to teleport an entity // (which can result in further follow-up events). var transaction = new TransactionWithMoveSupport(followUpEvent.Initiator); var args = new GameplayArgs(transaction, Map) as IFollowUpArgs; await tileInfo.Tile.OnFollowUpTransactionAsync(args, followUpEvent.Position); using (await _clientsLock.LockAsync()) await CommitAndBroadcastAsync(transaction, args.FollowUpEvents); } var elapsed = DateTimeOffset.Now - startTime; var remaining = targetElapsedTime - elapsed; if (remaining > TimeSpan.Zero) await Task.Delay(remaining); } }
/// <inheritdoc/> public async Task<bool> ResetRegionAsync(string playerId) { using (await _clientsLock.LockAsync()) { var player = Map.Metadata.Players[playerId]; var resetTransaction = new TransactionWithMoveSupport(MoveInitiator.Empty); var resetResult = await Map.ResetRegionAsync(player.Position, resetTransaction); if (resetResult.IsSuccessful) { await CommitAndBroadcastAsync(resetResult.Transaction, resetResult.FollowUpEvents); return true; } else { return false; } } }
/// <inheritdoc/> public async Task<RemoteMoveResult> MoveAsync(RemoteMoveRequest move, string playerId) { using (await _clientsLock.LockAsync()) { var connection = GetClient(playerId); await connection.MoveSemaphore.WaitAsync(); try { var source = await Map.GetAsync(move.SourcePosition); var target = await Map.GetAsync(move.TargetPosition); var initiator = new MoveInitiator(source.Tile.Entity, move.SourcePosition); var transaction = new TransactionWithMoveSupport(initiator); var moveResult = await Map.MoveAsync(move.SourcePosition, move.TargetPosition, transaction); // Compare affected tiles of the client and those of the server var clientAffectedTiles = move.AffectedPositions.Union(new[] { move.SourcePosition, move.TargetPosition }); var serverAffectedTiles = moveResult.Transaction.Changes .Select(c => new VersionedPoint(c.Key, c.Value.Version)) .Union(new[] { new VersionedPoint(move.SourcePosition, source.Version), new VersionedPoint(move.TargetPosition, target.Version) }) .ToList(); var versions = serverAffectedTiles.FullOuterJoin(clientAffectedTiles, vp => vp.Position, vp => vp.Position, (serverVP, clientVP, p) => new { Position = p, ServerVersion = serverVP.Version, ClientVersion = clientVP.Version }, VersionedPoint.Empty, VersionedPoint.Empty); if (versions.Any(v => v.ClientVersion != -1 && v.ServerVersion != -1 && v.ClientVersion > v.ServerVersion)) throw new NotImplementedException("This should not happen. Clients must not have a newer version than the server"); var conflictingVersions = versions.Where(v => (v.ClientVersion == -1 && v.ServerVersion != -1) || (v.ClientVersion != -1 && v.ServerVersion == -1) || (v.ClientVersion != -1 && v.ServerVersion != -1 && v.ClientVersion < v.ServerVersion)); if (conflictingVersions.Any()) { // Client not up to date => Cancel move request and send up-to-date chunks var chunks = await Task.WhenAll(conflictingVersions .Select(v => v.Position / Chunk.Size).Distinct() .Select(async index => new KeyValuePair<Point, IChunk>(index, await Map.ChunkLoader.GetAsync(index)))); return RemoteMoveResult.CreateFaulted(chunks); } else { // Client and server are on the same version (regarding affected tiles) // => Commit and schedule follow-up transactions // => Notify other clients about the changes var newVersion = await CommitAndBroadcastAsync(moveResult.Transaction, moveResult.FollowUpEvents, connection); return RemoteMoveResult.CreateSuccessful(newVersion); } } finally { connection.MoveSemaphore.Release(); } } }
/// <inheritdoc/> public async Task LeaveAsync(string playerId) { using (await _clientsLock.LockAsync()) { var connection = GetClient(playerId); var playerInfo = Map.Metadata.Players[playerId]; // Remove PlayerEntity from Map (despawn) var despawnTransaction = new TransactionWithMoveSupport(MoveInitiator.Empty); var despawnResult = await Map.DespawnAsync(playerInfo.Position, despawnTransaction); if (despawnResult.IsSuccessful) { await CommitAndBroadcastAsync(despawnResult.Transaction, despawnResult.FollowUpEvents, connection); // Unload all the chunks currently loaded by the client while (connection.LoadedChunks.Any()) UnloadChunkCore(connection.LoadedChunks.First(), connection); _clients.Remove(playerId); Map.Emit(new PlayerLeaveEvent(connection.ClientInfo)); } else { // TODO: What now? We could not despawn the player! throw new NotImplementedException(); } } }