diff --git a/.env.template b/.env.template index a54df82e..4ba9bcec 100644 --- a/.env.template +++ b/.env.template @@ -10,6 +10,12 @@ START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json # If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file. # Keep empty if you are sharing hard coded / clear text credentials. TURN_STATIC_AUTH_SECRET= +DISABLE_NOTIFICATIONS=true +SKIP_RENDER_OPTIMIZATIONS=false # The email address used by Let's encrypt to send renewal warnings (compulsory) ACME_EMAIL= + +MAX_PER_GROUP=4 +MAX_USERNAME_LENGTH=8 + diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index ce186cec..e33346d0 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -1,7 +1,13 @@ name: Build, push and deploy Docker image on: - - push + push: + branches: [master, develop] + release: + types: [created] + pull_request: + types: [ labeled, synchronize ] + # Enables BuildKit env: @@ -10,7 +16,7 @@ env: jobs: build-front: - + if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} runs-on: ubuntu-latest steps: @@ -30,11 +36,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-front - tags: ${{ env.GITHUB_REF_SLUG }} + tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} add_git_labels: true build-back: - + if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} runs-on: ubuntu-latest steps: @@ -53,11 +59,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-back - tags: ${{ env.GITHUB_REF_SLUG }} + tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} add_git_labels: true build-pusher: - + if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} runs-on: ubuntu-latest steps: @@ -76,11 +82,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-pusher - tags: ${{ env.GITHUB_REF_SLUG }} + tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} add_git_labels: true build-uploader: - + if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} runs-on: ubuntu-latest steps: @@ -99,11 +105,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-uploader - tags: ${{ env.GITHUB_REF_SLUG }} + tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} add_git_labels: true build-maps: - + if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} runs-on: ubuntu-latest steps: @@ -123,7 +129,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-maps - tags: ${{ env.GITHUB_REF_SLUG }} + tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} add_git_labels: true deeploy: @@ -134,6 +140,7 @@ jobs: - build-maps - build-uploader runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} steps: - name: Checkout @@ -142,6 +149,37 @@ jobs: # Create a slugified value of the branch - uses: rlespinasse/github-slug-action@3.1.0 + - name: Write certificate + run: echo "${CERTS_PRIVATE_KEY}" > secret.key && chmod 0600 secret.key + env: + CERTS_PRIVATE_KEY: ${{ secrets.CERTS_PRIVATE_KEY }} + + - name: Download certificate + run: mkdir secrets && scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i secret.key ubuntu@cert.workadventu.re:./config/live/workadventu.re/* secrets/ + + - name: Create namespace + uses: steebchen/kubectl@v1.0.0 + env: + KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }} + with: + args: create namespace workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} + continue-on-error: true + + - name: Delete old certificates in namespace + uses: steebchen/kubectl@v1.0.0 + env: + KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }} + with: + args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} delete secret certificate-tls + continue-on-error: true + + - name: Install certificates in namespace + uses: steebchen/kubectl@v1.0.0 + env: + KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }} + with: + args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} create secret tls certificate-tls --key="secrets/privkey.pem" --cert="secrets/fullchain.pem" + - name: Deploy uses: thecodingmachine/deeployer-action@master env: @@ -151,14 +189,14 @@ jobs: JITSI_URL: ${{ secrets.JITSI_URL }} SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }} TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }} + DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} with: - namespace: workadventure-${{ env.GITHUB_REF_SLUG }} + namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} - name: Add a comment in PR uses: unsplash/comment-on-pr@v1.2.0 - if: ${{ env.GITHUB_REF_SLUG != 'master' }} + if: ${{ github.event_name == 'pull_request' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - msg: Environment deployed at https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com - check_for_duplicate_msg: true + msg: "Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re \nTests available at https://maps-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re/tests" diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index bb6bcc89..ffa8273f 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -1,7 +1,8 @@ name: Cleanup images and environments on: - - delete + pull_request: + types: [ closed ] # Enables BuildKit env: @@ -14,13 +15,12 @@ jobs: steps: # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@1.1.0 + - uses: rlespinasse/github-slug-action@3.1.0 - name: Cleanup + continue-on-error: true uses: thecodingmachine/deeployer-cleanup-action@master env: KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }} with: -# FIXME: we are not using ${{ env.GITHUB_REF_SLUG }} that resolves to master BUT! we are not using a slugified namespace -# so complex namespace names will not be treated correctly - namespace: workadventure-${{ github.event.ref }} + namespace: workadventure-${{ env.GITHUB_HEAD_REF_SLUG }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..3a2a562d --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ develop ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] + schedule: + - cron: '24 17 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 607dbf79..faf50c7a 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -3,8 +3,11 @@ name: "Continuous Integration" on: - - "pull_request" - - "push" + push: + branches: + - master + - develop + pull_request: jobs: @@ -46,7 +49,11 @@ jobs: - name: "Build" run: yarn run build env: - API_URL: "localhost:8080" + PUSHER_URL: "//localhost:8080" + working-directory: "front" + + - name: "Svelte check" + run: yarn run svelte-check working-directory: "front" - name: "Lint" @@ -57,6 +64,11 @@ jobs: run: yarn test working-directory: "front" + # We will enable prettier checks on front in a few month, when most PRs without prettier have been merged + # - name: "Prettier" + # run: yarn run pretty-check + # working-directory: "front" + continuous-integration-pusher: name: "Continuous Integration Pusher" @@ -100,6 +112,10 @@ jobs: run: yarn test working-directory: "pusher" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "pusher" + continuous-integration-back: name: "Continuous Integration Back" @@ -143,3 +159,7 @@ jobs: run: yarn test working-directory: "back" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "back" + diff --git a/.github/workflows/push-to-npm.yml b/.github/workflows/push-to-npm.yml new file mode 100644 index 00000000..798e2530 --- /dev/null +++ b/.github/workflows/push-to-npm.yml @@ -0,0 +1,67 @@ +name: Push @workadventure/iframe-api-typings to NPM +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v2 + with: + node-version: '14.x' + registry-url: 'https://registry.npmjs.org' + + - name: Edit tsconfig.json to add declarations + run: "sed -i 's/\"declaration\": false/\"declaration\": true/g' tsconfig.json" + working-directory: "front" + + - name: Replace version number + run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json' + working-directory: "front/packages/iframe-api-typings" + + - name: Debug package.json + run: cat package.json + working-directory: "front/packages/iframe-api-typings" + + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' + + - name: "Install dependencies" + run: yarn install + working-directory: "front" + + - name: "Install messages dependencies" + run: yarn install + working-directory: "messages" + + - name: "Build proto messages" + run: yarn run proto && yarn run copy-to-front + working-directory: "messages" + + - name: "Create index.html" + run: ./templater.sh + working-directory: "front" + + - name: "Build" + run: yarn run build + env: + API_URL: "localhost:8080" + working-directory: "front" + + # We build the front to generate the typings of iframe_api, then we copy those typings in a separate package. + - name: Copy typings to package dir + run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts + + - name: Install dependencies in package + run: yarn install + working-directory: "front/packages/iframe-api-typings" + + - name: Publish package + run: yarn publish + working-directory: "front/packages/iframe-api-typings" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 70660058..2cbf66b1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ docker-compose.override.yaml *.DS_Store maps/yarn.lock maps/dist/computer.js -maps/dist/computer.js.map \ No newline at end of file +maps/dist/computer.js.map +/node_modules/ diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 00000000..31354ec1 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..5944be37 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,15 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +( +cd front || exit +yarn run precommit +) +( +cd pusher || exit +yarn run precommit +) +( +cd back || exit +yarn run precommit +) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..41f72510 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +## Version 1.4.x-dev + +### Updates + +- Added the ability to have animated tiles in maps #1216 #1217 +- Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218 +- Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219 +- Migrated the admin console to Svelte, and redesigned the console #1211 +- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1) +- New scripting API features : + - Use `WA.room.showLayer(): void` to show a layer + - Use `WA.room.hideLayer(): void` to hide a layer + - Use `WA.room.setProperty() : void` to add or change existing property of a layer + - Use `WA.player.onPlayerMove(): void` to track the movement of the current player + - Use `WA.room.getCurrentUser(): Promise` to get the ID, name and tags of the current player + - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started + - Use `WA.ui.registerMenuCommand(): void` to add a custom menu + +## Version 1.4.1 + +### Bugfixes + +- Loading errors after the preload stage should not crash the game anymore + +## Version 1.4.0 + +### BREAKING CHANGES + +- Scripting API: + - Changed function names: `restorePlayerControl` => `restorePlayerControls`, `disablePlayerControl` => `disablePlayerControls`. + Please keep in mind that the scripting API is still experimental. Some breaking changes can occur in it until we mark it as stable. + +### Updates + +- Added the emote feature to WorkAdventure. (@Kharhamel, @Tabascoeye) + - The emote menu can be opened by clicking on your character. + - Clicking on one of its element will close the menu and play an emote above your character. + - This emote can be seen by other players. +- Player names were improved. (@Kharhamel) + - We now create a GameObject.Text instead of GameObject.BitmapText + - now use the 'Press Start 2P' font family and added an outline + - As a result, we can now allow non-standard letters like french accents or chinese characters! + +- Added the contact card feature. (@Kharhamel) + - Click on another player to see its contact info. + - Premium-only feature unfortunately. I need to find a way to make it available for all. + - If no contact data is found (either because the user is anonymous or because no admin backend), display an error card. + +- Mobile support has been improved + - WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used + - Mouse wheel support to zoom in / out + - Pinch support on mobile to zoom in / out + - Improved virtual joystick size (adapts to the zoom level) +- Redesigned intermediate scenes + - Redesigned Select Companion scene + - Redesigned Enter Your Name scene + - Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use +- New scripting API features: + - Use `WA.loadSound(): Sound` to load / play / stop a sound + + +### Bug Fixes + +- Pinch gesture does no longer move the character + +## Version 1.3.0 + +### New Features + +* Maps can now contain "group" layers (layers that contain other layers) - #899 #779 (@Lurkars @moufmouf) + +### Updates + + +### Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b85d0a98 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to WorkAdventure + +Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to +ask questions and how to work on something. + +## Contributions we are seeking + +We love to receive contributions from our community — you! + +There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, +submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself. + +## Using the issue tracker + +First things first: **Do NOT report security vulnerabilities in public issues!**. +Please read the [security guide](SECURITY.md) to learn who to do a security disclosure to the WorkAdventure core team. + +You can use [GitHub issue tracker](https://github.com/thecodingmachine/workadventure/issues) to: + +- File bug reports +- Ask for feature requests + +If you have more general questions, a good place to ask is [our Discord server](https://discord.gg/YGtngdh9gt). + +Finally, you can come and talk to the WorkAdventure core team... on WorkAdventure, of course! [Our offices are here](https://play.staging.workadventu.re/@/tcm/workadventure/wa-village). + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope +and avoid containing unrelated commits. + +Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), +otherwise you risk spending a lot of time working on something that the project's developers might not want to merge +into the project. + +You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues). + +### Linting your code + +Before committing, be sure to install the "Prettier" precommit hook that will reformat your code to our coding style. + +In order to enable the "Prettier" precommit hook, at the root of the project, run: + +```console +$ yarn run install +$ yarn run prepare +``` + +If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need +to run code linting manually: + +```console +$ docker-compose exec front yarn run pretty +$ docker-compose exec pusher yarn run pretty +$ docker-compose exec back yarn run pretty +``` + +### Providing tests + +WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. + +Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine). + +If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain +some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file +to add a reference to your newly created test map. + diff --git a/README.md b/README.md index cf640d9c..322f06ba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/JVVhXzcE) +![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/YGtngdh9gt) ![WorkAdventure landscape image](README-INTRO.jpg) @@ -20,7 +20,7 @@ Install Docker. Run: ``` -docker-compose up +docker-compose up -d ``` The environment will start. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..562da3e0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Reporting a Vulnerability + +First things first: **Do NOT report security vulnerabilities in public issues!** + +Please disclose responsibly by sending +a mail at security@workadventu.re (you can also ping us in the GitHub issues, but please, no details in the issues!) + +We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix +and release available for an eventual public disclosure. + +We do not have a bug bounty program. + +## Supported Versions + +We only apply security patches on the latest tagged release and on the `master` and `develop` branches + +Unless specified otherwise, do not expect us to fix security issues on past releases. We are only maintaining one release: +the latest one, which is online at https://play.workadventu.re. diff --git a/back/.prettierignore b/back/.prettierignore new file mode 100644 index 00000000..1f453464 --- /dev/null +++ b/back/.prettierignore @@ -0,0 +1 @@ +src/Messages/generated diff --git a/back/.prettierrc.json b/back/.prettierrc.json new file mode 100644 index 00000000..e8980d15 --- /dev/null +++ b/back/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "tabWidth": 4 +} diff --git a/back/package.json b/back/package.json index 09d6794a..7015b9b8 100644 --- a/back/package.json +++ b/back/package.json @@ -10,8 +10,11 @@ "runprod": "node --max-old-space-size=4096 ./dist/server.js", "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", - "lint": "node_modules/.bin/eslint src/ . --ext .ts", - "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts" + "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", + "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", + "precommit": "lint-staged", + "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", + "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, "repository": { "type": "git", @@ -38,23 +41,17 @@ "homepage": "https://github.com/thecodingmachine/workadventure#readme", "dependencies": { "axios": "^0.21.1", - "body-parser": "^1.19.0", "busboy": "^0.3.1", "circular-json": "^0.5.9", "debug": "^4.3.1", "generic-type-guard": "^3.2.0", "google-protobuf": "^3.13.0", "grpc": "^1.24.4", - "http-status-codes": "^1.4.0", - "iterall": "^1.3.0", "jsonwebtoken": "^8.5.1", "mkdirp": "^1.0.4", - "multer": "^1.4.2", "prom-client": "^12.0.0", "query-string": "^6.13.3", "systeminformation": "^4.31.1", - "ts-node-dev": "^1.0.0-pre.44", - "typescript": "^3.8.3", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uuidv4": "^6.0.7" }, @@ -71,6 +68,15 @@ "@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/parser": "^2.26.0", "eslint": "^6.8.0", - "jasmine": "^3.5.0" + "jasmine": "^3.5.0", + "lint-staged": "^11.0.0", + "prettier": "^2.3.1", + "ts-node-dev": "^1.0.0-pre.44", + "typescript": "^3.8.3" + }, + "lint-staged": { + "*.ts": [ + "prettier --write" + ] } } diff --git a/back/src/App.ts b/back/src/App.ts index 4bcc56ba..a6c42abb 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -1,7 +1,7 @@ // lib/app.ts -import {PrometheusController} from "./Controller/PrometheusController"; -import {DebugController} from "./Controller/DebugController"; -import {App as uwsApp} from "./Server/sifrr.server"; +import { PrometheusController } from "./Controller/PrometheusController"; +import { DebugController } from "./Controller/DebugController"; +import { App as uwsApp } from "./Server/sifrr.server"; class App { public app: uwsApp; diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index 93c17ab4..dc510d6c 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -1,10 +1,9 @@ -import {HttpResponse} from "uWebSockets.js"; - +import { HttpResponse } from "uWebSockets.js"; export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept'); - res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.writeHeader('access-control-allow-origin', '*'); + res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); + res.writeHeader("access-control-allow-origin", "*"); } } diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index 509d8b2f..b7f037fd 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -1,53 +1,54 @@ -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {stringify} from "circular-json"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import { parse } from 'query-string'; -import {App} from "../Server/sifrr.server"; -import {socketManager} from "../Services/SocketManager"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { stringify } from "circular-json"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +import { parse } from "query-string"; +import { App } from "../Server/sifrr.server"; +import { socketManager } from "../Services/SocketManager"; export class DebugController { - constructor(private App : App) { + constructor(private App: App) { this.getDump(); } - - getDump(){ + getDump() { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send('Invalid token sent!'); + return res.status(401).send("Invalid token sent!"); } - return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( - socketManager.getWorlds(), - (key: unknown, value: unknown) => { - if (key === 'listeners') { - return 'Listeners'; - } - if (key === 'socket') { - return 'Socket'; - } - if (key === 'batchedMessages') { - return 'BatchedMessages'; - } - if(value instanceof Map) { - const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any - for (const [mapKey, mapValue] of value.entries()) { - obj[mapKey] = mapValue; + return res + .writeStatus("200 OK") + .writeHeader("Content-Type", "application/json") + .end( + stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { + if (key === "listeners") { + return "Listeners"; } - return obj; - } else if(value instanceof Set) { + if (key === "socket") { + return "Socket"; + } + if (key === "batchedMessages") { + return "BatchedMessages"; + } + if (value instanceof Map) { + const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any + for (const [mapKey, mapValue] of value.entries()) { + obj[mapKey] = mapValue; + } + return obj; + } else if (value instanceof Set) { const obj: Array = []; for (const [setKey, setValue] of value.entries()) { obj.push(setValue); } return obj; - } else { - return value; - } - } - )); + } else { + return value; + } + }) + ); }); } } diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts index e854cf43..3ab3d33f 100644 --- a/back/src/Controller/PrometheusController.ts +++ b/back/src/Controller/PrometheusController.ts @@ -1,7 +1,7 @@ -import {App} from "../Server/sifrr.server"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -const register = require('prom-client').register; -const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; +import { App } from "../Server/sifrr.server"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +const register = require("prom-client").register; +const collectDefaultMetrics = require("prom-client").collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { @@ -14,7 +14,7 @@ export class PrometheusController { } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', register.contentType); + res.writeHeader("Content-Type", register.contentType); res.end(register.metrics()); } } diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 8736a856..19eddd3e 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -1,16 +1,17 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; +const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; +const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; -const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; -const JITSI_ISS = process.env.JITSI_ISS || ''; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; -const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080; -const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051; +const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ""; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; +const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080; +const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051; export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed -export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ''; +export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ""; +export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export { MINIMUM_DISTANCE, @@ -23,5 +24,5 @@ export { CPU_OVERHEAT_THRESHOLD, JITSI_URL, JITSI_ISS, - SECRET_JITSI_KEY -} + SECRET_JITSI_KEY, +}; diff --git a/back/src/Model/Admin.ts b/back/src/Model/Admin.ts index a121d105..93396fa8 100644 --- a/back/src/Model/Admin.ts +++ b/back/src/Model/Admin.ts @@ -1,25 +1,12 @@ -import { Group } from "./Group"; -import { PointInterface } from "./Websocket/PointInterface"; -import {Zone} from "_Model/Zone"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {ServerDuplexStream} from "grpc"; import { - BatchMessage, - PusherToBackMessage, ServerToAdminClientMessage, - ServerToClientMessage, - SubMessage, UserJoinedRoomMessage, UserLeftRoomMessage + UserJoinedRoomMessage, + UserLeftRoomMessage, } from "../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; -import {AdminSocket} from "../RoomManager"; - +import { AdminSocket } from "../RoomManager"; export class Admin { - public constructor( - private readonly socket: AdminSocket - ) { - } + public constructor(private readonly socket: AdminSocket) {} public sendUserJoin(uuid: string, name: string, ip: string): void { const serverToAdminClientMessage = new ServerToAdminClientMessage(); @@ -34,7 +21,7 @@ export class Admin { this.socket.write(serverToAdminClientMessage); } - public sendUserLeft(uuid: string/*, name: string, ip: string*/): void { + public sendUserLeft(uuid: string /*, name: string, ip: string*/): void { const serverToAdminClientMessage = new ServerToAdminClientMessage(); const userLeftRoomMessage = new UserLeftRoomMessage(); diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 6a592ed0..020f4c29 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -1,16 +1,16 @@ -import {PointInterface} from "./Websocket/PointInterface"; -import {Group} from "./Group"; -import {User, UserSocket} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; -import {PositionNotifier} from "./PositionNotifier"; -import {Movable} from "_Model/Movable"; -import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; -import {arrayIntersect} from "../Services/ArrayHelper"; -import {JoinRoomMessage} from "../Messages/generated/messages_pb"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {ZoneSocket} from "src/RoomManager"; -import {Admin} from "../Model/Admin"; +import { PointInterface } from "./Websocket/PointInterface"; +import { Group } from "./Group"; +import { User, UserSocket } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; +import { PositionNotifier } from "./PositionNotifier"; +import { Movable } from "_Model/Movable"; +import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; +import { arrayIntersect } from "../Services/ArrayHelper"; +import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { ZoneSocket } from "src/RoomManager"; +import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; @@ -38,38 +38,34 @@ export class GameRoom { private readonly positionNotifier: PositionNotifier; public readonly roomId: string; - public readonly anonymous: boolean; - public tags: string[]; - public policyType: GameRoomPolicyTypes; public readonly roomSlug: string; - public readonly worldSlug: string = ''; - public readonly organizationSlug: string = ''; + public readonly worldSlug: string = ""; + public readonly organizationSlug: string = ""; + private versionNumber: number = 1; private nextUserId: number = 1; - constructor(roomId: string, - connectCallback: ConnectCallback, - disconnectCallback: DisconnectCallback, - minDistance: number, - groupRadius: number, - onEnters: EntersCallback, - onMoves: MovesCallback, - onLeaves: LeavesCallback) - { + constructor( + roomId: string, + connectCallback: ConnectCallback, + disconnectCallback: DisconnectCallback, + minDistance: number, + groupRadius: number, + onEnters: EntersCallback, + onMoves: MovesCallback, + onLeaves: LeavesCallback, + onEmote: EmoteCallback + ) { this.roomId = roomId; - this.anonymous = isRoomAnonymous(roomId); - this.tags = []; - this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY; - if (this.anonymous) { + if (isRoomAnonymous(roomId)) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); this.roomSlug = roomSlug; this.organizationSlug = organizationSlug; this.worldSlug = worldSlug; } - this.users = new Map(); this.usersByUuid = new Map(); this.admins = new Set(); @@ -79,7 +75,7 @@ export class GameRoom { this.minDistance = minDistance; this.groupRadius = groupRadius; // A zone is 10 sprites wide. - this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves); + this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote); } public getGroups(): Group[] { @@ -90,18 +86,22 @@ export class GameRoom { return this.users; } - public getUserByUuid(uuid: string): User|undefined { + public getUserByUuid(uuid: string): User | undefined { return this.usersByUuid.get(uuid); } + public getUserById(id: number): User | undefined { + return this.users.get(id); + } - public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User { + public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User { const positionMessage = joinRoomMessage.getPositionmessage(); if (positionMessage === undefined) { - throw new Error('Missing position message'); + throw new Error("Missing position message"); } const position = ProtobufUtils.toPointInterface(positionMessage); - const user = new User(this.nextUserId, + const user = new User( + this.nextUserId, joinRoomMessage.getUseruuid(), joinRoomMessage.getIpaddress(), position, @@ -109,8 +109,10 @@ export class GameRoom { this.positionNotifier, socket, joinRoomMessage.getTagList(), + joinRoomMessage.getVisitcardurl(), joinRoomMessage.getName(), - ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()) + ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()), + joinRoomMessage.getCompanion() ); this.nextUserId++; this.users.set(user.id, user); @@ -125,12 +127,12 @@ export class GameRoom { return user; } - public leave(user : User){ + public leave(user: User) { const userObj = this.users.get(user.id); if (userObj === undefined) { - console.warn('User ', user.id, 'does not belong to this game room! It should!'); + console.warn("User ", user.id, "does not belong to this game room! It should!"); } - if (userObj !== undefined && typeof userObj.group !== 'undefined') { + if (userObj !== undefined && typeof userObj.group !== "undefined") { this.leaveGroup(userObj); } this.users.delete(user.id); @@ -142,7 +144,7 @@ export class GameRoom { // Notify admins for (const admin of this.admins) { - admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/); + admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/); } } @@ -150,7 +152,7 @@ export class GameRoom { return this.users.size === 0 && this.admins.size === 0; } - public updatePosition(user : User, userPosition: PointInterface): void { + public updatePosition(user: User, userPosition: PointInterface): void { user.setPosition(userPosition); this.updateUserGroup(user); @@ -172,22 +174,24 @@ export class GameRoom { return; } - const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user); + const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user); if (closestItem !== null) { if (closestItem instanceof Group) { // Let's join the group! closestItem.join(user); } else { - const closestUser : User = closestItem; - const group: Group = new Group(this.roomId,[ - user, - closestUser - ], this.connectCallback, this.disconnectCallback, this.positionNotifier); + const closestUser: User = closestItem; + const group: Group = new Group( + this.roomId, + [user, closestUser], + this.connectCallback, + this.disconnectCallback, + this.positionNotifier + ); this.groups.add(group); } } - } else { // If the user is part of a group: // should he leave the group? @@ -228,7 +232,9 @@ export class GameRoom { this.positionNotifier.leave(group); group.destroy(); if (!this.groups.has(group)) { - throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World."); + throw new Error( + "Could not find group " + group.getId() + " referenced by user " + user.id + " in World." + ); } this.groups.delete(group); //todo: is the group garbage collected? @@ -246,16 +252,15 @@ export class GameRoom { * OR * - close enough to a group (distance <= groupRadius) */ - private searchClosestAvailableUserOrGroup(user: User): User|Group|null - { + private searchClosestAvailableUserOrGroup(user: User): User | Group | null { let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius); let matchingItem: User | Group | null = null; this.users.forEach((currentUser, userId) => { // Let's only check users that are not part of a group - if (typeof currentUser.group !== 'undefined') { + if (typeof currentUser.group !== "undefined") { return; } - if(currentUser === user) { + if (currentUser === user) { return; } if (currentUser.silent) { @@ -264,7 +269,7 @@ export class GameRoom { const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. - if(distance <= minimumDistanceFound && distance <= this.minDistance) { + if (distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; matchingItem = currentUser; } @@ -275,7 +280,7 @@ export class GameRoom { return; } const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); - if(distance <= minimumDistanceFound && distance <= this.groupRadius) { + if (distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; } @@ -284,15 +289,15 @@ export class GameRoom { return matchingItem; } - public static computeDistance(user1: User, user2: User): number - { + public static computeDistance(user1: User, user2: User): number { const user1Position = user1.getPosition(); const user2Position = user2.getPosition(); - return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)); + return Math.sqrt( + Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2) + ); } - public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number - { + public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number { return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); } @@ -304,10 +309,6 @@ export class GameRoom { return this.itemsState; } - public canAccess(userTags: string[]): boolean { - return arrayIntersect(userTags, this.tags); - } - public addZoneListener(call: ZoneSocket, x: number, y: number): Set { return this.positionNotifier.addZoneListener(call, x, y); } @@ -328,4 +329,13 @@ export class GameRoom { public adminLeave(admin: Admin): void { this.admins.delete(admin); } + + public incrementVersion(): number { + this.versionNumber++; + return this.versionNumber; + } + + public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) { + this.positionNotifier.emitEmoteEvent(user, emoteEventMessage); + } } diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index f26a0e0d..5a0f3be6 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,13 +1,12 @@ import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { User } from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {gaugeManager} from "../Services/GaugeManager"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { gaugeManager } from "../Services/GaugeManager"; +import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable"; export class Group implements Movable { - static readonly MAX_PER_GROUP = 4; - private static nextId: number = 1; private id: number; @@ -18,8 +17,13 @@ export class Group implements Movable { private wasDestroyed: boolean = false; private roomId: string; - - constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { + constructor( + roomId: string, + users: User[], + private connectCallback: ConnectCallback, + private disconnectCallback: DisconnectCallback, + private positionNotifier: PositionNotifier + ) { this.roomId = roomId; this.users = new Set(); this.id = Group.nextId; @@ -43,7 +47,7 @@ export class Group implements Movable { return Array.from(this.users.values()); } - getId() : number { + getId(): number { return this.id; } @@ -53,7 +57,7 @@ export class Group implements Movable { getPosition(): PositionInterface { return { x: this.x, - y: this.y + y: this.y, }; } @@ -83,31 +87,29 @@ export class Group implements Movable { if (oldX === undefined) { this.positionNotifier.enter(this); } else { - this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY}); + this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY }); } } isFull(): boolean { - return this.users.size >= Group.MAX_PER_GROUP; + return this.users.size >= MAX_PER_GROUP; } isEmpty(): boolean { return this.users.size <= 1; } - join(user: User): void - { + join(user: User): void { // Broadcast on the right event this.connectCallback(user, this); this.users.add(user); user.group = this; } - leave(user: User): void - { + leave(user: User): void { const success = this.users.delete(user); if (success === false) { - throw new Error("Could not find user "+user.id+" in the group "+this.id); + throw new Error("Could not find user " + user.id + " in the group " + this.id); } user.group = undefined; @@ -123,8 +125,7 @@ export class Group implements Movable { * Let's kick everybody out. * Usually used when there is only one user left. */ - destroy(): void - { + destroy(): void { if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); for (const user of this.users) { this.leave(user); @@ -132,7 +133,7 @@ export class Group implements Movable { this.wasDestroyed = true; } - get getSize(){ + get getSize() { return this.users.size; } } diff --git a/back/src/Model/Movable.ts b/back/src/Model/Movable.ts index 173db0ae..ca586b7c 100644 --- a/back/src/Model/Movable.ts +++ b/back/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import {PositionInterface} from "_Model/PositionInterface"; +import { PositionInterface } from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface + getPosition(): PositionInterface; } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts index d3b0dd47..65636759 100644 --- a/back/src/Model/PositionInterface.ts +++ b/back/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index 6eff17a3..c34c1ef1 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -8,10 +8,12 @@ * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * number of players around the current player. */ -import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; -import {Movable} from "_Model/Movable"; -import {PositionInterface} from "_Model/PositionInterface"; -import {ZoneSocket} from "../RoomManager"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone"; +import { Movable } from "_Model/Movable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { ZoneSocket } from "../RoomManager"; +import { User } from "_Model/User"; +import { EmoteEventMessage } from "../Messages/generated/messages_pb"; interface ZoneDescriptor { i: number; @@ -19,19 +21,24 @@ interface ZoneDescriptor { } export class PositionNotifier { - // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) private zones: Zone[][] = []; - constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback) { - } + constructor( + private zoneWidth: number, + private zoneHeight: number, + private onUserEnters: EntersCallback, + private onUserMoves: MovesCallback, + private onUserLeaves: LeavesCallback, + private onEmote: EmoteCallback + ) {} private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - } + }; } public enter(thing: Movable): void { @@ -77,7 +84,7 @@ export class PositionNotifier { let zone = this.zones[j][i]; if (zone === undefined) { - zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j); + zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j); this.zones[j][i] = zone; } return zone; @@ -93,4 +100,10 @@ export class PositionNotifier { const zone = this.getZone(x, y); zone.removeListener(call); } + + public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) { + const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); + const zone = this.getZone(zoneDesc.i, zoneDesc.j); + zone.emitEmoteEvent(emoteEventMessage); + } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts index 3ac62bca..d1de8800 100644 --- a/back/src/Model/RoomIdentifier.ts +++ b/back/src/Model/RoomIdentifier.ts @@ -1,30 +1,30 @@ //helper functions to parse room IDs export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith('_/')) { + if (roomID.startsWith("_/")) { return true; - } else if(roomID.startsWith('@/')) { + } else if (roomID.startsWith("@/")) { return false; } else { - throw new Error('Incorrect room ID: '+roomID); + throw new Error("Incorrect room ID: " + roomID); } -} +}; export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split('/'); - if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); - return idParts.slice(2).join('/'); -} + const idParts = roomId.split("/"); + if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); + return idParts.slice(2).join("/"); +}; export interface extractDataFromPrivateRoomIdResponse { organizationSlug: string; worldSlug: string; roomSlug: string; } export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split('/'); - if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const idParts = roomId.split("/"); + if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); const organizationSlug = idParts[1]; const worldSlug = idParts[2]; const roomSlug = idParts[3]; - return {organizationSlug, worldSlug, roomSlug} -} \ No newline at end of file + return { organizationSlug, worldSlug, roomSlug }; +}; diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index 51a1a617..186fb32a 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -1,11 +1,17 @@ import { Group } from "./Group"; import { PointInterface } from "./Websocket/PointInterface"; -import {Zone} from "_Model/Zone"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {ServerDuplexStream} from "grpc"; -import {BatchMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { Zone } from "_Model/Zone"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { ServerDuplexStream } from "grpc"; +import { + BatchMessage, + CompanionMessage, + PusherToBackMessage, + ServerToClientMessage, + SubMessage, +} from "../Messages/generated/messages_pb"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; export type UserSocket = ServerDuplexStream; @@ -22,8 +28,10 @@ export class User implements Movable { private positionNotifier: PositionNotifier, public readonly socket: UserSocket, public readonly tags: string[], + public readonly visitCardUrl: string | null, public readonly name: string, - public readonly characterLayers: CharacterLayer[] + public readonly characterLayers: CharacterLayer[], + public readonly companion?: CompanionMessage ) { this.listenedZones = new Set(); @@ -40,9 +48,8 @@ export class User implements Movable { this.positionNotifier.updatePosition(this, position, oldPosition); } - private batchedMessages: BatchMessage = new BatchMessage(); - private batchTimeout: NodeJS.Timeout|null = null; + private batchTimeout: NodeJS.Timeout | null = null; public emitInBatch(payload: SubMessage): void { this.batchedMessages.addPayload(payload); diff --git a/back/src/Model/Websocket/CharacterLayer.ts b/back/src/Model/Websocket/CharacterLayer.ts index 13d838ee..3e428790 100644 --- a/back/src/Model/Websocket/CharacterLayer.ts +++ b/back/src/Model/Websocket/CharacterLayer.ts @@ -1,4 +1,4 @@ export interface CharacterLayer { - name: string, - url: string|undefined + name: string; + url: string | undefined; } diff --git a/back/src/Model/Websocket/ItemEventMessage.ts b/back/src/Model/Websocket/ItemEventMessage.ts index b1f9203e..1bb7f615 100644 --- a/back/src/Model/Websocket/ItemEventMessage.ts +++ b/back/src/Model/Websocket/ItemEventMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = - new tg.IsInterface().withProperties({ +export const isItemEventMessageInterface = new tg.IsInterface() + .withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }).get(); + }) + .get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index ee43d58c..19b57d2e 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -1,7 +1,10 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; -export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { - } +export class Point implements PointInterface { + constructor( + public x: number, + public y: number, + public direction: string = "none", + public moving: boolean = false + ) {} } - diff --git a/back/src/Model/Websocket/PointInterface.ts b/back/src/Model/Websocket/PointInterface.ts index afb07a23..d7c7826e 100644 --- a/back/src/Model/Websocket/PointInterface.ts +++ b/back/src/Model/Websocket/PointInterface.ts @@ -7,11 +7,12 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = - new tg.IsInterface().withProperties({ +export const isPointInterface = new tg.IsInterface() + .withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean - }).get(); + moving: tg.isBoolean, + }) + .get(); export type PointInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/ProtobufUtils.ts b/back/src/Model/Websocket/ProtobufUtils.ts index b85a4257..68817a4f 100644 --- a/back/src/Model/Websocket/ProtobufUtils.ts +++ b/back/src/Model/Websocket/ProtobufUtils.ts @@ -1,34 +1,33 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage + PositionMessage, } from "../../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import Direction = PositionMessage.Direction; -import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; -import {PositionInterface} from "_Model/PositionInterface"; +import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; +import { PositionInterface } from "_Model/PositionInterface"; export class ProtobufUtils { - public static toPositionMessage(point: PointInterface): PositionMessage { let direction: Direction; switch (point.direction) { - case 'up': + case "up": direction = Direction.UP; break; - case 'down': + case "down": direction = Direction.DOWN; break; - case 'left': + case "left": direction = Direction.LEFT; break; - case 'right': + case "right": direction = Direction.RIGHT; break; default: - throw new Error('unexpected direction'); + throw new Error("unexpected direction"); } const position = new PositionMessage(); @@ -44,16 +43,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = 'up'; + direction = "up"; break; case Direction.DOWN: - direction = 'down'; + direction = "down"; break; case Direction.LEFT: - direction = 'left'; + direction = "left"; break; case Direction.RIGHT: - direction = 'right'; + direction = "right"; break; default: throw new Error("Unexpected direction"); @@ -82,7 +81,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - } + }; } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -96,7 +95,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function(characterLayer): CharacterLayerMessage { + return characterLayers.map(function (characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { @@ -107,7 +106,7 @@ export class ProtobufUtils { } public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] { - return characterLayers.map(function(characterLayer): CharacterLayer { + return characterLayers.map(function (characterLayer): CharacterLayer { const url = characterLayer.getUrl(); return { name: characterLayer.getName(), diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index ca695317..d236e489 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -1,37 +1,52 @@ -import {User} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "./Movable"; -import {Group} from "./Group"; -import {ZoneSocket} from "../RoomManager"; +import { User } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "./Movable"; +import { Group } from "./Group"; +import { ZoneSocket } from "../RoomManager"; +import { EmoteEventMessage } from "../Messages/generated/messages_pb"; -export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void; +export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; -export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void; +export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void; +export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; export class Zone { private things: Set = new Set(); private listeners: Set = new Set(); - /** - * @param x For debugging purpose only - * @param y For debugging purpose only - */ - constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, public readonly x: number, public readonly y: number) { - } + constructor( + private onEnters: EntersCallback, + private onMoves: MovesCallback, + private onLeaves: LeavesCallback, + private onEmote: EmoteCallback, + public readonly x: number, + public readonly y: number + ) {} /** * A user/thing leaves the zone */ - public leave(thing: Movable, newZone: Zone|null) { + public leave(thing: Movable, newZone: Zone | null) { const result = this.things.delete(thing); if (!result) { if (thing instanceof User) { - throw new Error('Could not find user in zone '+thing.id); + throw new Error("Could not find user in zone " + thing.id); } if (thing instanceof Group) { - throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')'); + throw new Error( + "Could not find group " + + thing.getId() + + " in zone (" + + this.x + + "," + + this.y + + "). Position of group: (" + + thing.getPosition().x + + "," + + thing.getPosition().y + + ")" + ); } - } this.notifyLeft(thing, newZone); } @@ -39,15 +54,13 @@ export class Zone { /** * Notify listeners of this zone that this user/thing left */ - private notifyLeft(thing: Movable, newZone: Zone|null) { + private notifyLeft(thing: Movable, newZone: Zone | null) { for (const listener of this.listeners) { - //if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) { - this.onLeaves(thing, newZone, listener); - //} + this.onLeaves(thing, newZone, listener); } } - public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { + public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { this.things.add(thing); this.notifyEnter(thing, oldZone, position); } @@ -55,22 +68,12 @@ export class Zone { /** * Notify listeners of this zone that this user entered */ - private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { + private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { for (const listener of this.listeners) { - - /*if (listener === thing) { - continue; - } - if (oldZone === null || !listener.listenedZones.has(oldZone)) { - this.onEnters(thing, listener); - } else { - this.onMoves(thing, position, listener); - }*/ this.onEnters(thing, oldZone, listener); } } - public move(thing: Movable, position: PositionInterface) { if (!this.things.has(thing)) { this.things.add(thing); @@ -80,33 +83,11 @@ export class Zone { for (const listener of this.listeners) { //if (listener !== thing) { - this.onMoves(thing,position, listener); + this.onMoves(thing, position, listener); //} } } - /*public startListening(listener: User): void { - for (const thing of this.things) { - if (thing !== listener) { - this.onEnters(thing, listener); - } - } - - this.listeners.add(listener); - listener.listenedZones.add(this); - } - - public stopListening(listener: User): void { - for (const thing of this.things) { - if (thing !== listener) { - this.onLeaves(thing, listener); - } - } - - this.listeners.delete(listener); - listener.listenedZones.delete(this); - }*/ - public getThings(): Set { return this.things; } @@ -119,4 +100,10 @@ export class Zone { public removeListener(socket: ZoneSocket): void { this.listeners.delete(socket); } + + public emitEmoteEvent(emoteEventMessage: EmoteEventMessage) { + for (const listener of this.listeners) { + this.onEmote(emoteEventMessage, listener); + } + } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 0a8e4a01..9aaf1edb 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -1,94 +1,124 @@ -import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; +import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb"; import { AdminGlobalMessage, AdminMessage, AdminPusherToBackMessage, AdminRoomMessage, BanMessage, + EmotePromptMessage, EmptyMessage, ItemEventMessage, JoinRoomMessage, PlayGlobalMessage, PusherToBackMessage, QueryJitsiJwtMessage, + RefreshRoomPromptMessage, ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, UserMovesMessage, - WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, - ZoneMessage + WebRtcSignalToServerMessage, + WorldFullWarningToRoomMessage, + ZoneMessage, } from "./Messages/generated/messages_pb"; -import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc"; -import {socketManager} from "./Services/SocketManager"; -import {emitError} from "./Services/MessageHelpers"; -import {User, UserSocket} from "./Model/User"; -import {GameRoom} from "./Model/GameRoom"; +import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; +import { socketManager } from "./Services/SocketManager"; +import { emitError } from "./Services/MessageHelpers"; +import { User, UserSocket } from "./Model/User"; +import { GameRoom } from "./Model/GameRoom"; import Debug from "debug"; -import {Admin} from "./Model/Admin"; +import { Admin } from "./Model/Admin"; -const debug = Debug('roommanager'); +const debug = Debug("roommanager"); export type AdminSocket = ServerDuplexStream; export type ZoneSocket = ServerWritableStream; const roomManager: IRoomManagerServer = { joinRoom: (call: UserSocket): void => { - console.log('joinRoom called'); + console.log("joinRoom called"); - let room: GameRoom|null = null; - let user: User|null = null; + let room: GameRoom | null = null; + let user: User | null = null; - call.on('data', (message: PusherToBackMessage) => { + call.on("data", (message: PusherToBackMessage) => { try { if (room === null || user === null) { if (message.hasJoinroommessage()) { - socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => { - room = gameRoom; - user = myUser; - }); + socketManager + .handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage) + .then(({ room: gameRoom, user: myUser }) => { + if (call.writable) { + room = gameRoom; + user = myUser; + } else { + //Connexion may have been closed before the init was finished, so we have to manually disconnect the user. + socketManager.leaveRoom(gameRoom, myUser); + } + }); } else { - throw new Error('The first message sent MUST be of type JoinRoomMessage'); + throw new Error("The first message sent MUST be of type JoinRoomMessage"); } } else { if (message.hasJoinroommessage()) { - throw new Error('Cannot call JoinRoomMessage twice!'); + throw new Error("Cannot call JoinRoomMessage twice!"); } else if (message.hasUsermovesmessage()) { - socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); + socketManager.handleUserMovesMessage( + room, + user, + message.getUsermovesmessage() as UserMovesMessage + ); } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo( + room, + user, + message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing( + room, + user, + message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - }else if (message.hasSendusermessage()) { + } else if (message.hasQueryjitsijwtmessage()) { + socketManager.handleQueryJitsiJwtMessage( + user, + message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage + ); + } else if (message.hasEmotepromptmessage()) { + socketManager.handleEmoteEventMessage( + room, + user, + message.getEmotepromptmessage() as EmotePromptMessage + ); + } else if (message.hasSendusermessage()) { const sendUserMessage = message.getSendusermessage(); - if(sendUserMessage !== undefined) { + if (sendUserMessage !== undefined) { socketManager.handlerSendUserMessage(user, sendUserMessage); } - }else if (message.hasBanusermessage()) { + } else if (message.hasBanusermessage()) { const banUserMessage = message.getBanusermessage(); - if(banUserMessage !== undefined) { + if (banUserMessage !== undefined) { socketManager.handlerBanUserMessage(room, user, banUserMessage); } } else { - throw new Error('Unhandled message type'); + throw new Error("Unhandled message type"); } } } catch (e) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (user !== null && room !== null) { socketManager.leaveRoom(room, user); } @@ -97,41 +127,40 @@ const roomManager: IRoomManagerServer = { user = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinRoom stream:", err); }); - }, listenZone(call: ZoneSocket): void { - debug('listenZone called'); + debug("listenZone called"); const zoneMessage = call.request; socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - call.on('cancelled', () => { - debug('listenZone cancelled'); + call.on("cancelled", () => { + debug("listenZone cancelled"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); - }) - - call.on('close', () => { - debug('listenZone connection closed'); + }); + + call.on("close", () => { + debug("listenZone connection closed"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - }).on('error', (e) => { - console.error('An error occurred in listenZone stream:', e); + }).on("error", (e) => { + console.error("An error occurred in listenZone stream:", e); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); }); }, adminRoom(call: AdminSocket): void { - console.log('adminRoom called'); + console.log("adminRoom called"); const admin = new Admin(call); - let room: GameRoom|null = null; + let room: GameRoom | null = null; - call.on('data', (message: AdminPusherToBackMessage) => { + call.on("data", (message: AdminPusherToBackMessage) => { try { if (room === null) { if (message.hasSubscribetoroom()) { @@ -140,18 +169,17 @@ const roomManager: IRoomManagerServer = { room = gameRoom; }); } else { - throw new Error('The first message sent MUST be of type JoinRoomMessage'); + throw new Error("The first message sent MUST be of type JoinRoomMessage"); } } } catch (e) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (room !== null) { socketManager.leaveAdminRoom(room, admin); } @@ -159,18 +187,21 @@ const roomManager: IRoomManagerServer = { room = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinAdminRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinAdminRoom stream:", err); }); }, sendAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - - socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()); + socketManager.sendAdminMessage( + call.request.getRoomid(), + call.request.getRecipientuuid(), + call.request.getMessage() + ); callback(null, new EmptyMessage()); }, sendGlobalAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - throw new Error('Not implemented yet'); + throw new Error("Not implemented yet"); // TODO callback(null, new EmptyMessage()); }, @@ -184,10 +215,20 @@ const roomManager: IRoomManagerServer = { socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); callback(null, new EmptyMessage()); }, - sendWorldFullWarningToRoom(call: ServerUnaryCall, callback: sendUnaryData): void { + sendWorldFullWarningToRoom( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { socketManager.dispatchWorlFullWarning(call.request.getRoomid()); callback(null, new EmptyMessage()); }, + sendRefreshRoomPrompt( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { + socketManager.dispatchRoomRefresh(call.request.getRoomid()); + callback(null, new EmptyMessage()); + }, }; -export {roomManager}; +export { roomManager }; diff --git a/back/src/Server/server/app.ts b/back/src/Server/server/app.ts index 3b98a9b3..4c422d5c 100644 --- a/back/src/Server/server/app.ts +++ b/back/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { App as _App, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/back/src/Server/server/baseapp.ts b/back/src/Server/server/baseapp.ts index accd8a99..6d973ac7 100644 --- a/back/src/Server/server/baseapp.ts +++ b/back/src/Server/server/baseapp.ts @@ -1,116 +1,109 @@ -import { Readable } from 'stream'; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { Readable } from "stream"; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; -import formData from './formdata'; -import { stob } from './utils'; -import { Handler } from './types'; -import {join} from "path"; +import formData from "./formdata"; +import { stob } from "./utils"; +import { Handler } from "./types"; +import { join } from "path"; -const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; +const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"]; const noOp = () => true; const handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader('content-type'); + const contType = req.getHeader("content-type"); - res.bodyStream = function() { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function () { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // uint and then slicing is bit faster than slice and then uint - stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // uint and then slicing is bit faster than slice and then uint + stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes('application/json')) - res.json = async () => JSON.parse(await res.body()); - if (contTypes.map(t => contType.includes(t)).includes(true)) - res.formData = formData.bind(res, contType); + if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); + if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp['ws']; - get!: TemplatedApp['get']; - _post!: TemplatedApp['post']; - _put!: TemplatedApp['put']; - _patch!: TemplatedApp['patch']; - _listen!: TemplatedApp['listen']; + _sockets = new Map(); + ws!: TemplatedApp["ws"]; + get!: TemplatedApp["get"]; + _post!: TemplatedApp["post"]; + _put!: TemplatedApp["put"]; + _patch!: TemplatedApp["patch"]; + _listen!: TemplatedApp["listen"]; - post(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === 'number' && typeof h === 'string') { - this._listen(h, p, socket => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error('cb undefined'); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === "number" && typeof h === "string") { + this._listen(h, p, (socket) => { + this._sockets.set(p, socket); + if (cb === undefined) { + throw new Error("cb undefined"); + } + cb(socket); + }); + } else if (typeof h === "number" && typeof p === "function") { + this._listen(h, (socket) => { + this._sockets.set(h, socket); + p(socket); + }); + } else { + throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)"); } - cb(socket); - }); - } else if (typeof h === 'number' && typeof p === 'function') { - this._listen(h, socket => { - this._sockets.set(h, socket); - p(socket); - }); - } else { - throw Error( - 'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)' - ); + + return this; } - return this; - } - - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach(app => { - us_listen_socket_close(app); - }); - this._sockets.clear(); + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach((app) => { + us_listen_socket_close(app); + }); + this._sockets.clear(); + } + return this; } - return this; - } } export default BaseApp; diff --git a/back/src/Server/server/formdata.ts b/back/src/Server/server/formdata.ts index 9dd08440..66e51db4 100644 --- a/back/src/Server/server/formdata.ts +++ b/back/src/Server/server/formdata.ts @@ -1,100 +1,99 @@ -import { createWriteStream } from 'fs'; -import { join, dirname } from 'path'; -import Busboy from 'busboy'; -import mkdirp from 'mkdirp'; +import { createWriteStream } from "fs"; +import { join, dirname } from "path"; +import Busboy from "busboy"; +import mkdirp from "mkdirp"; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log('Enter form data'); - options.headers = { - 'content-type': contType - }; + console.log("Enter form data"); + options.headers = { + "content-type": contType, + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on('limit', () => { - if (options.abortOnLimit) { - reject(Error('limit')); - } + busb.on("limit", () => { + if (options.abortOnLimit) { + reject(Error("limit")); + } + }); + + busb.on("file", function (fieldname, file, filename, encoding, mimetype) { + const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = { + filename, + encoding, + mimetype, + filePath: undefined, + }; + + if (typeof options.tmpDir === "string") { + if (typeof options.filename === "function") filename = options.filename(filename); + const fileToSave = join(options.tmpDir, filename); + mkdirp(dirname(fileToSave)); + + file.pipe(createWriteStream(fileToSave)); + value.filePath = fileToSave; + } + if (typeof options.onFile === "function") { + value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; + } + + setRetValue(ret, fieldname, value); + }); + + busb.on("field", function (fieldname, value) { + if (typeof options.onField === "function") options.onField(fieldname, value); + + setRetValue(ret, fieldname, value); + }); + + busb.on("finish", function () { + resolve(ret); + }); + + busb.on("error", reject); }); - - busb.on('file', function(fieldname, file, filename, encoding, mimetype) { - const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { - filename, - encoding, - mimetype, - filePath: undefined - }; - - if (typeof options.tmpDir === 'string') { - if (typeof options.filename === 'function') filename = options.filename(filename); - const fileToSave = join(options.tmpDir, filename); - mkdirp(dirname(fileToSave)); - - file.pipe(createWriteStream(fileToSave)); - value.filePath = fileToSave; - } - if (typeof options.onFile === 'function') { - value.filePath = - options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; - } - - setRetValue(ret, fieldname, value); - }); - - busb.on('field', function(fieldname, value) { - if (typeof options.onField === 'function') options.onField(fieldname, value); - - setRetValue(ret, fieldname, value); - }); - - busb.on('finish', function() { - resolve(ret); - }); - - busb.on('error', reject); - }); } function setRetValue( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith('[]')) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); + if (fieldname.endsWith("[]")) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else { + ret[fieldname] = [value]; + } } else { - ret[fieldname] = [value]; + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else if (ret[fieldname]) { + ret[fieldname] = [ret[fieldname], value]; + } else { + ret[fieldname] = value; + } } - } else { - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); - } else if (ret[fieldname]) { - ret[fieldname] = [ret[fieldname], value]; - } else { - ret[fieldname] = value; - } - } } export default formData; diff --git a/back/src/Server/server/sslapp.ts b/back/src/Server/server/sslapp.ts index 46ae89a5..80df0e4a 100644 --- a/back/src/Server/server/sslapp.ts +++ b/back/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/back/src/Server/server/types.ts b/back/src/Server/server/types.ts index 3d0f48c7..afc21d17 100644 --- a/back/src/Server/server/types.ts +++ b/back/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/back/src/Server/server/utils.ts b/back/src/Server/server/utils.ts index 80ea3938..dc813064 100644 --- a/back/src/Server/server/utils.ts +++ b/back/src/Server/server/utils.ts @@ -1,37 +1,36 @@ -import { ReadStream } from 'fs'; +import { ReadStream } from "fs"; -function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( - Object.keys(from) - ); - ownProps.forEach(prop => { - if (prop === 'constructor' || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extend(who: any, from: any, overwrite = true) { + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); + ownProps.forEach((prop) => { + if (prop === "constructor" || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise(resolve => { - const buffers: Buffer[] = []; - stream.on('data', buffers.push.bind(buffers)); + return new Promise((resolve) => { + const buffers: Buffer[] = []; + stream.on("data", buffers.push.bind(buffers)); - stream.on('end', () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } + stream.on("end", () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } + }); }); - }); } export { extend, stob }; diff --git a/back/src/Server/sifrr.server.ts b/back/src/Server/sifrr.server.ts index 47fba02c..4ef03721 100644 --- a/back/src/Server/sifrr.server.ts +++ b/back/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from 'query-string'; -import { HttpRequest } from 'uWebSockets.js'; -import App from './server/app'; -import SSLApp from './server/sslapp'; -import * as types from './server/types'; +import { parse } from "query-string"; +import { HttpRequest } from "uWebSockets.js"; +import App from "./server/app"; +import SSLApp from "./server/sslapp"; +import * as types from "./server/types"; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from './server/types'; +export * from "./server/types"; export default { - App, - SSLApp, - getQuery, - ...types + App, + SSLApp, + getQuery, + ...types, }; diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts deleted file mode 100644 index ef969a76..00000000 --- a/back/src/Services/AdminApi.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; -import Axios from "axios"; - -export interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - tags: string[] - policy_type: number - userUuid: string - messages?: unknown[], - textures: CharacterTexture[] -} - -export interface CharacterTexture { - id: number, - level: number, - url: string, - rights: string -} - -class AdminApi { - - async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - - const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { - organizationSlug, - worldSlug - }; - - if (roomSlug) { - params.roomSlug = roomSlug; - } - - const res = await Axios.get(ADMIN_API_URL + '/api/map', - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`}, - params - } - ) - return res.data; - } -} - -export const adminApi = new AdminApi(); diff --git a/back/src/Services/ArrayHelper.ts b/back/src/Services/ArrayHelper.ts index 67321d1b..8af1da9f 100644 --- a/back/src/Services/ArrayHelper.ts +++ b/back/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file +export const arrayIntersect = (array1: string[], array2: string[]): boolean => { + return array1.filter((value) => array2.includes(value)).length > 0; +}; diff --git a/back/src/Services/ClientEventsEmitter.ts b/back/src/Services/ClientEventsEmitter.ts index 381137a1..0f56d55c 100644 --- a/back/src/Services/ClientEventsEmitter.ts +++ b/back/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); -const clientJoinEvent = 'clientJoin'; -const clientLeaveEvent = 'clientLeave'; +const clientJoinEvent = "clientJoin"; +const clientLeaveEvent = "clientLeave"; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { diff --git a/back/src/Services/CpuTracker.ts b/back/src/Services/CpuTracker.ts index c7d57f3d..3d06ca70 100644 --- a/back/src/Services/CpuTracker.ts +++ b/back/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; +import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array|number) { +function secNSec2ms(secNSec: Array | number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() + let time = process.hrtime.bigint(); + let usage = process.cpuUsage(); setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() + const elapUsage = process.cpuUsage(usage); + usage = process.cpuUsage(); const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) + const elapUserMS = secNSec2ms(elapUsage.user); + const elapSystMS = secNSec2ms(elapUsage.system); + this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); time = elapTime; diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts index 80712856..6d2183d8 100644 --- a/back/src/Services/GaugeManager.ts +++ b/back/src/Services/GaugeManager.ts @@ -1,4 +1,4 @@ -import {Counter, Gauge} from "prom-client"; +import { Counter, Gauge } from "prom-client"; //this class should manage all the custom metrics used by prometheus class GaugeManager { @@ -10,29 +10,29 @@ class GaugeManager { constructor() { this.nbRoomsGauge = new Gauge({ - name: 'workadventure_nb_rooms', - help: 'Number of active rooms' + name: "workadventure_nb_rooms", + help: "Number of active rooms", }); this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + name: "workadventure_nb_sockets", + help: "Number of connected sockets", + labelNames: [], }); this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] + name: "workadventure_nb_clients_per_room", + help: "Number of clients per room", + labelNames: ["room"], }); this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] + name: "workadventure_counter_groups_per_room", + help: "Counter of groups per room", + labelNames: ["room"], }); this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] + name: "workadventure_nb_groups_per_room", + help: "Number of groups per room", + labelNames: ["room"], }); } @@ -54,13 +54,13 @@ class GaugeManager { } incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) + this.nbGroupsPerRoomCounter.inc({ room: roomId }); + this.nbGroupsPerRoomGauge.inc({ room: roomId }); } - + decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) + this.nbGroupsPerRoomGauge.dec({ room: roomId }); } } -export const gaugeManager = new GaugeManager(); \ No newline at end of file +export const gaugeManager = new GaugeManager(); diff --git a/back/src/Services/MessageHelpers.ts b/back/src/Services/MessageHelpers.ts index b2600a4a..493f7173 100644 --- a/back/src/Services/MessageHelpers.ts +++ b/back/src/Services/MessageHelpers.ts @@ -1,5 +1,5 @@ -import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb"; -import {UserSocket} from "_Model/User"; +import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb"; +import { UserSocket } from "_Model/User"; export function emitError(Client: UserSocket, message: string): void { const errorMessage = new ErrorMessage(); @@ -9,7 +9,7 @@ export function emitError(Client: UserSocket, message: string): void { serverToClientMessage.setErrormessage(errorMessage); //if (!Client.disconnecting) { - Client.write(serverToClientMessage); + Client.write(serverToClientMessage); //} console.warn(message); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 40023833..e61763cd 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -1,4 +1,4 @@ -import {GameRoom} from "../Model/GameRoom"; +import { GameRoom } from "../Model/GameRoom"; import { ItemEventMessage, ItemStateMessage, @@ -26,40 +26,40 @@ import { GroupLeftZoneMessage, WorldFullWarningMessage, UserLeftZoneMessage, + EmoteEventMessage, BanUserMessage, + RefreshRoomMessage, + EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {User, UserSocket} from "../Model/User"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {Group} from "../Model/Group"; -import {cpuTracker} from "./CpuTracker"; +import { User, UserSocket } from "../Model/User"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { Group } from "../Model/Group"; +import { cpuTracker } from "./CpuTracker"; import { GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY, - TURN_STATIC_AUTH_SECRET + TURN_STATIC_AUTH_SECRET, } from "../Enum/EnvironmentVariable"; -import {Movable} from "../Model/Movable"; -import {PositionInterface} from "../Model/PositionInterface"; -import {adminApi, CharacterTexture} from "./AdminApi"; +import { Movable } from "../Model/Movable"; +import { PositionInterface } from "../Model/PositionInterface"; import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {ZoneSocket} from "../RoomManager"; -import {Zone} from "_Model/Zone"; +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { clientEventsEmitter } from "./ClientEventsEmitter"; +import { gaugeManager } from "./GaugeManager"; +import { ZoneSocket } from "../RoomManager"; +import { Zone } from "_Model/Zone"; import Debug from "debug"; -import {Admin} from "_Model/Admin"; +import { Admin } from "_Model/Admin"; import crypto from "crypto"; - -const debug = Debug('sockermanager'); +const debug = Debug("sockermanager"); function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { // TODO: should we batch those every 100ms? const batchMessage = new BatchToPusherMessage(); batchMessage.addPayload(subMessage); - socket.write(batchMessage); } @@ -76,11 +76,20 @@ export class SocketManager { }); } - public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { - + public async handleJoinRoom( + socket: UserSocket, + joinRoomMessage: JoinRoomMessage + ): Promise<{ room: GameRoom; user: User }> { //join new previous room - const {room, user} = await this.joinRoom(socket, joinRoomMessage); + const { room, user } = await this.joinRoom(socket, joinRoomMessage); + if (!socket.writable) { + console.warn("Socket was aborted"); + return { + room, + user, + }; + } const roomJoinedMessage = new RoomJoinedMessage(); roomJoinedMessage.setTagList(joinRoomMessage.getTagList()); @@ -96,14 +105,12 @@ export class SocketManager { const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); - console.log('SENDING MESSAGE roomJoinedMessage'); socket.write(serverToClientMessage); return { room, - user + user, }; - } handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { @@ -117,22 +124,13 @@ export class SocketManager { } if (position === undefined) { - throw new Error('Position not found in message'); + throw new Error("Position not found in message"); } const viewport = userMoves.viewport; if (viewport === undefined) { - throw new Error('Viewport not found in message'); + throw new Error("Viewport not found in message"); } - // sending to all clients in room except sender - /*client.position = { - x: position.x, - y: position.y, - direction, - moving: position.moving, - }; - client.viewport = viewport;*/ - // update position in the world room.updatePosition(user, ProtobufUtils.toPointInterface(position)); //room.setViewport(client, client.viewport); @@ -186,26 +184,15 @@ export class SocketManager { } } - // TODO: handle this message in pusher - /*async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { - try { - const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); - if (!reportedSocket) { - throw 'reported socket user not found'; - } - //TODO report user on admin application - await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid) - } catch (e) { - console.error('An error occurred on "handleReportMessage"'); - console.error(e); - } - }*/ - emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); if (remoteUser === undefined) { - console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + console.warn( + "While exchanging a WebRTC signal: client with id ", + data.getReceiverid(), + " does not exist. This might be a race condition." + ); return; } @@ -213,8 +200,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -223,7 +210,7 @@ export class SocketManager { serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); + remoteUser.socket.write(serverToClientMessage); //} } @@ -231,7 +218,11 @@ export class SocketManager { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); if (remoteUser === undefined) { - console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + console.warn( + "While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", + data.getReceiverid(), + " does not exist. This might be a race condition." + ); return; } @@ -239,8 +230,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -249,11 +240,11 @@ export class SocketManager { serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); + remoteUser.socket.write(serverToClientMessage); //} } - leaveRoom(room: GameRoom, user: User){ + leaveRoom(room: GameRoom, user: User) { // leave previous room and world try { //user leave previous world @@ -264,40 +255,40 @@ export class SocketManager { debug('Room is empty. Deleting room "%s"', room.roomId); } } finally { - //delete Client.roomId; - //this.sockets.delete(Client.userId); clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); - console.log('A user left'); + console.log("A user left"); } } async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.rooms.get(roomId) - if(world === undefined){ + let world = this.rooms.get(roomId); + if (world === undefined) { world = new GameRoom( roomId, (user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.disConnectedUser(user, group), MINIMUM_DISTANCE, GROUP_RADIUS, - (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener), - (thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), - (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener) + (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => + this.onZoneEnter(thing, fromZone, listener), + (thing: Movable, position: PositionInterface, listener: ZoneSocket) => + this.onClientMove(thing, position, listener), + (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => + this.onClientLeave(thing, newZone, listener), + (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => + this.onEmote(emoteEventMessage, listener) ); - if (!world.anonymous) { - const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) - world.tags = data.tags - world.policyType = Number(data.policy_type) - } gaugeManager.incNbRoomGauge(); this.rooms.set(roomId, world); } - return Promise.resolve(world) + return Promise.resolve(world); } - private async joinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { - + private async joinRoom( + socket: UserSocket, + joinRoomMessage: JoinRoomMessage + ): Promise<{ room: GameRoom; user: User }> { const roomId = joinRoomMessage.getRoomid(); const room = await socketManager.getOrCreateRoom(roomId); @@ -306,21 +297,25 @@ export class SocketManager { const user = room.join(socket, joinRoomMessage); clientEventsEmitter.emitClientJoin(user.uuid, roomId); - console.log(new Date().toISOString() + ' A user joined'); - return {room, user}; + console.log(new Date().toISOString() + " A user joined"); + return { room, user }; } - private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) { + private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) { if (thing instanceof User) { const userJoinedZoneMessage = new UserJoinedZoneMessage(); if (!Number.isInteger(thing.id)) { - throw new Error('clientUser.userId is not an integer '+thing.id); + throw new Error("clientUser.userId is not an integer " + thing.id); } userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone)); + if (thing.visitCardUrl) { + userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl); + } + userJoinedZoneMessage.setCompanion(thing.companion); const subMessage = new SubToPusherMessage(); subMessage.setUserjoinedzonemessage(userJoinedZoneMessage); @@ -330,11 +325,11 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, fromZone, thing); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onClientMove(thing: Movable, position:PositionInterface, listener: ZoneSocket): void { + private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void { if (thing instanceof User) { const userMovedMessage = new UserMovedMessage(); userMovedMessage.setUserid(thing.id); @@ -349,21 +344,28 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, null, thing); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onClientLeave(thing: Movable, newZone: Zone|null, listener: ZoneSocket) { + private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) { if (thing instanceof User) { this.emitUserLeftEvent(listener, thing.id, newZone); } else if (thing instanceof Group) { this.emitDeleteGroupEvent(listener, thing.getId(), newZone); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void { + private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) { + const subMessage = new SubToPusherMessage(); + subMessage.setEmoteeventmessage(emoteEventMessage); + + emitZoneMessage(subMessage, client); + } + + private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void { const position = group.getPosition(); const pointMessage = new PointMessage(); pointMessage.setX(Math.floor(position.x)); @@ -381,7 +383,7 @@ export class SocketManager { //client.emitInBatch(subMessage); } - private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone|null): void { + private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void { const groupDeleteMessage = new GroupLeftZoneMessage(); groupDeleteMessage.setGroupid(groupId); groupDeleteMessage.setTozone(this.toProtoZone(newZone)); @@ -393,7 +395,7 @@ export class SocketManager { //user.emitInBatch(subMessage); } - private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone|null): void { + private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void { const userLeftMessage = new UserLeftZoneMessage(); userLeftMessage.setUserid(userId); userLeftMessage.setTozone(this.toProtoZone(newZone)); @@ -404,7 +406,7 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private toProtoZone(zone: Zone|null): ProtoZone|undefined { + private toProtoZone(zone: Zone | null): ProtoZone | undefined { if (zone !== null) { const zoneMessage = new ProtoZone(); zoneMessage.setX(zone.x); @@ -415,11 +417,6 @@ export class SocketManager { } private joinWebRtcRoom(user: User, group: Group) { - /*const roomId: string = "webrtcroom"+group.getId(); - if (user.socket.webRtcRoomId === roomId) { - return; - }*/ - for (const otherUser of group.getUsers()) { if (user === otherUser) { continue; @@ -430,8 +427,8 @@ export class SocketManager { webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage1.setWebrtcusername(username); webrtcStartMessage1.setWebrtcpassword(password); } @@ -440,16 +437,16 @@ export class SocketManager { serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage1); - //console.log('Sending webrtcstart initiator to '+user.socket.userId) + user.socket.write(serverToClientMessage1); + //console.log('Sending webrtcstart initiator to '+user.socket.userId) //} const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage2.setWebrtcusername(username); webrtcStartMessage2.setWebrtcpassword(password); } @@ -458,10 +455,9 @@ export class SocketManager { serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage2); - //console.log('Sending webrtcstart to '+otherUser.socket.userId) + otherUser.socket.write(serverToClientMessage2); + //console.log('Sending webrtcstart to '+otherUser.socket.userId) //} - } } @@ -470,17 +466,17 @@ export class SocketManager { * and the Coturn server. * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` */ - private getTURNCredentials(name: string, secret: string): {username: string, password: string} { - const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours - const username = [unixTimeStamp, name].join(':'); - const hmac = crypto.createHmac('sha1', secret); - hmac.setEncoding('base64'); + private getTURNCredentials(name: string, secret: string): { username: string; password: string } { + const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours + const username = [unixTimeStamp, name].join(":"); + const hmac = crypto.createHmac("sha1", secret); + hmac.setEncoding("base64"); hmac.write(username); hmac.end(); const password = hmac.read(); return { username: username, - password: password + password: password, }; } @@ -503,10 +499,9 @@ export class SocketManager { serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage1); + otherUser.socket.write(serverToClientMessage1); //} - const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); webrtcDisconnectMessage2.setUserid(otherUser.id); @@ -514,7 +509,7 @@ export class SocketManager { serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage2); + user.socket.write(serverToClientMessage2); //} } } @@ -531,53 +526,41 @@ export class SocketManager { console.error('An error occurred on "emitPlayGlobalMessage" event'); console.error(e); } - } public getWorlds(): Map { return this.rooms; } - /** - * - * @param token - */ - /*searchClientByUuid(uuid: string): ExSocketInterface | null { - for(const socket of this.sockets.values()){ - if(socket.userUuid === uuid){ - return socket; - } - } - return null; - }*/ - - public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) { const room = queryJitsiJwtMessage.getJitsiroom(); const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. - if (SECRET_JITSI_KEY === '') { - throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); + if (SECRET_JITSI_KEY === "") { + throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi."); } // Let's see if the current client has const isAdmin = user.tags.includes(tag); - const jwt = Jwt.sign({ - "aud": "jitsi", - "iss": JITSI_ISS, - "sub": JITSI_URL, - "room": room, - "moderator": isAdmin - }, SECRET_JITSI_KEY, { - expiresIn: '1d', - algorithm: "HS256", - header: - { - "alg": "HS256", - "typ": "JWT" - } - }); + const jwt = Jwt.sign( + { + aud: "jitsi", + iss: JITSI_ISS, + sub: JITSI_URL, + room: room, + moderator: isAdmin, + }, + SECRET_JITSI_KEY, + { + expiresIn: "1d", + algorithm: "HS256", + header: { + alg: "HS256", + typ: "JWT", + }, + } + ); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -589,7 +572,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){ + public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); sendUserMessage.setType(sendUserMessageToSend.getType()); @@ -599,7 +582,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){ + public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(banUserMessageToSend.getMessage()); banUserMessage.setType(banUserMessageToSend.getType()); @@ -619,7 +602,7 @@ export class SocketManager { public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In addZoneListener, could not find room with id '" + roomId + "'"); + console.error("In addZoneListener, could not find room with id '" + roomId + "'"); return; } @@ -634,6 +617,10 @@ export class SocketManager { userJoinedMessage.setName(thing.name); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); + if (thing.visitCardUrl) { + userJoinedMessage.setVisitcardurl(thing.visitCardUrl); + } + userJoinedMessage.setCompanion(thing.companion); const subMessage = new SubToPusherMessage(); subMessage.setUserjoinedzonemessage(userJoinedMessage); @@ -659,7 +646,7 @@ export class SocketManager { removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) { const room = this.rooms.get(roomId); if (!room) { - console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); + console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); return; } @@ -674,7 +661,7 @@ export class SocketManager { return room; } - public leaveAdminRoom(room: GameRoom, admin: Admin){ + public leaveAdminRoom(room: GameRoom, admin: Admin) { room.adminLeave(admin); if (room.isEmpty()) { this.rooms.delete(room.roomId); @@ -686,19 +673,27 @@ export class SocketManager { public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In sendAdminMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } const recipient = room.getUserByUuid(recipientUuid); if (recipient === undefined) { - console.error("In sendAdminMessage, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminMessage, could not find user with id '" + + recipientUuid + + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" + ); return; } const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('ban'); //todo: is the type correct? + sendUserMessage.setType("ban"); //todo: is the type correct? const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setSendusermessage(sendUserMessage); @@ -709,13 +704,21 @@ export class SocketManager { public banUser(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In banUser, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } const recipient = room.getUserByUuid(recipientUuid); if (recipient === undefined) { - console.error("In banUser, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); + console.error( + "In banUser, could not find user with id '" + + recipientUuid + + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" + ); return; } @@ -724,7 +727,7 @@ export class SocketManager { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(message); - banUserMessage.setType('banned'); + banUserMessage.setType("banned"); const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setBanusermessage(banUserMessage); @@ -734,19 +737,22 @@ export class SocketManager { recipient.socket.end(); } - sendAdminRoomMessage(roomId: string, message: string) { const room = this.rooms.get(roomId); if (!room) { //todo: this should cause the http call to return a 500 - console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminRoomMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } room.getUsers().forEach((recipient) => { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('message'); + sendUserMessage.setType("message"); const clientMessage = new ServerToClientMessage(); clientMessage.setSendusermessage(sendUserMessage); @@ -755,14 +761,18 @@ export class SocketManager { }); } - dispatchWorlFullWarning(roomId: string,): void { + dispatchWorlFullWarning(roomId: string): void { const room = this.rooms.get(roomId); if (!room) { //todo: this should cause the http call to return a 500 - console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminRoomMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } - + room.getUsers().forEach((recipient) => { const worldFullMessage = new WorldFullWarningMessage(); @@ -772,6 +782,32 @@ export class SocketManager { recipient.socket.write(clientMessage); }); } + + dispatchRoomRefresh(roomId: string): void { + const room = this.rooms.get(roomId); + if (!room) { + return; + } + + const versionNumber = room.incrementVersion(); + room.getUsers().forEach((recipient) => { + const worldFullMessage = new RefreshRoomMessage(); + worldFullMessage.setRoomid(roomId); + worldFullMessage.setVersionnumber(versionNumber); + + const clientMessage = new ServerToClientMessage(); + clientMessage.setRefreshroommessage(worldFullMessage); + + recipient.socket.write(clientMessage); + }); + } + + handleEmoteEventMessage(room: GameRoom, user: User, emotePromptMessage: EmotePromptMessage) { + const emoteEventMessage = new EmoteEventMessage(); + emoteEventMessage.setEmote(emotePromptMessage.getEmote()); + emoteEventMessage.setActoruserid(user.id); + room.emitEmoteEvent(user, emoteEventMessage); + } } export const socketManager = new SocketManager(); diff --git a/back/tests/GameRoomTest.ts b/back/tests/GameRoomTest.ts index 45721334..6bdc6912 100644 --- a/back/tests/GameRoomTest.ts +++ b/back/tests/GameRoomTest.ts @@ -5,6 +5,7 @@ import {Group} from "../src/Model/Group"; import {User, UserSocket} from "_Model/User"; import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; +import {EmoteCallback} from "_Model/Zone"; function createMockUser(userId: number): User { return { @@ -33,6 +34,8 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess return joinRoomMessage; } +const emote: EmoteCallback = (emoteEventMessage, listener): void => {} + describe("GameRoom", () => { it("should connect user1 and user2", () => { let connectCalledNumber: number = 0; @@ -43,7 +46,8 @@ describe("GameRoom", () => { } - const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote); @@ -72,7 +76,7 @@ describe("GameRoom", () => { } - const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100)); @@ -101,7 +105,7 @@ describe("GameRoom", () => { disconnectCallNumber++; } - const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100)); diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index 5901202f..1aaf2e13 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -1,10 +1,6 @@ import "jasmine"; -import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom"; -import {Point} from "../src/Model/Websocket/MessageUserPosition"; -import { Group } from "../src/Model/Group"; import {PositionNotifier} from "../src/Model/PositionNotifier"; import {User, UserSocket} from "../src/Model/User"; -import {PointInterface} from "../src/Model/Websocket/PointInterface"; import {Zone} from "_Model/Zone"; import {Movable} from "_Model/Movable"; import {PositionInterface} from "_Model/PositionInterface"; @@ -23,21 +19,21 @@ describe("PositionNotifier", () => { moveTriggered = true; }, (thing: Movable) => { leaveTriggered = true; - }); + }, () => {}); const user1 = new User(1, 'test', '10.0.0.2', { x: 500, y: 500, moving: false, direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], 'foo', []); + }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); const user2 = new User(2, 'test', '10.0.0.2', { x: -9999, y: -9999, moving: false, direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], 'foo', []); + }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); positionNotifier.addZoneListener({} as ZoneSocket, 0, 0); positionNotifier.addZoneListener({} as ZoneSocket, 0, 1); @@ -98,21 +94,21 @@ describe("PositionNotifier", () => { moveTriggered = true; }, (thing: Movable) => { leaveTriggered = true; - }); + }, () => {}); const user1 = new User(1, 'test', '10.0.0.2', { x: 500, y: 500, moving: false, direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], 'foo', []); + }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); const user2 = new User(2, 'test', '10.0.0.2', { x: 0, y: 0, moving: false, direction: 'down' - }, false, positionNotifier, {} as UserSocket, [], 'foo', []); + }, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); const listener = {} as ZoneSocket; positionNotifier.addZoneListener(listener, 0, 0); diff --git a/back/yarn.lock b/back/yarn.lock index 501146cb..242728db 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -117,6 +117,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" @@ -197,6 +202,14 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.10.0, ajv@^6.10.2: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" @@ -207,6 +220,11 @@ ajv@^6.10.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-escapes@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" @@ -214,6 +232,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.11.0" +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -241,6 +266,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -257,11 +289,6 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -append-field@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" - integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= - aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -330,6 +357,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -370,22 +402,6 @@ bintrees@1.0.1: resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= -body-parser@^1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -410,7 +426,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -427,14 +443,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -busboy@^0.2.11: - version "0.2.14" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" - integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= - dependencies: - dicer "0.2.5" - readable-stream "1.1.x" - busboy@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" @@ -449,11 +457,6 @@ bytebuffer@~5: dependencies: long "~3" -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -509,6 +512,14 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -549,6 +560,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -556,6 +572,14 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -607,11 +631,21 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + colour@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -622,26 +656,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.5.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -652,6 +671,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -663,6 +693,15 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -678,7 +717,7 @@ dateformat@~1.0.4-1.2.3: get-stdin "^4.0.1" meow "^3.3.0" -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: +debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -716,6 +755,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -753,24 +797,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= -dicer@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" - integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= - dependencies: - readable-stream "1.1.x" - streamsearch "0.1.2" - dicer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" @@ -804,11 +835,6 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -819,7 +845,14 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -error-ex@^1.2.0: +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -944,6 +977,21 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -1133,11 +1181,21 @@ generic-type-guard@^3.2.0: resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1" integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ== +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -1251,32 +1309,21 @@ has-values@^1.0.0: kind-of "^4.0.0" hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== http-status-codes@*: version "2.1.4" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== -http-status-codes@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477" - integrity sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -1303,6 +1350,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -1315,6 +1370,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1323,16 +1383,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - ini@~1.3.0: version "1.3.7" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" @@ -1490,6 +1545,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -1497,6 +1557,21 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -1507,11 +1582,6 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1534,11 +1604,6 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -iterall@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" - integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== - jasmine-core@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" @@ -1565,6 +1630,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1647,6 +1717,45 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -1704,9 +1813,27 @@ lodash.once@^4.0.0: integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" long@~3: version "3.2.0" @@ -1743,11 +1870,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -1764,6 +1886,11 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.2.3: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1788,17 +1915,13 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: - mime-db "1.44.0" + braces "^3.0.1" + picomatch "^2.2.3" mimic-fn@^2.1.0: version "2.1.0" @@ -1862,20 +1985,6 @@ ms@2.1.2, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -multer@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" - integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== - dependencies: - append-field "^1.0.0" - busboy "^0.2.11" - concat-stream "^1.5.2" - mkdirp "^0.5.1" - object-assign "^4.1.1" - on-finished "^2.3.0" - type-is "^1.6.4" - xtend "^4.0.0" - mute-stream@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" @@ -1982,6 +2091,13 @@ npm-packlist@^1.1.6: npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -1997,7 +2113,7 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -2025,13 +2141,6 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -on-finished@^2.3.0, on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2039,7 +2148,7 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -2088,6 +2197,13 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -2102,6 +2218,16 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -2129,6 +2255,11 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -2143,11 +2274,21 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2165,6 +2306,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -2175,6 +2323,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -2207,11 +2360,6 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - query-string@^6.13.3: version "6.13.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.4.tgz#b35a9a3bd4955bce55f94feb0e819b3d0be6f66f" @@ -2221,16 +2369,6 @@ query-string@^6.13.3: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -2258,17 +2396,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@1.1.x: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@^2.0.6, readable-stream@^2.2.2: +readable-stream@^2.0.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -2387,6 +2515,13 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2414,6 +2549,11 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -2444,11 +2584,6 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -2456,12 +2591,24 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -signal-exit@^3.0.0, signal-exit@^3.0.2: +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -2475,6 +2622,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -2590,11 +2755,6 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.5.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -2605,6 +2765,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -2640,10 +2805,14 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= +string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" string_decoder@~1.1.1: version "1.1.1" @@ -2652,6 +2821,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -2692,6 +2870,11 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -2763,7 +2946,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@^2.3.6: +through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -2807,11 +2990,6 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -2884,24 +3062,16 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@^1.6.4, type-is@~1.6.17: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - typescript@^3.8.3: version "3.9.7" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" @@ -2921,11 +3091,6 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -2989,6 +3154,13 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -3014,6 +3186,24 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -3032,15 +3222,20 @@ xtend@^4.0.0: integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + version "3.2.2" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" + integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== yallist@^3.0.0, yallist@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs@^3.10.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 8d4db6cf..2d25c58a 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -209,9 +209,9 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "requires": { "is-glob": "^4.0.1" } @@ -230,9 +230,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "indent-string": { "version": "2.1.0", @@ -688,9 +688,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" }, "xtend": { "version": "4.0.2", diff --git a/benchmark/package.json b/benchmark/package.json index b0cd6a23..586e97e6 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -24,7 +24,7 @@ "@types/ws": "^7.2.6", "ts-node-dev": "^1.0.0-pre.62", "typescript": "^4.0.2", - "ws": "^7.3.1" + "ws": "^7.4.6" }, "devDependencies": {} } diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index d93e3667..8dcffe52 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -148,8 +148,8 @@ get-stdin@^4.0.1: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" dependencies: is-glob "^4.0.1" @@ -169,8 +169,8 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" indent-string@^2.1.0: version "2.1.0" @@ -515,9 +515,9 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -ws@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" +ws@^7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" xtend@^4.0.0: version "4.0.2" diff --git a/contrib/docker/docker-compose.prod.yaml b/contrib/docker/docker-compose.prod.yaml index c726ba84..6b3b8520 100644 --- a/contrib/docker/docker-compose.prod.yaml +++ b/contrib/docker/docker-compose.prod.yaml @@ -37,7 +37,7 @@ services: DEBUG_MODE: "$DEBUG_MODE" JITSI_URL: $JITSI_URL JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - API_URL: pusher.${DOMAIN} + PUSHER_URL: //pusher.${DOMAIN} TURN_SERVER: "${TURN_SERVER}" TURN_USER: "${TURN_USER}" TURN_PASSWORD: "${TURN_PASSWORD}" diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 07f5f491..8d9c2bfd 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -1,8 +1,8 @@ { local env = std.extVar("env"), - local namespace = env.GITHUB_REF_SLUG, + local namespace = env.DEPLOY_REF, local tag = namespace, - local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com", + local url = namespace+".test.workadventu.re", // develop branch does not use admin because of issue with SSL certificate of admin as of now. local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null, "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", @@ -11,8 +11,7 @@ "back1": { "image": "thecodingmachine/workadventure-back:"+tag, "host": { - "url": "api1."+url, - "https": "enable", + "url": "api1-"+url, "containerPort": 8080 }, "ports": [8080, 50051], @@ -25,16 +24,12 @@ "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, } + (if adminUrl != null then { "ADMIN_API_URL": adminUrl, - } else {}) + if namespace != "master" then { - // Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids! - "NODE_TLS_REJECT_UNAUTHORIZED": "0" - } + } else {}) }, "back2": { "image": "thecodingmachine/workadventure-back:"+tag, "host": { - "url": "api2."+url, - "https": "enable", + "url": "api2-"+url, "containerPort": 8080 }, "ports": [8080, 50051], @@ -47,17 +42,13 @@ "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, } + (if adminUrl != null then { "ADMIN_API_URL": adminUrl, - } else {}) + if namespace != "master" then { - // Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids! - "NODE_TLS_REJECT_UNAUTHORIZED": "0" - } + } else {}) }, "pusher": { "replicas": 2, "image": "thecodingmachine/workadventure-pusher:"+tag, "host": { - "url": "pusher."+url, - "https": "enable" + "url": "pusher-"+url, }, "ports": [8080], "env": { @@ -69,37 +60,30 @@ "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, } + (if adminUrl != null then { "ADMIN_API_URL": adminUrl, - } else {}) + if namespace != "master" then { - // Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids! - "NODE_TLS_REJECT_UNAUTHORIZED": "0" - } + } else {}) }, "front": { "image": "thecodingmachine/workadventure-front:"+tag, "host": { - "url": "play."+url, - "https": "enable" + "url": "play-"+url, }, "ports": [80], "env": { - "API_URL": "pusher."+url, - "UPLOADER_URL": "uploader."+url, - "ADMIN_URL": url, + "PUSHER_URL": "//pusher-"+url, + "UPLOADER_URL": "//uploader-"+url, + "ADMIN_URL": "//"+url, "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", - "TURN_USER": "workadventure", - "TURN_PASSWORD": "WorkAdventure123", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", - "START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json" + "START_ROOM_URL": "/_/global/maps-"+url+"/Floor0/floor0.json" //"GA_TRACKING_ID": "UA-10196481-11" } }, "uploader": { "image": "thecodingmachine/workadventure-uploader:"+tag, "host": { - "url": "uploader."+url, - "https": "enable", + "url": "uploader-"+url, "containerPort": 8080 }, "ports": [8080], @@ -109,16 +93,12 @@ "maps": { "image": "thecodingmachine/workadventure-maps:"+tag, "host": { - "url": "maps."+url, - "https": "enable" + "url": "maps-"+url }, "ports": [80] }, }, "config": { - "https": { - "mail": "d.negrier@thecodingmachine.com" - }, k8sextension(k8sConf):: k8sConf + { back1+: { @@ -133,6 +113,14 @@ } } } + }, + ingress+: { + spec+: { + tls+: [{ + hosts: ["api1-"+url], + secretName: "certificate-tls" + }] + } } }, back2+: { @@ -147,6 +135,14 @@ } } } + }, + ingress+: { + spec+: { + tls+: [{ + hosts: ["api2-"+url], + secretName: "certificate-tls" + }] + } } }, pusher+: { @@ -161,8 +157,46 @@ } } } - } - } + }, + ingress+: { + spec+: { + tls+: [{ + hosts: ["pusher-"+url], + secretName: "certificate-tls" + }] + } + } + }, + front+: { + ingress+: { + spec+: { + tls+: [{ + hosts: ["play-"+url], + secretName: "certificate-tls" + }] + } + } + }, + uploader+: { + ingress+: { + spec+: { + tls+: [{ + hosts: ["uploader-"+url], + secretName: "certificate-tls" + }] + } + } + }, + maps+: { + ingress+: { + spec+: { + tls+: [{ + hosts: ["maps-"+url], + secretName: "certificate-tls" + }] + } + } + }, } } } diff --git a/docker-compose.single-domain.yaml b/docker-compose.single-domain.yaml new file mode 100644 index 00000000..345ccf8d --- /dev/null +++ b/docker-compose.single-domain.yaml @@ -0,0 +1,194 @@ +version: "3" +services: + reverse-proxy: + image: traefik:v2.0 + command: + - --api.insecure=true + - --providers.docker + - --entryPoints.web.address=:80 + - --entryPoints.websecure.address=:443 + ports: + - "80:80" + - "443:443" + # The Web UI (enabled by --api.insecure=true) + - "8080:8080" + depends_on: + - back + - front + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + front: + image: thecodingmachine/nodejs:14 + environment: + DEBUG_MODE: "$DEBUG_MODE" + JITSI_URL: $JITSI_URL + JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" + HOST: "0.0.0.0" + NODE_ENV: development + PUSHER_URL: /pusher + UPLOADER_URL: /uploader + ADMIN_URL: /admin + MAPS_URL: /maps + STARTUP_COMMAND_1: ./templater.sh + STARTUP_COMMAND_2: yarn install + TURN_SERVER: "turn:localhost:3478,turns:localhost:5349" + DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS" + SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS" + # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. + # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container + TURN_USER: "" + TURN_PASSWORD: "" + START_ROOM_URL: "$START_ROOM_URL" + command: yarn run start + volumes: + - ./front:/usr/src/app + labels: + - "traefik.http.routers.front.rule=PathPrefix(`/`)" + - "traefik.http.routers.front.entryPoints=web,traefik" + - "traefik.http.services.front.loadbalancer.server.port=8080" + - "traefik.http.routers.front-ssl.rule=PathPrefix(`/`)" + - "traefik.http.routers.front-ssl.entryPoints=websecure" + - "traefik.http.routers.front-ssl.tls=true" + - "traefik.http.routers.front-ssl.service=front" + + pusher: + image: thecodingmachine/nodejs:12 + command: yarn dev + #command: yarn run prod + #command: yarn run profile + environment: + DEBUG: "*" + STARTUP_COMMAND_1: yarn install + SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" + SECRET_KEY: yourSecretKey + ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" + API_URL: back:50051 + JITSI_URL: $JITSI_URL + JITSI_ISS: $JITSI_ISS + volumes: + - ./pusher:/usr/src/app + labels: + - "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher" + - "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)" + - "traefik.http.routers.pusher.middlewares=strip-pusher-prefix@docker" + - "traefik.http.routers.pusher.entryPoints=web" + - "traefik.http.services.pusher.loadbalancer.server.port=8080" + - "traefik.http.routers.pusher-ssl.rule=PathPrefix(`/pusher`)" + - "traefik.http.routers.pusher-ssl.middlewares=strip-pusher-prefix@docker" + - "traefik.http.routers.pusher-ssl.entryPoints=websecure" + - "traefik.http.routers.pusher-ssl.tls=true" + - "traefik.http.routers.pusher-ssl.service=pusher" + + maps: + image: thecodingmachine/nodejs:12-apache + environment: + DEBUG_MODE: "$DEBUG_MODE" + HOST: "0.0.0.0" + NODE_ENV: development + #APACHE_DOCUMENT_ROOT: dist/ + #APACHE_EXTENSIONS: headers + #APACHE_EXTENSION_HEADERS: 1 + STARTUP_COMMAND_0: sudo a2enmod headers + STARTUP_COMMAND_1: yarn install + STARTUP_COMMAND_2: yarn run dev & + volumes: + - ./maps:/var/www/html + labels: + - "traefik.http.middlewares.strip-maps-prefix.stripprefix.prefixes=/maps" + - "traefik.http.routers.maps.rule=PathPrefix(`/maps`)" + - "traefik.http.routers.maps.middlewares=strip-maps-prefix@docker" + - "traefik.http.routers.maps.entryPoints=web,traefik" + - "traefik.http.services.maps.loadbalancer.server.port=80" + - "traefik.http.routers.maps-ssl.rule=PathPrefix(`/maps`)" + - "traefik.http.routers.maps-ssl.middlewares=strip-maps-prefix@docker" + - "traefik.http.routers.maps-ssl.entryPoints=websecure" + - "traefik.http.routers.maps-ssl.tls=true" + - "traefik.http.routers.maps-ssl.service=maps" + + back: + image: thecodingmachine/nodejs:12 + command: yarn dev + #command: yarn run profile + environment: + DEBUG: "*" + STARTUP_COMMAND_1: yarn install + SECRET_KEY: yourSecretKey + SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" + ALLOW_ARTILLERY: "true" + ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" + JITSI_URL: $JITSI_URL + JITSI_ISS: $JITSI_ISS + MAX_PER_GROUP: "$MAX_PER_GROUP" + volumes: + - ./back:/usr/src/app + labels: + - "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api" + - "traefik.http.routers.back.rule=PathPrefix(`/api`)" + - "traefik.http.routers.back.middlewares=strip-api-prefix@docker" + - "traefik.http.routers.back.entryPoints=web" + - "traefik.http.services.back.loadbalancer.server.port=8080" + - "traefik.http.routers.back-ssl.rule=PathPrefix(`/api`)" + - "traefik.http.routers.back-ssl.middlewares=strip-api-prefix@docker" + - "traefik.http.routers.back-ssl.entryPoints=websecure" + - "traefik.http.routers.back-ssl.tls=true" + - "traefik.http.routers.back-ssl.service=back" + + uploader: + image: thecodingmachine/nodejs:12 + command: yarn dev + #command: yarn run profile + environment: + DEBUG: "*" + STARTUP_COMMAND_1: yarn install + volumes: + - ./uploader:/usr/src/app + labels: + - "traefik.http.middlewares.strip-uploader-prefix.stripprefix.prefixes=/uploader" + - "traefik.http.routers.uploader.rule=PathPrefix(`/uploader`)" + - "traefik.http.routers.uploader.middlewares=strip-uploader-prefix@docker" + - "traefik.http.routers.uploader.entryPoints=web" + - "traefik.http.services.uploader.loadbalancer.server.port=8080" + - "traefik.http.routers.uploader-ssl.rule=PathPrefix(`/uploader`)" + - "traefik.http.routers.uploader-ssl.middlewares=strip-uploader-prefix@docker" + - "traefik.http.routers.uploader-ssl.entryPoints=websecure" + - "traefik.http.routers.uploader-ssl.tls=true" + - "traefik.http.routers.uploader-ssl.service=uploader" + + messages: + #image: thecodingmachine/nodejs:14 + image: thecodingmachine/workadventure-back-base:latest + environment: + #STARTUP_COMMAND_0: sudo apt-get install -y inotify-tools + STARTUP_COMMAND_1: yarn install + STARTUP_COMMAND_2: yarn run proto:watch + volumes: + - ./messages:/usr/src/app + - ./back:/usr/src/back + - ./front:/usr/src/front + - ./pusher:/usr/src/pusher + +# coturn: +# image: coturn/coturn:4.5.2 +# command: +# - turnserver +# #- -c=/etc/coturn/turnserver.conf +# - --log-file=stdout +# - --external-ip=$$(detect-external-ip) +# - --listening-port=3478 +# - --min-port=10000 +# - --max-port=10010 +# - --tls-listening-port=5349 +# - --listening-ip=0.0.0.0 +# - --realm=localhost +# - --server-name=localhost +# - --lt-cred-mech +# # Enable Coturn "REST API" to validate temporary passwords. +# #- --use-auth-secret +# #- --static-auth-secret=SomeStaticAuthSecret +# #- --userdb=/var/lib/turn/turndb +# - --user=workadventure:WorkAdventure123 +# # use real-valid certificate/privatekey files +# #- --cert=/root/letsencrypt/fullchain.pem +# #- --pkey=/root/letsencrypt/privkey.pem +# network_mode: host diff --git a/docker-compose.yaml b/docker-compose.yaml index 98071437..1c1bcb8f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,24 +26,28 @@ services: JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" HOST: "0.0.0.0" NODE_ENV: development - API_URL: pusher.workadventure.localhost - UPLOADER_URL: uploader.workadventure.localhost - ADMIN_URL: workadventure.localhost + PUSHER_URL: //pusher.workadventure.localhost + UPLOADER_URL: //uploader.workadventure.localhost + ADMIN_URL: //workadventure.localhost STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_2: yarn install STUN_SERVER: "stun:stun.l.google.com:19302" TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349" + DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS" + SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS" # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container TURN_USER: "" TURN_PASSWORD: "" START_ROOM_URL: "$START_ROOM_URL" + MAX_PER_GROUP: "$MAX_PER_GROUP" + MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH" command: yarn run start volumes: - ./front:/usr/src/app labels: - "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)" - - "traefik.http.routers.front.entryPoints=web,traefik" + - "traefik.http.routers.front.entryPoints=web" - "traefik.http.services.front.loadbalancer.server.port=8080" - "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)" - "traefik.http.routers.front-ssl.entryPoints=websecure" @@ -53,10 +57,8 @@ services: pusher: image: thecodingmachine/nodejs:12 command: yarn dev - #command: yarn run prod - #command: yarn run profile environment: - DEBUG: "*" + DEBUG: "socket:*" STARTUP_COMMAND_1: yarn install SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_KEY: yourSecretKey @@ -112,6 +114,7 @@ services: JITSI_URL: $JITSI_URL JITSI_ISS: $JITSI_ISS TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret + MAX_PER_GROUP: "MAX_PER_GROUP" volumes: - ./back:/usr/src/app labels: diff --git a/docs/maps/animations.md b/docs/maps/animations.md new file mode 100644 index 00000000..276f2332 --- /dev/null +++ b/docs/maps/animations.md @@ -0,0 +1,33 @@ +{.section-title.accent.text-primary} +# Animating WorkAdventure maps + +A tile can run an animation in loops, for example to render water or blinking lights. Each animation frame is a single +32x32 tile. To create an animation, edit the tileset in Tiled and click on the tile to animate (or pick a free tile to +not overwrite existing ones) and click on the animation editor: + + +
+ +
+ +You can now add all tiles that should be part of the animation via drag and drop to the "playlist" and adjust the frame duration: + +
+
+ +
The tile animation editor
+
+
+ +You can preview animations directly in Tiled, using the "Show tile animations" option: + + +
+
+ +
The Show Tile Animations option
+
+
+ +{.alert.alert-info} +**Tip:** The engine does tile-updates every 100ms, animations with a shorter frame duration will most likely not look that good or may even do not work. diff --git a/docs/maps/api-chat.md b/docs/maps/api-chat.md new file mode 100644 index 00000000..49a40f97 --- /dev/null +++ b/docs/maps/api-chat.md @@ -0,0 +1,37 @@ +{.section-title.accent.text-primary} +# API Chat functions reference + +### Sending a message in the chat + +``` +WA.chat.sendChatMessage(message: string, author: string): void +``` + +Sends a message in the chat. The message is only visible in the browser of the current user. + +* **message**: the message to be displayed in the chat +* **author**: the name displayed for the author of the message. It does not have to be a real user. + +Example: + +```javascript +WA.chat.sendChatMessage('Hello world', 'Mr Robot'); +``` + +### Listening to messages from the chat + +```javascript +WA.chat.onChatMessage(callback: (message: string) => void): void +``` + +Listens to messages typed by the current user and calls the callback. Messages from other users in the chat cannot be listened to. + +* **callback**: the function that will be called when a message is received. It contains the message typed by the user. + +Example: + +```javascript +WA.chat.onChatMessage((message => { + console.log('The user typed a message', message); +})); +``` diff --git a/docs/maps/api-controls.md b/docs/maps/api-controls.md new file mode 100644 index 00000000..dcb0f17b --- /dev/null +++ b/docs/maps/api-controls.md @@ -0,0 +1,29 @@ +{.section-title.accent.text-primary} +# API Controls functions Reference + +### Disabling / restoring controls + +``` +WA.controls.disablePlayerControls(): void +WA.controls.restorePlayerControls(): void +``` + +These 2 methods can be used to completely disable player controls and to enable them again. + +When controls are disabled, the user cannot move anymore using keyboard input. This can be useful in a "First Time User Experience" part, to display an important message to a user before letting him/her move again. + +Example: + +```javascript +WA.room.onEnterZone('myZone', () => { + WA.controls.disablePlayerControls(); + WA.ui.openPopup("popupRectangle", 'This is an imporant message!', [{ + label: "Got it!", + className: "primary", + callback: (popup) => { + WA.controls.restorePlayerControls(); + popup.close(); + } + }]); +}); +``` diff --git a/docs/maps/api-deprecated.md b/docs/maps/api-deprecated.md new file mode 100644 index 00000000..930caebe --- /dev/null +++ b/docs/maps/api-deprecated.md @@ -0,0 +1,20 @@ +{.section-title.accent.text-primary} +# API Reference - Deprecated functions + +The list of functions below is **deprecated**. You should not use those but. use the replacement functions. + +- Method `WA.sendChatMessage` is deprecated. It has been renamed to `WA.chat.sendChatMessage`. +- Method `WA.disablePlayerControls` is deprecated. It has been renamed to `WA.controls.disablePlayerControls`. +- Method `WA.restorePlayerControls` is deprecated. It has been renamed to `WA.controls.restorePlayerControls`. +- Method `WA.displayBubble` is deprecated. It has been renamed to `WA.ui.displayBubble`. +- Method `WA.removeBubble` is deprecated. It has been renamed to `WA.ui.removeBubble`. +- Method `WA.openTab` is deprecated. It has been renamed to `WA.nav.openTab`. +- Method `WA.loadSound` is deprecated. It has been renamed to `WA.sound.loadSound`. +- Method `WA.goToPage` is deprecated. It has been renamed to `WA.nav.goToPage`. +- Method `WA.goToRoom` is deprecated. It has been renamed to `WA.nav.goToRoom`. +- Method `WA.openCoWebSite` is deprecated. It has been renamed to `WA.nav.openCoWebSite`. +- Method `WA.closeCoWebSite` is deprecated. It has been renamed to `WA.nav.closeCoWebSite`. +- Method `WA.openPopup` is deprecated. It has been renamed to `WA.ui.openPopup`. +- Method `WA.onChatMessage` is deprecated. It has been renamed to `WA.chat.onChatMessage`. +- Method `WA.onEnterZone` is deprecated. It has been renamed to `WA.room.onEnterZone`. +- Method `WA.onLeaveZone` is deprecated. It has been renamed to `WA.room.onLeaveZone`. diff --git a/docs/maps/api-nav.md b/docs/maps/api-nav.md new file mode 100644 index 00000000..29323632 --- /dev/null +++ b/docs/maps/api-nav.md @@ -0,0 +1,68 @@ +{.section-title.accent.text-primary} +# API Navigation functions reference + +### Opening a web page in a new tab + +``` +WA.nav.openTab(url: string): void +``` + +Opens the webpage at "url" in your browser, in a new tab. + +Example: + +```javascript +WA.nav.openTab('https://www.wikipedia.org/'); +``` + +### Opening a web page in the current tab + +``` +WA.nav.goToPage(url: string): void +``` + +Opens the webpage at "url" in your browser in place of WorkAdventure. WorkAdventure will be completely unloaded. + +Example: + +```javascript +WA.nav.goToPage('https://www.wikipedia.org/'); +``` + +### Going to a different map from the script + +``` + +WA.nav.goToRoom(url: string): void +``` + +Load the map at url without unloading workadventure + +relative urls: "../subFolder/map.json[#start-layer-name]" +global urls: "/_/global/domain/path/map.json[#start-layer-name]" + +Example: + +```javascript +WA.nav.goToRoom("/@/tcm/workadventure/floor0") // workadventure urls +WA.nav.goToRoom('../otherMap/map.json'); +WA.nav.goToRoom("/_/global/.json#start-layer-2") +``` + +### Opening/closing a web page in an iFrame + +``` +WA.nav.openCoWebSite(url: string): void +WA.nav.closeCoWebSite(): void +``` + +Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. + +Example: + +```javascript +WA.nav.openCoWebSite('https://www.wikipedia.org/'); +// ... +WA.nav.closeCoWebSite(); +``` + diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md new file mode 100644 index 00000000..f483731e --- /dev/null +++ b/docs/maps/api-player.md @@ -0,0 +1,21 @@ +{.section-title.accent.text-primary} +# API Player functions Reference + +### Listen to player movement +``` +WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; +``` +Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. + +The event has the following attributes : +* **moving (boolean):** **true** when the current player is moving, **false** otherwise. +* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. +* **x (number):** coordinate X of the current player. +* **y (number):** coordinate Y of the current player. + +**callback:** the function that will be called when the current player is moving. It contains the event. + +Example : +```javascript +WA.player.onPlayerMove(console.log); +``` \ No newline at end of file diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md new file mode 100644 index 00000000..30a11b2a --- /dev/null +++ b/docs/maps/api-reference.md @@ -0,0 +1,12 @@ +{.section-title.accent.text-primary} +# API Reference + +- [Navigation functions](api-nav.md) +- [Chat functions](api-chat.md) +- [Room functions](api-room.md) +- [Player functions](api-player.md) +- [UI functions](api-ui.md) +- [Sound functions](api-sound.md) +- [Controls functions](api-controls.md) + +- [List of deprecated functions](api-deprecated.md) \ No newline at end of file diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md new file mode 100644 index 00000000..d8381cc6 --- /dev/null +++ b/docs/maps/api-room.md @@ -0,0 +1,114 @@ +{.section-title.accent.text-primary} +# API Room functions Reference + +### Working with group layers +If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together. + +Example : +
+
+ +
+
+ +The name of the layers of this map are : +* `entries/start` +* `bottom/ground/under` +* `bottom/build/carpet` +* `wall` + +### Detecting when the user enters/leaves a zone + +``` +WA.room.onEnterZone(name: string, callback: () => void): void +WA.room.onLeaveZone(name: string, callback: () => void): void +``` + +Listens to the position of the current user. The event is triggered when the user enters or leaves a given zone. The name of the zone is stored in the map, on a dedicated layer with the `zone` property. + +
+
+ +
The `zone` property, applied on a layer
+
+
+ +* **name**: the name of the zone, as defined in the `zone` property. +* **callback**: the function that will be called when a user enters or leaves the zone. + +Example: + +```javascript +WA.room.onEnterZone('myZone', () => { + WA.chat.sendChatMessage("Hello!", 'Mr Robot'); +}) + +WA.room.onLeaveZone('myZone', () => { + WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); +}) +``` + +### Show / Hide a layer +``` +WA.room.showLayer(layerName : string): void +WA.room.hideLayer(layerName : string) : void +``` +These 2 methods can be used to show and hide a layer. + +Example : +```javascript +WA.room.showLayer('bottom'); +//... +WA.room.hideLayer('bottom'); +``` + +### Set/Create properties in a layer + +``` +WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; +``` + +Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. + +Example : +```javascript +WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); +``` + +### Getting information on the current room +``` +WA.room.getCurrentRoom(): Promise +``` +Return a promise that resolves to a `Room` object with the following attributes : +* **id (string) :** ID of the current room +* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. +* **mapUrl (string) :** Url of the JSON map file +* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer + +Example : +```javascript +WA.room.getCurrentRoom((room) => { + if (room.id === '42') { + console.log(room.map); + window.open(room.mapUrl, '_blank'); + } +}) +``` + +### Getting information on the current user +``` +WA.player.getCurrentUser(): Promise +``` +Return a promise that resolves to a `User` object with the following attributes : +* **id (string) :** ID of the current user +* **nickName (string) :** name displayed above the current user +* **tags (string[]) :** list of all the tags of the current user + +Example : +```javascript +WA.room.getCurrentUser().then((user) => { + if (user.nickName === 'ABC') { + console.log(user.tags); + } +}) +``` diff --git a/docs/maps/api-sound.md b/docs/maps/api-sound.md new file mode 100644 index 00000000..3c57ecae --- /dev/null +++ b/docs/maps/api-sound.md @@ -0,0 +1,34 @@ +{.section-title.accent.text-primary} +# API Sound functions Reference + +### Load a sound from an url + +``` +WA.sound.loadSound(url: string): Sound +``` + +Load a sound from an url + +Please note that `loadSound` returns an object of the `Sound` class + +The `Sound` class that represents a loaded sound contains two methods: `play(soundConfig : SoundConfig|undefined)` and `stop()` + +The parameter soundConfig is optional, if you call play without a Sound config the sound will be played with the basic configuration. + +Example: + +```javascript +var mySound = WA.sound.loadSound("Sound.ogg"); +var config = { + volume : 0.5, + loop : false, + rate : 1, + detune : 1, + delay : 0, + seek : 0, + mute : false +} +mySound.play(config); +// ... +mySound.stop(); +``` diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md new file mode 100644 index 00000000..286f2ac7 --- /dev/null +++ b/docs/maps/api-ui.md @@ -0,0 +1,89 @@ +{.section-title.accent.text-primary} +# API UI functions Reference + +### Opening a popup + +In order to open a popup window, you must first define the position of the popup on your map. + +You can position this popup by using a "rectangle" object in Tiled that you will place on an "object" layer. + +
+
+ +
+
+ +
+
+ +``` +WA.ui.openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup +``` + +* **targetObject**: the name of the rectangle object defined in Tiled. +* **message**: the message to display in the popup. +* **buttons**: an array of action buttons defined underneath the popup. + +Action buttons are `ButtonDescriptor` objects containing these properties. + +* **label (_string_)**: The label of the button. +* **className (_string_)**: The visual type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled". +* **callback (_(popup: Popup)=>void_)**: Callback called when the button is pressed. + +Please note that `openPopup` returns an object of the `Popup` class. Also, the callback called when a button is clicked is passed a `Popup` object. + +The `Popup` class that represents an open popup contains a single method: `close()`. This will obviously close the popup when called. + +```javascript +class Popup { + /** + * Closes the popup + */ + close() {}; +} +``` + +Example: + +```javascript +let helloWorldPopup; + +// Open the popup when we enter a given zone +helloWorldPopup = WA.room.onEnterZone('myZone', () => { + WA.ui.openPopup("popupRectangle", 'Hello world!', [{ + label: "Close", + className: "primary", + callback: (popup) => { + // Close the popup when the "Close" button is pressed. + popup.close(); + } + }); +}]); + +// Close the popup when we leave the zone. +WA.room.onLeaveZone('myZone', () => { + helloWorldPopup.close(); +}); +``` + +### Add custom menu + +```typescript +WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void +``` +Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`. +Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script. + +Example: + +```javascript + +WA.ui.registerMenuCommand("test", () => { + WA.chat.sendChatMessage("test clicked", "menu cmd") +}) + +``` + +
+ +
\ No newline at end of file diff --git a/docs/maps/scripting.md b/docs/maps/scripting.md new file mode 100644 index 00000000..5f645b81 --- /dev/null +++ b/docs/maps/scripting.md @@ -0,0 +1,117 @@ +{.alert.alert-danger style="width:80%"} +This feature is "_experimental_". We may apply changes in the near future to the way it works when we gather some feedback. + +{.section-title.accent.text-primary} +# Scripting WorkAdventure maps + +Do you want to add a bit of intelligence to your map? Scripts allow you to create maps with special features. + +You can for instance: + +* Create FTUE (First Time User Experience) scenarios where a first-time user will be displayed a notification popup. +* Create NPC (non playing characters) and interact with those characters using the chat. +* Organize interactions between an iframe and your map (for instance, walking on a special zone might add a product in the cart of an eCommerce website...) +* etc... + +Please note that scripting in WorkAdventure is at an early stage of development and that more features might be added in the future. You can actually voice your opinion about useful features by adding [an issue on Github](https://github.com/thecodingmachine/workadventure/issues). + +{.alert.alert-warning} +**Beware:** Scripts are executed in the browser of the current user only. Generally speaking, scripts cannot be used to trigger a change that will be displayed on other users screen. + +## Scripting language + +Client-side scripting is done in **Javascript** (or any language that transpiles to Javascript like _Typescript_). + +There are 2 ways you can use the scripting language: + +* **In the map**: By directly referring a Javascript file inside your map, in the `script` property of your map. +* **In an iFrame**: By placing your Javascript script into an iFrame, your script can communicate with the WorkAdventure game + +## Adding a script in the map + +Create a `script` property in your map. + +In Tiled, in order to access your map properties, you can click on _"Map > Map properties"_. + +
+
+ +
The Map properties menu
+
+
+ +Create a `script` property (a "string"), and put the URL of your script. + +You can put relative URLs. If your script file is next to your map, you can simply write the name of the script file here. + +
+
+ +
The script property
+
+
+ +Start by testing this with a simple message sent to the chat. + +**script.js** +```javascript +WA.sendChatMessage('Hello world', 'Mr Robot'); +``` + +The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.sendChatMessage` opens the chat and adds a message in it. + +In your browser console, when you open the map, the chat message should be displayed right away. + +## Adding a script in an iFrame + +In WorkAdventure, you can easily [open an iFrame using the `openWebsite` property on a layer](special-zones). However, by default, the iFrame is not allowed to communicate with WorkAdventure. + +This is done to improve security. In order to be able to execute a script that communicates with WorkAdventure inside an iFrame, you have to **explicitly allow the iFrame to use the "iFrame API"**. + +In order to allow communication with WorkAdventure, you need to add an additional property: `openWebsiteAllowApi`. This property must be _boolean_ and you must set it to "true". + +
+
+ +
The `openWebsiteAllowApi` property
+
+
+ +In your iFrame HTML page, you now need to import the _WorkAdventure client API Javascript library_. This library contains the `WA` object that you can use to communicate with WorkAdventure. + +The library is available at `https://play.workadventu.re/iframe_api.js`. + +_Note:_ if you are using a self-hosted version of WorkAdventure, use `https://[front_domain]/iframe_api.js` + +**iframe.html** +```html + + + + + + + + +``` + +You can now start by testing this with a simple message sent to the chat. + +**iframe.html** +```html +... + +... +``` + +Let's now review the complete list of methods available in this `WA` object. + +## Using Typescript + +View the dedicated page about [using Typescript with the scripting API](using-typescript). + +## Available features in the client API + +The list of available functions and features is [available in the API Reference page, with examples](api-reference). diff --git a/docs/maps/wa-maps.md b/docs/maps/wa-maps.md new file mode 100644 index 00000000..d7a349c5 --- /dev/null +++ b/docs/maps/wa-maps.md @@ -0,0 +1,92 @@ +{.section-title.accent.text-primary} +# About WorkAdventure maps + +A WorkAdventure map is a map in "JSON" format generated by [Tiled](https://www.mapeditor.org/). + +## Tiles + +A map is made of "tiles" (we can also call them "sprites"). In WorkAdventure, the tiles are small images of 32x32 pixels. + +Tiles may have transparent parts. Many tiles can be stored in a single PNG file. We call this file a "tileset". + +There are many tilesets available on the internet. Some examples of websites offering awesome tiles: + +* [itch.io](https://itch.io/) +* [opengameart.org](https://opengameart.org/) +* [deviantart.com](https://www.deviantart.com/) + +Keep in mind the size of tiles and do not forget to check the license of the tileset you are using! + + +## How to design "pixel" tiles + +You can design your own tiles as well as change existing tiles, this is usually referred to as "pixeling". You can start drawing your own tiles with [Piskel](https://www.piskelapp.com/). It is easy to use and well targeted at "pixeling". If you are getting serious about pixeling, the awesome folks at the Chaos Computer Club recommend the use of the editor [Krita](https://krita.org/). There are plenty of other editors as well. + +If you are using Krita: + +* Please double check that your tiles are 32x32 pixels in size. You can enable a grid under view -> show grid and under settings -> dockers -> grid you can select the grid size. +* Use transparency if you have to model transitions between different materials. This is more flexible and saves you time by not modeling every transition. +* You can follow the Pixel-Art Workshop by blinry: [media.ccc.de/v/34C3-jugend-hackt-1016-pixel_art_workshop](https://media.ccc.de/v/34C3-jugend-hackt-1016-pixel_art_workshop) + +## WorkAdventure Map Rules + +In order to design a map that will be readable by WorkAdventure, you will have to respect some constraints. + +In particular, you will need to: + +* set a start position for the players +* configure the "floor layer" (so that WorkAdventure can correctly display characters above the floor, but under the ceiling) +* eventually, you can place exits that link to other maps + +A few things to notice: + +* your map can have as many layers as you want +* your map MUST contain a layer named "floorLayer" of type "objectgroup" that represents the layer on which characters will be drawn. Every layer above the "floorLayer" will be displayed on top of the characters. +* the tilesets in your map MUST be embedded. You cannot refer to an external typeset in a TSX file. Click the "embed tileset" button in the tileset tab to embed tileset data. +* your map MUST be exported in JSON format. You need to use a recent version of Tiled to get JSON format export (1.3+) +* WorkAdventure doesn't support object layers and will ignore them +* If you are starting from a blank map, your map MUST be orthogonal and tiles size should be 32x32. + +
+
+ +
"floorLayer" is compulsory
+
+
+ +## Building walls and "collidable" areas + +By default, the characters can traverse any tiles. If you want to prevent your characeter from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile. + +To make a tile "collidable", you should: + +1. select the relevant tileset and switch to "edit" mode: + + ![](https://workadventu.re/img/docs/collides-1.png){.document-img} + +2. right click on a tile of the tileset to select it: + + ![](https://workadventu.re/img/docs/collides-2.png){.document-img} + +3. on the left pane in the custom properties section, right click and select "Add properties": + + ![](https://workadventu.re/img/docs/collides-3.png){.document-img} + + Please add a `collides` property. The type of the property must be **bool**. + +4. finally, check the checkbox for the `collides` property: + + ![](https://workadventu.re/img/docs/collides-4.png){.document-img} + +Repeat for every tile that should be "collidable". + +## Adding behaviour with properties + +In the next sections, you will see how you can add behaviour on your map by adding "properties". +You can add properties for a variety of features: putting exits, opening websites, meeting rooms, silent zones, etc... + +You can add properties either on individual tiles of a tileset OR on a complete layer. + +If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer. + +The exception is the "collides" property that can only be set on tiles, but not on a complete layer. diff --git a/front/.eslintrc.json b/front/.eslintrc.json index 3aab37d9..037fddae 100644 --- a/front/.eslintrc.json +++ b/front/.eslintrc.json @@ -25,6 +25,15 @@ ], "rules": { "no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "error" + "@typescript-eslint/no-explicit-any": "error", + + // TODO: remove those ignored rules and write a stronger code! + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/restrict-template-expressions": "off" } } diff --git a/front/.gitignore b/front/.gitignore index b6f01e2c..1842041a 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -1,5 +1,9 @@ /node_modules/ -/dist/bundle.js +/dist/*.js +/dist/*.js.map +/dist/*.js.LICENSE.txt +/dist/main.*.css +/dist/main.*.css.map /dist/tests/ /yarn-error.log /dist/webpack.config.js diff --git a/front/.prettierignore b/front/.prettierignore new file mode 100644 index 00000000..1f453464 --- /dev/null +++ b/front/.prettierignore @@ -0,0 +1 @@ +src/Messages/generated diff --git a/front/.prettierrc.json b/front/.prettierrc.json new file mode 100644 index 00000000..e8980d15 --- /dev/null +++ b/front/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "tabWidth": 4 +} diff --git a/front/Dockerfile b/front/Dockerfile index b0d17877..ef724e0f 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -3,11 +3,16 @@ WORKDIR /var/www/messages COPY --chown=docker:docker messages . RUN yarn install && yarn proto -# we are rebuilding on each deploy to cope with the API_URL environment URL +# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL FROM thecodingmachine/nodejs:14-apache COPY --chown=docker:docker front . COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated + +# Removing the iframe.html file from the final image as this adds a XSS attack. +# iframe.html is only in dev mode to circumvent a limitation +RUN rm dist/iframe.html + RUN yarn install ENV NODE_ENV=production diff --git a/front/dist/.gitignore b/front/dist/.gitignore index 05c474ec..785f2eb9 100644 --- a/front/dist/.gitignore +++ b/front/dist/.gitignore @@ -1,3 +1,4 @@ index.html index.tmpl.html.tmp -style.*.css \ No newline at end of file +/js/ +style.*.css diff --git a/front/dist/iframe.html b/front/dist/iframe.html new file mode 100644 index 00000000..c8fafb4b --- /dev/null +++ b/front/dist/iframe.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index 55adcebc..aa63229f 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -29,12 +29,16 @@ + + WorkAdventure
+
+
@@ -42,26 +46,6 @@ -
-
- -
-
-
-
- - -
-
- - -
-
- - -
-
-
@@ -96,30 +80,17 @@
-
-
- -
-
- - -
- - diff --git a/front/dist/resources/emotes/clap-emote.png b/front/dist/resources/emotes/clap-emote.png new file mode 100644 index 00000000..a64f2e5f Binary files /dev/null and b/front/dist/resources/emotes/clap-emote.png differ diff --git a/front/dist/resources/emotes/hand-emote.png b/front/dist/resources/emotes/hand-emote.png new file mode 100644 index 00000000..3bc01acf Binary files /dev/null and b/front/dist/resources/emotes/hand-emote.png differ diff --git a/front/dist/resources/emotes/heart-emote.png b/front/dist/resources/emotes/heart-emote.png new file mode 100644 index 00000000..867d6be1 Binary files /dev/null and b/front/dist/resources/emotes/heart-emote.png differ diff --git a/front/dist/resources/emotes/thanks-emote.png b/front/dist/resources/emotes/thanks-emote.png new file mode 100644 index 00000000..8e326ed5 Binary files /dev/null and b/front/dist/resources/emotes/thanks-emote.png differ diff --git a/front/dist/resources/emotes/thumb-down-emote.png b/front/dist/resources/emotes/thumb-down-emote.png new file mode 100644 index 00000000..8ec7c961 Binary files /dev/null and b/front/dist/resources/emotes/thumb-down-emote.png differ diff --git a/front/dist/resources/emotes/thumb-up-emote.png b/front/dist/resources/emotes/thumb-up-emote.png new file mode 100644 index 00000000..eecb0e57 Binary files /dev/null and b/front/dist/resources/emotes/thumb-up-emote.png differ diff --git a/front/dist/resources/fonts/fonts.css b/front/dist/resources/fonts/fonts.css new file mode 100644 index 00000000..a3d3cf71 --- /dev/null +++ b/front/dist/resources/fonts/fonts.css @@ -0,0 +1,5 @@ +/*This file is a workaround to allow phaser to load directly this font */ +@font-face { + font-family: "Press Start 2P"; + src: url("/fonts/press-start-2p-latin-400-normal.woff2") format('woff2'); +} \ No newline at end of file diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html index f0faf5c5..26be2a1c 100644 --- a/front/dist/resources/html/gameMenu.html +++ b/front/dist/resources/html/gameMenu.html @@ -1,4 +1,7 @@ +
+ +
+ + + \ No newline at end of file diff --git a/front/src/Components/CustomCharacterScene/CustomCharacterScene.svelte b/front/src/Components/CustomCharacterScene/CustomCharacterScene.svelte new file mode 100644 index 00000000..1807f15d --- /dev/null +++ b/front/src/Components/CustomCharacterScene/CustomCharacterScene.svelte @@ -0,0 +1,119 @@ + + + +
+

