Compare commits
1 commit
develop
...
localEnvir
Author | SHA1 | Date | |
---|---|---|---|
f47495850e |
|
@ -1,2 +0,0 @@
|
||||||
**/node_modules/**
|
|
||||||
**/Dockerfile
|
|
|
@ -5,27 +5,4 @@ JITSI_PRIVATE_MODE=false
|
||||||
JITSI_ISS=
|
JITSI_ISS=
|
||||||
SECRET_JITSI_KEY=
|
SECRET_JITSI_KEY=
|
||||||
ADMIN_API_TOKEN=123
|
ADMIN_API_TOKEN=123
|
||||||
START_ROOM_URL=/_/global/maps.workadventure.localhost/starter/map.json
|
HOST_NAME=workadventure.localhost
|
||||||
# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here.
|
|
||||||
# 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
|
|
||||||
|
|
||||||
OPID_CLIENT_ID=
|
|
||||||
OPID_CLIENT_SECRET=
|
|
||||||
OPID_CLIENT_ISSUER=
|
|
||||||
OPID_CLIENT_REDIRECT_URL=
|
|
||||||
OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen
|
|
||||||
OPID_PROFILE_SCREEN_PROVIDER=
|
|
||||||
DISABLE_ANONYMOUS=
|
|
||||||
|
|
||||||
# If you want to have a contact page in your menu, you MUST set CONTACT_URL to the URL of the page that you want
|
|
||||||
CONTACT_URL=
|
|
124
.github/workflows/build-and-deploy.yml
vendored
124
.github/workflows/build-and-deploy.yml
vendored
|
@ -1,13 +1,7 @@
|
||||||
name: Build, push and deploy Docker image
|
name: Build, push and deploy Docker image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
- push
|
||||||
branches: [master, develop]
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
pull_request:
|
|
||||||
types: [ labeled, synchronize ]
|
|
||||||
|
|
||||||
|
|
||||||
# Enables BuildKit
|
# Enables BuildKit
|
||||||
env:
|
env:
|
||||||
|
@ -16,7 +10,7 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-front:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -36,11 +30,11 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-front
|
repository: thecodingmachine/workadventure-front
|
||||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-back:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -59,11 +53,11 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-back
|
repository: thecodingmachine/workadventure-back
|
||||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-pusher:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -82,11 +76,11 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-pusher
|
repository: thecodingmachine/workadventure-pusher
|
||||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-uploader:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -105,11 +99,34 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-uploader
|
repository: thecodingmachine/workadventure-uploader
|
||||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
|
add_git_labels: true
|
||||||
|
|
||||||
|
build-website:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Create a slugified value of the branch
|
||||||
|
- uses: rlespinasse/github-slug-action@3.1.0
|
||||||
|
|
||||||
|
- name: "Build and push back image"
|
||||||
|
uses: docker/build-push-action@v1
|
||||||
|
with:
|
||||||
|
dockerfile: website/Dockerfile
|
||||||
|
path: website/
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
repository: thecodingmachine/workadventure-website
|
||||||
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-maps:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -129,7 +146,7 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-maps
|
repository: thecodingmachine/workadventure-maps
|
||||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
deeploy:
|
deeploy:
|
||||||
|
@ -139,8 +156,8 @@ jobs:
|
||||||
- build-pusher
|
- build-pusher
|
||||||
- build-maps
|
- build-maps
|
||||||
- build-uploader
|
- build-uploader
|
||||||
|
- build-website
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -149,37 +166,6 @@ jobs:
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@3.1.0
|
- 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
|
- name: Deploy
|
||||||
uses: thecodingmachine/deeployer-action@master
|
uses: thecodingmachine/deeployer-action@master
|
||||||
env:
|
env:
|
||||||
|
@ -188,17 +174,43 @@ jobs:
|
||||||
JITSI_ISS: ${{ secrets.JITSI_ISS }}
|
JITSI_ISS: ${{ secrets.JITSI_ISS }}
|
||||||
JITSI_URL: ${{ secrets.JITSI_URL }}
|
JITSI_URL: ${{ secrets.JITSI_URL }}
|
||||||
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
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 }}
|
|
||||||
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
|
|
||||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
|
||||||
with:
|
with:
|
||||||
namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
|
||||||
|
|
||||||
- name: Add a comment in PR
|
- name: Add a comment in PR
|
||||||
uses: unsplash/comment-on-pr@v1.2.0
|
uses: unsplash/comment-on-pr@v1.2.0
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
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"
|
msg: Environment deployed at https://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||||
|
check_for_duplicate_msg: true
|
||||||
|
|
||||||
|
- name: Run Cypress tests
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
|
||||||
|
env:
|
||||||
|
CYPRESS_BASE_URL: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||||
|
with:
|
||||||
|
env: host=play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com,port=80
|
||||||
|
spec: cypress/integration/spec.js
|
||||||
|
wait-on: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||||
|
working-directory: e2e
|
||||||
|
|
||||||
|
- name: Run Cypress tests in prod
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
if: ${{ env.GITHUB_REF_SLUG == 'master' }}
|
||||||
|
env:
|
||||||
|
CYPRESS_BASE_URL: https://play.workadventu.re
|
||||||
|
with:
|
||||||
|
env: host=play.workadventu.re
|
||||||
|
spec: cypress/integration/spec.js
|
||||||
|
wait-on: https://workadventu.re
|
||||||
|
working-directory: e2e
|
||||||
|
|
||||||
|
- name: "Upload the screenshot on test failure"
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: "screenshot"
|
||||||
|
path: "./e2e/cypress/screenshots/spec.js/WorkAdventureGame -- loads (failed).png"
|
||||||
|
|
10
.github/workflows/cleanup.yml
vendored
10
.github/workflows/cleanup.yml
vendored
|
@ -1,8 +1,7 @@
|
||||||
name: Cleanup images and environments
|
name: Cleanup images and environments
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
- delete
|
||||||
types: [ closed ]
|
|
||||||
|
|
||||||
# Enables BuildKit
|
# Enables BuildKit
|
||||||
env:
|
env:
|
||||||
|
@ -15,12 +14,13 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@3.1.0
|
- uses: rlespinasse/github-slug-action@1.1.0
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
continue-on-error: true
|
|
||||||
uses: thecodingmachine/deeployer-cleanup-action@master
|
uses: thecodingmachine/deeployer-cleanup-action@master
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
||||||
with:
|
with:
|
||||||
namespace: workadventure-${{ env.GITHUB_HEAD_REF_SLUG }}
|
# 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 }}
|
||||||
|
|
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
|
@ -1,71 +0,0 @@
|
||||||
# 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
|
|
80
.github/workflows/continuous_integration.yml
vendored
80
.github/workflows/continuous_integration.yml
vendored
|
@ -3,13 +3,11 @@
|
||||||
name: "Continuous Integration"
|
name: "Continuous Integration"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
- "pull_request"
|
||||||
branches:
|
- "push"
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
continuous-integration-front:
|
continuous-integration-front:
|
||||||
name: "Continuous Integration Front"
|
name: "Continuous Integration Front"
|
||||||
|
|
||||||
|
@ -38,87 +36,23 @@ jobs:
|
||||||
working-directory: "messages"
|
working-directory: "messages"
|
||||||
|
|
||||||
- name: "Build proto messages"
|
- name: "Build proto messages"
|
||||||
run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
|
run: yarn run proto && yarn run copy-to-front
|
||||||
working-directory: "messages"
|
working-directory: "messages"
|
||||||
|
|
||||||
- name: "Create index.html"
|
|
||||||
run: ./templater.sh
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: "Generate i18n files"
|
|
||||||
run: yarn run typesafe-i18n
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: yarn run build
|
run: yarn run build
|
||||||
env:
|
env:
|
||||||
PUSHER_URL: "//localhost:8080"
|
API_URL: "localhost:8080"
|
||||||
ADMIN_URL: "//localhost:80"
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: "Svelte check"
|
|
||||||
run: yarn run svelte-check
|
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
- name: "Lint"
|
- name: "Lint"
|
||||||
run: yarn run lint
|
run: yarn run lint
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
- name: "Pretty"
|
|
||||||
run: yarn run pretty-check
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: "Jasmine"
|
- name: "Jasmine"
|
||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
continuous-integration-pusher:
|
|
||||||
name: "Continuous Integration Pusher"
|
|
||||||
|
|
||||||
runs-on: "ubuntu-latest"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Checkout"
|
|
||||||
uses: "actions/checkout@v2.0.0"
|
|
||||||
|
|
||||||
- name: "Setup NodeJS"
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: '14.x'
|
|
||||||
|
|
||||||
- name: Install Protoc
|
|
||||||
uses: arduino/setup-protoc@v1
|
|
||||||
with:
|
|
||||||
version: '3.x'
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
|
||||||
run: yarn install
|
|
||||||
working-directory: "pusher"
|
|
||||||
|
|
||||||
- name: "Install messages dependencies"
|
|
||||||
run: yarn install
|
|
||||||
working-directory: "messages"
|
|
||||||
|
|
||||||
- name: "Build proto messages"
|
|
||||||
run: yarn run proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher
|
|
||||||
working-directory: "messages"
|
|
||||||
|
|
||||||
- name: "Build"
|
|
||||||
run: yarn run tsc
|
|
||||||
working-directory: "pusher"
|
|
||||||
|
|
||||||
- name: "Lint"
|
|
||||||
run: yarn run lint
|
|
||||||
working-directory: "pusher"
|
|
||||||
|
|
||||||
- name: "Jasmine"
|
|
||||||
run: yarn test
|
|
||||||
working-directory: "pusher"
|
|
||||||
|
|
||||||
- name: "Prettier"
|
|
||||||
run: yarn run pretty-check
|
|
||||||
working-directory: "pusher"
|
|
||||||
|
|
||||||
continuous-integration-back:
|
continuous-integration-back:
|
||||||
name: "Continuous Integration Back"
|
name: "Continuous Integration Back"
|
||||||
|
|
||||||
|
@ -162,7 +96,3 @@ jobs:
|
||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
||||||
- name: "Prettier"
|
|
||||||
run: yarn run pretty-check
|
|
||||||
working-directory: "back"
|
|
||||||
|
|
||||||
|
|
126
.github/workflows/end_to_end_tests.yml
vendored
126
.github/workflows/end_to_end_tests.yml
vendored
|
@ -1,126 +0,0 @@
|
||||||
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
|
|
||||||
|
|
||||||
name: "End to end tests"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
start-runner:
|
|
||||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|
|
||||||
name: Start self-hosted EC2 runner
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
label: ${{ steps.start-ec2-runner.outputs.label }}
|
|
||||||
ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }}
|
|
||||||
steps:
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
|
||||||
- name: Start EC2 runner
|
|
||||||
id: start-ec2-runner
|
|
||||||
uses: machulav/ec2-github-runner@v2
|
|
||||||
with:
|
|
||||||
mode: start
|
|
||||||
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
|
|
||||||
ec2-image-id: ami-094dbcc53250a2480
|
|
||||||
ec2-instance-type: m5.2xlarge
|
|
||||||
subnet-id: subnet-0ac40025f559df1bc
|
|
||||||
security-group-id: sg-0e36e96e3b8ed2d64
|
|
||||||
#iam-role-name: my-role-name # optional, requires additional permissions
|
|
||||||
#aws-resource-tags: > # optional, requires additional permissions
|
|
||||||
# [
|
|
||||||
# {"Key": "Name", "Value": "ec2-github-runner"},
|
|
||||||
# {"Key": "GitHubRepository", "Value": "${{ github.repository }}"}
|
|
||||||
# ]
|
|
||||||
|
|
||||||
|
|
||||||
end-to-end-tests:
|
|
||||||
name: "End-to-end testcafe tests"
|
|
||||||
|
|
||||||
needs: start-runner # required to start the main job when the runner is ready
|
|
||||||
runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Checkout"
|
|
||||||
uses: "actions/checkout@v2.0.0"
|
|
||||||
|
|
||||||
- name: "Setup NodeJS"
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: '14.x'
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
|
||||||
run: npm install
|
|
||||||
working-directory: "tests"
|
|
||||||
|
|
||||||
- name: "Setup .env file"
|
|
||||||
run: cp .env.template .env
|
|
||||||
|
|
||||||
- name: "Edit ownership of file for test cases"
|
|
||||||
run: sudo chown 1000:1000 -R .
|
|
||||||
|
|
||||||
- name: "Start environment"
|
|
||||||
run: LIVE_RELOAD=0 docker-compose up -d
|
|
||||||
|
|
||||||
- name: "Wait for environment to build (and downloading testcafe image)"
|
|
||||||
run: (docker-compose -f docker-compose.testcafe.yml build &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
|
|
||||||
|
|
||||||
# - name: "temp debug: display logs"
|
|
||||||
# run: docker-compose logs
|
|
||||||
#
|
|
||||||
# - name: "Wait for back start"
|
|
||||||
# run: docker-compose logs -f back | grep -q "WorkAdventure HTTP API starting on port"
|
|
||||||
#
|
|
||||||
# - name: "Wait for pusher start"
|
|
||||||
# run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port"
|
|
||||||
|
|
||||||
- name: "Run tests"
|
|
||||||
run: PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
|
|
||||||
|
|
||||||
- name: Upload failed tests
|
|
||||||
if: ${{ failure() }}
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: my-artifact
|
|
||||||
path: './tests/screenshots/'
|
|
||||||
|
|
||||||
- name: Display state
|
|
||||||
if: ${{ failure() }}
|
|
||||||
run: docker-compose ps
|
|
||||||
|
|
||||||
- name: Display logs
|
|
||||||
if: ${{ failure() }}
|
|
||||||
run: docker-compose logs
|
|
||||||
|
|
||||||
stop-runner:
|
|
||||||
name: Stop self-hosted EC2 runner
|
|
||||||
needs:
|
|
||||||
- start-runner # required to get output from the start-runner job
|
|
||||||
- end-to-end-tests # required to wait when the main job is done
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs
|
|
||||||
steps:
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
|
||||||
- name: Stop EC2 runner
|
|
||||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|
|
||||||
uses: machulav/ec2-github-runner@v2
|
|
||||||
with:
|
|
||||||
mode: stop
|
|
||||||
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
|
|
||||||
label: ${{ needs.start-runner.outputs.label }}
|
|
||||||
ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }}
|
|
73
.github/workflows/push-to-npm.yml
vendored
73
.github/workflows/push-to-npm.yml
vendored
|
@ -1,73 +0,0 @@
|
||||||
name: Push @workadventure/iframe-api-typings to NPM
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
push:
|
|
||||||
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: 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 ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
|
|
||||||
working-directory: "messages"
|
|
||||||
|
|
||||||
- name: "Create index.html"
|
|
||||||
run: ./templater.sh
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: "Generate i18n files"
|
|
||||||
run: yarn run typesafe-i18n
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: "Build"
|
|
||||||
run: yarn run build-typings
|
|
||||||
env:
|
|
||||||
PUSHER_URL: "//localhost:8080"
|
|
||||||
ADMIN_URL: "//localhost:80"
|
|
||||||
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: Copy typings to package dir (2)
|
|
||||||
run: cp -R front/dist/src/Api front/packages/iframe-api-typings/Api
|
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -6,6 +6,4 @@ docker-compose.override.yaml
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
maps/yarn.lock
|
maps/yarn.lock
|
||||||
maps/dist/computer.js
|
maps/dist/computer.js
|
||||||
maps/dist/computer.js.map
|
maps/dist/computer.js.map
|
||||||
node_modules
|
|
||||||
_
|
|
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
_
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
(
|
|
||||||
cd messages || exit
|
|
||||||
yarn run precommit
|
|
||||||
)
|
|
||||||
(
|
|
||||||
cd front || exit
|
|
||||||
yarn run precommit
|
|
||||||
)
|
|
||||||
(
|
|
||||||
cd pusher || exit
|
|
||||||
yarn run precommit
|
|
||||||
)
|
|
||||||
(
|
|
||||||
cd back || exit
|
|
||||||
yarn run precommit
|
|
||||||
)
|
|
166
CHANGELOG.md
166
CHANGELOG.md
|
@ -1,166 +0,0 @@
|
||||||
## Version develop
|
|
||||||
|
|
||||||
### Updates
|
|
||||||
- Added multi Co-Website management
|
|
||||||
|
|
||||||
### Bugfix
|
|
||||||
- Moving a discussion over a user will now add this user to the discussion
|
|
||||||
- Being in a silent zone new forces mediaConstraints to false (#1508)
|
|
||||||
- Fixes for the emote menu (#1501)
|
|
||||||
- Fixing chat message attributed to wrong user (#1507 #1528)
|
|
||||||
|
|
||||||
## Version 1.5.0
|
|
||||||
### Updates
|
|
||||||
- Added support for login with OpenID Connect
|
|
||||||
- New scripting library available to extend WorkAdventure: see [Scripting API Extra](https://github.com/workadventure/scripting-api-extra/)
|
|
||||||
- New menu design!
|
|
||||||
- New `openTab` property (#1419)
|
|
||||||
- Possible integration with Posthog (#1458)
|
|
||||||
|
|
||||||
### Bugfix
|
|
||||||
- Fixing layers flattened several times (#1427 @Lurkars)
|
|
||||||
- Fixing CSS of video elements
|
|
||||||
- Chat now scrolls to bottom when opened (#1450)
|
|
||||||
- Fixing silent zone not respected when exiting from Jitsi (#1456)
|
|
||||||
- Fixing "yarn install" failing because of missing rights on some Docker installs (#1457)
|
|
||||||
- Fixing audio not shut down when exiting a room (#1459)
|
|
||||||
|
|
||||||
### Misc
|
|
||||||
- Finished migrating "Build your map" documentation into the "/docs" directory of this repository (#1417 #1385)
|
|
||||||
- Refactoring documentation (dedicated page for variables) (#1414)
|
|
||||||
- Front container code is now completely linted (#1413)
|
|
||||||
|
|
||||||
## Version 1.4.15
|
|
||||||
|
|
||||||
### Updates
|
|
||||||
- New scripting API features :
|
|
||||||
- Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu` to add a custom menu or an iframe to the menu.
|
|
||||||
- New `jitsiWidth` parameter to set the width of Jitsi and Cowebsite (#1398 @tabascoeye)
|
|
||||||
- Refactored the way videos are displayed to better cope for vertical videos (on mobile)
|
|
||||||
- Fixing reconnection issues after 5 minutes of an inactive tab on Google Chrome
|
|
||||||
- Changes performed in `WA.room.setPropertyLayer` now have a real-time impact (#1395)
|
|
||||||
|
|
||||||
### Bugfixes
|
|
||||||
- Fixing streams in bubbles sometimes improperly muted when there are more than 2 people in the bubble (#1400 #1402)
|
|
||||||
- Properly displaying carriage returns in popups (#1388)
|
|
||||||
- `WA.state` now answers correctly to "in" keyword (#1393)
|
|
||||||
- Variables can now be nested in group layers (#1406)
|
|
||||||
|
|
||||||
## Version 1.4.14
|
|
||||||
|
|
||||||
### Updates
|
|
||||||
- New scripting API features :
|
|
||||||
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.
|
|
||||||
- Rewrote the way authentification works: the auth jwt token can now contains an email instead of an uuid
|
|
||||||
- Added an OpenId login flow than can be plugged to any OIDC provider.
|
|
||||||
- You can send a message to all rooms of your world from the console global message (user with tag admin only).
|
|
||||||
|
|
||||||
## Version 1.4.11
|
|
||||||
|
|
||||||
### 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.onInit(): Promise<void>` to wait for scripting API initialization
|
|
||||||
- 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, delete or change existing property of a layer
|
|
||||||
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
|
||||||
- Use `WA.player.id: string|undefined` to get the ID of the current player
|
|
||||||
- Use `WA.player.name: string` to get the name of the current player
|
|
||||||
- Use `WA.player.tags: string[]` to get the tags of the current player
|
|
||||||
- Use `WA.room.id: string` to get the ID of the room
|
|
||||||
- Use `WA.room.mapURL: string` to get the URL of the map
|
|
||||||
- Use `WA.room.mapURL: string` to get the URL of the map
|
|
||||||
- Use `WA.room.getMap(): Promise<ITiledMap>` to get the JSON map file
|
|
||||||
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
|
||||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
|
||||||
- Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable
|
|
||||||
- Use `WA.state.saveVariable(key: string, value: unknown): Promise<void>` to set a variable (across the room, for all users)
|
|
||||||
- Use `WA.state.onVariableChange(key: string): Observable<unknown>` to track a variable
|
|
||||||
- Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`)
|
|
||||||
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
|
||||||
- The text chat was redesigned to be prettier and to use more features :
|
|
||||||
- The chat is now persistent between discussions and always accessible
|
|
||||||
- The chat now tracks incoming and outcoming users in your conversation
|
|
||||||
- The chat allows your to see the visit card of users
|
|
||||||
- You can close the chat window with the escape key
|
|
||||||
- Added a 'Enable notifications' button in the menu.
|
|
||||||
- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed:
|
|
||||||
- `/api/map`: now accepts a complete room URL instead of organization/world/room slugs
|
|
||||||
- `/api/ban`: new endpoint to report users
|
|
||||||
- as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing
|
|
||||||
|
|
||||||
## Version 1.4.3 - 1.4.4 - 1.4.5
|
|
||||||
|
|
||||||
## Bugfixes
|
|
||||||
|
|
||||||
- Fixing the generation of @workadventure/iframe-api-typings
|
|
||||||
|
|
||||||
## Version 1.4.2
|
|
||||||
|
|
||||||
## Updates
|
|
||||||
|
|
||||||
- A script in an iframe opened by another script can use the IFrame API.
|
|
||||||
|
|
||||||
## 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
|
|
113
CONTRIBUTING.md
113
CONTRIBUTING.md
|
@ -1,113 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
## Contributing external resources
|
|
||||||
|
|
||||||
You can share your work on maps / articles / videos related to WorkAdventure on our [awesome-workadventure](https://github.com/workadventure/awesome-workadventure) list.
|
|
||||||
|
|
||||||
## Developer documentation
|
|
||||||
|
|
||||||
Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/)
|
|
||||||
|
|
||||||
## 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 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), or an end-to-end test (we use Testcafe).
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference
|
|
||||||
to your newly created test map
|
|
||||||
* if the features can be automatically tested, please provide a testcafe test
|
|
||||||
|
|
||||||
#### Running testcafe tests
|
|
||||||
|
|
||||||
End-to-end tests are available in the "/tests" directory.
|
|
||||||
|
|
||||||
To run these tests locally:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ LIVE_RELOAD=0 docker-compose up -d
|
|
||||||
$ cd tests
|
|
||||||
$ npm install
|
|
||||||
$ npm run test
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: If your tests fail on a Javascript error in "sockjs", this is due to the
|
|
||||||
Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting
|
|
||||||
WorkAdventure with the `LIVE_RELOAD=0` environment variable.
|
|
||||||
|
|
||||||
End-to-end tests can take a while to run. To run only one test, use:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ npm run test -- tests/[name of the test file].ts
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also run the tests inside a container (but you will not have visual feedbacks on your test, so we recommend using
|
|
||||||
the local tests).
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ LIVE_RELOAD=0 docker-compose up -d
|
|
||||||
# Wait 2-3 minutes for the environment to start, then:
|
|
||||||
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up
|
|
||||||
```
|
|
||||||
|
|
||||||
### A bad wording or a missing language
|
|
||||||
|
|
||||||
If you notice a translation error or missing language you can help us by following the [how to translate](docs/dev/how-to-translate.md) documentation.
|
|
BIN
README-INTRO.jpg
Normal file
BIN
README-INTRO.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 386 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 16 KiB |
BIN
README-MAP.png
BIN
README-MAP.png
Binary file not shown.
Before Width: | Height: | Size: 60 KiB |
48
README.md
48
README.md
|
@ -1,49 +1,51 @@
|
||||||
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/YGtngdh9gt)
|
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg)
|
||||||
|
|
||||||
![WorkAdventure logo](README-LOGO.svg)
|
![WorkAdventure landscape image](README-INTRO.jpg)
|
||||||
![WorkAdventure office image](README-MAP.png)
|
|
||||||
|
|
||||||
Live demo [here](https://play.workadventu.re/@/tcm/workadventure/wa-village).
|
Demo here : [https://workadventu.re/](https://workadventu.re/).
|
||||||
|
|
||||||
# WorkAdventure
|
# Work Adventure
|
||||||
|
|
||||||
WorkAdventure is a web-based collaborative workspace presented in the form of a
|
## Work in progress
|
||||||
|
|
||||||
|
Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
|
||||||
16-bit video game.
|
16-bit video game.
|
||||||
|
|
||||||
In WorkAdventure you can move around your office and talk to your colleagues (using a video-chat system, triggered when you approach someone).
|
In Work Adventure, you can move around your office and talk to your colleagues (using a video-chat feature that is
|
||||||
|
triggered when you move next to a colleague).
|
||||||
|
|
||||||
See more features for your virtual office: https://workadventu.re/virtual-office
|
|
||||||
|
|
||||||
## Community resources
|
## Getting started
|
||||||
|
|
||||||
Check out resources developed by the WorkAdventure community at [awesome-workadventure](https://github.com/workadventure/awesome-workadventure)
|
Install Docker : https://docs.docker.com/get-docker/
|
||||||
|
Install docker-compose : https://docs.docker.com/compose/install/
|
||||||
|
|
||||||
## Setting up a development environment
|
**Add your local environment variable**
|
||||||
|
Run:
|
||||||
|
|
||||||
Install Docker.
|
```
|
||||||
|
cp .en.template .env
|
||||||
|
```
|
||||||
|
_``If you want deploy on the dev server, you must update HOST_NAME in your `.env` by your private domain (workadventure.localhost => yourdomain.com).``
|
||||||
|
**_``Don't forgot to add A entry in DNS like this *.yourdomain.com. The different deployed for WorkAdventure are: play. ; pusher. ; maps. ; api. ; uploader. ;``_**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```
|
```
|
||||||
cp .env.template .env
|
docker-compose up
|
||||||
docker-compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The environment will start.
|
The environment will start.
|
||||||
|
|
||||||
You should now be able to browse to http://play.workadventure.localhost/ and see the application.
|
You should now be able to browse to http://workadventure.localhost/ and see the application.
|
||||||
You can view the dashboard at http://workadventure.localhost:8080/
|
|
||||||
|
|
||||||
Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
||||||
|
|
||||||
**/etc/hosts**
|
**/etc/hosts**
|
||||||
```
|
```
|
||||||
127.0.0.1 workadventure.localhost
|
workadventure.localhost 127.0.0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: If on the first run you get a page with "network error". Try to ``docker-compose stop`` , then ``docker-compose start``.
|
|
||||||
Note 2: If you are still getting "network error". Make sure you are authorizing the self-signed certificate by entering https://pusher.workadventure.localhost and accepting them.
|
|
||||||
|
|
||||||
### MacOS developers, your environment with Vagrant
|
### MacOS developers, your environment with Vagrant
|
||||||
|
|
||||||
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).
|
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).
|
||||||
|
@ -109,7 +111,5 @@ Vagrant destroy
|
||||||
* `Vagrant halt`: stop your VM Vagrant.
|
* `Vagrant halt`: stop your VM Vagrant.
|
||||||
* `Vagrant destroy`: delete your VM Vagrant.
|
* `Vagrant destroy`: delete your VM Vagrant.
|
||||||
|
|
||||||
## Setting up a production environment
|
## Features developed
|
||||||
|
You have more details of features developed in back [README.md](./back/README.md).
|
||||||
The way you set up your production environment will highly depend on your servers.
|
|
||||||
We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory.
|
|
||||||
|
|
20
SECURITY.md
20
SECURITY.md
|
@ -1,20 +0,0 @@
|
||||||
# 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.
|
|
|
@ -25,7 +25,6 @@
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error"
|
||||||
"no-throw-literal": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
src/Messages/generated
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 120,
|
|
||||||
"tabWidth": 4
|
|
||||||
}
|
|
|
@ -1,26 +1,16 @@
|
||||||
# protobuf build
|
FROM thecodingmachine/workadventure-back-base:latest as builder
|
||||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
|
WORKDIR /var/www/messages
|
||||||
WORKDIR /usr/src
|
COPY --chown=docker:docker messages .
|
||||||
COPY messages .
|
|
||||||
RUN yarn install && yarn proto
|
RUN yarn install && yarn proto
|
||||||
|
|
||||||
# typescript build
|
FROM thecodingmachine/nodejs:12
|
||||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2
|
|
||||||
WORKDIR /usr/src
|
COPY --chown=docker:docker back .
|
||||||
COPY back/yarn.lock back/package.json ./
|
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
COPY back .
|
|
||||||
COPY --from=builder /usr/src/generated src/Messages/generated
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN yarn run tsc
|
RUN yarn run tsc
|
||||||
|
|
||||||
# final production image
|
|
||||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d
|
|
||||||
WORKDIR /usr/src
|
|
||||||
COPY back/yarn.lock back/package.json ./
|
|
||||||
COPY --from=builder2 /usr/src/dist /usr/src/dist
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
RUN yarn install --production
|
|
||||||
|
|
||||||
USER node
|
|
||||||
CMD ["yarn", "run", "runprod"]
|
CMD ["yarn", "run", "runprod"]
|
||||||
|
|
||||||
|
|
61
back/README.md
Normal file
61
back/README.md
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# Back Features
|
||||||
|
|
||||||
|
## Login
|
||||||
|
To start your game, you must authenticate on the server back.
|
||||||
|
When you are authenticated, the back server return token and room starting.
|
||||||
|
```
|
||||||
|
POST => /login
|
||||||
|
Params :
|
||||||
|
email: email of user.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Join a room
|
||||||
|
When a user is connected, the user can join a room.
|
||||||
|
So you must send emit `join-room` with information user:
|
||||||
|
```
|
||||||
|
Socket.io => 'join-room'
|
||||||
|
|
||||||
|
userId: user id of gamer
|
||||||
|
roomId: room id when user enter in game
|
||||||
|
position: {
|
||||||
|
x: position x on map
|
||||||
|
y: position y on map
|
||||||
|
}
|
||||||
|
```
|
||||||
|
All data users are stocked on socket client.
|
||||||
|
|
||||||
|
## Send position user
|
||||||
|
When user move on the map, you can share new position on back with event `user-position`.
|
||||||
|
The information sent:
|
||||||
|
```
|
||||||
|
Socket.io => 'user-position'
|
||||||
|
|
||||||
|
userId: user id of gamer
|
||||||
|
roomId: room id when user enter in game
|
||||||
|
position: {
|
||||||
|
x: position x on map
|
||||||
|
y: position y on map
|
||||||
|
}
|
||||||
|
```
|
||||||
|
All data users are updated on socket client.
|
||||||
|
|
||||||
|
## Receive positions of all users
|
||||||
|
The application sends position of all users in each room in every few 10 milliseconds.
|
||||||
|
The data will pushed on event `user-position`:
|
||||||
|
```
|
||||||
|
Socket.io => 'user-position'
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
userId: user id of gamer
|
||||||
|
roomId: room id when user enter in game
|
||||||
|
position: {
|
||||||
|
x: position x on map
|
||||||
|
y: position y on map
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
[<<< back](../README.md)
|
|
@ -10,11 +10,8 @@
|
||||||
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
||||||
"profile": "tsc && node --prof ./dist/server.js",
|
"profile": "tsc && node --prof ./dist/server.js",
|
||||||
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||||
"lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
|
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
||||||
"fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts",
|
"fix": "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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -40,20 +37,24 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@workadventure/tiled-map-type-guard": "^1.0.3",
|
"axios": "^0.20.0",
|
||||||
"axios": "^0.21.2",
|
"body-parser": "^1.19.0",
|
||||||
"busboy": "^0.3.1",
|
"busboy": "^0.3.1",
|
||||||
"circular-json": "^0.5.9",
|
"circular-json": "^0.5.9",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"generic-type-guard": "^3.2.0",
|
"generic-type-guard": "^3.2.0",
|
||||||
"google-protobuf": "^3.13.0",
|
"google-protobuf": "^3.13.0",
|
||||||
"grpc": "^1.24.4",
|
"grpc": "^1.24.4",
|
||||||
"ipaddr.js": "^2.0.1",
|
"http-status-codes": "^1.4.0",
|
||||||
|
"iterall": "^1.3.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
|
"multer": "^1.4.2",
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"query-string": "^6.13.3",
|
"query-string": "^6.13.3",
|
||||||
"redis": "^3.1.2",
|
"systeminformation": "^4.30.5",
|
||||||
|
"ts-node-dev": "^1.0.0-pre.44",
|
||||||
|
"typescript": "^3.8.3",
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||||
"uuidv4": "^6.0.7"
|
"uuidv4": "^6.0.7"
|
||||||
},
|
},
|
||||||
|
@ -66,20 +67,10 @@
|
||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.10",
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
"@types/mkdirp": "^1.0.1",
|
"@types/mkdirp": "^1.0.1",
|
||||||
"@types/redis": "^2.8.31",
|
|
||||||
"@types/uuidv4": "^5.0.0",
|
"@types/uuidv4": "^5.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@typescript-eslint/parser": "^5.8.0",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
"eslint": "^8.5.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.1.8",
|
|
||||||
"typescript": "^4.5.4"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.ts": [
|
|
||||||
"prettier --write"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
// lib/server.ts
|
// lib/server.ts
|
||||||
import App from "./src/App";
|
import App from "./src/App";
|
||||||
import grpc from "grpc";
|
import grpc from "grpc";
|
||||||
import { roomManager } from "./src/RoomManager";
|
import {roomManager} from "./src/RoomManager";
|
||||||
import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb";
|
import {IRoomManagerServer, RoomManagerService} from "./src/Messages/generated/messages_grpc_pb";
|
||||||
import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable";
|
import {HTTP_PORT, GRPC_PORT} from "./src/Enum/EnvironmentVariable";
|
||||||
|
|
||||||
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT));
|
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT))
|
||||||
|
|
||||||
const server = new grpc.Server();
|
const server = new grpc.Server();
|
||||||
server.addService<IRoomManagerServer>(RoomManagerService, roomManager);
|
server.addService<IRoomManagerServer>(RoomManagerService, roomManager);
|
||||||
|
|
||||||
server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure());
|
server.bind('0.0.0.0:'+GRPC_PORT, grpc.ServerCredentials.createInsecure());
|
||||||
server.start();
|
server.start();
|
||||||
console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT);
|
console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// lib/app.ts
|
// lib/app.ts
|
||||||
import { PrometheusController } from "./Controller/PrometheusController";
|
import {PrometheusController} from "./Controller/PrometheusController";
|
||||||
import { DebugController } from "./Controller/DebugController";
|
import {DebugController} from "./Controller/DebugController";
|
||||||
import { App as uwsApp } from "./Server/sifrr.server";
|
import {App as uwsApp} from "./Server/sifrr.server";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: uwsApp;
|
public app: uwsApp;
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { HttpResponse } from "uWebSockets.js";
|
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||||
|
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
|
||||||
export class BaseController {
|
export class BaseController {
|
||||||
protected addCorsHeaders(res: HttpResponse): void {
|
protected addCorsHeaders(res: HttpResponse): void {
|
||||||
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
|
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-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
||||||
res.writeHeader("access-control-allow-origin", "*");
|
res.writeHeader('access-control-allow-origin', '*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +1,54 @@
|
||||||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||||
import { stringify } from "circular-json";
|
import {stringify} from "circular-json";
|
||||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||||
import { parse } from "query-string";
|
import { parse } from 'query-string';
|
||||||
import { App } from "../Server/sifrr.server";
|
import {App} from "../Server/sifrr.server";
|
||||||
import { socketManager } from "../Services/SocketManager";
|
import {socketManager} from "../Services/SocketManager";
|
||||||
|
import {ServerWritableStream} from "grpc";
|
||||||
|
|
||||||
export class DebugController {
|
export class DebugController {
|
||||||
constructor(private App: App) {
|
constructor(private App : App) {
|
||||||
this.getDump();
|
this.getDump();
|
||||||
}
|
}
|
||||||
|
|
||||||
getDump() {
|
|
||||||
|
getDump(){
|
||||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
||||||
(async () => {
|
const query = parse(req.getQuery());
|
||||||
const query = parse(req.getQuery());
|
|
||||||
|
|
||||||
if (ADMIN_API_TOKEN === "") {
|
if (query.token !== ADMIN_API_TOKEN) {
|
||||||
return res.writeStatus("401 Unauthorized").end("No token configured!");
|
return res.status(401).send('Invalid token sent!');
|
||||||
}
|
}
|
||||||
if (query.token !== ADMIN_API_TOKEN) {
|
|
||||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
|
||||||
.writeStatus("200 OK")
|
socketManager.getWorlds(),
|
||||||
.writeHeader("Content-Type", "application/json")
|
(key: unknown, value: unknown) => {
|
||||||
.end(
|
if (key === 'listeners') {
|
||||||
stringify(
|
return 'Listeners';
|
||||||
await Promise.all(socketManager.getWorlds().values()),
|
}
|
||||||
(key: unknown, value: unknown) => {
|
if (key === 'socket') {
|
||||||
if (key === "listeners") {
|
return 'Socket';
|
||||||
return "Listeners";
|
}
|
||||||
}
|
if (key === 'batchedMessages') {
|
||||||
if (key === "socket") {
|
return 'BatchedMessages';
|
||||||
return "Socket";
|
}
|
||||||
}
|
if(value instanceof Map) {
|
||||||
if (key === "batchedMessages") {
|
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
return "BatchedMessages";
|
for (const [mapKey, mapValue] of value.entries()) {
|
||||||
}
|
obj[mapKey] = mapValue;
|
||||||
if (value instanceof Map) {
|
}
|
||||||
const obj: { [key: string | number]: unknown } = {};
|
return obj;
|
||||||
for (const [mapKey, mapValue] of value.entries()) {
|
} else if(value instanceof Set) {
|
||||||
if (typeof mapKey === "number" || typeof mapKey === "string") {
|
const obj: Array<unknown> = [];
|
||||||
obj[mapKey] = mapValue;
|
for (const [setKey, setValue] of value.entries()) {
|
||||||
}
|
obj.push(setValue);
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
} else if (value instanceof Set) {
|
|
||||||
const obj: Array<unknown> = [];
|
|
||||||
for (const [setKey, setValue] of value.entries()) {
|
|
||||||
obj.push(setValue);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
return obj;
|
||||||
);
|
} else {
|
||||||
})().catch((e) => {
|
return value;
|
||||||
console.error(e);
|
}
|
||||||
res.writeStatus("500");
|
}
|
||||||
res.end("An error occurred");
|
));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
import { App } from "../Server/sifrr.server";
|
import {App} from "../Server/sifrr.server";
|
||||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||||
import { register, collectDefaultMetrics } from "prom-client";
|
const register = require('prom-client').register;
|
||||||
|
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
||||||
|
|
||||||
export class PrometheusController {
|
export class PrometheusController {
|
||||||
constructor(private App: App) {
|
constructor(private App: App) {
|
||||||
collectDefaultMetrics({
|
collectDefaultMetrics({
|
||||||
|
timeout: 10000,
|
||||||
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
||||||
});
|
});
|
||||||
|
|
||||||
this.App.get("/metrics", this.metrics.bind(this));
|
this.App.get("/metrics", this.metrics.bind(this));
|
||||||
this.App.get("/metrics.json", this.metricsAsJSON.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||||
res.writeHeader("Content-Type", register.contentType);
|
res.writeHeader('Content-Type', register.contentType);
|
||||||
res.end(register.metrics());
|
res.end(register.metrics());
|
||||||
}
|
}
|
||||||
private metricsAsJSON(res: HttpResponse, req: HttpRequest): void {
|
|
||||||
res.writeHeader('Content-Type', 'application/json');
|
|
||||||
res.end(JSON.stringify(register.getMetricsAsJSON()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,32 @@
|
||||||
|
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
||||||
|
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
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 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 ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
|
||||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
|
||||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "";
|
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
|
||||||
|
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600;
|
||||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
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_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
||||||
const JITSI_ISS = process.env.JITSI_ISS || "";
|
const JITSI_ISS = process.env.JITSI_ISS || '';
|
||||||
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
|
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
|
||||||
const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080;
|
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080;
|
||||||
const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
|
const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051;
|
||||||
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
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 MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
|
||||||
export const REDIS_HOST = process.env.REDIS_HOST || undefined;
|
|
||||||
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
|
|
||||||
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
|
|
||||||
export const STORE_VARIABLES_FOR_LOCAL_MAPS = process.env.STORE_VARIABLES_FOR_LOCAL_MAPS === "true";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
SECRET_KEY,
|
||||||
|
URL_ROOM_STARTED,
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
ADMIN_API_URL,
|
ADMIN_API_URL,
|
||||||
ADMIN_API_TOKEN,
|
ADMIN_API_TOKEN,
|
||||||
HTTP_PORT,
|
HTTP_PORT,
|
||||||
GRPC_PORT,
|
GRPC_PORT,
|
||||||
|
MAX_USERS_PER_ROOM,
|
||||||
GROUP_RADIUS,
|
GROUP_RADIUS,
|
||||||
ALLOW_ARTILLERY,
|
ALLOW_ARTILLERY,
|
||||||
CPU_OVERHEAT_THRESHOLD,
|
CPU_OVERHEAT_THRESHOLD,
|
||||||
JITSI_URL,
|
JITSI_URL,
|
||||||
JITSI_ISS,
|
JITSI_ISS,
|
||||||
SECRET_JITSI_KEY,
|
SECRET_JITSI_KEY
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,33 +1,36 @@
|
||||||
|
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 {
|
import {
|
||||||
|
BatchMessage,
|
||||||
|
PusherToBackMessage,
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
UserJoinedRoomMessage,
|
ServerToClientMessage,
|
||||||
UserLeftRoomMessage,
|
SubMessage
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { AdminSocket } from "../RoomManager";
|
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
|
||||||
|
import {AdminSocket} from "../RoomManager";
|
||||||
|
|
||||||
|
|
||||||
export class Admin {
|
export class Admin {
|
||||||
public constructor(private readonly socket: AdminSocket) {}
|
public constructor(
|
||||||
|
private readonly socket: AdminSocket
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public sendUserJoin(uuid: string, name: string, ip: string): void {
|
public sendUserJoin(uuid: string): void {
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
||||||
|
serverToAdminClientMessage.setUseruuidjoinedroom(uuid);
|
||||||
const userJoinedRoomMessage = new UserJoinedRoomMessage();
|
|
||||||
userJoinedRoomMessage.setUuid(uuid);
|
|
||||||
userJoinedRoomMessage.setName(name);
|
|
||||||
userJoinedRoomMessage.setIpaddress(ip);
|
|
||||||
|
|
||||||
serverToAdminClientMessage.setUserjoinedroom(userJoinedRoomMessage);
|
|
||||||
|
|
||||||
this.socket.write(serverToAdminClientMessage);
|
this.socket.write(serverToAdminClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendUserLeft(uuid: string /*, name: string, ip: string*/): void {
|
public sendUserLeft(uuid: string): void {
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
||||||
|
serverToAdminClientMessage.setUseruuidleftroom(uuid);
|
||||||
const userLeftRoomMessage = new UserLeftRoomMessage();
|
|
||||||
userLeftRoomMessage.setUuid(uuid);
|
|
||||||
|
|
||||||
serverToAdminClientMessage.setUserleftroom(userLeftRoomMessage);
|
|
||||||
|
|
||||||
this.socket.write(serverToAdminClientMessage);
|
this.socket.write(serverToAdminClientMessage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,186 +1,131 @@
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
import {PointInterface} from "./Websocket/PointInterface";
|
||||||
import { Group } from "./Group";
|
import {Group} from "./Group";
|
||||||
import { User, UserSocket } from "./User";
|
import {User, UserSocket} from "./User";
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
import {
|
import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
|
||||||
EmoteCallback,
|
import {PositionNotifier} from "./PositionNotifier";
|
||||||
EntersCallback,
|
import {Movable} from "_Model/Movable";
|
||||||
LeavesCallback,
|
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
|
||||||
MovesCallback,
|
import {arrayIntersect} from "../Services/ArrayHelper";
|
||||||
PlayerDetailsUpdatedCallback,
|
import {MAX_USERS_PER_ROOM} from "../Enum/EnvironmentVariable";
|
||||||
} from "_Model/Zone";
|
import {JoinRoomMessage} from "../Messages/generated/messages_pb";
|
||||||
import { PositionNotifier } from "./PositionNotifier";
|
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
||||||
import { Movable } from "_Model/Movable";
|
import {ZoneSocket} from "src/RoomManager";
|
||||||
import {
|
import {Admin} from "../Model/Admin";
|
||||||
BatchToPusherMessage,
|
|
||||||
BatchToPusherRoomMessage,
|
|
||||||
EmoteEventMessage,
|
|
||||||
ErrorMessage,
|
|
||||||
JoinRoomMessage,
|
|
||||||
SetPlayerDetailsMessage,
|
|
||||||
SubToPusherRoomMessage,
|
|
||||||
VariableMessage,
|
|
||||||
VariableWithTagMessage,
|
|
||||||
ServerToClientMessage,
|
|
||||||
} from "../Messages/generated/messages_pb";
|
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
|
||||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
|
||||||
import { Admin } from "../Model/Admin";
|
|
||||||
import { adminApi } from "../Services/AdminApi";
|
|
||||||
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
|
||||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
|
||||||
import { mapFetcher } from "../Services/MapFetcher";
|
|
||||||
import { VariablesManager } from "../Services/VariablesManager";
|
|
||||||
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
|
||||||
import { LocalUrlError } from "../Services/LocalUrlError";
|
|
||||||
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
|
||||||
import { VariableError } from "../Services/VariableError";
|
|
||||||
import { isRoomRedirect } from "../Services/AdminApi/RoomRedirect";
|
|
||||||
|
|
||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
|
|
||||||
export class GameRoom {
|
export enum GameRoomPolicyTypes {
|
||||||
// Users, sorted by ID
|
ANONYMOUS_POLICY = 1,
|
||||||
private readonly users = new Map<number, User>();
|
MEMBERS_ONLY_POLICY,
|
||||||
private readonly usersByUuid = new Map<string, User>();
|
USE_TAGS_POLICY,
|
||||||
private readonly groups = new Set<Group>();
|
}
|
||||||
private readonly admins = new Set<Admin>();
|
|
||||||
|
|
||||||
private itemsState = new Map<number, unknown>();
|
export class GameRoom {
|
||||||
|
private readonly minDistance: number;
|
||||||
|
private readonly groupRadius: number;
|
||||||
|
|
||||||
|
// Users, sorted by ID
|
||||||
|
private readonly users: Map<number, User>;
|
||||||
|
private readonly usersByUuid: Map<string, User>;
|
||||||
|
private readonly groups: Set<Group>;
|
||||||
|
private readonly admins: Set<Admin>;
|
||||||
|
|
||||||
|
private readonly connectCallback: ConnectCallback;
|
||||||
|
private readonly disconnectCallback: DisconnectCallback;
|
||||||
|
|
||||||
|
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||||
|
|
||||||
private readonly positionNotifier: PositionNotifier;
|
private readonly positionNotifier: PositionNotifier;
|
||||||
private versionNumber: number = 1;
|
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 = '';
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
|
|
||||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
constructor(roomId: string,
|
||||||
|
connectCallback: ConnectCallback,
|
||||||
|
disconnectCallback: DisconnectCallback,
|
||||||
|
minDistance: number,
|
||||||
|
groupRadius: number,
|
||||||
|
onEnters: EntersCallback,
|
||||||
|
onMoves: MovesCallback,
|
||||||
|
onLeaves: LeavesCallback)
|
||||||
|
{
|
||||||
|
this.roomId = roomId;
|
||||||
|
this.anonymous = isRoomAnonymous(roomId);
|
||||||
|
this.tags = [];
|
||||||
|
this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY;
|
||||||
|
|
||||||
private constructor(
|
if (this.anonymous) {
|
||||||
public readonly roomUrl: string,
|
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
||||||
private mapUrl: string,
|
} else {
|
||||||
private readonly connectCallback: ConnectCallback,
|
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
|
||||||
private readonly disconnectCallback: DisconnectCallback,
|
this.roomSlug = roomSlug;
|
||||||
private readonly minDistance: number,
|
this.organizationSlug = organizationSlug;
|
||||||
private readonly groupRadius: number,
|
this.worldSlug = worldSlug;
|
||||||
onEnters: EntersCallback,
|
}
|
||||||
onMoves: MovesCallback,
|
|
||||||
onLeaves: LeavesCallback,
|
|
||||||
onEmote: EmoteCallback,
|
this.users = new Map<number, User>();
|
||||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
this.usersByUuid = new Map<string, User>();
|
||||||
) {
|
this.admins = new Set<Admin>();
|
||||||
|
this.groups = new Set<Group>();
|
||||||
|
this.connectCallback = connectCallback;
|
||||||
|
this.disconnectCallback = disconnectCallback;
|
||||||
|
this.minDistance = minDistance;
|
||||||
|
this.groupRadius = groupRadius;
|
||||||
// A zone is 10 sprites wide.
|
// A zone is 10 sprites wide.
|
||||||
this.positionNotifier = new PositionNotifier(
|
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves);
|
||||||
320,
|
|
||||||
320,
|
|
||||||
onEnters,
|
|
||||||
onMoves,
|
|
||||||
onLeaves,
|
|
||||||
onEmote,
|
|
||||||
onPlayerDetailsUpdated
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async create(
|
public getGroups(): Group[] {
|
||||||
roomUrl: string,
|
return Array.from(this.groups.values());
|
||||||
connectCallback: ConnectCallback,
|
|
||||||
disconnectCallback: DisconnectCallback,
|
|
||||||
minDistance: number,
|
|
||||||
groupRadius: number,
|
|
||||||
onEnters: EntersCallback,
|
|
||||||
onMoves: MovesCallback,
|
|
||||||
onLeaves: LeavesCallback,
|
|
||||||
onEmote: EmoteCallback,
|
|
||||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
|
||||||
): Promise<GameRoom> {
|
|
||||||
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
|
||||||
|
|
||||||
const gameRoom = new GameRoom(
|
|
||||||
roomUrl,
|
|
||||||
mapDetails.mapUrl,
|
|
||||||
connectCallback,
|
|
||||||
disconnectCallback,
|
|
||||||
minDistance,
|
|
||||||
groupRadius,
|
|
||||||
onEnters,
|
|
||||||
onMoves,
|
|
||||||
onLeaves,
|
|
||||||
onEmote,
|
|
||||||
onPlayerDetailsUpdated
|
|
||||||
);
|
|
||||||
|
|
||||||
return gameRoom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUsers(): Map<number, User> {
|
public getUsers(): Map<number, User> {
|
||||||
return this.users;
|
return this.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserByUuid(uuid: string): User | undefined {
|
public getUserByUuid(uuid: string): User|undefined {
|
||||||
return this.usersByUuid.get(uuid);
|
return this.usersByUuid.get(uuid);
|
||||||
}
|
}
|
||||||
public getUserById(id: number): User | undefined {
|
|
||||||
return this.users.get(id);
|
|
||||||
}
|
|
||||||
public getUsersByUuid(uuid: string): User[] {
|
|
||||||
const userList: User[] = [];
|
|
||||||
for (const user of this.users.values()) {
|
|
||||||
if (user.uuid === uuid) {
|
|
||||||
userList.push(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User {
|
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User {
|
||||||
const positionMessage = joinRoomMessage.getPositionmessage();
|
const positionMessage = joinRoomMessage.getPositionmessage();
|
||||||
if (positionMessage === undefined) {
|
if (positionMessage === undefined) {
|
||||||
throw new Error("Missing position message");
|
throw new Error('Missing position message');
|
||||||
}
|
}
|
||||||
const position = ProtobufUtils.toPointInterface(positionMessage);
|
const position = ProtobufUtils.toPointInterface(positionMessage);
|
||||||
|
|
||||||
const user = new User(
|
const user = new User(this.nextUserId, joinRoomMessage.getUseruuid(), position, false, this.positionNotifier, socket, joinRoomMessage.getTagList(), joinRoomMessage.getName(), ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()));
|
||||||
this.nextUserId,
|
|
||||||
joinRoomMessage.getUseruuid(),
|
|
||||||
joinRoomMessage.getIpaddress(),
|
|
||||||
position,
|
|
||||||
false,
|
|
||||||
this.positionNotifier,
|
|
||||||
socket,
|
|
||||||
joinRoomMessage.getTagList(),
|
|
||||||
joinRoomMessage.getVisitcardurl(),
|
|
||||||
joinRoomMessage.getName(),
|
|
||||||
ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()),
|
|
||||||
joinRoomMessage.getCompanion()
|
|
||||||
);
|
|
||||||
this.nextUserId++;
|
this.nextUserId++;
|
||||||
this.users.set(user.id, user);
|
this.users.set(user.id, user);
|
||||||
this.usersByUuid.set(user.uuid, user);
|
this.usersByUuid.set(user.uuid, user);
|
||||||
|
// Let's call update position to trigger the join / leave room
|
||||||
|
//this.updatePosition(socket, userPosition);
|
||||||
this.updateUserGroup(user);
|
this.updateUserGroup(user);
|
||||||
|
|
||||||
// Notify admins
|
// Notify admins
|
||||||
for (const admin of this.admins) {
|
for (const admin of this.admins) {
|
||||||
admin.sendUserJoin(user.uuid, user.name, user.IPAddress);
|
admin.sendUserJoin(user.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public leave(user: User) {
|
public leave(user : User){
|
||||||
const userObj = this.users.get(user.id);
|
const userObj = this.users.get(user.id);
|
||||||
if (userObj === undefined) {
|
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.leaveGroup(userObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.hasFollowers()) {
|
|
||||||
user.stopLeading();
|
|
||||||
}
|
|
||||||
if (user.following) {
|
|
||||||
user.following.delFollower(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.users.delete(user.id);
|
this.users.delete(user.id);
|
||||||
this.usersByUuid.delete(user.uuid);
|
this.usersByUuid.delete(user.uuid);
|
||||||
|
|
||||||
|
@ -190,37 +135,32 @@ export class GameRoom {
|
||||||
|
|
||||||
// Notify admins
|
// Notify admins
|
||||||
for (const admin of this.admins) {
|
for (const admin of this.admins) {
|
||||||
admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
|
admin.sendUserLeft(user.uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isFull(): boolean {
|
||||||
|
return this.users.size >= MAX_USERS_PER_ROOM;
|
||||||
|
}
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
public isEmpty(): boolean {
|
||||||
return this.users.size === 0 && this.admins.size === 0;
|
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);
|
user.setPosition(userPosition);
|
||||||
|
|
||||||
this.updateUserGroup(user);
|
this.updateUserGroup(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
|
||||||
if (playerDetailsMessage.getRemoveoutlinecolor()) {
|
|
||||||
user.outlineColor = undefined;
|
|
||||||
} else {
|
|
||||||
user.outlineColor = playerDetailsMessage.getOutlinecolor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateUserGroup(user: User): void {
|
private updateUserGroup(user: User): void {
|
||||||
|
user.group?.updatePosition();
|
||||||
|
|
||||||
if (user.silent) {
|
if (user.silent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = user.group;
|
if (user.group === undefined) {
|
||||||
const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
|
|
||||||
|
|
||||||
if (group === undefined) {
|
|
||||||
// If the user is not part of a group:
|
// If the user is not part of a group:
|
||||||
// should he join a group?
|
// should he join a group?
|
||||||
|
|
||||||
|
@ -229,122 +169,30 @@ export class GameRoom {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user);
|
||||||
|
|
||||||
if (closestItem !== null) {
|
if (closestItem !== null) {
|
||||||
if (closestItem instanceof Group) {
|
if (closestItem instanceof Group) {
|
||||||
// Let's join the group!
|
// Let's join the group!
|
||||||
closestItem.join(user);
|
closestItem.join(user);
|
||||||
closestItem.setOutOfBounds(false);
|
|
||||||
} else {
|
} else {
|
||||||
const closestUser: User = closestItem;
|
const closestUser : User = closestItem;
|
||||||
const group: Group = new Group(
|
const group: Group = new Group(this.roomId,[
|
||||||
this.roomUrl,
|
user,
|
||||||
[user, closestUser],
|
closestUser
|
||||||
this.groupRadius,
|
], this.connectCallback, this.disconnectCallback, this.positionNotifier);
|
||||||
this.connectCallback,
|
|
||||||
this.disconnectCallback,
|
|
||||||
this.positionNotifier
|
|
||||||
);
|
|
||||||
this.groups.add(group);
|
this.groups.add(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let hasKickOutSomeone = false;
|
// If the user is part of a group:
|
||||||
let followingMembers: User[] = [];
|
// should he leave the group?
|
||||||
|
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition());
|
||||||
const previewNewGroupPosition = group.previewGroupPosition();
|
if (distance > this.groupRadius) {
|
||||||
|
|
||||||
if (!previewNewGroupPosition) {
|
|
||||||
this.leaveGroup(user);
|
this.leaveGroup(user);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.hasFollowers() || user.following) {
|
|
||||||
followingMembers = user.hasFollowers()
|
|
||||||
? group.getUsers().filter((currentUser) => currentUser.following === user)
|
|
||||||
: group.getUsers().filter((currentUser) => currentUser.following === user.following);
|
|
||||||
|
|
||||||
// If all group members are part of the same follow group
|
|
||||||
if (group.getUsers().length - 1 === followingMembers.length) {
|
|
||||||
let isOutOfBounds = false;
|
|
||||||
|
|
||||||
// If a follower is far away from the leader, "outOfBounds" is set to true
|
|
||||||
for (const member of followingMembers) {
|
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(
|
|
||||||
member.getPosition(),
|
|
||||||
previewNewGroupPosition
|
|
||||||
);
|
|
||||||
|
|
||||||
if (distance > this.groupRadius) {
|
|
||||||
isOutOfBounds = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.setOutOfBounds(isOutOfBounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the moving user has kicked out another user
|
|
||||||
for (const headMember of group.getGroupHeads()) {
|
|
||||||
if (!headMember.group) {
|
|
||||||
this.leaveGroup(headMember);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headPosition = headMember.getPosition();
|
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(headPosition, previewNewGroupPosition);
|
|
||||||
|
|
||||||
if (distance > this.groupRadius) {
|
|
||||||
hasKickOutSomeone = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the current moving user has kicked another user from the radius,
|
|
||||||
* the moving user leaves the group because he is too far away.
|
|
||||||
*/
|
|
||||||
const userDistance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), previewNewGroupPosition);
|
|
||||||
|
|
||||||
if (hasKickOutSomeone && userDistance > this.groupRadius) {
|
|
||||||
if (user.hasFollowers() && group.getUsers().length === 3 && followingMembers.length === 1) {
|
|
||||||
const other = group
|
|
||||||
.getUsers()
|
|
||||||
.find((currentUser) => !currentUser.hasFollowers() && !currentUser.following);
|
|
||||||
if (other) {
|
|
||||||
this.leaveGroup(other);
|
|
||||||
}
|
|
||||||
} else if (user.hasFollowers()) {
|
|
||||||
this.leaveGroup(user);
|
|
||||||
for (const member of followingMembers) {
|
|
||||||
this.leaveGroup(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-create a group with the followers
|
|
||||||
const newGroup: Group = new Group(
|
|
||||||
this.roomUrl,
|
|
||||||
[user, ...followingMembers],
|
|
||||||
this.groupRadius,
|
|
||||||
this.connectCallback,
|
|
||||||
this.disconnectCallback,
|
|
||||||
this.positionNotifier
|
|
||||||
);
|
|
||||||
this.groups.add(newGroup);
|
|
||||||
} else {
|
|
||||||
this.leaveGroup(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.group?.updatePosition();
|
|
||||||
user.group?.searchForNearbyUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendToOthersInGroupIncludingUser(user: User, message: ServerToClientMessage): void {
|
|
||||||
user.group?.getUsers().forEach((currentUser: User) => {
|
|
||||||
if (currentUser.id !== user.id) {
|
|
||||||
currentUser.socket.write(message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSilent(user: User, silent: boolean) {
|
setSilent(user: User, silent: boolean) {
|
||||||
|
@ -374,9 +222,10 @@ export class GameRoom {
|
||||||
}
|
}
|
||||||
group.leave(user);
|
group.leave(user);
|
||||||
if (group.isEmpty()) {
|
if (group.isEmpty()) {
|
||||||
|
this.positionNotifier.leave(group);
|
||||||
group.destroy();
|
group.destroy();
|
||||||
if (!this.groups.has(group)) {
|
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);
|
this.groups.delete(group);
|
||||||
//todo: is the group garbage collected?
|
//todo: is the group garbage collected?
|
||||||
|
@ -394,15 +243,16 @@ export class GameRoom {
|
||||||
* OR
|
* OR
|
||||||
* - close enough to a group (distance <= groupRadius)
|
* - 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 minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
||||||
let matchingItem: User | Group | null = null;
|
let matchingItem: User | Group | null = null;
|
||||||
this.users.forEach((currentUser, userId) => {
|
this.users.forEach((currentUser, userId) => {
|
||||||
// Let's only check users that are not part of a group
|
// Let's only check users that are not part of a group
|
||||||
if (typeof currentUser.group !== "undefined") {
|
if (typeof currentUser.group !== 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentUser === user) {
|
if(currentUser === user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentUser.silent) {
|
if (currentUser.silent) {
|
||||||
|
@ -411,7 +261,7 @@ export class GameRoom {
|
||||||
|
|
||||||
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
||||||
|
|
||||||
if (distance <= minimumDistanceFound && distance <= this.minDistance) {
|
if(distance <= minimumDistanceFound && distance <= this.minDistance) {
|
||||||
minimumDistanceFound = distance;
|
minimumDistanceFound = distance;
|
||||||
matchingItem = currentUser;
|
matchingItem = currentUser;
|
||||||
}
|
}
|
||||||
|
@ -422,7 +272,7 @@ export class GameRoom {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
||||||
if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
||||||
minimumDistanceFound = distance;
|
minimumDistanceFound = distance;
|
||||||
matchingItem = group;
|
matchingItem = group;
|
||||||
}
|
}
|
||||||
|
@ -431,15 +281,15 @@ export class GameRoom {
|
||||||
return matchingItem;
|
return matchingItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static computeDistance(user1: User, user2: User): number {
|
public static computeDistance(user1: User, user2: User): number
|
||||||
|
{
|
||||||
const user1Position = user1.getPosition();
|
const user1Position = user1.getPosition();
|
||||||
const user2Position = user2.getPosition();
|
const user2Position = user2.getPosition();
|
||||||
return Math.sqrt(
|
return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2));
|
||||||
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));
|
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,67 +301,8 @@ export class GameRoom {
|
||||||
return this.itemsState;
|
return this.itemsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
public canAccess(userTags: string[]): boolean {
|
||||||
// First, let's check if "user" is allowed to modify the variable.
|
return arrayIntersect(userTags, this.tags);
|
||||||
const variableManager = await this.getVariableManager();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const readableBy = variableManager.setVariable(name, value, user);
|
|
||||||
|
|
||||||
// If the variable was not changed, let's not dispatch anything.
|
|
||||||
if (readableBy === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: should we batch those every 100ms?
|
|
||||||
const variableMessage = new VariableWithTagMessage();
|
|
||||||
variableMessage.setName(name);
|
|
||||||
variableMessage.setValue(value);
|
|
||||||
if (readableBy) {
|
|
||||||
variableMessage.setReadableby(readableBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherRoomMessage();
|
|
||||||
subMessage.setVariablemessage(variableMessage);
|
|
||||||
|
|
||||||
const batchMessage = new BatchToPusherRoomMessage();
|
|
||||||
batchMessage.addPayload(subMessage);
|
|
||||||
|
|
||||||
// Dispatch the message on the room listeners
|
|
||||||
for (const socket of this.roomListeners) {
|
|
||||||
socket.write(batchMessage);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof VariableError) {
|
|
||||||
// Ok, we have an error setting a variable. Either the user is trying to hack the map... or the map
|
|
||||||
// is not up to date. So let's try to reload the map from scratch.
|
|
||||||
if (this.variableManagerLastLoad === undefined) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
const lastLoaded = new Date().getTime() - this.variableManagerLastLoad.getTime();
|
|
||||||
if (lastLoaded < 10000) {
|
|
||||||
console.log(
|
|
||||||
'An error occurred while setting the "' +
|
|
||||||
name +
|
|
||||||
"\" variable. But we tried to reload the map less than 10 seconds ago, so let's fail."
|
|
||||||
);
|
|
||||||
// Do not try to reload if we tried to reload less than 10 seconds ago.
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the variable manager
|
|
||||||
this.variableManagerPromise = undefined;
|
|
||||||
this.mapPromise = undefined;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'An error occurred while setting the "' + name + "\" variable. Let's reload the map and try again"
|
|
||||||
);
|
|
||||||
// Try to set the variable again!
|
|
||||||
await this.setVariable(name, value, user);
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
||||||
|
@ -527,131 +318,11 @@ export class GameRoom {
|
||||||
|
|
||||||
// Let's send all connected users
|
// Let's send all connected users
|
||||||
for (const user of this.users.values()) {
|
for (const user of this.users.values()) {
|
||||||
admin.sendUserJoin(user.uuid, user.name, user.IPAddress);
|
admin.sendUserJoin(user.uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public adminLeave(admin: Admin): void {
|
public adminLeave(admin: Admin): void {
|
||||||
this.admins.delete(admin);
|
this.admins.delete(admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public incrementVersion(): number {
|
|
||||||
this.versionNumber++;
|
|
||||||
return this.versionNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
|
||||||
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public addRoomListener(socket: RoomSocket) {
|
|
||||||
this.roomListeners.add(socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeRoomListener(socket: RoomSocket) {
|
|
||||||
this.roomListeners.delete(socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects to the admin server to fetch map details.
|
|
||||||
* If there is no admin server, the map details are generated by analysing the map URL (that must be in the form: /_/instance/map_url)
|
|
||||||
*/
|
|
||||||
private static async getMapDetails(roomUrl: string): Promise<MapDetailsData> {
|
|
||||||
if (!ADMIN_API_URL) {
|
|
||||||
const roomUrlObj = new URL(roomUrl);
|
|
||||||
|
|
||||||
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
|
||||||
if (!match) {
|
|
||||||
console.error("Unexpected room URL", roomUrl);
|
|
||||||
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapUrl = roomUrlObj.protocol + "//" + match[1];
|
|
||||||
|
|
||||||
return {
|
|
||||||
mapUrl,
|
|
||||||
policy_type: 1,
|
|
||||||
textures: [],
|
|
||||||
tags: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await adminApi.fetchMapDetails(roomUrl);
|
|
||||||
if (isRoomRedirect(result)) {
|
|
||||||
console.error("Unexpected room redirect received while querying map details", result);
|
|
||||||
throw new Error("Unexpected room redirect received while querying map details");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapPromise: Promise<ITiledMap> | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a promise to the map file.
|
|
||||||
* @throws LocalUrlError if the map we are trying to load is hosted on a local network
|
|
||||||
* @throws Error
|
|
||||||
*/
|
|
||||||
private getMap(): Promise<ITiledMap> {
|
|
||||||
if (!this.mapPromise) {
|
|
||||||
this.mapPromise = mapFetcher.fetchMap(this.mapUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.mapPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private variableManagerPromise: Promise<VariablesManager> | undefined;
|
|
||||||
private variableManagerLastLoad: Date | undefined;
|
|
||||||
|
|
||||||
private getVariableManager(): Promise<VariablesManager> {
|
|
||||||
if (!this.variableManagerPromise) {
|
|
||||||
this.variableManagerLastLoad = new Date();
|
|
||||||
this.variableManagerPromise = this.getMap()
|
|
||||||
.then((map) => {
|
|
||||||
const variablesManager = new VariablesManager(this.roomUrl, map);
|
|
||||||
return variablesManager.init();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (e instanceof LocalUrlError) {
|
|
||||||
// If we are trying to load a local URL, we are probably in test mode.
|
|
||||||
// In this case, let's bypass the server-side checks completely.
|
|
||||||
|
|
||||||
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const roomListener of this.roomListeners) {
|
|
||||||
emitErrorOnRoomSocket(
|
|
||||||
roomListener,
|
|
||||||
"You are loading a local map. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
const variablesManager = new VariablesManager(this.roomUrl, null);
|
|
||||||
return variablesManager.init();
|
|
||||||
} else {
|
|
||||||
// An error occurred while loading the map
|
|
||||||
// Right now, let's bypass the error. In the future, we should make sure the user is aware of that
|
|
||||||
// and that he/she will act on it to fix the problem.
|
|
||||||
|
|
||||||
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const roomListener of this.roomListeners) {
|
|
||||||
emitErrorOnRoomSocket(
|
|
||||||
roomListener,
|
|
||||||
"Your map does not seem accessible from the WorkAdventure servers. Is it behind a firewall or a proxy? Your map should be accessible from the WorkAdventure servers. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
const variablesManager = new VariablesManager(this.roomUrl, null);
|
|
||||||
return variablesManager.init();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.variableManagerPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getVariablesForTags(tags: string[]): Promise<Map<string, string>> {
|
|
||||||
const variablesManager = await this.getVariableManager();
|
|
||||||
return variablesManager.getVariablesForTags(tags);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,36 @@
|
||||||
import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom";
|
import { ConnectCallback, DisconnectCallback } from "./GameRoom";
|
||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
import { Movable } from "_Model/Movable";
|
import {Movable} from "_Model/Movable";
|
||||||
import { PositionNotifier } from "_Model/PositionNotifier";
|
import {PositionNotifier} from "_Model/PositionNotifier";
|
||||||
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
|
import {gaugeManager} from "../Services/GaugeManager";
|
||||||
import type { Zone } from "../Model/Zone";
|
|
||||||
|
|
||||||
export class Group implements Movable {
|
export class Group implements Movable {
|
||||||
|
static readonly MAX_PER_GROUP = 4;
|
||||||
|
|
||||||
private static nextId: number = 1;
|
private static nextId: number = 1;
|
||||||
|
|
||||||
private id: number;
|
private id: number;
|
||||||
private users: Set<User>;
|
private users: Set<User>;
|
||||||
private x!: number;
|
private x!: number;
|
||||||
private y!: number;
|
private y!: number;
|
||||||
|
private hasEditedGauge: boolean = false;
|
||||||
private wasDestroyed: boolean = false;
|
private wasDestroyed: boolean = false;
|
||||||
private roomId: string;
|
private roomId: string;
|
||||||
private currentZone: Zone | null = null;
|
|
||||||
/**
|
|
||||||
* When outOfBounds = true, a user if out of the bounds of the group BUT still considered inside it (because we are in following mode)
|
|
||||||
*/
|
|
||||||
private outOfBounds = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
roomId: string,
|
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) {
|
||||||
users: User[],
|
|
||||||
private groupRadius: number,
|
|
||||||
private connectCallback: ConnectCallback,
|
|
||||||
private disconnectCallback: DisconnectCallback,
|
|
||||||
private positionNotifier: PositionNotifier
|
|
||||||
) {
|
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
this.users = new Set<User>();
|
this.users = new Set<User>();
|
||||||
this.id = Group.nextId;
|
this.id = Group.nextId;
|
||||||
Group.nextId++;
|
Group.nextId++;
|
||||||
|
//we only send a event for prometheus metrics if the group lives more than 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.wasDestroyed) {
|
||||||
|
this.hasEditedGauge = true;
|
||||||
|
gaugeManager.incNbGroupsPerRoomGauge(roomId);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
users.forEach((user: User) => {
|
users.forEach((user: User) => {
|
||||||
this.join(user);
|
this.join(user);
|
||||||
|
@ -45,7 +43,7 @@ export class Group implements Movable {
|
||||||
return Array.from(this.users.values());
|
return Array.from(this.users.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): number {
|
getId() : number {
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,43 +53,10 @@ export class Group implements Movable {
|
||||||
getPosition(): PositionInterface {
|
getPosition(): PositionInterface {
|
||||||
return {
|
return {
|
||||||
x: this.x,
|
x: this.x,
|
||||||
y: this.y,
|
y: this.y
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of users of the group, ignoring any "followers".
|
|
||||||
* Useful to compute the position of the group if a follower is "trapped" far away from the the leader.
|
|
||||||
*/
|
|
||||||
getGroupHeads(): User[] {
|
|
||||||
return Array.from(this.users).filter((user) => user.group?.leader === user || !user.following);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview the position of the group but don't update it
|
|
||||||
*/
|
|
||||||
previewGroupPosition(): { x: number; y: number } | undefined {
|
|
||||||
const users = this.getGroupHeads();
|
|
||||||
|
|
||||||
let x = 0;
|
|
||||||
let y = 0;
|
|
||||||
|
|
||||||
if (users.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
users.forEach((user: User) => {
|
|
||||||
const position = user.getPosition();
|
|
||||||
x += position.x;
|
|
||||||
y += position.y;
|
|
||||||
});
|
|
||||||
|
|
||||||
x /= users.length;
|
|
||||||
y /= users.length;
|
|
||||||
|
|
||||||
return { x, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the barycenter of all users (i.e. the center of the group)
|
* Computes the barycenter of all users (i.e. the center of the group)
|
||||||
*/
|
*/
|
||||||
|
@ -99,63 +64,50 @@ export class Group implements Movable {
|
||||||
const oldX = this.x;
|
const oldX = this.x;
|
||||||
const oldY = this.y;
|
const oldY = this.y;
|
||||||
|
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
// Let's compute the barycenter of all users.
|
// Let's compute the barycenter of all users.
|
||||||
const newPosition = this.previewGroupPosition();
|
this.users.forEach((user: User) => {
|
||||||
|
const position = user.getPosition();
|
||||||
if (!newPosition) {
|
x += position.x;
|
||||||
return;
|
y += position.y;
|
||||||
|
});
|
||||||
|
x /= this.users.size;
|
||||||
|
y /= this.users.size;
|
||||||
|
if (this.users.size === 0) {
|
||||||
|
throw new Error("EMPTY GROUP FOUND!!!");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x, y } = newPosition;
|
|
||||||
|
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
|
|
||||||
if (this.outOfBounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldX === undefined) {
|
if (oldX === undefined) {
|
||||||
this.currentZone = this.positionNotifier.enter(this);
|
this.positionNotifier.enter(this);
|
||||||
} else {
|
} else {
|
||||||
this.currentZone = this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY });
|
this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchForNearbyUsers(): void {
|
|
||||||
if (!this.currentZone) return;
|
|
||||||
|
|
||||||
for (const user of this.positionNotifier.getAllUsersInSquareAroundZone(this.currentZone)) {
|
|
||||||
// Todo: Merge two groups with a leader
|
|
||||||
if (user.group || this.isFull()) return; //we ignore users that are already in a group.
|
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), this.getPosition());
|
|
||||||
if (distance < this.groupRadius) {
|
|
||||||
this.join(user);
|
|
||||||
this.setOutOfBounds(false);
|
|
||||||
this.updatePosition();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isFull(): boolean {
|
isFull(): boolean {
|
||||||
return this.users.size >= MAX_PER_GROUP;
|
return this.users.size >= Group.MAX_PER_GROUP;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this.users.size <= 1;
|
return this.users.size <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
join(user: User): void {
|
join(user: User): void
|
||||||
|
{
|
||||||
// Broadcast on the right event
|
// Broadcast on the right event
|
||||||
this.connectCallback(user, this);
|
this.connectCallback(user, this);
|
||||||
this.users.add(user);
|
this.users.add(user);
|
||||||
user.group = this;
|
user.group = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(user: User): void {
|
leave(user: User): void
|
||||||
|
{
|
||||||
const success = this.users.delete(user);
|
const success = this.users.delete(user);
|
||||||
if (success === false) {
|
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;
|
user.group = undefined;
|
||||||
|
|
||||||
|
@ -171,44 +123,16 @@ export class Group implements Movable {
|
||||||
* Let's kick everybody out.
|
* Let's kick everybody out.
|
||||||
* Usually used when there is only one user left.
|
* Usually used when there is only one user left.
|
||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void
|
||||||
if (!this.outOfBounds) {
|
{
|
||||||
this.positionNotifier.leave(this);
|
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
|
||||||
}
|
|
||||||
|
|
||||||
for (const user of this.users) {
|
for (const user of this.users) {
|
||||||
this.leave(user);
|
this.leave(user);
|
||||||
}
|
}
|
||||||
this.wasDestroyed = true;
|
this.wasDestroyed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get getSize() {
|
get getSize(){
|
||||||
return this.users.size;
|
return this.users.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A group can have at most one person leading the way in it.
|
|
||||||
*/
|
|
||||||
get leader(): User | undefined {
|
|
||||||
for (const user of this.users) {
|
|
||||||
if (user.hasFollowers()) {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOutOfBounds(outOfBounds: boolean): void {
|
|
||||||
if (this.outOfBounds === true && outOfBounds === false) {
|
|
||||||
this.positionNotifier.enter(this);
|
|
||||||
this.outOfBounds = false;
|
|
||||||
} else if (this.outOfBounds === false && outOfBounds === true) {
|
|
||||||
this.positionNotifier.leave(this);
|
|
||||||
this.outOfBounds = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get getOutOfBounds() {
|
|
||||||
return this.outOfBounds;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A physical object that can be placed into a Zone
|
* A physical object that can be placed into a Zone
|
||||||
*/
|
*/
|
||||||
export interface Movable {
|
export interface Movable {
|
||||||
getPosition(): PositionInterface;
|
getPosition(): PositionInterface
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export interface PositionInterface {
|
export interface PositionInterface {
|
||||||
x: number;
|
x: number,
|
||||||
y: number;
|
y: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,67 +8,40 @@
|
||||||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
* 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.
|
* number of players around the current player.
|
||||||
*/
|
*/
|
||||||
import {
|
import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
|
||||||
EmoteCallback,
|
import {Movable} from "_Model/Movable";
|
||||||
EntersCallback,
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
LeavesCallback,
|
import {ZoneSocket} from "../RoomManager";
|
||||||
MovesCallback,
|
|
||||||
PlayerDetailsUpdatedCallback,
|
|
||||||
Zone,
|
|
||||||
} from "./Zone";
|
|
||||||
import { Movable } from "_Model/Movable";
|
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
|
||||||
import { ZoneSocket } from "../RoomManager";
|
|
||||||
import { User } from "../Model/User";
|
|
||||||
import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";
|
|
||||||
|
|
||||||
interface ZoneDescriptor {
|
interface ZoneDescriptor {
|
||||||
i: number;
|
i: number;
|
||||||
j: number;
|
j: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* getNearbyDescriptorsMatrix(middleZoneDescriptor: ZoneDescriptor): Generator<ZoneDescriptor> {
|
|
||||||
for (let n = 0; n < 9; n++) {
|
|
||||||
const i = middleZoneDescriptor.i + ((n % 3) - 1);
|
|
||||||
const j = middleZoneDescriptor.j + (Math.floor(n / 3) - 1);
|
|
||||||
|
|
||||||
if (i >= 0 && j >= 0) {
|
|
||||||
yield { i, j };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PositionNotifier {
|
export class PositionNotifier {
|
||||||
// TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!)
|
|
||||||
|
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
||||||
|
|
||||||
private zones: Zone[][] = [];
|
private zones: Zone[][] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback) {
|
||||||
private zoneWidth: number,
|
}
|
||||||
private zoneHeight: number,
|
|
||||||
private onUserEnters: EntersCallback,
|
|
||||||
private onUserMoves: MovesCallback,
|
|
||||||
private onUserLeaves: LeavesCallback,
|
|
||||||
private onEmote: EmoteCallback,
|
|
||||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||||
return {
|
return {
|
||||||
i: Math.floor(x / this.zoneWidth),
|
i: Math.floor(x / this.zoneWidth),
|
||||||
j: Math.floor(y / this.zoneHeight),
|
j: Math.floor(y / this.zoneHeight),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enter(thing: Movable): Zone {
|
public enter(thing: Movable): void {
|
||||||
const position = thing.getPosition();
|
const position = thing.getPosition();
|
||||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
||||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||||
zone.enter(thing, null, position);
|
zone.enter(thing, null, position);
|
||||||
return zone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): Zone {
|
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): void {
|
||||||
// Did we change zone?
|
// Did we change zone?
|
||||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
||||||
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
||||||
|
@ -82,11 +55,9 @@ export class PositionNotifier {
|
||||||
|
|
||||||
// Enter new zone
|
// Enter new zone
|
||||||
newZone.enter(thing, oldZone, newPosition);
|
newZone.enter(thing, oldZone, newPosition);
|
||||||
return newZone;
|
|
||||||
} else {
|
} else {
|
||||||
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
zone.move(thing, newPosition);
|
zone.move(thing, newPosition);
|
||||||
return zone;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,15 +77,7 @@ export class PositionNotifier {
|
||||||
|
|
||||||
let zone = this.zones[j][i];
|
let zone = this.zones[j][i];
|
||||||
if (zone === undefined) {
|
if (zone === undefined) {
|
||||||
zone = new Zone(
|
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j);
|
||||||
this.onUserEnters,
|
|
||||||
this.onUserMoves,
|
|
||||||
this.onUserLeaves,
|
|
||||||
this.onEmote,
|
|
||||||
this.onPlayerDetailsUpdated,
|
|
||||||
i,
|
|
||||||
j
|
|
||||||
);
|
|
||||||
this.zones[j][i] = zone;
|
this.zones[j][i] = zone;
|
||||||
}
|
}
|
||||||
return zone;
|
return zone;
|
||||||
|
@ -130,29 +93,4 @@ export class PositionNotifier {
|
||||||
const zone = this.getZone(x, y);
|
const zone = this.getZone(x, y);
|
||||||
zone.removeListener(call);
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public *getAllUsersInSquareAroundZone(zone: Zone): Generator<User> {
|
|
||||||
const zoneDescriptor = this.getZoneDescriptorFromCoordinates(zone.x, zone.y);
|
|
||||||
for (const d of getNearbyDescriptorsMatrix(zoneDescriptor)) {
|
|
||||||
const zone = this.getZone(d.i, d.j);
|
|
||||||
for (const thing of zone.getThings()) {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
yield thing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
|
|
||||||
const position = user.getPosition();
|
|
||||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
|
||||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
|
||||||
zone.updatePlayerDetails(user, playerDetails);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
30
back/src/Model/RoomIdentifier.ts
Normal file
30
back/src/Model/RoomIdentifier.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
//helper functions to parse room IDs
|
||||||
|
|
||||||
|
export const isRoomAnonymous = (roomID: string): boolean => {
|
||||||
|
if (roomID.startsWith('_/')) {
|
||||||
|
return true;
|
||||||
|
} else if(roomID.startsWith('@/')) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
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('/');
|
||||||
|
}
|
||||||
|
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 organizationSlug = idParts[1];
|
||||||
|
const worldSlug = idParts[2];
|
||||||
|
const roomSlug = idParts[3];
|
||||||
|
return {organizationSlug, worldSlug, roomSlug}
|
||||||
|
}
|
|
@ -1,43 +1,28 @@
|
||||||
import { Group } from "./Group";
|
import { Group } from "./Group";
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
import { Zone } from "_Model/Zone";
|
import {Zone} from "_Model/Zone";
|
||||||
import { Movable } from "_Model/Movable";
|
import {Movable} from "_Model/Movable";
|
||||||
import { PositionNotifier } from "_Model/PositionNotifier";
|
import {PositionNotifier} from "_Model/PositionNotifier";
|
||||||
import { ServerDuplexStream } from "grpc";
|
import {ServerDuplexStream} from "grpc";
|
||||||
import {
|
import {BatchMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb";
|
||||||
BatchMessage,
|
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
|
||||||
CompanionMessage,
|
|
||||||
FollowAbortMessage,
|
|
||||||
FollowConfirmationMessage,
|
|
||||||
PusherToBackMessage,
|
|
||||||
ServerToClientMessage,
|
|
||||||
SetPlayerDetailsMessage,
|
|
||||||
SubMessage,
|
|
||||||
} from "../Messages/generated/messages_pb";
|
|
||||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
|
||||||
|
|
||||||
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||||
|
|
||||||
export class User implements Movable {
|
export class User implements Movable {
|
||||||
public listenedZones: Set<Zone>;
|
public listenedZones: Set<Zone>;
|
||||||
public group?: Group;
|
public group?: Group;
|
||||||
private _following: User | undefined;
|
|
||||||
private followedBy: Set<User> = new Set<User>();
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public id: number,
|
public id: number,
|
||||||
public readonly uuid: string,
|
public readonly uuid: string,
|
||||||
public readonly IPAddress: string,
|
|
||||||
private position: PointInterface,
|
private position: PointInterface,
|
||||||
public silent: boolean,
|
public silent: boolean,
|
||||||
private positionNotifier: PositionNotifier,
|
private positionNotifier: PositionNotifier,
|
||||||
public readonly socket: UserSocket,
|
public readonly socket: UserSocket,
|
||||||
public readonly tags: string[],
|
public readonly tags: string[],
|
||||||
public readonly visitCardUrl: string | null,
|
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly characterLayers: CharacterLayer[],
|
public readonly characterLayers: CharacterLayer[]
|
||||||
public readonly companion?: CompanionMessage,
|
|
||||||
private _outlineColor?: number | undefined
|
|
||||||
) {
|
) {
|
||||||
this.listenedZones = new Set<Zone>();
|
this.listenedZones = new Set<Zone>();
|
||||||
|
|
||||||
|
@ -54,47 +39,9 @@ export class User implements Movable {
|
||||||
this.positionNotifier.updatePosition(this, position, oldPosition);
|
this.positionNotifier.updatePosition(this, position, oldPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addFollower(follower: User): void {
|
|
||||||
this.followedBy.add(follower);
|
|
||||||
follower._following = this;
|
|
||||||
|
|
||||||
const message = new FollowConfirmationMessage();
|
|
||||||
message.setFollower(follower.id);
|
|
||||||
message.setLeader(this.id);
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
|
||||||
clientMessage.setFollowconfirmationmessage(message);
|
|
||||||
this.socket.write(clientMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public delFollower(follower: User): void {
|
|
||||||
this.followedBy.delete(follower);
|
|
||||||
follower._following = undefined;
|
|
||||||
|
|
||||||
const message = new FollowAbortMessage();
|
|
||||||
message.setFollower(follower.id);
|
|
||||||
message.setLeader(this.id);
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
|
||||||
clientMessage.setFollowabortmessage(message);
|
|
||||||
this.socket.write(clientMessage);
|
|
||||||
follower.socket.write(clientMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasFollowers(): boolean {
|
|
||||||
return this.followedBy.size !== 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get following(): User | undefined {
|
|
||||||
return this._following;
|
|
||||||
}
|
|
||||||
|
|
||||||
public stopLeading(): void {
|
|
||||||
for (const follower of this.followedBy) {
|
|
||||||
this.delFollower(follower);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private batchedMessages: BatchMessage = new BatchMessage();
|
private batchedMessages: BatchMessage = new BatchMessage();
|
||||||
private batchTimeout: NodeJS.Timeout | null = null;
|
private batchTimeout: NodeJS.Timeout|null = null;
|
||||||
|
|
||||||
public emitInBatch(payload: SubMessage): void {
|
public emitInBatch(payload: SubMessage): void {
|
||||||
this.batchedMessages.addPayload(payload);
|
this.batchedMessages.addPayload(payload);
|
||||||
|
@ -114,17 +61,4 @@ export class User implements Movable {
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public set outlineColor(value: number | undefined) {
|
|
||||||
this._outlineColor = value;
|
|
||||||
|
|
||||||
const playerDetails = new SetPlayerDetailsMessage();
|
|
||||||
if (value === undefined) {
|
|
||||||
playerDetails.setRemoveoutlinecolor(true);
|
|
||||||
} else {
|
|
||||||
playerDetails.setOutlinecolor(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.positionNotifier.updatePlayerDetails(this, playerDetails);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export interface CharacterLayer {
|
export interface CharacterLayer {
|
||||||
name: string;
|
name: string,
|
||||||
url: string | undefined;
|
url: string|undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isItemEventMessageInterface = new tg.IsInterface()
|
export const isItemEventMessageInterface =
|
||||||
.withProperties({
|
new tg.IsInterface().withProperties({
|
||||||
itemId: tg.isNumber,
|
itemId: tg.isNumber,
|
||||||
event: tg.isString,
|
event: tg.isString,
|
||||||
state: tg.isUnknown,
|
state: tg.isUnknown,
|
||||||
parameters: tg.isUnknown,
|
parameters: tg.isUnknown,
|
||||||
})
|
}).get();
|
||||||
.get();
|
|
||||||
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { PointInterface } from "./PointInterface";
|
import {PointInterface} from "./PointInterface";
|
||||||
|
|
||||||
export class Point implements PointInterface {
|
export class Point implements PointInterface{
|
||||||
constructor(
|
constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) {
|
||||||
public x: number,
|
}
|
||||||
public y: number,
|
|
||||||
public direction: string = "none",
|
|
||||||
public moving: boolean = false
|
|
||||||
) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,11 @@ import * as tg from "generic-type-guard";
|
||||||
readonly moving: boolean;
|
readonly moving: boolean;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
export const isPointInterface = new tg.IsInterface()
|
export const isPointInterface =
|
||||||
.withProperties({
|
new tg.IsInterface().withProperties({
|
||||||
x: tg.isNumber,
|
x: tg.isNumber,
|
||||||
y: tg.isNumber,
|
y: tg.isNumber,
|
||||||
direction: tg.isString,
|
direction: tg.isString,
|
||||||
moving: tg.isBoolean,
|
moving: tg.isBoolean
|
||||||
})
|
}).get();
|
||||||
.get();
|
|
||||||
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
||||||
|
|
|
@ -1,33 +1,34 @@
|
||||||
import { PointInterface } from "./PointInterface";
|
import {PointInterface} from "./PointInterface";
|
||||||
import {
|
import {
|
||||||
CharacterLayerMessage,
|
CharacterLayerMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
PointMessage,
|
PointMessage,
|
||||||
PositionMessage,
|
PositionMessage
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage";
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
export class ProtobufUtils {
|
export class ProtobufUtils {
|
||||||
|
|
||||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||||
let direction: Direction;
|
let direction: Direction;
|
||||||
switch (point.direction) {
|
switch (point.direction) {
|
||||||
case "up":
|
case 'up':
|
||||||
direction = Direction.UP;
|
direction = Direction.UP;
|
||||||
break;
|
break;
|
||||||
case "down":
|
case 'down':
|
||||||
direction = Direction.DOWN;
|
direction = Direction.DOWN;
|
||||||
break;
|
break;
|
||||||
case "left":
|
case 'left':
|
||||||
direction = Direction.LEFT;
|
direction = Direction.LEFT;
|
||||||
break;
|
break;
|
||||||
case "right":
|
case 'right':
|
||||||
direction = Direction.RIGHT;
|
direction = Direction.RIGHT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("unexpected direction");
|
throw new Error('unexpected direction');
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = new PositionMessage();
|
const position = new PositionMessage();
|
||||||
|
@ -43,16 +44,16 @@ export class ProtobufUtils {
|
||||||
let direction: string;
|
let direction: string;
|
||||||
switch (position.getDirection()) {
|
switch (position.getDirection()) {
|
||||||
case Direction.UP:
|
case Direction.UP:
|
||||||
direction = "up";
|
direction = 'up';
|
||||||
break;
|
break;
|
||||||
case Direction.DOWN:
|
case Direction.DOWN:
|
||||||
direction = "down";
|
direction = 'down';
|
||||||
break;
|
break;
|
||||||
case Direction.LEFT:
|
case Direction.LEFT:
|
||||||
direction = "left";
|
direction = 'left';
|
||||||
break;
|
break;
|
||||||
case Direction.RIGHT:
|
case Direction.RIGHT:
|
||||||
direction = "right";
|
direction = 'right';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Unexpected direction");
|
throw new Error("Unexpected direction");
|
||||||
|
@ -81,7 +82,7 @@ export class ProtobufUtils {
|
||||||
event: itemEventMessage.getEvent(),
|
event: itemEventMessage.getEvent(),
|
||||||
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
||||||
state: JSON.parse(itemEventMessage.getStatejson()),
|
state: JSON.parse(itemEventMessage.getStatejson()),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
||||||
|
@ -95,7 +96,7 @@ export class ProtobufUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
||||||
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
return characterLayers.map(function(characterLayer): CharacterLayerMessage {
|
||||||
const message = new CharacterLayerMessage();
|
const message = new CharacterLayerMessage();
|
||||||
message.setName(characterLayer.name);
|
message.setName(characterLayer.name);
|
||||||
if (characterLayer.url) {
|
if (characterLayer.url) {
|
||||||
|
@ -106,7 +107,7 @@ export class ProtobufUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
||||||
return characterLayers.map(function (characterLayer): CharacterLayer {
|
return characterLayers.map(function(characterLayer): CharacterLayer {
|
||||||
const url = characterLayer.getUrl();
|
const url = characterLayer.getUrl();
|
||||||
return {
|
return {
|
||||||
name: characterLayer.getName(),
|
name: characterLayer.getName(),
|
||||||
|
|
|
@ -1,53 +1,37 @@
|
||||||
import { User } from "./User";
|
import {User} from "./User";
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
import { Movable } from "./Movable";
|
import {Movable} from "./Movable";
|
||||||
import { Group } from "./Group";
|
import {Group} from "./Group";
|
||||||
import { ZoneSocket } from "../RoomManager";
|
import {ZoneSocket} from "../RoomManager";
|
||||||
import {
|
|
||||||
EmoteEventMessage,
|
|
||||||
SetPlayerDetailsMessage,
|
|
||||||
PlayerDetailsUpdatedMessage,
|
|
||||||
} 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 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 type PlayerDetailsUpdatedCallback = (
|
|
||||||
playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage,
|
|
||||||
listener: ZoneSocket
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export class Zone {
|
export class Zone {
|
||||||
private things: Set<Movable> = new Set<Movable>();
|
private things: Set<Movable> = new Set<Movable>();
|
||||||
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
||||||
|
|
||||||
constructor(
|
/**
|
||||||
private onEnters: EntersCallback,
|
* @param x For debugging purpose only
|
||||||
private onMoves: MovesCallback,
|
* @param y For debugging purpose only
|
||||||
private onLeaves: LeavesCallback,
|
*/
|
||||||
private onEmote: EmoteCallback,
|
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, public readonly x: number, public readonly y: number) {
|
||||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
|
}
|
||||||
public readonly x: number,
|
|
||||||
public readonly y: number
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A user/thing leaves the zone
|
* 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);
|
const result = this.things.delete(thing);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
if (thing instanceof User) {
|
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) {
|
if (thing instanceof Group) {
|
||||||
throw new Error(
|
throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')');
|
||||||
`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);
|
this.notifyLeft(thing, newZone);
|
||||||
}
|
}
|
||||||
|
@ -55,13 +39,15 @@ export class Zone {
|
||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user/thing left
|
* 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) {
|
for (const listener of this.listeners) {
|
||||||
this.onLeaves(thing, newZone, listener);
|
//if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) {
|
||||||
|
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.things.add(thing);
|
||||||
this.notifyEnter(thing, oldZone, position);
|
this.notifyEnter(thing, oldZone, position);
|
||||||
}
|
}
|
||||||
|
@ -69,12 +55,22 @@ export class Zone {
|
||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user entered
|
* 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) {
|
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);
|
this.onEnters(thing, oldZone, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public move(thing: Movable, position: PositionInterface) {
|
public move(thing: Movable, position: PositionInterface) {
|
||||||
if (!this.things.has(thing)) {
|
if (!this.things.has(thing)) {
|
||||||
this.things.add(thing);
|
this.things.add(thing);
|
||||||
|
@ -84,11 +80,33 @@ export class Zone {
|
||||||
|
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
//if (listener !== thing) {
|
//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<Movable> {
|
public getThings(): Set<Movable> {
|
||||||
return this.things;
|
return this.things;
|
||||||
}
|
}
|
||||||
|
@ -101,20 +119,4 @@ export class Zone {
|
||||||
public removeListener(socket: ZoneSocket): void {
|
public removeListener(socket: ZoneSocket): void {
|
||||||
this.listeners.delete(socket);
|
this.listeners.delete(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitEmoteEvent(emoteEventMessage: EmoteEventMessage) {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
this.onEmote(emoteEventMessage, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
|
|
||||||
const playerDetailsUpdatedMessage = new PlayerDetailsUpdatedMessage();
|
|
||||||
playerDetailsUpdatedMessage.setUserid(user.id);
|
|
||||||
playerDetailsUpdatedMessage.setDetails(playerDetails);
|
|
||||||
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,167 +1,92 @@
|
||||||
import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb";
|
import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb";
|
||||||
import {
|
import {
|
||||||
AdminGlobalMessage,
|
AdminGlobalMessage,
|
||||||
AdminMessage,
|
AdminMessage,
|
||||||
AdminPusherToBackMessage,
|
AdminPusherToBackMessage, BanMessage,
|
||||||
AdminRoomMessage,
|
ClientToServerMessage, EmptyMessage,
|
||||||
BanMessage,
|
|
||||||
BanUserMessage,
|
|
||||||
BatchToPusherMessage,
|
|
||||||
BatchToPusherRoomMessage,
|
|
||||||
EmotePromptMessage,
|
|
||||||
FollowRequestMessage,
|
|
||||||
FollowConfirmationMessage,
|
|
||||||
FollowAbortMessage,
|
|
||||||
EmptyMessage,
|
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
JoinRoomMessage,
|
JoinRoomMessage,
|
||||||
PlayGlobalMessage,
|
PlayGlobalMessage,
|
||||||
PusherToBackMessage,
|
PusherToBackMessage,
|
||||||
QueryJitsiJwtMessage,
|
QueryJitsiJwtMessage,
|
||||||
RefreshRoomPromptMessage,
|
ReportPlayerMessage,
|
||||||
RoomMessage,
|
RoomJoinedMessage,
|
||||||
SendUserMessage,
|
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
SetPlayerDetailsMessage,
|
ServerToClientMessage,
|
||||||
SilentMessage,
|
SilentMessage,
|
||||||
UserMovesMessage,
|
UserMovesMessage,
|
||||||
VariableMessage,
|
ViewportMessage,
|
||||||
WebRtcSignalToServerMessage,
|
WebRtcSignalToServerMessage,
|
||||||
WorldFullWarningToRoomMessage,
|
ZoneMessage
|
||||||
ZoneMessage,
|
|
||||||
} from "./Messages/generated/messages_pb";
|
} from "./Messages/generated/messages_pb";
|
||||||
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
import grpc, {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc";
|
||||||
import { socketManager } from "./Services/SocketManager";
|
import {Empty} from "google-protobuf/google/protobuf/empty_pb";
|
||||||
import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers";
|
import {socketManager} from "./Services/SocketManager";
|
||||||
import { User, UserSocket } from "./Model/User";
|
import {emitError} from "./Services/MessageHelpers";
|
||||||
import { GameRoom } from "./Model/GameRoom";
|
import {User, UserSocket} from "./Model/User";
|
||||||
|
import {GameRoom} from "./Model/GameRoom";
|
||||||
import Debug from "debug";
|
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<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, BatchToPusherMessage>;
|
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
|
||||||
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
|
||||||
|
|
||||||
const roomManager: IRoomManagerServer = {
|
const roomManager: IRoomManagerServer = {
|
||||||
joinRoom: (call: UserSocket): void => {
|
joinRoom: (call: UserSocket): void => {
|
||||||
console.log("joinRoom called");
|
console.log('joinRoom called');
|
||||||
|
|
||||||
let room: GameRoom | null = null;
|
let room: GameRoom|null = null;
|
||||||
let user: User | null = null;
|
let user: User|null = null;
|
||||||
|
|
||||||
call.on("data", (message: PusherToBackMessage) => {
|
call.on('data', (message: PusherToBackMessage) => {
|
||||||
(async () => {
|
try {
|
||||||
try {
|
if (room === null || user === null) {
|
||||||
if (room === null || user === null) {
|
if (message.hasJoinroommessage()) {
|
||||||
if (message.hasJoinroommessage()) {
|
socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => {
|
||||||
socketManager
|
room = gameRoom;
|
||||||
.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
|
user = myUser;
|
||||||
.then(({ room: gameRoom, user: myUser }) => {
|
});
|
||||||
if (call.writable) {
|
|
||||||
room = gameRoom;
|
|
||||||
user = myUser;
|
|
||||||
} else {
|
|
||||||
//Connection may have been closed before the init was finished, so we have to manually disconnect the user.
|
|
||||||
socketManager.leaveRoom(gameRoom, myUser);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => emitError(call, e));
|
|
||||||
} else {
|
|
||||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (message.hasJoinroommessage()) {
|
throw new Error('The first message sent MUST be of type JoinRoomMessage');
|
||||||
throw new Error("Cannot call JoinRoomMessage twice!");
|
}
|
||||||
} else if (message.hasUsermovesmessage()) {
|
} else {
|
||||||
socketManager.handleUserMovesMessage(
|
if (message.hasJoinroommessage()) {
|
||||||
room,
|
throw new Error('Cannot call JoinRoomMessage twice!');
|
||||||
user,
|
/*} else if (message.hasViewportmessage()) {
|
||||||
message.getUsermovesmessage() as UserMovesMessage
|
socketManager.handleViewport(client, message.getViewportmessage() as ViewportMessage);*/
|
||||||
);
|
} else if (message.hasUsermovesmessage()) {
|
||||||
} else if (message.hasSilentmessage()) {
|
socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage);
|
||||||
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
/*} else if (message.hasSetplayerdetailsmessage()) {
|
||||||
} else if (message.hasItemeventmessage()) {
|
socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);*/
|
||||||
socketManager.handleItemEvent(
|
} else if (message.hasSilentmessage()) {
|
||||||
room,
|
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
||||||
user,
|
} else if (message.hasItemeventmessage()) {
|
||||||
message.getItemeventmessage() as ItemEventMessage
|
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
||||||
);
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||||
} else if (message.hasVariablemessage()) {
|
socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
|
||||||
await socketManager.handleVariableEvent(
|
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||||
room,
|
socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
|
||||||
user,
|
} else if (message.hasPlayglobalmessage()) {
|
||||||
message.getVariablemessage() as VariableMessage
|
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
|
||||||
);
|
/*} else if (message.hasReportplayermessage()){
|
||||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);*/
|
||||||
socketManager.emitVideo(
|
} else if (message.hasQueryjitsijwtmessage()){
|
||||||
room,
|
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
|
||||||
user,
|
} else {
|
||||||
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
|
throw new Error('Unhandled message type');
|
||||||
);
|
|
||||||
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
|
||||||
socketManager.emitScreenSharing(
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
|
||||||
);
|
|
||||||
} 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.hasFollowrequestmessage()) {
|
|
||||||
socketManager.handleFollowRequestMessage(
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
message.getFollowrequestmessage() as FollowRequestMessage
|
|
||||||
);
|
|
||||||
} else if (message.hasFollowconfirmationmessage()) {
|
|
||||||
socketManager.handleFollowConfirmationMessage(
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
message.getFollowconfirmationmessage() as FollowConfirmationMessage
|
|
||||||
);
|
|
||||||
} else if (message.hasFollowabortmessage()) {
|
|
||||||
socketManager.handleFollowAbortMessage(
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
message.getFollowabortmessage() as FollowAbortMessage
|
|
||||||
);
|
|
||||||
} else if (message.hasSendusermessage()) {
|
|
||||||
const sendUserMessage = message.getSendusermessage();
|
|
||||||
socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage);
|
|
||||||
} else if (message.hasBanusermessage()) {
|
|
||||||
const banUserMessage = message.getBanusermessage();
|
|
||||||
socketManager.handlerBanUserMessage(room, user, banUserMessage as BanUserMessage);
|
|
||||||
} else if (message.hasSetplayerdetailsmessage()) {
|
|
||||||
const setPlayerDetailsMessage = message.getSetplayerdetailsmessage();
|
|
||||||
socketManager.handleSetPlayerDetails(
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
setPlayerDetailsMessage as SetPlayerDetailsMessage
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error("Unhandled message type");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
emitError(call, e);
|
|
||||||
call.end();
|
|
||||||
}
|
}
|
||||||
})().catch((e) => console.error(e));
|
} catch (e) {
|
||||||
|
emitError(call, e);
|
||||||
|
call.end();
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("end", () => {
|
call.on('end', () => {
|
||||||
debug("joinRoom ended");
|
debug('joinRoom ended');
|
||||||
if (user !== null && room !== null) {
|
if (user !== null && room !== null) {
|
||||||
socketManager.leaveRoom(room, user);
|
socketManager.leaveRoom(room, user);
|
||||||
}
|
}
|
||||||
|
@ -170,96 +95,84 @@ const roomManager: IRoomManagerServer = {
|
||||||
user = null;
|
user = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("error", (err: Error) => {
|
call.on('error', (err: Error) => {
|
||||||
console.error("An error occurred in joinRoom stream:", err);
|
console.error('An error occurred in joinRoom stream:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
listenZone(call: ZoneSocket): void {
|
listenZone(call: ZoneSocket): void {
|
||||||
debug("listenZone called");
|
debug('listenZone called');
|
||||||
const zoneMessage = call.request;
|
const zoneMessage = call.request;
|
||||||
|
|
||||||
socketManager
|
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
|
||||||
.catch((e) => {
|
|
||||||
emitErrorOnZoneSocket(call, e);
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("cancelled", () => {
|
call.on('cancelled', () => {
|
||||||
debug("listenZone cancelled");
|
debug('listenZone cancelled');
|
||||||
socketManager
|
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
call.end();
|
call.end();
|
||||||
});
|
})
|
||||||
|
|
||||||
call.on("close", () => {
|
/*call.on('finish', () => {
|
||||||
debug("listenZone connection closed");
|
debug('listenZone finish');
|
||||||
socketManager
|
})*/
|
||||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
call.on('close', () => {
|
||||||
.catch((e) => console.error(e));
|
debug('listenZone connection closed');
|
||||||
}).on("error", (e) => {
|
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
console.error("An error occurred in listenZone stream:", e);
|
}).on('error', (e) => {
|
||||||
socketManager
|
console.error('An error occurred in listenZone stream:', e);
|
||||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
.catch((e) => console.error(e));
|
|
||||||
call.end();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
listenRoom(call: RoomSocket): void {
|
|
||||||
debug("listenRoom called");
|
|
||||||
const roomMessage = call.request;
|
|
||||||
|
|
||||||
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
|
|
||||||
emitErrorOnRoomSocket(call, e);
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("cancelled", () => {
|
|
||||||
debug("listenRoom cancelled");
|
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
|
||||||
call.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("close", () => {
|
|
||||||
debug("listenRoom connection closed");
|
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
|
||||||
}).on("error", (e) => {
|
|
||||||
console.error("An error occurred in listenRoom stream:", e);
|
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
adminRoom(call: AdminSocket): void {
|
adminRoom(call: AdminSocket): void {
|
||||||
console.log("adminRoom called");
|
console.log('adminRoom called');
|
||||||
|
|
||||||
const admin = new Admin(call);
|
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 {
|
try {
|
||||||
if (room === null) {
|
if (room === null) {
|
||||||
if (message.hasSubscribetoroom()) {
|
if (message.hasSubscribetoroom()) {
|
||||||
const roomId = message.getSubscribetoroom();
|
const roomId = message.getSubscribetoroom();
|
||||||
socketManager
|
socketManager.handleJoinAdminRoom(admin, roomId).then((gameRoom: GameRoom) => {
|
||||||
.handleJoinAdminRoom(admin, roomId)
|
room = gameRoom;
|
||||||
.then((gameRoom: GameRoom) => {
|
});
|
||||||
room = gameRoom;
|
|
||||||
})
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
} else {
|
} 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!');
|
||||||
|
} else if (message.hasUsermovesmessage()) {
|
||||||
|
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);
|
||||||
|
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||||
|
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 {
|
||||||
|
throw new Error('Unhandled message type');
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emitError(call, e);
|
emitError(call, e);
|
||||||
call.end();
|
call.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("end", () => {
|
call.on('end', () => {
|
||||||
debug("joinRoom ended");
|
debug('joinRoom ended');
|
||||||
if (room !== null) {
|
if (room !== null) {
|
||||||
socketManager.leaveAdminRoom(room, admin);
|
socketManager.leaveAdminRoom(room, admin);
|
||||||
}
|
}
|
||||||
|
@ -267,58 +180,27 @@ const roomManager: IRoomManagerServer = {
|
||||||
room = null;
|
room = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("error", (err: Error) => {
|
call.on('error', (err: Error) => {
|
||||||
console.error("An error occurred in joinAdminRoom stream:", err);
|
console.error('An error occurred in joinAdminRoom stream:', err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
socketManager
|
|
||||||
.sendAdminMessage(
|
socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage());
|
||||||
call.request.getRoomid(),
|
|
||||||
call.request.getRecipientuuid(),
|
|
||||||
call.request.getMessage(),
|
|
||||||
call.request.getType()
|
|
||||||
)
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
throw new Error("Not implemented yet");
|
throw new Error('Not implemented yet');
|
||||||
// TODO
|
// TODO
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
// FIXME Work in progress
|
|
||||||
socketManager
|
|
||||||
.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
socketManager.banUser(call.request.getRoomid(), call.request.getRecipientuuid());
|
||||||
},
|
|
||||||
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
|
||||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
|
||||||
socketManager
|
|
||||||
.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage(), call.request.getType())
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
callback(null, new EmptyMessage());
|
|
||||||
},
|
|
||||||
sendWorldFullWarningToRoom(
|
|
||||||
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
|
|
||||||
callback: sendUnaryData<EmptyMessage>
|
|
||||||
): void {
|
|
||||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
|
||||||
socketManager.dispatchWorldFullWarning(call.request.getRoomid()).catch((e) => console.error(e));
|
|
||||||
callback(null, new EmptyMessage());
|
|
||||||
},
|
|
||||||
sendRefreshRoomPrompt(
|
|
||||||
call: ServerUnaryCall<RefreshRoomPromptMessage>,
|
|
||||||
callback: sendUnaryData<EmptyMessage>
|
|
||||||
): void {
|
|
||||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
|
||||||
socketManager.dispatchRoomRefresh(call.request.getRoomid()).catch((e) => console.error(e));
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { roomManager };
|
export {roomManager};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { App as _App, AppOptions } from "uWebSockets.js";
|
import { App as _App, AppOptions } from 'uWebSockets.js';
|
||||||
import BaseApp from "./baseapp";
|
import BaseApp from './baseapp';
|
||||||
import { extend } from "./utils";
|
import { extend } from './utils';
|
||||||
import { UwsApp } from "./types";
|
import { UwsApp } from './types';
|
||||||
|
|
||||||
class App extends (<UwsApp>_App) {
|
class App extends (<UwsApp>_App) {
|
||||||
constructor(options: AppOptions = {}) {
|
constructor(options: AppOptions = {}) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -1,111 +1,116 @@
|
||||||
/* eslint-disable */
|
import { Readable } from 'stream';
|
||||||
|
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
||||||
|
|
||||||
import { Readable } from "stream";
|
import formData from './formdata';
|
||||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
import { stob } from './utils';
|
||||||
|
import { Handler } from './types';
|
||||||
|
import {join} from "path";
|
||||||
|
|
||||||
import formData from "./formdata";
|
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
|
||||||
import { stob } from "./utils";
|
|
||||||
import { Handler } from "./types";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
|
||||||
const noOp = () => true;
|
const noOp = () => true;
|
||||||
|
|
||||||
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
||||||
const contType = req.getHeader("content-type");
|
const contType = req.getHeader('content-type');
|
||||||
|
|
||||||
res.bodyStream = function () {
|
res.bodyStream = function() {
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||||
|
|
||||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||||
// uint and then slicing is bit faster than slice and then uint
|
// 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
|
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
stream.push(null);
|
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 (contType.includes('application/json'))
|
||||||
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
|
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 {
|
class BaseApp {
|
||||||
_sockets = new Map();
|
_sockets = new Map();
|
||||||
ws!: TemplatedApp["ws"];
|
ws!: TemplatedApp['ws'];
|
||||||
get!: TemplatedApp["get"];
|
get!: TemplatedApp['get'];
|
||||||
_post!: TemplatedApp["post"];
|
_post!: TemplatedApp['post'];
|
||||||
_put!: TemplatedApp["put"];
|
_put!: TemplatedApp['put'];
|
||||||
_patch!: TemplatedApp["patch"];
|
_patch!: TemplatedApp['patch'];
|
||||||
_listen!: TemplatedApp["listen"];
|
_listen!: TemplatedApp['listen'];
|
||||||
|
|
||||||
post(pattern: string, handler: Handler) {
|
post(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
if (typeof handler !== 'function')
|
||||||
this._post(pattern, (res, req) => {
|
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
handleBody(res, req);
|
this._post(pattern, (res, req) => {
|
||||||
handler(res, req);
|
handleBody(res, req);
|
||||||
});
|
handler(res, req);
|
||||||
return this;
|
});
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
put(pattern: string, handler: Handler) {
|
put(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
if (typeof handler !== 'function')
|
||||||
this._put(pattern, (res, req) => {
|
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
handleBody(res, req);
|
this._put(pattern, (res, req) => {
|
||||||
|
handleBody(res, req);
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(pattern: string, handler: Handler) {
|
patch(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
if (typeof handler !== 'function')
|
||||||
this._patch(pattern, (res, req) => {
|
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
handleBody(res, req);
|
this._patch(pattern, (res, req) => {
|
||||||
|
handleBody(res, req);
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||||
if (typeof p === "number" && typeof h === "string") {
|
if (typeof p === 'number' && typeof h === 'string') {
|
||||||
this._listen(h, p, (socket) => {
|
this._listen(h, p, socket => {
|
||||||
this._sockets.set(p, socket);
|
this._sockets.set(p, socket);
|
||||||
if (cb === undefined) {
|
if (cb === undefined) {
|
||||||
throw new Error("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);
|
||||||
return this;
|
});
|
||||||
|
} 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)'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
close(port: null | number = null) {
|
return this;
|
||||||
if (port) {
|
}
|
||||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
|
||||||
this._sockets.delete(port);
|
close(port: null | number = null) {
|
||||||
} else {
|
if (port) {
|
||||||
this._sockets.forEach((app) => {
|
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||||
us_listen_socket_close(app);
|
this._sockets.delete(port);
|
||||||
});
|
} else {
|
||||||
this._sockets.clear();
|
this._sockets.forEach(app => {
|
||||||
}
|
us_listen_socket_close(app);
|
||||||
return this;
|
});
|
||||||
|
this._sockets.clear();
|
||||||
}
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseApp;
|
export default BaseApp;
|
||||||
|
|
|
@ -1,101 +1,100 @@
|
||||||
/* eslint-disable */
|
import { createWriteStream } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
import { createWriteStream } from "fs";
|
import Busboy from 'busboy';
|
||||||
import { join, dirname } from "path";
|
import mkdirp from 'mkdirp';
|
||||||
import Busboy from "busboy";
|
|
||||||
import mkdirp from "mkdirp";
|
|
||||||
|
|
||||||
function formData(
|
function formData(
|
||||||
contType: string,
|
contType: string,
|
||||||
options: busboy.BusboyConfig & {
|
options: busboy.BusboyConfig & {
|
||||||
abortOnLimit?: boolean;
|
abortOnLimit?: boolean;
|
||||||
tmpDir?: string;
|
tmpDir?: string;
|
||||||
onFile?: (
|
onFile?: (
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
file: NodeJS.ReadableStream,
|
file: NodeJS.ReadableStream,
|
||||||
filename: string,
|
filename: string,
|
||||||
encoding: string,
|
encoding: string,
|
||||||
mimetype: string
|
mimetype: string
|
||||||
) => string;
|
) => string;
|
||||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
filename?: (oldName: string) => string;
|
filename?: (oldName: string) => string;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
console.log("Enter form data");
|
console.log('Enter form data');
|
||||||
options.headers = {
|
options.headers = {
|
||||||
"content-type": contType,
|
'content-type': contType
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const busb = new Busboy(options);
|
const busb = new Busboy(options);
|
||||||
const ret = {};
|
const ret = {};
|
||||||
|
|
||||||
this.bodyStream().pipe(busb);
|
this.bodyStream().pipe(busb);
|
||||||
|
|
||||||
busb.on("limit", () => {
|
busb.on('limit', () => {
|
||||||
if (options.abortOnLimit) {
|
if (options.abortOnLimit) {
|
||||||
reject(Error("limit"));
|
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(
|
function setRetValue(
|
||||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
) {
|
) {
|
||||||
if (fieldname.endsWith("[]")) {
|
if (fieldname.endsWith('[]')) {
|
||||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||||
if (Array.isArray(ret[fieldname])) {
|
if (Array.isArray(ret[fieldname])) {
|
||||||
ret[fieldname].push(value);
|
ret[fieldname].push(value);
|
||||||
} else {
|
|
||||||
ret[fieldname] = [value];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(ret[fieldname])) {
|
ret[fieldname] = [value];
|
||||||
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;
|
export default formData;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js';
|
||||||
import BaseApp from "./baseapp";
|
import BaseApp from './baseapp';
|
||||||
import { extend } from "./utils";
|
import { extend } from './utils';
|
||||||
import { UwsApp } from "./types";
|
import { UwsApp } from './types';
|
||||||
|
|
||||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
class SSLApp extends (<UwsApp>_SSLApp) {
|
||||||
constructor(options: AppOptions) {
|
constructor(options: AppOptions) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SSLApp;
|
export default SSLApp;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
||||||
|
|
||||||
export type UwsApp = {
|
export type UwsApp = {
|
||||||
(options: AppOptions): TemplatedApp;
|
(options: AppOptions): TemplatedApp;
|
||||||
new (options: AppOptions): TemplatedApp;
|
new (options: AppOptions): TemplatedApp;
|
||||||
prototype: TemplatedApp;
|
prototype: TemplatedApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
||||||
|
|
|
@ -1,38 +1,37 @@
|
||||||
/* eslint-disable */
|
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(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
Object.keys(from)
|
||||||
function extend(who: any, from: any, overwrite = true) {
|
);
|
||||||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
|
ownProps.forEach(prop => {
|
||||||
ownProps.forEach((prop) => {
|
if (prop === 'constructor' || from[prop] === undefined) return;
|
||||||
if (prop === "constructor" || from[prop] === undefined) return;
|
if (who[prop] && overwrite) {
|
||||||
if (who[prop] && overwrite) {
|
who[`_${prop}`] = who[prop];
|
||||||
who[`_${prop}`] = who[prop];
|
}
|
||||||
}
|
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who);
|
||||||
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
|
else who[prop] = from[prop];
|
||||||
else who[prop] = from[prop];
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stob(stream: ReadStream): Promise<Buffer> {
|
function stob(stream: ReadStream): Promise<Buffer> {
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
const buffers: Buffer[] = [];
|
const buffers: Buffer[] = [];
|
||||||
stream.on("data", buffers.push.bind(buffers));
|
stream.on('data', buffers.push.bind(buffers));
|
||||||
|
|
||||||
stream.on("end", () => {
|
stream.on('end', () => {
|
||||||
switch (buffers.length) {
|
switch (buffers.length) {
|
||||||
case 0:
|
case 0:
|
||||||
resolve(Buffer.allocUnsafe(0));
|
resolve(Buffer.allocUnsafe(0));
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
resolve(buffers[0]);
|
resolve(buffers[0]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
resolve(Buffer.concat(buffers));
|
resolve(Buffer.concat(buffers));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { extend, stob };
|
export { extend, stob };
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { parse } from "query-string";
|
import { parse } from 'query-string';
|
||||||
import { HttpRequest } from "uWebSockets.js";
|
import { HttpRequest } from 'uWebSockets.js';
|
||||||
import App from "./server/app";
|
import App from './server/app';
|
||||||
import SSLApp from "./server/sslapp";
|
import SSLApp from './server/sslapp';
|
||||||
import * as types from "./server/types";
|
import * as types from './server/types';
|
||||||
|
|
||||||
const getQuery = (req: HttpRequest) => {
|
const getQuery = (req: HttpRequest) => {
|
||||||
return parse(req.getQuery());
|
return parse(req.getQuery());
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App, SSLApp, getQuery };
|
export { App, SSLApp, getQuery };
|
||||||
export * from "./server/types";
|
export * from './server/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
App,
|
App,
|
||||||
SSLApp,
|
SSLApp,
|
||||||
getQuery,
|
getQuery,
|
||||||
...types,
|
...types
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,30 +1,115 @@
|
||||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData";
|
import {v4} from "uuid";
|
||||||
import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect";
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchMemberDataByUuidResponse {
|
||||||
|
uuid: string;
|
||||||
|
tags: string[];
|
||||||
|
textures: CharacterTexture[];
|
||||||
|
messages: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
class AdminApi {
|
class AdminApi {
|
||||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
|
||||||
|
async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<AdminApiData> {
|
||||||
if (!ADMIN_API_URL) {
|
if (!ADMIN_API_URL) {
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
return Promise.reject('No admin backoffice set!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const params: { playUri: string } = {
|
const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
|
||||||
playUri,
|
organizationSlug,
|
||||||
|
worldSlug
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
if (roomSlug) {
|
||||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
params.roomSlug = roomSlug;
|
||||||
params,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) {
|
|
||||||
console.error("Unexpected answer from the /api/map admin endpoint.", res.data);
|
|
||||||
throw new Error("Unexpected answer from the /api/map admin endpoint.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const res = await Axios.get(ADMIN_API_URL + '/api/map',
|
||||||
|
{
|
||||||
|
headers: {"Authorization": `${ADMIN_API_TOKEN}`},
|
||||||
|
params
|
||||||
|
}
|
||||||
|
)
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchMemberDataByUuid(uuid: string): Promise<FetchMemberDataByUuidResponse> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject('No admin backoffice set!');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid,
|
||||||
|
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||||
|
)
|
||||||
|
return res.data;
|
||||||
|
} catch (e) {
|
||||||
|
if (e?.response?.status == 404) {
|
||||||
|
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
|
||||||
|
console.warn('Cannot find user with uuid "'+uuid+'". Performing an anonymous login instead.');
|
||||||
|
return {
|
||||||
|
uuid: v4(),
|
||||||
|
tags: [],
|
||||||
|
textures: [],
|
||||||
|
messages: [],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject('No admin backoffice set!');
|
||||||
|
}
|
||||||
|
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||||
|
const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken,
|
||||||
|
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||||
|
)
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject('No admin backoffice set!');
|
||||||
|
}
|
||||||
|
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||||
|
const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken,
|
||||||
|
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||||
|
)
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) {
|
||||||
|
return Axios.post(`${ADMIN_API_URL}/api/report`, {
|
||||||
|
reportedUserUuid,
|
||||||
|
reportedUserComment,
|
||||||
|
reporterUserUuid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {"Authorization": `${ADMIN_API_TOKEN}`}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const adminApi = new AdminApi();
|
export const adminApi = new AdminApi();
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import * as tg from "generic-type-guard";
|
|
||||||
|
|
||||||
export const isCharacterTexture = new tg.IsInterface()
|
|
||||||
.withProperties({
|
|
||||||
id: tg.isNumber,
|
|
||||||
level: tg.isNumber,
|
|
||||||
url: tg.isString,
|
|
||||||
rights: tg.isString,
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
export type CharacterTexture = tg.GuardedType<typeof isCharacterTexture>;
|
|
|
@ -1,21 +0,0 @@
|
||||||
import * as tg from "generic-type-guard";
|
|
||||||
import { isCharacterTexture } from "./CharacterTexture";
|
|
||||||
import { isAny, isNumber } from "generic-type-guard";
|
|
||||||
|
|
||||||
/*const isNumericEnum =
|
|
||||||
<T extends { [n: number]: string }>(vs: T) =>
|
|
||||||
(v: any): v is T =>
|
|
||||||
typeof v === "number" && v in vs;*/
|
|
||||||
|
|
||||||
export const isMapDetailsData = new tg.IsInterface()
|
|
||||||
.withProperties({
|
|
||||||
mapUrl: tg.isString,
|
|
||||||
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
|
||||||
tags: tg.isArray(tg.isString),
|
|
||||||
textures: tg.isArray(isCharacterTexture),
|
|
||||||
})
|
|
||||||
.withOptionalProperties({
|
|
||||||
roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
|
|
@ -1,8 +0,0 @@
|
||||||
import * as tg from "generic-type-guard";
|
|
||||||
|
|
||||||
export const isRoomRedirect = new tg.IsInterface()
|
|
||||||
.withProperties({
|
|
||||||
redirectUrl: tg.isString,
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
export type RoomRedirect = tg.GuardedType<typeof isRoomRedirect>;
|
|
|
@ -1,3 +1,3 @@
|
||||||
export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
|
export const arrayIntersect = (array1: string[], array2: string[]) : boolean => {
|
||||||
return array1.filter((value) => array2.includes(value)).length > 0;
|
return array1.filter(value => array2.includes(value)).length > 0;
|
||||||
};
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { EventEmitter } from "events";
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
const clientJoinEvent = "clientJoin";
|
const clientJoinEvent = 'clientJoin';
|
||||||
const clientLeaveEvent = "clientLeave";
|
const clientLeaveEvent = 'clientLeave';
|
||||||
|
|
||||||
class ClientEventsEmitter extends EventEmitter {
|
class ClientEventsEmitter extends EventEmitter {
|
||||||
emitClientJoin(clientUUid: string, roomId: string): void {
|
emitClientJoin(clientUUid: string, roomId: string): void {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
|
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
function secNSec2ms(secNSec: Array<number> | number) {
|
function secNSec2ms(secNSec: Array<number>|number) {
|
||||||
if (Array.isArray(secNSec)) {
|
if (Array.isArray(secNSec)) {
|
||||||
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
||||||
}
|
}
|
||||||
|
@ -12,17 +12,17 @@ class CpuTracker {
|
||||||
private overHeating: boolean = false;
|
private overHeating: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let time = process.hrtime.bigint();
|
let time = process.hrtime.bigint()
|
||||||
let usage = process.cpuUsage();
|
let usage = process.cpuUsage()
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const elapTime = process.hrtime.bigint();
|
const elapTime = process.hrtime.bigint();
|
||||||
const elapUsage = process.cpuUsage(usage);
|
const elapUsage = process.cpuUsage(usage)
|
||||||
usage = process.cpuUsage();
|
usage = process.cpuUsage()
|
||||||
|
|
||||||
const elapTimeMS = elapTime - time;
|
const elapTimeMS = elapTime - time;
|
||||||
const elapUserMS = secNSec2ms(elapUsage.user);
|
const elapUserMS = secNSec2ms(elapUsage.user)
|
||||||
const elapSystMS = secNSec2ms(elapUsage.system);
|
const elapSystMS = secNSec2ms(elapUsage.system)
|
||||||
this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
|
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000)
|
||||||
|
|
||||||
time = elapTime;
|
time = elapTime;
|
||||||
|
|
||||||
|
|
|
@ -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
|
//this class should manage all the custom metrics used by prometheus
|
||||||
class GaugeManager {
|
class GaugeManager {
|
||||||
|
@ -10,29 +10,29 @@ class GaugeManager {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.nbRoomsGauge = new Gauge({
|
this.nbRoomsGauge = new Gauge({
|
||||||
name: "workadventure_nb_rooms",
|
name: 'workadventure_nb_rooms',
|
||||||
help: "Number of active rooms",
|
help: 'Number of active rooms'
|
||||||
});
|
});
|
||||||
this.nbClientsGauge = new Gauge({
|
this.nbClientsGauge = new Gauge({
|
||||||
name: "workadventure_nb_sockets",
|
name: 'workadventure_nb_sockets',
|
||||||
help: "Number of connected sockets",
|
help: 'Number of connected sockets',
|
||||||
labelNames: [],
|
labelNames: [ ]
|
||||||
});
|
});
|
||||||
this.nbClientsPerRoomGauge = new Gauge({
|
this.nbClientsPerRoomGauge = new Gauge({
|
||||||
name: "workadventure_nb_clients_per_room",
|
name: 'workadventure_nb_clients_per_room',
|
||||||
help: "Number of clients per room",
|
help: 'Number of clients per room',
|
||||||
labelNames: ["room"],
|
labelNames: [ 'room' ]
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nbGroupsPerRoomCounter = new Counter({
|
this.nbGroupsPerRoomCounter = new Counter({
|
||||||
name: "workadventure_counter_groups_per_room",
|
name: 'workadventure_counter_groups_per_room',
|
||||||
help: "Counter of groups per room",
|
help: 'Counter of groups per room',
|
||||||
labelNames: ["room"],
|
labelNames: [ 'room' ]
|
||||||
});
|
});
|
||||||
this.nbGroupsPerRoomGauge = new Gauge({
|
this.nbGroupsPerRoomGauge = new Gauge({
|
||||||
name: "workadventure_nb_groups_per_room",
|
name: 'workadventure_nb_groups_per_room',
|
||||||
help: "Number of groups per room",
|
help: 'Number of groups per room',
|
||||||
labelNames: ["room"],
|
labelNames: [ 'room' ]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,15 @@ class GaugeManager {
|
||||||
this.nbClientsGauge.dec();
|
this.nbClientsGauge.dec();
|
||||||
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
incNbGroupsPerRoomGauge(roomId: string): void {
|
||||||
|
this.nbGroupsPerRoomCounter.inc({ room: roomId })
|
||||||
|
this.nbGroupsPerRoomGauge.inc({ room: roomId })
|
||||||
|
}
|
||||||
|
|
||||||
|
decNbGroupsPerRoomGauge(roomId: string): void {
|
||||||
|
this.nbGroupsPerRoomGauge.dec({ room: roomId })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const gaugeManager = new GaugeManager();
|
export const gaugeManager = new GaugeManager();
|
|
@ -1 +0,0 @@
|
||||||
export class LocalUrlError extends Error {}
|
|
|
@ -1,70 +0,0 @@
|
||||||
import Axios from "axios";
|
|
||||||
import ipaddr from "ipaddr.js";
|
|
||||||
import { Resolver } from "dns";
|
|
||||||
import { promisify } from "util";
|
|
||||||
import { LocalUrlError } from "./LocalUrlError";
|
|
||||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
|
|
||||||
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
|
||||||
import { STORE_VARIABLES_FOR_LOCAL_MAPS } from "../Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
class MapFetcher {
|
|
||||||
async fetchMap(mapUrl: string): Promise<ITiledMap> {
|
|
||||||
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
|
|
||||||
|
|
||||||
if ((await this.isLocalUrl(mapUrl)) && !STORE_VARIABLES_FOR_LOCAL_MAPS) {
|
|
||||||
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
|
|
||||||
// returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
|
|
||||||
// target to different servers (and one could trick Axios.get into loading resources on the internal network
|
|
||||||
// despite isLocalUrl checking that.
|
|
||||||
// We can deem this problem not that important because:
|
|
||||||
// - We make sure we are only passing "GET" requests
|
|
||||||
// - The result of the query is never displayed to the end user
|
|
||||||
const res = await Axios.get(mapUrl, {
|
|
||||||
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
|
|
||||||
timeout: 10000, // Timeout after 10 seconds
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isTiledMap(res.data)) {
|
|
||||||
//TODO fixme
|
|
||||||
//throw new Error("Invalid map format for map " + mapUrl);
|
|
||||||
console.error("Invalid map format for map " + mapUrl);
|
|
||||||
}
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-unsafe-return */
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the domain name is localhost of *.localhost
|
|
||||||
* Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x)
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
async isLocalUrl(url: string): Promise<boolean> {
|
|
||||||
const urlObj = new URL(url);
|
|
||||||
if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let addresses = [];
|
|
||||||
if (!ipaddr.isValid(urlObj.hostname)) {
|
|
||||||
const resolver = new Resolver();
|
|
||||||
addresses = await promisify(resolver.resolve).bind(resolver)(urlObj.hostname);
|
|
||||||
} else {
|
|
||||||
addresses = [urlObj.hostname];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const address of addresses) {
|
|
||||||
const addr = ipaddr.parse(address);
|
|
||||||
if (addr.range() !== "unicast") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mapFetcher = new MapFetcher();
|
|
|
@ -1,28 +1,7 @@
|
||||||
import {
|
import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb";
|
||||||
BatchMessage,
|
import {UserSocket} from "_Model/User";
|
||||||
BatchToPusherMessage,
|
|
||||||
BatchToPusherRoomMessage,
|
|
||||||
ErrorMessage,
|
|
||||||
ServerToClientMessage,
|
|
||||||
SubToPusherMessage,
|
|
||||||
SubToPusherRoomMessage,
|
|
||||||
} from "../Messages/generated/messages_pb";
|
|
||||||
import { UserSocket } from "_Model/User";
|
|
||||||
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
|
||||||
|
|
||||||
function getMessageFromError(error: unknown): string {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
} else if (typeof error === "string") {
|
|
||||||
return error;
|
|
||||||
} else {
|
|
||||||
return "Unknown error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emitError(Client: UserSocket, error: unknown): void {
|
|
||||||
const message = getMessageFromError(error);
|
|
||||||
|
|
||||||
|
export function emitError(Client: UserSocket, message: string): void {
|
||||||
const errorMessage = new ErrorMessage();
|
const errorMessage = new ErrorMessage();
|
||||||
errorMessage.setMessage(message);
|
errorMessage.setMessage(message);
|
||||||
|
|
||||||
|
@ -30,45 +9,7 @@ export function emitError(Client: UserSocket, error: unknown): void {
|
||||||
serverToClientMessage.setErrormessage(errorMessage);
|
serverToClientMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
//if (!Client.disconnecting) {
|
//if (!Client.disconnecting) {
|
||||||
Client.write(serverToClientMessage);
|
Client.write(serverToClientMessage);
|
||||||
//}
|
|
||||||
console.warn(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emitErrorOnRoomSocket(Client: RoomSocket, error: unknown): void {
|
|
||||||
console.error(error);
|
|
||||||
const message = getMessageFromError(error);
|
|
||||||
|
|
||||||
const errorMessage = new ErrorMessage();
|
|
||||||
errorMessage.setMessage(message);
|
|
||||||
|
|
||||||
const subToPusherRoomMessage = new SubToPusherRoomMessage();
|
|
||||||
subToPusherRoomMessage.setErrormessage(errorMessage);
|
|
||||||
|
|
||||||
const batchToPusherMessage = new BatchToPusherRoomMessage();
|
|
||||||
batchToPusherMessage.addPayload(subToPusherRoomMessage);
|
|
||||||
|
|
||||||
//if (!Client.disconnecting) {
|
|
||||||
Client.write(batchToPusherMessage);
|
|
||||||
//}
|
|
||||||
console.warn(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emitErrorOnZoneSocket(Client: ZoneSocket, error: unknown): void {
|
|
||||||
console.error(error);
|
|
||||||
const message = getMessageFromError(error);
|
|
||||||
|
|
||||||
const errorMessage = new ErrorMessage();
|
|
||||||
errorMessage.setMessage(message);
|
|
||||||
|
|
||||||
const subToPusherMessage = new SubToPusherMessage();
|
|
||||||
subToPusherMessage.setErrormessage(errorMessage);
|
|
||||||
|
|
||||||
const batchToPusherMessage = new BatchToPusherMessage();
|
|
||||||
batchToPusherMessage.addPayload(subToPusherMessage);
|
|
||||||
|
|
||||||
//if (!Client.disconnecting) {
|
|
||||||
Client.write(batchToPusherMessage);
|
|
||||||
//}
|
//}
|
||||||
console.warn(message);
|
console.warn(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { ClientOpts, createClient, RedisClient } from "redis";
|
|
||||||
import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
let redisClient: RedisClient | null = null;
|
|
||||||
|
|
||||||
if (REDIS_HOST !== undefined) {
|
|
||||||
const config: ClientOpts = {
|
|
||||||
host: REDIS_HOST,
|
|
||||||
port: REDIS_PORT,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (REDIS_PASSWORD) {
|
|
||||||
config.password = REDIS_PASSWORD;
|
|
||||||
}
|
|
||||||
|
|
||||||
redisClient = createClient(config);
|
|
||||||
|
|
||||||
redisClient.on("error", (err) => {
|
|
||||||
console.error("Error connecting to Redis:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { redisClient };
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { promisify } from "util";
|
|
||||||
import { RedisClient } from "redis";
|
|
||||||
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class in charge of saving/loading variables from the data store
|
|
||||||
*/
|
|
||||||
export class RedisVariablesRepository implements VariablesRepositoryInterface {
|
|
||||||
private readonly hgetall: OmitThisParameter<(arg1: string) => Promise<{ [p: string]: string }>>;
|
|
||||||
private readonly hset: OmitThisParameter<(arg1: [string, ...string[]]) => Promise<number>>;
|
|
||||||
private readonly hdel: OmitThisParameter<(arg1: string, arg2: string) => Promise<number>>;
|
|
||||||
|
|
||||||
constructor(private redisClient: RedisClient) {
|
|
||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
|
||||||
this.hgetall = promisify(redisClient.hgetall).bind(redisClient);
|
|
||||||
this.hset = promisify(redisClient.hset).bind(redisClient);
|
|
||||||
this.hdel = promisify(redisClient.hdel).bind(redisClient);
|
|
||||||
/* eslint-enable @typescript-eslint/unbound-method */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all variables for a room.
|
|
||||||
*
|
|
||||||
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
|
||||||
*/
|
|
||||||
async loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
|
||||||
return this.hgetall(roomUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
|
||||||
// The value is passed to JSON.stringify client side. If value is "undefined", JSON.stringify returns "undefined"
|
|
||||||
// which is translated to empty string when fetching the value in the pusher.
|
|
||||||
// Therefore, empty string server side == undefined client side.
|
|
||||||
if (value === "") {
|
|
||||||
return this.hdel(roomUrl, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: SLOW WRITING EVERY 2 SECONDS WITH A TIMEOUT
|
|
||||||
|
|
||||||
// @ts-ignore See https://stackoverflow.com/questions/63539317/how-do-i-use-hmset-with-node-promisify
|
|
||||||
return this.hset(roomUrl, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { RedisVariablesRepository } from "./RedisVariablesRepository";
|
|
||||||
import { redisClient } from "../RedisClient";
|
|
||||||
import { VoidVariablesRepository } from "./VoidVariablesRepository";
|
|
||||||
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
|
||||||
|
|
||||||
let variablesRepository: VariablesRepositoryInterface;
|
|
||||||
if (!redisClient) {
|
|
||||||
console.warn("WARNING: Redis isnot configured. No variables will be persisted.");
|
|
||||||
variablesRepository = new VoidVariablesRepository();
|
|
||||||
} else {
|
|
||||||
variablesRepository = new RedisVariablesRepository(redisClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { variablesRepository };
|
|
|
@ -1,10 +0,0 @@
|
||||||
export interface VariablesRepositoryInterface {
|
|
||||||
/**
|
|
||||||
* Load all variables for a room.
|
|
||||||
*
|
|
||||||
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
|
||||||
*/
|
|
||||||
loadVariables(roomUrl: string): Promise<{ [key: string]: string }>;
|
|
||||||
|
|
||||||
saveVariable(roomUrl: string, key: string, value: string): Promise<number>;
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock class in charge of NOT saving/loading variables from the data store
|
|
||||||
*/
|
|
||||||
export class VoidVariablesRepository implements VariablesRepositoryInterface {
|
|
||||||
loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
|
||||||
return Promise.resolve({});
|
|
||||||
}
|
|
||||||
|
|
||||||
saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
|
||||||
return Promise.resolve(0);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +0,0 @@
|
||||||
/**
|
|
||||||
* Errors related to variable handling.
|
|
||||||
*/
|
|
||||||
export class VariableError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
Object.setPrototypeOf(this, VariableError.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,229 +0,0 @@
|
||||||
/**
|
|
||||||
* Handles variables shared between the scripting API and the server.
|
|
||||||
*/
|
|
||||||
import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist";
|
|
||||||
import { User } from "_Model/User";
|
|
||||||
import { variablesRepository } from "./Repository/VariablesRepository";
|
|
||||||
import { redisClient } from "./RedisClient";
|
|
||||||
import { VariableError } from "./VariableError";
|
|
||||||
|
|
||||||
interface Variable {
|
|
||||||
defaultValue?: string;
|
|
||||||
persist?: boolean;
|
|
||||||
readableBy?: string;
|
|
||||||
writableBy?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VariablesManager {
|
|
||||||
/**
|
|
||||||
* The actual values of the variables for the current room
|
|
||||||
*/
|
|
||||||
private _variables = new Map<string, string>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of variables that are allowed
|
|
||||||
*/
|
|
||||||
private variableObjects: Map<string, Variable> | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param map The map can be "null" if it is hosted on a private network. In this case, we assume this is a test setup and bypass any server-side checks.
|
|
||||||
*/
|
|
||||||
constructor(private roomUrl: string, private map: ITiledMap | null) {
|
|
||||||
// We initialize the list of variable object at room start. The objects cannot be edited later
|
|
||||||
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
|
||||||
if (map) {
|
|
||||||
this.variableObjects = VariablesManager.findVariablesInMap(map);
|
|
||||||
|
|
||||||
// Let's initialize default values
|
|
||||||
for (const [name, variableObject] of this.variableObjects.entries()) {
|
|
||||||
if (variableObject.defaultValue !== undefined) {
|
|
||||||
this._variables.set(name, variableObject.defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Let's load data from the Redis backend.
|
|
||||||
*/
|
|
||||||
public async init(): Promise<VariablesManager> {
|
|
||||||
if (!this.shouldPersist()) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
const variables = await variablesRepository.loadVariables(this.roomUrl);
|
|
||||||
for (const key in variables) {
|
|
||||||
// Let's only set variables if they are in the map (if the map has changed, maybe stored variables do not exist anymore)
|
|
||||||
if (this.variableObjects) {
|
|
||||||
const variableObject = this.variableObjects.get(key);
|
|
||||||
if (variableObject === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!variableObject.persist) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._variables.set(key, variables[key]);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if saving should be enabled, and false otherwise.
|
|
||||||
*
|
|
||||||
* Saving is enabled if REDIS_HOST is set
|
|
||||||
* unless we are editing a local map
|
|
||||||
* unless we are in dev mode in which case it is ok to save
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private shouldPersist(): boolean {
|
|
||||||
return redisClient !== null && (this.map !== null || process.env.NODE_ENV === "development");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
|
||||||
const objects = new Map<string, Variable>();
|
|
||||||
for (const layer of map.layers) {
|
|
||||||
this.recursiveFindVariablesInLayer(layer, objects);
|
|
||||||
}
|
|
||||||
return objects;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): void {
|
|
||||||
if (layer.type === "objectgroup") {
|
|
||||||
for (const object of layer.objects) {
|
|
||||||
if (object.type === "variable") {
|
|
||||||
if (object.template) {
|
|
||||||
console.warn(
|
|
||||||
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We store a copy of the object (to make it immutable)
|
|
||||||
objects.set(object.name as string, this.iTiledObjectToVariable(object));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (layer.type === "group") {
|
|
||||||
for (const innerLayer of layer.layers as ITiledMapLayer[]) {
|
|
||||||
this.recursiveFindVariablesInLayer(innerLayer, objects);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
|
|
||||||
const variable: Variable = {};
|
|
||||||
|
|
||||||
if (object.properties) {
|
|
||||||
for (const property of object.properties) {
|
|
||||||
const value = property.value as unknown;
|
|
||||||
switch (property.name) {
|
|
||||||
case "default":
|
|
||||||
variable.defaultValue = JSON.stringify(value);
|
|
||||||
break;
|
|
||||||
case "persist":
|
|
||||||
if (typeof value !== "boolean") {
|
|
||||||
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
|
||||||
}
|
|
||||||
variable.persist = value;
|
|
||||||
break;
|
|
||||||
case "writableBy":
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
throw new Error(
|
|
||||||
'The writableBy property of variable "' + object.name + '" must be a string'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
variable.writableBy = value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "readableBy":
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
throw new Error(
|
|
||||||
'The readableBy property of variable "' + object.name + '" must be a string'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
variable.readableBy = value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return variable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the variable.
|
|
||||||
*
|
|
||||||
* Returns who is allowed to read the variable (the readableby property) or "undefined" if anyone can read it.
|
|
||||||
* Also, returns "false" if the variable was not modified (because we set it to the value it already has)
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* @param value
|
|
||||||
* @param user
|
|
||||||
*/
|
|
||||||
setVariable(name: string, value: string, user: User): string | undefined | false {
|
|
||||||
let readableBy: string | undefined;
|
|
||||||
let variableObject: Variable | undefined;
|
|
||||||
if (this.variableObjects) {
|
|
||||||
variableObject = this.variableObjects.get(name);
|
|
||||||
if (variableObject === undefined) {
|
|
||||||
throw new VariableError(
|
|
||||||
'Trying to set a variable "' + name + '" that is not defined as an object in the map.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) {
|
|
||||||
throw new VariableError(
|
|
||||||
'Trying to set a variable "' +
|
|
||||||
name +
|
|
||||||
'". User "' +
|
|
||||||
user.name +
|
|
||||||
'" does not have sufficient permission. Required tag: "' +
|
|
||||||
variableObject.writableBy +
|
|
||||||
'". User tags: ' +
|
|
||||||
user.tags.join(", ") +
|
|
||||||
"."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
readableBy = variableObject.readableBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the value is not modified, return false
|
|
||||||
if (this._variables.get(name) === value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._variables.set(name, value);
|
|
||||||
|
|
||||||
if (variableObject !== undefined && variableObject.persist) {
|
|
||||||
variablesRepository
|
|
||||||
.saveVariable(this.roomUrl, name, value)
|
|
||||||
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
return readableBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getVariablesForTags(tags: string[]): Map<string, string> {
|
|
||||||
if (this.variableObjects === undefined) {
|
|
||||||
return this._variables;
|
|
||||||
}
|
|
||||||
|
|
||||||
const readableVariables = new Map<string, string>();
|
|
||||||
|
|
||||||
for (const [key, value] of this._variables.entries()) {
|
|
||||||
const variableObject = this.variableObjects.get(key);
|
|
||||||
if (variableObject === undefined) {
|
|
||||||
throw new Error('Unexpected variable "' + key + '" found has no associated variableObject.');
|
|
||||||
}
|
|
||||||
if (!variableObject.readableBy || tags.includes(variableObject.readableBy)) {
|
|
||||||
readableVariables.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return readableVariables;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +1,54 @@
|
||||||
import "jasmine";
|
import "jasmine";
|
||||||
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
|
import {ConnectCallback, DisconnectCallback, GameRoom} from "../src/Model/GameRoom";
|
||||||
import { Point } from "../src/Model/Websocket/MessageUserPosition";
|
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||||
import { Group } from "../src/Model/Group";
|
import {Group} from "../src/Model/Group";
|
||||||
import { User, UserSocket } from "_Model/User";
|
import {User, UserSocket} from "_Model/User";
|
||||||
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
|
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
import { EmoteCallback } from "_Model/Zone";
|
|
||||||
|
|
||||||
function createMockUser(userId: number): User {
|
function createMockUser(userId: number): User {
|
||||||
return {
|
return {
|
||||||
userId,
|
userId
|
||||||
} as unknown as User;
|
} as unknown as User;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMockUserSocket(): UserSocket {
|
function createMockUserSocket(): UserSocket {
|
||||||
return {} as unknown as UserSocket;
|
return {
|
||||||
|
} as unknown as UserSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage {
|
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage
|
||||||
|
{
|
||||||
const positionMessage = new PositionMessage();
|
const positionMessage = new PositionMessage();
|
||||||
positionMessage.setX(x);
|
positionMessage.setX(x);
|
||||||
positionMessage.setY(y);
|
positionMessage.setY(y);
|
||||||
positionMessage.setDirection(Direction.DOWN);
|
positionMessage.setDirection(Direction.DOWN);
|
||||||
positionMessage.setMoving(false);
|
positionMessage.setMoving(false);
|
||||||
const joinRoomMessage = new JoinRoomMessage();
|
const joinRoomMessage = new JoinRoomMessage();
|
||||||
joinRoomMessage.setUseruuid("1");
|
joinRoomMessage.setUseruuid('1');
|
||||||
joinRoomMessage.setIpaddress("10.0.0.2");
|
joinRoomMessage.setName('foo');
|
||||||
joinRoomMessage.setName("foo");
|
joinRoomMessage.setRoomid('_/global/test.json');
|
||||||
joinRoomMessage.setRoomid("_/global/test.json");
|
|
||||||
joinRoomMessage.setPositionmessage(positionMessage);
|
joinRoomMessage.setPositionmessage(positionMessage);
|
||||||
return joinRoomMessage;
|
return joinRoomMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {};
|
|
||||||
|
|
||||||
describe("GameRoom", () => {
|
describe("GameRoom", () => {
|
||||||
it("should connect user1 and user2", async () => {
|
it("should connect user1 and user2", () => {
|
||||||
let connectCalledNumber: number = 0;
|
let connectCalledNumber: number = 0;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalledNumber++;
|
connectCalledNumber++;
|
||||||
};
|
}
|
||||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||||
|
|
||||||
const world = await GameRoom.create(
|
}
|
||||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
160,
|
|
||||||
160,
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
emote,
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
|
|
||||||
|
|
||||||
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||||
|
|
||||||
|
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 500, 100));
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(261, 100));
|
world.updatePosition(user2, new Point(261, 100));
|
||||||
|
|
||||||
|
@ -71,35 +62,26 @@ describe("GameRoom", () => {
|
||||||
expect(connectCalledNumber).toBe(2);
|
expect(connectCalledNumber).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should connect 3 users", async () => {
|
it("should connect 3 users", () => {
|
||||||
let connectCalled: boolean = false;
|
let connectCalled: boolean = false;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalled = true;
|
connectCalled = true;
|
||||||
};
|
}
|
||||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||||
|
|
||||||
const world = await GameRoom.create(
|
}
|
||||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
160,
|
|
||||||
160,
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
emote,
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100));
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||||
|
|
||||||
|
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 200, 100));
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
connectCalled = false;
|
connectCalled = false;
|
||||||
|
|
||||||
// baz joins at the outer limit of the group
|
// baz joins at the outer limit of the group
|
||||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100));
|
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 311, 100));
|
||||||
|
|
||||||
expect(connectCalled).toBe(false);
|
expect(connectCalled).toBe(false);
|
||||||
|
|
||||||
|
@ -108,41 +90,31 @@ describe("GameRoom", () => {
|
||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should disconnect user1 and user2", async () => {
|
it("should disconnect user1 and user2", () => {
|
||||||
let connectCalled: boolean = false;
|
let connectCalled: boolean = false;
|
||||||
let disconnectCallNumber: number = 0;
|
let disconnectCallNumber: number = 0;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalled = true;
|
connectCalled = true;
|
||||||
};
|
}
|
||||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||||
disconnectCallNumber++;
|
disconnectCallNumber++;
|
||||||
};
|
}
|
||||||
|
|
||||||
const world = await GameRoom.create(
|
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
160,
|
|
||||||
160,
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
emote,
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||||
|
|
||||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
|
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 259, 100));
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
expect(disconnectCallNumber).toBe(0);
|
expect(disconnectCallNumber).toBe(0);
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
|
world.updatePosition(user2, new Point(100+160+160+1, 100));
|
||||||
|
|
||||||
expect(disconnectCallNumber).toBe(2);
|
expect(disconnectCallNumber).toBe(2);
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(262, 100));
|
world.updatePosition(user2, new Point(262, 100));
|
||||||
expect(disconnectCallNumber).toBe(2);
|
expect(disconnectCallNumber).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
})
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { arrayIntersect } from "../src/Services/ArrayHelper";
|
|
||||||
import { mapFetcher } from "../src/Services/MapFetcher";
|
|
||||||
|
|
||||||
describe("MapFetcher", () => {
|
|
||||||
it("should return true on localhost ending URLs", async () => {
|
|
||||||
expect(await mapFetcher.isLocalUrl("https://localhost")).toBeTrue();
|
|
||||||
expect(await mapFetcher.isLocalUrl("https://foo.localhost")).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return true on DNS resolving to a local domain", async () => {
|
|
||||||
expect(await mapFetcher.isLocalUrl("https://127.0.0.1.nip.io")).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return true on an IP resolving to a local domain", async () => {
|
|
||||||
expect(await mapFetcher.isLocalUrl("https://127.0.0.1")).toBeTrue();
|
|
||||||
expect(await mapFetcher.isLocalUrl("https://192.168.0.1")).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false on an IP resolving to a global domain", async () => {
|
|
||||||
expect(await mapFetcher.isLocalUrl("https://51.12.42.42")).toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false on an DNS resolving to a global domain", async () => {
|
|
||||||
expect(await mapFetcher.isLocalUrl("https://maps.workadventu.re")).toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw error on invalid domain", async () => {
|
|
||||||
await expectAsync(
|
|
||||||
mapFetcher.isLocalUrl("https://this.domain.name.doesnotexistfoobgjkgfdjkgldf.com")
|
|
||||||
).toBeRejected();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,10 +1,15 @@
|
||||||
import "jasmine";
|
import "jasmine";
|
||||||
import { PositionNotifier } from "../src/Model/PositionNotifier";
|
import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom";
|
||||||
import { User, UserSocket } from "../src/Model/User";
|
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Group } from "../src/Model/Group";
|
||||||
import { Movable } from "_Model/Movable";
|
import {PositionNotifier} from "../src/Model/PositionNotifier";
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import {User, UserSocket} from "../src/Model/User";
|
||||||
import { ZoneSocket } from "../src/RoomManager";
|
import {PointInterface} from "../src/Model/Websocket/PointInterface";
|
||||||
|
import {Zone} from "_Model/Zone";
|
||||||
|
import {Movable} from "_Model/Movable";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
import {ZoneSocket} from "../src/RoomManager";
|
||||||
|
|
||||||
|
|
||||||
describe("PositionNotifier", () => {
|
describe("PositionNotifier", () => {
|
||||||
it("should receive notifications when player moves", () => {
|
it("should receive notifications when player moves", () => {
|
||||||
|
@ -12,59 +17,27 @@ describe("PositionNotifier", () => {
|
||||||
let moveTriggered = false;
|
let moveTriggered = false;
|
||||||
let leaveTriggered = false;
|
let leaveTriggered = false;
|
||||||
|
|
||||||
const positionNotifier = new PositionNotifier(
|
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => {
|
||||||
300,
|
enterTriggered = true;
|
||||||
300,
|
}, (thing: Movable, position: PositionInterface) => {
|
||||||
(thing: Movable) => {
|
moveTriggered = true;
|
||||||
enterTriggered = true;
|
}, (thing: Movable) => {
|
||||||
},
|
leaveTriggered = true;
|
||||||
(thing: Movable, position: PositionInterface) => {
|
});
|
||||||
moveTriggered = true;
|
|
||||||
},
|
|
||||||
(thing: Movable) => {
|
|
||||||
leaveTriggered = true;
|
|
||||||
},
|
|
||||||
() => {},
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
|
|
||||||
const user1 = new User(
|
const user1 = new User(1, 'test', {
|
||||||
1,
|
x: 500,
|
||||||
"test",
|
y: 500,
|
||||||
"10.0.0.2",
|
moving: false,
|
||||||
{
|
direction: 'down'
|
||||||
x: 500,
|
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
||||||
y: 500,
|
|
||||||
moving: false,
|
|
||||||
direction: "down",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
positionNotifier,
|
|
||||||
{} as UserSocket,
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
"foo",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const user2 = new User(
|
const user2 = new User(2, 'test', {
|
||||||
2,
|
x: -9999,
|
||||||
"test",
|
y: -9999,
|
||||||
"10.0.0.2",
|
moving: false,
|
||||||
{
|
direction: 'down'
|
||||||
x: -9999,
|
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
||||||
y: -9999,
|
|
||||||
moving: false,
|
|
||||||
direction: "down",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
positionNotifier,
|
|
||||||
{} as UserSocket,
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
"foo",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
|
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
|
||||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
|
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
|
||||||
|
@ -77,21 +50,21 @@ describe("PositionNotifier", () => {
|
||||||
bottom: 500
|
bottom: 500
|
||||||
});*/
|
});*/
|
||||||
|
|
||||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||||
|
|
||||||
expect(enterTriggered).toBe(true);
|
expect(enterTriggered).toBe(true);
|
||||||
expect(moveTriggered).toBe(false);
|
expect(moveTriggered).toBe(false);
|
||||||
enterTriggered = false;
|
enterTriggered = false;
|
||||||
|
|
||||||
// Move inside the zone
|
// Move inside the zone
|
||||||
user2.setPosition({ x: 501, y: 500, direction: "down", moving: false });
|
user2.setPosition({x:501, y:500, direction: 'down', moving: false});
|
||||||
|
|
||||||
expect(enterTriggered).toBe(false);
|
expect(enterTriggered).toBe(false);
|
||||||
expect(moveTriggered).toBe(true);
|
expect(moveTriggered).toBe(true);
|
||||||
moveTriggered = false;
|
moveTriggered = false;
|
||||||
|
|
||||||
// Move out of the zone in a zone that we don't track
|
// Move out of the zone in a zone that we don't track
|
||||||
user2.setPosition({ x: 901, y: 500, direction: "down", moving: false });
|
user2.setPosition({x: 901, y: 500, direction: 'down', moving: false});
|
||||||
|
|
||||||
expect(enterTriggered).toBe(false);
|
expect(enterTriggered).toBe(false);
|
||||||
expect(moveTriggered).toBe(false);
|
expect(moveTriggered).toBe(false);
|
||||||
|
@ -99,7 +72,7 @@ describe("PositionNotifier", () => {
|
||||||
leaveTriggered = false;
|
leaveTriggered = false;
|
||||||
|
|
||||||
// Move back in
|
// Move back in
|
||||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||||
expect(enterTriggered).toBe(true);
|
expect(enterTriggered).toBe(true);
|
||||||
expect(moveTriggered).toBe(false);
|
expect(moveTriggered).toBe(false);
|
||||||
expect(leaveTriggered).toBe(false);
|
expect(leaveTriggered).toBe(false);
|
||||||
|
@ -119,59 +92,27 @@ describe("PositionNotifier", () => {
|
||||||
let moveTriggered = false;
|
let moveTriggered = false;
|
||||||
let leaveTriggered = false;
|
let leaveTriggered = false;
|
||||||
|
|
||||||
const positionNotifier = new PositionNotifier(
|
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable, fromZone: Zone|null ) => {
|
||||||
300,
|
enterTriggered = true;
|
||||||
300,
|
}, (thing: Movable, position: PositionInterface) => {
|
||||||
(thing: Movable, fromZone: Zone | null) => {
|
moveTriggered = true;
|
||||||
enterTriggered = true;
|
}, (thing: Movable) => {
|
||||||
},
|
leaveTriggered = true;
|
||||||
(thing: Movable, position: PositionInterface) => {
|
});
|
||||||
moveTriggered = true;
|
|
||||||
},
|
|
||||||
(thing: Movable) => {
|
|
||||||
leaveTriggered = true;
|
|
||||||
},
|
|
||||||
() => {},
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
|
|
||||||
const user1 = new User(
|
const user1 = new User(1, 'test', {
|
||||||
1,
|
x: 500,
|
||||||
"test",
|
y: 500,
|
||||||
"10.0.0.2",
|
moving: false,
|
||||||
{
|
direction: 'down'
|
||||||
x: 500,
|
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
||||||
y: 500,
|
|
||||||
moving: false,
|
|
||||||
direction: "down",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
positionNotifier,
|
|
||||||
{} as UserSocket,
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
"foo",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const user2 = new User(
|
const user2 = new User(2, 'test', {
|
||||||
2,
|
x: 0,
|
||||||
"test",
|
y: 0,
|
||||||
"10.0.0.2",
|
moving: false,
|
||||||
{
|
direction: 'down'
|
||||||
x: 0,
|
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
||||||
y: 0,
|
|
||||||
moving: false,
|
|
||||||
direction: "down",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
positionNotifier,
|
|
||||||
{} as UserSocket,
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
"foo",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const listener = {} as ZoneSocket;
|
const listener = {} as ZoneSocket;
|
||||||
positionNotifier.addZoneListener(listener, 0, 0);
|
positionNotifier.addZoneListener(listener, 0, 0);
|
||||||
|
@ -187,12 +128,14 @@ describe("PositionNotifier", () => {
|
||||||
positionNotifier.enter(user1);
|
positionNotifier.enter(user1);
|
||||||
positionNotifier.enter(user2);
|
positionNotifier.enter(user2);
|
||||||
|
|
||||||
|
|
||||||
//expect(newUsers.length).toBe(2);
|
//expect(newUsers.length).toBe(2);
|
||||||
expect(enterTriggered).toBe(true);
|
expect(enterTriggered).toBe(true);
|
||||||
enterTriggered = false;
|
enterTriggered = false;
|
||||||
|
|
||||||
|
|
||||||
//positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0})
|
//positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0})
|
||||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||||
|
|
||||||
expect(enterTriggered).toBe(true);
|
expect(enterTriggered).toBe(true);
|
||||||
expect(moveTriggered).toBe(false);
|
expect(moveTriggered).toBe(false);
|
||||||
|
@ -243,4 +186,4 @@ describe("PositionNotifier", () => {
|
||||||
enterTriggered = false;
|
enterTriggered = false;
|
||||||
//expect(newUsers.length).toBe(2);
|
//expect(newUsers.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
|
19
back/tests/RoomIdentifierTest.ts
Normal file
19
back/tests/RoomIdentifierTest.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
|
||||||
|
|
||||||
|
describe("RoomIdentifier", () => {
|
||||||
|
it("should flag public id as anonymous", () => {
|
||||||
|
expect(isRoomAnonymous('_/global/test')).toBe(true);
|
||||||
|
});
|
||||||
|
it("should flag public id as not anonymous", () => {
|
||||||
|
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
|
||||||
|
});
|
||||||
|
it("should extract roomSlug from public ID", () => {
|
||||||
|
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
|
||||||
|
});
|
||||||
|
it("should extract correct from private ID", () => {
|
||||||
|
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
|
||||||
|
expect(organizationSlug).toBe('afup');
|
||||||
|
expect(worldSlug).toBe('afup2020');
|
||||||
|
expect(roomSlug).toBe('1floor');
|
||||||
|
});
|
||||||
|
})
|
|
@ -1,67 +0,0 @@
|
||||||
import "jasmine";
|
|
||||||
import { getNearbyDescriptorsMatrix } from "../src/Model/PositionNotifier";
|
|
||||||
|
|
||||||
describe("getNearbyDescriptorsMatrix", () => {
|
|
||||||
it("should create a matrix of coordinates in a square around the parameter", () => {
|
|
||||||
const matrix = [];
|
|
||||||
for (const d of getNearbyDescriptorsMatrix({ i: 1, j: 1 })) {
|
|
||||||
matrix.push(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(matrix).toEqual([
|
|
||||||
{ i: 0, j: 0 },
|
|
||||||
{ i: 1, j: 0 },
|
|
||||||
{ i: 2, j: 0 },
|
|
||||||
{ i: 0, j: 1 },
|
|
||||||
{ i: 1, j: 1 },
|
|
||||||
{ i: 2, j: 1 },
|
|
||||||
{ i: 0, j: 2 },
|
|
||||||
{ i: 1, j: 2 },
|
|
||||||
{ i: 2, j: 2 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a matrix of coordinates in a square around the parameter bis", () => {
|
|
||||||
const matrix = [];
|
|
||||||
for (const d of getNearbyDescriptorsMatrix({ i: 8, j: 3 })) {
|
|
||||||
matrix.push(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(matrix).toEqual([
|
|
||||||
{ i: 7, j: 2 },
|
|
||||||
{ i: 8, j: 2 },
|
|
||||||
{ i: 9, j: 2 },
|
|
||||||
{ i: 7, j: 3 },
|
|
||||||
{ i: 8, j: 3 },
|
|
||||||
{ i: 9, j: 3 },
|
|
||||||
{ i: 7, j: 4 },
|
|
||||||
{ i: 8, j: 4 },
|
|
||||||
{ i: 9, j: 4 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not create a matrix with negative coordinates", () => {
|
|
||||||
const matrix = [];
|
|
||||||
for (const d of getNearbyDescriptorsMatrix({ i: 0, j: 0 })) {
|
|
||||||
matrix.push(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(matrix).toEqual([
|
|
||||||
{ i: 0, j: 0 },
|
|
||||||
{ i: 1, j: 0 },
|
|
||||||
{ i: 0, j: 1 },
|
|
||||||
{ i: 1, j: 1 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*it("should not create a matrix with coordinates bigger than its dimmensions", () => {
|
|
||||||
const matrix = getNearbyDescriptorsMatrix({i: 4, j: 4}, 5, 5);
|
|
||||||
|
|
||||||
expect(matrix).toEqual([
|
|
||||||
{i: 3,j: 3},
|
|
||||||
{i: 4,j: 3},
|
|
||||||
{i: 3,j: 4},
|
|
||||||
{i: 4,j: 4},
|
|
||||||
])
|
|
||||||
});*/
|
|
||||||
});
|
|
|
@ -3,7 +3,7 @@
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
|
3044
back/yarn.lock
3044
back/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -13,6 +13,7 @@ RoomConnection.setWebsocketFactory((url: string) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function startOneUser(): Promise<void> {
|
async function startOneUser(): Promise<void> {
|
||||||
|
await connectionManager.anonymousLogin(true);
|
||||||
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
||||||
{
|
{
|
||||||
x: 783,
|
x: 783,
|
||||||
|
@ -22,7 +23,7 @@ async function startOneUser(): Promise<void> {
|
||||||
bottom: 200,
|
bottom: 200,
|
||||||
left: 500,
|
left: 500,
|
||||||
right: 800
|
right: 800
|
||||||
}, null);
|
});
|
||||||
|
|
||||||
const connection = onConnect.connection;
|
const connection = onConnect.connection;
|
||||||
|
|
||||||
|
|
24
benchmark/package-lock.json
generated
24
benchmark/package-lock.json
generated
|
@ -209,9 +209,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"glob-parent": {
|
"glob-parent": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
}
|
}
|
||||||
|
@ -230,9 +230,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hosted-git-info": {
|
"hosted-git-info": {
|
||||||
"version": "2.8.9",
|
"version": "2.8.8",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
|
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||||
},
|
},
|
||||||
"indent-string": {
|
"indent-string": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
@ -429,9 +429,9 @@
|
||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||||
},
|
},
|
||||||
"path-parse": {
|
"path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
||||||
},
|
},
|
||||||
"path-type": {
|
"path-type": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
@ -688,9 +688,9 @@
|
||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.6",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
|
||||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
|
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
|
||||||
},
|
},
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^7.2.6",
|
||||||
"ts-node-dev": "^1.0.0-pre.62",
|
"ts-node-dev": "^1.0.0-pre.62",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.0.2",
|
||||||
"ws": "^7.4.6"
|
"ws": "^7.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,8 +148,8 @@ get-stdin@^4.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||||
|
|
||||||
glob-parent@~5.1.0:
|
glob-parent@~5.1.0:
|
||||||
version "5.1.2"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob "^4.0.1"
|
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"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
|
|
||||||
hosted-git-info@^2.1.4:
|
hosted-git-info@^2.1.4:
|
||||||
version "2.8.9"
|
version "2.8.8"
|
||||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||||
|
|
||||||
indent-string@^2.1.0:
|
indent-string@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
|
@ -315,8 +315,8 @@ path-is-absolute@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||||
|
|
||||||
path-parse@^1.0.6:
|
path-parse@^1.0.6:
|
||||||
version "1.0.7"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
|
||||||
|
|
||||||
path-type@^1.0.0:
|
path-type@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
|
@ -515,9 +515,9 @@ wrappy@1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
|
||||||
ws@^7.4.6:
|
ws@^7.3.1:
|
||||||
version "7.4.6"
|
version "7.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
||||||
|
|
||||||
xtend@^4.0.0:
|
xtend@^4.0.0:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
# Security
|
|
||||||
#
|
|
||||||
|
|
||||||
SECRET_KEY=
|
|
||||||
ADMIN_API_TOKEN=
|
|
||||||
|
|
||||||
#
|
|
||||||
# Networking
|
|
||||||
#
|
|
||||||
|
|
||||||
# The base domain
|
|
||||||
DOMAIN=workadventure.localhost
|
|
||||||
|
|
||||||
# Subdomains
|
|
||||||
# MUST match the DOMAIN variable above
|
|
||||||
FRONT_HOST=front.workadventure.localhost
|
|
||||||
PUSHER_HOST=pusher.workadventure.localhost
|
|
||||||
BACK_HOST=api.workadventure.localhost
|
|
||||||
MAPS_HOST=maps.workadventure.localhost
|
|
||||||
ICON_HOST=icon.workadventure.localhost
|
|
||||||
|
|
||||||
# SAAS admin panel
|
|
||||||
ADMIN_API_URL=
|
|
||||||
|
|
||||||
#
|
|
||||||
# Basic configuration
|
|
||||||
#
|
|
||||||
|
|
||||||
# The directory to store data in
|
|
||||||
DATA_DIR=./wa
|
|
||||||
|
|
||||||
# The URL used by default, in the form: "/_/global/map/url.json"
|
|
||||||
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json
|
|
||||||
|
|
||||||
# If you want to have a contact page in your menu,
|
|
||||||
# you MUST set CONTACT_URL to the URL of the page that you want
|
|
||||||
CONTACT_URL=
|
|
||||||
|
|
||||||
MAX_PER_GROUP=4
|
|
||||||
MAX_USERNAME_LENGTH=8
|
|
||||||
DISABLE_ANONYMOUS=false
|
|
||||||
|
|
||||||
# The version of the docker image to use
|
|
||||||
# MUST uncomment "image" keys in the docker-compose file for it to be effective
|
|
||||||
VERSION=master
|
|
||||||
|
|
||||||
TZ=Europe/Paris
|
|
||||||
|
|
||||||
#
|
|
||||||
# Jitsi
|
|
||||||
#
|
|
||||||
|
|
||||||
JITSI_URL=meet.jit.si
|
|
||||||
# If your Jitsi environment has authentication set up,
|
|
||||||
# you MUST set JITSI_PRIVATE_MODE to "true"
|
|
||||||
# and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret
|
|
||||||
JITSI_PRIVATE_MODE=false
|
|
||||||
JITSI_ISS=
|
|
||||||
SECRET_JITSI_KEY=
|
|
||||||
|
|
||||||
#
|
|
||||||
# Turn/Stun
|
|
||||||
#
|
|
||||||
|
|
||||||
# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections)
|
|
||||||
TURN_SERVER=
|
|
||||||
TURN_USER=
|
|
||||||
TURN_PASSWORD=
|
|
||||||
# If your Turn server is configured to use the Turn REST API, you MUST put the shared auth secret here.
|
|
||||||
# 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=
|
|
||||||
# URL of the STUN server
|
|
||||||
STUN_SERVER=
|
|
||||||
|
|
||||||
#
|
|
||||||
# Certificate config
|
|
||||||
#
|
|
||||||
|
|
||||||
# The email address used by Let's encrypt to send renewal warnings (compulsory)
|
|
||||||
ACME_EMAIL=
|
|
||||||
|
|
||||||
#
|
|
||||||
# Additional app configs
|
|
||||||
# Configuration for apps which are not workadventure itself
|
|
||||||
#
|
|
||||||
|
|
||||||
# openID
|
|
||||||
OPID_CLIENT_ID=
|
|
||||||
OPID_CLIENT_SECRET=
|
|
||||||
OPID_CLIENT_ISSUER=
|
|
||||||
OPID_CLIENT_REDIRECT_URL=
|
|
||||||
OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen
|
|
||||||
OPID_PROFILE_SCREEN_PROVIDER=
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Advanced configuration
|
|
||||||
# Generally does not need to be changed
|
|
||||||
#
|
|
||||||
|
|
||||||
# Networking
|
|
||||||
HTTP_PORT=80
|
|
||||||
HTTPS_PORT=443
|
|
||||||
|
|
||||||
# Workadventure settings
|
|
||||||
DISABLE_NOTIFICATIONS=false
|
|
||||||
SKIP_RENDER_OPTIMIZATIONS=false
|
|
||||||
STORE_VARIABLES_FOR_LOCAL_MAPS=true
|
|
||||||
|
|
||||||
# Debugging options
|
|
||||||
DEBUG_MODE=false
|
|
||||||
LOG_LEVEL=WARN
|
|
||||||
|
|
||||||
# Internal URLs
|
|
||||||
API_URL=back:50051
|
|
||||||
|
|
||||||
RESTART_POLICY=unless-stopped
|
|
|
@ -1,62 +0,0 @@
|
||||||
version: "3.3"
|
|
||||||
services:
|
|
||||||
front-dev:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: front/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-front:master
|
|
||||||
environment:
|
|
||||||
LIVE_RELOAD: "true"
|
|
||||||
NODE_ENV: "develop"
|
|
||||||
DEBUG_MODE: "$DEBUG_MODE"
|
|
||||||
JITSI_URL: "$JITSI_URL"
|
|
||||||
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
|
|
||||||
PUSHER_URL: "https://pusher.${DOMAIN}"
|
|
||||||
ICON_URL: "https://icon.${DOMAIN}"
|
|
||||||
API_URL: "pusher.${DOMAIN}"
|
|
||||||
STUN_SERVER: "${STUN_SERVER}"
|
|
||||||
TURN_SERVER: "${TURN_SERVER}"
|
|
||||||
TURN_USER: "${TURN_USER}"
|
|
||||||
TURN_PASSWORD: "${TURN_PASSWORD}"
|
|
||||||
START_ROOM_URL: "${START_ROOM_URL}"
|
|
||||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
|
||||||
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
|
|
||||||
FALLBACK_LOCALE: "${DEFAULT_LOCALE}"
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8011:80"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
pusher-dev:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: pusher/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-pusher:master
|
|
||||||
command: yarn run runprod
|
|
||||||
environment:
|
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
|
||||||
SECRET_KEY: yourSecretKey
|
|
||||||
API_URL: back-dev:50051
|
|
||||||
JITSI_URL: $JITSI_URL
|
|
||||||
JITSI_ISS: $JITSI_ISS
|
|
||||||
FRONT_URL: https://play.${DOMAIN}
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8012:8080"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
back-dev:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: back/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-back:master
|
|
||||||
command: yarn run runprod
|
|
||||||
environment:
|
|
||||||
NODE_ENV: develop
|
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
|
||||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
|
||||||
ADMIN_API_URL: "$ADMIN_API_URL"
|
|
||||||
JITSI_URL: $JITSI_URL
|
|
||||||
JITSI_ISS: $JITSI_ISS
|
|
||||||
TURN_STATIC_AUTH_SECRET: "$TURN_STATIC_AUTH_SECRET"
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8013:8080"
|
|
||||||
restart: unless-stopped
|
|
|
@ -1,128 +0,0 @@
|
||||||
version: "3.5"
|
|
||||||
services:
|
|
||||||
reverse-proxy:
|
|
||||||
image: traefik:v2.6
|
|
||||||
command:
|
|
||||||
- --log.level=${LOG_LEVEL}
|
|
||||||
- --providers.docker
|
|
||||||
# Entry points
|
|
||||||
- --entryPoints.web.address=:${HTTP_PORT}
|
|
||||||
- --entrypoints.web.http.redirections.entryPoint.to=websecure
|
|
||||||
- --entrypoints.web.http.redirections.entryPoint.scheme=https
|
|
||||||
- --entryPoints.websecure.address=:${HTTPS_PORT}
|
|
||||||
# HTTP challenge
|
|
||||||
- --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL}
|
|
||||||
- --certificatesresolvers.myresolver.acme.storage=/acme.json
|
|
||||||
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
|
|
||||||
# Let's Encrypt's staging server
|
|
||||||
# uncomment during testing to avoid rate limiting
|
|
||||||
#- --certificatesresolvers.dnsresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
ports:
|
|
||||||
- "${HTTP_PORT}:80"
|
|
||||||
- "${HTTPS_PORT}:443"
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ${DATA_DIR}/letsencrypt/acme.json:/acme.json
|
|
||||||
restart: ${RESTART_POLICY}
|
|
||||||
|
|
||||||
|
|
||||||
front:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: front/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-front:${VERSION}
|
|
||||||
environment:
|
|
||||||
- DEBUG_MODE
|
|
||||||
- JITSI_URL
|
|
||||||
- JITSI_PRIVATE_MODE
|
|
||||||
- PUSHER_URL=//${PUSHER_HOST}
|
|
||||||
- ICON_URL=//${ICON_HOST}
|
|
||||||
- TURN_SERVER
|
|
||||||
- TURN_USER
|
|
||||||
- TURN_PASSWORD
|
|
||||||
- TURN_STATIC_AUTH_SECRET
|
|
||||||
- STUN_SERVER
|
|
||||||
- START_ROOM_URL
|
|
||||||
- SKIP_RENDER_OPTIMIZATIONS
|
|
||||||
- MAX_PER_GROUP
|
|
||||||
- MAX_USERNAME_LENGTH
|
|
||||||
- DISABLE_ANONYMOUS
|
|
||||||
- DISABLE_NOTIFICATIONS
|
|
||||||
labels:
|
|
||||||
- "traefik.http.routers.front.rule=Host(`${FRONT_HOST}`)"
|
|
||||||
- "traefik.http.routers.front.entryPoints=web"
|
|
||||||
- "traefik.http.services.front.loadbalancer.server.port=80"
|
|
||||||
- "traefik.http.routers.front-ssl.rule=Host(`${FRONT_HOST}`)"
|
|
||||||
- "traefik.http.routers.front-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.front-ssl.service=front"
|
|
||||||
- "traefik.http.routers.front-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.front-ssl.tls.certresolver=myresolver"
|
|
||||||
restart: ${RESTART_POLICY}
|
|
||||||
|
|
||||||
pusher:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: pusher/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-pusher:${VERSION}
|
|
||||||
command: yarn run runprod
|
|
||||||
environment:
|
|
||||||
- SECRET_JITSI_KEY
|
|
||||||
- SECRET_KEY
|
|
||||||
- API_URL
|
|
||||||
- FRONT_URL=https://${FRONT_HOST}
|
|
||||||
- JITSI_URL
|
|
||||||
- JITSI_ISS
|
|
||||||
- DISABLE_ANONYMOUS
|
|
||||||
labels:
|
|
||||||
- "traefik.http.routers.pusher.rule=Host(`${PUSHER_HOST}`)"
|
|
||||||
- "traefik.http.routers.pusher.entryPoints=web"
|
|
||||||
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.http.routers.pusher-ssl.rule=Host(${PUSHER_HOST}`)"
|
|
||||||
- "traefik.http.routers.pusher-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.pusher-ssl.service=pusher"
|
|
||||||
- "traefik.http.routers.pusher-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver"
|
|
||||||
restart: ${RESTART_POLICY}
|
|
||||||
|
|
||||||
back:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: back/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-back:${VERSION}
|
|
||||||
command: yarn run runprod
|
|
||||||
environment:
|
|
||||||
- SECRET_JITSI_KEY
|
|
||||||
- SECRET_KEY
|
|
||||||
- ADMIN_API_TOKEN
|
|
||||||
- ADMIN_API_URL
|
|
||||||
- TURN_SERVER
|
|
||||||
- TURN_USER
|
|
||||||
- TURN_PASSWORD
|
|
||||||
- TURN_STATIC_AUTH_SECRET
|
|
||||||
- STUN_SERVER
|
|
||||||
- JITSI_URL
|
|
||||||
- JITSI_ISS
|
|
||||||
- MAX_PER_GROUP
|
|
||||||
- STORE_VARIABLES_FOR_LOCAL_MAPS
|
|
||||||
labels:
|
|
||||||
- "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)"
|
|
||||||
- "traefik.http.routers.back.entryPoints=web"
|
|
||||||
- "traefik.http.services.back.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.http.routers.back-ssl.rule=Host(`${BACK_HOST}`)"
|
|
||||||
- "traefik.http.routers.back-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.back-ssl.service=back"
|
|
||||||
- "traefik.http.routers.back-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
|
|
||||||
restart: ${RESTART_POLICY}
|
|
||||||
|
|
||||||
icon:
|
|
||||||
image: matthiasluedtke/iconserver:v3.13.0
|
|
||||||
labels:
|
|
||||||
- "traefik.http.routers.icon.rule=Host(`${ICON_HOST}`)"
|
|
||||||
- "traefik.http.routers.icon.entryPoints=web,traefik"
|
|
||||||
- "traefik.http.services.icon.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.http.routers.icon-ssl.rule=Host(`${ICON_HOST}`)"
|
|
||||||
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.icon-ssl.service=icon"
|
|
||||||
- "traefik.http.routers.icon-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"
|
|
|
@ -1,63 +0,0 @@
|
||||||
version: "3.3"
|
|
||||||
services:
|
|
||||||
front:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: front/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-front:master
|
|
||||||
environment:
|
|
||||||
#LIVE_RELOAD: "true"
|
|
||||||
NODE_ENV: "production"
|
|
||||||
DEBUG_MODE: "$DEBUG_MODE"
|
|
||||||
JITSI_URL: "$JITSI_URL"
|
|
||||||
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
|
|
||||||
PUSHER_URL: "https://pusher.${DOMAIN}"
|
|
||||||
ICON_URL: "https://icon.${DOMAIN}"
|
|
||||||
API_URL: "pusher.${DOMAIN}"
|
|
||||||
STUN_SERVER: "${STUN_SERVER}"
|
|
||||||
TURN_SERVER: "${TURN_SERVER}"
|
|
||||||
TURN_USER: "${TURN_USER}"
|
|
||||||
TURN_PASSWORD: "${TURN_PASSWORD}"
|
|
||||||
START_ROOM_URL: "${START_ROOM_URL}"
|
|
||||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
|
||||||
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
|
|
||||||
FALLBACK_LOCALE: "${DEFAULT_LOCALE}"
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8001:80"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
pusher:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: pusher/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-pusher:master
|
|
||||||
command: yarn run runprod
|
|
||||||
environment:
|
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
|
||||||
SECRET_KEY: yourSecretKey
|
|
||||||
API_URL: back:50051
|
|
||||||
JITSI_URL: $JITSI_URL
|
|
||||||
JITSI_ISS: $JITSI_ISS
|
|
||||||
FRONT_URL: https://play.${DOMAIN}
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8002:8080"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
back:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: back/Dockerfile
|
|
||||||
#image: thecodingmachine/workadventure-back:master
|
|
||||||
command: yarn run runprod
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
|
||||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
|
||||||
ADMIN_API_URL: "$ADMIN_API_URL"
|
|
||||||
JITSI_URL: $JITSI_URL
|
|
||||||
JITSI_ISS: $JITSI_ISS
|
|
||||||
TURN_STATIC_AUTH_SECRET: "$TURN_STATIC_AUTH_SECRET"
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8003:8080"
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
# vim: syntax=conf
|
|
||||||
|
|
||||||
map $http_host $krautworld_upstream {
|
|
||||||
hostnames;
|
|
||||||
default http://127.0.0.1:8000;
|
|
||||||
|
|
||||||
icon.kraut.world http://127.0.0.1:7999;
|
|
||||||
|
|
||||||
play.kraut.world http://127.0.0.1:8001;
|
|
||||||
pusher.kraut.world http://127.0.0.1:8002;
|
|
||||||
api.kraut.world http://127.0.0.1:8003;
|
|
||||||
maps.kraut.world http://127.0.0.1:8004;
|
|
||||||
|
|
||||||
play.dev.kraut.world http://127.0.0.1:8011;
|
|
||||||
pusher.dev.kraut.world http://127.0.0.1:8012;
|
|
||||||
api.dev.kraut.world http://127.0.0.1:8013;
|
|
||||||
maps.dev.kraut.world http://127.0.0.1:8014;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 127.0.0.1:8443 ssl http2;
|
|
||||||
listen [::1]:8443 ssl http2;
|
|
||||||
server_name .kraut.world .dev.kraut.world;
|
|
||||||
|
|
||||||
ssl_certificate /var/lib/dehydrated/certs/play.kraut.world/fullchain.pem;
|
|
||||||
ssl_certificate_key /var/lib/dehydrated/certs/play.kraut.world/privkey.pem;
|
|
||||||
|
|
||||||
set $HSTS_header "max-age=16000000";
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass $krautworld_upstream;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_pass_header Set-Cookie;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Workadventure icon service
|
|
||||||
Requires=docker.service
|
|
||||||
After=docker.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart=/usr/bin/docker run --rm -p "127.0.0.1:7999:8080" --name icon matthiasluedtke/iconserver:v3.13.0
|
|
||||||
ExecStopPost=/usr/bin/docker rm -f icon
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -1,96 +1,91 @@
|
||||||
{
|
{
|
||||||
local env = std.extVar("env"),
|
local env = std.extVar("env"),
|
||||||
local namespace = env.DEPLOY_REF,
|
local namespace = env.GITHUB_REF_SLUG,
|
||||||
local tag = namespace,
|
local tag = namespace,
|
||||||
local url = namespace+".test.workadventu.re",
|
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
|
||||||
// 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://admin."+url else null,
|
||||||
local adminUrl = if std.startsWith(namespace, "admin") then "https://"+url else null,
|
|
||||||
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"containers": {
|
"containers": {
|
||||||
"back1": {
|
"back1": {
|
||||||
"image": "thecodingmachine/workadventure-back:"+tag,
|
"image": "thecodingmachine/workadventure-back:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "api1-"+url,
|
"url": "api1."+url,
|
||||||
|
"https": "enable",
|
||||||
"containerPort": 8080
|
"containerPort": 8080
|
||||||
},
|
},
|
||||||
"ports": [8080, 50051],
|
"ports": [8080, 50051],
|
||||||
"env": {
|
"env": {
|
||||||
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||||
|
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||||
"JITSI_ISS": env.JITSI_ISS,
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
} + if adminUrl != null then {
|
||||||
"REDIS_HOST": "redis",
|
|
||||||
} + (if adminUrl != null then {
|
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
} else {}
|
||||||
} else {})
|
|
||||||
},
|
},
|
||||||
"back2": {
|
"back2": {
|
||||||
"image": "thecodingmachine/workadventure-back:"+tag,
|
"image": "thecodingmachine/workadventure-back:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "api2-"+url,
|
"url": "api2."+url,
|
||||||
|
"https": "enable",
|
||||||
"containerPort": 8080
|
"containerPort": 8080
|
||||||
},
|
},
|
||||||
"ports": [8080, 50051],
|
"ports": [8080, 50051],
|
||||||
"env": {
|
"env": {
|
||||||
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||||
|
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||||
"JITSI_ISS": env.JITSI_ISS,
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
} + if adminUrl != null then {
|
||||||
"REDIS_HOST": "redis",
|
|
||||||
} + (if adminUrl != null then {
|
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
} else {}
|
||||||
} else {})
|
|
||||||
},
|
},
|
||||||
"pusher": {
|
"pusher": {
|
||||||
"replicas": 2,
|
"replicas": 2,
|
||||||
"image": "thecodingmachine/workadventure-pusher:"+tag,
|
"image": "thecodingmachine/workadventure-pusher:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "pusher-"+url,
|
"url": "pusher."+url,
|
||||||
|
"https": "enable"
|
||||||
},
|
},
|
||||||
"ports": [8080],
|
"ports": [8080],
|
||||||
"env": {
|
"env": {
|
||||||
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||||
|
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||||
"JITSI_ISS": env.JITSI_ISS,
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"API_URL": "back1:50051,back2:50051",
|
"API_URL": "back1:50051,back2:50051",
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"FRONT_URL": "https://play-"+url
|
} + if adminUrl != null then {
|
||||||
} + (if adminUrl != null then {
|
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
} else {}
|
||||||
"ADMIN_SOCKETS_TOKEN": env.ADMIN_SOCKETS_TOKEN,
|
|
||||||
} else {})
|
|
||||||
},
|
},
|
||||||
"front": {
|
"front": {
|
||||||
"image": "thecodingmachine/workadventure-front:"+tag,
|
"image": "thecodingmachine/workadventure-front:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "play-"+url,
|
"url": "play."+url,
|
||||||
|
"https": "enable"
|
||||||
},
|
},
|
||||||
"ports": [80],
|
"ports": [80],
|
||||||
"env": {
|
"env": {
|
||||||
"PUSHER_URL": "//pusher-"+url,
|
"API_URL": "pusher."+url,
|
||||||
"UPLOADER_URL": "//uploader-"+url,
|
"UPLOADER_URL": "uploader."+url,
|
||||||
"ADMIN_URL": "//"+url,
|
"ADMIN_URL": "admin."+url,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
#POSTHOG
|
|
||||||
"POSTHOG_API_KEY": if namespace == "master" then env.POSTHOG_API_KEY else "",
|
|
||||||
"POSTHOG_URL": if namespace == "master" then env.POSTHOG_URL else "",
|
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
|
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
|
||||||
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
|
"TURN_USER": "workadventure",
|
||||||
"START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json",
|
"TURN_PASSWORD": "WorkAdventure123",
|
||||||
"ICON_URL": "//icon-"+url,
|
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uploader": {
|
"uploader": {
|
||||||
"image": "thecodingmachine/workadventure-uploader:"+tag,
|
"image": "thecodingmachine/workadventure-uploader:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "uploader-"+url,
|
"url": "uploader."+url,
|
||||||
|
"https": "enable",
|
||||||
"containerPort": 8080
|
"containerPort": 8080
|
||||||
},
|
},
|
||||||
"ports": [8080],
|
"ports": [8080],
|
||||||
|
@ -100,27 +95,27 @@
|
||||||
"maps": {
|
"maps": {
|
||||||
"image": "thecodingmachine/workadventure-maps:"+tag,
|
"image": "thecodingmachine/workadventure-maps:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "maps-"+url
|
"url": "maps."+url,
|
||||||
|
"https": "enable"
|
||||||
|
},
|
||||||
|
"ports": [80]
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"image": "thecodingmachine/workadventure-website:"+tag,
|
||||||
|
"host": {
|
||||||
|
"url": url,
|
||||||
|
"https": "enable"
|
||||||
},
|
},
|
||||||
"ports": [80],
|
"ports": [80],
|
||||||
"env": {
|
"env": {
|
||||||
"FRONT_URL": "https://play-"+url
|
"GAME_URL": "https://play."+url
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"redis": {
|
|
||||||
"image": "redis:6",
|
|
||||||
"ports": [6379]
|
|
||||||
},
|
|
||||||
"iconserver": {
|
|
||||||
"image": "matthiasluedtke/iconserver:v3.13.0",
|
|
||||||
"host": {
|
|
||||||
"url": "icon-"+url,
|
|
||||||
"containerPort": 8080,
|
|
||||||
},
|
|
||||||
"ports": [8080]
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"https": {
|
||||||
|
"mail": "d.negrier@thecodingmachine.com"
|
||||||
|
},
|
||||||
k8sextension(k8sConf)::
|
k8sextension(k8sConf)::
|
||||||
k8sConf + {
|
k8sConf + {
|
||||||
back1+: {
|
back1+: {
|
||||||
|
@ -135,14 +130,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
ingress+: {
|
|
||||||
spec+: {
|
|
||||||
tls+: [{
|
|
||||||
hosts: ["api1-"+url],
|
|
||||||
secretName: "certificate-tls"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
back2+: {
|
back2+: {
|
||||||
|
@ -157,14 +144,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
ingress+: {
|
|
||||||
spec+: {
|
|
||||||
tls+: [{
|
|
||||||
hosts: ["api2-"+url],
|
|
||||||
secretName: "certificate-tls"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pusher+: {
|
pusher+: {
|
||||||
|
@ -179,56 +158,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
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"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
iconserver+: {
|
|
||||||
ingress+: {
|
|
||||||
spec+: {
|
|
||||||
tls+: [{
|
|
||||||
hosts: ["icon-"+url],
|
|
||||||
secretName: "certificate-tls"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
docker-compose.ci.yml
Normal file
20
docker-compose.ci.yml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
wait_app:
|
||||||
|
image: dadarek/wait-for-dependencies
|
||||||
|
depends_on:
|
||||||
|
- reverse-proxy
|
||||||
|
command: front:8080
|
||||||
|
cypress:
|
||||||
|
# the Docker image to use from https://github.com/cypress-io/cypress-docker-images
|
||||||
|
image: "cypress/included:3.8.3"
|
||||||
|
depends_on:
|
||||||
|
- reverse-proxy
|
||||||
|
environment:
|
||||||
|
# pass base url to test pointing at the web application
|
||||||
|
- CYPRESS_baseUrl=http://front:8080
|
||||||
|
working_dir: /e2e
|
||||||
|
volumes:
|
||||||
|
- ./e2e/:/e2e
|
|
@ -1,227 +0,0 @@
|
||||||
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
|
|
||||||
ICON_URL: /icon
|
|
||||||
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"
|
|
||||||
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
|
|
||||||
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:14
|
|
||||||
command: yarn dev
|
|
||||||
#command: yarn run prod
|
|
||||||
#command: yarn run profile
|
|
||||||
environment:
|
|
||||||
DEBUG: "*"
|
|
||||||
STARTUP_COMMAND_1: yarn install
|
|
||||||
# wait for files generated by "messages" container to exists
|
|
||||||
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
|
||||||
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
|
|
||||||
FRONT_URL: http://localhost
|
|
||||||
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
|
||||||
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
|
||||||
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
|
||||||
OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL
|
|
||||||
OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER
|
|
||||||
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
|
|
||||||
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/php:8.1-v4-apache-node12
|
|
||||||
environment:
|
|
||||||
DEBUG_MODE: "$DEBUG_MODE"
|
|
||||||
HOST: "0.0.0.0"
|
|
||||||
NODE_ENV: development
|
|
||||||
FRONT_URL: http://play.workadventure.localhost
|
|
||||||
#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
|
|
||||||
# wait for files generated by "messages" container to exists
|
|
||||||
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
|
||||||
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"
|
|
||||||
REDIS_HOST: redis
|
|
||||||
NODE_ENV: development
|
|
||||||
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
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:6
|
|
||||||
|
|
||||||
icon:
|
|
||||||
image: matthiasluedtke/iconserver:v3.13.0
|
|
||||||
labels:
|
|
||||||
- "traefik.http.middlewares.strip-icon-prefix.stripprefix.prefixes=/icon"
|
|
||||||
- "traefik.http.routers.icon.rule=PathPrefix(`/icon`)"
|
|
||||||
- "traefik.http.routers.icon.middlewares=strip-icon-prefix@docker"
|
|
||||||
- "traefik.http.routers.icon.entryPoints=web"
|
|
||||||
- "traefik.http.services.icon.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.http.routers.icon-ssl.rule=PathPrefix(`/icon`)"
|
|
||||||
- "traefik.http.routers.icon-ssl.middlewares=strip-icon-prefix@docker"
|
|
||||||
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.icon-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.icon-ssl.service=icon"
|
|
||||||
|
|
||||||
# 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
|
|
|
@ -1,19 +0,0 @@
|
||||||
version: "3.5"
|
|
||||||
services:
|
|
||||||
testcafe:
|
|
||||||
build: tests/
|
|
||||||
working_dir: /project/tests
|
|
||||||
command:
|
|
||||||
- --dev
|
|
||||||
# Run as root to have the right to access /var/run/docker.sock
|
|
||||||
user: root
|
|
||||||
environment:
|
|
||||||
BROWSER: "chromium --use-fake-device-for-media-stream"
|
|
||||||
PROJECT_DIR: ${PROJECT_DIR}
|
|
||||||
ADMIN_API_TOKEN: ${ADMIN_API_TOKEN}
|
|
||||||
volumes:
|
|
||||||
- ./:/project
|
|
||||||
- ./maps:/maps
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
# security_opt:
|
|
||||||
# - seccomp:unconfined
|
|
|
@ -1,29 +1,22 @@
|
||||||
version: "3.5"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
image: traefik:v2.5
|
image: traefik:v2.0
|
||||||
command:
|
command:
|
||||||
- --api.insecure=true
|
- --api.insecure=true
|
||||||
- --providers.docker
|
- --providers.docker
|
||||||
- --entryPoints.web.address=:80
|
- --entryPoints.web.address=:80
|
||||||
- --entryPoints.websecure.address=:443
|
- --entryPoints.websecure.address=:443
|
||||||
- "--providers.docker.exposedbydefault=false"
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
# The Web UI (enabled by --api.insecure=true)
|
# The Web UI (enabled by --api.insecure=true)
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
#depends_on:
|
depends_on:
|
||||||
# - back
|
- back
|
||||||
# - front
|
- front
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
networks:
|
|
||||||
default:
|
|
||||||
aliases:
|
|
||||||
- 'play.workadventure.localhost'
|
|
||||||
- 'pusher.workadventure.localhost'
|
|
||||||
- 'maps.workadventure.localhost'
|
|
||||||
|
|
||||||
front:
|
front:
|
||||||
image: thecodingmachine/nodejs:14
|
image: thecodingmachine/nodejs:14
|
||||||
|
@ -33,79 +26,55 @@ services:
|
||||||
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
|
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
|
||||||
HOST: "0.0.0.0"
|
HOST: "0.0.0.0"
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
PUSHER_URL: //pusher.workadventure.localhost
|
API_URL: pusher.$HOST_NAME
|
||||||
UPLOADER_URL: //uploader.workadventure.localhost
|
UPLOADER_URL: uploader.$HOST_NAME
|
||||||
#ADMIN_URL: //workadventure.localhost
|
STARTUP_COMMAND_1: yarn install
|
||||||
ICON_URL: //icon.workadventure.localhost
|
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
|
||||||
STARTUP_COMMAND_1: ./templater.sh
|
TURN_USER: workadventure
|
||||||
STARTUP_COMMAND_2: yarn install
|
TURN_PASSWORD: WorkAdventure123
|
||||||
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"
|
|
||||||
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
|
|
||||||
OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER"
|
|
||||||
LIVE_RELOAD: "$LIVE_RELOAD:-true"
|
|
||||||
command: yarn run start
|
command: yarn run start
|
||||||
volumes:
|
volumes:
|
||||||
- ./front:/usr/src/app
|
- ./front:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.http.routers.front.rule=Host(`play.$HOST_NAME`)"
|
||||||
- "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.services.front.loadbalancer.server.port=8080"
|
||||||
- "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)"
|
- "traefik.http.routers.front-ssl.rule=Host(`play.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.front-ssl.entryPoints=websecure"
|
- "traefik.http.routers.front-ssl.entryPoints=websecure"
|
||||||
- "traefik.http.routers.front-ssl.tls=true"
|
- "traefik.http.routers.front-ssl.tls=true"
|
||||||
- "traefik.http.routers.front-ssl.service=front"
|
- "traefik.http.routers.front-ssl.service=front"
|
||||||
|
|
||||||
pusher:
|
pusher:
|
||||||
image: thecodingmachine/nodejs:14
|
image: thecodingmachine/nodejs:12
|
||||||
command: yarn dev
|
command: yarn dev
|
||||||
|
#command: yarn run prod
|
||||||
|
#command: yarn run profile
|
||||||
environment:
|
environment:
|
||||||
DEBUG: "socket:*"
|
DEBUG: "*"
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
# wait for files generated by "messages" container to exists
|
|
||||||
STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||||
API_URL: back:50051
|
API_URL: back:50051
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
FRONT_URL: http://play.workadventure.localhost
|
|
||||||
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
|
||||||
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
|
||||||
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
|
||||||
OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL
|
|
||||||
OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER
|
|
||||||
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.http.routers.pusher.rule=Host(`pusher.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.pusher.entryPoints=web"
|
- "traefik.http.routers.pusher.entryPoints=web"
|
||||||
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
|
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
|
||||||
- "traefik.http.routers.pusher-ssl.rule=Host(`pusher.workadventure.localhost`)"
|
- "traefik.http.routers.pusher-ssl.rule=Host(`pusher.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.pusher-ssl.entryPoints=websecure"
|
- "traefik.http.routers.pusher-ssl.entryPoints=websecure"
|
||||||
- "traefik.http.routers.pusher-ssl.tls=true"
|
- "traefik.http.routers.pusher-ssl.tls=true"
|
||||||
- "traefik.http.routers.pusher-ssl.service=pusher"
|
- "traefik.http.routers.pusher-ssl.service=pusher"
|
||||||
|
|
||||||
maps:
|
maps:
|
||||||
image: thecodingmachine/php:8.1-v4-apache-node12
|
image: thecodingmachine/nodejs:12-apache
|
||||||
environment:
|
environment:
|
||||||
DEBUG_MODE: "$DEBUG_MODE"
|
DEBUG_MODE: "$DEBUG_MODE"
|
||||||
HOST: "0.0.0.0"
|
HOST: "0.0.0.0"
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
FRONT_URL: http://play.workadventure.localhost
|
|
||||||
#APACHE_DOCUMENT_ROOT: dist/
|
#APACHE_DOCUMENT_ROOT: dist/
|
||||||
#APACHE_EXTENSIONS: headers
|
#APACHE_EXTENSIONS: headers
|
||||||
#APACHE_EXTENSION_HEADERS: 1
|
#APACHE_EXTENSION_HEADERS: 1
|
||||||
|
@ -115,11 +84,10 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./maps:/var/www/html
|
- ./maps:/var/www/html
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.http.routers.maps.rule=Host(`maps.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.maps.entryPoints=web,traefik"
|
- "traefik.http.routers.maps.entryPoints=web,traefik"
|
||||||
- "traefik.http.services.maps.loadbalancer.server.port=80"
|
- "traefik.http.services.maps.loadbalancer.server.port=80"
|
||||||
- "traefik.http.routers.maps-ssl.rule=Host(`maps.workadventure.localhost`)"
|
- "traefik.http.routers.maps-ssl.rule=Host(`maps.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.maps-ssl.entryPoints=websecure"
|
- "traefik.http.routers.maps-ssl.entryPoints=websecure"
|
||||||
- "traefik.http.routers.maps-ssl.tls=true"
|
- "traefik.http.routers.maps-ssl.tls=true"
|
||||||
- "traefik.http.routers.maps-ssl.service=maps"
|
- "traefik.http.routers.maps-ssl.service=maps"
|
||||||
|
@ -131,27 +99,19 @@ services:
|
||||||
environment:
|
environment:
|
||||||
DEBUG: "*"
|
DEBUG: "*"
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
# wait for files generated by "messages" container to exists
|
|
||||||
STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||||
ALLOW_ARTILLERY: "true"
|
ALLOW_ARTILLERY: "true"
|
||||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
|
||||||
MAX_PER_GROUP: "MAX_PER_GROUP"
|
|
||||||
REDIS_HOST: redis
|
|
||||||
NODE_ENV: development
|
|
||||||
STORE_VARIABLES_FOR_LOCAL_MAPS: "true"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.http.routers.back.rule=Host(`api.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.back.entryPoints=web"
|
- "traefik.http.routers.back.entryPoints=web"
|
||||||
- "traefik.http.services.back.loadbalancer.server.port=8080"
|
- "traefik.http.services.back.loadbalancer.server.port=8080"
|
||||||
- "traefik.http.routers.back-ssl.rule=Host(`api.workadventure.localhost`)"
|
- "traefik.http.routers.back-ssl.rule=Host(`api.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.back-ssl.entryPoints=websecure"
|
- "traefik.http.routers.back-ssl.entryPoints=websecure"
|
||||||
- "traefik.http.routers.back-ssl.tls=true"
|
- "traefik.http.routers.back-ssl.tls=true"
|
||||||
- "traefik.http.routers.back-ssl.service=back"
|
- "traefik.http.routers.back-ssl.service=back"
|
||||||
|
@ -166,15 +126,31 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploader:/usr/src/app
|
- ./uploader:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.http.routers.uploader.rule=Host(`uploader.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.uploader.entryPoints=web"
|
- "traefik.http.routers.uploader.entryPoints=web"
|
||||||
- "traefik.http.services.uploader.loadbalancer.server.port=8080"
|
- "traefik.http.services.uploader.loadbalancer.server.port=8080"
|
||||||
- "traefik.http.routers.uploader-ssl.rule=Host(`uploader.workadventure.localhost`)"
|
- "traefik.http.routers.uploader-ssl.rule=Host(`uploader.$HOST_NAME`)"
|
||||||
- "traefik.http.routers.uploader-ssl.entryPoints=websecure"
|
- "traefik.http.routers.uploader-ssl.entryPoints=websecure"
|
||||||
- "traefik.http.routers.uploader-ssl.tls=true"
|
- "traefik.http.routers.uploader-ssl.tls=true"
|
||||||
- "traefik.http.routers.uploader-ssl.service=uploader"
|
- "traefik.http.routers.uploader-ssl.service=uploader"
|
||||||
|
|
||||||
|
website:
|
||||||
|
image: thecodingmachine/nodejs:12-apache
|
||||||
|
environment:
|
||||||
|
STARTUP_COMMAND_1: npm install
|
||||||
|
STARTUP_COMMAND_2: npm run watch &
|
||||||
|
APACHE_DOCUMENT_ROOT: dist/
|
||||||
|
volumes:
|
||||||
|
- ./website:/var/www/html
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.website.rule=Host(`$HOST_NAME`)"
|
||||||
|
- "traefik.http.routers.website.entryPoints=web"
|
||||||
|
- "traefik.http.services.website.loadbalancer.server.port=80"
|
||||||
|
- "traefik.http.routers.website-ssl.rule=Host(`$HOST_NAME`)"
|
||||||
|
- "traefik.http.routers.website-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.website-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.website-ssl.service=website"
|
||||||
|
|
||||||
messages:
|
messages:
|
||||||
#image: thecodingmachine/nodejs:14
|
#image: thecodingmachine/nodejs:14
|
||||||
image: thecodingmachine/workadventure-back-base:latest
|
image: thecodingmachine/workadventure-back-base:latest
|
||||||
|
@ -187,55 +163,3 @@ services:
|
||||||
- ./back:/usr/src/back
|
- ./back:/usr/src/back
|
||||||
- ./front:/usr/src/front
|
- ./front:/usr/src/front
|
||||||
- ./pusher:/usr/src/pusher
|
- ./pusher:/usr/src/pusher
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:6
|
|
||||||
|
|
||||||
redisinsight:
|
|
||||||
image: redislabs/redisinsight:latest
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.redisinsight.entryPoints=web"
|
|
||||||
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
|
|
||||||
- "traefik.http.routers.redisinsight-ssl.rule=Host(`redis.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.redisinsight-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.redisinsight-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.redisinsight-ssl.service=redisinsight"
|
|
||||||
|
|
||||||
icon:
|
|
||||||
image: matthiasluedtke/iconserver:v3.13.0
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.icon.entryPoints=web"
|
|
||||||
- "traefik.http.services.icon.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.http.routers.icon-ssl.rule=Host(`icon.workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.icon-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.icon-ssl.service=icon"
|
|
||||||
|
|
||||||
# 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=coturn.workadventure.localhost
|
|
||||||
# - --server-name=coturn.workadventure.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
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Developer documentation
|
|
||||||
|
|
||||||
This (work in progress) documentation provides a number of "how-to" guides explaining how to work on the WorkAdventure
|
|
||||||
code.
|
|
||||||
|
|
||||||
This documentation is targeted at developers looking to open Pull Requests on WorkAdventure.
|
|
||||||
|
|
||||||
If you "only" want to design dynamic maps, please refer instead to the [scripting API documentation](https://workadventu.re/map-building/scripting.md).
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Check out the [contributing guide](../../CONTRIBUTING.md)
|
|
||||||
|
|
||||||
## Front documentation
|
|
||||||
|
|
||||||
- [How to add new functions in the scripting API](contributing-to-scripting-api.md)
|
|
|
@ -1,276 +0,0 @@
|
||||||
# How to add new functions in the scripting API
|
|
||||||
|
|
||||||
This documentation is intended at contributors who want to participate in the development of WorkAdventure itself.
|
|
||||||
Before reading this, please be sure you are familiar with the [scripting API](https://workadventu.re/map-building/scripting.md).
|
|
||||||
|
|
||||||
The [scripting API](https://workadventu.re/map-building/scripting.md) allows map developers to add dynamic features in their maps.
|
|
||||||
|
|
||||||
## Why extend the scripting API?
|
|
||||||
|
|
||||||
The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to
|
|
||||||
offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance,
|
|
||||||
you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!)
|
|
||||||
|
|
||||||
We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that
|
|
||||||
a feature is missing in the API. This article is here to explain to you how to add this feature.
|
|
||||||
|
|
||||||
## How to extend the scripting API?
|
|
||||||
|
|
||||||
Extending the scripting API means modifying the core of WorkAdventure. You can of course run these
|
|
||||||
modifications on your self-hosted instance.
|
|
||||||
But if you want to share it with the wider community, I strongly encourage you to start by [opening an issue](https://github.com/thecodingmachine/workadventure/issues)
|
|
||||||
on GitHub before starting the development. Check with the core maintainers that they are willing to merge your idea
|
|
||||||
before starting developing it. Once a new function makes it into the scripting API, it is very difficult to make it
|
|
||||||
evolve (or to deprecate), so the design of the function you add needs to be carefully considered.
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
|
|
||||||
Scripts are executed in the browser, inside an iframe.
|
|
||||||
|
|
||||||
![](images/scripting_1.svg)
|
|
||||||
|
|
||||||
The iframe allows WorkAdventure to isolate the script in a sandbox. Because the iframe is sandbox (or on a different
|
|
||||||
domain than the WorkAdventure server), scripts cannot directly manipulate the DOM of WorkAdventure. They also cannot
|
|
||||||
directly access Phaser objects (Phaser is the game engine used in WorkAdventure). This is by-design. Since anyone
|
|
||||||
can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be
|
|
||||||
a huge XSS security flaw).
|
|
||||||
|
|
||||||
Instead, the only way the script can interact with WorkAdventure is by sending messages using the
|
|
||||||
[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
|
|
||||||
|
|
||||||
![](images/scripting_2.svg)
|
|
||||||
|
|
||||||
We want to make life easy for map developers. So instead of asking them to directly send messages using the postMessage
|
|
||||||
API, we provide a nice library that does this work for them. This library is what we call the "Scripting API" (we sometimes
|
|
||||||
refer to it as the "Client API").
|
|
||||||
|
|
||||||
The scripting API provides the global `WA` object.
|
|
||||||
|
|
||||||
## A simple example
|
|
||||||
|
|
||||||
So let's take an example with a sample script:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
WA.chat.sendChatMessage('Hello world!', 'John Doe');
|
|
||||||
```
|
|
||||||
|
|
||||||
When this script is called, the scripting API is dispatching a JSON message to WorkAdventure.
|
|
||||||
|
|
||||||
In our case, the `sendChatMessage` function looks like this:
|
|
||||||
|
|
||||||
**src/Api/iframe/chat.ts**
|
|
||||||
```typescript
|
|
||||||
sendChatMessage(message: string, author: string) {
|
|
||||||
sendToWorkadventure({
|
|
||||||
type: "chat",
|
|
||||||
data: {
|
|
||||||
message: message,
|
|
||||||
author: author,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `sendToWorkadventure` function is a utility function that dispatches the message to the main frame.
|
|
||||||
|
|
||||||
In WorkAdventure, the message is received in the [`IframeListener` listener class](http://github.com/thecodingmachine/workadventure/blob/1e6ce4dec8697340e2c91798864b94da9528b482/front/src/Api/IframeListener.ts#L200-L203).
|
|
||||||
This class is in charge of analyzing the JSON messages received and dispatching them to the right place in the WorkAdventure application.
|
|
||||||
|
|
||||||
The message callback implemented in `IframeListener` is a giant (and disgusting) `if` statement branching to the correct
|
|
||||||
part of the code depending on the `type` property.
|
|
||||||
|
|
||||||
**src/Api/IframeListener.ts**
|
|
||||||
```typescript
|
|
||||||
// ...
|
|
||||||
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
|
|
||||||
this._setPropertyStream.next(payload.data);
|
|
||||||
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
|
|
||||||
scriptUtils.sendAnonymousChat(payload.data);
|
|
||||||
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
|
|
||||||
this._openPopupStream.next(payload.data);
|
|
||||||
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
In this particular case, we call `scriptUtils.sendAnonymousChat` that is doing the work of displaying the chat message.
|
|
||||||
|
|
||||||
## Scripting API entry point
|
|
||||||
|
|
||||||
The `WA` object originates from the scripting API. This script is hosted on the front server, at `https://[front_WA_server]/iframe_api.js.`.
|
|
||||||
|
|
||||||
The entry point for this script is the file `front/src/iframe_api.ts`.
|
|
||||||
All the other files dedicated to the iframe API are located in the `src/Api/iframe` directory.
|
|
||||||
|
|
||||||
## Utility functions to exchange messages
|
|
||||||
|
|
||||||
In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the
|
|
||||||
[`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function.
|
|
||||||
|
|
||||||
Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes.
|
|
||||||
We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this.
|
|
||||||
|
|
||||||
Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function
|
|
||||||
from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a
|
|
||||||
[`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function.
|
|
||||||
|
|
||||||
## Types
|
|
||||||
|
|
||||||
The JSON messages sent over the postMessage API are strictly defined using Typescript types.
|
|
||||||
Those types are not defined using classical Typescript interfaces.
|
|
||||||
|
|
||||||
Indeed, Typescript interfaces only exist at compilation time but cannot be enforced on runtime. The postMessage API
|
|
||||||
is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could
|
|
||||||
send specially crafted JSON packages to try to hack WA).
|
|
||||||
|
|
||||||
In WorkAdventure, we use the [generic-type-guard](https://github.com/mscharley/generic-type-guard) package. This package
|
|
||||||
allows us to create interfaces AND custom type guards in one go.
|
|
||||||
|
|
||||||
Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
sendToWorkadventure({
|
|
||||||
type: "chat",
|
|
||||||
data: {
|
|
||||||
message: message,
|
|
||||||
author: author,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import * as tg from "generic-type-guard";
|
|
||||||
|
|
||||||
export const isChatEvent = new tg.IsInterface()
|
|
||||||
.withProperties({
|
|
||||||
message: tg.isString,
|
|
||||||
author: tg.isString,
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
/**
|
|
||||||
* A message sent from the iFrame to the game to add a message in the chat.
|
|
||||||
*/
|
|
||||||
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the generic-type-guard library, we start by writing a type guard function (`isChatEvent`).
|
|
||||||
From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code.
|
|
||||||
|
|
||||||
The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received
|
|
||||||
over the postMessage API is indeed correctly formatted.
|
|
||||||
|
|
||||||
If you are not familiar with Typescript type guards, you can read [an introduction to type guards in the Typescript documentation](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards).
|
|
||||||
|
|
||||||
### Typing one way messages
|
|
||||||
|
|
||||||
For "one-way" messages (from the iframe to WorkAdventure), the `sendToWorkadventure` method expects the passed
|
|
||||||
object to be of type `IframeEvent<keyof IframeEventMap>`.
|
|
||||||
|
|
||||||
Note: I'd like here to thank @jonnytest1 for helping set up this type system. It rocks ;)
|
|
||||||
|
|
||||||
The `IFrameEvent` type is defined in `front/src/Api/Events/IframeEvent.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type IframeEventMap = {
|
|
||||||
loadPage: LoadPageEvent;
|
|
||||||
chat: ChatEvent;
|
|
||||||
openPopup: OpenPopupEvent;
|
|
||||||
closePopup: ClosePopupEvent;
|
|
||||||
openTab: OpenTabEvent;
|
|
||||||
// ...
|
|
||||||
// All the possible messages go here
|
|
||||||
// The key goes into the "type" JSON property
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
|
||||||
type: T;
|
|
||||||
data: IframeEventMap[T];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Similarly, if you want to type messages from WorkAdventure to the iframe, there is a very similar `IframeResponseEvent`.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface IframeResponseEventMap {
|
|
||||||
userInputChat: UserInputChatEvent;
|
|
||||||
enterEvent: EnterLeaveEvent;
|
|
||||||
leaveEvent: EnterLeaveEvent;
|
|
||||||
// ...
|
|
||||||
// All the possible messages go here
|
|
||||||
// The key goes into the "type" JSON property
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
|
||||||
type: T;
|
|
||||||
data: IframeResponseEventMap[T];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Typing queries (messages with answers)
|
|
||||||
|
|
||||||
If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to
|
|
||||||
define the type of the query and the type of the response.
|
|
||||||
|
|
||||||
The signature of `queryWorkadventure` is:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function queryWorkadventure<T extends keyof IframeQueryMap>(
|
|
||||||
content: IframeQuery<T>
|
|
||||||
): Promise<IframeQueryMap[T]["answer"]>
|
|
||||||
```
|
|
||||||
|
|
||||||
Yes, that's a bit cryptic. Hopefully, all you need to know is that to add a new query, you need to edit the `iframeQueryMapTypeGuards`
|
|
||||||
array in `front/src/Api/Events/IframeEvent.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const iframeQueryMapTypeGuards = {
|
|
||||||
openCoWebsite: {
|
|
||||||
query: isOpenCoWebsiteEvent,
|
|
||||||
answer: isCoWebsite,
|
|
||||||
},
|
|
||||||
getCoWebsites: {
|
|
||||||
query: tg.isUndefined,
|
|
||||||
answer: tg.isArray(isCoWebsite),
|
|
||||||
},
|
|
||||||
// ...
|
|
||||||
// the `query` key points to the type guard of the query
|
|
||||||
// the `answer` key points to the type guard of the response
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Responding to a query on the WorkAdventure side
|
|
||||||
|
|
||||||
In the WorkAdventure code, each possible query should be handled by what we call an "answerer".
|
|
||||||
|
|
||||||
Registering an answerer happens using the `iframeListener.registerAnswerer()` method.
|
|
||||||
|
|
||||||
Here is a sample:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => {
|
|
||||||
// ...
|
|
||||||
|
|
||||||
return /*...*/;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format
|
|
||||||
(the one you defined in the `answer` key of `iframeQueryMapTypeGuards`).
|
|
||||||
|
|
||||||
Important:
|
|
||||||
|
|
||||||
- there can be only one answerer registered for a given query type.
|
|
||||||
- if the answerer is not valid any more, you need to unregister the answerer using `iframeListener.unregisterAnswerer`.
|
|
||||||
|
|
||||||
|
|
||||||
## sendToWorkadventure VS queryWorkadventure
|
|
||||||
|
|
||||||
- `sendToWorkadventure` is used to send messages one way from the iframe to WorkAdventure. No response is expected. In particular
|
|
||||||
if an error happens in WorkAdventure, the iframe will not be notified.
|
|
||||||
- `queryWorkadventure` is used to send queries that expect an answer. If an error happens in WorkAdventure (i.e. if an
|
|
||||||
exception is raised), the exception will be propagated to the iframe.
|
|
||||||
|
|
||||||
Because `queryWorkadventure` handles exceptions properly, it can be interesting to use `queryWorkadventure` instead
|
|
||||||
of `sendToWorkadventure`, even for "one-way" messages. The return message type is simply `undefined` in this case.
|
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
# How to translate WorkAdventure
|
|
||||||
|
|
||||||
We use the [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n) package to handle the translation.
|
|
||||||
|
|
||||||
## Add a new language
|
|
||||||
|
|
||||||
It is very easy to add a new language!
|
|
||||||
|
|
||||||
First, in the `front/src/i18n` folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)).
|
|
||||||
|
|
||||||
In the previously created folder, add a file named index.ts with the following content containing your language information (french from France in this example):
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { Translation } from "../i18n-types";
|
|
||||||
|
|
||||||
const fr_FR: Translation = {
|
|
||||||
...en_US,
|
|
||||||
language: "Français",
|
|
||||||
country: "France",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default fr_FR;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Add a new key
|
|
||||||
|
|
||||||
### Add a simple key
|
|
||||||
|
|
||||||
The keys are searched by a path through the properties of the sub-objects and it is therefore advisable to write your translation as a JavaScript object.
|
|
||||||
|
|
||||||
Please use kamelcase to name your keys!
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
coffeMachine: {
|
|
||||||
start: "Coffe machine has been started!";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the code you can translate using `$LL`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import LL from "../../i18n/i18n-svelte";
|
|
||||||
|
|
||||||
console.log($LL.messages.coffeMachine.start());
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add a key with parameters
|
|
||||||
|
|
||||||
You can also use parameters to make the translation dynamic.
|
|
||||||
Use the tag { [parameter name] } to apply your parameters in the translations
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
messages: {
|
|
||||||
coffeMachine: {
|
|
||||||
playerStart: "{ playerName } started the coffee machine!";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the code you can use it like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
$LL.messages.coffeMachine.playerStart.start({
|
|
||||||
playerName: "John",
|
|
||||||
});
|
|
||||||
```
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 86 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 64 KiB |
|
@ -1,33 +0,0 @@
|
||||||
{.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:
|
|
||||||
|
|
||||||
|
|
||||||
<div class="px-5 card rounded d-inline-block">
|
|
||||||
<img class="document-img" src="images/anims/camera.png" alt="" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<figure class="figure">
|
|
||||||
<img class="figure-img img-fluid rounded" src="images/anims/animation_editor.png" alt="" />
|
|
||||||
<figcaption class="figure-caption">The tile animation editor</figcaption>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
You can preview animations directly in Tiled, using the "Show tile animations" option:
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<figure class="figure">
|
|
||||||
<img class="figure-img img-fluid rounded" src="images/anims/settings_show_animations.png" alt="" />
|
|
||||||
<figcaption class="figure-caption">The Show Tile Animations option</figcaption>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{.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.
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue