Compare commits

...

5 Commits

Author SHA1 Message Date
f728240db9
Added crude client rendering with pixi.js
Added keyboard controlling
Added fixed ball reflections on right paddle
added reflections on screen borders
Added goal (miss the paddle) detection
2022-11-04 04:05:20 +01:00
16d001c084
Updated libman and fixed client side libs 2022-11-04 02:00:42 +01:00
f31d18bd0b
Blind and likely wrong untested game logic
Added gameloop worker
2022-11-04 01:46:48 +01:00
a4e22a4c52
Added game state, allow paddle movement 2022-11-04 00:13:00 +01:00
462e27c88a
Implemented basic Pong lobby system 2022-11-03 12:29:35 +01:00
30 changed files with 31491 additions and 3 deletions

View File

@ -0,0 +1,8 @@
namespace PongGame;
public enum GameStatus {
WaitingForPlayers,
InProgress,
Finished,
Paused,
}

View File

@ -0,0 +1,7 @@
namespace PongGame.Hubs;
public interface IPongClient {
Task GameStateChanged(GameStatus state);
Task ReceiveGameState(PongGameState state);
Task UsernameChanged(string value);
}

View 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
View 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;
}
}

View 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;
}
}

View File

@ -0,0 +1,7 @@
namespace PongGame;
public enum PongPaddleDirection {
Up = -1,
Stop,
Down,
}

View 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
View 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.");
}
}

View File

@ -6,4 +6,5 @@
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Welcome</h1> <h1 class="display-4">Welcome</h1>
<a asp-area="" asp-page="/Pong">Pong</a>
</div> </div>

View File

@ -9,7 +9,7 @@ public class IndexModel : PageModel {
_logger = logger; _logger = logger;
} }
public void OnGet() { public IActionResult OnGet() {
return RedirectToPage("Pong");
} }
} }

View 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>

View 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() {
}
}

View File

@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,7 +1,14 @@
using MessagePack;
using PongGame.Hubs;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddSingleton<PongLobby>(services
=> new(services.GetRequiredService<ILogger<PongLobby>>()));
builder.Services.AddSignalR()
.AddMessagePackProtocol();
var app = builder.Build(); var app = builder.Build();
@ -15,6 +22,9 @@ app.UseRouting();
app.UseAuthorization(); app.UseAuthorization();
app.MapRazorPages(); app.UseEndpoints(endpoints => {
endpoints.MapRazorPages();
endpoints.MapHub<PongHub>("/pong/hub");
});
app.Run(); app.Run();

37
PongGame/libman.json Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1116
PongGame/wwwroot/lib/pixi/dist/pixi.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

324
package-lock.json generated Normal file
View 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
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"@microsoft/signalr": "^6.0.10",
"@microsoft/signalr-protocol-msgpack": "^6.0.10"
}
}