Customize your WOKA

+
+
+ + +
+
+ {#if activeRow === 0} + + {/if} + {#if activeRow !== 0} + + {/if} + {#if activeRow === 5} + + {/if} + {#if activeRow !== 5} + + {/if} +
+ + + diff --git a/front/src/Components/EnableCamera/EnableCameraScene.svelte b/front/src/Components/EnableCamera/EnableCameraScene.svelte new file mode 100644 index 00000000..1f9ef027 --- /dev/null +++ b/front/src/Components/EnableCamera/EnableCameraScene.svelte @@ -0,0 +1,229 @@ + + +
+
+

Turn on your camera and microphone

+
+ {#if $localStreamStore.type === 'success' && $localStreamStore.stream} + + {:else } +
+ +
+ {/if} + + +
+ + {#if $cameraListStore.length > 1 } +
+ Camera +
+ +
+
+ {/if} + + {#if $microphoneListStore.length > 1 } +
+ Microphone +
+ +
+
+ {/if} + +
+
+ +
+
+ + + diff --git a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte new file mode 100644 index 00000000..79ad1810 --- /dev/null +++ b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte @@ -0,0 +1,82 @@ + + + +
+ {#each [...Array(NB_BARS).keys()] as i (i)} +
+ {/each} +
+ + diff --git a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte new file mode 100644 index 00000000..8f4de785 --- /dev/null +++ b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte @@ -0,0 +1,73 @@ + + +
+
+

Camera / Microphone access needed

+

Permission denied

+

You must allow camera and microphone access in your browser.

+

+ {#if isFirefox } +

Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the authorization.

+ + {:else if isChrome && !isAndroid } + + {/if} +

+
+
+ + +
+
+ + + diff --git a/front/dist/resources/objects/help-setting-camera-permission-chrome.png b/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png similarity index 100% rename from front/dist/resources/objects/help-setting-camera-permission-chrome.png rename to front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png diff --git a/front/dist/resources/objects/help-setting-camera-permission-firefox.png b/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png similarity index 100% rename from front/dist/resources/objects/help-setting-camera-permission-firefox.png rename to front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png diff --git a/front/src/Components/Login/LoginScene.svelte b/front/src/Components/Login/LoginScene.svelte new file mode 100644 index 00000000..fe3c2c63 --- /dev/null +++ b/front/src/Components/Login/LoginScene.svelte @@ -0,0 +1,122 @@ + + +
+
+ WorkAdventure logo +
+
+

Enter your name

+
+ {startValidating = true}} class:is-error={name.trim() === '' && startValidating} /> +
+ {#if name.trim() === '' && startValidating } +

The name is empty

+ {/if} +
+ + {#if DISPLAY_TERMS_OF_USE} +
+

By continuing, you are agreeing our terms of use, privacy policy and cookie policy.

+
+ {/if} +
+ +
+
+ + diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte new file mode 100644 index 00000000..241bf45f --- /dev/null +++ b/front/src/Components/Menu/MenuIcon.svelte @@ -0,0 +1,33 @@ + + +
+
+ +
+
+ + diff --git a/front/src/Components/MyCamera.svelte b/front/src/Components/MyCamera.svelte new file mode 100644 index 00000000..ed4154a9 --- /dev/null +++ b/front/src/Components/MyCamera.svelte @@ -0,0 +1,46 @@ + + + +
+
+ {#if $localStreamStore.type === "success" && $localStreamStore.stream } + + + {/if} +
+
diff --git a/front/src/Components/SelectCompanion/SelectCompanionScene.svelte b/front/src/Components/SelectCompanion/SelectCompanionScene.svelte new file mode 100644 index 00000000..04efe124 --- /dev/null +++ b/front/src/Components/SelectCompanion/SelectCompanionScene.svelte @@ -0,0 +1,87 @@ + + +
+
+

Select your companion

+ + +
+
+ + +
+
+ + diff --git a/front/src/Components/SoundMeterWidget.svelte b/front/src/Components/SoundMeterWidget.svelte new file mode 100644 index 00000000..74d3f7b3 --- /dev/null +++ b/front/src/Components/SoundMeterWidget.svelte @@ -0,0 +1,51 @@ + + + +
+ 5}> + 10}> + 15}> + 40}> + 70}> +
diff --git a/front/src/Components/UI/AudioPlaying.svelte b/front/src/Components/UI/AudioPlaying.svelte new file mode 100644 index 00000000..97f77429 --- /dev/null +++ b/front/src/Components/UI/AudioPlaying.svelte @@ -0,0 +1,52 @@ + + +
+ Audio playing +

Audio message

+ +
+ + diff --git a/front/src/Components/UI/ErrorDialog.svelte b/front/src/Components/UI/ErrorDialog.svelte new file mode 100644 index 00000000..3244de24 --- /dev/null +++ b/front/src/Components/UI/ErrorDialog.svelte @@ -0,0 +1,48 @@ + + +
+

Error

+
+ {#each $errorStore as error} +

{error}

+ {/each} +
+
+ +
+
+ + diff --git a/front/src/Components/UI/images/megaphone.svg b/front/src/Components/UI/images/megaphone.svg new file mode 100644 index 00000000..708f860c --- /dev/null +++ b/front/src/Components/UI/images/megaphone.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/front/src/Components/Video/ChatLayout.svelte b/front/src/Components/Video/ChatLayout.svelte new file mode 100644 index 00000000..ac91217e --- /dev/null +++ b/front/src/Components/Video/ChatLayout.svelte @@ -0,0 +1,35 @@ + + +
+ {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} + + {/each} +
diff --git a/front/src/Components/Video/LocalStreamMediaBox.svelte b/front/src/Components/Video/LocalStreamMediaBox.svelte new file mode 100644 index 00000000..55c7f22c --- /dev/null +++ b/front/src/Components/Video/LocalStreamMediaBox.svelte @@ -0,0 +1,16 @@ + + + +
+ {#if stream} + + {/if} +
diff --git a/front/src/Components/Video/MediaBox.svelte b/front/src/Components/Video/MediaBox.svelte new file mode 100644 index 00000000..9160a7a9 --- /dev/null +++ b/front/src/Components/Video/MediaBox.svelte @@ -0,0 +1,20 @@ + + +
+ {#if streamable instanceof VideoPeer} + + {:else if streamable instanceof ScreenSharingPeer} + + {:else} + + {/if} +
diff --git a/front/src/Components/Video/PresentationLayout.svelte b/front/src/Components/Video/PresentationLayout.svelte new file mode 100644 index 00000000..f68dd2f1 --- /dev/null +++ b/front/src/Components/Video/PresentationLayout.svelte @@ -0,0 +1,24 @@ + + +
+ {#if $videoFocusStore } + + {/if} +
+ diff --git a/front/src/Components/Video/ScreenSharingMediaBox.svelte b/front/src/Components/Video/ScreenSharingMediaBox.svelte new file mode 100644 index 00000000..c6e1564f --- /dev/null +++ b/front/src/Components/Video/ScreenSharingMediaBox.svelte @@ -0,0 +1,33 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if $streamStore === null} + {name} + {:else} + + {/if} +
+ + diff --git a/front/src/Components/Video/VideoMediaBox.svelte b/front/src/Components/Video/VideoMediaBox.svelte new file mode 100644 index 00000000..1a581914 --- /dev/null +++ b/front/src/Components/Video/VideoMediaBox.svelte @@ -0,0 +1,48 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if !$constraintStore || $constraintStore.video === false} + {name} + {/if} + {#if $constraintStore && $constraintStore.audio === false} + Muted + {/if} + + {#if $streamStore } + + {/if} + + {#if $constraintStore && $constraintStore.audio !== false} + + {/if} +
+ diff --git a/front/src/Components/Video/VideoOverlay.svelte b/front/src/Components/Video/VideoOverlay.svelte new file mode 100644 index 00000000..feb13743 --- /dev/null +++ b/front/src/Components/Video/VideoOverlay.svelte @@ -0,0 +1,23 @@ + + +
+ {#if $layoutModeStore === LayoutMode.Presentation } + + {:else } + + {/if} +
+ + diff --git a/front/src/Components/Video/images/blockSign.svg b/front/src/Components/Video/images/blockSign.svg new file mode 100644 index 00000000..c64ba294 --- /dev/null +++ b/front/src/Components/Video/images/blockSign.svg @@ -0,0 +1,22 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Video/images/report.svg b/front/src/Components/Video/images/report.svg new file mode 100644 index 00000000..14753256 --- /dev/null +++ b/front/src/Components/Video/images/report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/Video/utils.ts b/front/src/Components/Video/utils.ts new file mode 100644 index 00000000..ab7a63be --- /dev/null +++ b/front/src/Components/Video/utils.ts @@ -0,0 +1,27 @@ +export function getColorByString(str: string) : string|null { + let hash = 0; + if (str.length === 0) { + return null; + } + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 255; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} + +export function srcObject(node: HTMLVideoElement, stream: MediaStream) { + node.srcObject = stream; + return { + update(newStream: MediaStream) { + if (node.srcObject != newStream) { + node.srcObject = newStream + } + } + } +} diff --git a/front/src/Components/VisitCard/VisitCard.svelte b/front/src/Components/VisitCard/VisitCard.svelte new file mode 100644 index 00000000..78f10359 --- /dev/null +++ b/front/src/Components/VisitCard/VisitCard.svelte @@ -0,0 +1,84 @@ + + + + + +
+ {#if hidden} +
+ {/if} + + {#if !hidden} +
+ +
+ {/if} + +
+ + diff --git a/front/src/Components/images/cinema-close.svg b/front/src/Components/images/cinema-close.svg new file mode 100644 index 00000000..aa1d9b17 --- /dev/null +++ b/front/src/Components/images/cinema-close.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/cinema.svg b/front/src/Components/images/cinema.svg similarity index 100% rename from front/dist/resources/logos/cinema.svg rename to front/src/Components/images/cinema.svg diff --git a/front/src/Components/images/layout-chat.svg b/front/src/Components/images/layout-chat.svg new file mode 100644 index 00000000..af071d49 --- /dev/null +++ b/front/src/Components/images/layout-chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/images/layout-presentation.svg b/front/src/Components/images/layout-presentation.svg new file mode 100644 index 00000000..bf65d002 --- /dev/null +++ b/front/src/Components/images/layout-presentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/images/logo.png b/front/src/Components/images/logo.png new file mode 100644 index 00000000..f4440ad5 Binary files /dev/null and b/front/src/Components/images/logo.png differ diff --git a/front/src/Components/images/microphone-close.svg b/front/src/Components/images/microphone-close.svg new file mode 100644 index 00000000..16731829 --- /dev/null +++ b/front/src/Components/images/microphone-close.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/microphone.svg b/front/src/Components/images/microphone.svg similarity index 100% rename from front/dist/resources/logos/microphone.svg rename to front/src/Components/images/microphone.svg diff --git a/front/src/Components/images/monitor-close.svg b/front/src/Components/images/monitor-close.svg new file mode 100644 index 00000000..52f446d1 --- /dev/null +++ b/front/src/Components/images/monitor-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/images/monitor.svg b/front/src/Components/images/monitor.svg new file mode 100644 index 00000000..be50195b --- /dev/null +++ b/front/src/Components/images/monitor.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/front/src/Components/images/music-file.svg b/front/src/Components/images/music-file.svg new file mode 100644 index 00000000..a97656ba --- /dev/null +++ b/front/src/Components/images/music-file.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/front/src/Components/selectCharacter/SelectCharacterScene.svelte b/front/src/Components/selectCharacter/SelectCharacterScene.svelte new file mode 100644 index 00000000..5a37efda --- /dev/null +++ b/front/src/Components/selectCharacter/SelectCharacterScene.svelte @@ -0,0 +1,92 @@ + + +
+
+

Select your WOKA

+ + +
+
+ + +
+
+ + diff --git a/front/src/Connexion/AdminMessagesService.ts b/front/src/Connexion/AdminMessagesService.ts index 7f96a673..0cf7f95f 100644 --- a/front/src/Connexion/AdminMessagesService.ts +++ b/front/src/Connexion/AdminMessagesService.ts @@ -1,10 +1,11 @@ import {Subject} from "rxjs"; -import {BanUserMessage, SendUserMessage} from "../Messages/generated/messages_pb"; +import type {BanUserMessage, SendUserMessage} from "../Messages/generated/messages_pb"; export enum AdminMessageEventTypes { admin = 'message', audio = 'audio', ban = 'ban', + banned = 'banned', } interface AdminMessageEvent { @@ -18,11 +19,11 @@ interface AdminMessageEvent { class AdminMessagesService { private _messageStream: Subject = new Subject(); public messageStream = this._messageStream.asObservable(); - + constructor() { this.messageStream.subscribe((event) => console.log('message', event)) } - + onSendusermessage(message: SendUserMessage|BanUserMessage) { this._messageStream.next({ type: message.getType() as unknown as AdminMessageEventTypes, @@ -31,4 +32,4 @@ class AdminMessagesService { } } -export const adminMessagesService = new AdminMessagesService(); \ No newline at end of file +export const adminMessagesService = new AdminMessagesService(); diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 522407e3..8112ba17 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -1,21 +1,30 @@ import Axios from "axios"; -import {API_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable"; +import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable"; import {RoomConnection} from "./RoomConnection"; -import {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels"; +import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels"; import {GameConnexionTypes, urlManager} from "../Url/UrlManager"; import {localUserStore} from "./LocalUserStore"; -import {LocalUser} from "./LocalUser"; +import {CharacterTexture, LocalUser} from "./LocalUser"; import {Room} from "./Room"; -import {Subject} from "rxjs"; -import {ServerToClientMessage} from "../Messages/generated/messages_pb"; class ConnectionManager { private localUser!:LocalUser; private connexionType?: GameConnexionTypes - - public _serverToClientMessageStream:Subject = new Subject(); + private reconnectingTimeout: NodeJS.Timeout|null = null; + private _unloading:boolean = false; + + get unloading () { + return this._unloading; + } + + constructor() { + window.addEventListener('beforeunload', () => { + this._unloading = true; + if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout) + }) + } /** * Tries to login to the node server and return the starting map url to be loaded */ @@ -25,7 +34,7 @@ class ConnectionManager { this.connexionType = connexionType; if(connexionType === GameConnexionTypes.register) { const organizationMemberToken = urlManager.getOrganizationToken(); - const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data); + const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data); this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures); localUserStore.saveUser(this.localUser); @@ -33,12 +42,12 @@ class ConnectionManager { const worldSlug = data.worldSlug; const roomSlug = data.roomSlug; - const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.hash); + const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash); urlManager.pushRoomIdToUrl(room); return Promise.resolve(room); } else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) { - const localUser = localUserStore.getLocalUser(); + let localUser = localUserStore.getLocalUser(); if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) { this.localUser = localUser; try { @@ -48,27 +57,53 @@ class ConnectionManager { console.error('JWT token invalid. Did it expire? Login anonymously instead.'); await this.anonymousLogin(); } - } else { + }else{ await this.anonymousLogin(); } - let roomId: string + + localUser = localUserStore.getLocalUser(); + if(!localUser){ + throw "Error to store local user data"; + } + + let roomId: string; if (connexionType === GameConnexionTypes.empty) { roomId = START_ROOM_URL; } else { - roomId = window.location.pathname + window.location.hash; + roomId = window.location.pathname + window.location.search + window.location.hash; } - return Promise.resolve(new Room(roomId)); + + //get detail map for anonymous login and set texture in local storage + const room = new Room(roomId); + const mapDetail = await room.getMapDetail(); + if(mapDetail.textures != undefined && mapDetail.textures.length > 0) { + //check if texture was changed + if(localUser.textures.length === 0){ + localUser.textures = mapDetail.textures; + }else{ + mapDetail.textures.forEach((newTexture) => { + const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id); + if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){ + return; + } + localUser?.textures.push(newTexture) + }); + } + this.localUser = localUser; + localUserStore.saveUser(localUser); + } + return Promise.resolve(room); } - return Promise.reject('Invalid URL'); + return Promise.reject(new Error('Invalid URL')); } private async verifyToken(token: string): Promise { - await Axios.get(`${API_URL}/verify`, {params: {token}}); + await Axios.get(`${PUSHER_URL}/verify`, {params: {token}}); } public async anonymousLogin(isBenchmark: boolean = false): Promise { - const data = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data); + const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data); this.localUser = new LocalUser(data.userUuid, data.authToken, []); if (!isBenchmark) { // In benchmark, we don't have a local storage. localUserStore.saveUser(this.localUser); @@ -79,9 +114,9 @@ class ConnectionManager { this.localUser = new LocalUser('', 'test', []); } - public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise { + public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise { return new Promise((resolve, reject) => { - const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport); + const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion); connection.onConnectError((error: object) => { console.log('An error occurred while connecting to socket server. Retrying'); reject(error); @@ -99,10 +134,10 @@ class ConnectionManager { }).catch((err) => { // Let's retry in 4-6 seconds return new Promise((resolve, reject) => { - setTimeout(() => { + this.reconnectingTimeout = setTimeout(() => { //todo: allow a way to break recursion? //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely. - this.connectToRoomSocket(roomId, name, characterLayers, position, viewport).then((connection) => resolve(connection)); + this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection)); }, 4000 + Math.floor(Math.random() * 2000) ); }); }); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 519afcd3..b5a66296 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -1,8 +1,6 @@ -import {PlayerAnimationDirections} from "../Phaser/Player/Animation"; -import {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; -import {SignalData} from "simple-peer"; -import {RoomConnection} from "./RoomConnection"; -import {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; +import type {SignalData} from "simple-peer"; +import type {RoomConnection} from "./RoomConnection"; +import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; export enum EventMessage{ CONNECT = "connect", @@ -47,6 +45,8 @@ export interface MessageUserPositionInterface { name: string; characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; + visitCardUrl: string|null; + companion: string|null; } export interface MessageUserMovedInterface { @@ -58,7 +58,9 @@ export interface MessageUserJoined { userId: number; name: string; characterLayers: BodyResourceDescriptionInterface[]; - position: PointInterface + position: PointInterface; + visitCardUrl: string | null; + companion: string|null; } export interface PositionInterface { @@ -83,11 +85,6 @@ export interface WebRtcSignalReceivedMessageInterface { webRtcPassword: string | undefined } -export interface StartMapInterface { - mapUrlStart: string, - startInstance: string -} - export interface ViewportInterface { left: number, top: number, diff --git a/front/src/Connexion/EmoteEventStream.ts b/front/src/Connexion/EmoteEventStream.ts new file mode 100644 index 00000000..9a639697 --- /dev/null +++ b/front/src/Connexion/EmoteEventStream.ts @@ -0,0 +1,19 @@ +import {Subject} from "rxjs"; + +interface EmoteEvent { + userId: number, + emoteName: string, +} + +class EmoteEventStream { + + private _stream:Subject = new Subject(); + public stream = this._stream.asObservable(); + + + fire(userId: number, emoteName:string) { + this._stream.next({userId, emoteName}); + } +} + +export const emoteEventStream = new EmoteEventStream(); \ No newline at end of file diff --git a/front/src/Connexion/LocalUser.ts b/front/src/Connexion/LocalUser.ts index 06d98b70..d240a90e 100644 --- a/front/src/Connexion/LocalUser.ts +++ b/front/src/Connexion/LocalUser.ts @@ -1,3 +1,5 @@ +import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable"; + export interface CharacterTexture { id: number, level: number, @@ -5,7 +7,23 @@ export interface CharacterTexture { rights: string } +export const maxUserNameLength: number = MAX_USERNAME_LENGTH; + +export function isUserNameValid(value: unknown): boolean { + return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && /\S/.test(value); +} + +export function areCharacterLayersValid(value: string[] | null): boolean { + if (!value || !value.length) return false; + for (let i = 0; i < value.length; i++) { + if (/^\w+$/.exec(value[i]) === null) { + return false; + } + } + return true; +} + export class LocalUser { - constructor(public readonly uuid:string, public readonly jwtToken: string, public readonly textures: CharacterTexture[]) { + constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) { } } diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 702df561..ace7b17e 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -1,14 +1,16 @@ -import {LocalUser} from "./LocalUser"; +import {areCharacterLayersValid, isUserNameValid, LocalUser} from "./LocalUser"; const playerNameKey = 'playerName'; const selectedPlayerKey = 'selectedPlayer'; const customCursorPositionKey = 'customCursorPosition'; const characterLayersKey = 'characterLayers'; +const companionKey = 'companion'; const gameQualityKey = 'gameQuality'; const videoQualityKey = 'videoQuality'; const audioPlayerVolumeKey = 'audioVolume'; const audioPlayerMuteKey = 'audioMute'; -const helpCameraSettingsShown = 'helpCameraSettingsShown'; +const helpCameraSettingsShown = 'helpCameraSettingsShown'; +const fullscreenKey = 'fullscreen'; class LocalUserStore { saveUser(localUser: LocalUser) { @@ -22,8 +24,9 @@ class LocalUserStore { setName(name:string): void { localStorage.setItem(playerNameKey, name); } - getName(): string { - return localStorage.getItem(playerNameKey) || ''; + getName(): string|null { + const value = localStorage.getItem(playerNameKey) || ''; + return isUserNameValid(value) ? value : null; } setPlayerCharacterIndex(playerCharacterIndex: number): void { @@ -44,7 +47,24 @@ class LocalUserStore { localStorage.setItem(characterLayersKey, JSON.stringify(layers)); } getCharacterLayers(): string[]|null { - return JSON.parse(localStorage.getItem(characterLayersKey) || "null"); + const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null"); + return areCharacterLayersValid(value) ? value : null; + } + + setCompanion(companion: string|null): void { + return localStorage.setItem(companionKey, JSON.stringify(companion)); + } + getCompanion(): string|null { + const companion = JSON.parse(localStorage.getItem(companionKey) || "null"); + + if (typeof companion !== "string" || companion === "") { + return null; + } + + return companion; + } + wasCompanionSet(): boolean { + return localStorage.getItem(companionKey) ? true : false; } setGameQualityValue(value: number): void { @@ -81,6 +101,13 @@ class LocalUserStore { getHelpCameraSettingsShown(): boolean { return localStorage.getItem(helpCameraSettingsShown) === '1'; } + + setFullscreen(value: boolean): void { + localStorage.setItem(fullscreenKey, value.toString()); + } + getFullscreen(): boolean { + return localStorage.getItem(fullscreenKey) === 'true'; + } } export const localUserStore = new LocalUserStore(); diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 72c3b42e..3ae8d2ed 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -1,29 +1,37 @@ import Axios from "axios"; -import {API_URL} from "../Enum/EnvironmentVariable"; +import {PUSHER_URL} from "../Enum/EnvironmentVariable"; +import type {CharacterTexture} from "./LocalUser"; + +export class MapDetail{ + constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) { + } +} export class Room { public readonly id: string; public readonly isPublic: boolean; private mapUrl: string|undefined; + private textures: CharacterTexture[]|undefined; private instance: string|undefined; + private _search: URLSearchParams; constructor(id: string) { - if (id.startsWith('/')) { - id = id.substr(1); + const url = new URL(id, 'https://example.com'); + + this.id = url.pathname; + + if (this.id.startsWith('/')) { + this.id = this.id.substr(1); } - this.id = id; - if (id.startsWith('_/')) { + if (this.id.startsWith('_/')) { this.isPublic = true; - } else if (id.startsWith('@/')) { + } else if (this.id.startsWith('@/')) { this.isPublic = false; } else { throw new Error('Invalid room ID'); } - const indexOfHash = this.id.indexOf('#'); - if (indexOfHash !== -1) { - this.id = this.id.substr(0, indexOfHash); - } + this._search = new URLSearchParams(url.search); } public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} { @@ -49,10 +57,10 @@ export class Room { return {roomId, hash} } - public async getMapUrl(): Promise { - return new Promise((resolve, reject) => { - if (this.mapUrl !== undefined) { - resolve(this.mapUrl); + public async getMapDetail(): Promise { + return new Promise((resolve, reject) => { + if (this.mapUrl !== undefined && this.textures != undefined) { + resolve(new MapDetail(this.mapUrl, this.textures)); return; } @@ -60,17 +68,17 @@ export class Room { const match = /_\/[^/]+\/(.+)/.exec(this.id); if (!match) throw new Error('Could not extract url from "'+this.id+'"'); this.mapUrl = window.location.protocol+'//'+match[1]; - resolve(this.mapUrl); + resolve(new MapDetail(this.mapUrl, this.textures)); return; } else { // We have a private ID, we need to query the map URL from the server. const urlParts = this.parsePrivateUrl(this.id); - Axios.get(`${API_URL}/map`, { + Axios.get(`${PUSHER_URL}/map`, { params: urlParts }).then(({data}) => { console.log('Map ', this.id, ' resolves to URL ', data.mapUrl); - resolve(data.mapUrl); + resolve(data); return; }).catch((reason) => { reject(reason); @@ -117,4 +125,17 @@ export class Room { } return results; } + + public isDisconnected(): boolean + { + const alone = this._search.get('alone'); + if (alone && alone !== '0' && alone.toLowerCase() !== 'false') { + return true; + } + return false; + } + + public get search(): URLSearchParams { + return this._search; + } } diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index debb99f4..dc9830c3 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -1,4 +1,4 @@ -import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; +import { PUSHER_URL, UPLOADER_URL } from "../Enum/EnvironmentVariable"; import Axios from "axios"; import { BatchMessage, @@ -27,12 +27,15 @@ import { SendJitsiJwtMessage, CharacterLayerMessage, PingMessage, - SendUserMessage, BanUserMessage + EmoteEventMessage, + EmotePromptMessage, + SendUserMessage, + BanUserMessage, } from "../Messages/generated/messages_pb" -import {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; +import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import Direction = PositionMessage.Direction; -import {ProtobufClientUtils} from "../Network/ProtobufClientUtils"; +import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; import { EventMessage, GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, @@ -41,22 +44,24 @@ import { ViewportInterface, WebRtcDisconnectMessageInterface, WebRtcSignalReceivedMessageInterface, } from "./ConnexionModels"; -import {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; -import {adminMessagesService} from "./AdminMessagesService"; -import {worldFullMessageStream} from "./WorldFullMessageStream"; -import {worldFullWarningStream} from "./WorldFullWarningStream"; +import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; +import { adminMessagesService } from "./AdminMessagesService"; +import { worldFullMessageStream } from "./WorldFullMessageStream"; +import { worldFullWarningStream } from "./WorldFullWarningStream"; +import { connectionManager } from "./ConnectionManager"; +import { emoteEventStream } from "./EmoteEventStream"; const manualPingDelay = 20000; export class RoomConnection implements RoomConnection { private readonly socket: WebSocket; - private userId: number|null = null; + private userId: number | null = null; private listeners: Map = new Map(); - private static websocketFactory: null|((url: string)=>any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any + private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any private closed: boolean = false; private tags: string[] = []; - public static setWebsocketFactory(websocketFactory: (url: string)=>any): void { // eslint-disable-line @typescript-eslint/no-explicit-any + public static setWebsocketFactory(websocketFactory: (url: string) => any): void { // eslint-disable-line @typescript-eslint/no-explicit-any RoomConnection.websocketFactory = websocketFactory; } @@ -65,21 +70,28 @@ export class RoomConnection implements RoomConnection { * @param token A JWT token containing the UUID of the user * @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" */ - public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { - let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); - url += '/room'; - url += '?roomId='+(roomId ?encodeURIComponent(roomId):''); - url += '&token='+(token ?encodeURIComponent(token):''); - url += '&name='+encodeURIComponent(name); - for (const layer of characterLayers) { - url += '&characterLayers='+encodeURIComponent(layer); + public constructor(token: string | null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string | null) { + let url = new URL(PUSHER_URL, window.location.toString()).toString(); + url = url.replace('http://', 'ws://').replace('https://', 'wss://'); + if (!url.endsWith('/')) { + url += '/'; + } + url += 'room'; + url += '?roomId=' + (roomId ? encodeURIComponent(roomId) : ''); + url += '&token=' + (token ? encodeURIComponent(token) : ''); + url += '&name=' + encodeURIComponent(name); + for (const layer of characterLayers) { + url += '&characterLayers=' + encodeURIComponent(layer); + } + url += '&x=' + Math.floor(position.x); + url += '&y=' + Math.floor(position.y); + url += '&top=' + Math.floor(viewport.top); + url += '&bottom=' + Math.floor(viewport.bottom); + url += '&left=' + Math.floor(viewport.left); + url += '&right=' + Math.floor(viewport.right); + if (typeof companion === 'string') { + url += '&companion=' + encodeURIComponent(companion); } - url += '&x='+Math.floor(position.x); - url += '&y='+Math.floor(position.y); - url += '&top='+Math.floor(viewport.top); - url += '&bottom='+Math.floor(viewport.bottom); - url += '&left='+Math.floor(viewport.left); - url += '&right='+Math.floor(viewport.right); if (RoomConnection.websocketFactory) { this.socket = RoomConnection.websocketFactory(url); @@ -89,7 +101,7 @@ export class RoomConnection implements RoomConnection { this.socket.binaryType = 'arraybuffer'; - let interval: ReturnType|undefined = undefined; + let interval: ReturnType | undefined = undefined; this.socket.onopen = (ev) => { //we manually ping every 20s to not be logged out by the server, even when the game is in background. @@ -114,7 +126,7 @@ export class RoomConnection implements RoomConnection { if (message.hasBatchmessage()) { for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) { - let event: string; + let event: string|null = null; let payload; if (subMessage.hasUsermovedmessage()) { event = EventMessage.USER_MOVED; @@ -134,16 +146,21 @@ export class RoomConnection implements RoomConnection { } else if (subMessage.hasItemeventmessage()) { event = EventMessage.ITEM_EVENT; payload = subMessage.getItemeventmessage(); + } else if (subMessage.hasEmoteeventmessage()) { + const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage; + emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); } else { throw new Error('Unexpected batch message type'); } - this.dispatch(event, payload); + if (event) { + this.dispatch(event, payload); + } } } else if (message.hasRoomjoinedmessage()) { const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; - const items: { [itemId: number] : unknown } = {}; + const items: { [itemId: number]: unknown } = {}; for (const item of roomJoinedMessage.getItemList()) { items[item.getItemid()] = JSON.parse(item.getStatejson()); } @@ -160,6 +177,9 @@ export class RoomConnection implements RoomConnection { } else if (message.hasWorldfullmessage()) { worldFullMessageStream.onMessage(); this.closed = true; + } else if (message.hasWorldconnexionmessage()) { + worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); + this.closed = true; } else if (message.hasWebrtcsignaltoclientmessage()) { this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { @@ -179,9 +199,11 @@ export class RoomConnection implements RoomConnection { } else if (message.hasSendusermessage()) { adminMessagesService.onSendusermessage(message.getSendusermessage() as SendUserMessage); } else if (message.hasBanusermessage()) { - adminMessagesService.onSendusermessage(message.getSendusermessage() as BanUserMessage); + adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage); } else if (message.hasWorldfullwarningmessage()) { worldFullWarningStream.onMessage(); + } else if (message.hasRefreshroommessage()) { + //todo: implement a way to notify the user the room was refreshed. } else { throw new Error('Unknown message received'); } @@ -215,7 +237,7 @@ export class RoomConnection implements RoomConnection { this.closed = true; } - private toPositionMessage(x : number, y : number, direction : string, moving: boolean): PositionMessage { + private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessage { const positionMessage = new PositionMessage(); positionMessage.setX(Math.floor(x)); positionMessage.setY(Math.floor(y)); @@ -252,8 +274,8 @@ export class RoomConnection implements RoomConnection { return viewportMessage; } - public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{ - if(!this.socket){ + public sharePosition(x: number, y: number, direction: string, moving: boolean, viewport: ViewportInterface): void { + if (!this.socket) { return; } @@ -315,11 +337,15 @@ export class RoomConnection implements RoomConnection { } }) + const companion = message.getCompanion(); + return { userId: message.getUserid(), name: message.getName(), characterLayers, - position: ProtobufClientUtils.toPointInterface(position) + visitCardUrl: message.getVisitcardurl(), + position: ProtobufClientUtils.toPointInterface(position), + companion: companion ? companion.getName() : null } } @@ -381,9 +407,6 @@ export class RoomConnection implements RoomConnection { this.socket.addEventListener('error', callback) } - /*public onConnect(callback: (e: Event) => void): void { - this.socket.addEventListener('open', callback) - }*/ public onConnect(callback: (roomConnection: OnConnectInterface) => void): void { //this.socket.addEventListener('open', callback) this.onMessage(EventMessage.CONNECT, callback); @@ -452,17 +475,17 @@ export class RoomConnection implements RoomConnection { }); } - public onServerDisconnected(callback: (event: CloseEvent) => void): void { + public onServerDisconnected(callback: () => void): void { this.socket.addEventListener('close', (event) => { - if (this.closed === true) { + if (this.closed === true || connectionManager.unloading) { return; } - console.log('Socket closed with code '+event.code+". Reason: "+event.reason); + console.log('Socket closed with code ' + event.code + ". Reason: " + event.reason); if (event.code === 1000) { // Normal closure case return; } - callback(event); + callback(); }); } @@ -503,8 +526,8 @@ export class RoomConnection implements RoomConnection { }); } - public uploadAudio(file : FormData){ - return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: {data:{}}) => { + public uploadAudio(file: FormData) { + return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: { data: {} }) => { return res.data; }).catch((err) => { console.error(err); @@ -535,7 +558,7 @@ export class RoomConnection implements RoomConnection { }); } - public emitGlobalMessage(message: PlayGlobalMessageInterface){ + public emitGlobalMessage(message: PlayGlobalMessageInterface) { const playGlobalMessage = new PlayGlobalMessage(); playGlobalMessage.setId(message.id); playGlobalMessage.setType(message.type); @@ -547,7 +570,7 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } - public emitReportPlayerMessage(reportedUserId: number, reportComment: string ): void { + public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void { const reportPlayerMessage = new ReportPlayerMessage(); reportPlayerMessage.setReporteduserid(reportedUserId); reportPlayerMessage.setReportcomment(reportComment); @@ -558,7 +581,7 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } - public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string|undefined ): void { + public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string | undefined): void { const queryJitsiJwtMessage = new QueryJitsiJwtMessage(); queryJitsiJwtMessage.setJitsiroom(jitsiRoom); if (tag !== undefined) { @@ -584,4 +607,18 @@ export class RoomConnection implements RoomConnection { public isAdmin(): boolean { return this.hasTag('admin'); } + + public emitEmoteEvent(emoteName: string): void { + const emoteMessage = new EmotePromptMessage(); + emoteMessage.setEmote(emoteName) + + const clientToServerMessage = new ClientToServerMessage(); + clientToServerMessage.setEmotepromptmessage(emoteMessage); + + this.socket.send(clientToServerMessage.serializeBinary().buffer); + } + + public getAllTags() : string[] { + return this.tags; + } } diff --git a/front/src/Connexion/WorldFullMessageStream.ts b/front/src/Connexion/WorldFullMessageStream.ts index c02530fa..c9f65d84 100644 --- a/front/src/Connexion/WorldFullMessageStream.ts +++ b/front/src/Connexion/WorldFullMessageStream.ts @@ -2,12 +2,12 @@ import {Subject} from "rxjs"; class WorldFullMessageStream { - private _stream:Subject = new Subject(); + private _stream:Subject = new Subject(); public stream = this._stream.asObservable(); - onMessage() { - this._stream.next(); + onMessage(message? :string) { + this._stream.next(message); } } diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 844bf564..73f6427c 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,27 +1,30 @@ const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json'; -const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "pusher.workadventure.localhost"); -const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost'); -const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "workadventure.localhost"); +const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost'; +const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost'; const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; const TURN_SERVER: string = process.env.TURN_SERVER || ""; +const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true"; +const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true"; const TURN_USER: string = process.env.TURN_USER || ''; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ''; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; -const RESOLUTION = 2; -const ZOOM_LEVEL = 1/*3/4*/; const POSITION_DELAY = 200; // Wait 200ms between sending position events const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player +export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8; +export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); +export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true'; + +export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) ); export { DEBUG_MODE, START_ROOM_URL, - API_URL, + SKIP_RENDER_OPTIMIZATIONS, + DISABLE_NOTIFICATIONS, + PUSHER_URL, UPLOADER_URL, - ADMIN_URL, - RESOLUTION, - ZOOM_LEVEL, POSITION_DELAY, MAX_EXTRAPOLATION_TIME, STUN_SERVER, diff --git a/front/src/Network/ProtobufClientUtils.ts b/front/src/Network/ProtobufClientUtils.ts index 6a402e97..3d2d5c32 100644 --- a/front/src/Network/ProtobufClientUtils.ts +++ b/front/src/Network/ProtobufClientUtils.ts @@ -1,6 +1,6 @@ import {PositionMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; -import {PointInterface} from "../Connexion/ConnexionModels"; +import type {PointInterface} from "../Connexion/ConnexionModels"; export class ProtobufClientUtils { diff --git a/front/src/Phaser/Companion/Companion.ts b/front/src/Phaser/Companion/Companion.ts new file mode 100644 index 00000000..1c43f452 --- /dev/null +++ b/front/src/Phaser/Companion/Companion.ts @@ -0,0 +1,220 @@ +import Sprite = Phaser.GameObjects.Sprite; +import Container = Phaser.GameObjects.Container; +import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation"; + +export interface CompanionStatus { + x: number; + y: number; + name: string; + moving: boolean; + direction: PlayerAnimationDirections; +} + +export class Companion extends Container { + public sprites: Map; + + private delta: number; + private invisible: boolean; + private updateListener: Function; + private target: { x: number, y: number, direction: PlayerAnimationDirections }; + + private companionName: string; + private direction: PlayerAnimationDirections; + private animationType: PlayerAnimationTypes; + + constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise) { + super(scene, x + 14, y + 4); + + this.sprites = new Map(); + + this.delta = 0; + this.invisible = true; + this.target = { x, y, direction: PlayerAnimationDirections.Down }; + + this.direction = PlayerAnimationDirections.Down; + this.animationType = PlayerAnimationTypes.Idle; + + this.companionName = name; + + texturePromise.then(resource => { + this.addResource(resource); + this.invisible = false; + }) + + this.scene.physics.world.enableBody(this); + + this.getBody().setImmovable(true); + this.getBody().setCollideWorldBounds(false); + this.setSize(16, 16); + this.getBody().setSize(16, 16); + this.getBody().setOffset(0, 8); + + this.setDepth(-1); + + this.updateListener = this.step.bind(this); + this.scene.events.addListener('update', this.updateListener); + + this.scene.add.existing(this); + } + + public setTarget(x: number, y: number, direction: PlayerAnimationDirections) { + this.target = { x, y: y + 4, direction }; + } + + public step(time: number, delta: number) { + if (typeof this.target === 'undefined') return; + + this.delta += delta; + if (this.delta < 128) { + return; + } + this.delta = 0; + + const xDist = this.target.x - this.x; + const yDist = this.target.y - this.y; + + const distance = Math.pow(xDist, 2) + Math.pow(yDist, 2); + + if (distance < 650) { + this.animationType = PlayerAnimationTypes.Idle; + this.direction = this.target.direction; + + this.getBody().stop(); + } else { + this.animationType = PlayerAnimationTypes.Walk; + + const xDir = xDist / Math.max(Math.abs(xDist), 1); + const yDir = yDist / Math.max(Math.abs(yDist), 1); + + const speed = 256; + this.getBody().setVelocity(Math.min(Math.abs(xDist * 2.5), speed) * xDir, Math.min(Math.abs(yDist * 2.5), speed) * yDir); + + if (Math.abs(xDist) > Math.abs(yDist)) { + if (xDist < 0) { + this.direction = PlayerAnimationDirections.Left; + } else { + this.direction = PlayerAnimationDirections.Right; + } + } else { + if (yDist < 0) { + this.direction = PlayerAnimationDirections.Up; + } else { + this.direction = PlayerAnimationDirections.Down; + } + } + } + + this.setDepth(this.y); + this.playAnimation(this.direction, this.animationType); + } + + public getStatus(): CompanionStatus { + const { x, y, direction, animationType, companionName } = this; + + return { + x, + y, + direction, + moving: animationType === PlayerAnimationTypes.Walk, + name: companionName + } + } + + private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void { + if (this.invisible) return; + + for (const [resource, sprite] of this.sprites.entries()) { + sprite.play(`${resource}-${direction}-${type}`, true); + } + } + + private addResource(resource: string, frame?: string | number): void { + const sprite = new Sprite(this.scene, 0, 0, resource, frame); + + this.add(sprite); + + this.getAnimations(resource).forEach(animation => { + this.scene.anims.create(animation); + }); + + this.scene.sys.updateList.add(sprite); + this.sprites.set(resource, sprite); + } + + private getAnimations(resource: string): Phaser.Types.Animations.Animation[] { + return [ + { + key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [1]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [4]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [7]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [10]}), + frameRate: 10, + repeat: 1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [0, 1, 2]}), + frameRate: 15, + repeat: -1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [3, 4, 5]}), + frameRate: 15, + repeat: -1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [6, 7, 8]}), + frameRate: 15, + repeat: -1 + }, + { + key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`, + frames: this.scene.anims.generateFrameNumbers(resource, {frames: [9, 10, 11]}), + frameRate: 15, + repeat: -1 + } + ] + } + + private getBody(): Phaser.Physics.Arcade.Body { + const body = this.body; + + if (!(body instanceof Phaser.Physics.Arcade.Body)) { + throw new Error('Container does not have arcade body'); + } + + return body; + } + + public destroy(): void { + for (const sprite of this.sprites.values()) { + if (this.scene) { + this.scene.sys.updateList.remove(sprite); + } + } + + if (this.scene) { + this.scene.events.removeListener('update', this.updateListener); + } + + super.destroy(); + } +} diff --git a/front/src/Phaser/Companion/CompanionTextures.ts b/front/src/Phaser/Companion/CompanionTextures.ts new file mode 100644 index 00000000..84eaf38f --- /dev/null +++ b/front/src/Phaser/Companion/CompanionTextures.ts @@ -0,0 +1,14 @@ +export interface CompanionResourceDescriptionInterface { + name: string, + img: string, + behaviour: "dog" | "cat" +} + +export const COMPANION_RESOURCES: CompanionResourceDescriptionInterface[] = [ + { name: "dog1", img: "resources/characters/pipoya/Dog 01-1.png", behaviour: "dog" }, + { name: "dog2", img: "resources/characters/pipoya/Dog 01-2.png", behaviour: "dog" }, + { name: "dog3", img: "resources/characters/pipoya/Dog 01-3.png", behaviour: "dog" }, + { name: "cat1", img: "resources/characters/pipoya/Cat 01-1.png", behaviour: "cat" }, + { name: "cat2", img: "resources/characters/pipoya/Cat 01-2.png", behaviour: "cat" }, + { name: "cat3", img: "resources/characters/pipoya/Cat 01-3.png", behaviour: "cat" }, +] diff --git a/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts new file mode 100644 index 00000000..75c20a48 --- /dev/null +++ b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts @@ -0,0 +1,29 @@ +import LoaderPlugin = Phaser.Loader.LoaderPlugin; +import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./CompanionTextures"; + +export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => { + COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => { + lazyLoadCompanionResource(loader, resource.name); + }); + + return COMPANION_RESOURCES; +} + +export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise => { + return new Promise((resolve, reject) => { + const resource = COMPANION_RESOURCES.find(item => item.name === name); + + if (typeof resource === 'undefined') { + return reject(`Texture '${name}' not found!`); + } + + if (loader.textureManager.exists(resource.name)) { + return resolve(resource.name); + } + + loader.spritesheet(resource.name, resource.img, { frameWidth: 32, frameHeight: 32, endFrame: 12 }); + loader.once(`filecomplete-spritesheet-${resource.name}`, () => resolve(resource.name)); + + loader.start(); // It's only automatically started during the Scene preload. + }); +} diff --git a/front/src/Phaser/Components/ChatModeIcon.ts b/front/src/Phaser/Components/ChatModeIcon.ts index 932a4d88..69449a1d 100644 --- a/front/src/Phaser/Components/ChatModeIcon.ts +++ b/front/src/Phaser/Components/ChatModeIcon.ts @@ -1,3 +1,5 @@ +import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; + export class ChatModeIcon extends Phaser.GameObjects.Sprite { constructor(scene: Phaser.Scene, x: number, y: number) { super(scene, x, y, 'layout_modes', 3); @@ -6,6 +8,6 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite { this.setOrigin(0, 1); this.setInteractive(); this.setVisible(false); - this.setDepth(99999); + this.setDepth(DEPTH_INGAME_TEXT_INDEX); } } \ No newline at end of file diff --git a/front/src/Phaser/Components/MobileJoystick.ts b/front/src/Phaser/Components/MobileJoystick.ts new file mode 100644 index 00000000..b3fc021b --- /dev/null +++ b/front/src/Phaser/Components/MobileJoystick.ts @@ -0,0 +1,65 @@ +import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js'; +import {waScaleManager} from "../Services/WaScaleManager"; +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; + +//the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free +export const joystickBaseKey = 'joystickBase'; +export const joystickBaseImg = 'resources/objects/joystickSplitted.png'; +export const joystickThumbKey = 'joystickThumb'; +export const joystickThumbImg = 'resources/objects/smallHandleFilledGrey.png'; + +const baseSize = 50; +const thumbSize = 25; +const radius = 17.5; + +export class MobileJoystick extends VirtualJoystick { + private resizeCallback: () => void; + + constructor(scene: Phaser.Scene) { + super(scene, { + x: -1000, + y: -1000, + radius: radius * window.devicePixelRatio, + base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX), + thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX), + enable: true, + dir: "8dir", + }); + this.visible = false; + this.enable = false; + + this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { + if (!pointer.wasTouch) { + return; + } + + // Let's only display the joystick if there is one finger on the screen + if ((pointer.event as TouchEvent).touches.length === 1) { + this.x = pointer.x; + this.y = pointer.y; + this.visible = true; + this.enable = true; + } else { + this.visible = false; + this.enable = false; + } + }); + this.scene.input.on('pointerup', () => { + this.visible = false; + this.enable = false; + }); + this.resizeCallback = this.resize.bind(this); + this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback); + } + + private resize() { + this.base.setDisplaySize(baseSize / waScaleManager.zoomModifier * window.devicePixelRatio, baseSize / waScaleManager.zoomModifier * window.devicePixelRatio); + this.thumb.setDisplaySize(thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio, thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio); + this.setRadius(radius / waScaleManager.zoomModifier * window.devicePixelRatio); + } + + public destroy() { + super.destroy(); + this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback); + } +} diff --git a/front/src/Phaser/Components/OpenChatIcon.ts b/front/src/Phaser/Components/OpenChatIcon.ts index 1e9429e8..ab07a80c 100644 --- a/front/src/Phaser/Components/OpenChatIcon.ts +++ b/front/src/Phaser/Components/OpenChatIcon.ts @@ -1,4 +1,5 @@ import {discussionManager} from "../../WebRtc/DiscussionManager"; +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; export const openChatIconName = 'openChatIcon'; export class OpenChatIcon extends Phaser.GameObjects.Image { @@ -9,7 +10,7 @@ export class OpenChatIcon extends Phaser.GameObjects.Image { this.setOrigin(0, 1); this.setInteractive(); this.setVisible(false); - this.setDepth(99999); + this.setDepth(DEPTH_INGAME_TEXT_INDEX); this.on("pointerup", () => discussionManager.showDiscussionPart()); } diff --git a/front/src/Phaser/Components/PresentationModeIcon.ts b/front/src/Phaser/Components/PresentationModeIcon.ts index 49ff2ea1..09c8beb5 100644 --- a/front/src/Phaser/Components/PresentationModeIcon.ts +++ b/front/src/Phaser/Components/PresentationModeIcon.ts @@ -1,3 +1,5 @@ +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; + export class PresentationModeIcon extends Phaser.GameObjects.Sprite { constructor(scene: Phaser.Scene, x: number, y: number) { super(scene, x, y, 'layout_modes', 0); @@ -6,6 +8,6 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite { this.setOrigin(0, 1); this.setInteractive(); this.setVisible(false); - this.setDepth(99999); + this.setDepth(DEPTH_INGAME_TEXT_INDEX); } } \ No newline at end of file diff --git a/front/src/Phaser/Components/RadialMenu.ts b/front/src/Phaser/Components/RadialMenu.ts new file mode 100644 index 00000000..1094f73a --- /dev/null +++ b/front/src/Phaser/Components/RadialMenu.ts @@ -0,0 +1,74 @@ +import Sprite = Phaser.GameObjects.Sprite; +import {DEPTH_UI_INDEX} from "../Game/DepthIndexes"; +import {waScaleManager} from "../Services/WaScaleManager"; + +export interface RadialMenuItem { + image: string, + name: string, +} + +export const RadialMenuClickEvent = 'radialClick'; + +export class RadialMenu extends Phaser.GameObjects.Container { + private resizeCallback: OmitThisParameter<() => void>; + + constructor(scene: Phaser.Scene, x: number, y: number, private items: RadialMenuItem[]) { + super(scene, x, y); + this.setDepth(DEPTH_UI_INDEX) + this.scene.add.existing(this); + this.initItems(); + + this.resize(); + this.resizeCallback = this.resize.bind(this); + this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback); + } + + private initItems() { + const itemsNumber = this.items.length; + const menuRadius = 70 + (waScaleManager.uiScalingFactor - 1) * 20; + this.items.forEach((item, index) => this.createRadialElement(item, index, itemsNumber, menuRadius)) + } + + private createRadialElement(item: RadialMenuItem, index: number, itemsNumber: number, menuRadius: number) { + const image = new Sprite(this.scene, 0, menuRadius, item.image); + this.add(image); + this.scene.sys.updateList.add(image); + const scalingFactor = waScaleManager.uiScalingFactor * 0.075; + image.setScale(scalingFactor) + image.setInteractive({ + useHandCursor: true, + }); + image.on('pointerdown', () => this.emit(RadialMenuClickEvent, item)); + image.on('pointerover', () => { + this.scene.tweens.add({ + targets: image, + props: { + scale: 2 * scalingFactor, + }, + duration: 500, + ease: 'Power3', + }) + }); + image.on('pointerout', () => { + this.scene.tweens.add({ + targets: image, + props: { + scale: scalingFactor, + }, + duration: 500, + ease: 'Power3', + }) + }); + const angle = 2 * Math.PI * index / itemsNumber; + Phaser.Actions.RotateAroundDistance([image], {x: 0, y: 0}, angle, menuRadius); + } + + private resize() { + this.setScale(waScaleManager.uiScalingFactor); + } + + public destroy() { + this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback); + super.destroy(); + } +} \ No newline at end of file diff --git a/front/src/Phaser/Components/SoundMeter.ts b/front/src/Phaser/Components/SoundMeter.ts index af75940e..53802d31 100644 --- a/front/src/Phaser/Components/SoundMeter.ts +++ b/front/src/Phaser/Components/SoundMeter.ts @@ -1,3 +1,5 @@ +import type {IAnalyserNode, IAudioContext, IMediaStreamAudioSourceNode} from 'standardized-audio-context'; + /** * Class to measure the sound volume of a media stream */ @@ -5,10 +7,10 @@ export class SoundMeter { private instant: number; private clip: number; //private script: ScriptProcessorNode; - private analyser: AnalyserNode|undefined; + private analyser: IAnalyserNode|undefined; private dataArray: Uint8Array|undefined; - private context: AudioContext|undefined; - private source: MediaStreamAudioSourceNode|undefined; + private context: IAudioContext|undefined; + private source: IMediaStreamAudioSourceNode|undefined; constructor() { this.instant = 0.0; @@ -16,19 +18,21 @@ export class SoundMeter { //this.script = context.createScriptProcessor(2048, 1, 1); } - private init(context: AudioContext) { - if (this.context === undefined) { - this.context = context; - this.analyser = this.context.createAnalyser(); + private init(context: IAudioContext) { + this.context = context; + this.analyser = this.context.createAnalyser(); - this.analyser.fftSize = 2048; - const bufferLength = this.analyser.fftSize; - this.dataArray = new Uint8Array(bufferLength); - } + this.analyser.fftSize = 2048; + const bufferLength = this.analyser.fftSize; + this.dataArray = new Uint8Array(bufferLength); } - public connectToSource(stream: MediaStream, context: AudioContext): void + public connectToSource(stream: MediaStream, context: IAudioContext): void { + if (this.source !== undefined) { + this.stop(); + } + this.init(context); this.source = this.context?.createMediaStreamSource(stream); @@ -83,56 +87,3 @@ export class SoundMeter { } - -// Meter class that generates a number correlated to audio volume. -// The meter class itself displays nothing, but it makes the -// instantaneous and time-decaying volumes available for inspection. -// It also reports on the fraction of samples that were at or near -// the top of the measurement range. -/*function SoundMeter(context) { - this.context = context; - this.instant = 0.0; - this.slow = 0.0; - this.clip = 0.0; - this.script = context.createScriptProcessor(2048, 1, 1); - const that = this; - this.script.onaudioprocess = function(event) { - const input = event.inputBuffer.getChannelData(0); - let i; - let sum = 0.0; - let clipcount = 0; - for (i = 0; i < input.length; ++i) { - sum += input[i] * input[i]; - if (Math.abs(input[i]) > 0.99) { - clipcount += 1; - } - } - that.instant = Math.sqrt(sum / input.length); - that.slow = 0.95 * that.slow + 0.05 * that.instant; - that.clip = clipcount / input.length; - }; -} - -SoundMeter.prototype.connectToSource = function(stream, callback) { - console.log('SoundMeter connecting'); - try { - this.mic = this.context.createMediaStreamSource(stream); - this.mic.connect(this.script); - // necessary to make sample run, but should not be. - this.script.connect(this.context.destination); - if (typeof callback !== 'undefined') { - callback(null); - } - } catch (e) { - console.error(e); - if (typeof callback !== 'undefined') { - callback(e); - } - } -}; - -SoundMeter.prototype.stop = function() { - this.mic.disconnect(); - this.script.disconnect(); -}; -*/ diff --git a/front/src/Phaser/Components/SoundMeterSprite.ts b/front/src/Phaser/Components/SoundMeterSprite.ts deleted file mode 100644 index 2787059d..00000000 --- a/front/src/Phaser/Components/SoundMeterSprite.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Container = Phaser.GameObjects.Container; -import {Scene} from "phaser"; -import GameObject = Phaser.GameObjects.GameObject; -import Rectangle = Phaser.GameObjects.Rectangle; - - -export class SoundMeterSprite extends Container { - private rectangles: Rectangle[] = new Array(); - private static readonly NB_BARS = 20; - - constructor(scene: Scene, x?: number, y?: number, children?: GameObject[]) { - super(scene, x, y, children); - - for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) { - const rectangle = new Rectangle(scene, i * 13, 0, 10, 20, (Math.round(255 - i * 255 / SoundMeterSprite.NB_BARS) << 8) + (Math.round(i * 255 / SoundMeterSprite.NB_BARS) << 16)); - this.add(rectangle); - this.rectangles.push(rectangle); - } - } - - /** - * A number between 0 and 100 - * - * @param volume - */ - public setVolume(volume: number): void { - - const normalizedVolume = volume / 100 * SoundMeterSprite.NB_BARS; - for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) { - if (normalizedVolume < i) { - this.rectangles[i].alpha = 0.5; - } else { - this.rectangles[i].alpha = 1; - } - } - } - - public getWidth(): number { - return SoundMeterSprite.NB_BARS * 13; - } - - - -} diff --git a/front/src/Phaser/Components/TextInput.ts b/front/src/Phaser/Components/TextInput.ts index 1e01029b..a8ea772f 100644 --- a/front/src/Phaser/Components/TextInput.ts +++ b/front/src/Phaser/Components/TextInput.ts @@ -1,46 +1,68 @@ +const IGNORED_KEYS = new Set([ + 'Esc', + 'Escape', + 'Alt', + 'Meta', + 'Control', + 'Ctrl', + 'Space', + 'Backspace' +]) + export class TextInput extends Phaser.GameObjects.BitmapText { private minUnderLineLength = 4; private underLine: Phaser.GameObjects.Text; + private domInput = document.createElement('input'); - constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, onChange: (text: string) => void) { + constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, + onChange: (text: string) => void) { super(scene, x, y, 'main_font', text, 32); - this.setOrigin(0.5).setCenterAlign() + this.setOrigin(0.5).setCenterAlign(); this.scene.add.existing(this); - this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}) - this.underLine.setOrigin(0.5) + const style = {fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}; + this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), style); + this.underLine.setOrigin(0.5); + this.domInput.maxLength = maxLength; + this.domInput.style.opacity = "0"; + if (text) { + this.domInput.value = text; + } - this.scene.input.keyboard.on('keydown', (event: KeyboardEvent) => { - if (event.keyCode === 8 && this.text.length > 0) { - this.deleteLetter(); - } else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) { - this.addLetter(event.key); + this.domInput.addEventListener('keydown', event => { + if (IGNORED_KEYS.has(event.key)) { + return; } + + if (!/[a-zA-Z0-9:.!&?()+-]/.exec(event.key)) { + event.preventDefault(); + } + }); + + this.domInput.addEventListener('input', (event) => { + if (event.defaultPrevented) { + return; + } + this.text = this.domInput.value; this.underLine.text = this.getUnderLineBody(this.text.length); onChange(this.text); }); + + document.body.append(this.domInput); + this.focus(); } - + private getUnderLineBody(textLength:number): string { if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength; let text = '_______'; for (let i = this.minUnderLineLength; i < textLength; i++) { - text += '__' + text += '__'; } return text; } - private deleteLetter() { - this.text = this.text.substr(0, this.text.length - 1); - } - - - private addLetter(letter: string) { - this.text += letter; - } - getText(): string { return this.text; } @@ -56,4 +78,13 @@ export class TextInput extends Phaser.GameObjects.BitmapText { this.underLine.y = y+1; return this; } + + focus() { + this.domInput.focus(); + } + + destroy(): void { + super.destroy(); + this.domInput.remove(); + } } diff --git a/front/src/Phaser/Components/TextUtils.ts b/front/src/Phaser/Components/TextUtils.ts new file mode 100644 index 00000000..db9a97fb --- /dev/null +++ b/front/src/Phaser/Components/TextUtils.ts @@ -0,0 +1,51 @@ +import type {ITiledMapObject} from "../Map/ITiledMap"; +import type {GameScene} from "../Game/GameScene"; + +export class TextUtils { + public static createTextFromITiledMapObject(scene: GameScene, object: ITiledMapObject): void { + if (object.text === undefined) { + throw new Error('This object has not textual representation.'); + } + const options: { + fontStyle?: string, + fontSize?: string, + fontFamily?: string, + color?: string, + align?: string, + wordWrap?: { + width: number, + useAdvancedWrap?: boolean + } + } = {}; + if (object.text.italic) { + options.fontStyle = 'italic'; + } + // Note: there is no support for "strikeout" and "underline" + let fontSize: number = 16; + if (object.text.pixelsize) { + fontSize = object.text.pixelsize; + } + options.fontSize = fontSize + 'px'; + if (object.text.fontfamily) { + options.fontFamily = '"'+object.text.fontfamily+'"'; + } + let color = '#000000'; + if (object.text.color !== undefined) { + color = object.text.color; + } + options.color = color; + if (object.text.wrap === true) { + options.wordWrap = { + width: object.width, + //useAdvancedWrap: true + } + } + if (object.text.halign !== undefined) { + options.align = object.text.halign; + } + + console.warn(options); + const textElem = scene.add.text(object.x, object.y, object.text.text, options); + textElem.setAngle(object.rotation); + } +} diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 797616f8..7263a584 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -1,9 +1,16 @@ import {PlayerAnimationDirections, PlayerAnimationTypes} from "../Player/Animation"; import {SpeechBubble} from "./SpeechBubble"; -import BitmapText = Phaser.GameObjects.BitmapText; +import Text = Phaser.GameObjects.Text; import Container = Phaser.GameObjects.Container; import Sprite = Phaser.GameObjects.Sprite; import {TextureError} from "../../Exception/TextureError"; +import {Companion} from "../Companion/Companion"; +import type {GameScene} from "../Game/GameScene"; +import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; +import {waScaleManager} from "../Services/WaScaleManager"; +import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; + +const playerNameY = - 25; interface AnimationData { key: string; @@ -13,25 +20,35 @@ interface AnimationData { frames : number[] } +const interactiveRadius = 35; + export abstract class Character extends Container { private bubble: SpeechBubble|null = null; - private readonly playerName: BitmapText; + private readonly playerName: Text; public PlayerValue: string; public sprites: Map; private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down; //private teleportation: Sprite; private invisible: boolean; + public companion?: Companion; + private emote: Phaser.GameObjects.Sprite | null = null; + private emoteTween: Phaser.Tweens.Tween|null = null; + scene: GameScene; - constructor(scene: Phaser.Scene, + constructor(scene: GameScene, x: number, y: number, texturesPromise: Promise, name: string, direction: PlayerAnimationDirections, moving: boolean, - frame?: string | number + frame: string | number, + isClickable: boolean, + companion: string|null, + companionTexturePromise?: Promise ) { super(scene, x, y/*, texture, frame*/); + this.scene = scene; this.PlayerValue = name; this.invisible = true @@ -43,19 +60,31 @@ export abstract class Character extends Container { this.invisible = false }) - /*this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3); - this.teleportation.setInteractive(); - this.teleportation.visible = false; - this.teleportation.on('pointerup', () => { - this.report.visible = false; - this.teleportation.visible = false; - }); - this.add(this.teleportation);*/ - - this.playerName = new BitmapText(scene, 0, - 25, 'main_font', name, 7); - this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999); + this.playerName = new Text(scene, 0, playerNameY, name, {fontFamily: '"Press Start 2P"', fontSize: '8px', strokeThickness: 2, stroke: "gray"}); + this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX); this.add(this.playerName); + if (isClickable) { + this.setInteractive({ + hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius), + hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method + useHandCursor: true, + }); + + this.on('pointerover',() => { + this.getOutlinePlugin()?.add(this.playerName, { + thickness: 2, + outlineColor: 0xffff00 + }); + this.scene.markDirty(); + }); + this.on('pointerout',() => { + this.getOutlinePlugin()?.remove(this.playerName); + this.scene.markDirty(); + }) + + } + scene.add.existing(this); this.scene.physics.world.enableBody(this); @@ -67,15 +96,28 @@ export abstract class Character extends Container { this.setDepth(-1); this.playAnimation(direction, moving); + + if (typeof companion === 'string') { + this.addCompanion(companion, companionTexturePromise); + } + } + + private getOutlinePlugin(): OutlinePipelinePlugin|undefined { + return this.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined; + } + + public addCompanion(name: string, texturePromise?: Promise): void { + if (typeof texturePromise !== 'undefined') { + this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); + } } public addTextures(textures: string[], frame?: string | number): void { for (const texture of textures) { - if(!this.scene.textures.exists(texture)){ + if(this.scene && !this.scene.textures.exists(texture)){ throw new TextureError('texture not found'); } const sprite = new Sprite(this.scene, 0, 0, texture, frame); - sprite.setInteractive({useHandCursor: true}); this.add(sprite); this.getPlayerAnimations(texture).forEach(d => { this.scene.anims.create({ @@ -189,6 +231,10 @@ export abstract class Character extends Container { } this.setDepth(this.y); + + if (this.companion) { + this.companion.setTarget(this.x, this.y, this.lastDirection); + } } stop(){ @@ -213,7 +259,84 @@ export abstract class Character extends Container { this.scene.sys.updateList.remove(sprite); } } + this.list.forEach(objectContaining => objectContaining.destroy()) super.destroy(); - this.playerName.destroy(); + } + + playEmote(emoteKey: string) { + this.cancelPreviousEmote(); + + const scalingFactor = waScaleManager.uiScalingFactor * 0.05; + const emoteY = -30 - scalingFactor * 10; + + this.playerName.setVisible(false); + this.emote = new Sprite(this.scene, 0, 0, emoteKey); + this.emote.setAlpha(0); + this.emote.setScale(0.1 * scalingFactor); + this.add(this.emote); + this.scene.sys.updateList.add(this.emote); + + this.createStartTransition(scalingFactor, emoteY); + } + + private createStartTransition(scalingFactor: number, emoteY: number) { + this.emoteTween = this.scene?.tweens.add({ + targets: this.emote, + props: { + scale: scalingFactor, + alpha: 1, + y: emoteY, + }, + ease: 'Power2', + duration: 500, + onComplete: () => { + this.startPulseTransition(emoteY, scalingFactor); + } + }); + } + + private startPulseTransition(emoteY: number, scalingFactor: number) { + this.emoteTween = this.scene?.tweens.add({ + targets: this.emote, + props: { + y: emoteY * 1.3, + scale: scalingFactor * 1.1 + }, + duration: 250, + yoyo: true, + repeat: 1, + completeDelay: 200, + onComplete: () => { + this.startExitTransition(emoteY); + } + }); + } + + private startExitTransition(emoteY: number) { + this.emoteTween = this.scene?.tweens.add({ + targets: this.emote, + props: { + alpha: 0, + y: 2 * emoteY, + }, + ease: 'Power2', + duration: 500, + onComplete: () => { + this.destroyEmote(); + } + }); + } + + cancelPreviousEmote() { + if (!this.emote) return; + + this.emoteTween?.remove(); + this.destroyEmote() + } + + private destroyEmote() { + this.emote?.destroy(); + this.emote = null; + this.playerName.setVisible(true); } } diff --git a/front/src/Phaser/Entity/CustomizedCharacter.ts b/front/src/Phaser/Entity/CustomizedCharacter.ts new file mode 100644 index 00000000..3a7f1597 --- /dev/null +++ b/front/src/Phaser/Entity/CustomizedCharacter.ts @@ -0,0 +1,20 @@ +import Container = Phaser.GameObjects.Container; +import type {Scene} from "phaser"; +import Sprite = Phaser.GameObjects.Sprite; + +/** + * A sprite of a customized character (used in the Customize Scene only) + */ +export class CustomizedCharacter extends Container { + public constructor(scene: Scene, x: number, y: number, layers: string[]) { + super(scene, x, y); + this.updateSprites(layers); + } + + public updateSprites(layers: string[]): void { + this.removeAll(true); + for (const layer of layers) { + this.add(new Sprite(this.scene, 0, 0, layer)); + } + } +} diff --git a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts index 78b66c10..95f00a9e 100644 --- a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts +++ b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts @@ -1,8 +1,11 @@ import LoaderPlugin = Phaser.Loader.LoaderPlugin; -import TextureManager = Phaser.Textures.TextureManager; -import {CharacterTexture} from "../../Connexion/LocalUser"; +import type {CharacterTexture} from "../../Connexion/LocalUser"; import {BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES} from "./PlayerTextures"; +export interface FrameConfig { + frameWidth: number, + frameHeight: number, +} export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => { const returnArray:BodyResourceDescriptionInterface[][] = []; @@ -27,7 +30,10 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise => { const name = 'customCharacterTexture'+texture.id; const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level} - return createLoadingPromise(loaderPlugin, playerResourceDescriptor); + return createLoadingPromise(loaderPlugin, playerResourceDescriptor, { + frameWidth: 32, + frameHeight: 32 + }); } export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array): Promise => { @@ -37,7 +43,10 @@ export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, textur //TODO refactor const playerResourceDescriptor = getRessourceDescriptor(textureKey); if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { - promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor)); + promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, { + frameWidth: 32, + frameHeight: 32 + })); } }catch (err){ console.error(err); @@ -62,7 +71,7 @@ export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptio const textureName:string = typeof textureKey === 'string' ? textureKey : textureKey.name; const playerResource = PLAYER_RESOURCES[textureName]; if (playerResource !== undefined) return playerResource; - + for (let i=0; i { +export const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface, frameConfig: FrameConfig) => { return new Promise((res) => { if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { return res(playerResourceDescriptor); } - loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, { - frameWidth: 32, - frameHeight: 32 - }); + loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig); loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor)); }); -} \ No newline at end of file +} diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts index a6bd4f40..32881e97 100644 --- a/front/src/Phaser/Entity/RemotePlayer.ts +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -1,13 +1,16 @@ -import {GameScene} from "../Game/GameScene"; -import {PointInterface} from "../../Connexion/ConnexionModels"; +import type {GameScene} from "../Game/GameScene"; +import type {PointInterface} from "../../Connexion/ConnexionModels"; import {Character} from "../Entity/Character"; -import {PlayerAnimationDirections} from "../Player/Animation"; +import type {PlayerAnimationDirections} from "../Player/Animation"; +import {requestVisitCardsStore} from "../../Stores/GameStore"; + /** * Class representing the sprite of a remote player (a player that plays on another computer) */ export class RemotePlayer extends Character { userId: number; + private visitCardUrl: string|null; constructor( userId: number, @@ -17,19 +20,31 @@ export class RemotePlayer extends Character { name: string, texturesPromise: Promise, direction: PlayerAnimationDirections, - moving: boolean + moving: boolean, + visitCardUrl: string|null, + companion: string|null, + companionTexturePromise?: Promise ) { - super(Scene, x, y, texturesPromise, name, direction, moving, 1); - + super(Scene, x, y, texturesPromise, name, direction, moving, 1, !!visitCardUrl, companion, companionTexturePromise); + //set data this.userId = userId; + this.visitCardUrl = visitCardUrl; + + this.on('pointerdown', () => { + requestVisitCardsStore.set(this.visitCardUrl); + }) } updatePosition(position: PointInterface): void { this.playAnimation(position.direction as PlayerAnimationDirections, position.moving); this.setX(position.x); this.setY(position.y); - + this.setDepth(position.y); //this is to make sure the perspective (player models closer the bottom of the screen will appear in front of models nearer the top of the screen). + + if (this.companion) { + this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections); + } } } diff --git a/front/src/Phaser/Entity/SpeechBubble.ts b/front/src/Phaser/Entity/SpeechBubble.ts index 231de875..17b7ed0e 100644 --- a/front/src/Phaser/Entity/SpeechBubble.ts +++ b/front/src/Phaser/Entity/SpeechBubble.ts @@ -1,12 +1,12 @@ import Scene = Phaser.Scene; -import {Character} from "./Character"; +import type {Character} from "./Character"; //todo: improve this WIP export class SpeechBubble { private bubble: Phaser.GameObjects.Graphics; private content: Phaser.GameObjects.Text; - + constructor(scene: Scene, player: Character, text: string = "") { const bubbleHeight = 50; diff --git a/front/src/Phaser/Game/AddPlayerInterface.ts b/front/src/Phaser/Game/AddPlayerInterface.ts index 0edf197e..1a5176f0 100644 --- a/front/src/Phaser/Game/AddPlayerInterface.ts +++ b/front/src/Phaser/Game/AddPlayerInterface.ts @@ -1,9 +1,11 @@ -import {PointInterface} from "../../Connexion/ConnexionModels"; -import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; +import type {PointInterface} from "../../Connexion/ConnexionModels"; +import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; export interface AddPlayerInterface { userId: number; name: string; characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; + visitCardUrl: string|null; + companion: string|null; } diff --git a/front/src/Phaser/Game/DepthIndexes.ts b/front/src/Phaser/Game/DepthIndexes.ts new file mode 100644 index 00000000..d2d38328 --- /dev/null +++ b/front/src/Phaser/Game/DepthIndexes.ts @@ -0,0 +1,8 @@ +//this file contains all the depth indexes which will be used in our game + +export const DEPTH_TILE_INDEX = 0; +//Note: Player characters use their y coordinate as their depth to simulate a perspective. +//See the Character class. +export const DEPTH_OVERLAY_INDEX = 10000; +export const DEPTH_INGAME_TEXT_INDEX = 100000; +export const DEPTH_UI_INDEX = 1000000; diff --git a/front/src/Phaser/Game/DirtyScene.ts b/front/src/Phaser/Game/DirtyScene.ts new file mode 100644 index 00000000..20602cca --- /dev/null +++ b/front/src/Phaser/Game/DirtyScene.ts @@ -0,0 +1,80 @@ +import {ResizableScene} from "../Login/ResizableScene"; +import GameObject = Phaser.GameObjects.GameObject; +import Events = Phaser.Scenes.Events; +import AnimationEvents = Phaser.Animations.Events; +import StructEvents = Phaser.Structs.Events; +import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; + +/** + * A scene that can track its dirty/pristine state. + */ +export abstract class DirtyScene extends ResizableScene { + private isAlreadyTracking: boolean = false; + protected dirty:boolean = true; + private objectListChanged:boolean = true; + private physicsEnabled: boolean = false; + + /** + * Track all objects added to the scene and adds a callback each time an animation is added. + * Whenever an object is added, removed, or when an animation is played, the dirty state is set to true. + * + * Note: this does not work with animations from sprites inside containers. + */ + protected trackDirtyAnims(): void { + if (this.isAlreadyTracking || SKIP_RENDER_OPTIMIZATIONS) { + return; + } + this.isAlreadyTracking = true; + const trackAnimationFunction = this.trackAnimation.bind(this); + this.sys.updateList.on(StructEvents.PROCESS_QUEUE_ADD, (gameObject: GameObject) => { + this.objectListChanged = true; + gameObject.on(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction); + }); + this.sys.updateList.on(StructEvents.PROCESS_QUEUE_REMOVE, (gameObject: GameObject) => { + this.objectListChanged = true; + gameObject.removeListener(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction); + }); + + this.events.on(Events.RENDER, () => { + this.objectListChanged = false; + this.dirty = false; + }); + + this.physics.disableUpdate(); + this.events.on(Events.POST_UPDATE, () => { + let objectMoving = false; + for (const body of this.physics.world.bodies.entries) { + if (body.velocity.x !== 0 || body.velocity.y !== 0) { + this.objectListChanged = true; + objectMoving = true; + if (!this.physicsEnabled) { + this.physics.enableUpdate(); + this.physicsEnabled = true; + } + break; + } + } + if (!objectMoving && this.physicsEnabled) { + this.physics.disableUpdate(); + this.physicsEnabled = false; + } + }); + + } + + private trackAnimation(): void { + this.objectListChanged = true; + } + + public isDirty(): boolean { + return this.dirty || this.objectListChanged; + } + + public markDirty(): void { + this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true); + } + + public onResize(): void { + this.objectListChanged = true; + } +} diff --git a/front/src/Phaser/Game/EmoteManager.ts b/front/src/Phaser/Game/EmoteManager.ts new file mode 100644 index 00000000..2e0bbd67 --- /dev/null +++ b/front/src/Phaser/Game/EmoteManager.ts @@ -0,0 +1,73 @@ +import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; +import {emoteEventStream} from "../../Connexion/EmoteEventStream"; +import type {GameScene} from "./GameScene"; +import type {RadialMenuItem} from "../Components/RadialMenu"; +import LoaderPlugin = Phaser.Loader.LoaderPlugin; +import type {Subscription} from "rxjs"; + + +interface RegisteredEmote extends BodyResourceDescriptionInterface { + name: string; + img: string; +} + +export const emotes: {[key: string]: RegisteredEmote} = { + 'emote-heart': {name: 'emote-heart', img: 'resources/emotes/heart-emote.png'}, + 'emote-clap': {name: 'emote-clap', img: 'resources/emotes/clap-emote.png'}, + 'emote-hand': {name: 'emote-hand', img: 'resources/emotes/hand-emote.png'}, + 'emote-thanks': {name: 'emote-thanks', img: 'resources/emotes/thanks-emote.png'}, + 'emote-thumb-up': {name: 'emote-thumb-up', img: 'resources/emotes/thumb-up-emote.png'}, + 'emote-thumb-down': {name: 'emote-thumb-down', img: 'resources/emotes/thumb-down-emote.png'}, +}; + +export class EmoteManager { + private subscription: Subscription; + + constructor(private scene: GameScene) { + this.subscription = emoteEventStream.stream.subscribe((event) => { + const actor = this.scene.MapPlayersByKey.get(event.userId); + if (actor) { + this.lazyLoadEmoteTexture(event.emoteName).then(emoteKey => { + actor.playEmote(emoteKey); + }) + } + }) + } + createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) { + return new Promise((res) => { + if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { + return res(playerResourceDescriptor.name); + } + loadPlugin.image(playerResourceDescriptor.name, playerResourceDescriptor.img); + loadPlugin.once('filecomplete-image-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor.name)); + }); + } + + lazyLoadEmoteTexture(textureKey: string): Promise { + const emoteDescriptor = emotes[textureKey]; + if (emoteDescriptor === undefined) { + throw 'Emote not found!'; + } + const loadPromise = this.createLoadingPromise(this.scene.load, emoteDescriptor); + this.scene.load.start(); + return loadPromise + } + + getMenuImages(): Promise { + const promises = []; + for (const key in emotes) { + const promise = this.lazyLoadEmoteTexture(key).then((textureKey) => { + return { + image: textureKey, + name: textureKey, + } + }); + promises.push(promise); + } + return Promise.all(promises); + } + + destroy() { + this.subscription.unsubscribe(); + } +} \ No newline at end of file diff --git a/front/src/Phaser/Game/Game.ts b/front/src/Phaser/Game/Game.ts new file mode 100644 index 00000000..e267e80a --- /dev/null +++ b/front/src/Phaser/Game/Game.ts @@ -0,0 +1,132 @@ +import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; +import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; +import {waScaleManager} from "../Services/WaScaleManager"; +import {ResizableScene} from "../Login/ResizableScene"; + +const Events = Phaser.Core.Events; + +/** + * A specialization of the main Phaser Game scene. + * It comes with an optimization to skip rendering. + * + * Beware, the "step" function might vary in future versions of Phaser. + * + * It also automatically calls "onResize" on any scenes extending ResizableScene. + */ +export class Game extends Phaser.Game { + + private _isDirty = false; + + + constructor(GameConfig: Phaser.Types.Core.GameConfig) { + super(GameConfig); + + this.scale.on(Phaser.Scale.Events.RESIZE, () => { + for (const scene of this.scene.getScenes(true)) { + if (scene instanceof ResizableScene) { + scene.onResize(); + } + } + }) + + /*window.addEventListener('resize', (event) => { + // Let's trigger the onResize method of any active scene that is a ResizableScene + for (const scene of this.scene.getScenes(true)) { + if (scene instanceof ResizableScene) { + scene.onResize(event); + } + } + });*/ + } + + public step(time: number, delta: number) + { + // @ts-ignore + if (this.pendingDestroy) + { + // @ts-ignore + return this.runDestroy(); + } + + const eventEmitter = this.events; + + // Global Managers like Input and Sound update in the prestep + + eventEmitter.emit(Events.PRE_STEP, time, delta); + + // This is mostly meant for user-land code and plugins + + eventEmitter.emit(Events.STEP, time, delta); + + // Update the Scene Manager and all active Scenes + + this.scene.update(time, delta); + + // Our final event before rendering starts + + eventEmitter.emit(Events.POST_STEP, time, delta); + + // This "if" is the changed introduced by the new "Game" class to avoid rendering unnecessarily. + if (SKIP_RENDER_OPTIMIZATIONS || this.isDirty()) { + const renderer = this.renderer; + + // Run the Pre-render (clearing the canvas, setting background colors, etc) + + renderer.preRender(); + + eventEmitter.emit(Events.PRE_RENDER, renderer, time, delta); + + // The main render loop. Iterates all Scenes and all Cameras in those scenes, rendering to the renderer instance. + + this.scene.render(renderer); + + // The Post-Render call. Tidies up loose end, takes snapshots, etc. + + renderer.postRender(); + + // The final event before the step repeats. Your last chance to do anything to the canvas before it all starts again. + + eventEmitter.emit(Events.POST_RENDER, renderer, time, delta); + } else { + // @ts-ignore + this.scene.isProcessing = false; + } + } + + private isDirty(): boolean { + if (this._isDirty) { + this._isDirty = false; + return true; + } + + // Loop through the scenes in forward order + for (let i = 0; i < this.scene.scenes.length; i++) + { + const scene = this.scene.scenes[i]; + const sys = scene.sys; + + if (sys.settings.visible && sys.settings.status >= Phaser.Scenes.LOADING && sys.settings.status < Phaser.Scenes.SLEEPING) + { + // @ts-ignore + if(typeof scene.isDirty === 'function') { + // @ts-ignore + const isDirty = scene.isDirty() || scene.tweens.getAllTweens().length > 0; + if (isDirty) { + return true; + } + } else { + return true; + } + } + } + + return false; + } + + /** + * Marks the game as needing to be redrawn. + */ + public markDirty(): void { + this._isDirty = true; + } +} diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 8751796f..a694b32e 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,19 +1,16 @@ import {GameScene} from "./GameScene"; import {connectionManager} from "../../Connexion/ConnectionManager"; -import {Room} from "../../Connexion/Room"; +import type {Room} from "../../Connexion/Room"; import {MenuScene, MenuSceneName} from "../Menu/MenuScene"; -import {HelpCameraSettingsScene, HelpCameraSettingsSceneName} from "../Menu/HelpCameraSettingsScene"; import {LoginSceneName} from "../Login/LoginScene"; import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import {EnableCameraSceneName} from "../Login/EnableCameraScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; +import {get} from "svelte/store"; +import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore"; +import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore"; + -export interface HasMovedEvent { - direction: string; - moving: boolean; - x: number; - y: number; -} /** * This class should be responsible for any scene starting/stopping @@ -21,21 +18,23 @@ export interface HasMovedEvent { export class GameManager { private playerName: string|null; private characterLayers: string[]|null; + private companion: string|null; private startRoom!:Room; currentGameSceneName: string|null = null; - + constructor() { this.playerName = localUserStore.getName(); this.characterLayers = localUserStore.getCharacterLayers(); + this.companion = localUserStore.getCompanion(); } public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise { this.startRoom = await connectionManager.initGameConnexion(); await this.loadMap(this.startRoom, scenePlugin); - + if (!this.playerName) { return LoginSceneName; - } else if (!this.characterLayers) { + } else if (!this.characterLayers || !this.characterLayers.length) { return SelectCharacterSceneName; } else { return EnableCameraSceneName; @@ -64,13 +63,21 @@ export class GameManager { } + setCompanion(companion: string|null): void { + this.companion = companion; + } + + getCompanion(): string|null { + return this.companion; + } + public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise { const roomID = room.id; - const mapUrl = await room.getMapUrl(); + const mapDetail = await room.getMapDetail(); const gameIndex = scenePlugin.getIndex(roomID); if(gameIndex === -1){ - const game : Phaser.Scene = new GameScene(room, mapUrl); + const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl); scenePlugin.add(roomID, game, false); } } @@ -79,12 +86,13 @@ export class GameManager { console.log('starting '+ (this.currentGameSceneName || this.startRoom.id)) scenePlugin.start(this.currentGameSceneName || this.startRoom.id); scenePlugin.launch(MenuSceneName); - - if (!localUserStore.getHelpCameraSettingsShown()) { - scenePlugin.launch(HelpCameraSettingsSceneName);//700 + + if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){ + helpCameraSettingsVisibleStore.set(true); + localUserStore.setHelpCameraSettingsShown(); } } - + public gameSceneIsCreated(scene: GameScene) { this.currentGameSceneName = scene.scene.key; const menuScene: MenuScene = scene.scene.get(MenuSceneName) as MenuScene; @@ -118,7 +126,7 @@ export class GameManager { scene.scene.run(fallbackSceneName) } } - + public getCurrentGameScene(scene: Phaser.Scene): GameScene { if (this.currentGameSceneName === null) throw 'No current scene id set!'; return scene.scene.get(this.currentGameSceneName) as GameScene diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 110a29d6..fba410d9 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,4 +1,7 @@ -import {ITiledMap} from "../Map/ITiledMap"; +import type {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty} from "../Map/ITiledMap"; +import { flattenGroupLayersMap } from "../Map/LayersFlattener"; +import TilemapLayer = Phaser.Tilemaps.TilemapLayer; +import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -7,13 +10,43 @@ export type PropertyChangeCallback = (newValue: string | number | boolean | unde * It is used to handle layer properties. */ export class GameMap { - private key: number|undefined; - private lastProperties = new Map(); + private key: number | undefined; + private lastProperties = new Map(); private callbacks = new Map>(); - public constructor(private map: ITiledMap) { + private tileSetPropertyMap: { [tile_index: number]: Array } = {} + public readonly flatLayers: ITiledMapLayer[]; + public readonly phaserLayers: TilemapLayer[] = []; + + public exitUrls: Array = [] + + public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array) { + this.flatLayers = flattenGroupLayersMap(map); + let depth = -2; + for (const layer of this.flatLayers) { + if(layer.type === 'tilelayer'){ + this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth)); + } + if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { + depth = DEPTH_OVERLAY_INDEX; + } + } + for (const tileset of map.tilesets) { + tileset?.tiles?.forEach(tile => { + if (tile.properties) { + this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties + tile.properties.forEach(prop => { + if (prop.name == "exitUrl" && typeof prop.value == "string") { + this.exitUrls.push(prop.value); + } + }) + } + }) + } } + + /** * Sets the position of the current player (in pixels) * This will trigger events if properties are changing. @@ -48,21 +81,27 @@ export class GameMap { } } - public getCurrentProperties(): Map { + public getCurrentProperties(): Map { return this.lastProperties; } - private getProperties(key: number): Map { - const properties = new Map(); + private getProperties(key: number): Map { + const properties = new Map(); - for (const layer of this.map.layers) { + for (const layer of this.flatLayers) { if (layer.type !== 'tilelayer') { continue; } - const tiles = layer.data as number[]; - if (tiles[key] == 0) { - continue; + + let tileIndex: number | undefined = undefined; + if (layer.data) { + const tiles = layer.data as number[]; + if (tiles[key] == 0) { + continue; + } + tileIndex = tiles[key] } + // There is a tile in this layer, let's embed the properties if (layer.properties !== undefined) { for (const layerProperty of layer.properties) { @@ -72,11 +111,25 @@ export class GameMap { properties.set(layerProperty.name, layerProperty.value); } } + + if (tileIndex) { + this.tileSetPropertyMap[tileIndex]?.forEach(property => { + if (property.value) { + properties.set(property.name, property.value) + } else if (properties.has(property.name)) { + properties.delete(property.name) + } + }) + } } return properties; } + public getMap(): ITiledMap{ + return this.map; + } + private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map) { const callbacksArray = this.callbacks.get(propName); if (callbacksArray !== undefined) { @@ -97,4 +150,19 @@ export class GameMap { } callbacksArray.push(callback); } + + public findLayer(layerName: string): ITiledMapLayer | undefined { + return this.flatLayers.find((layer) => layer.name === layerName); + } + + public findPhaserLayer(layerName: string): TilemapLayer | undefined { + return this.phaserLayers.find((layer) => layer.layer.name === layerName); + } + + public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { + for (const phaserLayer of this.phaserLayers) { + phaserLayer.tileset.push(terrain); + } + } + } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 15872658..e6e93b57 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,5 +1,10 @@ -import {gameManager, HasMovedEvent} from "./GameManager"; -import { +import { Queue } from 'queue-typescript'; +import type { Subscription } from "rxjs"; +import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; +import { userMessageManager } from "../../Administration/UserMessageManager"; +import { iframeListener } from "../../Api/IframeListener"; +import { connectionManager } from "../../Connexion/ConnectionManager"; +import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserMovedInterface, @@ -9,66 +14,91 @@ import { PositionInterface, RoomJoinedMessageInterface } from "../../Connexion/ConnexionModels"; -import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; -import {DEBUG_MODE, JITSI_PRIVATE_MODE, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable"; -import {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapObject, ITiledTileSet} from "../Map/ITiledMap"; -import {AddPlayerInterface} from "./AddPlayerInterface"; -import {PlayerAnimationDirections} from "../Player/Animation"; -import {PlayerMovement} from "./PlayerMovement"; -import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; -import {RemotePlayer} from "../Entity/RemotePlayer"; -import {Queue} from 'queue-typescript'; -import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; -import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { Room } from "../../Connexion/Room"; +import type { RoomConnection } from "../../Connexion/RoomConnection"; +import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { - CenterListener, + DEBUG_MODE, + JITSI_PRIVATE_MODE, + MAX_PER_GROUP, + POSITION_DELAY +} from "../../Enum/EnvironmentVariable"; +import { TextureError } from "../../Exception/TextureError"; +import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; +import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; +import { touchScreenManager } from "../../Touch/TouchScreenManager"; +import { urlManager } from "../../Url/UrlManager"; +import { audioManager } from "../../WebRtc/AudioManager"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { HtmlUtils } from "../../WebRtc/HtmlUtils"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { + AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, + Box, JITSI_MESSAGE_PROPERTIES, layoutManager, - LayoutMode, ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, - WEBSITE_MESSAGE_PROPERTIES, - AUDIO_VOLUME_PROPERTY, - AUDIO_LOOP_PROPERTY + WEBSITE_MESSAGE_PROPERTIES } from "../../WebRtc/LayoutManager"; -import {GameMap} from "./GameMap"; -import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; -import {mediaManager} from "../../WebRtc/MediaManager"; -import {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; -import {ActionableItem} from "../Items/ActionableItem"; -import {UserInputManager} from "../UserInput/UserInputManager"; -import {UserMovedMessage} from "../../Messages/generated/messages_pb"; -import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; -import {connectionManager} from "../../Connexion/ConnectionManager"; -import {RoomConnection} from "../../Connexion/RoomConnection"; -import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; -import {userMessageManager} from "../../Administration/UserMessageManager"; -import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; -import {ResizableScene} from "../Login/ResizableScene"; -import {Room} from "../../Connexion/Room"; -import {jitsiFactory} from "../../WebRtc/JitsiFactory"; -import {urlManager} from "../../Url/UrlManager"; -import {audioManager} from "../../WebRtc/AudioManager"; -import {PresentationModeIcon} from "../Components/PresentationModeIcon"; -import {ChatModeIcon} from "../Components/ChatModeIcon"; -import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; -import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; -import {TextureError} from "../../Exception/TextureError"; -import {addLoader} from "../Components/Loader"; -import {ErrorSceneName} from "../Reconnecting/ErrorScene"; -import {localUserStore} from "../../Connexion/LocalUserStore"; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; +import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import { ChatModeIcon } from "../Components/ChatModeIcon"; +import { addLoader } from "../Components/Loader"; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; +import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; +import { PresentationModeIcon } from "../Components/PresentationModeIcon"; +import { TextUtils } from "../Components/TextUtils"; +import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; +import { RemotePlayer } from "../Entity/RemotePlayer"; +import type { ActionableItem } from "../Items/ActionableItem"; +import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +import type { + ITiledMap, + ITiledMapLayer, + ITiledMapLayerProperty, + ITiledMapObject, + ITiledMapTileLayer, + ITiledTileSet } from "../Map/ITiledMap"; +import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; +import { PlayerAnimationDirections } from "../Player/Animation"; +import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; +import { ErrorSceneName } from "../Reconnecting/ErrorScene"; +import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; +import { waScaleManager } from "../Services/WaScaleManager"; +import { PinchManager } from "../UserInput/PinchManager"; +import { UserInputManager } from "../UserInput/UserInputManager"; +import type { AddPlayerInterface } from "./AddPlayerInterface"; +import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; +import { DirtyScene } from "./DirtyScene"; +import { EmoteManager } from "./EmoteManager"; +import { gameManager } from "./GameManager"; +import { GameMap } from "./GameMap"; +import { PlayerMovement } from "./PlayerMovement"; +import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; -import {Subscription} from "rxjs"; -import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; +import DOMElement = Phaser.GameObjects.DOMElement; +import EVENT_TYPE = Phaser.Scenes.Events +import RenderTexture = Phaser.GameObjects.RenderTexture; +import Tilemap = Phaser.Tilemaps.Tilemap; +import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; + +import AnimatedTiles from "phaser-animated-tiles"; +import {soundManager} from "./SoundManager"; +import {peerStore, screenSharingPeerStore} from "../../Stores/PeerStore"; +import {videoFocusStore} from "../../Stores/VideoFocusStore"; +import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore"; export interface GameSceneInitInterface { - initPosition: PointInterface|null, + initPosition: PointInterface | null, reconnecting: boolean } @@ -104,61 +134,66 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; -export class GameScene extends ResizableScene implements CenterListener { - Terrains : Array; - CurrentPlayer!: CurrentGamerInterface; +export class GameScene extends DirtyScene { + Terrains: Array; + CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; - MapPlayersByKey : Map = new Map(); + MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; - Layers!: Array; Objects!: Array; mapFile!: ITiledMap; + animatedTiles!: AnimatedTiles; groups: Map; startX!: number; startY!: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; - pendingEvents: Queue = new Queue(); - private initPosition: PositionInterface|null = null; + pendingEvents: Queue = new Queue(); + private initPosition: PositionInterface | null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); - public connection!: RoomConnection; + public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; - public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private connectionAnswerPromise: Promise; - private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike) => void; + private connectionAnswerPromiseResolve!: (value: RoomJoinedMessageInterface | PromiseLike) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; - + private iframeSubscriptionList!: Array; + private peerStoreUnsubscribe!: () => void; + private biggestAvailableAreaStoreUnsubscribe!: () => void; MapUrlFile: string; RoomId: string; instance: string; currentTick!: number; lastSentTick!: number; // The last tick at which a position was sent. - lastMoveEventSent: HasMovedEvent = { + lastMoveEventSent: HasPlayerMovedEvent = { direction: '', moving: false, x: -1000, y: -1000 } - private presentationModeSprite!: Sprite; - private chatModeSprite!: Sprite; private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. - private outlinedItem: ActionableItem|null = null; + private outlinedItem: ActionableItem | null = null; public userInputManager!: UserInputManager; - private isReconnecting: boolean|undefined = undefined; + private isReconnecting: boolean | undefined = undefined; private startLayerName!: string | null; private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; - private messageSubscription: Subscription|null = null; + private companion!: string | null; + private messageSubscription: Subscription | null = null; + private popUpElements: Map = new Map(); + private originalMapUrl: string | undefined; + private pinchManager: PinchManager | undefined; + private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. + private emoteManager!: EmoteManager; - constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { + constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ key: customKey ?? room.id }); @@ -172,10 +207,10 @@ export class GameScene extends ResizableScene implements CenterListener { this.createPromise = new Promise((resolve, reject): void => { this.createPromiseResolve = resolve; - }) + }); this.connectionAnswerPromise = new Promise((resolve, reject): void => { this.connectionAnswerPromiseResolve = resolve; - }) + }); } //hook preload scene @@ -189,14 +224,49 @@ export class GameScene extends ResizableScene implements CenterListener { } this.load.image(openChatIconName, 'resources/objects/talk.png'); - this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { + if (touchScreenManager.supportTouchScreen) { + this.load.image(joystickBaseKey, joystickBaseImg); + this.load.image(joystickThumbKey, joystickThumbImg); + } + this.load.audio('audio-webrtc-in', '/resources/objects/webrtc-in.mp3'); + this.load.audio('audio-webrtc-out', '/resources/objects/webrtc-out.mp3'); + //this.load.audio('audio-report-message', '/resources/objects/report-message.mp3'); + this.sound.pauseOnBlur = false; + + this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { + // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) + if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { + this.originalMapUrl = this.MapUrlFile; + this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://'); + this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.onMapLoad(data); + }); + return; + } + // 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP. + // So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes) + // See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure + const url = new URL(file.src); + const host = url.host.split(':')[0]; + if (window.location.protocol === 'https:' && file.src === this.MapUrlFile && (host === '127.0.0.1' || host === 'localhost' || host.endsWith('.localhost')) && this.originalMapUrl === undefined) { + this.originalMapUrl = this.MapUrlFile; + this.MapUrlFile = this.MapUrlFile.replace('https://', 'http://'); + this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.onMapLoad(data); + }); + return; + } + this.scene.start(ErrorSceneName, { title: 'Network error', subTitle: 'An error occurred while loading resource:', - message: file.src + message: this.originalMapUrl ?? file.src }); }); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.scenePlugin('AnimatedTiles', AnimatedTiles, 'animatedTiles', 'animatedTiles'); + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token @@ -208,9 +278,17 @@ export class GameScene extends ResizableScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32}); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + //eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.load as any).rexWebFont({ + custom: { + families: ['Press Start 2P'], + urls: ['/resources/fonts/fonts.css'], + testString: 'abcdefg' + }, + }); + //this function must stay at the end of preload function addLoader(this); } @@ -236,7 +314,7 @@ export class GameScene extends ResizableScene implements CenterListener { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup') { for (const object of layer.objects) { - let objectsOfType: ITiledMapObject[]|undefined; + let objectsOfType: ITiledMapObject[] | undefined; if (!objects.has(object.type)) { objectsOfType = new Array(); } else { @@ -263,7 +341,8 @@ export class GameScene extends ResizableScene implements CenterListener { break; } default: - throw new Error('Unsupported object type: "'+ itemType +'"'); + continue; + //throw new Error('Unsupported object type: "'+ itemType +'"'); } itemFactory.preload(this.load); @@ -289,10 +368,16 @@ export class GameScene extends ResizableScene implements CenterListener { }); }); } + + // Now, let's load the script, if any + const scripts = this.getScriptUrls(this.mapFile); + for (const script of scripts) { + iframeListener.registerScript(script); + } } //hook initialisation - init(initData : GameSceneInitInterface) { + init(initData: GameSceneInitInterface) { if (initData.initPosition !== undefined) { this.initPosition = initData.initPosition; //todo: still used? } @@ -303,11 +388,17 @@ export class GameScene extends ResizableScene implements CenterListener { //hook create scene create(): void { + this.trackDirtyAnims(); + gameManager.gameSceneIsCreated(this); urlManager.pushRoomIdToUrl(this.room); this.startLayerName = urlManager.getStartLayerNameFromUrl(); - - this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError()) + + if (touchScreenManager.supportTouchScreen) { + this.pinchManager = new PinchManager(this); + } + + this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError(message)) const playerName = gameManager.getPlayerName(); if (!playerName) { @@ -315,11 +406,10 @@ export class GameScene extends ResizableScene implements CenterListener { } this.playerName = playerName; this.characterLayers = gameManager.getCharacterLayers(); - + this.companion = gameManager.getCompanion(); //initalise map this.Map = this.add.tilemap(this.MapUrlFile); - this.gameMap = new GameMap(this.mapFile); const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.Terrains.push(this.Map.addTilesetImage(tileset.name, `${mapDirUrl}/${tileset.image}`, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing/*, tileset.firstgid*/)); @@ -329,11 +419,9 @@ export class GameScene extends ResizableScene implements CenterListener { this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); //add layer on map - this.Layers = new Array(); - let depth = -2; - for (const layer of this.mapFile.layers) { + this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); + for (const layer of this.gameMap.flatLayers) { if (layer.type === 'tilelayer') { - this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { @@ -344,13 +432,18 @@ export class GameScene extends ResizableScene implements CenterListener { this.loadNextGame(exitUrl); } } - if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { - depth = 10000; + if (layer.type === 'objectgroup') { + for (const object of layer.objects) { + if (object.text) { + TextUtils.createTextFromITiledMapObject(this, object); + } + } } } - if (depth === -2) { - throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at.'); - } + + this.gameMap.exitUrls.forEach(exitUrl => { + this.loadNextGame(exitUrl) + }) this.initStartXAndStartY(); @@ -358,34 +451,44 @@ export class GameScene extends ResizableScene implements CenterListener { this.Objects = new Array(); //initialise list of other player - this.MapPlayers = this.physics.add.group({immovable: true}); + this.MapPlayers = this.physics.add.group({ immovable: true }); + //create input to move this.userInputManager = new UserInputManager(this); mediaManager.setUserInputManager(this.userInputManager); + if (localUserStore.getFullscreen()) { + document.querySelector('body')?.requestFullscreen(); + } + //notify game manager can to create currentUser in map this.createCurrentPlayer(); this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted this.initCamera(); + this.animatedTiles.init(this.Map); + this.events.on('tileanimationupdate', () => this.dirty = true); + this.initCirclesCanvas(); // Let's pause the scene if the connection is not established yet - if (this.isReconnecting) { - setTimeout(() => { - this.scene.sleep(); - this.scene.launch(ReconnectingSceneName); - }, 0); - } else if (this.connection === undefined) { - // Let's wait 1 second before printing the "connecting" screen to avoid blinking - setTimeout(() => { - if (this.connection === undefined) { + if (!this.room.isDisconnected()) { + if (this.isReconnecting) { + setTimeout(() => { this.scene.sleep(); this.scene.launch(ReconnectingSceneName); - } - }, 1000); + }, 0); + } else if (this.connection === undefined) { + // Let's wait 1 second before printing the "connecting" screen to avoid blinking + setTimeout(() => { + if (this.connection === undefined) { + this.scene.sleep(); + this.scene.launch(ReconnectingSceneName); + } + }, 1000); + } } this.createPromiseResolve(); @@ -394,23 +497,43 @@ export class GameScene extends ResizableScene implements CenterListener { this.outlinedItem?.activate(); }); - this.presentationModeSprite = new PresentationModeIcon(this, 36, this.game.renderer.height - 2); - this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); - this.chatModeSprite = new ChatModeIcon(this, 70, this.game.renderer.height - 2); - this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2) - // FIXME: change this to use the UserInputManager class for input - this.input.keyboard.on('keyup-M', () => { - this.switchLayoutMode(); - }); - this.reposition(); // From now, this game scene will be notified of reposition events - layoutManager.setListener(this); - this.triggerOnMapLayerPropertyChange(); + this.biggestAvailableAreaStoreUnsubscribe = biggestAvailableAreaStore.subscribe((box) => this.updateCameraOffset(box)); + this.triggerOnMapLayerPropertyChange(); + this.listenToIframeEvents(); + + + if (!this.room.isDisconnected()) { + this.connect(); + } + + this.emoteManager = new EmoteManager(this); + + let oldPeerNumber = 0; + this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { + const newPeerNumber = peers.size; + if (newPeerNumber > oldPeerNumber) { + this.sound.play('audio-webrtc-in', { + volume: 0.2 + }); + } else if (newPeerNumber < oldPeerNumber) { + this.sound.play('audio-webrtc-out', { + volume: 0.2 + }); + } + oldPeerNumber = newPeerNumber; + }); + } + + /** + * Initializes the connection to Pusher. + */ + private connect(): void { const camera = this.cameras.main; connectionManager.connectToRoomSocket( @@ -426,7 +549,9 @@ export class GameScene extends ResizableScene implements CenterListener { top: camera.scrollY, right: camera.scrollX + camera.width, bottom: camera.scrollY + camera.height, - }).then((onConnect: OnConnectInterface) => { + }, + this.companion + ).then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; this.connection.onUserJoins((message: MessageUserJoined) => { @@ -434,7 +559,9 @@ export class GameScene extends ResizableScene implements CenterListener { userId: message.userId, characterLayers: message.characterLayers, name: message.name, - position: message.position + position: message.position, + visitCardUrl: message.visitCardUrl, + companion: message.companion } this.addPlayer(userMessage); }); @@ -471,9 +598,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.connection.onServerDisconnected(() => { console.log('Player disconnected from server. Reloading scene.'); - - this.simplePeer.closeAllConnections(); - this.simplePeer.unregister(); + this.cleanupClosingScene(); const gameSceneKey = 'somekey' + Math.round(Math.random() * 10000); const game: Phaser.Scene = new GameScene(this.room, this.MapUrlFile, gameSceneKey); @@ -508,21 +633,20 @@ export class GameScene extends ResizableScene implements CenterListener { // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); + peerStore.connectToSimplePeer(this.simplePeer); + screenSharingPeerStore.connectToSimplePeer(this.simplePeer); + videoFocusStore.connectToSimplePeer(this.simplePeer); this.GlobalMessageManager = new GlobalMessageManager(this.connection); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); const self = this; this.simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { - self.presentationModeSprite.setVisible(true); - self.chatModeSprite.setVisible(true); + onConnect(peer) { self.openChatIcon.setVisible(true); audioManager.decreaseVolume(); }, onDisconnect(userId: number) { if (self.simplePeer.getNbConnections() === 0) { - self.presentationModeSprite.setVisible(false); - self.chatModeSprite.setVisible(false); self.openChatIcon.setVisible(false); audioManager.restoreVolume(); } @@ -532,14 +656,13 @@ export class GameScene extends ResizableScene implements CenterListener { //listen event to share position of user this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { + this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { this.gameMap.setPosition(event.x, event.y); }) //this.initUsersPosition(roomJoinedMessage.users); this.connectionAnswerPromiseResolve(onConnect.room); // Analyze tags to find if we are admin. If yes, show console. - this.ConsoleGlobalMessageManager = new ConsoleGlobalMessageManager(this.connection, this.userInputManager, this.connection.isAdmin()); this.scene.wake(); @@ -550,7 +673,6 @@ export class GameScene extends ResizableScene implements CenterListener { }); } - //todo: into dedicated classes private initCirclesCanvas(): void { // Let's generate the circle for the group delimiter @@ -579,23 +701,23 @@ export class GameScene extends ResizableScene implements CenterListener { const contextRed = this.circleRedTexture.context; contextRed.beginPath(); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); - // context.lineWidth = 5; + //context.lineWidth = 5; contextRed.strokeStyle = '#ff0000'; contextRed.stroke(); this.circleRedTexture.refresh(); } - - private safeParseJSONstring(jsonString: string|undefined, propertyName: string) { + + private safeParseJSONstring(jsonString: string | undefined, propertyName: string) { try { return jsonString ? JSON.parse(jsonString) : {}; - } catch(e) { + } catch (e) { console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e); return {} } } - private triggerOnMapLayerPropertyChange(){ + private triggerOnMapLayerPropertyChange() { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); @@ -606,22 +728,22 @@ export class GameScene extends ResizableScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('openWebsite', this.userInputManager); coWebsiteManager.closeCoWebsite(); - }else{ + } else { const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsitePolicy') as string | undefined); + coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); - if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); - if(message === undefined){ - message = 'Press on SPACE to open the web site'; + if (message === undefined) { + message = 'Press SPACE or touch here to open web site'; } layoutManager.addActionButton('openWebsite', message.toString(), () => { openWebsiteFunction(); }, this.userInputManager); - }else{ + } else { openWebsiteFunction(); } } @@ -630,14 +752,14 @@ export class GameScene extends ResizableScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('jitsiRoom', this.userInputManager); this.stopJitsi(); - }else{ + } else { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; if (JITSI_PRIVATE_MODE && !jitsiUrl) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + const adminTag = allProps.get("jitsiRoomAdminTag") as string | undefined; - this.connection.emitQueryJitsiJwtMessage(roomName, adminTag); + this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { this.startJitsi(roomName, undefined); } @@ -645,45 +767,227 @@ export class GameScene extends ResizableScene implements CenterListener { } const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); - if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { - message = 'Press on SPACE to enter in jitsi meet room'; + message = 'Press SPACE or touch here to enter Jitsi Meet room'; } layoutManager.addActionButton('jitsiRoom', message.toString(), () => { openJitsiRoomFunction(); }, this.userInputManager); - }else{ + } else { openJitsiRoomFunction(); } } }); this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { if (newValue === undefined || newValue === false || newValue === '') { - this.connection.setSilent(false); + this.connection?.setSilent(false); } else { - this.connection.setSilent(true); + this.connection?.setSilent(true); } }); this.gameMap.onPropertyChange('playAudio', (newValue, oldValue, allProps) => { - const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number|undefined; - const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean|undefined; + const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined; + const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined; newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); }); // TODO: This legacy property should be removed at some point this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => { newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true); }); + + this.gameMap.onPropertyChange('zone', (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === '') { + iframeListener.sendLeaveEvent(oldValue as string); + + } else { + iframeListener.sendEnterEvent(newValue as string); + } + }); } + private listenToIframeEvents(): void { + this.iframeSubscriptionList = []; + this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { + + let objectLayerSquare: ITiledMapObject; + const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject); + if (targetObjectData !== undefined) { + objectLayerSquare = targetObjectData; + } else { + console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map."); + return; + } + const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message); + let html = `