[Scummvm-git-logs] scummvm-sites multiplayer -> 8c88e8770dd51893c0e7dd4bdd4f95387a49fa98
sev-
noreply at scummvm.org
Tue Mar 28 23:16:14 UTC 2023
This automated email contains information about 21 new commits which have been
pushed to the 'scummvm-sites' repo located at https://github.com/scummvm/scummvm-sites .
Summary:
5548e39a11 MULTIPLAYER: Initial codebase rewrite.
28043e8ce7 MULTIPLAYER: DOCKER: Use Debian image.
512fbde9e6 MULTIPLAYER: Relay support.
44ac81fbf2 MULTIPLAYER: Add supported Backyard Sports titles.
ec3d0ded4f MULTIPLAYER: WEB: Don't use url_for for static.
979f88da2d MULTIPLAYER: Remove output whitespace
8432f3566e MULTIPLAYER: Add lobby server code.
c375eb861f MULTIPLAYER: LOBBY: Send session server address.
ad9bbd832d MULTIPLAYER: LOBBY: Rename login endpoint.
08e75d131a MULTIPLAYER: LOBBY: Removed relay messages.
da987f4117 MULTIPLAYER: Improve relay setup through Redis.
b0c13bda75 MULTIPLAYER: Accept and show max players.
0eb4eebe28 README: Update repo link
85b476f346 MULTIPLAYER: Format Python code.
e1a7a482a9 MULTIPLAYER: Add logo and adjust paths to it.
47a3ac5a40 MULTIPLAYER: Add missing end line.
a59fb4347c MULTIPLAYER: Replace aioredis with redis, update README
8190b98cbc MULTIPLAYER: Make redis start before server
d002097a1b MULTIPLAYER: Base web image on python:3.9
08b6a47f2f MULTIPLAYER: Add GPLv3 license and file headers.
8c88e8770d MULTIPLAYER: Sanity check game data packets.
Commit: 5548e39a110518c1bbc341506c3121410bbf55aa
https://github.com/scummvm/scummvm-sites/commit/5548e39a110518c1bbc341506c3121410bbf55aa
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Initial codebase rewrite.
Session creation, storage, and joining works.
Includes web server using FastAPI.
Changed paths:
A Dockerfile
A main.py
A requirements.txt
A web/Dockerfile
A web/config.py
A web/main.py
A web/requirements.txt
A web/static/icons/moonbase.png
A web/static/style.css
A web/templates/game.html
A web/templates/index.html
R composer.json
R composer.lock
R logs/README.md
R phpunit.xml
R public/.htaccess
R public/cloud-style.css
R public/images/box.png
R public/images/dropbox.png
R public/images/google_drive.png
R public/images/onedrive.png
R public/index.php
R src/dependencies.php
R src/middleware.php
R src/routes.php
R src/settings.php
R templates/index.phtml
R templates/token.phtml
R tests/Functional/BaseTestCase.php
R tests/Functional/HomepageTest.php
.gitignore
README.md
docker-compose.yml
diff --git a/.gitignore b/.gitignore
index adf1997..68bc17f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,160 @@
-/vendor/
-/logs/*
-!/logs/README.md
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3f16dba
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM python:3.9-buster
+
+WORKDIR /usr/src/app
+
+COPY requirements.txt ./
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+CMD [ "python", "main.py" ]
\ No newline at end of file
diff --git a/README.md b/README.md
index d7091a3..1f7197d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,48 @@
-# ScummVM Multiplayer Server
+# ScummVM Multiplayer
-Currently supports multiplayer games for Moonbase Commander
+This project contains code for hosting online multiplayer lobbies for Moonbase Commander. This is currently WIP.
+## Getting Started
+### Installing
+Both the session and web servers requires Redis to be installed. This is needed for both servers to share session data to each other.
+
+Clone this repo and checkout to the multiplayer branch:
+```
+git clone https://github.com/LittleToonCat/scummvm-sites.git
+
+git checkout multiplayer
+```
+
+To start the session server, create a new virtual envrionment and install the requirements.
+```
+python3 -m venv .env
+source .env/bin/activate
+
+python3 -m pip install -r requirements.txt
+```
+if you're planning to run the web server, install the requirements located in the web directory.
+```
+python3 -m pip install -r web/requirements.txt
+```
+
+### Running
+To run the session server, simply run the main.py script
+```
+python3 main.py
+```
+It should listen for connections on port 9120. Remember to configure ScummVM to connect to localhost or whatever address your server is running in.
+
+Running a web server is unnecessary if you just want your server to host sessions, but if you want to, you can start one up by using uvicorn.
+```
+cd web
+uvicorn main:app --reload
+```
+
+## Deployment
+Both the session and web server can be run within Docker via docker-compose.
+```
+docker-compose build
+docker-compose up
+```
+
+This will build Docker images for both servers and starts a container for them simultaneously alongside with Redis.
\ No newline at end of file
diff --git a/composer.json b/composer.json
deleted file mode 100644
index fcc8057..0000000
--- a/composer.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "name": "scummvm/multiplayer",
- "description": "ScummVM Multiplayer server",
- "keywords": ["scummvm"],
- "homepage": "https://multiplayer.scummvm.org",
- "license": "MIT",
- "authors": [
- {
- "name": "Matan Bareket",
- "email": "mataniko at scummvm.org"
- },
- {
- "name": "Eugene Sandulenko",
- "email": "sev at scummvm.org"
- }
- ],
- "require": {
- "php": ">=5.6",
- "guzzlehttp/guzzle": "^6.3",
- "monolog/monolog": "^1.17",
- "nikolaposa/rate-limit": "^1.0",
- "pragmarx/random": "^0.2.2",
- "predis/predis": "^1.1",
- "slim/php-view": "^2.0",
- "slim/slim": "^3.1"
- },
- "require-dev": {
- "phpunit/phpunit": ">=5.0",
- "squizlabs/php_codesniffer": "^3.4"
- },
- "autoload-dev": {
- "psr-4": {
- "Tests\\": "tests/"
- }
- },
- "config": {
- "process-timeout": 0,
- "sort-packages": true
- },
- "scripts": {
- "start": "php -S localhost:8080 -t public",
- "test": "phpunit",
- "lint": [
- "phpcbf ./src"
- ]
- }
-}
diff --git a/composer.lock b/composer.lock
deleted file mode 100644
index 1a99022..0000000
--- a/composer.lock
+++ /dev/null
@@ -1,2407 +0,0 @@
-{
- "_readme": [
- "This file locks the dependencies of your project to a known state",
- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
- "This file is @generated automatically"
- ],
- "content-hash": "52c7975a1c8df80709566b48118c2dc5",
- "packages": [
- {
- "name": "container-interop/container-interop",
- "version": "1.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/container-interop/container-interop.git",
- "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8",
- "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8",
- "shasum": ""
- },
- "require": {
- "psr/container": "^1.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Interop\\Container\\": "src/Interop/Container/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
- "homepage": "https://github.com/container-interop/container-interop",
- "time": "2017-02-14T19:40:03+00:00"
- },
- {
- "name": "guzzlehttp/guzzle",
- "version": "6.3.3",
- "source": {
- "type": "git",
- "url": "https://github.com/guzzle/guzzle.git",
- "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
- "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
- "shasum": ""
- },
- "require": {
- "guzzlehttp/promises": "^1.0",
- "guzzlehttp/psr7": "^1.4",
- "php": ">=5.5"
- },
- "require-dev": {
- "ext-curl": "*",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
- "psr/log": "^1.0"
- },
- "suggest": {
- "psr/log": "Required for using the Log middleware"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "6.3-dev"
- }
- },
- "autoload": {
- "files": [
- "src/functions_include.php"
- ],
- "psr-4": {
- "GuzzleHttp\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Michael Dowling",
- "email": "mtdowling at gmail.com",
- "homepage": "https://github.com/mtdowling"
- }
- ],
- "description": "Guzzle is a PHP HTTP client library",
- "homepage": "http://guzzlephp.org/",
- "keywords": [
- "client",
- "curl",
- "framework",
- "http",
- "http client",
- "rest",
- "web service"
- ],
- "time": "2018-04-22T15:46:56+00:00"
- },
- {
- "name": "guzzlehttp/promises",
- "version": "v1.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/guzzle/promises.git",
- "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
- "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
- "shasum": ""
- },
- "require": {
- "php": ">=5.5.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.4-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "GuzzleHttp\\Promise\\": "src/"
- },
- "files": [
- "src/functions_include.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Michael Dowling",
- "email": "mtdowling at gmail.com",
- "homepage": "https://github.com/mtdowling"
- }
- ],
- "description": "Guzzle promises library",
- "keywords": [
- "promise"
- ],
- "time": "2016-12-20T10:07:11+00:00"
- },
- {
- "name": "guzzlehttp/psr7",
- "version": "1.6.1",
- "source": {
- "type": "git",
- "url": "https://github.com/guzzle/psr7.git",
- "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
- "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
- "shasum": ""
- },
- "require": {
- "php": ">=5.4.0",
- "psr/http-message": "~1.0",
- "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
- },
- "provide": {
- "psr/http-message-implementation": "1.0"
- },
- "require-dev": {
- "ext-zlib": "*",
- "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
- },
- "suggest": {
- "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.6-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "GuzzleHttp\\Psr7\\": "src/"
- },
- "files": [
- "src/functions_include.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Michael Dowling",
- "email": "mtdowling at gmail.com",
- "homepage": "https://github.com/mtdowling"
- },
- {
- "name": "Tobias Schultze",
- "homepage": "https://github.com/Tobion"
- }
- ],
- "description": "PSR-7 message implementation that also provides common utility methods",
- "keywords": [
- "http",
- "message",
- "psr-7",
- "request",
- "response",
- "stream",
- "uri",
- "url"
- ],
- "time": "2019-07-01T23:21:34+00:00"
- },
- {
- "name": "monolog/monolog",
- "version": "1.25.1",
- "source": {
- "type": "git",
- "url": "https://github.com/Seldaek/monolog.git",
- "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
- "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0",
- "psr/log": "~1.0"
- },
- "provide": {
- "psr/log-implementation": "1.0.0"
- },
- "require-dev": {
- "aws/aws-sdk-php": "^2.4.9 || ^3.0",
- "doctrine/couchdb": "~1.0 at dev",
- "graylog2/gelf-php": "~1.0",
- "jakub-onderka/php-parallel-lint": "0.9",
- "php-amqplib/php-amqplib": "~2.4",
- "php-console/php-console": "^3.1.3",
- "phpunit/phpunit": "~4.5",
- "phpunit/phpunit-mock-objects": "2.3.0",
- "ruflin/elastica": ">=0.90 <3.0",
- "sentry/sentry": "^0.13",
- "swiftmailer/swiftmailer": "^5.3|^6.0"
- },
- "suggest": {
- "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
- "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
- "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
- "ext-mongo": "Allow sending log messages to a MongoDB server",
- "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
- "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
- "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
- "php-console/php-console": "Allow sending log messages to Google Chrome",
- "rollbar/rollbar": "Allow sending log messages to Rollbar",
- "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
- "sentry/sentry": "Allow sending log messages to a Sentry server"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Monolog\\": "src/Monolog"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jordi Boggiano",
- "email": "j.boggiano at seld.be",
- "homepage": "http://seld.be"
- }
- ],
- "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
- "homepage": "http://github.com/Seldaek/monolog",
- "keywords": [
- "log",
- "logging",
- "psr-3"
- ],
- "time": "2019-09-06T13:49:17+00:00"
- },
- {
- "name": "nikic/fast-route",
- "version": "v1.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/nikic/FastRoute.git",
- "reference": "181d480e08d9476e61381e04a71b34dc0432e812"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
- "reference": "181d480e08d9476e61381e04a71b34dc0432e812",
- "shasum": ""
- },
- "require": {
- "php": ">=5.4.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.8.35|~5.7"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "FastRoute\\": "src/"
- },
- "files": [
- "src/functions.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Nikita Popov",
- "email": "nikic at php.net"
- }
- ],
- "description": "Fast request router for PHP",
- "keywords": [
- "router",
- "routing"
- ],
- "time": "2018-02-13T20:26:39+00:00"
- },
- {
- "name": "nikolaposa/rate-limit",
- "version": "1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/nikolaposa/rate-limit.git",
- "reference": "8c1c6a08b7dff2e87a2335b0dd20f123fece0aa2"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/nikolaposa/rate-limit/zipball/8c1c6a08b7dff2e87a2335b0dd20f123fece0aa2",
- "reference": "8c1c6a08b7dff2e87a2335b0dd20f123fece0aa2",
- "shasum": ""
- },
- "require": {
- "php": "^7.0",
- "psr/http-message": "^1.0"
- },
- "require-dev": {
- "friendsofphp/php-cs-fixer": "^2.0",
- "phpunit/phpunit": "^4.7 | ^5.0",
- "zendframework/zend-diactoros": "^1.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "RateLimit\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nikola Poša",
- "email": "posa.nikola at gmail.com",
- "homepage": "http://www.nikolaposa.in.rs"
- }
- ],
- "description": "Standalone component that facilitates rate-limiting functionality. Also provides a middleware designed for API and/or other application endpoints.",
- "keywords": [
- "middleware",
- "rate limit"
- ],
- "time": "2017-10-25T19:47:36+00:00"
- },
- {
- "name": "pimple/pimple",
- "version": "v3.2.3",
- "source": {
- "type": "git",
- "url": "https://github.com/silexphp/Pimple.git",
- "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32",
- "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0",
- "psr/container": "^1.0"
- },
- "require-dev": {
- "symfony/phpunit-bridge": "^3.2"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.2.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Pimple": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien at symfony.com"
- }
- ],
- "description": "Pimple, a simple Dependency Injection Container",
- "homepage": "http://pimple.sensiolabs.org",
- "keywords": [
- "container",
- "dependency injection"
- ],
- "time": "2018-01-21T07:42:36+00:00"
- },
- {
- "name": "pragmarx/random",
- "version": "v0.2.2",
- "source": {
- "type": "git",
- "url": "https://github.com/antonioribeiro/random.git",
- "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/antonioribeiro/random/zipball/daf08a189c5d2d40d1a827db46364d3a741a51b7",
- "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7",
- "shasum": ""
- },
- "require": {
- "php": ">=7.0"
- },
- "require-dev": {
- "fzaninotto/faker": "~1.7",
- "phpunit/phpunit": "~6.4",
- "pragmarx/trivia": "~0.1",
- "squizlabs/php_codesniffer": "^2.3"
- },
- "suggest": {
- "fzaninotto/faker": "Allows you to get dozens of randomized types",
- "pragmarx/trivia": "For the trivia database"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "PragmaRX\\Random\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Antonio Carlos Ribeiro",
- "email": "acr at antoniocarlosribeiro.com",
- "homepage": "https://antoniocarlosribeiro.com",
- "role": "Developer"
- }
- ],
- "description": "Create random chars, numbers, strings",
- "homepage": "https://github.com/antonioribeiro/random",
- "keywords": [
- "Randomize",
- "faker",
- "pragmarx",
- "random",
- "random number",
- "random pattern",
- "random string"
- ],
- "time": "2017-11-21T05:26:22+00:00"
- },
- {
- "name": "predis/predis",
- "version": "v1.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/nrk/predis.git",
- "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1",
- "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.9"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.8"
- },
- "suggest": {
- "ext-curl": "Allows access to Webdis when paired with phpiredis",
- "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Predis\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Daniele Alessandri",
- "email": "suppakilla at gmail.com",
- "homepage": "http://clorophilla.net"
- }
- ],
- "description": "Flexible and feature-complete Redis client for PHP and HHVM",
- "homepage": "http://github.com/nrk/predis",
- "keywords": [
- "nosql",
- "predis",
- "redis"
- ],
- "time": "2016-06-16T16:22:20+00:00"
- },
- {
- "name": "psr/container",
- "version": "1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/container.git",
- "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
- "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Container\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
- }
- ],
- "description": "Common Container Interface (PHP FIG PSR-11)",
- "homepage": "https://github.com/php-fig/container",
- "keywords": [
- "PSR-11",
- "container",
- "container-interface",
- "container-interop",
- "psr"
- ],
- "time": "2017-02-14T16:28:37+00:00"
- },
- {
- "name": "psr/http-message",
- "version": "1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/http-message.git",
- "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
- "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Http\\Message\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
- }
- ],
- "description": "Common interface for HTTP messages",
- "homepage": "https://github.com/php-fig/http-message",
- "keywords": [
- "http",
- "http-message",
- "psr",
- "psr-7",
- "request",
- "response"
- ],
- "time": "2016-08-06T14:39:51+00:00"
- },
- {
- "name": "psr/log",
- "version": "1.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/log.git",
- "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
- "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Log\\": "Psr/Log/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
- }
- ],
- "description": "Common interface for logging libraries",
- "homepage": "https://github.com/php-fig/log",
- "keywords": [
- "log",
- "psr",
- "psr-3"
- ],
- "time": "2018-11-20T15:27:04+00:00"
- },
- {
- "name": "ralouphie/getallheaders",
- "version": "3.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/ralouphie/getallheaders.git",
- "reference": "120b605dfeb996808c31b6477290a714d356e822"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
- "reference": "120b605dfeb996808c31b6477290a714d356e822",
- "shasum": ""
- },
- "require": {
- "php": ">=5.6"
- },
- "require-dev": {
- "php-coveralls/php-coveralls": "^2.1",
- "phpunit/phpunit": "^5 || ^6.5"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/getallheaders.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Ralph Khattar",
- "email": "ralph.khattar at gmail.com"
- }
- ],
- "description": "A polyfill for getallheaders.",
- "time": "2019-03-08T08:55:37+00:00"
- },
- {
- "name": "slim/php-view",
- "version": "2.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/slimphp/PHP-View.git",
- "reference": "a13ada9d7962ca1b48799c0d9ffbca4c33245aed"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/slimphp/PHP-View/zipball/a13ada9d7962ca1b48799c0d9ffbca4c33245aed",
- "reference": "a13ada9d7962ca1b48799c0d9ffbca4c33245aed",
- "shasum": ""
- },
- "require": {
- "psr/http-message": "^1.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.8",
- "slim/slim": "^3.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Slim\\Views\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Glenn Eggleton",
- "email": "geggleto at gmail.com"
- }
- ],
- "description": "Render PHP view scripts into a PSR-7 Response object.",
- "keywords": [
- "framework",
- "php",
- "phtml",
- "renderer",
- "slim",
- "template",
- "view"
- ],
- "time": "2019-04-15T20:43:28+00:00"
- },
- {
- "name": "slim/slim",
- "version": "3.12.2",
- "source": {
- "type": "git",
- "url": "https://github.com/slimphp/Slim.git",
- "reference": "200c6143f15baa477601879b64ab2326847aac0b"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/slimphp/Slim/zipball/200c6143f15baa477601879b64ab2326847aac0b",
- "reference": "200c6143f15baa477601879b64ab2326847aac0b",
- "shasum": ""
- },
- "require": {
- "container-interop/container-interop": "^1.2",
- "ext-json": "*",
- "ext-libxml": "*",
- "ext-simplexml": "*",
- "nikic/fast-route": "^1.0",
- "php": ">=5.5.0",
- "pimple/pimple": "^3.0",
- "psr/container": "^1.0",
- "psr/http-message": "^1.0"
- },
- "provide": {
- "psr/http-message-implementation": "1.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.0",
- "squizlabs/php_codesniffer": "^2.5"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Slim\\": "Slim"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Josh Lockhart",
- "email": "hello at joshlockhart.com",
- "homepage": "https://joshlockhart.com"
- },
- {
- "name": "Andrew Smith",
- "email": "a.smith at silentworks.co.uk",
- "homepage": "http://silentworks.co.uk"
- },
- {
- "name": "Rob Allen",
- "email": "rob at akrabat.com",
- "homepage": "http://akrabat.com"
- },
- {
- "name": "Gabriel Manricks",
- "email": "gmanricks at me.com",
- "homepage": "http://gabrielmanricks.com"
- }
- ],
- "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
- "homepage": "https://slimframework.com",
- "keywords": [
- "api",
- "framework",
- "micro",
- "router"
- ],
- "time": "2019-08-20T18:46:05+00:00"
- }
- ],
- "packages-dev": [
- {
- "name": "doctrine/instantiator",
- "version": "1.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/instantiator.git",
- "reference": "a2c590166b2133a4633738648b6b064edae0814a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
- "reference": "a2c590166b2133a4633738648b6b064edae0814a",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "require-dev": {
- "doctrine/coding-standard": "^6.0",
- "ext-pdo": "*",
- "ext-phar": "*",
- "phpbench/phpbench": "^0.13",
- "phpstan/phpstan-phpunit": "^0.11",
- "phpstan/phpstan-shim": "^0.11",
- "phpunit/phpunit": "^7.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Marco Pivetta",
- "email": "ocramius at gmail.com",
- "homepage": "http://ocramius.github.com/"
- }
- ],
- "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
- "keywords": [
- "constructor",
- "instantiate"
- ],
- "time": "2019-03-17T17:37:11+00:00"
- },
- {
- "name": "myclabs/deep-copy",
- "version": "1.9.3",
- "source": {
- "type": "git",
- "url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
- "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "replace": {
- "myclabs/deep-copy": "self.version"
- },
- "require-dev": {
- "doctrine/collections": "^1.0",
- "doctrine/common": "^2.6",
- "phpunit/phpunit": "^7.1"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
- },
- "files": [
- "src/DeepCopy/deep_copy.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "Create deep copies (clones) of your objects",
- "keywords": [
- "clone",
- "copy",
- "duplicate",
- "object",
- "object graph"
- ],
- "time": "2019-08-09T12:45:53+00:00"
- },
- {
- "name": "phar-io/manifest",
- "version": "1.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/phar-io/manifest.git",
- "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
- "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
- "shasum": ""
- },
- "require": {
- "ext-dom": "*",
- "ext-phar": "*",
- "phar-io/version": "^2.0",
- "php": "^5.6 || ^7.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Arne Blankerts",
- "email": "arne at blankerts.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Heuer",
- "email": "sebastian at phpeople.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "Developer"
- }
- ],
- "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
- "time": "2018-07-08T19:23:20+00:00"
- },
- {
- "name": "phar-io/version",
- "version": "2.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/phar-io/version.git",
- "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
- "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
- "shasum": ""
- },
- "require": {
- "php": "^5.6 || ^7.0"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Arne Blankerts",
- "email": "arne at blankerts.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Heuer",
- "email": "sebastian at phpeople.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "Developer"
- }
- ],
- "description": "Library for handling version information and constraints",
- "time": "2018-07-08T19:19:57+00:00"
- },
- {
- "name": "phpdocumentor/reflection-common",
- "version": "2.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
- "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "~6"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jaap van Otterdijk",
- "email": "opensource at ijaap.nl"
- }
- ],
- "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
- "homepage": "http://www.phpdoc.org",
- "keywords": [
- "FQSEN",
- "phpDocumentor",
- "phpdoc",
- "reflection",
- "static analysis"
- ],
- "time": "2018-08-07T13:53:10+00:00"
- },
- {
- "name": "phpdocumentor/reflection-docblock",
- "version": "4.3.2",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
- "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
- "shasum": ""
- },
- "require": {
- "php": "^7.0",
- "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
- "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
- "webmozart/assert": "^1.0"
- },
- "require-dev": {
- "doctrine/instantiator": "^1.0.5",
- "mockery/mockery": "^1.0",
- "phpunit/phpunit": "^6.4"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src/"
- ]
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mike van Riel",
- "email": "me at mikevanriel.com"
- }
- ],
- "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "time": "2019-09-12T14:27:41+00:00"
- },
- {
- "name": "phpdocumentor/type-resolver",
- "version": "1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
- "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
- "shasum": ""
- },
- "require": {
- "php": "^7.1",
- "phpdocumentor/reflection-common": "^2.0"
- },
- "require-dev": {
- "ext-tokenizer": "^7.1",
- "mockery/mockery": "~1",
- "phpunit/phpunit": "^7.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mike van Riel",
- "email": "me at mikevanriel.com"
- }
- ],
- "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
- "time": "2019-08-22T18:11:29+00:00"
- },
- {
- "name": "phpspec/prophecy",
- "version": "1.9.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpspec/prophecy.git",
- "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203",
- "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203",
- "shasum": ""
- },
- "require": {
- "doctrine/instantiator": "^1.0.2",
- "php": "^5.3|^7.0",
- "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
- "sebastian/comparator": "^1.1|^2.0|^3.0",
- "sebastian/recursion-context": "^1.0|^2.0|^3.0"
- },
- "require-dev": {
- "phpspec/phpspec": "^2.5|^3.2",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.8.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Prophecy\\": "src/Prophecy"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Konstantin Kudryashov",
- "email": "ever.zet at gmail.com",
- "homepage": "http://everzet.com"
- },
- {
- "name": "Marcello Duarte",
- "email": "marcello.duarte at gmail.com"
- }
- ],
- "description": "Highly opinionated mocking framework for PHP 5.3+",
- "homepage": "https://github.com/phpspec/prophecy",
- "keywords": [
- "Double",
- "Dummy",
- "fake",
- "mock",
- "spy",
- "stub"
- ],
- "time": "2019-10-03T11:07:50+00:00"
- },
- {
- "name": "phpunit/php-code-coverage",
- "version": "6.1.4",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
- "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
- "shasum": ""
- },
- "require": {
- "ext-dom": "*",
- "ext-xmlwriter": "*",
- "php": "^7.1",
- "phpunit/php-file-iterator": "^2.0",
- "phpunit/php-text-template": "^1.2.1",
- "phpunit/php-token-stream": "^3.0",
- "sebastian/code-unit-reverse-lookup": "^1.0.1",
- "sebastian/environment": "^3.1 || ^4.0",
- "sebastian/version": "^2.0.1",
- "theseer/tokenizer": "^1.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.0"
- },
- "suggest": {
- "ext-xdebug": "^2.6.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "6.1-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "lead"
- }
- ],
- "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
- "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
- "keywords": [
- "coverage",
- "testing",
- "xunit"
- ],
- "time": "2018-10-31T16:06:48+00:00"
- },
- {
- "name": "phpunit/php-file-iterator",
- "version": "2.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "050bedf145a257b1ff02746c31894800e5122946"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
- "reference": "050bedf145a257b1ff02746c31894800e5122946",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "lead"
- }
- ],
- "description": "FilterIterator implementation that filters files based on a list of suffixes.",
- "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
- "keywords": [
- "filesystem",
- "iterator"
- ],
- "time": "2018-09-13T20:33:42+00:00"
- },
- {
- "name": "phpunit/php-text-template",
- "version": "1.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "lead"
- }
- ],
- "description": "Simple template engine.",
- "homepage": "https://github.com/sebastianbergmann/php-text-template/",
- "keywords": [
- "template"
- ],
- "time": "2015-06-21T13:50:34+00:00"
- },
- {
- "name": "phpunit/php-timer",
- "version": "2.1.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
- "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.1-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "lead"
- }
- ],
- "description": "Utility class for timing",
- "homepage": "https://github.com/sebastianbergmann/php-timer/",
- "keywords": [
- "timer"
- ],
- "time": "2019-06-07T04:22:29+00:00"
- },
- {
- "name": "phpunit/php-token-stream",
- "version": "3.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
- "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
- "shasum": ""
- },
- "require": {
- "ext-tokenizer": "*",
- "php": "^7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.1-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Wrapper around PHP's tokenizer extension.",
- "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
- "keywords": [
- "tokenizer"
- ],
- "time": "2019-09-17T06:23:10+00:00"
- },
- {
- "name": "phpunit/phpunit",
- "version": "7.5.16",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "316afa6888d2562e04aeb67ea7f2017a0eb41661"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/316afa6888d2562e04aeb67ea7f2017a0eb41661",
- "reference": "316afa6888d2562e04aeb67ea7f2017a0eb41661",
- "shasum": ""
- },
- "require": {
- "doctrine/instantiator": "^1.1",
- "ext-dom": "*",
- "ext-json": "*",
- "ext-libxml": "*",
- "ext-mbstring": "*",
- "ext-xml": "*",
- "myclabs/deep-copy": "^1.7",
- "phar-io/manifest": "^1.0.2",
- "phar-io/version": "^2.0",
- "php": "^7.1",
- "phpspec/prophecy": "^1.7",
- "phpunit/php-code-coverage": "^6.0.7",
- "phpunit/php-file-iterator": "^2.0.1",
- "phpunit/php-text-template": "^1.2.1",
- "phpunit/php-timer": "^2.1",
- "sebastian/comparator": "^3.0",
- "sebastian/diff": "^3.0",
- "sebastian/environment": "^4.0",
- "sebastian/exporter": "^3.1",
- "sebastian/global-state": "^2.0",
- "sebastian/object-enumerator": "^3.0.3",
- "sebastian/resource-operations": "^2.0",
- "sebastian/version": "^2.0.1"
- },
- "conflict": {
- "phpunit/phpunit-mock-objects": "*"
- },
- "require-dev": {
- "ext-pdo": "*"
- },
- "suggest": {
- "ext-soap": "*",
- "ext-xdebug": "*",
- "phpunit/php-invoker": "^2.0"
- },
- "bin": [
- "phpunit"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "7.5-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "lead"
- }
- ],
- "description": "The PHP Unit Testing framework.",
- "homepage": "https://phpunit.de/",
- "keywords": [
- "phpunit",
- "testing",
- "xunit"
- ],
- "time": "2019-09-14T09:08:39+00:00"
- },
- {
- "name": "sebastian/code-unit-reverse-lookup",
- "version": "1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
- "shasum": ""
- },
- "require": {
- "php": "^5.6 || ^7.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^5.7 || ^6.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Looks up which function or method a line of code belongs to",
- "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
- "time": "2017-03-04T06:30:41+00:00"
- },
- {
- "name": "sebastian/comparator",
- "version": "3.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
- "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
- "shasum": ""
- },
- "require": {
- "php": "^7.1",
- "sebastian/diff": "^3.0",
- "sebastian/exporter": "^3.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Jeff Welch",
- "email": "whatthejeff at gmail.com"
- },
- {
- "name": "Volker Dusch",
- "email": "github at wallbash.com"
- },
- {
- "name": "Bernhard Schussek",
- "email": "bschussek at 2bepublished.at"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Provides the functionality to compare PHP values for equality",
- "homepage": "https://github.com/sebastianbergmann/comparator",
- "keywords": [
- "comparator",
- "compare",
- "equality"
- ],
- "time": "2018-07-12T15:12:46+00:00"
- },
- {
- "name": "sebastian/diff",
- "version": "3.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
- "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.5 || ^8.0",
- "symfony/process": "^2 || ^3.3 || ^4"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Kore Nordmann",
- "email": "mail at kore-nordmann.de"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Diff implementation",
- "homepage": "https://github.com/sebastianbergmann/diff",
- "keywords": [
- "diff",
- "udiff",
- "unidiff",
- "unified diff"
- ],
- "time": "2019-02-04T06:01:07+00:00"
- },
- {
- "name": "sebastian/environment",
- "version": "4.2.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
- "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.5"
- },
- "suggest": {
- "ext-posix": "*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.2-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Provides functionality to handle HHVM/PHP environments",
- "homepage": "http://www.github.com/sebastianbergmann/environment",
- "keywords": [
- "Xdebug",
- "environment",
- "hhvm"
- ],
- "time": "2019-05-05T09:05:15+00:00"
- },
- {
- "name": "sebastian/exporter",
- "version": "3.1.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
- "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
- "shasum": ""
- },
- "require": {
- "php": "^7.0",
- "sebastian/recursion-context": "^3.0"
- },
- "require-dev": {
- "ext-mbstring": "*",
- "phpunit/phpunit": "^6.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.1.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- },
- {
- "name": "Jeff Welch",
- "email": "whatthejeff at gmail.com"
- },
- {
- "name": "Volker Dusch",
- "email": "github at wallbash.com"
- },
- {
- "name": "Adam Harvey",
- "email": "aharvey at php.net"
- },
- {
- "name": "Bernhard Schussek",
- "email": "bschussek at gmail.com"
- }
- ],
- "description": "Provides the functionality to export PHP variables for visualization",
- "homepage": "http://www.github.com/sebastianbergmann/exporter",
- "keywords": [
- "export",
- "exporter"
- ],
- "time": "2019-09-14T09:02:43+00:00"
- },
- {
- "name": "sebastian/global-state",
- "version": "2.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
- "shasum": ""
- },
- "require": {
- "php": "^7.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^6.0"
- },
- "suggest": {
- "ext-uopz": "*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Snapshotting of global state",
- "homepage": "http://www.github.com/sebastianbergmann/global-state",
- "keywords": [
- "global state"
- ],
- "time": "2017-04-27T15:39:26+00:00"
- },
- {
- "name": "sebastian/object-enumerator",
- "version": "3.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
- "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
- "shasum": ""
- },
- "require": {
- "php": "^7.0",
- "sebastian/object-reflector": "^1.1.1",
- "sebastian/recursion-context": "^3.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^6.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Traverses array structures and object graphs to enumerate all referenced objects",
- "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
- "time": "2017-08-03T12:35:26+00:00"
- },
- {
- "name": "sebastian/object-reflector",
- "version": "1.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/object-reflector.git",
- "reference": "773f97c67f28de00d397be301821b06708fca0be"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
- "reference": "773f97c67f28de00d397be301821b06708fca0be",
- "shasum": ""
- },
- "require": {
- "php": "^7.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^6.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.1-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Allows reflection of object attributes, including inherited and non-public ones",
- "homepage": "https://github.com/sebastianbergmann/object-reflector/",
- "time": "2017-03-29T09:07:27+00:00"
- },
- {
- "name": "sebastian/recursion-context",
- "version": "3.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
- "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
- "shasum": ""
- },
- "require": {
- "php": "^7.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^6.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Jeff Welch",
- "email": "whatthejeff at gmail.com"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- },
- {
- "name": "Adam Harvey",
- "email": "aharvey at php.net"
- }
- ],
- "description": "Provides functionality to recursively process PHP variables",
- "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
- "time": "2017-03-03T06:23:57+00:00"
- },
- {
- "name": "sebastian/resource-operations",
- "version": "2.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
- "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
- "shasum": ""
- },
- "require": {
- "php": "^7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de"
- }
- ],
- "description": "Provides a list of PHP built-in functions that operate on resources",
- "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2018-10-04T04:07:39+00:00"
- },
- {
- "name": "sebastian/version",
- "version": "2.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/version.git",
- "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
- "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
- "shasum": ""
- },
- "require": {
- "php": ">=5.6"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian at phpunit.de",
- "role": "lead"
- }
- ],
- "description": "Library that helps with managing the version number of Git-hosted PHP projects",
- "homepage": "https://github.com/sebastianbergmann/version",
- "time": "2016-10-03T07:35:21+00:00"
- },
- {
- "name": "squizlabs/php_codesniffer",
- "version": "3.5.1",
- "source": {
- "type": "git",
- "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "82cd0f854ceca17731d6d019c7098e3755c45060"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/82cd0f854ceca17731d6d019c7098e3755c45060",
- "reference": "82cd0f854ceca17731d6d019c7098e3755c45060",
- "shasum": ""
- },
- "require": {
- "ext-simplexml": "*",
- "ext-tokenizer": "*",
- "ext-xmlwriter": "*",
- "php": ">=5.4.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
- },
- "bin": [
- "bin/phpcs",
- "bin/phpcbf"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.x-dev"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Greg Sherwood",
- "role": "lead"
- }
- ],
- "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
- "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
- "keywords": [
- "phpcs",
- "standards"
- ],
- "time": "2019-10-16T21:14:26+00:00"
- },
- {
- "name": "symfony/polyfill-ctype",
- "version": "v1.12.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
- "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "suggest": {
- "ext-ctype": "For best performance"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.12-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Ctype\\": ""
- },
- "files": [
- "bootstrap.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Gert de Pagter",
- "email": "BackEndTea at gmail.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill for ctype functions",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "ctype",
- "polyfill",
- "portable"
- ],
- "time": "2019-08-06T08:03:45+00:00"
- },
- {
- "name": "theseer/tokenizer",
- "version": "1.1.3",
- "source": {
- "type": "git",
- "url": "https://github.com/theseer/tokenizer.git",
- "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
- "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
- "shasum": ""
- },
- "require": {
- "ext-dom": "*",
- "ext-tokenizer": "*",
- "ext-xmlwriter": "*",
- "php": "^7.0"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Arne Blankerts",
- "email": "arne at blankerts.de",
- "role": "Developer"
- }
- ],
- "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
- "time": "2019-06-13T22:48:21+00:00"
- },
- {
- "name": "webmozart/assert",
- "version": "1.5.0",
- "source": {
- "type": "git",
- "url": "https://github.com/webmozart/assert.git",
- "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
- "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
- "shasum": ""
- },
- "require": {
- "php": "^5.3.3 || ^7.0",
- "symfony/polyfill-ctype": "^1.8"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.8.36 || ^7.5.13"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.3-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Webmozart\\Assert\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Bernhard Schussek",
- "email": "bschussek at gmail.com"
- }
- ],
- "description": "Assertions to validate method input/output with nice error messages.",
- "keywords": [
- "assert",
- "check",
- "validate"
- ],
- "time": "2019-08-24T08:43:50+00:00"
- }
- ],
- "aliases": [],
- "minimum-stability": "stable",
- "stability-flags": [],
- "prefer-stable": false,
- "prefer-lowest": false,
- "platform": {
- "php": ">=5.6"
- },
- "platform-dev": []
-}
diff --git a/docker-compose.yml b/docker-compose.yml
index e77790d..574361b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,18 +1,16 @@
-version: '2'
-
-volumes:
- logs:
- driver: local
-
+version: "3.9"
services:
- slim:
- image: php:7-alpine
- working_dir: /var/www
- command: php -S 0.0.0.0:8080 -t public
- environment:
- docker: "true"
- ports:
- - 8080:8080
- volumes:
- - .:/var/www
- - logs:/var/www/logs
+ server:
+ build: .
+ environment:
+ - REDIS_HOST=redis
+ ports:
+ - "9120:9120/udp"
+ web:
+ build: ./web/
+ environment:
+ - "REDIS_URL=redis://redis:6379"
+ ports:
+ - "80:80"
+ redis:
+ image: "redis:latest"
\ No newline at end of file
diff --git a/logs/README.md b/logs/README.md
deleted file mode 100644
index d4a602e..0000000
--- a/logs/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Your Slim Framework application's log files will be written to this directory.
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..d3cda69
--- /dev/null
+++ b/main.py
@@ -0,0 +1,182 @@
+import json
+import logging
+import os
+import signal
+
+import enet
+import redis as redis_package
+
+# Games to accept:
+GAMES = [
+ "moonbase" # Moonbase Commander (v1.0/v1.1/Demo)
+]
+
+# Version variants to accept for a specific game.
+# If none exist but game exist in the GAMES list,
+# that means there's only one game version.
+VERSIONS = {
+ "moonbase": ["1.0", "1.1", "Demo"]
+}
+
+def get_full_game_names():
+ games = []
+ for game in GAMES:
+ versions = VERSIONS.get(game)
+ if versions:
+ for version in versions:
+ games.append(f"{game}:{version}")
+ else:
+ games.append(game)
+ return games
+
+if os.environ.get("DEBUG"):
+ logging.basicConfig(level=logging.DEBUG)
+
+redis = redis_package.Redis(os.environ.get("REDIS_HOST", "127.0.0.1"),
+ retry_on_timeout=True, decode_responses=True)
+for game in get_full_game_names():
+ # Reset session counter
+ redis.set(f"{game}:counter", 0)
+ if redis.exists(f"{game}:sessions"):
+ # Clear out the sessions
+ logging.info(f"Clearing out {game} sessions")
+ for session_id in range(redis.llen(f"{game}:sessions")):
+ redis.delete(f"{game}.session:{session_id}")
+ redis.delete(f"{game}:sessions")
+ redis.delete(f"{game}:sessionByAddress")
+
+# Create our host to listen connections from. 4095 is the maxinum amount.
+host = enet.Host(enet.Address(b"0.0.0.0", 9120), peerCount=4095, channelLimit=1)
+print("Listening for messages in port 9120", flush=True)
+
+def send(peer, data):
+ logging.debug(f"{peer.address}: OUT: {data}")
+ data = json.dumps(data).encode()
+ peer.send(0, enet.Packet(data, enet.PACKET_FLAG_RELIABLE))
+
+def get_peer_by_address(address):
+ for peer in host.peers:
+ if str(peer.address) == address:
+ return peer
+ return None
+
+def get_session_by_address(game, address):
+ session_id = redis.hget(f"{game}:sessionByAddress", str(address))
+ if session_id:
+ return redis.hgetall(f"{game}:session:{session_id}")
+ return None
+
+def create_session(name, address):
+ # Get our new session ID
+ session_id = redis.incr(f"{game}:counter")
+ # Create and store our new session
+ redis.hset(f"{game}:session:{session_id}",
+ mapping={"name": name, "players": 0, "address": str(event.peer.address)})
+ # Add session to sessions list
+ redis.rpush(f"{game}:sessions", session_id)
+
+ # Store to address to session hash
+ redis.hset(f"{game}:sessionByAddress", str(address), session_id)
+
+ logging.debug(f"{address}: NEW SESSION: \"{name}\"")
+ return session_id
+
+do_loop = True
+def exit(*args):
+ global do_loop
+ do_loop = False
+
+# For Docker, they grace stop with SIGTERM
+signal.signal(signal.SIGTERM, exit)
+# SIGINT: Ctrl+C KeyboardInterrupt
+signal.signal(signal.SIGINT, exit)
+
+while do_loop:
+ # Main event loop
+ event = host.service(1000)
+ if event.type == enet.EVENT_TYPE_CONNECT:
+ logging.debug(f"{event.peer.address}: CONNECT")
+ elif event.type == enet.EVENT_TYPE_DISCONNECT:
+ logging.debug(f"{event.peer.address}: DISCONNECT")
+ # Close out sessions relating to the address
+ for game in get_full_game_names():
+ session_id = redis.hget(f"{game}:sessionByAddress", str(event.peer.address))
+ if session_id:
+ redis.delete(f"{game}:session:{session_id}")
+ redis.lrem(f"{game}:sessions", 0, session_id)
+ redis.hdel(f"{game}:sessionByAddress", str(event.peer.address))
+
+ elif event.type == enet.EVENT_TYPE_RECEIVE:
+ logging.debug(f"{event.peer.address}: IN: {event.packet.data}")
+ # TODO: Relays
+ try:
+ data = json.loads(event.packet.data)
+ except:
+ logging.warning(f"{event.peer.address}: Received non-JSON data.")
+ continue
+ command = data.get("cmd")
+ game = data.get("game")
+ version = data.get("version")
+ if not command:
+ logging.warning(f"{event.peer.address}: Command missing")
+ continue
+ if not game:
+ logging.warning(f"{event.peer.address}: Game missing")
+ continue
+
+ if game not in GAMES:
+ logging.warning(f"Game \"{game}\" not supported.")
+ continue
+
+ versions = VERSIONS.get(game)
+ if versions:
+ if not version:
+ logging.warning(f"{event.peer.address}: Version missing")
+ continue
+
+ if version not in version:
+ logging.warning(f"Game \"{game}\" with version \"{version}\" not supported.")
+ continue
+
+ # Update the game to contain the version
+ game = f"{game}:{version}"
+
+ if command == "host_session":
+ name = data.get("name")
+
+ session_id = create_session(name, event.peer.address)
+ send(event.peer, {"cmd": "host_session_resp", "id": session_id})
+
+ elif command == "update_players":
+ players = data.get("players")
+
+ session_id = redis.hget(f"{game}:sessionByAddress", str(event.peer.address))
+ if session_id:
+ redis.hset(f"{game}:session:{session_id}", "players", players)
+
+ elif command == "get_sessions":
+ sessions = []
+
+ num_sessions = redis.llen(f"{game}:sessions")
+ session_ids = redis.lrange(f"{game}:sessions", 0, num_sessions)
+ for id in session_ids:
+ session = redis.hgetall(f"{game}:session:{id}")
+ sessions.append({"id": int(id), "name": session["name"],
+ "players": int(session["players"]), "address": str(session["address"])})
+
+ send(event.peer, {"cmd": "get_sessions_resp",
+ "address": str(event.peer.address), "sessions": sessions})
+ elif command == "join_session":
+ session_id = data.get("id")
+
+ if not (redis.exists(f"{game}:session:{id}")):
+ logging.warn(f"Session {game}:{session_id} not found")
+ continue
+
+ address = redis.hget(f"{game}:session:{id}", "address")
+ peer = get_peer_by_address(address)
+ if not peer:
+ continue
+
+ # Send the joiner's address to the hoster for hole-punching
+ send(peer, {"cmd": "joining_session", "address": str(event.peer.address)})
diff --git a/phpunit.xml b/phpunit.xml
deleted file mode 100644
index c1441fd..0000000
--- a/phpunit.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<phpunit bootstrap="vendor/autoload.php">
- <testsuites>
- <testsuite name="SlimSkeleton">
- <directory>tests</directory>
- </testsuite>
- </testsuites>
-</phpunit>
\ No newline at end of file
diff --git a/public/.htaccess b/public/.htaccess
deleted file mode 100644
index f5d1969..0000000
--- a/public/.htaccess
+++ /dev/null
@@ -1,21 +0,0 @@
-<IfModule mod_rewrite.c>
- RewriteEngine On
-
- # Some hosts may require you to use the `RewriteBase` directive.
- # Determine the RewriteBase automatically and set it as environment variable.
- # If you are using Apache aliases to do mass virtual hosting or installed the
- # project in a subdirectory, the base path will be prepended to allow proper
- # resolution of the index.php file and to redirect to the correct URI. It will
- # work in environments without path prefix as well, providing a safe, one-size
- # fits all solution. But as you do not need it in this case, you can comment
- # the following 2 lines to eliminate the overhead.
- RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
- RewriteRule ^(.*) - [E=BASE:%1]
-
- # If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the
- # absolute physical path to the directory that contains this htaccess file.
- # RewriteBase /
-
- RewriteCond %{REQUEST_FILENAME} !-f
- RewriteRule ^ index.php [QSA,L]
-</IfModule>
diff --git a/public/images/box.png b/public/images/box.png
deleted file mode 100644
index a7c73ca..0000000
Binary files a/public/images/box.png and /dev/null differ
diff --git a/public/images/dropbox.png b/public/images/dropbox.png
deleted file mode 100644
index f78401e..0000000
Binary files a/public/images/dropbox.png and /dev/null differ
diff --git a/public/images/google_drive.png b/public/images/google_drive.png
deleted file mode 100644
index c91d8f1..0000000
Binary files a/public/images/google_drive.png and /dev/null differ
diff --git a/public/images/onedrive.png b/public/images/onedrive.png
deleted file mode 100644
index 6b29f14..0000000
Binary files a/public/images/onedrive.png and /dev/null differ
diff --git a/public/index.php b/public/index.php
deleted file mode 100644
index 4b56727..0000000
--- a/public/index.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-if (PHP_SAPI == 'cli-server') {
- // To help the built-in PHP dev server, check if the request was actually for
- // something which should probably be served as a static file
- $url = parse_url($_SERVER['REQUEST_URI']);
- $file = __DIR__ . $url['path'];
- if (is_file($file)) {
- return false;
- }
-}
-
-require __DIR__ . '/../vendor/autoload.php';
-
-session_start();
-
-// Instantiate the app
-$settings = include __DIR__ . '/../src/settings.php';
-$app = new \Slim\App($settings);
-
-// Set up dependencies
-$dependencies = include __DIR__ . '/../src/dependencies.php';
-$dependencies($app);
-
-// Register middleware
-$middleware = include __DIR__ . '/../src/middleware.php';
-$middleware($app);
-
-// Register routes
-$routes = include __DIR__ . '/../src/routes.php';
-$routes($app);
-
-// Run app
-$app->run();
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4b09e90
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+pyenet==1.3.14
+redis==4.3.4
\ No newline at end of file
diff --git a/src/dependencies.php b/src/dependencies.php
deleted file mode 100644
index 8e2692e..0000000
--- a/src/dependencies.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-use Slim\App;
-
-return function (App $app) {
- $container = $app->getContainer();
-
- // view renderer
- $container['renderer'] = function ($c) {
- $settings = $c->get('settings')['renderer'];
- return new \Slim\Views\PhpRenderer($settings['template_path']);
- };
-
- // monolog
- $container['logger'] = function ($c) {
- $settings = $c->get('settings')['logger'];
- $logger = new \Monolog\Logger($settings['name']);
- $logger->pushProcessor(new \Monolog\Processor\UidProcessor());
- $logger->pushHandler(new \Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
- return $logger;
- };
-};
diff --git a/src/middleware.php b/src/middleware.php
deleted file mode 100644
index 53984bb..0000000
--- a/src/middleware.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-use Slim\App;
-
-return function (App $app) {
- // e.g: $app->add(new \Slim\Csrf\Guard);
-
- $app->add(
- \RateLimit\Middleware\RateLimitMiddleware::createDefault(
- \RateLimit\RateLimiterFactory::createRedisBackedRateLimiter(
- [
- 'host' => 'localhost',
- 'port' => 6379,
- ], 500, 60
- ),
- [
- 'limitExceededHandler' => function ($request, $response) {
- return $response->withJson(
- [
- 'message' => 'API rate limit exceeded',
- ], 429
- );
- },
- ]
- )
- );
-};
diff --git a/src/routes.php b/src/routes.php
deleted file mode 100644
index ce724c6..0000000
--- a/src/routes.php
+++ /dev/null
@@ -1,399 +0,0 @@
-<?php
-
-use Slim\App;
-use Slim\Http\Request;
-use Slim\Http\Response;
-
-define("KEYPREFIX", "moonbase");
-define("NET_SEND_TYPE_INDIVIDUAL", 1);
-define("NET_SEND_TYPE_GROUP", 2);
-define("NET_SEND_TYPE_HOST", 3);
-define("NET_SEND_TYPE_ALL", 4);
-
-function redisConnect(array $redisOptions = []) {
- $redisOptions = array_merge([
- 'host' => '127.0.0.1',
- 'port' => 6379,
- 'timeout' => 0.0,
- ], $redisOptions);
-
- $redis = new Redis();
-
- $redis->connect($redisOptions['host'], $redisOptions['port'], $redisOptions['timeout']);
-
- return $redis;
-}
-
-function getUserIpAddr() {
- if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
- //ip from share internet
- $ip = $_SERVER['HTTP_CLIENT_IP'];
- } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
- //ip pass from proxy
- $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
- } else {
- $ip = $_SERVER['REMOTE_ADDR'];
- }
- return $ip;
-}
-
-return function (App $app) {
- $container = $app->getContainer();
-
- $app->get(
- '/', function (Request $request, Response $response, array $args) use ($container) {
- // Sample log message
- $container->get('logger')->info("Slim-Skeleton '/' route");
-
- // Render index view
- return $container->get('renderer')->render($response, 'index.phtml', $args);
- }
- );
-
- $app->post(
- '/moonbase/createsession', function (Request $request, Response $response, array $args) use ($container) {
- // Sample log message
- $container->get('logger')->info("Slim-Skeleton '/moonbase/createsession' route");
-
- $parsedBody = $request->getParsedBody();
-
- if (array_key_exists('name', $parsedBody)) {
- $container->get('logger')->info("Got $parsedBody[name]");
- }
-
- $ip = getUserIpAddr();
-
- $redis = redisConnect();
-
- $keys = $redis->keys(KEYPREFIX.";sessions;$ip;*");
-
- if (!sizeof($keys)) {
- $sessionid = rand();
-
- $redis->setEx(KEYPREFIX.";sessions;$ip;$sessionid", 3600, "{\"sessionid\": $sessionid, \"name\": \"$parsedBody[name]\"}");
-
- $container->get('logger')->info("Generated session $sessionid");
- } else {
- $session = $redis->get($keys[0]);
-
- $par = json_decode($session);
-
- $sessionid = $par->sessionid;
-
- $container->get('logger')->info("Retrieved session $sessionid");
- }
-
- return $response->withJson(["sessionid" => $sessionid]);
- }
- );
-
- $app->post(
- '/moonbase/disablesession', function (Request $request, Response $response, array $args) use ($container) {
- // Sample log message
- $container->get('logger')->info("Slim-Skeleton '/moonbase/disablesession' route");
-
- $parsedBody = $request->getParsedBody();
-
- if (!array_key_exists('sessionid', $parsedBody)) {
- return $response->withJson(["error" => "No sessionid specified"]);
- }
-
- $sessionid = $parsedBody['sessionid'];
-
- $redis = redisConnect();
-
- $keys = $redis->keys(KEYPREFIX.";sessions;*;$sessionid");
-
- if (!sizeof($keys)) {
- return $response->withJson(["error" => "Unknown sessionid $sessionid"]);
- }
-
- $sessionkey = $keys[0];
-
- $session = json_decode($redis->get($sessionkey));
-
- $session->disabled = 1;
-
- $redis->setEx($sessionkey, 3600, json_encode($session));
-
- return $response->withJson([]);
- }
- );
-
- $app->post(
- '/moonbase/endsession', function (Request $request, Response $response, array $args) use ($container) {
- // Sample log message
- $container->get('logger')->info("Slim-Skeleton '/moonbase/endsession' route");
-
- $parsedBody = $request->getParsedBody();
-
- if (!array_key_exists('sessionid', $parsedBody)) {
- return $response->withJson(["error" => "No sessionid specified"]);
- }
-
- $sessionid = $parsedBody['sessionid'];
- $userid = $parsedBody['userid'] ?? 0;
-
- $redis = redisConnect();
-
- $keys = $redis->keys(KEYPREFIX.";sessions;*;$sessionid");
-
- if (!sizeof($keys)) {
- return $response->withJson(["error" => "Unknown sessionid $sessionid"]);
- }
-
- $sessionkey = $keys[0];
-
- $session = json_decode($redis->get($sessionkey));
-
- if ($session->host != $userid) {
- return $response->withJson(["error" => "Unauthorized user $userid"]);
- }
-
- $redis->unlink($keys);
-
- return $response->withJson([]);
- }
- );
-
- $app->post(
- '/moonbase/adduser', function (Request $request, Response $response, array $args) use ($container) {
- // Sample log message
- $container->get('logger')->info("Slim-Skeleton '/moonbase/adduser' route");
-
- $parsedBody = $request->getParsedBody();
-
- if (!array_key_exists('sessionid', $parsedBody)) {
- return $response->withJson(["error" => "No sessionid specified"]);
- }
-
- $sessionid = $parsedBody['sessionid'];
-
- $redis = redisConnect();
-
- $keys = $redis->keys(KEYPREFIX.";sessions;*;$sessionid");
-
- if (!sizeof($keys)) {
- return $response->withJson(["error" => "Unknown sessionid $sessionid"]);
- }
-
- $sessionkey = $keys[0];
-
- $session = json_decode($redis->get($sessionkey));
-
- $playerid = rand();
- $playerkey = rand();
-
- if (array_key_exists('players', $session)) {
- if (sizeof($session->players) > 3) {
- return $response->withJson(["error" => "Too many players in $sessionid"]);
- }
- } else {
- $session->players = [];
- $session->host = $playerid;
- }
-
- array_push($session->players, [ "shortname" => $parsedBody['shortname'],
- "longname" => $parsedBody['longname'],
- "id" => $playerid]);
-
- $redis->setEx($sessionkey, 3600, json_encode($session));
-
- if (sizeof($session->players) > 1) {
- $players = json_decode($redis->get(KEYPREFIX.";players;$sessionid"));
- } else {
- $players = [];
- }
-
- array_push($players, ["id" => $playerid, "playerkey" => $playerkey]);
-
- $redis->setEx(KEYPREFIX.";players;$sessionid", 3600, json_encode($players));
-
- return $response->withJson(["userid" => $playerid, "playerkey" => $playerkey]);
- }
- );
-
- $app->post(
- '/moonbase/removeuser', function (Request $request, Response $response, array $args) use ($container) {
- // Sample log message
- $container->get('logger')->info("Slim-Skeleton '/moonbase/removeuser' route");
-
- $parsedBody = $request->getParsedBody();
-
- if (!array_key_exists('sessionid', $parsedBody)) {
- return $response->withJson(["error" => "No sessionid specified"]);
- }
-
- $sessionid = $parsedBody['sessionid'];
-
- if (!array_key_exists('userid', $parsedBody)) {
- return $response->withJson(["error" => "No userid specified"]);
- }
-
- $userid = $parsedBody['userid'];
-
- $redis = redisConnect();
-
- $keys = $redis->keys(KEYPREFIX.";sessions;*;$sessionid");
-
- if (!sizeof($keys)) {
- return $response->withJson(["error" => "Unknown sessionid $sessionid"]);
- }
-
- $sessionkey = $keys[0];
-
- $session = json_decode($redis->get($sessionkey));
-
- for ($i = 0; $i < count($session->players); $i++) {
- if ($session->players[$i]->id == $userid) {
- unset($session->players[$i]);
- $redis->setEx($sessionkey, 3600, json_encode($session));
-
- return $response->withJson([]);
- }
- }
-
- return $response->withJson(["error" => "Unknown userid $userid in session $sessionid"]);
- }
- );
-
- $app->get(
- '/moonbase/lobbies', function (Request $request, Response $response, array $args) use ($container) {
- $container->get('logger')->info("Slim-Skeleton '/moonbase/lobbies' route");
-
- $redis = redisConnect();
-
- $keys = $redis->keys(KEYPREFIX.";sessions;*");
-
- $res = [];
-
- foreach ($keys as $key) {
- $session = json_decode($redis->get($key));
-
- if (!array_key_exists('disabled', $session))
- array_push($res, $session);
- }
-
- return $response->withJson($res);
- }
- );
-
- $app->post(
- '/moonbase/packet', function (Request $request, Response $response, array $args) use ($container) {
- $container->get('logger')->info("Slim-Skeleton '/moonbase/packet' route");
-
- $container->get('logger')->info("got " .$request->getBody());
-
- $parsedBody = $request->getParsedBody();
-
- if (!array_key_exists('sessionid', $parsedBody)) {
- return $response->withJson(["error" => "No sessionid specified"]);
- }
-
- $sessionid = $parsedBody['sessionid'];
-
- $container->get('logger')->info("/moonbase/packet: sess: $sessionid");
-
- // Get session
- $redis = redisConnect();
-
- $keys = $redis->keys(KEYPREFIX.";sessions;*;$sessionid");
-
- if (!sizeof($keys)) {
- return $response->withJson(["error" => "Unknown sessionid $sessionid"]);
- }
-
- $count = $redis->incr(KEYPREFIX.";packets;$sessionid");
-
- $redis->setEx(KEYPREFIX.";packets;$sessionid;$count", 3600, json_encode($parsedBody));
-
- return $response->withJson([]);
- }
- );
-
- $app->post(
- '/moonbase/getpacket', function (Request $request, Response $response, array $args) use ($container) {
- // Sample log message
- //$container->get('logger')->info("Slim-Skeleton '/moonbase/getpacket' route");
-
- $parsedBody = $request->getParsedBody();
-
- if (!array_key_exists('sessionid', $parsedBody)) {
- return $response->withJson(["error" => "No sessionid specified"]);
- }
-
- $sessionid = $parsedBody['sessionid'];
-
- if (!array_key_exists('playerid', $parsedBody)) {
- return $response->withJson(["error" => "No playerid specified"]);
- }
-
- $playerid = $parsedBody['playerid'];
-
-
- $redis = redisConnect();
-
- $sessioncount = $redis->get(KEYPREFIX.";packets;$sessionid");
- $playercount = $redis->get(KEYPREFIX.";players;$sessionid;$playerid");
-
- $keys = $redis->keys(KEYPREFIX.";sessions;*;$sessionid");
-
- if (!sizeof($keys)) {
- return $response->withJson(["error" => "Unknown sessionid $sessionid"]);
- }
-
- $sessionkey = $keys[0];
-
- $session = json_decode($redis->get($sessionkey));
-
- for (;;) {
- if ($playercount >= $sessioncount) // No more packets
- return $response->withJson(["size" => 0]);
-
- $playercount = $redis->incr(KEYPREFIX.";players;$sessionid;$playerid");
-
- $container->get('logger')->info("'/moonbase/getpacket' reading packet $playercount");
-
- if (!$redis->exists(KEYPREFIX.";packets;$sessionid;$playercount")) {
- return $response->withJson(["error" => "Too big playercount: $playercount > $sessioncount"]);
- }
-
- $packet = json_decode($redis->get(KEYPREFIX.";packets;$sessionid;$playercount"));
-
- $from = $packet->from;
- $type = $packet->type;
-
- $to = -1;
- switch ($type) {
- case NET_SEND_TYPE_INDIVIDUAL: // 1
- $to = $packet->toparam;
- break;
-
- case NET_SEND_TYPE_GROUP: // 2
- $to = -1;
- break;
-
- case NET_SEND_TYPE_HOST: // 3
- $to = $session->host;
- break;
-
- case NET_SEND_TYPE_ALL: // 4
- default:
- $to = -1;
- break;
- }
-
- $container->get('logger')->info("'/moonbase/getpacket' type: $type from: $from, to: $to, playerid: $playerid, size: " . $packet->size);
-
- if (($to == -1 && $from != $playerid) || $to == $playerid) { // Send to all or to me
- return $response->withJson($packet);
- } else {
- $container->get('logger')->info("'/moonbase/getpacket' not ours: to $to");
- }
-
- # It is not pur packet, loop over to next one
- }
- }
- );
-
-};
diff --git a/src/settings.php b/src/settings.php
deleted file mode 100644
index 2346883..0000000
--- a/src/settings.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-return [
- 'settings' => [
- 'displayErrorDetails' => true, // set to false in production
- 'addContentLengthHeader' => false, // Allow the web server to send the content-length header
-
- // Renderer settings
- 'renderer' => [
- 'template_path' => __DIR__ . '/../templates/',
- ],
-
- // Monolog settings
- 'logger' => [
- 'name' => 'slim-app',
- 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
- 'level' => \Monolog\Logger::DEBUG,
- ],
- ],
-];
diff --git a/templates/index.phtml b/templates/index.phtml
deleted file mode 100644
index 7ee7a49..0000000
--- a/templates/index.phtml
+++ /dev/null
@@ -1,36 +0,0 @@
-<!doctype html>
-<html>
- <head>
- <title>ScummVM</title>
- <meta charset="utf-8"/>
- <link rel="stylesheet" type="text/css" href="/cloud-style.css"/>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <img src="https://scummvm.org/images/scummvm_logo.png"/>
- </div>
- <div class="content">
- <p>Which cloud do you want to connect?</p>
- <div class="links-list">
- <a href="/dropbox" class="link">
- <img src="/images/dropbox.png" />
- <b>Dropbox</b>
- </a>
- <a href="/onedrive" class="link">
- <img src="/images/onedrive.png" />
- <b>OneDrive</b>
- </a>
- <a href="/gdrive" class="link">
- <img src="/images/google_drive.png" />
- <b>Google Drive</b>
- </a>
- <a href="/box" class="link">
- <img src="/images/box.png" />
- <b>Box</b>
- </a>
- </div>
- </div>
- </div>
- </body>
-</html>
diff --git a/templates/token.phtml b/templates/token.phtml
deleted file mode 100644
index 062a530..0000000
--- a/templates/token.phtml
+++ /dev/null
@@ -1,18 +0,0 @@
-<!doctype html>
-<html>
- <head>
- <title>ScummVM</title>
- <meta charset="utf-8"/>
- <link rel="stylesheet" type="text/css" href="/cloud-style.css"/>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <img src="https://scummvm.org/images/scummvm_logo.png"/>
- </div>
- <div class="content">
- <p>Please enter the following code in ScummVM: <span class="shortcode"><?= $shortcode ?></span></p>
- </div>
- </div>
- </body>
-</html>
diff --git a/tests/Functional/BaseTestCase.php b/tests/Functional/BaseTestCase.php
deleted file mode 100644
index 5e96209..0000000
--- a/tests/Functional/BaseTestCase.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-
-namespace Tests\Functional;
-
-use Slim\App;
-use Slim\Http\Request;
-use Slim\Http\Response;
-use Slim\Http\Environment;
-use PHPUnit\Framework\TestCase;
-
-/**
- * This is an example class that shows how you could set up a method that
- * runs the application. Note that it doesn't cover all use-cases and is
- * tuned to the specifics of this skeleton app, so if your needs are
- * different, you'll need to change it.
- */
-class BaseTestCase extends TestCase
-{
- /**
- * Use middleware when running application?
- *
- * @var bool
- */
- protected $withMiddleware = true;
-
- /**
- * Process the application given a request method and URI
- *
- * @param string $requestMethod the request method (e.g. GET, POST, etc.)
- * @param string $requestUri the request URI
- * @param array|object|null $requestData the request data
- * @return \Slim\Http\Response
- */
- public function runApp($requestMethod, $requestUri, $requestData = null)
- {
- // Create a mock environment for testing with
- $environment = Environment::mock(
- [
- 'REQUEST_METHOD' => $requestMethod,
- 'REQUEST_URI' => $requestUri
- ]
- );
-
- // Set up a request object based on the environment
- $request = Request::createFromEnvironment($environment);
-
- // Add request data, if it exists
- if (isset($requestData)) {
- $request = $request->withParsedBody($requestData);
- }
-
- // Set up a response object
- $response = new Response();
-
- // Use the application settings
- $settings = include __DIR__ . '/../../src/settings.php';
-
- // Instantiate the application
- $app = new App($settings);
-
- // Set up dependencies
- $dependencies = include __DIR__ . '/../../src/dependencies.php';
- $dependencies($app);
-
- // Register middleware
- if ($this->withMiddleware) {
- $middleware = include __DIR__ . '/../../src/middleware.php';
- $middleware($app);
- }
-
- // Register routes
- $routes = include __DIR__ . '/../../src/routes.php';
- $routes($app);
-
- // Process the application
- $response = $app->process($request, $response);
-
- // Return the response
- return $response;
- }
-}
diff --git a/tests/Functional/HomepageTest.php b/tests/Functional/HomepageTest.php
deleted file mode 100644
index 6bcf043..0000000
--- a/tests/Functional/HomepageTest.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-namespace Tests\Functional;
-
-class HomepageTest extends BaseTestCase
-{
- /**
- * Test that the index route returns a rendered response containing the text 'SlimFramework' but not a greeting
- */
- public function testGetHomepageWithoutName()
- {
- $response = $this->runApp('GET', '/');
-
- $this->assertEquals(200, $response->getStatusCode());
- $this->assertContains('SlimFramework', (string)$response->getBody());
- $this->assertNotContains('Hello', (string)$response->getBody());
- }
-
- /**
- * Test that the index route with optional name argument returns a rendered greeting
- */
- public function testGetHomepageWithGreeting()
- {
- $response = $this->runApp('GET', '/name');
-
- $this->assertEquals(200, $response->getStatusCode());
- $this->assertContains('Hello name!', (string)$response->getBody());
- }
-
- /**
- * Test that the index route won't accept a post request
- */
- public function testPostHomepageNotAllowed()
- {
- $response = $this->runApp('POST', '/', ['test']);
-
- $this->assertEquals(405, $response->getStatusCode());
- $this->assertContains('Method not allowed', (string)$response->getBody());
- }
-}
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 0000000..52eb120
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,7 @@
+FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
+
+COPY ./requirements.txt /app/requirements.txt
+
+RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
+
+COPY ./ /app
\ No newline at end of file
diff --git a/web/config.py b/web/config.py
new file mode 100644
index 0000000..7a7fd39
--- /dev/null
+++ b/web/config.py
@@ -0,0 +1,17 @@
+# Games to accept:
+GAMES = [
+ "moonbase", # Moonbase Commander (v1.0/v1.1/Demo)
+]
+
+# Full names of accepted games. Make sure that they
+# match with the GAMES list above
+NAMES = {
+ "moonbase": "Moonbase Commander",
+}
+
+# Version variants to accept for a specific game.
+# If none exist but game exist in the GAMES list,
+# that means there's only one game version.
+VERSIONS = {
+ "moonbase": ["1.0", "1.1", "Demo"]
+}
\ No newline at end of file
diff --git a/web/main.py b/web/main.py
new file mode 100644
index 0000000..79c261a
--- /dev/null
+++ b/web/main.py
@@ -0,0 +1,69 @@
+import os
+
+from fastapi import FastAPI
+from fastapi.templating import Jinja2Templates
+from fastapi.staticfiles import StaticFiles
+from fastapi.requests import Request
+from fastapi.routing import Mount
+
+import aioredis
+
+from config import *
+
+routes = (
+ Mount('/static', StaticFiles(directory='static'), name='static'),
+)
+
+app = FastAPI(debug=os.environ.get('DEBUG', False), routes=routes, title="ScummVM Multiplayer")
+
+templates = Jinja2Templates(directory="templates")
+
+redis = aioredis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379"),
+ retry_on_timeout=True, decode_responses=True)
+
+ at app.get('/')
+def index(request: Request, format: str = 'html'):
+ if format == 'json':
+ return {"games": NAMES}
+ return templates.TemplateResponse("index.html", {'request': request, 'games': GAMES, 'names': NAMES})
+
+async def get_sessions(game: str, version: str = None):
+ sessions = []
+ key = game
+ if version:
+ key += f":{version}"
+
+ num_sessions = await redis.llen(f"{key}:sessions")
+ session_ids = await redis.lrange(f"{key}:sessions", 0, num_sessions)
+ for id in session_ids:
+ session = await redis.hgetall(f"{key}:session:{id}")
+ sessions.append({"id": int(id), "version": version, "name": session["name"],
+ "players": int(session["players"]), "address": str(session["address"])})
+ return sessions
+
+ at app.get('/{game}')
+async def game_page(request: Request, game: str, version: str = None, format: str = 'html'):
+ sessions = []
+ error = None
+ if game in GAMES:
+ versions = VERSIONS.get(game)
+ if versions:
+ version_request = version
+ if version_request and version_request in versions:
+ # Get sessions for a specific version
+ sessions = await get_sessions(game, version_request)
+ else:
+ # Get sessions for all versions
+ for version in versions:
+ sessions += await get_sessions(game, version)
+ else:
+ # No version variants
+ sessions = await get_sessions(game)
+ else:
+ error = f"Not supported game: \"{game}\""
+
+ if format == 'json':
+ if error:
+ return {"error": error}
+ return {"sessions": sessions}
+ return templates.TemplateResponse("game.html", {'request': request, 'name': NAMES.get(game, game), 'sessions': sessions, 'error': error})
diff --git a/web/requirements.txt b/web/requirements.txt
new file mode 100644
index 0000000..3af1006
--- /dev/null
+++ b/web/requirements.txt
@@ -0,0 +1,4 @@
+fastapi==0.89.1
+aioredis==2.0.1
+uvicorn==0.20.0
+Jinja2==3.1.2
\ No newline at end of file
diff --git a/web/static/icons/moonbase.png b/web/static/icons/moonbase.png
new file mode 100644
index 0000000..ef898ff
Binary files /dev/null and b/web/static/icons/moonbase.png differ
diff --git a/public/cloud-style.css b/web/static/style.css
similarity index 53%
rename from public/cloud-style.css
rename to web/static/style.css
index 8b8d977..e9379e1 100644
--- a/public/cloud-style.css
+++ b/web/static/style.css
@@ -2,6 +2,10 @@ html {
background: #c60;
}
+body {
+ font-family: 'verdana', 'tahoma', 'arial', 'helvetica', sans-serif;
+}
+
.container {
width: 80%;
max-width: 500pt;
@@ -16,11 +20,14 @@ html {
.content {
padding: 8pt;
- background: #FFF;
- font-family: 'verdana', 'tahoma', 'arial', 'helvetica', sans-serif;
+ background: #fff2cd;
font-size: 16pt;
}
+.small {
+ font-size: 8pt;
+}
+
.content p {
margin: 0 0 6pt 0;
text-align: center;
@@ -32,15 +39,13 @@ html {
flex-wrap: wrap;
}
-a { color: #434343; }
-
-a:hover { color: #0080FF; }
+a { color: #000000; }
a.link {
display: inline-block;
- min-width: calc(25% - 2*10pt);
- width: calc(25% - 2*10pt);
- height: calc(25% - 2*10pt);
+ min-width: calc(20% - 2*10pt);
+ width: calc(20% - 2*10pt);
+ height: calc(20% - 2*10pt);
padding: 10pt;
margin: 0;
border: 0;
@@ -49,7 +54,7 @@ a.link {
}
a.link:hover {
- background: #F3F3F3;
+ background: #00ce31;
}
a.link > img {
@@ -59,15 +64,33 @@ a.link > img {
}
a.link > b {
- font-size: 16pt;
+ font-size: 14pt;
}
.shortcode {
font-family: monospace;
}
+/* Style the button that is used to open and close the collapsible content */
+.collapsible {
+ background-color: #eee;
+ color: #444;
+ cursor: pointer;
+ padding: 18px;
+ width: 100%;
+ border: none;
+ text-align: left;
+ outline: none;
+ font-size: 15px;
+ }
+
+ /* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
+ .active, .collapsible:hover {
+ background-color: #cccccc;
+ }
+
@media only screen and (max-width: 640px) {
a.link > b {
font-size: 10pt;
-}
-}
+ }
+}
\ No newline at end of file
diff --git a/web/templates/game.html b/web/templates/game.html
new file mode 100644
index 0000000..121e8aa
--- /dev/null
+++ b/web/templates/game.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>ScummVM Multiplayer</title>
+ <link rel="stylesheet" type="text/css" href="{{url_for('static', path='style.css')}}"/>
+ <link rel="icon" type="image/x-icon" href="https://scummvm.org/favicon.ico">
+ </head>
+ <body>
+ <div class="container">
+ <div class="header">
+ <img src="https://scummvm.org/images/scummvm_logo.png"/>
+ </div>
+ <div class="content">
+ {% if error -%}
+ <p>{{error}}</p>
+ {% else -%}
+ {% if not sessions -%}
+ <p>There are no {{name}} games currently.</p>
+ {% else -%}
+ <dl>
+ {% for session in sessions -%}
+ <dt>{{session["name"]}}</li>
+ <dd>- Players: {{session["players"]}}/4</dd>
+ <dd>- Version: {{session["version"]}}</dd>
+ {% endfor -%}
+ </dl>
+ {% endif -%}
+ {% endif -%}
+ </div>
+ </div>
+ </body>
+</html>
\ No newline at end of file
diff --git a/web/templates/index.html b/web/templates/index.html
new file mode 100644
index 0000000..83b0d86
--- /dev/null
+++ b/web/templates/index.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>ScummVM Multiplayer</title>
+ <link rel="stylesheet" type="text/css" href="{{url_for('static', path='style.css')}}"/>
+ <link rel="icon" type="image/x-icon" href="https://scummvm.org/favicon.ico">
+ </head>
+ <body>
+ <div class="container">
+ <div class="header">
+ <img src="https://scummvm.org/images/scummvm_logo.png"/>
+ </div>
+ <div class="content">
+ <p>Multiplayer Lobbies</p>
+ <div class="links-list">
+ {% for game in games -%}
+ <a href="/{{game}}" class="link">
+ <img src="{{url_for('static', path='icons/{}.png'.format(game))}}" />
+ <b>{{names[game]}}</b>
+ </a>
+ {% endfor -%}
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
\ No newline at end of file
Commit: 28043e8ce7f6583bef67d23f3d3733fb127924a4
https://github.com/scummvm/scummvm-sites/commit/28043e8ce7f6583bef67d23f3d3733fb127924a4
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: DOCKER: Use Debian image.
Changed paths:
Dockerfile
diff --git a/Dockerfile b/Dockerfile
index 3f16dba..f44b28f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,16 @@
-FROM python:3.9-buster
+FROM debian:bullseye
+
+# Install pyenet from apt
+RUN apt-get update && apt-get install -y \
+ python3 \
+ python3-enet \
+ python3-pip \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
-COPY requirements.txt ./
-RUN pip install --no-cache-dir -r requirements.txt
+RUN pip install --no-cache-dir \
+ redis
COPY . .
-CMD [ "python", "main.py" ]
\ No newline at end of file
+CMD [ "python3", "main.py" ]
\ No newline at end of file
Commit: 512fbde9e69995a7ae8173be601ec3f5af586a02
https://github.com/scummvm/scummvm-sites/commit/512fbde9e69995a7ae8173be601ec3f5af586a02
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Relay support.
Changed paths:
A net_defines.py
main.py
diff --git a/main.py b/main.py
index d3cda69..072e7dd 100644
--- a/main.py
+++ b/main.py
@@ -5,6 +5,7 @@ import signal
import enet
import redis as redis_package
+from net_defines import *
# Games to accept:
GAMES = [
@@ -49,24 +50,24 @@ for game in get_full_game_names():
host = enet.Host(enet.Address(b"0.0.0.0", 9120), peerCount=4095, channelLimit=1)
print("Listening for messages in port 9120", flush=True)
-def send(peer, data):
+def send(peer, data: dict):
logging.debug(f"{peer.address}: OUT: {data}")
data = json.dumps(data).encode()
peer.send(0, enet.Packet(data, enet.PACKET_FLAG_RELIABLE))
-def get_peer_by_address(address):
+def get_peer_by_address(address: str):
for peer in host.peers:
if str(peer.address) == address:
return peer
return None
-def get_session_by_address(game, address):
+def get_session_by_address(game: str, address: str):
session_id = redis.hget(f"{game}:sessionByAddress", str(address))
if session_id:
return redis.hgetall(f"{game}:session:{session_id}")
return None
-def create_session(name, address):
+def create_session(name: str, address: str):
# Get our new session ID
session_id = redis.incr(f"{game}:counter")
# Create and store our new session
@@ -81,6 +82,96 @@ def create_session(name, address):
logging.debug(f"{address}: NEW SESSION: \"{name}\"")
return session_id
+def relay_data(data, sent_peer):
+ from_user = data.get("from")
+ type_of_send = data.get("to")
+ send_type_param = data.get("toparam")
+
+ session_id = relays.get(sent_peer)
+ if not session_id:
+ return
+
+ peers_by_user_id = session_to_relay_user_ids.get(session_id)
+ if not peers_by_user_id:
+ logging.warning(f"relay_data: Missing peers on session_to_relay_user_ids[{session_id}]!")
+ return
+
+ user_id_by_peers = {v: k for k, v in peers_by_user_id.items()}
+
+ logging.debug(f"relay_data: Players of session {session_id}:")
+ for user_id, peer in peers_by_user_id.items():
+ logging.debug(f"relay_data: - {user_id}: {str(peer.address)}")
+
+ if user_id_by_peers.get(sent_peer) != 1:
+ # To make things easier, just send all non-host data to the host.
+ # It'll send it back to us if it actually needs to be relayed somewhere.
+ host_peer = peers_by_user_id.get(1)
+ if not host_peer:
+ logging.warning("relay_data: Host user (1) is missing!")
+ return
+ logging.debug(f"relay_data: Relaying data from user {user_id_by_peers.get(sent_peer)} to host (1).")
+ send(host_peer, data)
+ return
+
+ peers_to_send = set()
+ if type_of_send == PN_SENDTYPE_INDIVIDUAL:
+ peer = peers_by_user_id.get(send_type_param)
+ if not peer:
+ logging.warning(f"relay_data: user {send_type_param} not in relay, Host does not know, something might be wrong.")
+ return
+ logging.debug(f"relay_data: Relaying data to user {send_type_param}")
+ peers_to_send.add(peer)
+ elif type_of_send == PN_SENDTYPE_GROUP:
+ logging.warning("STUB: PN_SENDTYPE_GROUP")
+ return
+ elif type_of_send == PN_SENDTYPE_HOST:
+ # Chances are that the host is user_id 1.
+ peer = peers_by_user_id.get(1)
+ if not peer:
+ return
+ logging.debug(f"relay_data: Relaying data to host (user 1)")
+ peers_to_send.add(peer)
+ elif type_of_send == PN_SENDTYPE_ALL:
+ # Send to all peers
+ for peer in peers_by_user_id.values():
+ peers_to_send.add(peer)
+
+ logging.debug(f"relay_data: Relaying data to all peers: {str(list(peers_by_user_id.keys()))}")
+ else:
+ logging.warning(f"relay_data: Unknown type of send: {type_of_send}")
+
+ # Remove self from set.
+ if sent_peer in peers_to_send:
+ peers_to_send.remove(sent_peer)
+
+ for peer in peers_to_send:
+ send(peer, data)
+
+def remove_user_from_relay(peer):
+ session_id = relays.get(peer)
+ if not session_id:
+ return
+
+ del relays[peer]
+
+ peers_by_user_id = session_to_relay_user_ids.get(session_id)
+ if not peers_by_user_id:
+ return
+
+ user_id_by_peers = {v: k for k, v in peers_by_user_id.items()}
+ user_id = user_id_by_peers.get(peer)
+ if not user_id:
+ return
+
+ del session_to_relay_user_ids[session_id][user_id]
+
+ # Send the remove_user request to the host.
+ host_peer = peers_by_user_id.get(1)
+ if not host_peer:
+ return
+
+ send(host_peer, {"cmd": "remove_user", "id": user_id})
+
do_loop = True
def exit(*args):
global do_loop
@@ -91,6 +182,9 @@ signal.signal(signal.SIGTERM, exit)
# SIGINT: Ctrl+C KeyboardInterrupt
signal.signal(signal.SIGINT, exit)
+relays = {} # peer: sessionId
+session_to_relay_user_ids = {} # sessionId: {userId: peer}
+
while do_loop:
# Main event loop
event = host.service(1000)
@@ -106,15 +200,28 @@ while do_loop:
redis.lrem(f"{game}:sessions", 0, session_id)
redis.hdel(f"{game}:sessionByAddress", str(event.peer.address))
+ # Cleanup Relays (if any):
+ if session_id in session_to_relay_user_ids:
+ del session_to_relay_user_ids[session_id]
+ remove_user_from_relay(event.peer)
+
+
elif event.type == enet.EVENT_TYPE_RECEIVE:
logging.debug(f"{event.peer.address}: IN: {event.packet.data}")
- # TODO: Relays
try:
data = json.loads(event.packet.data)
except:
- logging.warning(f"{event.peer.address}: Received non-JSON data.")
+ logging.warning(f"{event.peer.address}: Received non-JSON data.", event.packet.data.decode())
continue
command = data.get("cmd")
+
+ if command == "game":
+ relay_data(data, event.peer)
+ continue
+ elif command == "remove_user":
+ remove_user_from_relay(event.peer)
+ continue
+
game = data.get("game")
version = data.get("version")
if not command:
@@ -170,7 +277,7 @@ while do_loop:
session_id = data.get("id")
if not (redis.exists(f"{game}:session:{id}")):
- logging.warn(f"Session {game}:{session_id} not found")
+ logging.warning(f"Session {game}:{session_id} not found")
continue
address = redis.hget(f"{game}:session:{id}", "address")
@@ -180,3 +287,46 @@ while do_loop:
# Send the joiner's address to the hoster for hole-punching
send(peer, {"cmd": "joining_session", "address": str(event.peer.address)})
+ elif command == "start_relay":
+ session_id = data.get("session")
+
+ if not (redis.exists(f"{game}:session:{id}")):
+ logging.warning(f"Session {game}:{session_id} not found")
+ continue
+
+ address = redis.hget(f"{game}:session:{id}", "address")
+ peer = get_peer_by_address(address)
+ if not peer:
+ continue
+
+ if session_id not in session_to_relay_user_ids:
+ # The host peer is usually always has the userId of 1:
+ session_to_relay_user_ids[session_id] = {1: peer}
+ if peer not in relays:
+ relays[peer] = session_id
+
+ # Send the add_user request to the host (with joiner's address for context):
+ send(peer, {"cmd": "add_user_for_relay", "address": str(event.peer.address)})
+ elif command == "add_user_resp":
+ address = data.get("address")
+ user_id = data.get("id")
+
+ session_id = int(redis.hget(f"{game}:sessionByAddress", str(event.peer.address)))
+ if not session_id:
+ logging.warning(f"Could not find session for address {str(event.peer.address)}!")
+ continue
+ if session_id not in session_to_relay_user_ids:
+ logging.warning(f"Session ID {session_id} not found in session2RelayUserIds!")
+ continue
+ if user_id in session_to_relay_user_ids[session_id]:
+ logging.warning(f"Duplicate user ID {user_id} in session2RelayUserIds[{session_id}]!")
+ continue
+ peer = get_peer_by_address(address)
+ if not peer:
+ logging.warning(f"Could not find peer for address: {address}!")
+ continue
+
+ session_to_relay_user_ids[session_id][user_id] = peer
+ relays[peer] = session_id
+ # Send the response back to the peer:
+ send(peer, {"cmd": "add_user_resp", "id": user_id})
diff --git a/net_defines.py b/net_defines.py
new file mode 100644
index 0000000..367622e
--- /dev/null
+++ b/net_defines.py
@@ -0,0 +1,8 @@
+# Copied from engines/scumm/he/moonbase/net_defines.h
+
+# These are used for relaying.
+PN_PRIORITY_HIGH = 0x00000001
+PN_SENDTYPE_INDIVIDUAL = 1
+PN_SENDTYPE_GROUP = 2
+PN_SENDTYPE_HOST = 3
+PN_SENDTYPE_ALL = 4
\ No newline at end of file
Commit: 44ac81fbf23278d16fdc41d3f70ea1f2271a759d
https://github.com/scummvm/scummvm-sites/commit/44ac81fbf23278d16fdc41d3f70ea1f2271a759d
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Add supported Backyard Sports titles.
Changed paths:
A web/static/icons/baseball2001.png
A web/static/icons/football.png
A web/static/icons/football2002.png
main.py
web/config.py
diff --git a/main.py b/main.py
index 072e7dd..d9df2bb 100644
--- a/main.py
+++ b/main.py
@@ -9,7 +9,10 @@ from net_defines import *
# Games to accept:
GAMES = [
- "moonbase" # Moonbase Commander (v1.0/v1.1/Demo)
+ "football", # Backyard Football (1999)
+ "baseball2001", # Backyard Baseball 2001
+ "football2002", # Backyard Football 2002
+ "moonbase", # Moonbase Commander (v1.0/v1.1/Demo)
]
# Version variants to accept for a specific game.
diff --git a/web/config.py b/web/config.py
index 7a7fd39..c4e81a6 100644
--- a/web/config.py
+++ b/web/config.py
@@ -1,12 +1,18 @@
# Games to accept:
GAMES = [
+ "football", # Backyard Football (1999)
+ "baseball2001", # Backyard Baseball 2001
+ "football2002", # Backyard Football 2002
"moonbase", # Moonbase Commander (v1.0/v1.1/Demo)
]
# Full names of accepted games. Make sure that they
# match with the GAMES list above
NAMES = {
- "moonbase": "Moonbase Commander",
+ "football": "Backyard Football",
+ "baseball2001": "Backyard Baseball 2001",
+ "football2002": "Backyard Football 2002",
+ "moonbase": "Moonbase Commander"
}
# Version variants to accept for a specific game.
diff --git a/web/static/icons/baseball2001.png b/web/static/icons/baseball2001.png
new file mode 100644
index 0000000..84977e7
Binary files /dev/null and b/web/static/icons/baseball2001.png differ
diff --git a/web/static/icons/football.png b/web/static/icons/football.png
new file mode 100644
index 0000000..747baff
Binary files /dev/null and b/web/static/icons/football.png differ
diff --git a/web/static/icons/football2002.png b/web/static/icons/football2002.png
new file mode 100644
index 0000000..e95a3c4
Binary files /dev/null and b/web/static/icons/football2002.png differ
Commit: ec3d0ded4fcd9d05f45cf65051925768feb5b4a7
https://github.com/scummvm/scummvm-sites/commit/ec3d0ded4fcd9d05f45cf65051925768feb5b4a7
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: WEB: Don't use url_for for static.
Changed paths:
web/templates/game.html
web/templates/index.html
diff --git a/web/templates/game.html b/web/templates/game.html
index 121e8aa..e349504 100644
--- a/web/templates/game.html
+++ b/web/templates/game.html
@@ -2,7 +2,7 @@
<html>
<head>
<title>ScummVM Multiplayer</title>
- <link rel="stylesheet" type="text/css" href="{{url_for('static', path='style.css')}}"/>
+ <link rel="stylesheet" type="text/css" href="/static/style.css"/>
<link rel="icon" type="image/x-icon" href="https://scummvm.org/favicon.ico">
</head>
<body>
diff --git a/web/templates/index.html b/web/templates/index.html
index 83b0d86..a306038 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -2,7 +2,7 @@
<html>
<head>
<title>ScummVM Multiplayer</title>
- <link rel="stylesheet" type="text/css" href="{{url_for('static', path='style.css')}}"/>
+ <link rel="stylesheet" type="text/css" href="/static/style.css"/>
<link rel="icon" type="image/x-icon" href="https://scummvm.org/favicon.ico">
</head>
<body>
@@ -15,7 +15,7 @@
<div class="links-list">
{% for game in games -%}
<a href="/{{game}}" class="link">
- <img src="{{url_for('static', path='icons/{}.png'.format(game))}}" />
+ <img src="{{'/static/icons/{}.png'.format(game)}}" />
<b>{{names[game]}}</b>
</a>
{% endfor -%}
Commit: 979f88da2dfb8da53b43efabd093c10ecfcd2a78
https://github.com/scummvm/scummvm-sites/commit/979f88da2dfb8da53b43efabd093c10ecfcd2a78
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Remove output whitespace
Changed paths:
main.py
diff --git a/main.py b/main.py
index d9df2bb..3e74cf4 100644
--- a/main.py
+++ b/main.py
@@ -55,7 +55,7 @@ print("Listening for messages in port 9120", flush=True)
def send(peer, data: dict):
logging.debug(f"{peer.address}: OUT: {data}")
- data = json.dumps(data).encode()
+ data = json.dumps(data, separators=(',', ':')).encode()
peer.send(0, enet.Packet(data, enet.PACKET_FLAG_RELIABLE))
def get_peer_by_address(address: str):
Commit: 8432f3566e4cf1c4df2a68ab72116cc7c3bcef12
https://github.com/scummvm/scummvm-sites/commit/8432f3566e4cf1c4df2a68ab72116cc7c3bcef12
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Add lobby server code.
Changed paths:
A lobby/.dockerignore
A lobby/.gitignore
A lobby/Dockerfile
A lobby/README.md
A lobby/config.yaml
A lobby/database/Redis.js
A lobby/database/WebAPI.js
A lobby/discord/Discord.js
A lobby/global/Areas.js
A lobby/global/EventLogger.js
A lobby/global/Stats.js
A lobby/net/AreaMessages.js
A lobby/net/ChallengeMessages.js
A lobby/net/DatabaseMessages.js
A lobby/net/NetworkConnection.js
A lobby/net/NetworkListener.js
A lobby/net/SessionMessages.js
A lobby/package-lock.json
A lobby/package.json
A lobby/run.js
README.md
docker-compose.yml
diff --git a/README.md b/README.md
index 1f7197d..b927000 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# ScummVM Multiplayer
-This project contains code for hosting online multiplayer lobbies for Moonbase Commander. This is currently WIP.
+This project contains code for hosting online multiplayer lobbies for compatable Humongous Entertainment games.
## Getting Started
### Installing
@@ -20,7 +20,11 @@ source .env/bin/activate
python3 -m pip install -r requirements.txt
```
-if you're planning to run the web server, install the requirements located in the web directory.
+
+Backyard Football and Backyard Baseball 2001 needs a lobby server to play online. You will need
+[Node.js](https://nodejs.org/en/) installed to run it.
+
+If you're planning to run the web server, install the requirements located in the web directory.
```
python3 -m pip install -r web/requirements.txt
```
@@ -32,17 +36,28 @@ python3 main.py
```
It should listen for connections on port 9120. Remember to configure ScummVM to connect to localhost or whatever address your server is running in.
+To start the lobby server, go to the `lobby` directory and install the dependencies:
+```
+cd lobby
+npm install
+```
+
+After that's done, you can simply run the `run.js` file.
+```
+node run.js
+```
+
Running a web server is unnecessary if you just want your server to host sessions, but if you want to, you can start one up by using uvicorn.
```
cd web
uvicorn main:app --reload
```
-## Deployment
-Both the session and web server can be run within Docker via docker-compose.
+## Docker
+The session, lobby and web server can be run within Docker via docker-compose.
```
docker-compose build
docker-compose up
```
-This will build Docker images for both servers and starts a container for them simultaneously alongside with Redis.
\ No newline at end of file
+This will build Docker images for all three servers and starts a container for them simultaneously alongside with Redis.
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 574361b..837f6a7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,6 +6,12 @@ services:
- REDIS_HOST=redis
ports:
- "9120:9120/udp"
+ lobby:
+ build: ./lobby/
+ environment:
+ - REDIS_HOST=redis
+ ports:
+ - "9130:9130"
web:
build: ./web/
environment:
diff --git a/lobby/.dockerignore b/lobby/.dockerignore
new file mode 100644
index 0000000..7a8827b
--- /dev/null
+++ b/lobby/.dockerignore
@@ -0,0 +1,4 @@
+node_modules
+npm-debug.log
+.gitignore
+**/.git
\ No newline at end of file
diff --git a/lobby/.gitignore b/lobby/.gitignore
new file mode 100644
index 0000000..8f536d9
--- /dev/null
+++ b/lobby/.gitignore
@@ -0,0 +1,121 @@
+# Project specific
+credentials.yaml
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+.env.production
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
diff --git a/lobby/Dockerfile b/lobby/Dockerfile
new file mode 100644
index 0000000..31acebc
--- /dev/null
+++ b/lobby/Dockerfile
@@ -0,0 +1,11 @@
+FROM node:16
+
+WORKDIR /usr/src/app
+
+COPY package*.json ./
+
+RUN npm install
+
+COPY . .
+
+CMD [ "node", "run.js" ]
diff --git a/lobby/README.md b/lobby/README.md
new file mode 100644
index 0000000..c1e1b0e
--- /dev/null
+++ b/lobby/README.md
@@ -0,0 +1,11 @@
+# ScummVM Lobby Server
+
+Lobby server code for Backyard Football and Backyard Baseball 2001.
+
+This server is the same one being used for [Backyard Sports Online](https://backyardsports.online/), which is
+now provided by the ScummVM project.
+
+An archive of the original code can be located here:
+https://github.com/Backyard-Sports-Online/server
+
+Follow the README.md in the base directory to get it up and running.
diff --git a/lobby/config.yaml b/lobby/config.yaml
new file mode 100644
index 0000000..1a3d393
--- /dev/null
+++ b/lobby/config.yaml
@@ -0,0 +1,11 @@
+# Process cores to fork the network processes
+cores: 1
+
+network:
+ host: 0.0.0.0
+ port: 9130
+
+ # TLS certificate. Set the path to accept
+ # TLS connections, this MUST be set in production.
+ # key: /path/to/private_key.pem
+ # cert: /path/to/certificate.crt
diff --git a/lobby/database/Redis.js b/lobby/database/Redis.js
new file mode 100644
index 0000000..ed94d67
--- /dev/null
+++ b/lobby/database/Redis.js
@@ -0,0 +1,236 @@
+"use strict";
+
+const createLogger = require('logging').default;
+const ioredis = require("ioredis")
+const Areas = require('../global/Areas.js');
+
+class Redis {
+ constructor(config) {
+ this.logger = createLogger("Redis");
+ this.redis = new ioredis(config);
+
+ this.redis.on("ready", async () => {
+ this.logger.info("Connected");
+ if (database != this) {
+ // Use a different database so the dev accounts won't overlap.
+ await this.redis.select(1);
+ }
+ if (process.env.FIRST_WORKER) {
+ // Initalization and cache cleanup goes here.
+ if (database == this) {
+ this.logger.info("Using Redis as default database.")
+ if (!await this.redis.exists("byonline:globals:userId")) {
+ this.logger.info("Creating userId sequence...");
+ await this.redis.set("byonline:globals:userId", 0);
+ }
+
+ // Clear out the user list in every area.
+ const keys = [];
+ for (const areaId of Object.keys(Areas.Areas)) {
+ keys.push(`byonline:areas:football:${areaId}`,
+ `byonline:areas:baseball:${areaId}`);
+ }
+ this.redis.del(keys);
+ } else {
+ // Flush the entire cache and start off clean.
+ await this.redis.flushdb();
+ }
+ }
+ });
+ }
+
+ async getUserById(userId, game) {
+ const response = await this.redis.hgetall(`byonline:users:${userId}`);
+ if (Object.keys(response).length == 0) {
+ this.logger.warn(`User ${userId} not found in Redis!`);
+ return {};
+ }
+ let stats;
+ if (database == this) {
+ stats = (game == 'football' ? response['f_stats'] : response['b_stats']);
+ } else {
+ stats = response['stats'];
+ }
+ return {
+ 'id': Number(userId),
+ 'user': response['user'],
+ 'icon': Number(response['icon']),
+ 'stats': stats
+ .split(',').map(Number),
+ 'game': response['game'],
+ 'area': Number(response['area']),
+ 'inGame': Number(response['inGame']),
+ 'phone': Number(response['phone']),
+ 'opponent': Number(response['opponent'])
+ }
+ }
+
+ async getUserByName(username) {
+ const userId = await this.redis.hget("byonline:users:nameToId", username);
+ if (userId) {
+ return await this.getUserById(userId);
+ }
+ return undefined;
+ }
+
+ async addUser(userId, user, game) {
+ // Add some server specific keys.
+ user.game = game;
+ user.area = 0;
+ user.phone = 0;
+ user.opponent = 0;
+ user.inGame = 0;
+
+ await this.redis.hmset(`byonline:users:${userId}`, user);
+ await this.redis.hset("byonline:users:nameToId", user['user'].toUpperCase(), userId);
+ }
+
+ async getUser(username, password, game) {
+ if (database != this) {
+ this.logger.warn("Redis isn't set as default database, calling getUser shouldn't be possible");
+ return {error: 1, message: "Internal error."};
+ }
+ let user = await this.getUserByName(username);
+ if (user) {
+ await this.addUser(user.id, user, game)
+ return user;
+ } else {
+ const userId = await this.redis.incr("byonline:globals:userId")
+ user = {
+ 'user': username,
+ 'icon': 0,
+ 'f_stats': Array(42).fill(0),
+ 'b_stats': Array(29).fill(0),
+ }
+ this.addUser(userId, user, game);
+ user['id'] = userId;
+ return user;
+ }
+ }
+
+ async getUserWithToken(token, game) {
+ return {error: 1, message: "Redis does not support token logins."};
+ }
+
+ async removeUser(userId, game) {
+ // We don't want to remove users from a dev database.
+ if (database != this) {
+ // We need the name to clear the nameToId from.
+ const user = await this.getUserById(userId, game);
+ if (Object.keys(user).length == 0)
+ return;
+
+ await this.redis.del(`byonline:users:${userId}`);
+ await this.redis.hdel('byonline:users:nameToId', user.user.toUpperCase());
+ } else {
+ await this.redis.hmset(`byonline:users:${userId}`, {
+ 'game': '',
+ 'area': 0,
+ 'phone': 0,
+ 'opponent': 0
+ });
+ }
+
+ await this.removeOngoingResults(userId, game);
+ }
+
+ async setIcon(userId, icon) {
+ await this.redis.hset(`byonline:users:${userId}`, 'icon', icon);
+ }
+
+ async getUserIdsInArea(areaId, game) {
+ return await this.redis.lrange(`byonline:areas:${game}:${areaId}`, 0, -1)
+ .then((users) => {
+ return users.map(Number);
+ });
+ }
+
+ async getUsersInArea(areaId, game) {
+ const usersList = await this.getUserIdsInArea(areaId, game);
+ const users = [];
+ for (const userId of usersList) {
+ const user = await this.getUserById(userId);
+ // If the user doesn't exist or in a game, skip them out.
+ if (Object.keys(user).length == 0 || user.inGame)
+ continue;
+ users.push(user);
+ }
+ return users;
+ }
+
+ async addUserToArea(userId, areaId, game) {
+ if (await this.redis.lpos(`byonline:areas:${game}:${areaId}`, userId) == null) {
+ await this.redis.rpush(`byonline:areas:${game}:${areaId}`, userId);
+ await this.redis.hset(`byonline:users:${userId}`, {area: areaId});
+ }
+ // This calls sendUsersInArea
+ await this.setPhoneStatus(userId, areaId, game, 0);
+ // TODO: Only send games playing to this specific user.
+ await this.sendGamesPlayingInArea(areaId, game);
+ }
+
+ async removeUserFromArea(userId, areaId, game) {
+ await this.redis.lrem(`byonline:areas:${game}:${areaId}`, 0, userId);
+ await this.redis.hset(`byonline:users:${userId}`, {area: 0});
+ await this.sendUsersInArea(areaId, game);
+ await this.sendGamesPlayingInArea(areaId, game);
+
+ }
+
+ async sendUsersInArea(areaId, game) {
+ const users = await this.getUsersInArea(areaId, game);
+ process.send({cmd: 'update_players_list',
+ area: areaId,
+ game: game,
+ users: users
+ });
+ }
+
+ async setPhoneStatus(userId, areaId, game, phoneStatus) {
+ await this.redis.hset(`byonline:users:${userId}`, {phone: phoneStatus});
+ // Update the clients in area.
+ await this.sendUsersInArea(areaId, game);
+ }
+
+ async setInGame(userId, inGame) {
+ await this.redis.hset(`byonline:users:${userId}`, {inGame: inGame});
+ }
+
+ async sendGamesPlayingInArea(areaId, game) {
+ const usersList = await this.getUserIdsInArea(areaId, game);
+ let gamesPlaying = 0;
+ for (const userId of usersList) {
+ const user = await this.getUserById(userId);
+ if (user.inGame) {
+ gamesPlaying += .5;
+ }
+ }
+ process.send({cmd: "update_games_playing",
+ area: areaId,
+ game: game,
+ games: Math.floor(gamesPlaying)
+ });
+ }
+
+ async setOngoingResults(userId, game, ongoingResults) {
+ await this.redis.hmset(`byonline:ongoingResults:${game}:${userId}`, ongoingResults);
+ }
+
+ async getOngoingResults(userId, game) {
+ const ongoingResults = await this.redis.hgetall(`byonline:ongoingResults:${game}:${userId}`);
+ return ongoingResults;
+ }
+
+ async removeOngoingResults(userId, game) {
+ const resultsKey = `byonline:ongoingResults:${game}:${userId}`;
+ if (await this.redis.exists(resultsKey)) {
+ await this.redis.del(resultsKey);
+ }
+ }
+
+ async getTeam(userId, game) {
+ return {error: 1, message: "Redis API does not support teams"};
+ }
+}
+
+module.exports = Redis;
diff --git a/lobby/database/WebAPI.js b/lobby/database/WebAPI.js
new file mode 100644
index 0000000..10553de
--- /dev/null
+++ b/lobby/database/WebAPI.js
@@ -0,0 +1,57 @@
+"use strict";
+const createLogger = require('logging').default;
+const bent = require('bent');
+
+class WebAPI {
+ constructor(config) {
+ this.logger = createLogger('WebAPI');
+
+ this.token = config['token'] || "";
+
+ const endpoint = config['endpoint'];
+ this.get = bent(endpoint, 'GET', 'json', 200);
+ this.post = bent(endpoint, 'POST', 'json', 200);
+ }
+
+ async getUser(username, password, game) {
+ // TODO: Replace with /login
+ const user = await this.post('/new_login', {token: this.token,
+ user: username,
+ pass: password,
+ game: game});
+ if (user.error) {
+ return user;
+ }
+ // Store the user into the Redis cache
+ redis.addUser(user.id, {user: user.user,
+ icon: user.icon,
+ stats: user.stats}, game);
+ return user;
+ }
+
+ async setIcon(userId, icon) {
+ const response = await this.post('/set_icon', {token: this.token,
+ userId: userId,
+ icon: icon});
+ if (response.error) {
+ this.logger.error("Failed to set icon!", { response });
+ return;
+ }
+
+ // Set the icon in the Redis cache.
+ redis.setIcon(userId, icon);
+ }
+
+ async getTeam(userId, game) {
+ const response = await this.post('/get_team', {token: this.token,
+ userId: userId,
+ game: game});
+ if (response.error) {
+ this.logger.error("Failed to get team!", { response });
+ }
+
+ return response;
+ }
+}
+
+module.exports = WebAPI;
diff --git a/lobby/discord/Discord.js b/lobby/discord/Discord.js
new file mode 100644
index 0000000..90a0e19
--- /dev/null
+++ b/lobby/discord/Discord.js
@@ -0,0 +1,179 @@
+"use strict";
+
+const createLogger = require('logging').default;
+const { table, getBorderCharacters } = require('table');
+const { Client, Intents, MessageEmbed } = require('discord.js');
+const { Areas, Groups } = require('../global/Areas.js');
+
+class Discord {
+ constructor(config) {
+ this.logger = createLogger("Discord");
+
+ this.sentOffline = false;
+
+ this.scoreboardTableConfig = {
+ border: getBorderCharacters('ramac'),
+ drawHorizontalLine: (lineIndex, rowCount) => {return lineIndex <= 1 || lineIndex % 2 == 1}
+ };
+
+ this.client = new Client({intents: [Intents.FLAGS.GUILDS]});
+ this.client.once('ready', async () => {
+ this.logger.info("Connected.");
+ this.channel = await this.client.channels.fetch(config['channel']);
+ let messages = await this.channel.messages.fetch();
+ messages = messages.filter(m => m.author.id === config['client']);
+ if (messages.first())
+ this.lastMessageId = messages.first().id;
+ this.logger.info("Now sending population.");
+ await this.sendPopulation();
+
+ // Set a timeout
+ this.populationTimer = setTimeout(async () => {
+ await this.sendPopulation();
+ this.populationTimer.refresh();
+ }, 30000);
+
+ });
+
+ this.client.login(config['token']);
+ }
+
+ async sendPopulation() {
+ let usersOnline = 0;
+ let gamesPlaying = 0;
+
+ let baseballUsers = '';
+ let footballUsers = '';
+
+ const userIds = Object.values(await redis.redis.hgetall("byonline:users:nameToId")).map(Number);
+ let ongoingScoresByHome = {};
+ let inGameUserIdsToNames = {}; // Storing so we don't have to call redis for these again later
+ for (const userId of userIds) {
+ const user = await redis.getUserById(userId);
+ if (Object.keys(user).length == 0 || !user.game)
+ // Not logged in.
+ continue;
+
+ usersOnline++;
+ if (user.inGame) {
+ gamesPlaying += .5;
+
+ const ongoingResultsStrings = await redis.getOngoingResults(userId, user.game);
+ const ongoingResults = Object.fromEntries(
+ Object.entries(ongoingResultsStrings).map(([k, stat]) => [k, Number(stat)])
+ );
+ if (ongoingResults) {
+ inGameUserIdsToNames[userId] = user.user;
+ if ((ongoingResults.opponentId in ongoingScoresByHome) && (ongoingScoresByHome[ongoingResults.opponentId]["awayId"] == userId)) {
+ // This is the away team; we already have the home team's score for this game
+ ongoingScoresByHome[ongoingResults.opponentId]["awayScore"] = ongoingResults.runs;
+ ongoingScoresByHome[ongoingResults.opponentId]["awayHits"] = ongoingResults.hits;
+ ongoingScoresByHome[ongoingResults.opponentId]["completedInnings"] = ongoingResults.completedInnings;
+ } else if ((userId in ongoingScoresByHome) && ("awayScore" in ongoingScoresByHome[userId])) {
+ // We already have the away team's score for this game. This must be the home team
+ ongoingScoresByHome[userId]["homeScore"] = ongoingResults.runs;
+ ongoingScoresByHome[userId]["homeHits"] = ongoingResults.hits;
+ ongoingScoresByHome[userId]["completedInnings"] = ongoingResults.completedInnings;
+ } else if (ongoingResults.isHome == 1) {
+ // We don't have either team's score yet and this is the home team. Let's add it
+ ongoingScoresByHome[userId] = {
+ "awayId": ongoingResults.opponentId,
+ "homeScore": ongoingResults.runs,
+ "homeHits": ongoingResults.hits,
+ "completedInnings": ongoingResults.completedInnings,
+ };
+ } else if (ongoingResults.isHome == 0) {
+ // We don't have either team's score yet and this is the away team. Let's add it
+ ongoingScoresByHome[ongoingResults.opponentId] = {
+ "awayId": userId,
+ "awayScore": ongoingResults.runs,
+ "awayHits": ongoingResults.hits,
+ "completedInnings": ongoingResults.completedInnings,
+ };
+ }
+ }
+
+ }
+
+ let area = "(Online)";
+ let groupName = "";
+ if (user.area) {
+ const groups = Object.values(Groups);
+ if (groups[0].includes(user.area))
+ groupName = "Easy Street";
+ else if (groups[1].includes(user.area))
+ groupName = "Mediumville";
+ else if (groups[2].includes(user.area))
+ groupName = "Toughy Town";
+
+ area = `${user.inGame ? '(In-Game) ' : ''}(${Areas[user.area]}, ${groupName})`;
+ }
+ if (user.game == "baseball") {
+ baseballUsers += `${user.user} ${area}\n`;
+ } else {
+ footballUsers += `${user.user} ${area}\n`;
+ }
+ }
+
+ const embed = new MessageEmbed()
+ .setTitle('Server Population:')
+ .setFooter("Updates every 30 seconds.")
+ .setColor("GREY")
+ .setTimestamp();
+
+ if (!usersOnline)
+ embed.setDescription("No one is currently online. :(");
+ else {
+ embed.setDescription(`Total Population: ${usersOnline}\nGames Currently Playing: ${Math.floor(gamesPlaying)}`);
+ if (baseballUsers) {
+ let baseballScoresData = [];
+ for (const homeId in ongoingScoresByHome) {
+ baseballScoresData.push(
+ [
+ inGameUserIdsToNames[ongoingScoresByHome[homeId]["awayId"]],
+ ongoingScoresByHome[homeId]["awayScore"],
+ ongoingScoresByHome[homeId]["awayHits"],
+ ongoingScoresByHome[homeId]["completedInnings"] + 1,
+ ],
+ [
+ inGameUserIdsToNames[homeId],
+ ongoingScoresByHome[homeId]["homeScore"],
+ ongoingScoresByHome[homeId]["homeHits"],
+ "",
+ ]
+ )
+ }
+
+ embed.addField("Backyard Baseball 2001", baseballUsers);
+ if (baseballScoresData.length > 0) {
+ const baseballScoreboardText = table(
+ [[ '', 'R', 'H', 'Inn' ]].concat(baseballScoresData),
+ this.scoreboardTableConfig
+ );
+ embed.addField(
+ "Backyard Baseball 2001 Scoreboard", "```" + baseballScoreboardText + "```"
+ );
+ }
+ }
+
+ if (footballUsers)
+ embed.addField("Backyard Football", footballUsers);
+ }
+
+ if ((!usersOnline && !this.sentOffline) || usersOnline) {
+ if (this.lastMessageId) {
+ const message = await this.channel.messages.fetch(this.lastMessageId)
+ await message.edit({ embeds: [embed] });
+ } else {
+ const message = await this.channel.send({ embeds: [embed] });
+ this.lastMessageId = message.id;
+ }
+ if (!usersOnline)
+ this.sentOffline = true;
+ else
+ this.sentOffline = false;
+ }
+ }
+}
+
+module.exports = Discord;
diff --git a/lobby/global/Areas.js b/lobby/global/Areas.js
new file mode 100644
index 0000000..465022d
--- /dev/null
+++ b/lobby/global/Areas.js
@@ -0,0 +1,32 @@
+"use strict";
+
+const Areas = {
+ // Easy Street
+ 8: "Wilderness",
+ 9: "Mountain Aire",
+ // Mediumville
+ 16: "Dogwood",
+ 17: "Brookline",
+ 18: "Talula Lake",
+ 19: "Prince Rupert",
+ // Toughy Town
+ 24: "Vespucci Park",
+ 25: "Capitol Hill",
+ 26: "Lewis Ave.",
+
+ 33: "Baseball 2001 Lobby"
+};
+
+const Groups = {
+ // Easy Street
+ '-248': [8, 9],
+ // Mediumville
+ '-240': [16, 17, 18, 19],
+ // Toughy Town
+ '-232': [24, 25, 26]
+};
+
+module.exports = {
+ Areas: Areas,
+ Groups: Groups
+};
diff --git a/lobby/global/EventLogger.js b/lobby/global/EventLogger.js
new file mode 100644
index 0000000..72624e2
--- /dev/null
+++ b/lobby/global/EventLogger.js
@@ -0,0 +1,22 @@
+"use strict";
+const createLogger = require('logging').default;
+const logger = createLogger('Event');
+
+const logEvent = (eventName, client, version, additionalData) => {
+ const timestamp = new Date().toISOString();
+ const eventDesc = Object.assign(
+ {
+ eventName: eventName,
+ timestamp: timestamp,
+ user: client.userId,
+ version: version || client.version,
+ game: client.game,
+ },
+ additionalData,
+ );
+ logger.info(JSON.stringify(eventDesc));
+};
+
+module.exports = {
+ logEvent: logEvent
+};
diff --git a/lobby/global/Stats.js b/lobby/global/Stats.js
new file mode 100644
index 0000000..eaf2f36
--- /dev/null
+++ b/lobby/global/Stats.js
@@ -0,0 +1,35 @@
+"use strict";
+
+const ResultsMappers = {
+ // These take an array that the game sends to `/game_results`
+ // and return an ongoing results object that can be stored in redis
+ "baseball": (resultsFields, isHome, opponentId) => {
+ const ongoingResults = {
+ winning: resultsFields[0],
+ runs: resultsFields[1],
+ atBats: resultsFields[2],
+ hits: resultsFields[3],
+ homeRuns: resultsFields[4],
+ longestHomeRun: resultsFields[5],
+ singles: resultsFields[6],
+ doubles: resultsFields[7],
+ triples: resultsFields[8],
+ steals: resultsFields[9],
+ strikeouts: resultsFields[10],
+ walks: resultsFields[11],
+ disconnect: resultsFields[12],
+ completedInnings: resultsFields[13],
+ isHome: isHome,
+ opponentId: opponentId,
+ };
+ return ongoingResults;
+ },
+ // TODO: Football
+ "football": (resultsFields, isHome, opponentId) => {
+ return {"not_yet_supported": 1}
+ }
+};
+
+module.exports = {
+ ResultsMappers: ResultsMappers,
+};
\ No newline at end of file
diff --git a/lobby/net/AreaMessages.js b/lobby/net/AreaMessages.js
new file mode 100644
index 0000000..754265f
--- /dev/null
+++ b/lobby/net/AreaMessages.js
@@ -0,0 +1,135 @@
+"use strict";
+const createLogger = require('logging').default;
+const logger = createLogger('AreaMessages');
+
+const Areas = require('../global/Areas.js');
+const logEvent = require('../global/EventLogger.js').logEvent;
+
+server.handleMessage('get_population', async (client, args) => {
+ const areaId = args.area;
+ if (areaId === undefined) {
+ logger.warn('Got get_population message without area id! Ignoring.');
+ return;
+ }
+
+ let population = 0;
+ if (areaId in Areas.Groups) {
+ for (const area of Areas.Groups[areaId]) {
+ const users = await redis.getUserIdsInArea(area, client.game);
+ population += users.length;
+ }
+ } else {
+ population = (await redis.getUserIdsInArea(areaId, client.game)).length;
+ }
+ client.send('population_resp', {area: areaId, population: population});
+
+});
+
+server.handleMessage('enter_area', async (client, args) => {
+ const areaId = args.area;
+ logEvent('enter_area', client, args.version, {'area': areaId});
+ if (areaId === undefined) {
+ logger.warn('Got enter_area message without area id! Ignoring.');
+ return;
+ }
+ if (areaId == 33) {
+ // HACK
+ return;
+ }
+ client.areaId = areaId;
+ await redis.addUserToArea(client.userId, areaId, client.game);
+});
+
+server.handleMessage('leave_area', async (client, args) => {
+ logEvent('leave_area', client, args.version, {'area': client.areaId});
+ if (!client.areaId) {
+ // this.logger.error("Got leave_area without being in an area!");
+ return;
+ }
+ const oldAreaId = client.areaId;
+ client.areaId = 0;
+ client.sliceStart = 0;
+ client.sliceEnd = 0;
+ await redis.removeUserFromArea(client.userId, oldAreaId, client.game);
+});
+
+server.handleMessage('get_players', async (client, args) => {
+ const start = args.start;
+ const end = args.end + 1;
+
+ if (!client.areaId) {
+ logger.warn("Got get_players without being in an area!");
+ return;
+ }
+
+ client.sliceStart = start;
+ client.sliceEnd = end;
+
+ const users = await redis.getUsersInArea(client.areaId, client.game);
+
+ const players = [];
+ for (const user of users) {
+ if (user.id == client.userId) {
+ // Don't add ourselves in.
+ continue;
+ }
+ players.push([user.user, user.id, user.icon, user.stats[0], user.stats[1], user.stats[2], user.phone, user.opponent]);
+ }
+ client.send('players_list', {players: players.slice(client.sliceStart, client.sliceEnd)});
+
+});
+
+process.on('update_players_list', (args) => {
+ const areaId = args.area;
+ const game = args.game;
+ const users = args.users;
+
+ for (const client of server.connections) {
+ if (client.areaId == areaId && client.game == game) {
+ const players = [];
+ for (const user of users) {
+ if (user.id == client.userId) {
+ // Don't add ourselves in.
+ continue;
+ }
+ players.push([user.user, user.id, user.icon, user.stats[0], user.stats[1], user.stats[2], user.phone, user.opponent]);
+ }
+ client.send('players_list', {players: players.slice(client.sliceStart, client.sliceEnd)});
+ }
+ }
+});
+
+server.handleMessage('game_started', async (client, args) => {
+ const playerId = args.user;
+ logEvent('game_started', client, args.version, {'area': client.areaId, 'opponent': playerId});
+
+ await redis.setInGame(client.userId, 1);
+ await redis.setInGame(playerId, 1);
+
+ await redis.sendUsersInArea(client.areaId, client.game);
+ await redis.sendGamesPlayingInArea(client.areaId, client.game);
+
+ await redis.removeOngoingResults(client.userId, client.game); // Just in case ongoing results didn't get removed after a previous game
+ await redis.removeOngoingResults(playerId, client.game);
+});
+
+server.handleMessage('game_finished', async (client, args) => {
+ logEvent('game_finished', client, args.version, {'area': client.areaId, 'opponent': client.opponentId});
+ await redis.setInGame(client.userId, 0);
+ await redis.sendGamesPlayingInArea(client.areaId, client.game);
+
+ await redis.removeOngoingResults(client.opponentId, client.game);
+ await redis.removeOngoingResults(client.userId, client.game);
+});
+
+process.on('update_games_playing', async (args) => {
+ const areaId = args.area;
+ const game = args.game;
+ const gamesPlaying = args.games;
+
+ for (const client of server.connections) {
+ if (client.areaId == areaId && client.game == game) {
+ client.send('games_playing', {games: gamesPlaying});
+ }
+ }
+});
diff --git a/lobby/net/ChallengeMessages.js b/lobby/net/ChallengeMessages.js
new file mode 100644
index 0000000..2f156d8
--- /dev/null
+++ b/lobby/net/ChallengeMessages.js
@@ -0,0 +1,375 @@
+"use strict";
+const createLogger = require('logging').default;
+const logger = createLogger('ChallengeMessages');
+
+const logEvent = require('../global/EventLogger.js').logEvent;
+
+// Hack for baseball
+const busyTimeouts = {};
+
+server.handleMessage('set_phone_status', async (client, args) => {
+ const status = args.status;
+ if (status === undefined) {
+ return;
+ }
+ if (!client.areaId) {
+ logger.warn('Attempted to set phone status without being in an area.');
+ return;
+ }
+
+ await redis.setPhoneStatus(client.userId, client.areaId, client.game, status);
+});
+
+server.handleMessage('challenge_player', async (client, args) => {
+ const challengeUserId = args.user;
+ const stadium = args.stadium;
+ logEvent('challenge_player', client, args.version, {'area': client.areaId, 'opponent': challengeUserId, 'stadium': stadium});
+
+ if (challengeUserId === undefined) {
+ logger.error("Missing user argument on challenge_player!");
+ return;
+ } else if (stadium === undefined) {
+ logger.error("Missing stadium argument for challenge_player!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got challenge_player but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(challengeUserId)) {
+ logger.error(`Got challenge_player but our player (${challengeUserId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ if (client.userId in busyTimeouts) {
+ clearTimeout(busyTimeouts[client.userId]);
+ }
+
+ process.send({cmd: "receive_challenge", user: challengeUserId,
+ opponent: client.userId,
+ stadium: stadium});
+});
+
+process.on('receive_challenge', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+ const stadium = args.stadium;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got receive_challenge from server but I'm (${client.userId}) not in an area!`);
+ return;
+ } else if (stadium === undefined) {
+ logger.error("Missing stadium argument for receive_challenge!");
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got receive_challenge but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ const userData = await redis.getUserById(opponentId);
+ if (Object.keys(userData).length == 0) {
+ logger.error(`Got receive_challenge but our player (${opponentId}) doesn't exist!`);
+ return;
+ }
+ client.send("receive_challenge", {user: opponentId, stadium: stadium, name: userData.user.toUpperCase()});
+ return;
+ }
+ }
+});
+
+server.handleMessage('challenge_timeout', async (client, args) => {
+ const challengeUserId = args.user;
+ logEvent('challenge_timeout', client, args.version, {'area': client.areaId, 'opponent': challengeUserId});
+ if (challengeUserId === undefined) {
+ logger.error("Missing user argument on challenge_timeout!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got challenge_timeout but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(challengeUserId)) {
+ logger.error(`Got challenge_timeout but our player (${challengeUserId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ process.send({cmd: "challenge_timeout", user: challengeUserId,
+ opponent: client.userId});
+});
+
+process.on('challenge_timeout', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got challenge_timeout from server but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got challenge_timeout but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+ client.send("decline_challenge", {not_responding: 1});
+ return;
+ }
+ }
+});
+
+server.handleMessage('receiver_busy', async (client,args) => {
+ const userId = args.user;
+
+ if (userId === undefined) {
+ logger.error("Missing user argument on receiver_busy!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got receiver_busy but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(userId)) {
+ logger.error(`Got receiver_busy but our player (${userId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ process.send({cmd: 'receiver_busy', user: userId,
+ opponent: client.userId});
+});
+
+process.on('receiver_busy', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got receiver_busy but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got receiver_busy but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.send("receiver_busy");
+
+ // HACK: In baseball, the game does not automatically hang up
+ // the phone and return to the users list. We have to send a
+ // decline_challenge message to get back there.
+ // TODO: Possibly make this a client-sided hack?
+ if (client.game == "baseball") {
+ busyTimeouts[client.userId] = setTimeout((client) => {
+ client.send("decline_challenge", {not_responding: 1});
+ }, 7000, client);
+ }
+ return;
+ }
+ }
+});
+
+server.handleMessage('considering_challenge', async (client, args) => {
+ const userId = args.user;
+
+ if (userId === undefined) {
+ logger.error("Missing user argument on considering_challenge!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got considering_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(userId)) {
+ logger.error(`Got considering_challenge but our player (${userId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.opponentId = userId;
+ process.send({cmd: 'considering_challenge', user: userId,
+ opponent: client.userId});
+});
+
+process.on('considering_challenge', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got considering_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got considering_challenge but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.opponentId = opponentId;
+ client.send("considering_challenge");
+ return;
+ }
+ }
+});
+
+server.handleMessage('counter_challenge', async (client, args) => {
+ const stadium = args.stadium;
+ logEvent('counter_challenge', client, args.version, {'area': client.areaId, 'opponent': client.opponentId, 'stadium': stadium});
+
+ if (stadium === undefined) {
+ logger.error("Got counter_challenge but stadium is not defined!");
+ return;
+ }
+ else if (client.areaId == 0) {
+ logger.error(`Got counter_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(client.opponentId)) {
+ logger.error(`Got counter_challenge but our player (${client.opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ process.send({cmd: 'counter_challenge', user: client.opponentId,
+ stadium: stadium});
+});
+
+process.on('counter_challenge', async (args) => {
+ const userId = args.user;
+ const stadium = args.stadium;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got considering_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(client.opponentId)) {
+ logger.error(`Got considering_challenge but our player (${client.opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+ client.send('counter_challenge', {stadium: stadium});
+ return;
+ }
+ }
+});
+
+server.handleMessage('decline_challenge', async (client, args) => {
+ const challengeUserId = args.user;
+ logEvent('decline_challenge', client, args.version, {'area': client.areaId, 'opponent': challengeUserId});
+
+ if (challengeUserId === undefined) {
+ logger.error("Missing user argument on decline_challenge!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got decline_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(challengeUserId)) {
+ logger.error(`Got decline_challenge but our player (${challengeUserId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.opponentId = 0;
+ process.send({cmd: 'decline_challenge', user: challengeUserId,
+ opponent: client.userId,
+ not_responding: 0});
+});
+
+process.on('decline_challenge', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+ const notResponding = args.not_responding;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got decline_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got decline_challenge but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.opponentId = 0;
+ client.send("decline_challenge", {not_responding: notResponding});
+ return;
+ }
+ }
+});
+
+server.handleMessage('accept_challenge', async (client, args) => {
+ const challengeUserId = args.user;
+ logEvent('accept_challenge', client, args.version, {'area': client.areaId, 'opponent': challengeUserId});
+
+ if (challengeUserId === undefined) {
+ logger.error("Missing user argument on accept_challenge!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got accept_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(challengeUserId)) {
+ logger.error(`Got accept_challenge but our player (${challengeUserId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.opponentId = challengeUserId;
+ process.send({cmd: 'accept_challenge', user: challengeUserId,
+ opponent: client.userId});
+});
+
+process.on('accept_challenge', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got accept_challenge but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got accept_challenge but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.opponentId = opponentId;
+ client.send("accept_challenge");
+ return;
+ }
+ }
+});
diff --git a/lobby/net/DatabaseMessages.js b/lobby/net/DatabaseMessages.js
new file mode 100644
index 0000000..b55e7aa
--- /dev/null
+++ b/lobby/net/DatabaseMessages.js
@@ -0,0 +1,177 @@
+"use strict";
+const createLogger = require('logging').default;
+const logger = createLogger('DatabaseMessages');
+
+const Areas = require('../global/Areas.js');
+const Stats = require('../global/Stats.js');
+const logEvent = require('../global/EventLogger.js').logEvent;
+
+server.handleMessage("login", async (client, args) => {
+ const username = args.user;
+ const password = args.pass;
+ const game = args.game;
+ const version = args.version;
+
+ if (username === undefined) {
+ client.kick("Missing username parameter!");
+ return;
+ } else if (password === undefined) {
+ client.kick("Missing password parameter!");
+ return;
+ } else if (game === undefined) {
+ client.kick("Missing game parameter!");
+ return;
+ } else if (version == undefined) {
+ client.kick("Missing version paremeter!");
+ return;
+ }
+
+ const games = ["football", "baseball"];
+ if (!games.includes(game)) {
+ client.kick("Game not supported.");
+ return;
+ }
+ client.game = game;
+ client.version = version;
+
+ const user = await database.getUser(username, password, game);
+ logEvent('login', client, args.version, {'user': user.id, 'username': user.user, 'game': game});
+ if (user.error) {
+ client.send("login_resp", {error_code: user.error,
+ id: 0,
+ response: user.message});
+ return;
+ }
+
+ // Kick the other clients out if they're logged in
+ // as the same user.
+ process.send({cmd: 'kick',
+ userId: user.id,
+ type: 901,
+ reason: "You have been disconnected because someone else just logged in using your account on another computer."});
+
+ // We finish setting up the login details at the end of the event loop
+ // to prevent ourselves from getting kicked out after logging in.
+ setTimeout(() => {
+ client.userId = user.id;
+ client.send("login_resp", {error_code: 0,
+ id: user.id,
+ response: "All ok"});
+
+ }, 50);
+});
+
+server.handleMessage('get_profile', async (client, args) => {
+ let userId = args.user_id;
+ if (userId === undefined) {
+ // Must be self.
+ userId = client.userId;
+ }
+ const user = await redis.getUserById(userId, client.game);
+ if (Object.keys(user).length == 0)
+ return;
+
+ const profile = [user.icon].concat(user.stats);
+ client.send("profile_info", {profile: profile});
+});
+
+server.handleMessage('set_icon', async (client, args) => {
+ const icon = args.icon;
+ logEvent('set_icon', client, args.version, {'icon': icon});
+
+ if (client.userId == 0) {
+ client.kick("Attempting to set icon without logging in first.");
+ return;
+ } else if (icon === undefined) {
+ logger.warn("Got set_icon with missing icon! Ignoring.");
+ return;
+ }
+
+ await database.setIcon(client.userId, icon);
+});
+
+server.handleMessage('locate_player', async (client, args) => {
+ const username = args.user;
+ if (client.userId == 0) {
+ client.kick("Attempting to locate player without logging in first.");
+ return;
+ } else if (username == undefined) {
+ logger.warn("Got locate_user without username set. Ignoring.");
+ return;
+ }
+
+ const response = {code: 0,
+ areaId: 0,
+ area: ""};
+
+ const user = await redis.getUserByName(username);
+ if (!user || !user.game || user.game != client.game) {
+ // Player not logged in or in the different game
+ client.send("locate_resp", response);
+ return
+ }
+
+ if (!user.area) {
+ // Logged in but not in an area.
+ response.code = 4;
+ } else if (user.inGame) {
+ response.code = 2;
+ } else {
+ response.code = 1;
+ response.areaId = user.area;
+ response.area = Areas.Areas[user.area] || `Unknown area (${user.area})`;
+ }
+
+ client.send("locate_resp", response);
+});
+
+server.handleMessage("game_results", async (client, args) => {
+ const resultsUserId = args.user;
+ const reportingUserId = client.userId;
+ let isHome;
+ let opponentId;
+ // The home team always reports the game results, so we can use that
+ // to tell whether the results are for the home or away team.
+ // TODO: Verify that this is true for football (it is for baseball)
+ if (reportingUserId == resultsUserId) {
+ isHome = 1;
+ opponentId = client.opponentId;
+ } else {
+ isHome = 0;
+ opponentId = client.userId;
+ }
+ const resultsFields = args.fields;
+ const ongoingResults = Stats.ResultsMappers[client.game](
+ resultsFields, isHome, opponentId
+ );
+ logEvent('game_results', client, args.version, {'results': ongoingResults, 'rawResults': resultsFields});
+
+ await redis.setOngoingResults(resultsUserId, client.game, ongoingResults);
+});
+
+server.handleMessage('get_teams', async (client, args) => {
+ const userId = client.userId;
+ const opponentId = args.opponent_id;
+
+ const game = client.game;
+
+ const userTeamResponse = await database.getTeam(userId, game);
+ const opponentTeamResponse = await database.getTeam(opponentId, game);
+
+ logEvent('get_teams', client, args.version, {'userTeam': userTeamResponse, 'opponentTeam': opponentTeamResponse});
+
+ let userMessages = [];
+ if (userTeamResponse.error) {
+ userMessages.push("User: " + userTeamResponse.message);
+ }
+ if (opponentTeamResponse.error) {
+ userMessages.push("Opponent: " + opponentTeamResponse.message);
+ }
+ if (userMessages.length > 0) {
+ const errorMessage = userMessages.join(" ");
+ client.send("teams", {error: 1, message: errorMessage, user: [], opponent: []});
+ } else {
+ const teams = {error: 0, message: "", user: userTeamResponse.team, opponent: opponentTeamResponse.team};
+ client.send("teams", teams);
+ }
+});
diff --git a/lobby/net/NetworkConnection.js b/lobby/net/NetworkConnection.js
new file mode 100644
index 0000000..30d6632
--- /dev/null
+++ b/lobby/net/NetworkConnection.js
@@ -0,0 +1,97 @@
+"use strict";
+const createLogger = require('logging').default;
+
+class NetworkConnection {
+ constructor(socket) {
+ this.socket = socket;
+ this.logger = createLogger(socket.remoteAddress + ':' + socket.remotePort)
+ this.terminated = false;
+ this.buffer = '';
+
+ this.userId = 0;
+ this.game = '';
+ this.version = '';
+
+ this.areaId = 0;
+
+ this.sliceStart = 0;
+ this.sliceEnd = 0;
+
+ this.opponentId = 0;
+
+ this.receivedHeartbeat = true;
+ this.heartbeatTimer = setTimeout(() => {
+ if (this.terminated) return;
+ if (this.receivedHeartbeat) {
+ this.send("heartbeat");
+ this.receivedHeartbeat = false;
+ this.heartbeatTimer.refresh();
+ } else {
+ this.kick(1, "Heartbeat timeout.");
+ }
+ }, 30 * 1000);
+
+ this.socket.on('close', (hadError) => {
+ if (this.terminated) return;
+ this.logger.debug("Connection closed");
+ server.handleDisconnect(this, true);
+ });
+
+ this.socket.on('error', (error) => {
+ if (this.terminated) return;
+ this.logger.error(`Error on connection ${this.socket.remoteAddress}:${this.socket.remotePort}.`, error);
+ // 'close' event emits after.
+ });
+
+ this.socket.on('data', (data) => {
+ if (this.terminated) return;
+ this.buffer += data;
+ if (this.buffer.includes('\n')) {
+ const messages = this.buffer.split('\n');
+ // Reset the buffer
+ this.buffer = messages.pop();
+
+ for (const message of messages) {
+ let json;
+ try {
+ json = JSON.parse(message)
+ } catch (e) {
+ this.logger.error("Received trunciated data!")
+ continue;
+ }
+
+ const command = json.cmd;
+ if (!command) {
+ this.logger.warn("Received data without command value!")
+ continue;
+ }
+ delete json.cmd;
+ this.logger.debug(`Received "${command}" message:`, json);
+
+ if (command in server.messages) {
+ server.messages[command](this, json);
+ } else {
+ this.logger.error("Got unknown message:", {command});
+ }
+ }
+ }
+ });
+ }
+
+ send(command, object = {}) {
+ if (this.terminated) return;
+ object.cmd = command;
+ const json = JSON.stringify(object);
+ this.logger.debug("Writing to connection:", json);
+ this.socket.write(json + "\n");
+ }
+
+ kick(type, reason) {
+ if (this.terminated) return;
+ this.logger.warn("Kicking:", reason);
+ this.send("disconnect", {"type": type, "message": reason});
+ server.handleDisconnect(this);
+ }
+}
+
+module.exports = NetworkConnection;
diff --git a/lobby/net/NetworkListener.js b/lobby/net/NetworkListener.js
new file mode 100644
index 0000000..f019365
--- /dev/null
+++ b/lobby/net/NetworkListener.js
@@ -0,0 +1,113 @@
+"use strict";
+const createLogger = require('logging').default;
+const net = require('net');
+const tls = require('tls');
+const fs = require('fs');
+
+const NetworkConnection = require('./NetworkConnection');
+
+class NetworkListener {
+ constructor(config) {
+ this.logger = createLogger('NetworkListener');
+ this.config = config;
+ this.connections = new Set();
+
+ // message: function
+ // Add new messages with server.handleMessage.
+ this.messages = {
+ 'heartbeat': (client, args) => {
+ client.receivedHeartbeat = true;
+ client.heartbeatTimer.refresh();
+ },
+ 'disconnect': async (client, args) => {
+ await this.handleDisconnect(client);
+ }
+ };
+
+ const host = this.config['host'];
+ const port = Number(this.config['port']);
+ const keyPath = this.config['key'];
+ const certPath = this.config['cert'];
+
+ if (keyPath && certPath) {
+ const readCerts = (keyPath, certPath) => {
+ return {
+ key: fs.readFileSync(keyPath),
+ cert: fs.readFileSync(certPath)
+ }
+ }
+ this.server = tls.createServer(readCerts(keyPath, certPath), (socket) => {
+ this.logger.debug('Got incoming connection from ' + socket.remoteAddress + ':' + socket.remotePort);
+ socket.setEncoding('utf-8');
+ this.connections.add(new NetworkConnection(socket))
+ });
+
+ // Watch the cert path for updates, if it does, re-read the latest
+ // certs, active connections will not be interrupted.
+ let certUpdateTimeout;
+ fs.watch(keyPath, () => {
+ this.logger.info("TLS Certificates updated, Reading in 1 second...");
+ clearTimeout(certUpdateTimeout);
+ certUpdateTimeout = setTimeout(() => {
+ this.server.setSecureContext(readCerts(keyPath, certPath));
+ this.logger.info("Updated secure context.");
+ }, 1000);
+ });
+
+ this.server.listen(port, host, () => {
+ this.logger.info('Now listening for TLS connections on ' + host + ':' + port);
+ });
+ } else {
+ this.logger.warn("Creating raw TCP server, DO NOT USE THIS IN PRODUCTION!!!")
+ this.server = net.createServer((socket) => {
+ this.logger.debug('Got incoming connection from ' + socket.remoteAddress + ':' + socket.remotePort);
+ socket.setEncoding('utf-8');
+ this.connections.add(new NetworkConnection(socket))
+ });
+
+ this.server.listen(port, host, () => {
+ this.logger.info('Now listening for TCP connections on ' + host + ':' + port);
+ });
+ }
+
+ process.on("kick", (args) => {
+ const userId = args.userId;
+ const type = args.type;
+ const reason = args.reason;
+ if (!userId) {
+ this.logger.warn("Received kick message without user id! Ignoring.");
+ return;
+ }
+ for (const client of this.connections) {
+ if (client.userId == userId) {
+ client.kick(type, reason);
+ break;
+ }
+ }
+ });
+
+ }
+
+ handleMessage(message, func) {
+ this.messages[message] = func;
+ }
+
+ async handleDisconnect(client, lost = false) {
+ if (client.terminated) return;
+ client.terminated = true;
+ clearTimeout(client.heartbeatTimer);
+
+ if (client.userId && client.areaId) {
+ await redis.removeUserFromArea(client.userId, client.areaId, client.game);
+ }
+ if (client.userId) {
+ await redis.removeUser(client.userId, client.game);
+ }
+
+ this.connections.delete(client);
+ client.socket.end();
+ }
+
+}
+
+module.exports = NetworkListener;
diff --git a/lobby/net/SessionMessages.js b/lobby/net/SessionMessages.js
new file mode 100644
index 0000000..ccd5b74
--- /dev/null
+++ b/lobby/net/SessionMessages.js
@@ -0,0 +1,103 @@
+"use strict";
+const createLogger = require('logging').default;
+const logger = createLogger('SessionMessages');
+
+const logEvent = require('../global/EventLogger.js').logEvent;
+
+server.handleMessage('send_session', async (client, args) => {
+ const userId = args.user;
+ const sessionId = args.session;
+ logEvent('send_session', client, args.version, {'session': sessionId, 'opponent': userId});
+
+ if (userId === undefined) {
+ logger.error("Missing user argument on send_session!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got send_session but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(userId)) {
+ logger.error(`Got send_session but our player (${userId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ process.send({cmd: 'game_session', user: userId,
+ opponent: client.userId,
+ session: sessionId});
+});
+
+process.on('game_session', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+ const sessionId = args.session;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got game_session but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got game_session but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.send("game_session", {session: sessionId});
+ return;
+ }
+ }
+});
+
+server.handleMessage('send_relay', async (client, args) => {
+ const userId = args.user;
+ const relayId = args.relay;
+ logEvent('send_relay', client, args.version, {'relay': relayId, 'opponent': userId});
+
+ if (userId === undefined) {
+ logger.error("Missing user argument on send_relay!");
+ return;
+ } else if (client.areaId == 0) {
+ logger.error(`Got send_relay but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(userId)) {
+ logger.error(`Got send_relay but our player (${userId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ process.send({cmd: 'game_relay', user: userId,
+ opponent: client.userId,
+ relay: relayId});
+});
+
+process.on('game_relay', async (args) => {
+ const userId = args.user;
+ const opponentId = args.opponent;
+ const relayId = args.relay;
+
+ for (const client of server.connections) {
+ if (client.userId == userId) {
+ if (client.areaId == 0) {
+ logger.error(`Got game_session but I'm (${client.userId}) not in an area!`);
+ return;
+ }
+ // Check if the opponent is in our area.
+ const users = await redis.getUserIdsInArea(client.areaId, client.game);
+ if (!users.includes(opponentId)) {
+ logger.error(`Got game_session but our player (${opponentId}) isn't in area (${client.areaId})!`);
+ return;
+ }
+
+ client.send("game_relay", {relay: relayId});
+ return;
+ }
+ }
+});
diff --git a/lobby/package-lock.json b/lobby/package-lock.json
new file mode 100644
index 0000000..f031d60
--- /dev/null
+++ b/lobby/package-lock.json
@@ -0,0 +1,1283 @@
+{
+ "name": "scummvm-multiplayer-lobby",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "scummvm-multiplayer-lobby",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "bent": "^7.3.12",
+ "ioredis": "^4.27.7",
+ "js-yaml": "^4.1.0",
+ "logging": "^3.3.0"
+ },
+ "optionalDependencies": {
+ "discord.js": "^13.1.0",
+ "table": "^6.8.0"
+ }
+ },
+ "node_modules/@discordjs/builders": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.5.0.tgz",
+ "integrity": "sha512-HP5y4Rqw68o61Qv4qM5tVmDbWi4mdTFftqIOGRo33SNPpLJ1Ga3KEIR2ibKofkmsoQhEpLmopD1AZDs3cKpHuw==",
+ "optional": true,
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.1",
+ "discord-api-types": "^0.22.0",
+ "ow": "^0.27.0",
+ "ts-mixer": "^6.0.0",
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=14.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/@discordjs/collection": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz",
+ "integrity": "sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog==",
+ "optional": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@discordjs/form-data": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz",
+ "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==",
+ "optional": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@sapphire/async-queue": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz",
+ "integrity": "sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA==",
+ "optional": true,
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
+ "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "16.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz",
+ "integrity": "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==",
+ "optional": true
+ },
+ "node_modules/@types/ws": {
+ "version": "7.4.7",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
+ "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz",
+ "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==",
+ "optional": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "optional": true
+ },
+ "node_modules/bent": {
+ "version": "7.3.12",
+ "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz",
+ "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==",
+ "dependencies": {
+ "bytesish": "^0.4.1",
+ "caseless": "~0.12.0",
+ "is-stream": "^2.0.0"
+ }
+ },
+ "node_modules/bytesish": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz",
+ "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ=="
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
+ "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "optional": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "optional": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/denque": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
+ "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/discord-api-types": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz",
+ "integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/discord.js": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.1.0.tgz",
+ "integrity": "sha512-gxO4CXKdHpqA+WKG+f5RNnd3srTDj5uFJHgOathksDE90YNq/Qijkd2WlMgTTMS6AJoEnHxI7G9eDQHCuZ+xDA==",
+ "optional": true,
+ "dependencies": {
+ "@discordjs/builders": "^0.5.0",
+ "@discordjs/collection": "^0.2.1",
+ "@discordjs/form-data": "^3.0.1",
+ "@sapphire/async-queue": "^1.1.4",
+ "@types/ws": "^7.4.7",
+ "discord-api-types": "^0.22.0",
+ "node-fetch": "^2.6.1",
+ "ws": "^7.5.1"
+ },
+ "engines": {
+ "node": ">=16.6.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/dot-prop": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
+ "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
+ "optional": true,
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "optional": true
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "optional": true
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ioredis": {
+ "version": "4.27.7",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.7.tgz",
+ "integrity": "sha512-lqvFFmUyGIHlrNyDvBoakzy1+ioJzNyoP6CP97GWtdTjWq9IOAnv6l0HUTsqhvd/z9etGgtrDHZ4kWCMAwNkug==",
+ "dependencies": {
+ "cluster-key-slot": "^1.1.0",
+ "debug": "^4.3.1",
+ "denque": "^1.1.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isarguments": "^3.1.0",
+ "p-map": "^2.1.0",
+ "redis-commands": "1.7.0",
+ "redis-errors": "^1.2.0",
+ "redis-parser": "^3.0.0",
+ "standard-as-callback": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "optional": true
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
+ },
+ "node_modules/lodash.isarguments": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+ "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+ "optional": true
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=",
+ "optional": true
+ },
+ "node_modules/logging": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/logging/-/logging-3.3.0.tgz",
+ "integrity": "sha512-Hnmu3KlGTbXMVS7ONjBpnjjiF9cBlK5qsmj77sOcqRkNpvO9ouUGPKe2PmBCWWYpKAbxb96b08cYEv4hiBk3lQ==",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "debug": "^4.3.1",
+ "nicely-format": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.49.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
+ "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.32",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
+ "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
+ "optional": true,
+ "dependencies": {
+ "mime-db": "1.49.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/nicely-format": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/nicely-format/-/nicely-format-1.1.0.tgz",
+ "integrity": "sha1-bDUT0fOAd9Ze2XIecWqOGRe6J7Y=",
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "esutils": "^2.0.2"
+ }
+ },
+ "node_modules/nicely-format/node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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==",
+ "optional": true,
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ow": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
+ "integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
+ "optional": true,
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.1",
+ "callsites": "^3.1.0",
+ "dot-prop": "^6.0.1",
+ "lodash.isequal": "^4.5.0",
+ "type-fest": "^1.2.1",
+ "vali-date": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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==",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/redis-commands": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
+ "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
+ },
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
+ "dependencies": {
+ "redis-errors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "optional": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "optional": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/table": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
+ "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==",
+ "optional": true,
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
+ "optional": true
+ },
+ "node_modules/ts-mixer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.0.tgz",
+ "integrity": "sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ==",
+ "optional": true
+ },
+ "node_modules/tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "optional": true
+ },
+ "node_modules/type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "optional": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vali-date": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
+ "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
+ "optional": true
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+ "optional": true,
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
+ "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
+ "optional": true,
+ "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": {
+ "@discordjs/builders": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.5.0.tgz",
+ "integrity": "sha512-HP5y4Rqw68o61Qv4qM5tVmDbWi4mdTFftqIOGRo33SNPpLJ1Ga3KEIR2ibKofkmsoQhEpLmopD1AZDs3cKpHuw==",
+ "optional": true,
+ "requires": {
+ "@sindresorhus/is": "^4.0.1",
+ "discord-api-types": "^0.22.0",
+ "ow": "^0.27.0",
+ "ts-mixer": "^6.0.0",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@discordjs/collection": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz",
+ "integrity": "sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog==",
+ "optional": true
+ },
+ "@discordjs/form-data": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz",
+ "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==",
+ "optional": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "@sapphire/async-queue": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz",
+ "integrity": "sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA==",
+ "optional": true
+ },
+ "@sindresorhus/is": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
+ "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==",
+ "optional": true
+ },
+ "@types/node": {
+ "version": "16.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz",
+ "integrity": "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==",
+ "optional": true
+ },
+ "@types/ws": {
+ "version": "7.4.7",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
+ "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
+ "optional": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "ajv": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz",
+ "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==",
+ "optional": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "optional": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "optional": true
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "optional": true
+ },
+ "bent": {
+ "version": "7.3.12",
+ "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz",
+ "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==",
+ "requires": {
+ "bytesish": "^0.4.1",
+ "caseless": "~0.12.0",
+ "is-stream": "^2.0.0"
+ }
+ },
+ "bytesish": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz",
+ "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ=="
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "optional": true
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "cluster-key-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
+ "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "optional": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "debug": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "optional": true
+ },
+ "denque": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
+ "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
+ },
+ "discord-api-types": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz",
+ "integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==",
+ "optional": true
+ },
+ "discord.js": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.1.0.tgz",
+ "integrity": "sha512-gxO4CXKdHpqA+WKG+f5RNnd3srTDj5uFJHgOathksDE90YNq/Qijkd2WlMgTTMS6AJoEnHxI7G9eDQHCuZ+xDA==",
+ "optional": true,
+ "requires": {
+ "@discordjs/builders": "^0.5.0",
+ "@discordjs/collection": "^0.2.1",
+ "@discordjs/form-data": "^3.0.1",
+ "@sapphire/async-queue": "^1.1.4",
+ "@types/ws": "^7.4.7",
+ "discord-api-types": "^0.22.0",
+ "node-fetch": "^2.6.1",
+ "ws": "^7.5.1"
+ }
+ },
+ "dot-prop": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
+ "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
+ "optional": true,
+ "requires": {
+ "is-obj": "^2.0.0"
+ }
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "optional": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "optional": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "ioredis": {
+ "version": "4.27.7",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.7.tgz",
+ "integrity": "sha512-lqvFFmUyGIHlrNyDvBoakzy1+ioJzNyoP6CP97GWtdTjWq9IOAnv6l0HUTsqhvd/z9etGgtrDHZ4kWCMAwNkug==",
+ "requires": {
+ "cluster-key-slot": "^1.1.0",
+ "debug": "^4.3.1",
+ "denque": "^1.1.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isarguments": "^3.1.0",
+ "p-map": "^2.1.0",
+ "redis-commands": "1.7.0",
+ "redis-errors": "^1.2.0",
+ "redis-parser": "^3.0.0",
+ "standard-as-callback": "^2.1.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "optional": true
+ },
+ "is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "optional": true
+ },
+ "is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "optional": true
+ },
+ "lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
+ },
+ "lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
+ },
+ "lodash.isarguments": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+ "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
+ },
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+ "optional": true
+ },
+ "lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=",
+ "optional": true
+ },
+ "logging": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/logging/-/logging-3.3.0.tgz",
+ "integrity": "sha512-Hnmu3KlGTbXMVS7ONjBpnjjiF9cBlK5qsmj77sOcqRkNpvO9ouUGPKe2PmBCWWYpKAbxb96b08cYEv4hiBk3lQ==",
+ "requires": {
+ "chalk": "^4.1.0",
+ "debug": "^4.3.1",
+ "nicely-format": "^1.1.0"
+ }
+ },
+ "mime-db": {
+ "version": "1.49.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
+ "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==",
+ "optional": true
+ },
+ "mime-types": {
+ "version": "2.1.32",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
+ "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
+ "optional": true,
+ "requires": {
+ "mime-db": "1.49.0"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "nicely-format": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/nicely-format/-/nicely-format-1.1.0.tgz",
+ "integrity": "sha1-bDUT0fOAd9Ze2XIecWqOGRe6J7Y=",
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "esutils": "^2.0.2"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+ }
+ }
+ },
+ "node-fetch": {
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "optional": true,
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
+ },
+ "ow": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
+ "integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
+ "optional": true,
+ "requires": {
+ "@sindresorhus/is": "^4.0.1",
+ "callsites": "^3.1.0",
+ "dot-prop": "^6.0.1",
+ "lodash.isequal": "^4.5.0",
+ "type-fest": "^1.2.1",
+ "vali-date": "^1.0.0"
+ }
+ },
+ "p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "optional": true
+ },
+ "redis-commands": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
+ "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
+ },
+ "redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
+ },
+ "redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
+ "requires": {
+ "redis-errors": "^1.0.0"
+ }
+ },
+ "require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "optional": true
+ },
+ "slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "optional": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ }
+ },
+ "standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "optional": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "table": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz",
+ "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==",
+ "optional": true,
+ "requires": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
+ "optional": true
+ },
+ "ts-mixer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.0.tgz",
+ "integrity": "sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ==",
+ "optional": true
+ },
+ "tslib": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+ "optional": true
+ },
+ "type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+ "optional": true
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "optional": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "vali-date": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
+ "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
+ "optional": true
+ },
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
+ "optional": true
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+ "optional": true,
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "ws": {
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
+ "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
+ "optional": true,
+ "requires": {}
+ }
+ }
+}
diff --git a/lobby/package.json b/lobby/package.json
new file mode 100644
index 0000000..d168f7b
--- /dev/null
+++ b/lobby/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "scummvm-multiplayer-lobby",
+ "version": "1.0.0",
+ "description": "Lobby server code for ScummVM Multiplayer",
+ "main": "run.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/scummvm/scummvm-sites.git"
+ },
+ "author": "ScummVM (Backyard Sports Online)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/scummvm/scummvm-sites/issues"
+ },
+ "homepage": "https://github.com/scummvm/scummvm-sites#readme",
+ "dependencies": {
+ "bent": "^7.3.12",
+ "ioredis": "^4.27.7",
+ "js-yaml": "^4.1.0",
+ "logging": "^3.3.0"
+ },
+ "optionalDependencies": {
+ "discord.js": "^13.1.0",
+ "table": "^6.8.0"
+ }
+}
diff --git a/lobby/run.js b/lobby/run.js
new file mode 100644
index 0000000..f47675e
--- /dev/null
+++ b/lobby/run.js
@@ -0,0 +1,72 @@
+"use strict"
+
+const yaml = require('js-yaml');
+const fs = require('fs');
+const createLogger = require('logging').default;
+const cluster = require('cluster');
+
+// Read the configuration files.
+const config = yaml.load(fs.readFileSync('config.yaml'));
+
+// Get credentials and such from environment variables
+const credentials = {
+ web: {
+ endpoint: process.env.WEB_ENDPOINT,
+ token: process.env.WEB_TOKEN,
+ },
+ redis: {
+ host: process.env.REDIS_HOST,
+ port: process.env.REDIS_PORT,
+ },
+ discord: {
+ client: process.env.DISCORD_CLIENT,
+ token: process.env.DISCORD_TOKEN,
+ channel: process.env.DISCORD_CHANNEL,
+ }
+};
+
+if (cluster.isMaster) {
+ // Fork out workers
+ for (let i = 0; i < (config['cores'] || 1); i++) {
+ const env = {};
+ if (i == 0) {
+ env.FIRST_WORKER = true;
+ }
+ const worker = cluster.fork(env);
+ worker.on('message', (message) => {
+ for (const id in cluster.workers) {
+ cluster.workers[id].send(message);
+ }
+ });
+ }
+} else {
+ // Worker
+ const NetworkListener = require('./net/NetworkListener');
+ const Redis = require('./database/Redis');
+ const Discord = require('./discord/Discord');
+ global.server = new NetworkListener(config['network'])
+ global.redis = new Redis(credentials.redis);
+ if (process.env.DATABASE == 'web') {
+ const WebAPI = require('./database/WebAPI');
+ global.database = new WebAPI(credentials.web);
+ } else {
+ global.database = global.redis;
+ }
+
+ // Load message functions
+ require('./net/DatabaseMessages.js');
+ require('./net/AreaMessages.js');
+ require('./net/ChallengeMessages.js');
+ require('./net/SessionMessages.js');
+
+ if (process.env.FIRST_WORKER && credentials.discord.client) {
+ global.discord = new Discord(credentials['discord']);
+ }
+
+ // Handle messages from other processes.
+ process.on('message', (message) => {
+ const cmd = message.cmd;
+ delete message.cmd;
+ process.emit(cmd, message);
+ });
+}
Commit: c375eb861ff29e7f18f121660c2ff7a0bfd75afb
https://github.com/scummvm/scummvm-sites/commit/c375eb861ff29e7f18f121660c2ff7a0bfd75afb
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: LOBBY: Send session server address.
Changed paths:
lobby/config.yaml
lobby/net/DatabaseMessages.js
lobby/net/NetworkListener.js
diff --git a/lobby/config.yaml b/lobby/config.yaml
index 1a3d393..c53de91 100644
--- a/lobby/config.yaml
+++ b/lobby/config.yaml
@@ -9,3 +9,7 @@ network:
# TLS connections, this MUST be set in production.
# key: /path/to/private_key.pem
# cert: /path/to/certificate.crt
+
+ # This sets the session server that the game will connect to
+ # to host and join their sessions.
+ session_server: "127.0.0.1:9120"
\ No newline at end of file
diff --git a/lobby/net/DatabaseMessages.js b/lobby/net/DatabaseMessages.js
index b55e7aa..7437ca5 100644
--- a/lobby/net/DatabaseMessages.js
+++ b/lobby/net/DatabaseMessages.js
@@ -54,8 +54,10 @@ server.handleMessage("login", async (client, args) => {
// to prevent ourselves from getting kicked out after logging in.
setTimeout(() => {
client.userId = user.id;
+ console.log(server.sessionServer);
client.send("login_resp", {error_code: 0,
id: user.id,
+ sessionServer: server.sessionServer,
response: "All ok"});
}, 50);
diff --git a/lobby/net/NetworkListener.js b/lobby/net/NetworkListener.js
index f019365..bd02a0e 100644
--- a/lobby/net/NetworkListener.js
+++ b/lobby/net/NetworkListener.js
@@ -24,6 +24,9 @@ class NetworkListener {
}
};
+ // Store session server address
+ this.sessionServer = this.config['session_server'] || '127.0.0.1:9120';
+
const host = this.config['host'];
const port = Number(this.config['port']);
const keyPath = this.config['key'];
Commit: ad9bbd832db278bfdd84e321ad95b57cc2ef4dd2
https://github.com/scummvm/scummvm-sites/commit/ad9bbd832db278bfdd84e321ad95b57cc2ef4dd2
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: LOBBY: Rename login endpoint.
Changed paths:
lobby/database/WebAPI.js
lobby/net/DatabaseMessages.js
diff --git a/lobby/database/WebAPI.js b/lobby/database/WebAPI.js
index 10553de..07fdd44 100644
--- a/lobby/database/WebAPI.js
+++ b/lobby/database/WebAPI.js
@@ -14,11 +14,10 @@ class WebAPI {
}
async getUser(username, password, game) {
- // TODO: Replace with /login
- const user = await this.post('/new_login', {token: this.token,
- user: username,
- pass: password,
- game: game});
+ const user = await this.post('/login', {token: this.token,
+ user: username,
+ pass: password,
+ game: game});
if (user.error) {
return user;
}
diff --git a/lobby/net/DatabaseMessages.js b/lobby/net/DatabaseMessages.js
index 7437ca5..1a388da 100644
--- a/lobby/net/DatabaseMessages.js
+++ b/lobby/net/DatabaseMessages.js
@@ -39,6 +39,7 @@ server.handleMessage("login", async (client, args) => {
if (user.error) {
client.send("login_resp", {error_code: user.error,
id: 0,
+ sessionServer: "",
response: user.message});
return;
}
@@ -54,7 +55,6 @@ server.handleMessage("login", async (client, args) => {
// to prevent ourselves from getting kicked out after logging in.
setTimeout(() => {
client.userId = user.id;
- console.log(server.sessionServer);
client.send("login_resp", {error_code: 0,
id: user.id,
sessionServer: server.sessionServer,
Commit: 08e75d131ae2e401b35c96280b4ef02682760c4e
https://github.com/scummvm/scummvm-sites/commit/08e75d131ae2e401b35c96280b4ef02682760c4e
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: LOBBY: Removed relay messages.
The client and the session server now does the relaying work.
Changed paths:
lobby/net/SessionMessages.js
diff --git a/lobby/net/SessionMessages.js b/lobby/net/SessionMessages.js
index ccd5b74..5142fe4 100644
--- a/lobby/net/SessionMessages.js
+++ b/lobby/net/SessionMessages.js
@@ -52,52 +52,3 @@ process.on('game_session', async (args) => {
}
}
});
-
-server.handleMessage('send_relay', async (client, args) => {
- const userId = args.user;
- const relayId = args.relay;
- logEvent('send_relay', client, args.version, {'relay': relayId, 'opponent': userId});
-
- if (userId === undefined) {
- logger.error("Missing user argument on send_relay!");
- return;
- } else if (client.areaId == 0) {
- logger.error(`Got send_relay but I'm (${client.userId}) not in an area!`);
- return;
- }
-
- // Check if the opponent is in our area.
- const users = await redis.getUserIdsInArea(client.areaId, client.game);
- if (!users.includes(userId)) {
- logger.error(`Got send_relay but our player (${userId}) isn't in area (${client.areaId})!`);
- return;
- }
-
- process.send({cmd: 'game_relay', user: userId,
- opponent: client.userId,
- relay: relayId});
-});
-
-process.on('game_relay', async (args) => {
- const userId = args.user;
- const opponentId = args.opponent;
- const relayId = args.relay;
-
- for (const client of server.connections) {
- if (client.userId == userId) {
- if (client.areaId == 0) {
- logger.error(`Got game_session but I'm (${client.userId}) not in an area!`);
- return;
- }
- // Check if the opponent is in our area.
- const users = await redis.getUserIdsInArea(client.areaId, client.game);
- if (!users.includes(opponentId)) {
- logger.error(`Got game_session but our player (${opponentId}) isn't in area (${client.areaId})!`);
- return;
- }
-
- client.send("game_relay", {relay: relayId});
- return;
- }
- }
-});
Commit: da987f4117df9970813ebc03da2b286cd20fd72d
https://github.com/scummvm/scummvm-sites/commit/da987f4117df9970813ebc03da2b286cd20fd72d
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Improve relay setup through Redis.
Changed paths:
main.py
diff --git a/main.py b/main.py
index 3e74cf4..e497d9d 100644
--- a/main.py
+++ b/main.py
@@ -45,7 +45,8 @@ for game in get_full_game_names():
# Clear out the sessions
logging.info(f"Clearing out {game} sessions")
for session_id in range(redis.llen(f"{game}:sessions")):
- redis.delete(f"{game}.session:{session_id}")
+ redis.delete(f"{game}:session:{session_id}")
+ redis.delete(f"{game}:relay:{session_id}")
redis.delete(f"{game}:sessions")
redis.delete(f"{game}:sessionByAddress")
@@ -90,23 +91,33 @@ def relay_data(data, sent_peer):
type_of_send = data.get("to")
send_type_param = data.get("toparam")
- session_id = relays.get(sent_peer)
+ session_id = int(redis.hget(f"relays:{str(sent_peer.address)}", "session"))
if not session_id:
return
- peers_by_user_id = session_to_relay_user_ids.get(session_id)
- if not peers_by_user_id:
- logging.warning(f"relay_data: Missing peers on session_to_relay_user_ids[{session_id}]!")
+ game = redis.hget(f"relays:{str(sent_peer.address)}", "game")
+ relay_users = redis.hgetall(f"{game}:relay:{session_id}")
+ if not relay_users:
+ logging.warning(f"relay_data: Missing users on {game}:relay:{session_id}!")
return
- user_id_by_peers = {v: k for k, v in peers_by_user_id.items()}
+ logging.debug(f"relay_data: Players of \"{game}\" session {session_id}:")
+ for user_id, address in relay_users.items():
+ logging.debug(f"relay_data: - {user_id}: {address}")
+
+ peers_by_user_id = {}
+ for user_id, address in relay_users.items():
+ peer = get_peer_by_address(address)
+ if not peer:
+ logging.warning(f"relay_data: Peer for {address} does not exist!")
+ continue
+ peers_by_user_id[int(user_id)] = peer
- logging.debug(f"relay_data: Players of session {session_id}:")
- for user_id, peer in peers_by_user_id.items():
- logging.debug(f"relay_data: - {user_id}: {str(peer.address)}")
+ user_id_by_peers = {v: k for k, v in peers_by_user_id.items()}
if user_id_by_peers.get(sent_peer) != 1:
- # To make things easier, just send all non-host data to the host.
+ # To make things easier, just send all non-host data to the host, so it can
+ # transfer data to peers that are connected directly to the host.
# It'll send it back to us if it actually needs to be relayed somewhere.
host_peer = peers_by_user_id.get(1)
if not host_peer:
@@ -151,25 +162,30 @@ def relay_data(data, sent_peer):
send(peer, data)
def remove_user_from_relay(peer):
- session_id = relays.get(peer)
+ session_id = redis.hget(f"relays:{str(peer.address)}", "session")
if not session_id:
return
- del relays[peer]
+ game = redis.hget(f"relays:{str(peer.address)}", "game")
+ redis.delete(f"relays:{str(peer.address)}")
- peers_by_user_id = session_to_relay_user_ids.get(session_id)
- if not peers_by_user_id:
+ address_by_user_id = redis.hgetall(f"{game}:relay:{session_id}")
+ if not address_by_user_id:
return
- user_id_by_peers = {v: k for k, v in peers_by_user_id.items()}
- user_id = user_id_by_peers.get(peer)
+ user_id_by_address = {v: k for k, v in address_by_user_id.items()}
+ user_id = user_id_by_address.get(str(peer.address))
if not user_id:
return
- del session_to_relay_user_ids[session_id][user_id]
+ redis.hdel(f"{game}:relay:{session_id}", user_id)
# Send the remove_user request to the host.
- host_peer = peers_by_user_id.get(1)
+ host_address = address_by_user_id.get(1)
+ if not host_address:
+ return
+
+ host_peer = get_peer_by_address(host_address)
if not host_peer:
return
@@ -185,9 +201,6 @@ signal.signal(signal.SIGTERM, exit)
# SIGINT: Ctrl+C KeyboardInterrupt
signal.signal(signal.SIGINT, exit)
-relays = {} # peer: sessionId
-session_to_relay_user_ids = {} # sessionId: {userId: peer}
-
while do_loop:
# Main event loop
event = host.service(1000)
@@ -204,8 +217,6 @@ while do_loop:
redis.hdel(f"{game}:sessionByAddress", str(event.peer.address))
# Cleanup Relays (if any):
- if session_id in session_to_relay_user_ids:
- del session_to_relay_user_ids[session_id]
remove_user_from_relay(event.peer)
@@ -279,11 +290,11 @@ while do_loop:
elif command == "join_session":
session_id = data.get("id")
- if not (redis.exists(f"{game}:session:{id}")):
+ if not (redis.exists(f"{game}:session:{session_id}")):
logging.warning(f"Session {game}:{session_id} not found")
continue
- address = redis.hget(f"{game}:session:{id}", "address")
+ address = redis.hget(f"{game}:session:{session_id}", "address")
peer = get_peer_by_address(address)
if not peer:
continue
@@ -293,20 +304,24 @@ while do_loop:
elif command == "start_relay":
session_id = data.get("session")
- if not (redis.exists(f"{game}:session:{id}")):
+ if not redis.exists(f"{game}:session:{session_id}"):
logging.warning(f"Session {game}:{session_id} not found")
continue
- address = redis.hget(f"{game}:session:{id}", "address")
+ # Get peer of the session host
+ address = redis.hget(f"{game}:session:{session_id}", "address")
peer = get_peer_by_address(address)
if not peer:
continue
+
+ if redis.exists(f"{game}:relay:{session_id}"):
+ logging.warning(f"Relay for {game}:{session_id} already exists!")
+ continue
- if session_id not in session_to_relay_user_ids:
- # The host peer is usually always has the userId of 1:
- session_to_relay_user_ids[session_id] = {1: peer}
- if peer not in relays:
- relays[peer] = session_id
+ # Store new relay with the host (which usually always has the
+ # userId of 1).
+ redis.hset(f"{game}:relay:{session_id}", 1, str(peer.address))
+ redis.hset(f"relays:{str(peer.address)}", mapping={"game": game, "session": session_id})
# Send the add_user request to the host (with joiner's address for context):
send(peer, {"cmd": "add_user_for_relay", "address": str(event.peer.address)})
@@ -318,18 +333,25 @@ while do_loop:
if not session_id:
logging.warning(f"Could not find session for address {str(event.peer.address)}!")
continue
- if session_id not in session_to_relay_user_ids:
- logging.warning(f"Session ID {session_id} not found in session2RelayUserIds!")
+
+ if not redis.exists(f"{game}:relay:{session_id}"):
+ logging.warning(f"{game}:relay:{session_id} does not exist!")
continue
- if user_id in session_to_relay_user_ids[session_id]:
- logging.warning(f"Duplicate user ID {user_id} in session2RelayUserIds[{session_id}]!")
+
+ if redis.hexists(f"{game}:relay:{session_id}", user_id):
+ logging.warning(f"Duplicate User ID {user_id} in {game}:relay:{session_id}!")
continue
+
peer = get_peer_by_address(address)
if not peer:
logging.warning(f"Could not find peer for address: {address}!")
continue
+
+ if redis.exists(f"relays:{str(peer.address)}"):
+ logging.warning(f"Peer {str(peer.address)} is already in a relay!")
+ continue
- session_to_relay_user_ids[session_id][user_id] = peer
- relays[peer] = session_id
+ redis.hset(f"{game}:relay:{session_id}", user_id, str(peer.address))
+ redis.hset(f"relays:{str(peer.address)}", mapping={"game": game, "session": session_id})
# Send the response back to the peer:
send(peer, {"cmd": "add_user_resp", "id": user_id})
Commit: b0c13bda75082d5c84a4f7481390c75946675fed
https://github.com/scummvm/scummvm-sites/commit/b0c13bda75082d5c84a4f7481390c75946675fed
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Accept and show max players.
Changed paths:
main.py
web/main.py
web/templates/game.html
diff --git a/main.py b/main.py
index e497d9d..7a6e6e2 100644
--- a/main.py
+++ b/main.py
@@ -71,12 +71,13 @@ def get_session_by_address(game: str, address: str):
return redis.hgetall(f"{game}:session:{session_id}")
return None
-def create_session(name: str, address: str):
+def create_session(name: str, maxplayers:int, address: str):
# Get our new session ID
session_id = redis.incr(f"{game}:counter")
# Create and store our new session
redis.hset(f"{game}:session:{session_id}",
- mapping={"name": name, "players": 0, "address": str(event.peer.address)})
+ mapping={"name": name, "players": 0, "maxplayers": maxplayers,
+ "address": str(event.peer.address)})
# Add session to sessions list
redis.rpush(f"{game}:sessions", session_id)
@@ -264,8 +265,9 @@ while do_loop:
if command == "host_session":
name = data.get("name")
+ maxplayers = data.get("maxplayers")
- session_id = create_session(name, event.peer.address)
+ session_id = create_session(name, maxplayers, event.peer.address)
send(event.peer, {"cmd": "host_session_resp", "id": session_id})
elif command == "update_players":
diff --git a/web/main.py b/web/main.py
index 79c261a..e3254ba 100644
--- a/web/main.py
+++ b/web/main.py
@@ -38,7 +38,8 @@ async def get_sessions(game: str, version: str = None):
for id in session_ids:
session = await redis.hgetall(f"{key}:session:{id}")
sessions.append({"id": int(id), "version": version, "name": session["name"],
- "players": int(session["players"]), "address": str(session["address"])})
+ "players": int(session["players"]), "maxplayers": int(session["maxplayers"]),
+ "address": str(session["address"])})
return sessions
@app.get('/{game}')
diff --git a/web/templates/game.html b/web/templates/game.html
index e349504..ae73cc3 100644
--- a/web/templates/game.html
+++ b/web/templates/game.html
@@ -20,8 +20,10 @@
<dl>
{% for session in sessions -%}
<dt>{{session["name"]}}</li>
- <dd>- Players: {{session["players"]}}/4</dd>
- <dd>- Version: {{session["version"]}}</dd>
+ <dd>- Players: {{session["players"]}}/{{session["maxplayers"]}}</dd>
+ {% if session["version"] -%}
+ <dd>- Version: {{session["version"]}}</dd>
+ {% endif -%}
{% endfor -%}
</dl>
{% endif -%}
Commit: 0eb4eebe281257fc2cad52754ddf0a371a5321fc
https://github.com/scummvm/scummvm-sites/commit/0eb4eebe281257fc2cad52754ddf0a371a5321fc
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
README: Update repo link
Changed paths:
README.md
diff --git a/README.md b/README.md
index b927000..2a85efc 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Both the session and web servers requires Redis to be installed. This is needed
Clone this repo and checkout to the multiplayer branch:
```
-git clone https://github.com/LittleToonCat/scummvm-sites.git
+git clone https://github.com/scummvm/scummvm-sites.git
git checkout multiplayer
```
@@ -60,4 +60,4 @@ docker-compose build
docker-compose up
```
-This will build Docker images for all three servers and starts a container for them simultaneously alongside with Redis.
\ No newline at end of file
+This will build Docker images for all three servers and starts a container for them simultaneously alongside with Redis.
Commit: 85b476f3468178c783d7ddc6fa1dada137884e9a
https://github.com/scummvm/scummvm-sites/commit/85b476f3468178c783d7ddc6fa1dada137884e9a
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Format Python code.
Changed paths:
main.py
net_defines.py
web/config.py
web/main.py
diff --git a/main.py b/main.py
index 7a6e6e2..f3fd45f 100644
--- a/main.py
+++ b/main.py
@@ -9,351 +9,404 @@ from net_defines import *
# Games to accept:
GAMES = [
- "football", # Backyard Football (1999)
- "baseball2001", # Backyard Baseball 2001
- "football2002", # Backyard Football 2002
- "moonbase", # Moonbase Commander (v1.0/v1.1/Demo)
+ "football", # Backyard Football (1999)
+ "baseball2001", # Backyard Baseball 2001
+ "football2002", # Backyard Football 2002
+ "moonbase", # Moonbase Commander (v1.0/v1.1/Demo)
]
# Version variants to accept for a specific game.
# If none exist but game exist in the GAMES list,
# that means there's only one game version.
-VERSIONS = {
- "moonbase": ["1.0", "1.1", "Demo"]
-}
-
-def get_full_game_names():
- games = []
- for game in GAMES:
- versions = VERSIONS.get(game)
- if versions:
- for version in versions:
- games.append(f"{game}:{version}")
- else:
- games.append(game)
- return games
-
-if os.environ.get("DEBUG"):
- logging.basicConfig(level=logging.DEBUG)
-
-redis = redis_package.Redis(os.environ.get("REDIS_HOST", "127.0.0.1"),
- retry_on_timeout=True, decode_responses=True)
-for game in get_full_game_names():
- # Reset session counter
- redis.set(f"{game}:counter", 0)
- if redis.exists(f"{game}:sessions"):
- # Clear out the sessions
- logging.info(f"Clearing out {game} sessions")
- for session_id in range(redis.llen(f"{game}:sessions")):
- redis.delete(f"{game}:session:{session_id}")
- redis.delete(f"{game}:relay:{session_id}")
- redis.delete(f"{game}:sessions")
- redis.delete(f"{game}:sessionByAddress")
-
-# Create our host to listen connections from. 4095 is the maxinum amount.
-host = enet.Host(enet.Address(b"0.0.0.0", 9120), peerCount=4095, channelLimit=1)
-print("Listening for messages in port 9120", flush=True)
-
-def send(peer, data: dict):
- logging.debug(f"{peer.address}: OUT: {data}")
- data = json.dumps(data, separators=(',', ':')).encode()
- peer.send(0, enet.Packet(data, enet.PACKET_FLAG_RELIABLE))
-
-def get_peer_by_address(address: str):
- for peer in host.peers:
- if str(peer.address) == address:
- return peer
- return None
-
-def get_session_by_address(game: str, address: str):
- session_id = redis.hget(f"{game}:sessionByAddress", str(address))
- if session_id:
- return redis.hgetall(f"{game}:session:{session_id}")
- return None
-
-def create_session(name: str, maxplayers:int, address: str):
- # Get our new session ID
- session_id = redis.incr(f"{game}:counter")
- # Create and store our new session
- redis.hset(f"{game}:session:{session_id}",
- mapping={"name": name, "players": 0, "maxplayers": maxplayers,
- "address": str(event.peer.address)})
- # Add session to sessions list
- redis.rpush(f"{game}:sessions", session_id)
-
- # Store to address to session hash
- redis.hset(f"{game}:sessionByAddress", str(address), session_id)
-
- logging.debug(f"{address}: NEW SESSION: \"{name}\"")
- return session_id
-
-def relay_data(data, sent_peer):
- from_user = data.get("from")
- type_of_send = data.get("to")
- send_type_param = data.get("toparam")
-
- session_id = int(redis.hget(f"relays:{str(sent_peer.address)}", "session"))
- if not session_id:
- return
-
- game = redis.hget(f"relays:{str(sent_peer.address)}", "game")
- relay_users = redis.hgetall(f"{game}:relay:{session_id}")
- if not relay_users:
- logging.warning(f"relay_data: Missing users on {game}:relay:{session_id}!")
- return
-
- logging.debug(f"relay_data: Players of \"{game}\" session {session_id}:")
- for user_id, address in relay_users.items():
- logging.debug(f"relay_data: - {user_id}: {address}")
-
- peers_by_user_id = {}
- for user_id, address in relay_users.items():
- peer = get_peer_by_address(address)
- if not peer:
- logging.warning(f"relay_data: Peer for {address} does not exist!")
- continue
- peers_by_user_id[int(user_id)] = peer
-
- user_id_by_peers = {v: k for k, v in peers_by_user_id.items()}
-
- if user_id_by_peers.get(sent_peer) != 1:
- # To make things easier, just send all non-host data to the host, so it can
- # transfer data to peers that are connected directly to the host.
- # It'll send it back to us if it actually needs to be relayed somewhere.
- host_peer = peers_by_user_id.get(1)
- if not host_peer:
- logging.warning("relay_data: Host user (1) is missing!")
- return
- logging.debug(f"relay_data: Relaying data from user {user_id_by_peers.get(sent_peer)} to host (1).")
- send(host_peer, data)
- return
-
- peers_to_send = set()
- if type_of_send == PN_SENDTYPE_INDIVIDUAL:
- peer = peers_by_user_id.get(send_type_param)
- if not peer:
- logging.warning(f"relay_data: user {send_type_param} not in relay, Host does not know, something might be wrong.")
- return
- logging.debug(f"relay_data: Relaying data to user {send_type_param}")
- peers_to_send.add(peer)
- elif type_of_send == PN_SENDTYPE_GROUP:
- logging.warning("STUB: PN_SENDTYPE_GROUP")
- return
- elif type_of_send == PN_SENDTYPE_HOST:
- # Chances are that the host is user_id 1.
- peer = peers_by_user_id.get(1)
- if not peer:
- return
- logging.debug(f"relay_data: Relaying data to host (user 1)")
- peers_to_send.add(peer)
- elif type_of_send == PN_SENDTYPE_ALL:
- # Send to all peers
- for peer in peers_by_user_id.values():
- peers_to_send.add(peer)
-
- logging.debug(f"relay_data: Relaying data to all peers: {str(list(peers_by_user_id.keys()))}")
- else:
- logging.warning(f"relay_data: Unknown type of send: {type_of_send}")
-
- # Remove self from set.
- if sent_peer in peers_to_send:
- peers_to_send.remove(sent_peer)
-
- for peer in peers_to_send:
- send(peer, data)
-
-def remove_user_from_relay(peer):
- session_id = redis.hget(f"relays:{str(peer.address)}", "session")
- if not session_id:
- return
-
- game = redis.hget(f"relays:{str(peer.address)}", "game")
- redis.delete(f"relays:{str(peer.address)}")
-
- address_by_user_id = redis.hgetall(f"{game}:relay:{session_id}")
- if not address_by_user_id:
- return
-
- user_id_by_address = {v: k for k, v in address_by_user_id.items()}
- user_id = user_id_by_address.get(str(peer.address))
- if not user_id:
- return
-
- redis.hdel(f"{game}:relay:{session_id}", user_id)
-
- # Send the remove_user request to the host.
- host_address = address_by_user_id.get(1)
- if not host_address:
- return
-
- host_peer = get_peer_by_address(host_address)
- if not host_peer:
- return
-
- send(host_peer, {"cmd": "remove_user", "id": user_id})
-
-do_loop = True
-def exit(*args):
- global do_loop
- do_loop = False
-
-# For Docker, they grace stop with SIGTERM
-signal.signal(signal.SIGTERM, exit)
-# SIGINT: Ctrl+C KeyboardInterrupt
-signal.signal(signal.SIGINT, exit)
-
-while do_loop:
- # Main event loop
- event = host.service(1000)
- if event.type == enet.EVENT_TYPE_CONNECT:
- logging.debug(f"{event.peer.address}: CONNECT")
- elif event.type == enet.EVENT_TYPE_DISCONNECT:
- logging.debug(f"{event.peer.address}: DISCONNECT")
- # Close out sessions relating to the address
- for game in get_full_game_names():
- session_id = redis.hget(f"{game}:sessionByAddress", str(event.peer.address))
- if session_id:
- redis.delete(f"{game}:session:{session_id}")
- redis.lrem(f"{game}:sessions", 0, session_id)
- redis.hdel(f"{game}:sessionByAddress", str(event.peer.address))
-
- # Cleanup Relays (if any):
- remove_user_from_relay(event.peer)
-
-
- elif event.type == enet.EVENT_TYPE_RECEIVE:
- logging.debug(f"{event.peer.address}: IN: {event.packet.data}")
- try:
- data = json.loads(event.packet.data)
- except:
- logging.warning(f"{event.peer.address}: Received non-JSON data.", event.packet.data.decode())
- continue
- command = data.get("cmd")
-
- if command == "game":
- relay_data(data, event.peer)
- continue
- elif command == "remove_user":
- remove_user_from_relay(event.peer)
- continue
-
- game = data.get("game")
- version = data.get("version")
- if not command:
- logging.warning(f"{event.peer.address}: Command missing")
- continue
- if not game:
- logging.warning(f"{event.peer.address}: Game missing")
- continue
-
- if game not in GAMES:
- logging.warning(f"Game \"{game}\" not supported.")
- continue
-
- versions = VERSIONS.get(game)
- if versions:
- if not version:
- logging.warning(f"{event.peer.address}: Version missing")
- continue
-
- if version not in version:
- logging.warning(f"Game \"{game}\" with version \"{version}\" not supported.")
- continue
-
- # Update the game to contain the version
- game = f"{game}:{version}"
-
- if command == "host_session":
- name = data.get("name")
- maxplayers = data.get("maxplayers")
-
- session_id = create_session(name, maxplayers, event.peer.address)
- send(event.peer, {"cmd": "host_session_resp", "id": session_id})
-
- elif command == "update_players":
- players = data.get("players")
-
- session_id = redis.hget(f"{game}:sessionByAddress", str(event.peer.address))
- if session_id:
- redis.hset(f"{game}:session:{session_id}", "players", players)
-
- elif command == "get_sessions":
- sessions = []
-
- num_sessions = redis.llen(f"{game}:sessions")
- session_ids = redis.lrange(f"{game}:sessions", 0, num_sessions)
- for id in session_ids:
- session = redis.hgetall(f"{game}:session:{id}")
- sessions.append({"id": int(id), "name": session["name"],
- "players": int(session["players"]), "address": str(session["address"])})
-
- send(event.peer, {"cmd": "get_sessions_resp",
- "address": str(event.peer.address), "sessions": sessions})
- elif command == "join_session":
- session_id = data.get("id")
-
- if not (redis.exists(f"{game}:session:{session_id}")):
- logging.warning(f"Session {game}:{session_id} not found")
- continue
-
- address = redis.hget(f"{game}:session:{session_id}", "address")
- peer = get_peer_by_address(address)
- if not peer:
- continue
-
- # Send the joiner's address to the hoster for hole-punching
- send(peer, {"cmd": "joining_session", "address": str(event.peer.address)})
- elif command == "start_relay":
- session_id = data.get("session")
-
- if not redis.exists(f"{game}:session:{session_id}"):
- logging.warning(f"Session {game}:{session_id} not found")
- continue
-
- # Get peer of the session host
- address = redis.hget(f"{game}:session:{session_id}", "address")
- peer = get_peer_by_address(address)
- if not peer:
- continue
-
- if redis.exists(f"{game}:relay:{session_id}"):
- logging.warning(f"Relay for {game}:{session_id} already exists!")
- continue
-
- # Store new relay with the host (which usually always has the
- # userId of 1).
- redis.hset(f"{game}:relay:{session_id}", 1, str(peer.address))
- redis.hset(f"relays:{str(peer.address)}", mapping={"game": game, "session": session_id})
-
- # Send the add_user request to the host (with joiner's address for context):
- send(peer, {"cmd": "add_user_for_relay", "address": str(event.peer.address)})
- elif command == "add_user_resp":
- address = data.get("address")
- user_id = data.get("id")
-
- session_id = int(redis.hget(f"{game}:sessionByAddress", str(event.peer.address)))
- if not session_id:
- logging.warning(f"Could not find session for address {str(event.peer.address)}!")
- continue
-
- if not redis.exists(f"{game}:relay:{session_id}"):
- logging.warning(f"{game}:relay:{session_id} does not exist!")
- continue
-
- if redis.hexists(f"{game}:relay:{session_id}", user_id):
- logging.warning(f"Duplicate User ID {user_id} in {game}:relay:{session_id}!")
- continue
-
- peer = get_peer_by_address(address)
- if not peer:
- logging.warning(f"Could not find peer for address: {address}!")
- continue
-
- if redis.exists(f"relays:{str(peer.address)}"):
- logging.warning(f"Peer {str(peer.address)} is already in a relay!")
- continue
-
- redis.hset(f"{game}:relay:{session_id}", user_id, str(peer.address))
- redis.hset(f"relays:{str(peer.address)}", mapping={"game": game, "session": session_id})
- # Send the response back to the peer:
- send(peer, {"cmd": "add_user_resp", "id": user_id})
+VERSIONS = {"moonbase": ["1.0", "1.1", "Demo"]}
+
+if __name__ == "__main__":
+
+ def get_full_game_names():
+ games = []
+ for game in GAMES:
+ versions = VERSIONS.get(game)
+ if versions:
+ for version in versions:
+ games.append(f"{game}:{version}")
+ else:
+ games.append(game)
+ return games
+
+ if os.environ.get("DEBUG"):
+ logging.basicConfig(level=logging.DEBUG)
+
+ redis = redis_package.Redis(
+ os.environ.get("REDIS_HOST", "127.0.0.1"),
+ retry_on_timeout=True,
+ decode_responses=True,
+ )
+ for game in get_full_game_names():
+ # Reset session counter
+ redis.set(f"{game}:counter", 0)
+ if redis.exists(f"{game}:sessions"):
+ # Clear out the sessions
+ logging.info(f"Clearing out {game} sessions")
+ for session_id in range(redis.llen(f"{game}:sessions")):
+ redis.delete(f"{game}:session:{session_id}")
+ redis.delete(f"{game}:relay:{session_id}")
+ redis.delete(f"{game}:sessions")
+ redis.delete(f"{game}:sessionByAddress")
+
+ # Create our host to listen connections from. 4095 is the maxinum amount.
+ host = enet.Host(enet.Address(b"0.0.0.0", 9120), peerCount=4095, channelLimit=1)
+ print("Listening for messages in port 9120", flush=True)
+
+ def send(peer, data: dict):
+ logging.debug(f"{peer.address}: OUT: {data}")
+ data = json.dumps(data, separators=(",", ":")).encode()
+ peer.send(0, enet.Packet(data, enet.PACKET_FLAG_RELIABLE))
+
+ def get_peer_by_address(address: str):
+ for peer in host.peers:
+ if str(peer.address) == address:
+ return peer
+ return None
+
+ def get_session_by_address(game: str, address: str):
+ session_id = redis.hget(f"{game}:sessionByAddress", str(address))
+ if session_id:
+ return redis.hgetall(f"{game}:session:{session_id}")
+ return None
+
+ def create_session(name: str, maxplayers: int, address: str):
+ # Get our new session ID
+ session_id = redis.incr(f"{game}:counter")
+ # Create and store our new session
+ redis.hset(
+ f"{game}:session:{session_id}",
+ mapping={
+ "name": name,
+ "players": 0,
+ "maxplayers": maxplayers,
+ "address": str(event.peer.address),
+ },
+ )
+ # Add session to sessions list
+ redis.rpush(f"{game}:sessions", session_id)
+
+ # Store to address to session hash
+ redis.hset(f"{game}:sessionByAddress", str(address), session_id)
+
+ logging.debug(f'{address}: NEW SESSION: "{name}"')
+ return session_id
+
+ def relay_data(data, sent_peer):
+ from_user = data.get("from")
+ type_of_send = data.get("to")
+ send_type_param = data.get("toparam")
+
+ session_id = int(redis.hget(f"relays:{str(sent_peer.address)}", "session"))
+ if not session_id:
+ return
+
+ game = redis.hget(f"relays:{str(sent_peer.address)}", "game")
+ relay_users = redis.hgetall(f"{game}:relay:{session_id}")
+ if not relay_users:
+ logging.warning(f"relay_data: Missing users on {game}:relay:{session_id}!")
+ return
+
+ logging.debug(f'relay_data: Players of "{game}" session {session_id}:')
+ for user_id, address in relay_users.items():
+ logging.debug(f"relay_data: - {user_id}: {address}")
+
+ peers_by_user_id = {}
+ for user_id, address in relay_users.items():
+ peer = get_peer_by_address(address)
+ if not peer:
+ logging.warning(f"relay_data: Peer for {address} does not exist!")
+ continue
+ peers_by_user_id[int(user_id)] = peer
+
+ user_id_by_peers = {v: k for k, v in peers_by_user_id.items()}
+
+ if user_id_by_peers.get(sent_peer) != 1:
+ # To make things easier, just send all non-host data to the host, so it can
+ # transfer data to peers that are connected directly to the host.
+ # It'll send it back to us if it actually needs to be relayed somewhere.
+ host_peer = peers_by_user_id.get(1)
+ if not host_peer:
+ logging.warning("relay_data: Host user (1) is missing!")
+ return
+ logging.debug(
+ f"relay_data: Relaying data from user {user_id_by_peers.get(sent_peer)} to host (1)."
+ )
+ send(host_peer, data)
+ return
+
+ peers_to_send = set()
+ if type_of_send == PN_SENDTYPE_INDIVIDUAL:
+ peer = peers_by_user_id.get(send_type_param)
+ if not peer:
+ logging.warning(
+ f"relay_data: user {send_type_param} not in relay, Host does not know, something might be wrong."
+ )
+ return
+ logging.debug(f"relay_data: Relaying data to user {send_type_param}")
+ peers_to_send.add(peer)
+ elif type_of_send == PN_SENDTYPE_GROUP:
+ logging.warning("STUB: PN_SENDTYPE_GROUP")
+ return
+ elif type_of_send == PN_SENDTYPE_HOST:
+ # Chances are that the host is user_id 1.
+ peer = peers_by_user_id.get(1)
+ if not peer:
+ return
+ logging.debug(f"relay_data: Relaying data to host (user 1)")
+ peers_to_send.add(peer)
+ elif type_of_send == PN_SENDTYPE_ALL:
+ # Send to all peers
+ for peer in peers_by_user_id.values():
+ peers_to_send.add(peer)
+
+ logging.debug(
+ f"relay_data: Relaying data to all peers: {str(list(peers_by_user_id.keys()))}"
+ )
+ else:
+ logging.warning(f"relay_data: Unknown type of send: {type_of_send}")
+
+ # Remove self from set.
+ if sent_peer in peers_to_send:
+ peers_to_send.remove(sent_peer)
+
+ for peer in peers_to_send:
+ send(peer, data)
+
+ def remove_user_from_relay(peer):
+ session_id = redis.hget(f"relays:{str(peer.address)}", "session")
+ if not session_id:
+ return
+
+ game = redis.hget(f"relays:{str(peer.address)}", "game")
+ redis.delete(f"relays:{str(peer.address)}")
+
+ address_by_user_id = redis.hgetall(f"{game}:relay:{session_id}")
+ if not address_by_user_id:
+ return
+
+ user_id_by_address = {v: k for k, v in address_by_user_id.items()}
+ user_id = user_id_by_address.get(str(peer.address))
+ if not user_id:
+ return
+
+ redis.hdel(f"{game}:relay:{session_id}", user_id)
+
+ # Send the remove_user request to the host.
+ host_address = address_by_user_id.get(1)
+ if not host_address:
+ return
+
+ host_peer = get_peer_by_address(host_address)
+ if not host_peer:
+ return
+
+ send(host_peer, {"cmd": "remove_user", "id": user_id})
+
+ do_loop = True
+
+ def exit(*args):
+ global do_loop
+ do_loop = False
+
+ # For Docker, they grace stop with SIGTERM
+ signal.signal(signal.SIGTERM, exit)
+ # SIGINT: Ctrl+C KeyboardInterrupt
+ signal.signal(signal.SIGINT, exit)
+
+ while do_loop:
+ # Main event loop
+ event = host.service(1000)
+ if event.type == enet.EVENT_TYPE_CONNECT:
+ logging.debug(f"{event.peer.address}: CONNECT")
+ elif event.type == enet.EVENT_TYPE_DISCONNECT:
+ logging.debug(f"{event.peer.address}: DISCONNECT")
+ # Close out sessions relating to the address
+ for game in get_full_game_names():
+ session_id = redis.hget(
+ f"{game}:sessionByAddress", str(event.peer.address)
+ )
+ if session_id:
+ redis.delete(f"{game}:session:{session_id}")
+ redis.lrem(f"{game}:sessions", 0, session_id)
+ redis.hdel(f"{game}:sessionByAddress", str(event.peer.address))
+
+ # Cleanup Relays (if any):
+ remove_user_from_relay(event.peer)
+
+ elif event.type == enet.EVENT_TYPE_RECEIVE:
+ logging.debug(f"{event.peer.address}: IN: {event.packet.data}")
+ try:
+ data = json.loads(event.packet.data)
+ except:
+ logging.warning(
+ f"{event.peer.address}: Received non-JSON data.",
+ event.packet.data.decode(),
+ )
+ continue
+ command = data.get("cmd")
+
+ if command == "game":
+ relay_data(data, event.peer)
+ continue
+ elif command == "remove_user":
+ remove_user_from_relay(event.peer)
+ continue
+
+ game = data.get("game")
+ version = data.get("version")
+ if not command:
+ logging.warning(f"{event.peer.address}: Command missing")
+ continue
+ if not game:
+ logging.warning(f"{event.peer.address}: Game missing")
+ continue
+
+ if game not in GAMES:
+ logging.warning(f'Game "{game}" not supported.')
+ continue
+
+ versions = VERSIONS.get(game)
+ if versions:
+ if not version:
+ logging.warning(f"{event.peer.address}: Version missing")
+ continue
+
+ if version not in version:
+ logging.warning(
+ f'Game "{game}" with version "{version}" not supported.'
+ )
+ continue
+
+ # Update the game to contain the version
+ game = f"{game}:{version}"
+
+ if command == "host_session":
+ name = data.get("name")
+ maxplayers = data.get("maxplayers")
+
+ session_id = create_session(name, maxplayers, event.peer.address)
+ send(event.peer, {"cmd": "host_session_resp", "id": session_id})
+
+ elif command == "update_players":
+ players = data.get("players")
+
+ session_id = redis.hget(
+ f"{game}:sessionByAddress", str(event.peer.address)
+ )
+ if session_id:
+ redis.hset(f"{game}:session:{session_id}", "players", players)
+
+ elif command == "get_sessions":
+ sessions = []
+
+ num_sessions = redis.llen(f"{game}:sessions")
+ session_ids = redis.lrange(f"{game}:sessions", 0, num_sessions)
+ for id in session_ids:
+ session = redis.hgetall(f"{game}:session:{id}")
+ sessions.append(
+ {
+ "id": int(id),
+ "name": session["name"],
+ "players": int(session["players"]),
+ "address": str(session["address"]),
+ }
+ )
+
+ send(
+ event.peer,
+ {
+ "cmd": "get_sessions_resp",
+ "address": str(event.peer.address),
+ "sessions": sessions,
+ },
+ )
+ elif command == "join_session":
+ session_id = data.get("id")
+
+ if not (redis.exists(f"{game}:session:{session_id}")):
+ logging.warning(f"Session {game}:{session_id} not found")
+ continue
+
+ address = redis.hget(f"{game}:session:{session_id}", "address")
+ peer = get_peer_by_address(address)
+ if not peer:
+ continue
+
+ # Send the joiner's address to the hoster for hole-punching
+ send(
+ peer, {"cmd": "joining_session", "address": str(event.peer.address)}
+ )
+ elif command == "start_relay":
+ session_id = data.get("session")
+
+ if not redis.exists(f"{game}:session:{session_id}"):
+ logging.warning(f"Session {game}:{session_id} not found")
+ continue
+
+ # Get peer of the session host
+ address = redis.hget(f"{game}:session:{session_id}", "address")
+ peer = get_peer_by_address(address)
+ if not peer:
+ continue
+
+ if redis.exists(f"{game}:relay:{session_id}"):
+ logging.warning(f"Relay for {game}:{session_id} already exists!")
+ continue
+
+ # Store new relay with the host (which usually always has the
+ # userId of 1).
+ redis.hset(f"{game}:relay:{session_id}", 1, str(peer.address))
+ redis.hset(
+ f"relays:{str(peer.address)}",
+ mapping={"game": game, "session": session_id},
+ )
+
+ # Send the add_user request to the host (with joiner's address for context):
+ send(
+ peer,
+ {"cmd": "add_user_for_relay", "address": str(event.peer.address)},
+ )
+ elif command == "add_user_resp":
+ address = data.get("address")
+ user_id = data.get("id")
+
+ session_id = int(
+ redis.hget(f"{game}:sessionByAddress", str(event.peer.address))
+ )
+ if not session_id:
+ logging.warning(
+ f"Could not find session for address {str(event.peer.address)}!"
+ )
+ continue
+
+ if not redis.exists(f"{game}:relay:{session_id}"):
+ logging.warning(f"{game}:relay:{session_id} does not exist!")
+ continue
+
+ if redis.hexists(f"{game}:relay:{session_id}", user_id):
+ logging.warning(
+ f"Duplicate User ID {user_id} in {game}:relay:{session_id}!"
+ )
+ continue
+
+ peer = get_peer_by_address(address)
+ if not peer:
+ logging.warning(f"Could not find peer for address: {address}!")
+ continue
+
+ if redis.exists(f"relays:{str(peer.address)}"):
+ logging.warning(f"Peer {str(peer.address)} is already in a relay!")
+ continue
+
+ redis.hset(f"{game}:relay:{session_id}", user_id, str(peer.address))
+ redis.hset(
+ f"relays:{str(peer.address)}",
+ mapping={"game": game, "session": session_id},
+ )
+ # Send the response back to the peer:
+ send(peer, {"cmd": "add_user_resp", "id": user_id})
diff --git a/net_defines.py b/net_defines.py
index 367622e..9bc11b2 100644
--- a/net_defines.py
+++ b/net_defines.py
@@ -1,8 +1,8 @@
# Copied from engines/scumm/he/moonbase/net_defines.h
# These are used for relaying.
-PN_PRIORITY_HIGH = 0x00000001
-PN_SENDTYPE_INDIVIDUAL = 1
-PN_SENDTYPE_GROUP = 2
-PN_SENDTYPE_HOST = 3
-PN_SENDTYPE_ALL = 4
\ No newline at end of file
+PN_PRIORITY_HIGH = 0x00000001
+PN_SENDTYPE_INDIVIDUAL = 1
+PN_SENDTYPE_GROUP = 2
+PN_SENDTYPE_HOST = 3
+PN_SENDTYPE_ALL = 4
diff --git a/web/config.py b/web/config.py
index c4e81a6..4e23a6a 100644
--- a/web/config.py
+++ b/web/config.py
@@ -1,23 +1,21 @@
# Games to accept:
GAMES = [
- "football", # Backyard Football (1999)
- "baseball2001", # Backyard Baseball 2001
- "football2002", # Backyard Football 2002
- "moonbase", # Moonbase Commander (v1.0/v1.1/Demo)
+ "football", # Backyard Football (1999)
+ "baseball2001", # Backyard Baseball 2001
+ "football2002", # Backyard Football 2002
+ "moonbase", # Moonbase Commander (v1.0/v1.1/Demo)
]
# Full names of accepted games. Make sure that they
# match with the GAMES list above
NAMES = {
- "football": "Backyard Football",
- "baseball2001": "Backyard Baseball 2001",
- "football2002": "Backyard Football 2002",
- "moonbase": "Moonbase Commander"
+ "football": "Backyard Football",
+ "baseball2001": "Backyard Baseball 2001",
+ "football2002": "Backyard Football 2002",
+ "moonbase": "Moonbase Commander",
}
# Version variants to accept for a specific game.
# If none exist but game exist in the GAMES list,
# that means there's only one game version.
-VERSIONS = {
- "moonbase": ["1.0", "1.1", "Demo"]
-}
\ No newline at end of file
+VERSIONS = {"moonbase": ["1.0", "1.1", "Demo"]}
diff --git a/web/main.py b/web/main.py
index e3254ba..47427de 100644
--- a/web/main.py
+++ b/web/main.py
@@ -10,61 +10,89 @@ import aioredis
from config import *
-routes = (
- Mount('/static', StaticFiles(directory='static'), name='static'),
-)
+# NOTE: Top-level code for this file is not under a __name__ == "__main__" case
+# because it requires an ASGI web server such as Uvicorn to run the code.
+
+routes = (Mount("/static", StaticFiles(directory="static"), name="static"),)
-app = FastAPI(debug=os.environ.get('DEBUG', False), routes=routes, title="ScummVM Multiplayer")
+app = FastAPI(
+ debug=os.environ.get("DEBUG", False), routes=routes, title="ScummVM Multiplayer"
+)
templates = Jinja2Templates(directory="templates")
-redis = aioredis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379"),
- retry_on_timeout=True, decode_responses=True)
+redis = aioredis.from_url(
+ os.environ.get("REDIS_URL", "redis://localhost:6379"),
+ retry_on_timeout=True,
+ decode_responses=True,
+)
+
+
+ at app.get("/")
+def index(request: Request, format: str = "html"):
+ if format == "json":
+ return {"games": NAMES}
+ return templates.TemplateResponse(
+ "index.html", {"request": request, "games": GAMES, "names": NAMES}
+ )
- at app.get('/')
-def index(request: Request, format: str = 'html'):
- if format == 'json':
- return {"games": NAMES}
- return templates.TemplateResponse("index.html", {'request': request, 'games': GAMES, 'names': NAMES})
async def get_sessions(game: str, version: str = None):
- sessions = []
- key = game
- if version:
- key += f":{version}"
-
- num_sessions = await redis.llen(f"{key}:sessions")
- session_ids = await redis.lrange(f"{key}:sessions", 0, num_sessions)
- for id in session_ids:
- session = await redis.hgetall(f"{key}:session:{id}")
- sessions.append({"id": int(id), "version": version, "name": session["name"],
- "players": int(session["players"]), "maxplayers": int(session["maxplayers"]),
- "address": str(session["address"])})
- return sessions
-
- at app.get('/{game}')
-async def game_page(request: Request, game: str, version: str = None, format: str = 'html'):
- sessions = []
- error = None
- if game in GAMES:
- versions = VERSIONS.get(game)
- if versions:
- version_request = version
- if version_request and version_request in versions:
- # Get sessions for a specific version
- sessions = await get_sessions(game, version_request)
- else:
- # Get sessions for all versions
- for version in versions:
- sessions += await get_sessions(game, version)
- else:
- # No version variants
- sessions = await get_sessions(game)
- else:
- error = f"Not supported game: \"{game}\""
-
- if format == 'json':
- if error:
- return {"error": error}
- return {"sessions": sessions}
- return templates.TemplateResponse("game.html", {'request': request, 'name': NAMES.get(game, game), 'sessions': sessions, 'error': error})
+ sessions = []
+ key = game
+ if version:
+ key += f":{version}"
+
+ num_sessions = await redis.llen(f"{key}:sessions")
+ session_ids = await redis.lrange(f"{key}:sessions", 0, num_sessions)
+ for id in session_ids:
+ session = await redis.hgetall(f"{key}:session:{id}")
+ sessions.append(
+ {
+ "id": int(id),
+ "version": version,
+ "name": session["name"],
+ "players": int(session["players"]),
+ "maxplayers": int(session["maxplayers"]),
+ "address": str(session["address"]),
+ }
+ )
+ return sessions
+
+
+ at app.get("/{game}")
+async def game_page(
+ request: Request, game: str, version: str = None, format: str = "html"
+):
+ sessions = []
+ error = None
+ if game in GAMES:
+ versions = VERSIONS.get(game)
+ if versions:
+ version_request = version
+ if version_request and version_request in versions:
+ # Get sessions for a specific version
+ sessions = await get_sessions(game, version_request)
+ else:
+ # Get sessions for all versions
+ for version in versions:
+ sessions += await get_sessions(game, version)
+ else:
+ # No version variants
+ sessions = await get_sessions(game)
+ else:
+ error = f'Not supported game: "{game}"'
+
+ if format == "json":
+ if error:
+ return {"error": error}
+ return {"sessions": sessions}
+ return templates.TemplateResponse(
+ "game.html",
+ {
+ "request": request,
+ "name": NAMES.get(game, game),
+ "sessions": sessions,
+ "error": error,
+ },
+ )
Commit: e1a7a482a9d6e197b2c4fe532bd4cf4442c15381
https://github.com/scummvm/scummvm-sites/commit/e1a7a482a9d6e197b2c4fe532bd4cf4442c15381
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Add logo and adjust paths to it.
Changed paths:
A web/static/scummvm_logo.png
web/templates/game.html
web/templates/index.html
diff --git a/web/static/scummvm_logo.png b/web/static/scummvm_logo.png
new file mode 100644
index 0000000..dc8f39f
Binary files /dev/null and b/web/static/scummvm_logo.png differ
diff --git a/web/templates/game.html b/web/templates/game.html
index ae73cc3..159fb8a 100644
--- a/web/templates/game.html
+++ b/web/templates/game.html
@@ -8,7 +8,7 @@
<body>
<div class="container">
<div class="header">
- <img src="https://scummvm.org/images/scummvm_logo.png"/>
+ <img src="/static/scummvm_logo.png"/>
</div>
<div class="content">
{% if error -%}
@@ -31,4 +31,4 @@
</div>
</div>
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/web/templates/index.html b/web/templates/index.html
index a306038..6b3b5bb 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -8,7 +8,7 @@
<body>
<div class="container">
<div class="header">
- <img src="https://scummvm.org/images/scummvm_logo.png"/>
+ <img src="/static/scummvm_logo.png"/>
</div>
<div class="content">
<p>Multiplayer Lobbies</p>
@@ -23,4 +23,4 @@
</div>
</div>
</body>
-</html>
\ No newline at end of file
+</html>
Commit: 47a3ac5a40838e23c09512d14c26e0bfe097dd08
https://github.com/scummvm/scummvm-sites/commit/47a3ac5a40838e23c09512d14c26e0bfe097dd08
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Add missing end line.
Changed paths:
web/static/style.css
diff --git a/web/static/style.css b/web/static/style.css
index e9379e1..0aeaec0 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -83,7 +83,7 @@ a.link > b {
outline: none;
font-size: 15px;
}
-
+
/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active, .collapsible:hover {
background-color: #cccccc;
@@ -93,4 +93,4 @@ a.link > b {
a.link > b {
font-size: 10pt;
}
-}
\ No newline at end of file
+}
Commit: a59fb4347c8ed5a4f9e50aeb94c79bcd77e23b72
https://github.com/scummvm/scummvm-sites/commit/a59fb4347c8ed5a4f9e50aeb94c79bcd77e23b72
Author: shkupfer (shkupfer at ncsu.edu)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Replace aioredis with redis, update README
Changed paths:
README.md
web/main.py
web/requirements.txt
diff --git a/README.md b/README.md
index 2a85efc..cc46116 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,7 @@
This project contains code for hosting online multiplayer lobbies for compatable Humongous Entertainment games.
-## Getting Started
-### Installing
-Both the session and web servers requires Redis to be installed. This is needed for both servers to share session data to each other.
-
+# Local Development
Clone this repo and checkout to the multiplayer branch:
```
git clone https://github.com/scummvm/scummvm-sites.git
@@ -13,51 +10,58 @@ git clone https://github.com/scummvm/scummvm-sites.git
git checkout multiplayer
```
-To start the session server, create a new virtual envrionment and install the requirements.
+## With Docker
+The session, lobby and web server can be run within Docker via docker-compose.
```
-python3 -m venv .env
-source .env/bin/activate
-
-python3 -m pip install -r requirements.txt
+docker-compose build
+docker-compose up
```
-Backyard Football and Backyard Baseball 2001 needs a lobby server to play online. You will need
-[Node.js](https://nodejs.org/en/) installed to run it.
+This will build Docker images for all three servers and starts a container for them simultaneously alongside with Redis.
-If you're planning to run the web server, install the requirements located in the web directory.
+## Without Docker
+### Redis
+Both the session and web servers use Redis. It is needed for both servers to share session data to each other. To install Redis, you can follow [the instructions on their website](https://redis.io/docs/getting-started/installation/). Then, start up a Redis instance by running
```
-python3 -m pip install -r web/requirements.txt
+redis-server
```
+### Session server
+To start the session server, first create a new virtual envrionment and install its requirements.
+```
+python3 -m venv .env
+source .env/bin/activate
-### Running
-To run the session server, simply run the main.py script
+python3 -m pip install -r requirements.txt
+```
+Then, run it with the main.py script:
```
python3 main.py
```
It should listen for connections on port 9120. Remember to configure ScummVM to connect to localhost or whatever address your server is running in.
+### Lobby server
+Backyard Football and Backyard Baseball 2001 need a lobby server to play online. You will need
+[Node.js](https://nodejs.org/en/) installed to run it.
+
To start the lobby server, go to the `lobby` directory and install the dependencies:
```
cd lobby
npm install
```
-
After that's done, you can simply run the `run.js` file.
```
node run.js
```
-Running a web server is unnecessary if you just want your server to host sessions, but if you want to, you can start one up by using uvicorn.
+### Web server
+Running the web server isn't necessary if you just want your server to host sessions, but if you want to you can run it. First, go to the `web` directory and install the requirements there.
```
cd web
-uvicorn main:app --reload
+python3 -m venv .env
+source .env/bin/activate
+python3 -m pip install -r requirements.txt
```
-
-## Docker
-The session, lobby and web server can be run within Docker via docker-compose.
+And then start the server with uvicorn
```
-docker-compose build
-docker-compose up
+uvicorn main:app --reload
```
-
-This will build Docker images for all three servers and starts a container for them simultaneously alongside with Redis.
diff --git a/web/main.py b/web/main.py
index 47427de..a22e880 100644
--- a/web/main.py
+++ b/web/main.py
@@ -6,7 +6,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.requests import Request
from fastapi.routing import Mount
-import aioredis
+from redis import asyncio as aioredis
from config import *
diff --git a/web/requirements.txt b/web/requirements.txt
index 3af1006..2a839be 100644
--- a/web/requirements.txt
+++ b/web/requirements.txt
@@ -1,4 +1,4 @@
fastapi==0.89.1
-aioredis==2.0.1
+redis==4.5.3
uvicorn==0.20.0
Jinja2==3.1.2
\ No newline at end of file
Commit: 8190b98cbc9ed3bfef98796a757e3f4570076203
https://github.com/scummvm/scummvm-sites/commit/8190b98cbc9ed3bfef98796a757e3f4570076203
Author: shkupfer (shkupfer at ncsu.edu)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Make redis start before server
Changed paths:
docker-compose.yml
diff --git a/docker-compose.yml b/docker-compose.yml
index 837f6a7..b73ca90 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,6 +6,8 @@ services:
- REDIS_HOST=redis
ports:
- "9120:9120/udp"
+ depends_on:
+ - redis
lobby:
build: ./lobby/
environment:
Commit: d002097a1b007af970baf1cdc0c20544576624e0
https://github.com/scummvm/scummvm-sites/commit/d002097a1b007af970baf1cdc0c20544576624e0
Author: shkupfer (shkupfer at ncsu.edu)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Base web image on python:3.9
Changed paths:
A web/gunicorn_conf.py
web/Dockerfile
web/requirements.txt
diff --git a/web/Dockerfile b/web/Dockerfile
index 52eb120..7fbc68a 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,7 +1,11 @@
-FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
+FROM python:3.9
-COPY ./requirements.txt /app/requirements.txt
+ENV PYTHONPATH=/app
+WORKDIR /app
+COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
-COPY ./ /app
\ No newline at end of file
+COPY ./ /app
+
+CMD gunicorn -k uvicorn.workers.UvicornWorker -c gunicorn_conf.py main:app
\ No newline at end of file
diff --git a/web/gunicorn_conf.py b/web/gunicorn_conf.py
new file mode 100644
index 0000000..33330e3
--- /dev/null
+++ b/web/gunicorn_conf.py
@@ -0,0 +1,48 @@
+# Inspired by https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/58ce0895f8c38b895e84f7ddb2128d66748b437c/docker-images/gunicorn_conf.py
+import multiprocessing
+import os
+
+workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
+max_workers_str = os.getenv("MAX_WORKERS")
+use_max_workers = None
+if max_workers_str:
+ use_max_workers = int(max_workers_str)
+web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
+
+host = os.getenv("HOST", "0.0.0.0")
+port = os.getenv("PORT", "80")
+bind_env = os.getenv("BIND", None)
+use_loglevel = os.getenv("LOG_LEVEL", "info")
+if bind_env:
+ use_bind = bind_env
+else:
+ use_bind = f"{host}:{port}"
+
+cores = multiprocessing.cpu_count()
+workers_per_core = float(workers_per_core_str)
+default_web_concurrency = workers_per_core * cores
+if web_concurrency_str:
+ web_concurrency = int(web_concurrency_str)
+ assert web_concurrency > 0
+else:
+ web_concurrency = max(int(default_web_concurrency), 2)
+ if use_max_workers:
+ web_concurrency = min(web_concurrency, use_max_workers)
+accesslog_var = os.getenv("ACCESS_LOG", "-")
+use_accesslog = accesslog_var or None
+errorlog_var = os.getenv("ERROR_LOG", "-")
+use_errorlog = errorlog_var or None
+graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
+timeout_str = os.getenv("TIMEOUT", "120")
+keepalive_str = os.getenv("KEEP_ALIVE", "5")
+
+# Gunicorn config variables
+loglevel = use_loglevel
+workers = web_concurrency
+bind = use_bind
+errorlog = use_errorlog
+worker_tmp_dir = "/dev/shm"
+accesslog = use_accesslog
+graceful_timeout = int(graceful_timeout_str)
+timeout = int(timeout_str)
+keepalive = int(keepalive_str)
diff --git a/web/requirements.txt b/web/requirements.txt
index 2a839be..4e5c8a4 100644
--- a/web/requirements.txt
+++ b/web/requirements.txt
@@ -1,4 +1,5 @@
fastapi==0.89.1
redis==4.5.3
-uvicorn==0.20.0
+gunicorn==20.1.0
+uvicorn[standard]==0.21.1
Jinja2==3.1.2
\ No newline at end of file
Commit: 08b6a47f2f9335b596c965f2f85547b22f3539b7
https://github.com/scummvm/scummvm-sites/commit/08b6a47f2f9335b596c965f2f85547b22f3539b7
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Add GPLv3 license and file headers.
Changed paths:
A LICENSE
lobby/database/Redis.js
lobby/database/WebAPI.js
lobby/discord/Discord.js
lobby/global/Areas.js
lobby/global/EventLogger.js
lobby/global/Stats.js
lobby/net/AreaMessages.js
lobby/net/ChallengeMessages.js
lobby/net/DatabaseMessages.js
lobby/net/NetworkConnection.js
lobby/net/NetworkListener.js
lobby/net/SessionMessages.js
lobby/run.js
main.py
net_defines.py
web/main.py
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/lobby/database/Redis.js b/lobby/database/Redis.js
index ed94d67..ee85f94 100644
--- a/lobby/database/Redis.js
+++ b/lobby/database/Redis.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
diff --git a/lobby/database/WebAPI.js b/lobby/database/WebAPI.js
index 07fdd44..3dda8e5 100644
--- a/lobby/database/WebAPI.js
+++ b/lobby/database/WebAPI.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
const bent = require('bent');
diff --git a/lobby/discord/Discord.js b/lobby/discord/Discord.js
index 90a0e19..d6366ba 100644
--- a/lobby/discord/Discord.js
+++ b/lobby/discord/Discord.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
diff --git a/lobby/global/Areas.js b/lobby/global/Areas.js
index 465022d..0f55c05 100644
--- a/lobby/global/Areas.js
+++ b/lobby/global/Areas.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const Areas = {
diff --git a/lobby/global/EventLogger.js b/lobby/global/EventLogger.js
index 72624e2..3576b5d 100644
--- a/lobby/global/EventLogger.js
+++ b/lobby/global/EventLogger.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
const logger = createLogger('Event');
diff --git a/lobby/global/Stats.js b/lobby/global/Stats.js
index eaf2f36..5444eca 100644
--- a/lobby/global/Stats.js
+++ b/lobby/global/Stats.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const ResultsMappers = {
@@ -32,4 +53,4 @@ const ResultsMappers = {
module.exports = {
ResultsMappers: ResultsMappers,
-};
\ No newline at end of file
+};
diff --git a/lobby/net/AreaMessages.js b/lobby/net/AreaMessages.js
index 754265f..bcc9391 100644
--- a/lobby/net/AreaMessages.js
+++ b/lobby/net/AreaMessages.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
const logger = createLogger('AreaMessages');
diff --git a/lobby/net/ChallengeMessages.js b/lobby/net/ChallengeMessages.js
index 2f156d8..4d1fa62 100644
--- a/lobby/net/ChallengeMessages.js
+++ b/lobby/net/ChallengeMessages.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
const logger = createLogger('ChallengeMessages');
diff --git a/lobby/net/DatabaseMessages.js b/lobby/net/DatabaseMessages.js
index 1a388da..5c140ec 100644
--- a/lobby/net/DatabaseMessages.js
+++ b/lobby/net/DatabaseMessages.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
const logger = createLogger('DatabaseMessages');
diff --git a/lobby/net/NetworkConnection.js b/lobby/net/NetworkConnection.js
index 30d6632..12b2b1d 100644
--- a/lobby/net/NetworkConnection.js
+++ b/lobby/net/NetworkConnection.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
diff --git a/lobby/net/NetworkListener.js b/lobby/net/NetworkListener.js
index bd02a0e..417086f 100644
--- a/lobby/net/NetworkListener.js
+++ b/lobby/net/NetworkListener.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
const net = require('net');
diff --git a/lobby/net/SessionMessages.js b/lobby/net/SessionMessages.js
index 5142fe4..dac0fb5 100644
--- a/lobby/net/SessionMessages.js
+++ b/lobby/net/SessionMessages.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict";
const createLogger = require('logging').default;
const logger = createLogger('SessionMessages');
diff --git a/lobby/run.js b/lobby/run.js
index f47675e..89ae487 100644
--- a/lobby/run.js
+++ b/lobby/run.js
@@ -1,3 +1,24 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
"use strict"
const yaml = require('js-yaml');
diff --git a/main.py b/main.py
index f3fd45f..db154a3 100644
--- a/main.py
+++ b/main.py
@@ -1,3 +1,22 @@
+# ScummVM - Graphic Adventure Engine
+#
+# ScummVM is the legal property of its developers, whose names
+# are too numerous to list here. Please refer to the COPYRIGHT
+# file distributed with this source distribution.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
import json
import logging
import os
diff --git a/net_defines.py b/net_defines.py
index 9bc11b2..fcf4a00 100644
--- a/net_defines.py
+++ b/net_defines.py
@@ -1,3 +1,22 @@
+# ScummVM - Graphic Adventure Engine
+#
+# ScummVM is the legal property of its developers, whose names
+# are too numerous to list here. Please refer to the COPYRIGHT
+# file distributed with this source distribution.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
# Copied from engines/scumm/he/moonbase/net_defines.h
# These are used for relaying.
diff --git a/web/main.py b/web/main.py
index a22e880..90c9677 100644
--- a/web/main.py
+++ b/web/main.py
@@ -1,3 +1,22 @@
+# ScummVM - Graphic Adventure Engine
+#
+# ScummVM is the legal property of its developers, whose names
+# are too numerous to list here. Please refer to the COPYRIGHT
+# file distributed with this source distribution.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
import os
from fastapi import FastAPI
Commit: 8c88e8770dd51893c0e7dd4bdd4f95387a49fa98
https://github.com/scummvm/scummvm-sites/commit/8c88e8770dd51893c0e7dd4bdd4f95387a49fa98
Author: Little Cat (toontownlittlecat at gmail.com)
Date: 2023-03-29T01:16:01+02:00
Commit Message:
MULTIPLAYER: Sanity check game data packets.
Changed paths:
main.py
net_defines.py
diff --git a/main.py b/main.py
index db154a3..c79428d 100644
--- a/main.py
+++ b/main.py
@@ -119,9 +119,48 @@ if __name__ == "__main__":
from_user = data.get("from")
type_of_send = data.get("to")
send_type_param = data.get("toparam")
+ packet_type = data.get("type")
+ packet_data = data.get("data")
+
+ if None in (from_user, type_of_send, send_type_param, packet_type, packet_data):
+ logging.warning(
+ f"relay_data: Got malformed game data from {str(sent_peer.address)}: {data}"
+ )
+ return
+
+ # Check the packet received to see if it contains the proper data.
+ if packet_type in (
+ PACKETTYPE_REMOTESTARTSCRIPT,
+ PACKETTYPE_REMOTESTARTSCRIPTRETURN,
+ PACKETTYPE_REMOTESTARTSCRIPTRESULT,
+ ):
+ params = packet_data.get("params")
+ if not params or not isinstance(params, list):
+ logging.warning(
+ f"relay_data: Missing params in a remote start script packet from {str(sent_peer.address)}: {data}"
+ )
+ return
+ elif packet_type == PACKETTYPE_REMOTESENDSCUMMARRAY:
+ dim1start = packet_data.get("dim1start")
+ dim1end = packet_data.get("dim1end")
+ dim2start = packet_data.get("dim2start")
+ dim2end = packet_data.get("dim2end")
+ atype = packet_data.get("type")
+
+ if not all(
+ isinstance(i, int)
+ for i in (dim1start, dim1end, dim2start, dim2end, atype)
+ ):
+ logging.warning(
+ f"relay_data: Malformed SCUMM array data from {str(sent_peer.address)}: {data}"
+ )
+ return
session_id = int(redis.hget(f"relays:{str(sent_peer.address)}", "session"))
if not session_id:
+ logging.warning(
+ f"relay_data: Could not find session id for peer: {str(sent_peer.address)}"
+ )
return
game = redis.hget(f"relays:{str(sent_peer.address)}", "game")
diff --git a/net_defines.py b/net_defines.py
index fcf4a00..74ce2e1 100644
--- a/net_defines.py
+++ b/net_defines.py
@@ -25,3 +25,9 @@ PN_SENDTYPE_INDIVIDUAL = 1
PN_SENDTYPE_GROUP = 2
PN_SENDTYPE_HOST = 3
PN_SENDTYPE_ALL = 4
+
+# These are used for sanity checking.
+PACKETTYPE_REMOTESTARTSCRIPT = 1
+PACKETTYPE_REMOTESTARTSCRIPTRETURN = 2
+PACKETTYPE_REMOTESTARTSCRIPTRESULT = 3
+PACKETTYPE_REMOTESENDSCUMMARRAY = 4
More information about the Scummvm-git-logs
mailing list