do not respawn inactive players
This commit is contained in:
		
							parent
							
								
									cd12ab7bde
								
							
						
					
					
						commit
						abad2c95c8
					
				
					 11 changed files with 91 additions and 48 deletions
				
			
		
							
								
								
									
										2
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
										
									
									
									
								
							|  | @ -6,5 +6,5 @@ build: | |||
| 	podman build . --tag=$(TAG) | ||||
| 
 | ||||
| run: build | ||||
| 	podman run -i -p 80:3000 localhost/$(TAG):latest | ||||
| 	podman run -i -p 3000:3000 localhost/$(TAG):latest | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ type PlayerInfoMessage = { | |||
|     readonly scores: Scores; | ||||
|     readonly controls: string; | ||||
|     readonly tank?: TankInfo; | ||||
|     readonly openConnections: number; | ||||
| } | ||||
| 
 | ||||
| export default function PlayerInfo({player}: { player: string }) { | ||||
|  | @ -81,6 +82,8 @@ export default function PlayerInfo({player}: { player: string }) { | |||
|             <ScoreRow name="pixels moved" value={lastJsonMessage.scores.pixelsMoved}/> | ||||
| 
 | ||||
|             <ScoreRow name="score" value={lastJsonMessage.scores.overallScore}/> | ||||
| 
 | ||||
|             <ScoreRow name="connections" value={lastJsonMessage.openConnections}/> | ||||
|             </tbody> | ||||
|         </table> | ||||
|     </Column>; | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ internal sealed class TankSpawnQueue( | |||
|             return false; // no one on queue | ||||
| 
 | ||||
|         var now = DateTime.Now; | ||||
|         if (player.LastInput + _idleTimeout < now) | ||||
|         if (player.OpenConnections < 1 || player.LastInput + _idleTimeout < now) | ||||
|         { | ||||
|             // player idle | ||||
|             _queue.Enqueue(player); | ||||
|  |  | |||
|  | @ -7,7 +7,8 @@ namespace TanksServer.Interactivity; | |||
| internal sealed class ClientScreenServer( | ||||
|     ILogger<ClientScreenServer> logger, | ||||
|     ILoggerFactory loggerFactory | ||||
| ) : WebsocketServer<ClientScreenServerConnection>(logger), IFrameConsumer | ||||
| ) : WebsocketServer<ClientScreenServerConnection>(logger), | ||||
|     IFrameConsumer | ||||
| { | ||||
|     public Task HandleClientAsync(WebSocket socket, Player? player) | ||||
|         => base.HandleClientAsync(new ClientScreenServerConnection( | ||||
|  |  | |||
|  | @ -1,16 +1,11 @@ | |||
| using System.Buffers; | ||||
| using System.Diagnostics; | ||||
| using System.Net.WebSockets; | ||||
| using DisplayCommands; | ||||
| using TanksServer.Graphics; | ||||
| 
 | ||||
| namespace TanksServer.Interactivity; | ||||
| 
 | ||||
| internal sealed class ClientScreenServerConnection( | ||||
|     WebSocket webSocket, | ||||
|     ILogger<ClientScreenServerConnection> logger, | ||||
|     Player? player | ||||
| ) : WebsocketServerConnection(logger, new ByteChannelWebSocket(webSocket, logger, 0)) | ||||
| internal sealed class ClientScreenServerConnection : WebsocketServerConnection | ||||
| { | ||||
|     private sealed record class Package( | ||||
|         IMemoryOwner<byte> PixelsOwner, | ||||
|  | @ -20,12 +15,21 @@ internal sealed class ClientScreenServerConnection( | |||
|     ); | ||||
| 
 | ||||
|     private readonly MemoryPool<byte> _memoryPool = MemoryPool<byte>.Shared; | ||||
|     private readonly PlayerScreenData? _playerDataBuilder; | ||||
|     private readonly Player? _player; | ||||
|     private int _wantsFrameOnTick = 1; | ||||
|     private Package? _next; | ||||
| 
 | ||||
|     private readonly PlayerScreenData? _playerDataBuilder = player == null | ||||
|         ? null | ||||
|         : new PlayerScreenData(logger, player); | ||||
|     public ClientScreenServerConnection(WebSocket webSocket, | ||||
|         ILogger<ClientScreenServerConnection> logger, | ||||
|         Player? player) : base(logger, new ByteChannelWebSocket(webSocket, logger, 0)) | ||||
|     { | ||||
|         _player = player; | ||||
|         _player?.IncrementConnectionCount(); | ||||
|         _playerDataBuilder = player == null | ||||
|             ? null | ||||
|             : new PlayerScreenData(logger, player); | ||||
|     } | ||||
| 
 | ||||
|     protected override ValueTask HandleMessageAsync(Memory<byte> _) | ||||
|     { | ||||
|  | @ -72,6 +76,12 @@ internal sealed class ClientScreenServerConnection( | |||
|         oldNext?.PlayerDataOwner?.Dispose(); | ||||
|     } | ||||
| 
 | ||||
|     public override ValueTask RemovedAsync() | ||||
|     { | ||||
|         _player?.DecrementConnectionCount(); | ||||
|         return ValueTask.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     private async ValueTask SendAndDisposeAsync(Package package) | ||||
|     { | ||||
|         try | ||||
|  |  | |||
|  | @ -2,12 +2,18 @@ using System.Net.WebSockets; | |||
| 
 | ||||
| namespace TanksServer.Interactivity; | ||||
| 
 | ||||
| internal sealed class ControlsServerConnection( | ||||
|     WebSocket socket, | ||||
|     ILogger<ControlsServerConnection> logger, | ||||
|     Player player | ||||
| ) : WebsocketServerConnection(logger, new ByteChannelWebSocket(socket, logger, 2)) | ||||
| internal sealed class ControlsServerConnection : WebsocketServerConnection | ||||
| { | ||||
|     private readonly Player _player; | ||||
| 
 | ||||
|     public ControlsServerConnection(WebSocket socket, | ||||
|         ILogger<ControlsServerConnection> logger, | ||||
|         Player player) : base(logger, new ByteChannelWebSocket(socket, logger, 2)) | ||||
|     { | ||||
|         _player = player; | ||||
|         _player.IncrementConnectionCount(); | ||||
|     } | ||||
| 
 | ||||
|     private enum MessageType : byte | ||||
|     { | ||||
|         Enable = 0x01, | ||||
|  | @ -28,7 +34,7 @@ internal sealed class ControlsServerConnection( | |||
|         var type = (MessageType)buffer.Span[0]; | ||||
|         var control = (InputType)buffer.Span[1]; | ||||
| 
 | ||||
|         Logger.LogTrace("player input {} {} {}", player.Name, type, control); | ||||
|         Logger.LogTrace("player input {} {} {}", _player.Name, type, control); | ||||
| 
 | ||||
|         var isEnable = type switch | ||||
|         { | ||||
|  | @ -37,24 +43,24 @@ internal sealed class ControlsServerConnection( | |||
|             _ => throw new ArgumentException("invalid message type") | ||||
|         }; | ||||
| 
 | ||||
|         player.LastInput = DateTime.Now; | ||||
|         _player.LastInput = DateTime.Now; | ||||
| 
 | ||||
|         switch (control) | ||||
|         { | ||||
|             case InputType.Forward: | ||||
|                 player.Controls.Forward = isEnable; | ||||
|                 _player.Controls.Forward = isEnable; | ||||
|                 break; | ||||
|             case InputType.Backward: | ||||
|                 player.Controls.Backward = isEnable; | ||||
|                 _player.Controls.Backward = isEnable; | ||||
|                 break; | ||||
|             case InputType.Left: | ||||
|                 player.Controls.TurnLeft = isEnable; | ||||
|                 _player.Controls.TurnLeft = isEnable; | ||||
|                 break; | ||||
|             case InputType.Right: | ||||
|                 player.Controls.TurnRight = isEnable; | ||||
|                 _player.Controls.TurnRight = isEnable; | ||||
|                 break; | ||||
|             case InputType.Shoot: | ||||
|                 player.Controls.Shoot = isEnable; | ||||
|                 _player.Controls.Shoot = isEnable; | ||||
|                 break; | ||||
|             default: | ||||
|                 throw new ArgumentException("invalid control type"); | ||||
|  | @ -62,4 +68,10 @@ internal sealed class ControlsServerConnection( | |||
| 
 | ||||
|         return ValueTask.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     public override ValueTask RemovedAsync() | ||||
|     { | ||||
|         _player.DecrementConnectionCount(); | ||||
|         return ValueTask.CompletedTask; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -4,16 +4,23 @@ using TanksServer.GameLogic; | |||
| 
 | ||||
| namespace TanksServer.Interactivity; | ||||
| 
 | ||||
| internal sealed class PlayerInfoConnection( | ||||
|     Player player, | ||||
|     ILogger logger, | ||||
|     WebSocket rawSocket, | ||||
|     MapEntityManager entityManager | ||||
| ) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)) | ||||
| internal sealed class PlayerInfoConnection : WebsocketServerConnection | ||||
| { | ||||
|     private int _wantsInfoOnTick = 1; | ||||
|     private byte[]? _lastMessage = null; | ||||
|     private byte[]? _nextMessage = null; | ||||
|     private readonly Player _player; | ||||
|     private readonly MapEntityManager _entityManager; | ||||
| 
 | ||||
|     public PlayerInfoConnection(Player player, | ||||
|         ILogger logger, | ||||
|         WebSocket rawSocket, | ||||
|         MapEntityManager entityManager) : base(logger, new ByteChannelWebSocket(rawSocket, logger, 0)) | ||||
|     { | ||||
|         _player = player; | ||||
|         _entityManager = entityManager; | ||||
|         _player.IncrementConnectionCount(); | ||||
|     } | ||||
| 
 | ||||
|     protected override ValueTask HandleMessageAsync(Memory<byte> buffer) | ||||
|     { | ||||
|  | @ -41,9 +48,15 @@ internal sealed class PlayerInfoConnection( | |||
|         Interlocked.Exchange(ref _nextMessage, response); | ||||
|     } | ||||
| 
 | ||||
|     public override ValueTask RemovedAsync() | ||||
|     { | ||||
|         _player.DecrementConnectionCount(); | ||||
|         return ValueTask.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     private byte[] GetMessageToSend() | ||||
|     { | ||||
|         var tank = entityManager.GetCurrentTankOfPlayer(player); | ||||
|         var tank = _entityManager.GetCurrentTankOfPlayer(_player); | ||||
| 
 | ||||
|         TankInfo? tankInfo = null; | ||||
|         if (tank != null) | ||||
|  | @ -52,7 +65,12 @@ internal sealed class PlayerInfoConnection( | |||
|             tankInfo = new TankInfo(tank.Orientation, magazine, tank.Position.ToPixelPosition(), tank.Moving); | ||||
|         } | ||||
| 
 | ||||
|         var info = new PlayerInfo(player.Name, player.Scores, player.Controls.ToDisplayString(), tankInfo); | ||||
|         var info = new PlayerInfo( | ||||
|             _player.Name, | ||||
|             _player.Scores, | ||||
|             _player.Controls.ToDisplayString(), | ||||
|             tankInfo, | ||||
|             _player.OpenConnections); | ||||
| 
 | ||||
|         // TODO: switch to async version with pre-allocated buffer / IMemoryOwner | ||||
|         return JsonSerializer.SerializeToUtf8Bytes(info, AppSerializerContext.Default.PlayerInfo); | ||||
|  |  | |||
|  | @ -47,6 +47,7 @@ internal abstract class WebsocketServer<T>( | |||
|         await AddConnectionAsync(connection); | ||||
|         await connection.ReceiveAsync(); | ||||
|         await RemoveConnectionAsync(connection); | ||||
|         await connection.RemovedAsync(); | ||||
|     } | ||||
| 
 | ||||
|     private async ValueTask LockedAsync(Func<ValueTask> action, CancellationToken cancellationToken) | ||||
|  | @ -62,7 +63,7 @@ internal abstract class WebsocketServer<T>( | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() => _mutex.Dispose(); | ||||
|     public virtual void Dispose() => _mutex.Dispose(); | ||||
| 
 | ||||
|     public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,20 +22,9 @@ internal abstract class WebsocketServerConnection( | |||
|         Logger.LogTrace("done receiving"); | ||||
|     } | ||||
| 
 | ||||
|     public abstract ValueTask RemovedAsync(); | ||||
| 
 | ||||
|     protected abstract ValueTask HandleMessageAsync(Memory<byte> buffer); | ||||
| 
 | ||||
|     protected async ValueTask LockedAsync(Func<ValueTask> action) | ||||
|     { | ||||
|         await _mutex.WaitAsync(); | ||||
|         try | ||||
|         { | ||||
|             await action(); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             _mutex.Release(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() => _mutex.Dispose(); | ||||
|     public virtual void Dispose() => _mutex.Dispose(); | ||||
| } | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ namespace TanksServer.Models; | |||
| 
 | ||||
| internal sealed class Player : IEquatable<Player> | ||||
| { | ||||
|     private int _openConnections; | ||||
| 
 | ||||
|     public required string Name { get; init; } | ||||
| 
 | ||||
|     [JsonIgnore] public PlayerControls Controls { get; } = new(); | ||||
|  | @ -12,6 +14,8 @@ internal sealed class Player : IEquatable<Player> | |||
| 
 | ||||
|     public DateTime LastInput { get; set; } = DateTime.Now; | ||||
| 
 | ||||
|     public int OpenConnections => _openConnections; | ||||
| 
 | ||||
|     public override bool Equals(object? obj) => obj is Player p && Equals(p); | ||||
| 
 | ||||
|     public bool Equals(Player? other) => other?.Name == Name; | ||||
|  | @ -21,4 +25,8 @@ internal sealed class Player : IEquatable<Player> | |||
|     public static bool operator ==(Player? left, Player? right) => Equals(left, right); | ||||
| 
 | ||||
|     public static bool operator !=(Player? left, Player? right) => !Equals(left, right); | ||||
| 
 | ||||
|     internal void IncrementConnectionCount() => Interlocked.Increment(ref _openConnections); | ||||
| 
 | ||||
|     internal void DecrementConnectionCount() => Interlocked.Decrement(ref _openConnections); | ||||
| } | ||||
|  |  | |||
|  | @ -11,5 +11,6 @@ internal record struct PlayerInfo( | |||
|     string Name, | ||||
|     Scores Scores, | ||||
|     string Controls, | ||||
|     TankInfo? Tank | ||||
|     TankInfo? Tank, | ||||
|     int OpenConnections | ||||
| ); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter