Compare commits
	
		
			5 Commits
		
	
	
		
			1254e32749
			...
			f728240db9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						f728240db9
	
				 | 
					
					
						|||
| 
						
						
							
						
						16d001c084
	
				 | 
					
					
						|||
| 
						
						
							
						
						f31d18bd0b
	
				 | 
					
					
						|||
| 
						
						
							
						
						a4e22a4c52
	
				 | 
					
					
						|||
| 
						
						
							
						
						462e27c88a
	
				 | 
					
					
						
							
								
								
									
										8
									
								
								PongGame/Hubs/GameState.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								PongGame/Hubs/GameState.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
namespace PongGame;
 | 
			
		||||
 | 
			
		||||
public enum GameStatus {
 | 
			
		||||
    WaitingForPlayers,
 | 
			
		||||
    InProgress,
 | 
			
		||||
    Finished,
 | 
			
		||||
    Paused,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								PongGame/Hubs/IPongClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								PongGame/Hubs/IPongClient.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
namespace PongGame.Hubs;
 | 
			
		||||
 | 
			
		||||
public interface IPongClient {
 | 
			
		||||
    Task GameStateChanged(GameStatus state);
 | 
			
		||||
    Task ReceiveGameState(PongGameState state);
 | 
			
		||||
    Task UsernameChanged(string value);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										110
									
								
								PongGame/Hubs/PongGameState.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								PongGame/Hubs/PongGameState.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
using System.Drawing;
 | 
			
		||||
 | 
			
		||||
namespace PongGame.Hubs;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Pong game state saving the positions of the paddles and the ball.
 | 
			
		||||
/// The Pong board has aspect ratio 1:2 with height 500 and width 1000.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public struct PongGameState {
 | 
			
		||||
    public const float HEIGHT = 500.0f;
 | 
			
		||||
    public const float WIDTH = 2 * HEIGHT;
 | 
			
		||||
    public const float PADDLE1_OFFSET = WIDTH / 20;
 | 
			
		||||
    public const float PADDLE2_OFFSET = WIDTH - PADDLE1_OFFSET;
 | 
			
		||||
 | 
			
		||||
    public static readonly PongGameState Initial = new() {
 | 
			
		||||
        BallState = PongBallState.Initial,
 | 
			
		||||
        Paddle1 = PongPaddleState.Initial,
 | 
			
		||||
        Paddle2 = PongPaddleState.Initial,
 | 
			
		||||
        Status = GameStatus.WaitingForPlayers,
 | 
			
		||||
        WinnerLeft = null
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public PongPaddleState Paddle1;
 | 
			
		||||
    public PongPaddleState Paddle2;
 | 
			
		||||
    public PongBallState BallState;
 | 
			
		||||
    public GameStatus Status;
 | 
			
		||||
    public bool? WinnerLeft;
 | 
			
		||||
 | 
			
		||||
    public static void Update(ref PongGameState state) {
 | 
			
		||||
        if (state.Status is not GameStatus.InProgress) return;
 | 
			
		||||
 | 
			
		||||
        PongPaddleState.Update(ref state.Paddle1);
 | 
			
		||||
        PongPaddleState.Update(ref state.Paddle2);
 | 
			
		||||
        PongBallState.Update(ref state.BallState);
 | 
			
		||||
        var ballX = state.BallState.Pos.X;
 | 
			
		||||
        if (ballX is < 0 or > WIDTH) {
 | 
			
		||||
            state.WinnerLeft = ballX < 0;
 | 
			
		||||
            state.Status = GameStatus.Finished;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Collide(in state.Paddle1, ref state.BallState, true);
 | 
			
		||||
        Collide(in state.Paddle2, ref state.BallState, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void Collide(in PongPaddleState paddle, ref PongBallState ballState, bool left) {
 | 
			
		||||
        var paddleX = left ? PADDLE1_OFFSET : PADDLE2_OFFSET;
 | 
			
		||||
        var intersection = RectangleF.Intersect(paddle.GetCollider(paddleX), ballState.GetCollider());
 | 
			
		||||
        if (intersection.IsEmpty) return;
 | 
			
		||||
 | 
			
		||||
        // TODO: continuous collision
 | 
			
		||||
        var ratio = (ballState.Pos.Y - paddle.Height + PongPaddleState.PADDLE_HALF_LENGTH) / PongPaddleState.PADDLE_LENGTH;
 | 
			
		||||
 | 
			
		||||
        // TODO: lesser angles
 | 
			
		||||
        var upAngle = left ? MathF.PI * 3 / 8 : MathF.PI * 5 / 8;
 | 
			
		||||
        var downAngle = left ? -MathF.PI * 3 / 8 : MathF.PI * 11 / 8;
 | 
			
		||||
 | 
			
		||||
        // TODO: reflect ball on surface instead of launching
 | 
			
		||||
        ballState.BallAngle = ratio * downAngle + (1 - ratio) * upAngle;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct PongBallState {
 | 
			
		||||
        public const float BALL_SPEED = 2 * BALL_RADIUS;
 | 
			
		||||
        public const float BALL_RADIUS = HEIGHT / 125;
 | 
			
		||||
 | 
			
		||||
        public static readonly PongBallState Initial = new() {
 | 
			
		||||
            BallAngle = 0.0f,
 | 
			
		||||
            Pos = new() {
 | 
			
		||||
                X = WIDTH / 2,
 | 
			
		||||
                Y = HEIGHT / 2
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        public PointF Pos;
 | 
			
		||||
        public float BallAngle;
 | 
			
		||||
 | 
			
		||||
        public static void Update(ref PongBallState state) {
 | 
			
		||||
            var (dy, dx) = MathF.SinCos(state.BallAngle);
 | 
			
		||||
            state.Pos.X += BALL_SPEED * dx;
 | 
			
		||||
            state.Pos.Y -= BALL_SPEED * dy;
 | 
			
		||||
 | 
			
		||||
            if (state.Pos.Y < BALL_RADIUS
 | 
			
		||||
                || state.Pos.Y > HEIGHT - BALL_RADIUS)
 | 
			
		||||
                state.BallAngle = 2 * MathF.PI - state.BallAngle;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RectangleF GetCollider() => new(Pos.X - BALL_RADIUS, Pos.Y - BALL_RADIUS, 2 * BALL_RADIUS, 2 * BALL_RADIUS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public struct PongPaddleState {
 | 
			
		||||
        public const float PADDLE_LENGTH = HEIGHT / 10;
 | 
			
		||||
        public const float PADDLE_HALF_LENGTH = PADDLE_LENGTH / 2;
 | 
			
		||||
        public const float PADDLE_WIDTH = PADDLE_LENGTH / 5;
 | 
			
		||||
        public const float PADDLE_SPEED = 8;
 | 
			
		||||
 | 
			
		||||
        public static readonly PongPaddleState Initial = new() {
 | 
			
		||||
            Direction = PongPaddleDirection.Stop,
 | 
			
		||||
            Height = HEIGHT / 2
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        public float Height;
 | 
			
		||||
        public PongPaddleDirection Direction;
 | 
			
		||||
 | 
			
		||||
        public static void Update(ref PongPaddleState state) {
 | 
			
		||||
            state.Height = Math.Clamp(state.Height - ((int)state.Direction) * PADDLE_SPEED, PADDLE_HALF_LENGTH, HEIGHT - PADDLE_HALF_LENGTH);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RectangleF GetCollider(float x) => new(x - PADDLE_WIDTH / 2, Height - PADDLE_HALF_LENGTH, PADDLE_WIDTH, PADDLE_LENGTH);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								PongGame/Hubs/PongHub.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								PongGame/Hubs/PongHub.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
using Microsoft.AspNetCore.SignalR;
 | 
			
		||||
 | 
			
		||||
namespace PongGame.Hubs;
 | 
			
		||||
 | 
			
		||||
public class PongHub : Hub<IPongClient> {
 | 
			
		||||
    private const string PLAYER_KEY = "PLAYER";
 | 
			
		||||
    private readonly PongLobby Lobby;
 | 
			
		||||
    private readonly ILogger<PongHub> Logger;
 | 
			
		||||
 | 
			
		||||
    public PongHub(PongLobby lobby, ILogger<PongHub> logger) : base() {
 | 
			
		||||
        Lobby = lobby;
 | 
			
		||||
        Logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PongPlayer Player {
 | 
			
		||||
        get => (Context.Items.TryGetValue(PLAYER_KEY, out var player) ? player as PongPlayer : null)
 | 
			
		||||
            ?? throw new InvalidProgramException("Player was not assigned at connection start!");
 | 
			
		||||
        set => Context.Items[PLAYER_KEY] = value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task OnConnectedAsync() {
 | 
			
		||||
        Player = Lobby.CreatePlayer();
 | 
			
		||||
        Player.Client = Clients.Client(Context.ConnectionId);
 | 
			
		||||
        Player.Username = "Anon";
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void AssertNotInRoom() {
 | 
			
		||||
        if (Player.ConnectedRoom is PongRoom currentRoom)
 | 
			
		||||
            throw new HubException($"User is already connected to room [{currentRoom}]");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PongRoom AssertInRoom() {
 | 
			
		||||
        if (Player.ConnectedRoom is not PongRoom currentRoom)
 | 
			
		||||
            throw new HubException($"User is not in any room!");
 | 
			
		||||
        return currentRoom;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<string> CreateRoom() {
 | 
			
		||||
        AssertNotInRoom();
 | 
			
		||||
        var room = Lobby.CreateRoom(Player);
 | 
			
		||||
        return Task.FromResult(room.ID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<string> JoinRoom(string roomId) {
 | 
			
		||||
        AssertNotInRoom();
 | 
			
		||||
        var room = Lobby.JoinRoom(Player, roomId);
 | 
			
		||||
        return Task.FromResult(room.ID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task MovePaddle(int dir) {
 | 
			
		||||
        var room = AssertInRoom();
 | 
			
		||||
        var direction = (PongPaddleDirection)dir;
 | 
			
		||||
        if (!Enum.IsDefined(direction))
 | 
			
		||||
            throw new HubException($"Invalid direction: {dir}!");
 | 
			
		||||
        room.MovePaddle(Player, direction);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task LeaveRoom() {
 | 
			
		||||
        Lobby.LeaveRoom(Player);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task RequestUsernameChange(string username) {
 | 
			
		||||
        // TOOD: check this
 | 
			
		||||
        Logger.LogInformation($"Player {Player} requested username change to [{username}]");
 | 
			
		||||
        Player.Username = username;
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task OnDisconnectedAsync(Exception? exception) {
 | 
			
		||||
        Lobby.RemovePlayer(Player);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								PongGame/Hubs/PongLobbyCollection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								PongGame/Hubs/PongLobbyCollection.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using Microsoft.AspNetCore.SignalR;
 | 
			
		||||
 | 
			
		||||
namespace PongGame.Hubs;
 | 
			
		||||
 | 
			
		||||
public class PongLobby {
 | 
			
		||||
    private readonly HashSet<PongPlayer> connectedPlayers = new();
 | 
			
		||||
    private readonly Dictionary<string, PongRoom> PongRooms = new();
 | 
			
		||||
 | 
			
		||||
    public PongLobby(ILogger<PongLobby> logger)
 | 
			
		||||
        => Logger = logger;
 | 
			
		||||
 | 
			
		||||
    public const int ROOM_ID_LENGTH = 4;
 | 
			
		||||
 | 
			
		||||
    public PongPlayer CreatePlayer() {
 | 
			
		||||
        var player = new PongPlayer();
 | 
			
		||||
        lock (connectedPlayers)
 | 
			
		||||
            connectedPlayers.Add(player);
 | 
			
		||||
        return player;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void RemovePlayer(PongPlayer player) {
 | 
			
		||||
        if (player.ConnectedRoom is PongRoom room)
 | 
			
		||||
            room.Leave(player);
 | 
			
		||||
        lock (connectedPlayers)
 | 
			
		||||
            _ = connectedPlayers.Remove(player);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PongRoom CreateRoom(PongPlayer player) {
 | 
			
		||||
        PongRoom room;
 | 
			
		||||
        lock (PongRooms) {
 | 
			
		||||
            room = new(GenerateRoomId(), Logger);
 | 
			
		||||
            PongRooms.Add(room.ID, room);
 | 
			
		||||
        }
 | 
			
		||||
        room.Join(player);
 | 
			
		||||
        return room;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PongRoom JoinRoom(PongPlayer player, string roomId) {
 | 
			
		||||
        PongRoom? room;
 | 
			
		||||
        lock (PongRooms) {
 | 
			
		||||
            room = PongRooms.GetValueOrDefault(roomId);
 | 
			
		||||
        }
 | 
			
		||||
        if (room is null) throw new HubException($"Room [{roomId}] not found!");
 | 
			
		||||
        room.Join(player);
 | 
			
		||||
        return room;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void LeaveRoom(PongPlayer player) {
 | 
			
		||||
        if (player.ConnectedRoom is PongRoom room)
 | 
			
		||||
            room.Leave(player);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private readonly Random random = new();
 | 
			
		||||
    private readonly ILogger<PongLobby> Logger;
 | 
			
		||||
    private const string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 | 
			
		||||
 | 
			
		||||
    private string GenerateRoomId() {
 | 
			
		||||
        string id;
 | 
			
		||||
        do {
 | 
			
		||||
            id = string.Concat(Enumerable.Range(0, ROOM_ID_LENGTH).Select(_ => ALPHABET[random.Next(ALPHABET.Length)]));
 | 
			
		||||
        } while (PongRooms.ContainsKey(id));
 | 
			
		||||
        return id;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								PongGame/Hubs/PongPaddleDirection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								PongGame/Hubs/PongPaddleDirection.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
namespace PongGame;
 | 
			
		||||
 | 
			
		||||
public enum PongPaddleDirection {
 | 
			
		||||
    Up = -1,
 | 
			
		||||
    Stop,
 | 
			
		||||
    Down,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								PongGame/Hubs/PongPlayer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								PongGame/Hubs/PongPlayer.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
 | 
			
		||||
namespace PongGame.Hubs;
 | 
			
		||||
 | 
			
		||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
 | 
			
		||||
public class PongPlayer {
 | 
			
		||||
    private string username = default!;
 | 
			
		||||
 | 
			
		||||
    public PongRoom? ConnectedRoom { get; internal set; }
 | 
			
		||||
    public string Username {
 | 
			
		||||
        get => username;
 | 
			
		||||
        internal set {
 | 
			
		||||
            if (username != value) {
 | 
			
		||||
                username = value;
 | 
			
		||||
                Task.Run(() => Client.UsernameChanged(value));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public IPongClient Client { get; internal set; } = default!;
 | 
			
		||||
 | 
			
		||||
    public override string ToString() => $"[{Username}]";
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										99
									
								
								PongGame/Hubs/PongRoom.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								PongGame/Hubs/PongRoom.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using Microsoft.AspNetCore.SignalR;
 | 
			
		||||
 | 
			
		||||
namespace PongGame.Hubs;
 | 
			
		||||
 | 
			
		||||
public class PongRoom {
 | 
			
		||||
    public string ID { get; }
 | 
			
		||||
    private readonly ILogger Logger;
 | 
			
		||||
    private const string JOIN_LOG_TEMPLATE = "[{ID}] {Player} joined pong room as player {Number}!";
 | 
			
		||||
    private const string LEAVE_LOG_TEMPLATE = "[{ID}] Player {Number} {Player} left pong room!";
 | 
			
		||||
    private const string DIRECTION_LOG_TEMPLATE = "[{ID}] Player {Number} {player} moves paddle in direction: {direction}!";
 | 
			
		||||
 | 
			
		||||
    public PongRoom(string id, ILogger logger) {
 | 
			
		||||
        ID = id;
 | 
			
		||||
        Logger = logger;
 | 
			
		||||
        gameWorker.DoWork += GameLoop;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PongPlayer? Player1 { get; private set; }
 | 
			
		||||
    public PongPlayer? Player2 { get; private set; }
 | 
			
		||||
    private PongGameState State = PongGameState.Initial;
 | 
			
		||||
 | 
			
		||||
    public void Join(PongPlayer player) {
 | 
			
		||||
        // TODO: synchronize this
 | 
			
		||||
        if (Player1 is null) {
 | 
			
		||||
            Player1 = player;
 | 
			
		||||
            Logger.LogInformation(JOIN_LOG_TEMPLATE, ID, player, 1);
 | 
			
		||||
        } else if (Player2 is null) {
 | 
			
		||||
            Player2 = player;
 | 
			
		||||
            Logger.LogInformation(JOIN_LOG_TEMPLATE, ID, player, 1);
 | 
			
		||||
        } else
 | 
			
		||||
            throw new HubException($"Lobby [{ID}] is already full!");
 | 
			
		||||
        _ = Task.Run(PlayersChanged);
 | 
			
		||||
        player.ConnectedRoom = this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Leave(PongPlayer player) {
 | 
			
		||||
        if (Player1 == player) {
 | 
			
		||||
            Player1 = null;
 | 
			
		||||
            Logger.LogInformation(LEAVE_LOG_TEMPLATE, ID, 1, player);
 | 
			
		||||
        } else if (Player2 == player) {
 | 
			
		||||
            Player2 = null;
 | 
			
		||||
            Logger.LogInformation(LEAVE_LOG_TEMPLATE, ID, 2, player);
 | 
			
		||||
        }
 | 
			
		||||
        player.ConnectedRoom = null;
 | 
			
		||||
        _ = Task.Run(PlayersChanged);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private BackgroundWorker gameWorker = new() {
 | 
			
		||||
        WorkerSupportsCancellation = true
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private void GameLoop(object? sender, DoWorkEventArgs e) {
 | 
			
		||||
        while (!gameWorker.CancellationPending
 | 
			
		||||
            && Player1 is PongPlayer p1
 | 
			
		||||
            && Player2 is PongPlayer p2
 | 
			
		||||
            && State.Status is GameStatus.InProgress) {
 | 
			
		||||
            PongGameState.Update(ref State);
 | 
			
		||||
            _ = Task.Run(() => Task.WhenAll(
 | 
			
		||||
                p1.Client.ReceiveGameState(State),
 | 
			
		||||
                p2.Client.ReceiveGameState(State)));
 | 
			
		||||
            Thread.Sleep(1000 / 60);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task PlayersChanged() {
 | 
			
		||||
        if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) {
 | 
			
		||||
            Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2);
 | 
			
		||||
            ResumeGame();
 | 
			
		||||
        } else if (Player1 is null && Player2 is null) {
 | 
			
		||||
            CloseRoom();
 | 
			
		||||
        } else
 | 
			
		||||
            PauseGame();
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void ResumeGame() {
 | 
			
		||||
        State.Status = GameStatus.InProgress;
 | 
			
		||||
        if (!gameWorker.IsBusy) gameWorker.RunWorkerAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void PauseGame() => State.Status = GameStatus.Paused;
 | 
			
		||||
    private void CloseRoom() => State.Status = GameStatus.Finished;
 | 
			
		||||
 | 
			
		||||
    public override string ToString() => $"[{ID}]";
 | 
			
		||||
 | 
			
		||||
    public void MovePaddle(PongPlayer player, PongPaddleDirection direction) {
 | 
			
		||||
        if (Player1 == player) {
 | 
			
		||||
            State.Paddle1.Direction = direction;
 | 
			
		||||
            Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction);
 | 
			
		||||
            return;
 | 
			
		||||
        } else if (Player2 == player) {
 | 
			
		||||
            State.Paddle2.Direction = direction;
 | 
			
		||||
            Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        throw new InvalidOperationException("Player is not in this room, but moved! Assumably players room wasn't deleted.");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,4 +6,5 @@
 | 
			
		||||
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
    <h1 class="display-4">Welcome</h1>
 | 
			
		||||
    <a asp-area="" asp-page="/Pong">Pong</a>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ public class IndexModel : PageModel {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void OnGet() {
 | 
			
		||||
 | 
			
		||||
    public IActionResult OnGet() {
 | 
			
		||||
        return RedirectToPage("Pong");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								PongGame/Pages/Pong.cshtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								PongGame/Pages/Pong.cshtml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
@page
 | 
			
		||||
@model PongModel
 | 
			
		||||
@{
 | 
			
		||||
  ViewData["Title"] = "Pong";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
  <h1 class="display-4">Pong</h1>
 | 
			
		||||
  <h3 id="connection">Connection Status</h3>
 | 
			
		||||
 | 
			
		||||
  <button id="createlobby" class="btn btn-primary mb-3">Create</button>
 | 
			
		||||
  <button id="leavelobby" class="btn btn-primary mb-3">Leave</button>
 | 
			
		||||
 | 
			
		||||
  <div class="input-group mb-3">
 | 
			
		||||
    <input id="roomid" type="text" class="form-control" placeholder="Room ID" aria-label="Room ID" aria-describedby="joinroom">
 | 
			
		||||
    <button id="joinroom" class="btn btn-outline-secondary" type="button">Button</button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="input-group mb-3">
 | 
			
		||||
    <input id="username" type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="setusername">
 | 
			
		||||
    <button id="setusername" class="btn btn-outline-secondary" type="button">Set Username</button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div id="canvas-container" class="mb-3"></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
 | 
			
		||||
<script src="~/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js"></script>
 | 
			
		||||
<script src="~/lib/pixi/dist/pixi.min.js"></script>
 | 
			
		||||
<script src="~/js/pong.js"></script>
 | 
			
		||||
							
								
								
									
										15
									
								
								PongGame/Pages/Pong.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								PongGame/Pages/Pong.cshtml.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.RazorPages;
 | 
			
		||||
 | 
			
		||||
namespace PongGame.Pages;
 | 
			
		||||
public class PongModel : PageModel {
 | 
			
		||||
    private readonly ILogger<PongModel> _logger;
 | 
			
		||||
 | 
			
		||||
    public PongModel(ILogger<PongModel> logger) {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void OnGet() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.10" />
 | 
			
		||||
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,14 @@
 | 
			
		||||
using MessagePack;
 | 
			
		||||
using PongGame.Hubs;
 | 
			
		||||
 | 
			
		||||
var builder = WebApplication.CreateBuilder(args);
 | 
			
		||||
 | 
			
		||||
// Add services to the container.
 | 
			
		||||
builder.Services.AddRazorPages();
 | 
			
		||||
builder.Services.AddSingleton<PongLobby>(services
 | 
			
		||||
    => new(services.GetRequiredService<ILogger<PongLobby>>()));
 | 
			
		||||
builder.Services.AddSignalR()
 | 
			
		||||
    .AddMessagePackProtocol();
 | 
			
		||||
 | 
			
		||||
var app = builder.Build();
 | 
			
		||||
 | 
			
		||||
@@ -15,6 +22,9 @@ app.UseRouting();
 | 
			
		||||
 | 
			
		||||
app.UseAuthorization();
 | 
			
		||||
 | 
			
		||||
app.MapRazorPages();
 | 
			
		||||
app.UseEndpoints(endpoints => {
 | 
			
		||||
    endpoints.MapRazorPages();
 | 
			
		||||
    endpoints.MapHub<PongHub>("/pong/hub");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.Run();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								PongGame/libman.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								PongGame/libman.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
{
 | 
			
		||||
  "version": "1.0",
 | 
			
		||||
  "defaultProvider": "jsdelivr",
 | 
			
		||||
  "libraries": [
 | 
			
		||||
    {
 | 
			
		||||
      "library": "@microsoft/signalr-protocol-msgpack@6.0.10",
 | 
			
		||||
      "destination": "wwwroot/lib/signalr/",
 | 
			
		||||
      "files": [
 | 
			
		||||
        "dist/browser/signalr-protocol-msgpack.js",
 | 
			
		||||
        "dist/browser/signalr-protocol-msgpack.js.map",
 | 
			
		||||
        "dist/browser/signalr-protocol-msgpack.min.js",
 | 
			
		||||
        "dist/browser/signalr-protocol-msgpack.min.js.map"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "library": "@microsoft/signalr@6.0.10",
 | 
			
		||||
      "destination": "wwwroot/lib/signalr/",
 | 
			
		||||
      "files": [
 | 
			
		||||
        "dist/browser/signalr.js",
 | 
			
		||||
        "dist/browser/signalr.js.map",
 | 
			
		||||
        "dist/browser/signalr.min.js",
 | 
			
		||||
        "dist/browser/signalr.min.js.map"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "provider": "jsdelivr",
 | 
			
		||||
      "library": "pixi.js@7.0.2",
 | 
			
		||||
      "destination": "wwwroot/lib/pixi/",
 | 
			
		||||
      "files": [
 | 
			
		||||
        "dist/pixi.js",
 | 
			
		||||
        "dist/pixi.js.map",
 | 
			
		||||
        "dist/pixi.min.js.map",
 | 
			
		||||
        "dist/pixi.min.js"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										169
									
								
								PongGame/wwwroot/js/pong.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								PongGame/wwwroot/js/pong.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,169 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const connection = new signalR.HubConnectionBuilder()
 | 
			
		||||
  .withUrl("/pong/hub")
 | 
			
		||||
  .withAutomaticReconnect()
 | 
			
		||||
  .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
 | 
			
		||||
  .build();
 | 
			
		||||
 | 
			
		||||
function getElement(id) {
 | 
			
		||||
  return document.getElementById(id) ?? console.error(`Element #${id} not found!`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const connectionStatus = getElement("connection");
 | 
			
		||||
const createlobby = getElement("createlobby");
 | 
			
		||||
const roomidinput = getElement("roomid");
 | 
			
		||||
const joinroom = getElement("joinroom");
 | 
			
		||||
const usernameinput = getElement("username");
 | 
			
		||||
const setusername = getElement("setusername");
 | 
			
		||||
const leavelobby = getElement("leavelobby");
 | 
			
		||||
 | 
			
		||||
connection.onclose(function (error) {
 | 
			
		||||
  if (error) {
 | 
			
		||||
    connectionStatus.textContent = "Unexpected error!";
 | 
			
		||||
    return console.error(`Connection aborted: ${error.message}`);
 | 
			
		||||
  }
 | 
			
		||||
  console.info("Disconnected!");
 | 
			
		||||
  connectionStatus.textContent = "Closed!";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
connection.onreconnecting(function (error) {
 | 
			
		||||
  if (error) {
 | 
			
		||||
    connectionStatus.textContent = "Reconnecting!";
 | 
			
		||||
    return console.error(`Connection reconnecting: ${error.message}`);
 | 
			
		||||
  }
 | 
			
		||||
  console.info("Reconnecting!");
 | 
			
		||||
  connectionStatus.textContent = "Reconnecting!";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
connection.onreconnected(function (connectionId) {
 | 
			
		||||
  console.info(`Connected as ${connectionId}!`);
 | 
			
		||||
  connectionStatus.textContent = "Connected!";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
createlobby.addEventListener("click", function (event) {
 | 
			
		||||
  connection.invoke("CreateRoom").then(function (roomId) {
 | 
			
		||||
    roomidinput.value = roomId;
 | 
			
		||||
    console.info(`Joined room [${roomId}]`);
 | 
			
		||||
  }).catch(function (err) {
 | 
			
		||||
    return console.error(err.toString());
 | 
			
		||||
  });
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
connection.on("GameStateChanged", function (state) {
 | 
			
		||||
  console.info(`Game is now in state ${state}`);
 | 
			
		||||
});
 | 
			
		||||
connection.on("UsernameChanged", function (username) {
 | 
			
		||||
  console.info(`Username is now ${username}`);
 | 
			
		||||
  usernameinput.value = username;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
joinroom.addEventListener("click", function (event) {
 | 
			
		||||
  connection.invoke("JoinRoom", roomidinput.value).catch(function (err) {
 | 
			
		||||
    return console.error(err.toString());
 | 
			
		||||
  });
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
setusername.addEventListener("click", function (event) {
 | 
			
		||||
  connection.invoke("RequestUsernameChange", usernameinput.value).catch(function (err) {
 | 
			
		||||
    return console.error(err.toString());
 | 
			
		||||
  });
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
leavelobby.addEventListener("click", function (event) {
 | 
			
		||||
  connection.invoke("LeaveRoom").catch(function (err) {
 | 
			
		||||
    return console.error(err.toString());
 | 
			
		||||
  });
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function movePaddle(direction) {
 | 
			
		||||
  connection.invoke("MovePaddle", direction).catch(function (err) {
 | 
			
		||||
    return console.error(err.toString());
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function moveUp() { return movePaddle(-1); }
 | 
			
		||||
function stopPaddle() { return movePaddle(0); }
 | 
			
		||||
function moveDown() { return movePaddle(1); }
 | 
			
		||||
 | 
			
		||||
// Create the application helper and add its render target to the page
 | 
			
		||||
let app = new PIXI.Application({ width: 1000, height: 500 });
 | 
			
		||||
getElement('canvas-container').appendChild(app.view);
 | 
			
		||||
 | 
			
		||||
let graphics = new PIXI.Graphics();
 | 
			
		||||
app.stage.addChild(graphics);
 | 
			
		||||
 | 
			
		||||
function renderPaddle(graphics, state, xMid) {
 | 
			
		||||
  var xLeft = xMid - 5;
 | 
			
		||||
  graphics.beginFill(0x00FFFF);
 | 
			
		||||
  graphics.drawRect(xLeft, state.Height - 25, 10, 50);
 | 
			
		||||
  graphics.endFill();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderBall(graphics, state) {
 | 
			
		||||
  graphics.beginFill(0xFF00FF);
 | 
			
		||||
  graphics.drawCircle(state.Pos.X, state.Pos.Y, 4);
 | 
			
		||||
  graphics.endFill();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderGameState(graphics, state) {
 | 
			
		||||
  graphics.clear();
 | 
			
		||||
  renderPaddle(graphics, state.Paddle1, 50);
 | 
			
		||||
  renderPaddle(graphics, state.Paddle2, 1000 - 50);
 | 
			
		||||
  renderBall(graphics, state.BallState);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
connection.on("ReceiveGameState", function (state) {
 | 
			
		||||
  renderGameState(graphics, state);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const keyEvent = (function () {
 | 
			
		||||
  var upPressed = false;
 | 
			
		||||
  var downPressed = false;
 | 
			
		||||
 | 
			
		||||
  function moveUpdated() {
 | 
			
		||||
    if (upPressed == downPressed) stopPaddle();
 | 
			
		||||
    else if (upPressed) moveUp();
 | 
			
		||||
    else if (downPressed) moveDown();
 | 
			
		||||
    else console.error("unknown move!");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handler(event) {
 | 
			
		||||
    if (event.repeat) return;
 | 
			
		||||
    if (event.path.indexOf(document.body) != 0) return; // only use key if it was pressed on empty space
 | 
			
		||||
    var pressed = event.type == "keyup";
 | 
			
		||||
    // W Key is 87, Up arrow is 87
 | 
			
		||||
    // S Key is 83, Down arrow is 40
 | 
			
		||||
    switch (event.keyCode) {
 | 
			
		||||
      case 87:
 | 
			
		||||
      case 38:
 | 
			
		||||
        upPressed = pressed;
 | 
			
		||||
        break;
 | 
			
		||||
      case 83:
 | 
			
		||||
      case 40:
 | 
			
		||||
        downPressed = pressed;
 | 
			
		||||
        break;
 | 
			
		||||
      default: return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    moveUpdated();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return handler;
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
document.addEventListener('keydown', keyEvent);
 | 
			
		||||
document.addEventListener('keyup', keyEvent);
 | 
			
		||||
 | 
			
		||||
connection.start().then(function () {
 | 
			
		||||
  console.info(`Connected!`);
 | 
			
		||||
  connectionStatus.textContent = "Connected!";
 | 
			
		||||
}).catch(function (err) {
 | 
			
		||||
  connectionStatus.textContent = "Connection failed!";
 | 
			
		||||
  return console.error(err.toString());
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										24183
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24183
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1116
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1116
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.min.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								PongGame/wwwroot/lib/pixi/dist/pixi.min.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2077
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2077
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3115
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3115
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.min.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								PongGame/wwwroot/lib/signalr/dist/browser/signalr.min.js.map
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										324
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,324 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "PongGame",
 | 
			
		||||
  "lockfileVersion": 2,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@microsoft/signalr": "^6.0.10",
 | 
			
		||||
        "@microsoft/signalr-protocol-msgpack": "^6.0.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@microsoft/signalr": {
 | 
			
		||||
      "version": "6.0.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-6.0.10.tgz",
 | 
			
		||||
      "integrity": "sha512-ND9LiIYac+ZDgCgW2QzpNfe9BTiOtjc2AX/2GtFIhRGhEzx5CixcNANg2VGj27IAxycAPPnEoy7+QA31Eil7QQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "abort-controller": "^3.0.0",
 | 
			
		||||
        "eventsource": "^1.0.7",
 | 
			
		||||
        "fetch-cookie": "^0.11.0",
 | 
			
		||||
        "node-fetch": "^2.6.7",
 | 
			
		||||
        "ws": "^7.4.5"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@microsoft/signalr-protocol-msgpack": {
 | 
			
		||||
      "version": "6.0.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-6.0.10.tgz",
 | 
			
		||||
      "integrity": "sha512-Xj3QuH/HMcLGtc+iZjE8BH/auQVn4FQY9adv7M/wsMpT9TEe1iQJKjD0Hlh+Y42ioNaO0MFVlfFXhkYUK7y5QQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@microsoft/signalr": ">=6.0.10",
 | 
			
		||||
        "@msgpack/msgpack": "^2.7.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@msgpack/msgpack": {
 | 
			
		||||
      "version": "2.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/abort-controller": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "event-target-shim": "^5.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.5"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/event-target-shim": {
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/eventsource": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.12.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fetch-cookie": {
 | 
			
		||||
      "version": "0.11.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz",
 | 
			
		||||
      "integrity": "sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tough-cookie": "^2.3.3 || ^3.0.1 || ^4.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/node-fetch": {
 | 
			
		||||
      "version": "2.6.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
 | 
			
		||||
      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "whatwg-url": "^5.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "4.x || >=6.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "encoding": "^0.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "encoding": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/psl": {
 | 
			
		||||
      "version": "1.9.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
 | 
			
		||||
      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/punycode": {
 | 
			
		||||
      "version": "2.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/querystringify": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/requires-port": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tough-cookie": {
 | 
			
		||||
      "version": "4.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "psl": "^1.1.33",
 | 
			
		||||
        "punycode": "^2.1.1",
 | 
			
		||||
        "universalify": "^0.2.0",
 | 
			
		||||
        "url-parse": "^1.5.3"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tr46": {
 | 
			
		||||
      "version": "0.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/universalify": {
 | 
			
		||||
      "version": "0.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/url-parse": {
 | 
			
		||||
      "version": "1.5.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
 | 
			
		||||
      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "querystringify": "^2.1.1",
 | 
			
		||||
        "requires-port": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/webidl-conversions": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/whatwg-url": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tr46": "~0.0.3",
 | 
			
		||||
        "webidl-conversions": "^3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ws": {
 | 
			
		||||
      "version": "7.5.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
 | 
			
		||||
      "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8.3.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "bufferutil": "^4.0.1",
 | 
			
		||||
        "utf-8-validate": "^5.0.2"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "bufferutil": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        },
 | 
			
		||||
        "utf-8-validate": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@microsoft/signalr": {
 | 
			
		||||
      "version": "6.0.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-6.0.10.tgz",
 | 
			
		||||
      "integrity": "sha512-ND9LiIYac+ZDgCgW2QzpNfe9BTiOtjc2AX/2GtFIhRGhEzx5CixcNANg2VGj27IAxycAPPnEoy7+QA31Eil7QQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "abort-controller": "^3.0.0",
 | 
			
		||||
        "eventsource": "^1.0.7",
 | 
			
		||||
        "fetch-cookie": "^0.11.0",
 | 
			
		||||
        "node-fetch": "^2.6.7",
 | 
			
		||||
        "ws": "^7.4.5"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@microsoft/signalr-protocol-msgpack": {
 | 
			
		||||
      "version": "6.0.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-6.0.10.tgz",
 | 
			
		||||
      "integrity": "sha512-Xj3QuH/HMcLGtc+iZjE8BH/auQVn4FQY9adv7M/wsMpT9TEe1iQJKjD0Hlh+Y42ioNaO0MFVlfFXhkYUK7y5QQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@microsoft/signalr": ">=6.0.10",
 | 
			
		||||
        "@msgpack/msgpack": "^2.7.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@msgpack/msgpack": {
 | 
			
		||||
      "version": "2.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "abort-controller": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "event-target-shim": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "event-target-shim": {
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "eventsource": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA=="
 | 
			
		||||
    },
 | 
			
		||||
    "fetch-cookie": {
 | 
			
		||||
      "version": "0.11.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz",
 | 
			
		||||
      "integrity": "sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "tough-cookie": "^2.3.3 || ^3.0.1 || ^4.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node-fetch": {
 | 
			
		||||
      "version": "2.6.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
 | 
			
		||||
      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "whatwg-url": "^5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "psl": {
 | 
			
		||||
      "version": "1.9.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
 | 
			
		||||
      "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
 | 
			
		||||
    },
 | 
			
		||||
    "punycode": {
 | 
			
		||||
      "version": "2.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
 | 
			
		||||
    },
 | 
			
		||||
    "querystringify": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "requires-port": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "tough-cookie": {
 | 
			
		||||
      "version": "4.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "psl": "^1.1.33",
 | 
			
		||||
        "punycode": "^2.1.1",
 | 
			
		||||
        "universalify": "^0.2.0",
 | 
			
		||||
        "url-parse": "^1.5.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tr46": {
 | 
			
		||||
      "version": "0.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
 | 
			
		||||
    },
 | 
			
		||||
    "universalify": {
 | 
			
		||||
      "version": "0.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="
 | 
			
		||||
    },
 | 
			
		||||
    "url-parse": {
 | 
			
		||||
      "version": "1.5.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
 | 
			
		||||
      "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "querystringify": "^2.1.1",
 | 
			
		||||
        "requires-port": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "webidl-conversions": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "whatwg-url": {
 | 
			
		||||
      "version": "5.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "tr46": "~0.0.3",
 | 
			
		||||
        "webidl-conversions": "^3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "ws": {
 | 
			
		||||
      "version": "7.5.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
 | 
			
		||||
      "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
 | 
			
		||||
      "requires": {}
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@microsoft/signalr": "^6.0.10",
 | 
			
		||||
    "@microsoft/signalr-protocol-msgpack": "^6.0.10"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user