Compare commits
1 commit
develop
...
securityis
Author | SHA1 | Date | |
---|---|---|---|
be18a81e64 |
|
@ -1,2 +0,0 @@
|
||||||
**/node_modules/**
|
|
||||||
**/Dockerfile
|
|
|
@ -1,31 +1 @@
|
||||||
DEBUG_MODE=false
|
DEBUG_MODE=false
|
||||||
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=
|
|
||||||
ADMIN_API_TOKEN=123
|
|
||||||
START_ROOM_URL=/_/global/maps.workadventure.localhost/starter/map.json
|
|
||||||
# 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=
|
|
168
.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:
|
||||||
|
@ -26,21 +20,21 @@ 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@master
|
||||||
|
|
||||||
- name: "Build and push front image"
|
- name: "Build and push front image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: front/Dockerfile
|
dockerfile: front/Dockerfile
|
||||||
path: ./
|
path: front/
|
||||||
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:
|
||||||
|
@ -49,21 +43,21 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# 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@master
|
||||||
|
|
||||||
- name: "Build and push back image"
|
- name: "Build and push back image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: back/Dockerfile
|
dockerfile: back/Dockerfile
|
||||||
path: ./
|
path: back/
|
||||||
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-website:
|
||||||
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:
|
||||||
|
@ -72,133 +66,73 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# 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@master
|
||||||
|
|
||||||
- name: "Build and push back image"
|
- name: "Build and push back image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: pusher/Dockerfile
|
dockerfile: website/Dockerfile
|
||||||
path: ./
|
path: website/
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-pusher
|
repository: thecodingmachine/workadventure-website
|
||||||
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-uploader:
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- 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: uploader/Dockerfile
|
|
||||||
path: ./
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
repository: thecodingmachine/workadventure-uploader
|
|
||||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
|
||||||
add_git_labels: true
|
|
||||||
|
|
||||||
build-maps:
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- 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 front image"
|
|
||||||
uses: docker/build-push-action@v1
|
|
||||||
with:
|
|
||||||
dockerfile: maps/Dockerfile
|
|
||||||
path: maps/
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
repository: thecodingmachine/workadventure-maps
|
|
||||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
deeploy:
|
deeploy:
|
||||||
needs:
|
needs:
|
||||||
- build-front
|
- build-front
|
||||||
- build-back
|
- build-back
|
||||||
- build-pusher
|
|
||||||
- build-maps
|
|
||||||
- build-uploader
|
|
||||||
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
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# 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: 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@master
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
||||||
ADMIN_API_TOKEN: ${{ secrets.ADMIN_API_TOKEN }}
|
|
||||||
JITSI_ISS: ${{ secrets.JITSI_ISS }}
|
|
||||||
JITSI_URL: ${{ secrets.JITSI_URL }}
|
|
||||||
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
|
||||||
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
|
|
||||||
DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
|
||||||
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@v1
|
||||||
|
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@v1
|
||||||
|
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
|
@ -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
|
@ -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
|
|
106
.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"
|
||||||
|
|
||||||
|
@ -22,103 +20,26 @@ jobs:
|
||||||
- name: "Setup NodeJS"
|
- name: "Setup NodeJS"
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: '12.x'
|
||||||
|
|
||||||
- name: Install Protoc
|
|
||||||
uses: arduino/setup-protoc@v1
|
|
||||||
with:
|
|
||||||
version: '3.x'
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: yarn install
|
run: yarn install
|
||||||
working-directory: "front"
|
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"
|
- name: "Build"
|
||||||
run: yarn run build
|
run: yarn run build
|
||||||
env:
|
env:
|
||||||
PUSHER_URL: "//localhost:8080"
|
API_URL: "http://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"
|
||||||
|
|
||||||
|
@ -133,23 +54,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '12.x'
|
||||||
|
|
||||||
- name: Install Protoc
|
|
||||||
uses: arduino/setup-protoc@v1
|
|
||||||
with:
|
|
||||||
version: '3.x'
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: yarn install
|
run: yarn install
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
||||||
- name: "Install messages dependencies"
|
|
||||||
run: yarn install
|
|
||||||
working-directory: "messages"
|
|
||||||
|
|
||||||
- name: "Build proto messages"
|
|
||||||
run: yarn run proto && yarn run copy-to-back
|
|
||||||
working-directory: "messages"
|
|
||||||
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: yarn run tsc
|
run: yarn run tsc
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
@ -162,7 +70,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
|
@ -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
|
@ -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' }}
|
|
8
.gitignore
vendored
|
@ -2,10 +2,4 @@
|
||||||
.idea
|
.idea
|
||||||
.vagrant
|
.vagrant
|
||||||
Vagrantfile
|
Vagrantfile
|
||||||
docker-compose.override.yaml
|
docker-compose.override.yaml
|
||||||
*.DS_Store
|
|
||||||
maps/yarn.lock
|
|
||||||
maps/dist/computer.js
|
|
||||||
maps/dist/computer.js.map
|
|
||||||
node_modules
|
|
||||||
_
|
|
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
|
@ -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
|
@ -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.
|
|
Before Width: | Height: | Size: 16 KiB |
BIN
README-MAP.png
Before Width: | Height: | Size: 60 KiB |
84
README.md
|
@ -1,48 +1,84 @@
|
||||||
![](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)
|
# Work Adventure
|
||||||
![WorkAdventure office image](README-MAP.png)
|
|
||||||
|
|
||||||
Live demo [here](https://play.workadventu.re/@/tcm/workadventure/wa-village).
|
## Work in progress
|
||||||
|
|
||||||
# WorkAdventure
|
Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
|
||||||
|
|
||||||
WorkAdventure is a web-based collaborative workspace 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)
|
|
||||||
|
|
||||||
## Setting up a development environment
|
|
||||||
|
|
||||||
Install Docker.
|
Install Docker.
|
||||||
|
|
||||||
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``.
|
## Designing a map
|
||||||
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.
|
|
||||||
|
If you want to design your own map, you can use [Tiled](https://www.mapeditor.org/).
|
||||||
|
|
||||||
|
A few things to notice:
|
||||||
|
|
||||||
|
- your map can have as many layers as you want
|
||||||
|
- your map MUST contain a layer named "floorLayer" of type "objectgroup" that represents the layer on which characters will be drawn.
|
||||||
|
- the tilesets in your map MUST be embedded. You cannot refer to an external typeset in a TSX file. Click the "embed tileset" button in the tileset tab to embed tileset data.
|
||||||
|
- your map MUST be exported in JSON format. You need to use a recent version of Tiled to get JSON format export (1.3+)
|
||||||
|
- WorkAdventure doesn't support object layers and will ignore them
|
||||||
|
- If you are starting from a blank map, your map MUST be orthogonal and tiles size should be 32x32.
|
||||||
|
|
||||||
|
![](doc/images/tiled_screenshot_1.png)
|
||||||
|
|
||||||
|
### Defining a default entry point
|
||||||
|
|
||||||
|
In order to define a default start position, you MUST create a layer named "start" on your map.
|
||||||
|
This layer MUST contain at least one tile. The players will start on the tile of this layer.
|
||||||
|
If the layer contains many tiles selected, the players will start randomly on one of those tiles.
|
||||||
|
|
||||||
|
### Defining exits
|
||||||
|
|
||||||
|
In order to place an exit on your scene that leads to another scene:
|
||||||
|
|
||||||
|
- You must create a specific layer. When a character reaches ANY tile of that layer, it will exit the scene.
|
||||||
|
- In layer properties, you MUST add "exitSceneUrl" property. It represents the map URL of the next scene. For example : `/<map folder>/<map>.json`. Be careful, if you want the next map to be correctly loaded, you must check that the map files are in folder `back/src/Assets/Maps/<your map folder>`. The files will be accessible by url `<HOST>/map/files/<your map folder>/...`.
|
||||||
|
- In layer properties, you CAN add an "exitInstance" property. If set, you will join the map of the specified instance. Otherwise, you will stay on the same instance.
|
||||||
|
- If you want to have multiple exits, you can create many layers with name "exit". Each layer has a different key `exitSceneUrl` and have tiles that represent exits to another scene.
|
||||||
|
|
||||||
|
![](doc/images/exit_layer_map.png)
|
||||||
|
|
||||||
|
### Defining several entry points
|
||||||
|
|
||||||
|
Often your map will have several exits, and therefore, several entry points. For instance, if there
|
||||||
|
is an exit by a door that leads to the garden map, when you come back from the garden you expect to
|
||||||
|
come back by the same door. Therefore, a map can have several entry points.
|
||||||
|
Those entry points are "named" (they have a name).
|
||||||
|
|
||||||
|
In order to create a named entry point:
|
||||||
|
|
||||||
|
- You must create a specific layer. When a character enters the map by this entry point, it will enter the map randomly on ANY tile of that layer.
|
||||||
|
- In layer properties, you MUST add a boolean "startLayer" property. It should be set to true.
|
||||||
|
- The name of the entry point is the name of the layer
|
||||||
|
- To enter via this entry point, simply add a hash with the entry point name to the URL ("#[*startLayerName*]"). For instance: "https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point".
|
||||||
|
- You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)
|
||||||
|
|
||||||
|
|
||||||
### MacOS developers, your environment with Vagrant
|
### MacOS developers, your environment with Vagrant
|
||||||
|
|
||||||
|
@ -109,7 +145,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
|
@ -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.
|
|
|
@ -2,7 +2,7 @@
|
||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
# Box / OS
|
# Box / OS
|
||||||
VAGRANT_BOX = 'bento/ubuntu-20.04'
|
VAGRANT_BOX = 'bento/ubuntu-19.10'
|
||||||
|
|
||||||
# VM User — 'vagrant' by default
|
# VM User — 'vagrant' by default
|
||||||
VM_USER = 'vagrant'
|
VM_USER = 'vagrant'
|
||||||
|
@ -58,7 +58,7 @@ Vagrant.configure(2) do |config|
|
||||||
apt-get update -y
|
apt-get update -y
|
||||||
apt-get install -y git
|
apt-get install -y git
|
||||||
apt-get install -y apt-transport-https
|
apt-get install -y apt-transport-https
|
||||||
apt-get install -y ca-certificates
|
apt-get install -y build-essential
|
||||||
apt-get install -y curl
|
apt-get install -y curl
|
||||||
apt-get install -y gnupg-agent
|
apt-get install -y gnupg-agent
|
||||||
apt-get install -y software-properties-common
|
apt-get install -y software-properties-common
|
||||||
|
@ -66,8 +66,8 @@ Vagrant.configure(2) do |config|
|
||||||
apt-key fingerprint 0EBFCD88
|
apt-key fingerprint 0EBFCD88
|
||||||
add-apt-repository \
|
add-apt-repository \
|
||||||
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
|
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
|
||||||
$(lsb_release -cs) \
|
$(lsb_release -cs) \
|
||||||
stable"
|
stable"
|
||||||
apt-get update -y
|
apt-get update -y
|
||||||
apt-get install -y docker-ce docker-ce-cli containerd.io
|
apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||||
curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
|
|
@ -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,9 @@
|
||||||
# protobuf build
|
FROM thecodingmachine/nodejs:12
|
||||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
|
|
||||||
WORKDIR /usr/src
|
|
||||||
COPY messages .
|
|
||||||
RUN yarn install && yarn proto
|
|
||||||
|
|
||||||
# typescript build
|
COPY --chown=docker:docker . .
|
||||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2
|
|
||||||
WORKDIR /usr/src
|
|
||||||
COPY back/yarn.lock back/package.json ./
|
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
COPY back .
|
|
||||||
COPY --from=builder /usr/src/generated src/Messages/generated
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
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
|
ENV NODE_ENV=production
|
||||||
RUN yarn install --production
|
|
||||||
|
|
||||||
USER node
|
CMD ["yarn", "run", "prod"]
|
||||||
CMD ["yarn", "run", "runprod"]
|
|
||||||
|
|
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)
|
|
@ -5,16 +5,12 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"dev": "ts-node-dev --respawn ./server.ts",
|
"dev": "ts-node-dev --respawn --transpileOnly ./server.ts",
|
||||||
"prod": "tsc && node --max-old-space-size=4096 ./dist/server.js",
|
"prod": "tsc && node ./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,46 +36,28 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@workadventure/tiled-map-type-guard": "^1.0.3",
|
"@types/express": "^4.17.4",
|
||||||
"axios": "^0.21.2",
|
"@types/http-status-codes": "^1.2.0",
|
||||||
"busboy": "^0.3.1",
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
"circular-json": "^0.5.9",
|
"@types/socket.io": "^2.1.4",
|
||||||
"debug": "^4.3.1",
|
"@types/uuidv4": "^5.0.0",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
"generic-type-guard": "^3.2.0",
|
"generic-type-guard": "^3.2.0",
|
||||||
"google-protobuf": "^3.13.0",
|
"http-status-codes": "^1.4.0",
|
||||||
"grpc": "^1.24.4",
|
|
||||||
"ipaddr.js": "^2.0.1",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mkdirp": "^1.0.4",
|
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"query-string": "^6.13.3",
|
"socket.io": "^2.3.0",
|
||||||
"redis": "^3.1.2",
|
"systeminformation": "^4.26.5",
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
"ts-node-dev": "^1.0.0-pre.44",
|
||||||
|
"typescript": "^3.8.3",
|
||||||
"uuidv4": "^6.0.7"
|
"uuidv4": "^6.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/busboy": "^0.2.3",
|
|
||||||
"@types/circular-json": "^0.4.0",
|
|
||||||
"@types/debug": "^4.1.5",
|
|
||||||
"@types/google-protobuf": "^3.7.3",
|
|
||||||
"@types/http-status-codes": "^1.2.0",
|
|
||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.10",
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@types/mkdirp": "^1.0.1",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
"@types/redis": "^2.8.31",
|
"eslint": "^6.8.0",
|
||||||
"@types/uuidv4": "^5.0.0",
|
"jasmine": "^3.5.0"
|
||||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
|
||||||
"@typescript-eslint/parser": "^5.8.0",
|
|
||||||
"eslint": "^8.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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
148
back/position-test.js
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
// Constants
|
||||||
|
let MIN_DISTANCE = 12;
|
||||||
|
let MAX_PER_GROUP = 3;
|
||||||
|
let NB_USERS = 10;
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
let rand = function(min, max) {
|
||||||
|
min = Math.ceil(min);
|
||||||
|
max = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
};
|
||||||
|
|
||||||
|
let compareDistances = function(distA, distB) {
|
||||||
|
if (distA.distance < distB.distance) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (distA.distance > distB.distance) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let computeDistance = function (user1, user2) {
|
||||||
|
return Math.sqrt(Math.pow(user2.X - user1.X, 2) + Math.pow(user2.Y - user1.Y, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test Data
|
||||||
|
let users = [];
|
||||||
|
for(let i = 1; i <= NB_USERS; i++) {
|
||||||
|
let user = {};
|
||||||
|
user.id = rand(0,99999);
|
||||||
|
user.X = rand(0, 40);
|
||||||
|
user.Y = rand(0, 40);
|
||||||
|
users.push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute distance between each user
|
||||||
|
let getDistanceOfEachUser = function(users) {
|
||||||
|
let i = 0;
|
||||||
|
let distances = [];
|
||||||
|
|
||||||
|
users.forEach(function(user1, key1) {
|
||||||
|
users.forEach(function(user2, key2) {
|
||||||
|
if(key1 < key2) {
|
||||||
|
let distanceObj = {};
|
||||||
|
distanceObj.distance = computeDistance(user1, user2);
|
||||||
|
distanceObj.first = user1;
|
||||||
|
distanceObj.second = user2;
|
||||||
|
|
||||||
|
distances[i] = distanceObj;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Organise groups
|
||||||
|
let createGroups = function(distances) {
|
||||||
|
let i = 0;
|
||||||
|
let groups = [];
|
||||||
|
let alreadyInAGroup = [];
|
||||||
|
|
||||||
|
for(let j = 0; j < distances.length; j++) {
|
||||||
|
let dist = distances[j];
|
||||||
|
|
||||||
|
if(dist.distance <= MIN_DISTANCE) {
|
||||||
|
if(typeof groups[i] === 'undefined') {
|
||||||
|
groups[i] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(groups[i].indexOf(dist.first) === -1 && typeof alreadyInAGroup[dist.first.id] === 'undefined') {
|
||||||
|
if(groups[i].length > 1) {
|
||||||
|
// if group is not empty we check current user can be added in the group according to its distance to the others already in it
|
||||||
|
for(let l = 0; l < groups[i].length; l++) {
|
||||||
|
let userTotest = groups[i][l];
|
||||||
|
if(computeDistance(dist.first, userTotest) <= MIN_DISTANCE) {
|
||||||
|
groups[i].push(dist.first);
|
||||||
|
alreadyInAGroup[dist.first.id] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
groups[i].push(dist.first);
|
||||||
|
alreadyInAGroup[dist.first.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(groups[i].length === MAX_PER_GROUP) {
|
||||||
|
i++; // on créé un nouveau groupe
|
||||||
|
if(i > (NB_USERS / MAX_PER_GROUP)) {
|
||||||
|
console.log('There is no room left for user ID : ' + dist.second.id + ' !');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(groups[i].indexOf(dist.second) === -1 && typeof alreadyInAGroup[dist.second.id] === 'undefined') {
|
||||||
|
if(groups[i].length > 1) {
|
||||||
|
// if group is not empty we check current user can be added in the group according to its distance to the others already in it
|
||||||
|
for(let l = 0; l < groups[i].length; l++) {
|
||||||
|
let userTotest = groups[i][l];
|
||||||
|
if(computeDistance(dist.second, userTotest) <= MIN_DISTANCE) {
|
||||||
|
groups[i].push(dist.second);
|
||||||
|
alreadyInAGroup[dist.second.id] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
groups[i].push(dist.second);
|
||||||
|
alreadyInAGroup[dist.second.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
let distances = getDistanceOfEachUser(users);
|
||||||
|
|
||||||
|
// ordonner par distance pour prioriser l'association en groupe des utilisateurs les plus proches
|
||||||
|
distances.sort(compareDistances);
|
||||||
|
|
||||||
|
let groups = createGroups(distances);
|
||||||
|
|
||||||
|
// Compute distance between each user of a already existing group
|
||||||
|
let checkGroupDistance = function(groups) {
|
||||||
|
for(let i = 0; i < groups.length; i++) {
|
||||||
|
let group = groups[i];
|
||||||
|
group.forEach(function(user1, key1) {
|
||||||
|
group.forEach(function(user2, key2) {
|
||||||
|
if(key1 < key2) {
|
||||||
|
let distance = computeDistance(user1, user2);
|
||||||
|
if(distance > MIN_DISTANCE) {
|
||||||
|
// TODO : message a user1 et user2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(users);
|
||||||
|
console.log(distances);
|
||||||
|
console.log(groups);
|
||||||
|
|
|
@ -1,15 +1,3 @@
|
||||||
// lib/server.ts
|
// lib/server.ts
|
||||||
import App from "./src/App";
|
import App from "./src/App";
|
||||||
import grpc from "grpc";
|
App.listen(8080, () => console.log(`Example app listening on port 8080!`))
|
||||||
import { roomManager } from "./src/RoomManager";
|
|
||||||
import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb";
|
|
||||||
import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT));
|
|
||||||
|
|
||||||
const server = new grpc.Server();
|
|
||||||
server.addService<IRoomManagerServer>(RoomManagerService, roomManager);
|
|
||||||
|
|
||||||
server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure());
|
|
||||||
server.start();
|
|
||||||
console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT);
|
|
|
@ -1,19 +1,55 @@
|
||||||
// lib/app.ts
|
// lib/app.ts
|
||||||
import { PrometheusController } from "./Controller/PrometheusController";
|
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
||||||
import { DebugController } from "./Controller/DebugController";
|
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
||||||
import { App as uwsApp } from "./Server/sifrr.server";
|
import express from "express";
|
||||||
|
import {Application, Request, Response} from 'express';
|
||||||
|
import bodyParser = require('body-parser');
|
||||||
|
import * as http from "http";
|
||||||
|
import {MapController} from "./Controller/MapController";
|
||||||
|
import {PrometheusController} from "./Controller/PrometheusController";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: uwsApp;
|
public app: Application;
|
||||||
|
public server: http.Server;
|
||||||
|
public ioSocketController: IoSocketController;
|
||||||
|
public authenticateController: AuthenticateController;
|
||||||
|
public mapController: MapController;
|
||||||
public prometheusController: PrometheusController;
|
public prometheusController: PrometheusController;
|
||||||
private debugController: DebugController;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = new uwsApp();
|
this.app = express();
|
||||||
|
|
||||||
this.prometheusController = new PrometheusController(this.app);
|
//config server http
|
||||||
this.debugController = new DebugController(this.app);
|
this.server = http.createServer(this.app);
|
||||||
|
|
||||||
|
this.config();
|
||||||
|
this.crossOrigin();
|
||||||
|
|
||||||
|
//TODO add middleware with access token to secure api
|
||||||
|
|
||||||
|
//create socket controllers
|
||||||
|
this.ioSocketController = new IoSocketController(this.server);
|
||||||
|
this.authenticateController = new AuthenticateController(this.app);
|
||||||
|
this.mapController = new MapController(this.app);
|
||||||
|
this.prometheusController = new PrometheusController(this.app, this.ioSocketController);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add session user
|
||||||
|
private config(): void {
|
||||||
|
this.app.use(bodyParser.json());
|
||||||
|
this.app.use(bodyParser.urlencoded({extended: false}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private crossOrigin(){
|
||||||
|
this.app.use((req: Request, res: Response, next) => {
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*"); // update to match the domain you will make the request from
|
||||||
|
// Request methods you wish to allow
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
||||||
|
// Request headers you wish to allow
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
|
next();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new App().app;
|
export default new App().server;
|
||||||
|
|
986
back/src/Assets/Maps/Floor0/floor0.json
Normal file
BIN
back/src/Assets/Maps/Floor0/floortileset.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
back/src/Assets/Maps/Floor0/tilesets_deviant_milkian_1.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
back/src/Assets/Maps/Floor1/FloorTile_S.jpg
Normal file
After Width: | Height: | Size: 671 KiB |
513
back/src/Assets/Maps/Floor1/floor1.json
Normal file
|
@ -0,0 +1,513 @@
|
||||||
|
{ "compressionlevel":-1,
|
||||||
|
"editorsettings":
|
||||||
|
{
|
||||||
|
"export":
|
||||||
|
{
|
||||||
|
"target":"."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"height":18,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4541, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4541, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":18,
|
||||||
|
"id":12,
|
||||||
|
"name":"start",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168],
|
||||||
|
"height":18,
|
||||||
|
"id":1,
|
||||||
|
"name":"bottom",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4541, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4541, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":18,
|
||||||
|
"id":11,
|
||||||
|
"name":"exit",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"exitSceneUrl",
|
||||||
|
"type":"string",
|
||||||
|
"value":"..\/Floor0\/floor0.json#down-the-stairs"
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 4797, 4798, 4799, 0, 0, 0, 0, 0, 4638, 4638, 4638, 4638, 4526, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 4729, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4638, 4638, 4638, 4638, 4526, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 4745, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4526, 4526, 4526, 4526, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 4761, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4639, 4639, 4639, 4639, 4526, 0, 0, 4526, 0, 0, 4526, 4526, 4526, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 4777, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4639, 4639, 4639, 4639, 4526, 0, 0, 4526, 0, 0, 4526, 0, 0, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 0, 0, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4526, 4526, 4526, 4526, 0, 0, 4526, 0, 0, 4526, 0, 0, 4526, 4638, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4650, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4638, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4638, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 0, 0, 4526, 4526, 4526, 0, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 0, 0, 4526, 4526, 0, 0, 0, 0, 0, 4526, 4526, 0, 0, 4526, 4526, 0, 0, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4729, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 4526, 4526, 4526, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4745, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 4526, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4761, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 4526, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4777, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 4526, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526, 4526],
|
||||||
|
"height":18,
|
||||||
|
"id":5,
|
||||||
|
"name":"wall",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4665, 0, 0, 0, 0, 4603, 4603, 4668, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4800, 4800, 0, 4614, 0, 0, 0, 4615, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4601, 4602, 0, 0, 0, 4667, 4619, 4619, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4641, 4642, 0, 4630, 0, 0, 0, 4631, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4601, 4602, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4657, 4658, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4617, 4618, 4649, 0, 0, 0, 4603, 4605, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4666, 0, 0, 0, 0, 0, 4619, 4621, 4668, 0, 0, 4625, 4626, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4615, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4749, 0, 0, 0, 0, 0, 0, 4631, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4614, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4630, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4667, 4603, 4605, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4665, 0, 4665, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4619, 4621, 0, 0, 0, 0, 4665, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4705, 4706, 4706, 4706, 4707, 4668, 0, 0, 4729, 0, 0, 0, 0, 0, 4665, 0, 0, 0, 0, 0, 4603, 4601, 4602, 0, 0, 0, 4604, 4601, 4602, 0, 4603, 4601, 4602, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4667, 4721, 4722, 4722, 4722, 4723, 0, 0, 0, 4745, 0, 0, 0, 4603, 4601, 4602, 0, 0, 0, 0, 0, 4619, 4617, 4618, 0, 0, 0, 4620, 4617, 4618, 0, 4603, 4617, 4618, 0, 0, 0, 0, 4708, 4709, 4710, 0, 0, 0, 0, 0, 0, 4737, 4738, 4738, 4738, 4739, 0, 0, 0, 4761, 0, 0, 0, 4619, 4617, 4618, 4766, 0, 0, 0, 0, 0, 4666, 0, 0, 0, 0, 0, 0, 0, 0, 4619, 0, 0, 0, 0, 0, 0, 4724, 4725, 4726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4777, 0, 0, 0, 0, 4666, 0, 0, 4800, 0, 4800, 0, 0, 0, 0, 0, 4800, 4800, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":18,
|
||||||
|
"id":6,
|
||||||
|
"name":"things",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4743, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4665, 0, 4665, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4759, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4705, 4706, 4706, 4706, 4707, 4668, 0, 0, 4633, 4796, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4667, 4721, 4722, 4722, 4722, 4723, 0, 0, 0, 4795, 4634, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4737, 4738, 4738, 4738, 4739, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4634, 4720, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4733, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4749, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4743, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4759, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4795, 4796, 0, 0, 0, 0, 0, 4634, 0, 4782, 4795, 4634, 0, 0, 0, 0, 4665, 0, 4665, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4795, 4796, 0, 0, 0, 0, 4649, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4667, 0, 0, 0, 4668, 0, 0, 0, 0, 0, 0, 4666, 0, 4666, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4666, 0, 0, 0, 4666, 0, 0, 0, 0, 0, 4666, 0, 4666, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4649, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":18,
|
||||||
|
"id":7,
|
||||||
|
"name":"floor",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 4583, 4584, 0, 0, 0, 4583, 4584, 0, 0, 0, 4583, 4584, 0, 0, 0, 0, 4583, 4584, 0, 0, 0, 4583, 4584, 0, 0, 0, 4583, 4584, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4666, 0, 4666, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4583, 4584, 0, 0, 0, 4583, 4584, 0, 0, 0, 0, 0, 0, 0, 4583, 4584, 0, 0, 0, 0, 0, 4583, 4584, 0, 0, 0, 0, 4583, 4584, 0, 0, 0, 4583, 4584, 0, 0, 0, 0, 0, 4583, 4584, 0, 0, 0],
|
||||||
|
"height":18,
|
||||||
|
"id":8,
|
||||||
|
"name":"ecran",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"floorLayer",
|
||||||
|
"objects":[],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4742, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4758, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4609, 4610, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4717, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4733, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4526, 4526, 0, 0, 4526, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4742, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4758, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4734, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4750, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":18,
|
||||||
|
"id":9,
|
||||||
|
"name":"floor_depth1",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4717, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":18,
|
||||||
|
"id":10,
|
||||||
|
"name":"floor_depth2",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":46,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":14,
|
||||||
|
"nextobjectid":1,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"1.3.3",
|
||||||
|
"tileheight":32,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":66,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"FloorTile_S.jpg",
|
||||||
|
"imageheight":2180,
|
||||||
|
"imagewidth":2141,
|
||||||
|
"margin":0,
|
||||||
|
"name":"FloorTile_S",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":4488,
|
||||||
|
"tileheight":32,
|
||||||
|
"tilewidth":32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns":8,
|
||||||
|
"firstgid":4489,
|
||||||
|
"image":"floortileset.png",
|
||||||
|
"imageheight":256,
|
||||||
|
"imagewidth":256,
|
||||||
|
"margin":0,
|
||||||
|
"name":"floortileset",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":64,
|
||||||
|
"tileheight":32,
|
||||||
|
"tiles":[
|
||||||
|
{
|
||||||
|
"id":37,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"tilewidth":32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns":16,
|
||||||
|
"firstgid":4553,
|
||||||
|
"image":"tilesets_deviant_milkian_1.png",
|
||||||
|
"imageheight":512,
|
||||||
|
"imagewidth":512,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tilesets_deviant_milkian_1",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":256,
|
||||||
|
"tileheight":32,
|
||||||
|
"tiles":[
|
||||||
|
{
|
||||||
|
"id":48,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":49,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":50,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":51,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":52,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":64,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":65,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":66,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":67,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":68,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":72,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":73,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":152,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":153,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":154,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":155,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":156,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":157,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":168,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":169,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":170,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":176,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":177,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":178,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":192,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":193,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":194,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":196,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":208,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":209,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":210,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":213,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":224,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":225,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":226,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"tilewidth":32
|
||||||
|
}],
|
||||||
|
"tilewidth":32,
|
||||||
|
"type":"map",
|
||||||
|
"version":1.2,
|
||||||
|
"width":46
|
||||||
|
}
|
BIN
back/src/Assets/Maps/Floor1/floortileset.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
back/src/Assets/Maps/Floor1/tilesets_deviant_milkian_1.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
back/src/Assets/Maps/Lyon/floortileset.png
Normal file
After Width: | Height: | Size: 85 KiB |
|
@ -1,63 +1,71 @@
|
||||||
{ "compressionlevel":-1,
|
{ "compressionlevel":-1,
|
||||||
"height":10,
|
"editorsettings":
|
||||||
|
{
|
||||||
|
"chunksize":
|
||||||
|
{
|
||||||
|
"height":32,
|
||||||
|
"width":32
|
||||||
|
},
|
||||||
|
"export":
|
||||||
|
{
|
||||||
|
"format":"json",
|
||||||
|
"target":"map.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"height":16,
|
||||||
"infinite":false,
|
"infinite":false,
|
||||||
"layers":[
|
"layers":[
|
||||||
{
|
{
|
||||||
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
"height":10,
|
"height":16,
|
||||||
"id":1,
|
"id":8,
|
||||||
"name":"floor",
|
|
||||||
"opacity":1,
|
|
||||||
"type":"tilelayer",
|
|
||||||
"visible":true,
|
|
||||||
"width":10,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
||||||
"height":10,
|
|
||||||
"id":2,
|
|
||||||
"name":"start",
|
"name":"start",
|
||||||
"opacity":1,
|
"opacity":1,
|
||||||
"type":"tilelayer",
|
"type":"tilelayer",
|
||||||
"visible":true,
|
"visible":true,
|
||||||
"width":10,
|
"width":15,
|
||||||
"x":0,
|
"x":0,
|
||||||
"y":0
|
"y":0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data":[0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, 0, 0, 0, 0, 0, 23, 23, 23, 23, 23],
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
"height":10,
|
"height":16,
|
||||||
"id":5,
|
"id":9,
|
||||||
"name":"first_cowebsite",
|
"name":"exit",
|
||||||
"opacity":1,
|
|
||||||
"type":"tilelayer",
|
|
||||||
"visible":true,
|
|
||||||
"width":10,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, 0, 0, 0, 0, 0, 12, 12, 12, 12, 12],
|
|
||||||
"height":10,
|
|
||||||
"id":7,
|
|
||||||
"name":"second_cowebsite",
|
|
||||||
"opacity":1,
|
"opacity":1,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"jitsiRoom",
|
"name":"exitSceneUrl",
|
||||||
"type":"string",
|
"type":"string",
|
||||||
"value":"ChillZone"
|
"value":"..\/Floor0\/floor0.json"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"jitsiTrigger",
|
|
||||||
"type":"string",
|
|
||||||
"value":"onaction"
|
|
||||||
}],
|
}],
|
||||||
"type":"tilelayer",
|
"type":"tilelayer",
|
||||||
"visible":true,
|
"visible":true,
|
||||||
"width":10,
|
"width":15,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 295, 295, 295, 294, 294, 294],
|
||||||
|
"height":16,
|
||||||
|
"id":2,
|
||||||
|
"name":"bottom",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":15,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 29, 29, 29, 29, 29, 29, 30, 2, 3, 0, 0, 0, 0, 0, 0, 45, 45, 45, 45, 45, 45, 46, 18, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 35, 0, 0, 0, 0, 0, 0, 0, 0, 53, 53, 0, 0, 0, 248, 180, 0, 0, 0, 0, 0, 0, 0, 115, 52, 53, 116, 0, 0, 95, 196, 0, 0, 0, 0, 0, 0, 0, 0, 68, 69, 0, 0, 0, 111, 212, 0, 0, 0, 0, 0, 0, 0, 0, 49, 50, 0, 0, 0, 248, 228, 0, 0, 0, 0, 0, 0, 0, 0, 65, 66, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 0, 0, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 53, 0, 0, 0, 0, 0, 0, 0, 113, 0, 113, 0, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 49, 50, 49, 50, 53, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 49, 50, 49, 50, 69, 116, 0, 115, 53, 0, 0, 0, 0, 0, 0, 65, 66, 65, 66, 213, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0, 114, 0, 114, 0, 229, 0, 0, 0, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":16,
|
||||||
|
"id":1,
|
||||||
|
"name":"top",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":15,
|
||||||
"x":0,
|
"x":0,
|
||||||
"y":0
|
"y":0
|
||||||
},
|
},
|
||||||
|
@ -65,25 +73,7 @@
|
||||||
"draworder":"topdown",
|
"draworder":"topdown",
|
||||||
"id":3,
|
"id":3,
|
||||||
"name":"floorLayer",
|
"name":"floorLayer",
|
||||||
"objects":[
|
"objects":[],
|
||||||
{
|
|
||||||
"height":220.405430001517,
|
|
||||||
"id":1,
|
|
||||||
"name":"Tests",
|
|
||||||
"rotation":0,
|
|
||||||
"text":
|
|
||||||
{
|
|
||||||
"fontfamily":"Sans Serif",
|
|
||||||
"pixelsize":8,
|
|
||||||
"text":"Test 1:\nEnter \/cowebsite open https:\/\/wikipedia.com on the chat\nResult:\nA cowebsite must have been opened\n\nDo the first test 4 more times\n\nTest 2:\nEnter \/cowebsite close 0 on the chat\nResult:\nThe main co-website has been closed\n\nTest 3:\nEnter \/cowebsite close all on the chat\nResult:\nAll co-websites has been closed\n\nTest 4:\nGo on the white carpet to open a Jitsi & open a co-website \/cowebsite open https:\/\/wikipedia.com on the chat\nResult:\nThere are two co-websites",
|
|
||||||
"wrap":true
|
|
||||||
},
|
|
||||||
"type":"",
|
|
||||||
"visible":true,
|
|
||||||
"width":316.770833333333,
|
|
||||||
"x":1.64026713939023,
|
|
||||||
"y":97.5557662166938
|
|
||||||
}],
|
|
||||||
"opacity":1,
|
"opacity":1,
|
||||||
"type":"objectgroup",
|
"type":"objectgroup",
|
||||||
"visible":true,
|
"visible":true,
|
||||||
|
@ -91,96 +81,54 @@
|
||||||
"y":0
|
"y":0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 82, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 27, 0, 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 190, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 207, 206, 0, 0, 0, 0, 231, 0, 0, 0, 0, 0, 0, 0, 0, 175, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 190, 0, 0, 0, 0, 0, 165, 0, 0, 0, 0, 0, 0, 0, 0, 206, 176, 0, 0, 0, 0, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 190, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 243, 244, 243, 244, 206, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 175, 0, 241, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
"height":10,
|
"height":16,
|
||||||
"id":8,
|
"id":7,
|
||||||
"name":"objects",
|
"name":"books",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"depth",
|
||||||
|
"type":"int",
|
||||||
|
"value":3
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":15,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":16,
|
||||||
|
"id":6,
|
||||||
|
"name":"override",
|
||||||
"opacity":1,
|
"opacity":1,
|
||||||
"type":"tilelayer",
|
"type":"tilelayer",
|
||||||
"visible":true,
|
"visible":true,
|
||||||
"width":10,
|
"width":15,
|
||||||
"x":0,
|
"x":0,
|
||||||
"y":0
|
"y":0
|
||||||
}],
|
}],
|
||||||
"nextlayerid":9,
|
"nextlayerid":10,
|
||||||
"nextobjectid":3,
|
"nextobjectid":1,
|
||||||
"orientation":"orthogonal",
|
"orientation":"orthogonal",
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"script",
|
|
||||||
"type":"string",
|
|
||||||
"value":"script.js"
|
|
||||||
}],
|
|
||||||
"renderorder":"right-down",
|
"renderorder":"right-down",
|
||||||
"tiledversion":"1.7.2",
|
"tiledversion":"1.3.3",
|
||||||
"tileheight":32,
|
"tileheight":32,
|
||||||
"tilesets":[
|
"tilesets":[
|
||||||
{
|
{
|
||||||
"columns":11,
|
"columns":16,
|
||||||
"firstgid":1,
|
"firstgid":1,
|
||||||
"image":"tileset1.png",
|
"image":"tilesets_deviant_milkian_1.png",
|
||||||
"imageheight":352,
|
"imageheight":512,
|
||||||
"imagewidth":352,
|
"imagewidth":512,
|
||||||
"margin":0,
|
"margin":0,
|
||||||
"name":"tileset1",
|
"name":"office_1",
|
||||||
"spacing":0,
|
"spacing":0,
|
||||||
"tilecount":121,
|
"tilecount":256,
|
||||||
"tileheight":32,
|
"tileheight":32,
|
||||||
"tiles":[
|
"tiles":[
|
||||||
{
|
|
||||||
"id":1,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":2,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":3,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":4,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":5,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":6,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id":7,
|
"id":7,
|
||||||
"properties":[
|
"properties":[
|
||||||
|
@ -190,44 +138,26 @@
|
||||||
"value":true
|
"value":true
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id":8,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":9,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":10,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id":12,
|
"id":12,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
"type":"bool",
|
"type":"bool",
|
||||||
"value":true
|
"value":false
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":16,
|
"id":13,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":14,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -236,43 +166,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":17,
|
"id":15,
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":18,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":19,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":20,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":21,
|
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -289,42 +183,6 @@
|
||||||
"value":true
|
"value":true
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id":24,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":25,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":26,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":27,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id":28,
|
"id":28,
|
||||||
"properties":[
|
"properties":[
|
||||||
|
@ -362,7 +220,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":32,
|
"id":39,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -371,39 +229,12 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":34,
|
"id":44,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
"type":"bool",
|
"type":"bool",
|
||||||
"value":true
|
"value":false
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":35,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":42,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":43,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -412,11 +243,11 @@
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
"type":"bool",
|
"type":"bool",
|
||||||
"value":true
|
"value":false
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":46,
|
"id":48,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -425,7 +256,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":59,
|
"id":49,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -434,7 +265,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":60,
|
"id":50,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -443,7 +274,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":70,
|
"id":51,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -452,7 +283,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":71,
|
"id":52,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -461,7 +292,25 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":80,
|
"id":56,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":57,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":64,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -470,7 +319,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":81,
|
"id":65,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -479,7 +328,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":89,
|
"id":66,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -488,7 +337,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":91,
|
"id":67,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -497,7 +346,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":93,
|
"id":68,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -506,7 +355,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":94,
|
"id":72,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -515,7 +364,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":95,
|
"id":73,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -524,7 +373,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":96,
|
"id":84,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -533,7 +382,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":97,
|
"id":152,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -542,7 +391,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":100,
|
"id":153,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -551,7 +400,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":102,
|
"id":154,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -560,7 +409,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":103,
|
"id":155,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -569,7 +418,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":104,
|
"id":156,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -578,7 +427,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":105,
|
"id":157,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -587,7 +436,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":106,
|
"id":161,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -596,7 +445,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":107,
|
"id":162,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -605,7 +454,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":108,
|
"id":168,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -614,7 +463,7 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":114,
|
"id":169,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -623,7 +472,147 @@
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":115,
|
"id":170,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":171,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":172,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":173,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":177,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":178,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":184,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":185,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":186,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":196,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":198,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":212,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":214,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":228,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"collides",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"tilewidth":32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns":8,
|
||||||
|
"firstgid":257,
|
||||||
|
"image":"floortileset.png",
|
||||||
|
"imageheight":256,
|
||||||
|
"imagewidth":256,
|
||||||
|
"margin":0,
|
||||||
|
"name":"floortileset",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":64,
|
||||||
|
"tileheight":32,
|
||||||
|
"tiles":[
|
||||||
|
{
|
||||||
|
"id":37,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"collides",
|
"name":"collides",
|
||||||
|
@ -635,6 +624,6 @@
|
||||||
}],
|
}],
|
||||||
"tilewidth":32,
|
"tilewidth":32,
|
||||||
"type":"map",
|
"type":"map",
|
||||||
"version":"1.6",
|
"version":1.2,
|
||||||
"width":10
|
"width":15
|
||||||
}
|
}
|
BIN
back/src/Assets/Maps/Lyon/tilesets_deviant_milkian_1.png
Normal file
After Width: | Height: | Size: 33 KiB |
40
back/src/Controller/AuthenticateController.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import {Application, Request, Response} from "express";
|
||||||
|
import Jwt from "jsonwebtoken";
|
||||||
|
import {BAD_REQUEST, OK} from "http-status-codes";
|
||||||
|
import {SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||||
|
import { uuid } from 'uuidv4';
|
||||||
|
|
||||||
|
export interface TokenInterface {
|
||||||
|
name: string,
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthenticateController {
|
||||||
|
App : Application;
|
||||||
|
|
||||||
|
constructor(App : Application) {
|
||||||
|
this.App = App;
|
||||||
|
this.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
//permit to login on application. Return token to connect on Websocket IO.
|
||||||
|
login(){
|
||||||
|
// For now, let's completely forget the /login route.
|
||||||
|
this.App.post("/login", (req: Request, res: Response) => {
|
||||||
|
const param = req.body;
|
||||||
|
/*if(!param.name){
|
||||||
|
return res.status(BAD_REQUEST).send({
|
||||||
|
message: "email parameter is empty"
|
||||||
|
});
|
||||||
|
}*/
|
||||||
|
//TODO check user email for The Coding Machine game
|
||||||
|
const userId = uuid();
|
||||||
|
const token = Jwt.sign({name: param.name, userId: userId} as TokenInterface, SECRET_KEY, {expiresIn: '24h'});
|
||||||
|
return res.status(OK).send({
|
||||||
|
token: token,
|
||||||
|
mapUrlStart: URL_ROOM_STARTED,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
import { HttpResponse } from "uWebSockets.js";
|
|
||||||
|
|
||||||
export class BaseController {
|
|
||||||
protected addCorsHeaders(res: HttpResponse): void {
|
|
||||||
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
|
|
||||||
res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
|
||||||
res.writeHeader("access-control-allow-origin", "*");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
|
||||||
import { stringify } from "circular-json";
|
|
||||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
|
||||||
import { parse } from "query-string";
|
|
||||||
import { App } from "../Server/sifrr.server";
|
|
||||||
import { socketManager } from "../Services/SocketManager";
|
|
||||||
|
|
||||||
export class DebugController {
|
|
||||||
constructor(private App: App) {
|
|
||||||
this.getDump();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDump() {
|
|
||||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
(async () => {
|
|
||||||
const query = parse(req.getQuery());
|
|
||||||
|
|
||||||
if (ADMIN_API_TOKEN === "") {
|
|
||||||
return res.writeStatus("401 Unauthorized").end("No token configured!");
|
|
||||||
}
|
|
||||||
if (query.token !== ADMIN_API_TOKEN) {
|
|
||||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
.writeStatus("200 OK")
|
|
||||||
.writeHeader("Content-Type", "application/json")
|
|
||||||
.end(
|
|
||||||
stringify(
|
|
||||||
await Promise.all(socketManager.getWorlds().values()),
|
|
||||||
(key: unknown, value: unknown) => {
|
|
||||||
if (key === "listeners") {
|
|
||||||
return "Listeners";
|
|
||||||
}
|
|
||||||
if (key === "socket") {
|
|
||||||
return "Socket";
|
|
||||||
}
|
|
||||||
if (key === "batchedMessages") {
|
|
||||||
return "BatchedMessages";
|
|
||||||
}
|
|
||||||
if (value instanceof Map) {
|
|
||||||
const obj: { [key: string | number]: unknown } = {};
|
|
||||||
for (const [mapKey, mapValue] of value.entries()) {
|
|
||||||
if (typeof mapKey === "number" || typeof mapKey === "string") {
|
|
||||||
obj[mapKey] = mapValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
res.writeStatus("500");
|
|
||||||
res.end("An error occurred");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
431
back/src/Controller/IoSocketController.ts
Normal file
|
@ -0,0 +1,431 @@
|
||||||
|
import socketIO = require('socket.io');
|
||||||
|
import {Socket} from "socket.io";
|
||||||
|
import * as http from "http";
|
||||||
|
import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
||||||
|
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||||
|
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
||||||
|
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||||
|
import {World} from "../Model/World";
|
||||||
|
import {Group} from "_Model/Group";
|
||||||
|
import {UserInterface} from "_Model/UserInterface";
|
||||||
|
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
||||||
|
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
||||||
|
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
||||||
|
import si from "systeminformation";
|
||||||
|
import {Gauge} from "prom-client";
|
||||||
|
import os from 'os';
|
||||||
|
import {TokenInterface} from "../Controller/AuthenticateController";
|
||||||
|
import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
|
||||||
|
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
|
||||||
|
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
||||||
|
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
||||||
|
|
||||||
|
enum SockerIoEvent {
|
||||||
|
CONNECTION = "connection",
|
||||||
|
DISCONNECT = "disconnect",
|
||||||
|
JOIN_ROOM = "join-room", // bi-directional
|
||||||
|
USER_POSITION = "user-position", // bi-directional
|
||||||
|
USER_MOVED = "user-moved", // From server to client
|
||||||
|
USER_LEFT = "user-left", // From server to client
|
||||||
|
WEBRTC_SIGNAL = "webrtc-signal",
|
||||||
|
WEBRTC_START = "webrtc-start",
|
||||||
|
WEBRTC_DISCONNECT = "webrtc-disconect",
|
||||||
|
MESSAGE_ERROR = "message-error",
|
||||||
|
GROUP_CREATE_UPDATE = "group-create-update",
|
||||||
|
GROUP_DELETE = "group-delete",
|
||||||
|
SET_PLAYER_DETAILS = "set-player-details"
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IoSocketController {
|
||||||
|
public readonly Io: socketIO.Server;
|
||||||
|
private Worlds: Map<string, World> = new Map<string, World>();
|
||||||
|
private sockets: Map<string, ExSocketInterface> = new Map<string, ExSocketInterface>();
|
||||||
|
private nbClientsGauge: Gauge<string>;
|
||||||
|
private nbClientsPerRoomGauge: Gauge<string>;
|
||||||
|
|
||||||
|
constructor(server: http.Server) {
|
||||||
|
this.Io = socketIO(server);
|
||||||
|
this.nbClientsGauge = new Gauge({
|
||||||
|
name: 'workadventure_nb_sockets',
|
||||||
|
help: 'Number of connected sockets',
|
||||||
|
labelNames: [ 'host' ]
|
||||||
|
});
|
||||||
|
this.nbClientsPerRoomGauge = new Gauge({
|
||||||
|
name: 'workadventure_nb_clients_per_room',
|
||||||
|
help: 'Number of clients per room',
|
||||||
|
labelNames: [ 'host', 'room' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authentication with token. it will be decoded and stored in the socket.
|
||||||
|
// Completely commented for now, as we do not use the "/login" route at all.
|
||||||
|
this.Io.use((socket: Socket, next) => {
|
||||||
|
if (!socket.handshake.query || !socket.handshake.query.token) {
|
||||||
|
console.error('An authentication error happened, a user tried to connect without a token.');
|
||||||
|
return next(new Error('Authentication error'));
|
||||||
|
}
|
||||||
|
if(this.searchClientByToken(socket.handshake.query.token)){
|
||||||
|
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
||||||
|
return next(new Error('Authentication error'));
|
||||||
|
}
|
||||||
|
Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('An authentication error happened, invalid JsonWebToken.', err);
|
||||||
|
return next(new Error('Authentication error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isValidToken(tokenDecoded)) {
|
||||||
|
return next(new Error('Authentication error, invalid token structure'));
|
||||||
|
}
|
||||||
|
|
||||||
|
(socket as ExSocketInterface).token = socket.handshake.query.token;
|
||||||
|
(socket as ExSocketInterface).userId = tokenDecoded.userId;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ioConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidToken(token: object): token is TokenInterface {
|
||||||
|
if (typeof((token as TokenInterface).userId) !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof((token as TokenInterface).name) !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
searchClientByToken(token: string): ExSocketInterface | null {
|
||||||
|
const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[];
|
||||||
|
for (let i = 0; i < clients.length; i++) {
|
||||||
|
const client = clients[i];
|
||||||
|
if (client.token !== token) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendUpdateGroupEvent(group: Group): void {
|
||||||
|
// Let's get the room of the group. To do this, let's get anyone in the group and find its room.
|
||||||
|
// Note: this is suboptimal
|
||||||
|
const userId = group.getUsers()[0].id;
|
||||||
|
const client: ExSocketInterface = this.searchClientByIdOrFail(userId);
|
||||||
|
const roomId = client.roomId;
|
||||||
|
this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
|
||||||
|
position: group.getPosition(),
|
||||||
|
groupId: group.getId()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void {
|
||||||
|
// Let's get the room of the group. To do this, let's get anyone in the group and find its room.
|
||||||
|
const userId = lastUser.id;
|
||||||
|
const client: ExSocketInterface = this.searchClientByIdOrFail(userId);
|
||||||
|
const roomId = client.roomId;
|
||||||
|
this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
ioConnection() {
|
||||||
|
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
||||||
|
const client : ExSocketInterface = socket as ExSocketInterface;
|
||||||
|
this.sockets.set(client.userId, client);
|
||||||
|
|
||||||
|
// Let's log server load when a user joins
|
||||||
|
const srvSockets = this.Io.sockets.sockets;
|
||||||
|
this.nbClientsGauge.inc({ host: os.hostname() });
|
||||||
|
console.log(new Date().toISOString() + ' A user joined (', Object.keys(srvSockets).length, ' connected users)');
|
||||||
|
si.currentLoad().then(data => console.log(' Current load: ', data.avgload));
|
||||||
|
si.currentLoad().then(data => console.log(' CPU: ', data.currentload, '%'));
|
||||||
|
// End log server load
|
||||||
|
|
||||||
|
/*join-rom event permit to join one room.
|
||||||
|
message :
|
||||||
|
userId : user identification
|
||||||
|
roomId: room identification
|
||||||
|
position: position of user in map
|
||||||
|
x: user x position on map
|
||||||
|
y: user y position on map
|
||||||
|
*/
|
||||||
|
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
||||||
|
try {
|
||||||
|
if (!isJoinRoomMessageInterface(message)) {
|
||||||
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'});
|
||||||
|
console.warn('Invalid JOIN_ROOM message received: ', message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roomId = message.roomId;
|
||||||
|
|
||||||
|
const Client = (socket as ExSocketInterface);
|
||||||
|
|
||||||
|
if (Client.roomId === roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//leave previous room
|
||||||
|
this.leaveRoom(Client);
|
||||||
|
|
||||||
|
//join new previous room
|
||||||
|
const world = this.joinRoom(Client, roomId, message.position);
|
||||||
|
|
||||||
|
//add function to refresh position user in real time.
|
||||||
|
//this.refreshUserPosition(Client);
|
||||||
|
|
||||||
|
const messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.character, Client.position);
|
||||||
|
|
||||||
|
socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined);
|
||||||
|
|
||||||
|
// The answer shall contain the list of all users of the room with their positions:
|
||||||
|
const listOfUsers = Array.from(world.getUsers(), ([key, user]) => {
|
||||||
|
const player = this.searchClientByIdOrFail(user.id);
|
||||||
|
return new MessageUserPosition(user.id, player.name, player.character, player.position);
|
||||||
|
});
|
||||||
|
answerFn(listOfUsers);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "join_room" event');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
||||||
|
try {
|
||||||
|
if (!isPointInterface(position)) {
|
||||||
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
||||||
|
console.warn('Invalid USER_POSITION message received: ', position);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Client = (socket as ExSocketInterface);
|
||||||
|
|
||||||
|
// sending to all clients in room except sender
|
||||||
|
Client.position = position;
|
||||||
|
|
||||||
|
// update position in the world
|
||||||
|
const world = this.Worlds.get(Client.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("Could not find world with id '", Client.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.updatePosition(Client, position);
|
||||||
|
|
||||||
|
socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "user_position" event');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: unknown) => {
|
||||||
|
if (!isWebRtcSignalMessageInterface(data)) {
|
||||||
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SIGNAL message.'});
|
||||||
|
console.warn('Invalid WEBRTC_SIGNAL message received: ', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//send only at user
|
||||||
|
const client = this.sockets.get(data.receiverId);
|
||||||
|
if (client === undefined) {
|
||||||
|
console.warn("While exchanging a WebRTC signal: client with id ", data.receiverId, " does not exist. This might be a race condition.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(SockerIoEvent.DISCONNECT, () => {
|
||||||
|
const Client = (socket as ExSocketInterface);
|
||||||
|
try {
|
||||||
|
//leave room
|
||||||
|
this.leaveRoom(Client);
|
||||||
|
|
||||||
|
//leave webrtc room
|
||||||
|
//socket.leave(Client.webRtcRoomId);
|
||||||
|
|
||||||
|
//delete all socket information
|
||||||
|
delete Client.webRtcRoomId;
|
||||||
|
delete Client.roomId;
|
||||||
|
delete Client.token;
|
||||||
|
delete Client.position;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "disconnect"');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
this.sockets.delete(Client.userId);
|
||||||
|
|
||||||
|
// Let's log server load when a user leaves
|
||||||
|
const srvSockets = this.Io.sockets.sockets;
|
||||||
|
this.nbClientsGauge.dec({ host: os.hostname() });
|
||||||
|
console.log('A user left (', Object.keys(srvSockets).length, ' connected users)');
|
||||||
|
si.currentLoad().then(data => console.log('Current load: ', data.avgload));
|
||||||
|
si.currentLoad().then(data => console.log('CPU: ', data.currentload, '%'));
|
||||||
|
// End log server load
|
||||||
|
});
|
||||||
|
|
||||||
|
// Let's send the user id to the user
|
||||||
|
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
||||||
|
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||||
|
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
||||||
|
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Client = (socket as ExSocketInterface);
|
||||||
|
Client.name = playerDetails.name;
|
||||||
|
Client.character = playerDetails.character;
|
||||||
|
answerFn(Client.userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchClientByIdOrFail(userId: string): ExSocketInterface {
|
||||||
|
const client: ExSocketInterface|undefined = this.sockets.get(userId);
|
||||||
|
if (client === undefined) {
|
||||||
|
throw new Error("Could not find user with id " + userId);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveRoom(Client : ExSocketInterface){
|
||||||
|
// leave previous room and world
|
||||||
|
if(Client.roomId){
|
||||||
|
Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId);
|
||||||
|
|
||||||
|
//user leave previous world
|
||||||
|
const world : World|undefined = this.Worlds.get(Client.roomId);
|
||||||
|
if(world){
|
||||||
|
world.leave(Client);
|
||||||
|
}
|
||||||
|
//user leave previous room
|
||||||
|
Client.leave(Client.roomId);
|
||||||
|
this.nbClientsPerRoomGauge.dec({ host: os.hostname(), room: Client.roomId });
|
||||||
|
delete Client.roomId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World {
|
||||||
|
//join user in room
|
||||||
|
Client.join(roomId);
|
||||||
|
this.nbClientsPerRoomGauge.inc({ host: os.hostname(), room: roomId });
|
||||||
|
Client.roomId = roomId;
|
||||||
|
Client.position = position;
|
||||||
|
|
||||||
|
//check and create new world for a room
|
||||||
|
let world = this.Worlds.get(roomId)
|
||||||
|
if(world === undefined){
|
||||||
|
world = new World((user1: string, group: Group) => {
|
||||||
|
this.connectedUser(user1, group);
|
||||||
|
}, (user1: string, group: Group) => {
|
||||||
|
this.disConnectedUser(user1, group);
|
||||||
|
}, MINIMUM_DISTANCE, GROUP_RADIUS, (group: Group) => {
|
||||||
|
this.sendUpdateGroupEvent(group);
|
||||||
|
}, (groupUuid: string, lastUser: UserInterface) => {
|
||||||
|
this.sendDeleteGroupEvent(groupUuid, lastUser);
|
||||||
|
});
|
||||||
|
this.Worlds.set(roomId, world);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch groups position to newly connected user
|
||||||
|
world.getGroups().forEach((group: Group) => {
|
||||||
|
Client.emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
|
||||||
|
position: group.getPosition(),
|
||||||
|
groupId: group.getId()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//join world
|
||||||
|
world.join(Client, Client.position);
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param socket
|
||||||
|
* @param roomId
|
||||||
|
*/
|
||||||
|
joinWebRtcRoom(socket: ExSocketInterface, roomId: string) {
|
||||||
|
if (socket.webRtcRoomId === roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.join(roomId);
|
||||||
|
socket.webRtcRoomId = roomId;
|
||||||
|
//if two persons in room share
|
||||||
|
if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
|
||||||
|
.filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
|
||||||
|
//send start at one client to initialise offer webrtc
|
||||||
|
//send all users in room to create PeerConnection in front
|
||||||
|
clients.forEach((client: ExSocketInterface, index: number) => {
|
||||||
|
|
||||||
|
const clientsId = clients.reduce((tabs: Array<UserInGroupInterface>, clientId: ExSocketInterface, indexClientId: number) => {
|
||||||
|
if (!clientId.userId || clientId.userId === client.userId) {
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
tabs.push({
|
||||||
|
userId: clientId.userId,
|
||||||
|
name: clientId.name,
|
||||||
|
initiator: index <= indexClientId
|
||||||
|
});
|
||||||
|
return tabs;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
client.emit(SockerIoEvent.WEBRTC_START, {clients: clientsId, roomId: roomId});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** permit to share user position
|
||||||
|
** users position will send in event 'user-position'
|
||||||
|
** The data sent is an array with information for each user :
|
||||||
|
[
|
||||||
|
{
|
||||||
|
userId: <string>,
|
||||||
|
roomId: <string>,
|
||||||
|
position: {
|
||||||
|
x : <number>,
|
||||||
|
y : <number>,
|
||||||
|
direction: <string>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
**/
|
||||||
|
|
||||||
|
//connected user
|
||||||
|
connectedUser(userId: string, group: Group) {
|
||||||
|
/*let Client = this.sockets.get(userId);
|
||||||
|
if (Client === undefined) {
|
||||||
|
return;
|
||||||
|
}*/
|
||||||
|
const Client = this.searchClientByIdOrFail(userId);
|
||||||
|
this.joinWebRtcRoom(Client, group.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
//disconnect user
|
||||||
|
disConnectedUser(userId: string, group: Group) {
|
||||||
|
const Client = this.searchClientByIdOrFail(userId);
|
||||||
|
Client.to(group.getId()).emit(SockerIoEvent.WEBRTC_DISCONNECT, {
|
||||||
|
userId: userId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
|
||||||
|
// which will be shut for the other player).
|
||||||
|
// However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player,
|
||||||
|
// the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing).
|
||||||
|
// So we also send the disconnect event to the other player.
|
||||||
|
for (const user of group.getUsers()) {
|
||||||
|
Client.emit(SockerIoEvent.WEBRTC_DISCONNECT, {
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//disconnect webrtc room
|
||||||
|
if(!Client.webRtcRoomId){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Client.leave(Client.webRtcRoomId);
|
||||||
|
delete Client.webRtcRoomId;
|
||||||
|
}
|
||||||
|
}
|
28
back/src/Controller/MapController.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import express from "express";
|
||||||
|
import {Application, Request, Response} from "express";
|
||||||
|
import {OK} from "http-status-codes";
|
||||||
|
import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
export class MapController {
|
||||||
|
App: Application;
|
||||||
|
|
||||||
|
constructor(App: Application) {
|
||||||
|
this.App = App;
|
||||||
|
this.getStartMap();
|
||||||
|
this.assetMaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
assetMaps() {
|
||||||
|
this.App.use('/map/files', express.static('src/Assets/Maps'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a map mapping map name to file name of the map
|
||||||
|
getStartMap() {
|
||||||
|
this.App.get("/start-map", (req: Request, res: Response) => {
|
||||||
|
res.status(OK).send({
|
||||||
|
mapUrlStart: req.headers.host + "/map/files" + URL_ROOM_STARTED,
|
||||||
|
startInstance: "global"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,20 @@
|
||||||
import { App } from "../Server/sifrr.server";
|
import {Application, Request, Response} from "express";
|
||||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
import {IoSocketController} from "_Controller/IoSocketController";
|
||||||
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: Application, private ioSocketController: IoSocketController) {
|
||||||
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(req: Request, res: Response): void {
|
||||||
res.writeHeader("Content-Type", register.contentType);
|
res.set('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,11 @@
|
||||||
|
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 ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
|
||||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "";
|
|
||||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
|
||||||
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
|
|
||||||
const JITSI_ISS = process.env.JITSI_ISS || "";
|
|
||||||
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
|
|
||||||
const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080;
|
|
||||||
const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
|
|
||||||
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
|
||||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
|
||||||
export 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,
|
GROUP_RADIUS
|
||||||
ADMIN_API_TOKEN,
|
}
|
||||||
HTTP_PORT,
|
|
||||||
GRPC_PORT,
|
|
||||||
GROUP_RADIUS,
|
|
||||||
ALLOW_ARTILLERY,
|
|
||||||
CPU_OVERHEAT_THRESHOLD,
|
|
||||||
JITSI_URL,
|
|
||||||
JITSI_ISS,
|
|
||||||
SECRET_JITSI_KEY,
|
|
||||||
};
|
|
||||||
|
|
1
back/src/Messages/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/generated/
|
|
|
@ -1,34 +0,0 @@
|
||||||
import {
|
|
||||||
ServerToAdminClientMessage,
|
|
||||||
UserJoinedRoomMessage,
|
|
||||||
UserLeftRoomMessage,
|
|
||||||
} from "../Messages/generated/messages_pb";
|
|
||||||
import { AdminSocket } from "../RoomManager";
|
|
||||||
|
|
||||||
export class Admin {
|
|
||||||
public constructor(private readonly socket: AdminSocket) {}
|
|
||||||
|
|
||||||
public sendUserJoin(uuid: string, name: string, ip: string): void {
|
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
|
||||||
|
|
||||||
const userJoinedRoomMessage = new UserJoinedRoomMessage();
|
|
||||||
userJoinedRoomMessage.setUuid(uuid);
|
|
||||||
userJoinedRoomMessage.setName(name);
|
|
||||||
userJoinedRoomMessage.setIpaddress(ip);
|
|
||||||
|
|
||||||
serverToAdminClientMessage.setUserjoinedroom(userJoinedRoomMessage);
|
|
||||||
|
|
||||||
this.socket.write(serverToAdminClientMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendUserLeft(uuid: string /*, name: string, ip: string*/): void {
|
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
|
||||||
|
|
||||||
const userLeftRoomMessage = new UserLeftRoomMessage();
|
|
||||||
userLeftRoomMessage.setUuid(uuid);
|
|
||||||
|
|
||||||
serverToAdminClientMessage.setUserleftroom(userLeftRoomMessage);
|
|
||||||
|
|
||||||
this.socket.write(serverToAdminClientMessage);
|
|
||||||
}
|
|
||||||
}
|
|
7
back/src/Model/Distance.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import {MessageUserPosition} from "../Model/Websocket/MessageUserPosition";
|
||||||
|
|
||||||
|
export interface Distance {
|
||||||
|
distance: number,
|
||||||
|
first: MessageUserPosition,
|
||||||
|
second: MessageUserPosition,
|
||||||
|
}
|
|
@ -1,657 +0,0 @@
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
|
||||||
import { Group } from "./Group";
|
|
||||||
import { User, UserSocket } from "./User";
|
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
|
||||||
import {
|
|
||||||
EmoteCallback,
|
|
||||||
EntersCallback,
|
|
||||||
LeavesCallback,
|
|
||||||
MovesCallback,
|
|
||||||
PlayerDetailsUpdatedCallback,
|
|
||||||
} from "_Model/Zone";
|
|
||||||
import { PositionNotifier } from "./PositionNotifier";
|
|
||||||
import { Movable } from "_Model/Movable";
|
|
||||||
import {
|
|
||||||
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 DisconnectCallback = (user: User, group: Group) => void;
|
|
||||||
|
|
||||||
export class GameRoom {
|
|
||||||
// Users, sorted by ID
|
|
||||||
private readonly users = new Map<number, User>();
|
|
||||||
private readonly usersByUuid = new Map<string, User>();
|
|
||||||
private readonly groups = new Set<Group>();
|
|
||||||
private readonly admins = new Set<Admin>();
|
|
||||||
|
|
||||||
private itemsState = new Map<number, unknown>();
|
|
||||||
|
|
||||||
private readonly positionNotifier: PositionNotifier;
|
|
||||||
private versionNumber: number = 1;
|
|
||||||
private nextUserId: number = 1;
|
|
||||||
|
|
||||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
public readonly roomUrl: string,
|
|
||||||
private mapUrl: string,
|
|
||||||
private readonly connectCallback: ConnectCallback,
|
|
||||||
private readonly disconnectCallback: DisconnectCallback,
|
|
||||||
private readonly minDistance: number,
|
|
||||||
private readonly groupRadius: number,
|
|
||||||
onEnters: EntersCallback,
|
|
||||||
onMoves: MovesCallback,
|
|
||||||
onLeaves: LeavesCallback,
|
|
||||||
onEmote: EmoteCallback,
|
|
||||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
|
||||||
) {
|
|
||||||
// A zone is 10 sprites wide.
|
|
||||||
this.positionNotifier = new PositionNotifier(
|
|
||||||
320,
|
|
||||||
320,
|
|
||||||
onEnters,
|
|
||||||
onMoves,
|
|
||||||
onLeaves,
|
|
||||||
onEmote,
|
|
||||||
onPlayerDetailsUpdated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async create(
|
|
||||||
roomUrl: string,
|
|
||||||
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> {
|
|
||||||
return this.users;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getUserByUuid(uuid: string): User | undefined {
|
|
||||||
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 {
|
|
||||||
const positionMessage = joinRoomMessage.getPositionmessage();
|
|
||||||
if (positionMessage === undefined) {
|
|
||||||
throw new Error("Missing position message");
|
|
||||||
}
|
|
||||||
const position = ProtobufUtils.toPointInterface(positionMessage);
|
|
||||||
|
|
||||||
const user = new User(
|
|
||||||
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.users.set(user.id, user);
|
|
||||||
this.usersByUuid.set(user.uuid, user);
|
|
||||||
this.updateUserGroup(user);
|
|
||||||
|
|
||||||
// Notify admins
|
|
||||||
for (const admin of this.admins) {
|
|
||||||
admin.sendUserJoin(user.uuid, user.name, user.IPAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public leave(user: User) {
|
|
||||||
const userObj = this.users.get(user.id);
|
|
||||||
if (userObj === undefined) {
|
|
||||||
console.warn("User ", user.id, "does not belong to this game room! It should!");
|
|
||||||
}
|
|
||||||
if (userObj !== undefined && typeof userObj.group !== "undefined") {
|
|
||||||
this.leaveGroup(userObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.hasFollowers()) {
|
|
||||||
user.stopLeading();
|
|
||||||
}
|
|
||||||
if (user.following) {
|
|
||||||
user.following.delFollower(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.users.delete(user.id);
|
|
||||||
this.usersByUuid.delete(user.uuid);
|
|
||||||
|
|
||||||
if (userObj !== undefined) {
|
|
||||||
this.positionNotifier.leave(userObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify admins
|
|
||||||
for (const admin of this.admins) {
|
|
||||||
admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
|
||||||
return this.users.size === 0 && this.admins.size === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public updatePosition(user: User, userPosition: PointInterface): void {
|
|
||||||
user.setPosition(userPosition);
|
|
||||||
|
|
||||||
this.updateUserGroup(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
|
||||||
if (playerDetailsMessage.getRemoveoutlinecolor()) {
|
|
||||||
user.outlineColor = undefined;
|
|
||||||
} else {
|
|
||||||
user.outlineColor = playerDetailsMessage.getOutlinecolor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateUserGroup(user: User): void {
|
|
||||||
if (user.silent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = user.group;
|
|
||||||
const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
|
|
||||||
|
|
||||||
if (group === undefined) {
|
|
||||||
// If the user is not part of a group:
|
|
||||||
// should he join a group?
|
|
||||||
|
|
||||||
// If the user is moving, don't try to join
|
|
||||||
if (user.getPosition().moving) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closestItem !== null) {
|
|
||||||
if (closestItem instanceof Group) {
|
|
||||||
// Let's join the group!
|
|
||||||
closestItem.join(user);
|
|
||||||
closestItem.setOutOfBounds(false);
|
|
||||||
} else {
|
|
||||||
const closestUser: User = closestItem;
|
|
||||||
const group: Group = new Group(
|
|
||||||
this.roomUrl,
|
|
||||||
[user, closestUser],
|
|
||||||
this.groupRadius,
|
|
||||||
this.connectCallback,
|
|
||||||
this.disconnectCallback,
|
|
||||||
this.positionNotifier
|
|
||||||
);
|
|
||||||
this.groups.add(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let hasKickOutSomeone = false;
|
|
||||||
let followingMembers: User[] = [];
|
|
||||||
|
|
||||||
const previewNewGroupPosition = group.previewGroupPosition();
|
|
||||||
|
|
||||||
if (!previewNewGroupPosition) {
|
|
||||||
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) {
|
|
||||||
if (user.silent === silent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.silent = silent;
|
|
||||||
if (silent && user.group !== undefined) {
|
|
||||||
this.leaveGroup(user);
|
|
||||||
}
|
|
||||||
if (!silent) {
|
|
||||||
// If we are back to life, let's trigger a position update to see if we can join some group.
|
|
||||||
this.updatePosition(user, user.getPosition());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
|
||||||
*
|
|
||||||
* @param user
|
|
||||||
*/
|
|
||||||
private leaveGroup(user: User): void {
|
|
||||||
const group = user.group;
|
|
||||||
if (group === undefined) {
|
|
||||||
throw new Error("The user is part of no group");
|
|
||||||
}
|
|
||||||
group.leave(user);
|
|
||||||
if (group.isEmpty()) {
|
|
||||||
group.destroy();
|
|
||||||
if (!this.groups.has(group)) {
|
|
||||||
throw new Error(`Could not find group ${group.getId()} referenced by user ${user.id} in World.`);
|
|
||||||
}
|
|
||||||
this.groups.delete(group);
|
|
||||||
//todo: is the group garbage collected?
|
|
||||||
} else {
|
|
||||||
group.updatePosition();
|
|
||||||
//this.positionNotifier.updatePosition(group, group.getPosition(), oldPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks for the closest user that is:
|
|
||||||
* - close enough (distance <= minDistance)
|
|
||||||
* - not in a group
|
|
||||||
* - not silent
|
|
||||||
* OR
|
|
||||||
* - close enough to a group (distance <= groupRadius)
|
|
||||||
*/
|
|
||||||
private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
|
|
||||||
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
|
||||||
let matchingItem: User | Group | null = null;
|
|
||||||
this.users.forEach((currentUser, userId) => {
|
|
||||||
// Let's only check users that are not part of a group
|
|
||||||
if (typeof currentUser.group !== "undefined") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentUser === user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentUser.silent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
|
||||||
|
|
||||||
if (distance <= minimumDistanceFound && distance <= this.minDistance) {
|
|
||||||
minimumDistanceFound = distance;
|
|
||||||
matchingItem = currentUser;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.groups.forEach((group: Group) => {
|
|
||||||
if (group.isFull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
|
||||||
if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
|
||||||
minimumDistanceFound = distance;
|
|
||||||
matchingItem = group;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return matchingItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static computeDistance(user1: User, user2: User): number {
|
|
||||||
const user1Position = user1.getPosition();
|
|
||||||
const user2Position = user2.getPosition();
|
|
||||||
return Math.sqrt(
|
|
||||||
Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
public setItemState(itemId: number, state: unknown) {
|
|
||||||
this.itemsState.set(itemId, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getItemsState(): Map<number, unknown> {
|
|
||||||
return this.itemsState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
|
||||||
// First, let's check if "user" is allowed to modify the variable.
|
|
||||||
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> {
|
|
||||||
return this.positionNotifier.addZoneListener(call, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeZoneListener(call: ZoneSocket, x: number, y: number): void {
|
|
||||||
return this.positionNotifier.removeZoneListener(call, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public adminJoin(admin: Admin): void {
|
|
||||||
this.admins.add(admin);
|
|
||||||
|
|
||||||
// Let's send all connected users
|
|
||||||
for (const user of this.users.values()) {
|
|
||||||
admin.sendUserJoin(user.uuid, user.name, user.IPAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public adminLeave(admin: Admin): void {
|
|
||||||
this.admins.delete(admin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public incrementVersion(): number {
|
|
||||||
this.versionNumber++;
|
|
||||||
return this.versionNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
|
||||||
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,51 +1,33 @@
|
||||||
import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom";
|
import { World, ConnectCallback, DisconnectCallback } from "./World";
|
||||||
import { User } from "./User";
|
import { UserInterface } from "./UserInterface";
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
import { Movable } from "_Model/Movable";
|
import {uuid} from "uuidv4";
|
||||||
import { PositionNotifier } from "_Model/PositionNotifier";
|
|
||||||
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
|
|
||||||
import type { Zone } from "../Model/Zone";
|
|
||||||
|
|
||||||
export class Group implements Movable {
|
export class Group {
|
||||||
private static nextId: number = 1;
|
static readonly MAX_PER_GROUP = 4;
|
||||||
|
|
||||||
private id: number;
|
private id: string;
|
||||||
private users: Set<User>;
|
private users: UserInterface[];
|
||||||
private x!: number;
|
private connectCallback: ConnectCallback;
|
||||||
private y!: number;
|
private disconnectCallback: DisconnectCallback;
|
||||||
private wasDestroyed: boolean = false;
|
|
||||||
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,
|
|
||||||
users: User[],
|
|
||||||
private groupRadius: number,
|
|
||||||
private connectCallback: ConnectCallback,
|
|
||||||
private disconnectCallback: DisconnectCallback,
|
|
||||||
private positionNotifier: PositionNotifier
|
|
||||||
) {
|
|
||||||
this.roomId = roomId;
|
|
||||||
this.users = new Set<User>();
|
|
||||||
this.id = Group.nextId;
|
|
||||||
Group.nextId++;
|
|
||||||
|
|
||||||
users.forEach((user: User) => {
|
constructor(users: UserInterface[], connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback) {
|
||||||
|
this.users = [];
|
||||||
|
this.connectCallback = connectCallback;
|
||||||
|
this.disconnectCallback = disconnectCallback;
|
||||||
|
this.id = uuid();
|
||||||
|
|
||||||
|
users.forEach((user: UserInterface) => {
|
||||||
this.join(user);
|
this.join(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updatePosition();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsers(): User[] {
|
getUsers(): UserInterface[] {
|
||||||
return Array.from(this.users.values());
|
return this.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): number {
|
getId() : string{
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,162 +35,75 @@ export class Group implements Movable {
|
||||||
* Returns the barycenter of all users (i.e. the center of the group)
|
* Returns the barycenter of all users (i.e. the center of the group)
|
||||||
*/
|
*/
|
||||||
getPosition(): PositionInterface {
|
getPosition(): PositionInterface {
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
// Let's compute the barycenter of all users.
|
||||||
|
this.users.forEach((user: UserInterface) => {
|
||||||
|
x += user.position.x;
|
||||||
|
y += user.position.y;
|
||||||
|
});
|
||||||
|
x /= this.users.length;
|
||||||
|
y /= this.users.length;
|
||||||
return {
|
return {
|
||||||
x: this.x,
|
x,
|
||||||
y: this.y,
|
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)
|
|
||||||
*/
|
|
||||||
updatePosition(): void {
|
|
||||||
const oldX = this.x;
|
|
||||||
const oldY = this.y;
|
|
||||||
|
|
||||||
// Let's compute the barycenter of all users.
|
|
||||||
const newPosition = this.previewGroupPosition();
|
|
||||||
|
|
||||||
if (!newPosition) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y } = newPosition;
|
|
||||||
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
|
|
||||||
if (this.outOfBounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldX === undefined) {
|
|
||||||
this.currentZone = this.positionNotifier.enter(this);
|
|
||||||
} else {
|
|
||||||
this.currentZone = 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.length >= Group.MAX_PER_GROUP;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this.users.size <= 1;
|
return this.users.length <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
join(user: User): void {
|
join(user: UserInterface): void
|
||||||
|
{
|
||||||
// Broadcast on the right event
|
// Broadcast on the right event
|
||||||
this.connectCallback(user, this);
|
this.connectCallback(user.id, this);
|
||||||
this.users.add(user);
|
this.users.push(user);
|
||||||
user.group = this;
|
user.group = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(user: User): void {
|
isPartOfGroup(user: UserInterface): boolean
|
||||||
const success = this.users.delete(user);
|
{
|
||||||
if (success === false) {
|
return this.users.includes(user);
|
||||||
throw new Error(`Could not find user ${user.id} in the group ${this.id}`);
|
}
|
||||||
|
|
||||||
|
/*removeFromGroup(users: UserInterface[]): void
|
||||||
|
{
|
||||||
|
for(let i = 0; i < users.length; i++){
|
||||||
|
let user = users[i];
|
||||||
|
const index = this.users.indexOf(user, 0);
|
||||||
|
if (index > -1) {
|
||||||
|
this.users.splice(index, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
leave(user: UserInterface): void
|
||||||
|
{
|
||||||
|
const index = this.users.indexOf(user, 0);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error("Could not find user in the group");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.users.splice(index, 1);
|
||||||
user.group = undefined;
|
user.group = undefined;
|
||||||
|
|
||||||
if (this.users.size !== 0) {
|
|
||||||
this.updatePosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast on the right event
|
// Broadcast on the right event
|
||||||
this.disconnectCallback(user, this);
|
this.disconnectCallback(user.id, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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);
|
this.users.forEach((user: UserInterface) => {
|
||||||
}
|
|
||||||
|
|
||||||
for (const user of this.users) {
|
|
||||||
this.leave(user);
|
this.leave(user);
|
||||||
}
|
})
|
||||||
this.wasDestroyed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get getSize() {
|
|
||||||
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 +0,0 @@
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A physical object that can be placed into a Zone
|
|
||||||
*/
|
|
||||||
export interface Movable {
|
|
||||||
getPosition(): PositionInterface;
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
export interface PositionInterface {
|
export interface PositionInterface {
|
||||||
x: number;
|
x: number,
|
||||||
y: number;
|
y: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,158 +0,0 @@
|
||||||
/**
|
|
||||||
* Tracks the position of every player on the map, and sends notifications to the players interested in knowing about the move
|
|
||||||
* (i.e. players that are looking at the zone the player is currently in)
|
|
||||||
*
|
|
||||||
* Internally, the PositionNotifier works with Zones. A zone is a square area of a map.
|
|
||||||
* Each player is in a given zone, and each player tracks one or many zones (depending on the player viewport)
|
|
||||||
*
|
|
||||||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
|
||||||
* number of players around the current player.
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
EmoteCallback,
|
|
||||||
EntersCallback,
|
|
||||||
LeavesCallback,
|
|
||||||
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 {
|
|
||||||
i: 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 {
|
|
||||||
// TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!)
|
|
||||||
|
|
||||||
private zones: Zone[][] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
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 {
|
|
||||||
return {
|
|
||||||
i: Math.floor(x / this.zoneWidth),
|
|
||||||
j: Math.floor(y / this.zoneHeight),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public enter(thing: Movable): Zone {
|
|
||||||
const position = thing.getPosition();
|
|
||||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
|
||||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
|
||||||
zone.enter(thing, null, position);
|
|
||||||
return zone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): Zone {
|
|
||||||
// Did we change zone?
|
|
||||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
|
||||||
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
|
||||||
|
|
||||||
if (oldZoneDesc.i != newZoneDesc.i || oldZoneDesc.j != newZoneDesc.j) {
|
|
||||||
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
|
||||||
const newZone = this.getZone(newZoneDesc.i, newZoneDesc.j);
|
|
||||||
|
|
||||||
// Leave old zone
|
|
||||||
oldZone.leave(thing, newZone);
|
|
||||||
|
|
||||||
// Enter new zone
|
|
||||||
newZone.enter(thing, oldZone, newPosition);
|
|
||||||
return newZone;
|
|
||||||
} else {
|
|
||||||
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
|
||||||
zone.move(thing, newPosition);
|
|
||||||
return zone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public leave(thing: Movable): void {
|
|
||||||
const oldPosition = thing.getPosition();
|
|
||||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
|
||||||
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
|
||||||
oldZone.leave(thing, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getZone(i: number, j: number): Zone {
|
|
||||||
let zoneRow = this.zones[j];
|
|
||||||
if (zoneRow === undefined) {
|
|
||||||
zoneRow = new Array<Zone>();
|
|
||||||
this.zones[j] = zoneRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
let zone = this.zones[j][i];
|
|
||||||
if (zone === undefined) {
|
|
||||||
zone = new Zone(
|
|
||||||
this.onUserEnters,
|
|
||||||
this.onUserMoves,
|
|
||||||
this.onUserLeaves,
|
|
||||||
this.onEmote,
|
|
||||||
this.onPlayerDetailsUpdated,
|
|
||||||
i,
|
|
||||||
j
|
|
||||||
);
|
|
||||||
this.zones[j][i] = zone;
|
|
||||||
}
|
|
||||||
return zone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
|
||||||
const zone = this.getZone(x, y);
|
|
||||||
zone.addListener(call);
|
|
||||||
return zone.getThings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeZoneListener(call: ZoneSocket, x: number, y: number): void {
|
|
||||||
const zone = this.getZone(x, y);
|
|
||||||
zone.removeListener(call);
|
|
||||||
}
|
|
||||||
|
|
||||||
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
|
||||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
|
|
||||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
|
||||||
zone.emitEmoteEvent(emoteEventMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
import { Group } from "./Group";
|
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
|
||||||
import { Zone } from "_Model/Zone";
|
|
||||||
import { Movable } from "_Model/Movable";
|
|
||||||
import { PositionNotifier } from "_Model/PositionNotifier";
|
|
||||||
import { ServerDuplexStream } from "grpc";
|
|
||||||
import {
|
|
||||||
BatchMessage,
|
|
||||||
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 class User implements Movable {
|
|
||||||
public listenedZones: Set<Zone>;
|
|
||||||
public group?: Group;
|
|
||||||
private _following: User | undefined;
|
|
||||||
private followedBy: Set<User> = new Set<User>();
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public id: number,
|
|
||||||
public readonly uuid: string,
|
|
||||||
public readonly IPAddress: string,
|
|
||||||
private position: PointInterface,
|
|
||||||
public silent: boolean,
|
|
||||||
private positionNotifier: PositionNotifier,
|
|
||||||
public readonly socket: UserSocket,
|
|
||||||
public readonly tags: string[],
|
|
||||||
public readonly visitCardUrl: string | null,
|
|
||||||
public readonly name: string,
|
|
||||||
public readonly characterLayers: CharacterLayer[],
|
|
||||||
public readonly companion?: CompanionMessage,
|
|
||||||
private _outlineColor?: number | undefined
|
|
||||||
) {
|
|
||||||
this.listenedZones = new Set<Zone>();
|
|
||||||
|
|
||||||
this.positionNotifier.enter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getPosition(): PointInterface {
|
|
||||||
return this.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setPosition(position: PointInterface): void {
|
|
||||||
const oldPosition = this.position;
|
|
||||||
this.position = position;
|
|
||||||
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 batchTimeout: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
public emitInBatch(payload: SubMessage): void {
|
|
||||||
this.batchedMessages.addPayload(payload);
|
|
||||||
|
|
||||||
if (this.batchTimeout === null) {
|
|
||||||
this.batchTimeout = setTimeout(() => {
|
|
||||||
/*if (socket.disconnecting) {
|
|
||||||
return;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setBatchmessage(this.batchedMessages);
|
|
||||||
|
|
||||||
this.socket.write(serverToClientMessage);
|
|
||||||
this.batchedMessages = new BatchMessage();
|
|
||||||
this.batchTimeout = null;
|
|
||||||
}, 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);
|
|
||||||
}
|
|
||||||
}
|
|
8
back/src/Model/UserInterface.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Group } from "./Group";
|
||||||
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
|
|
||||||
|
export interface UserInterface {
|
||||||
|
id: string,
|
||||||
|
group?: Group,
|
||||||
|
position: PointInterface
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
export interface CharacterLayer {
|
|
||||||
name: string;
|
|
||||||
url: string | undefined;
|
|
||||||
}
|
|
14
back/src/Model/Websocket/ExSocketInterface.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import {Socket} from "socket.io";
|
||||||
|
import {PointInterface} from "./PointInterface";
|
||||||
|
import {Identificable} from "./Identificable";
|
||||||
|
import {TokenInterface} from "../../Controller/AuthenticateController";
|
||||||
|
|
||||||
|
export interface ExSocketInterface extends Socket, Identificable {
|
||||||
|
token: string;
|
||||||
|
roomId: string;
|
||||||
|
webRtcRoomId: string;
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
character: string;
|
||||||
|
position: PointInterface;
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
export interface Identificable {
|
export interface Identificable {
|
||||||
userId: number;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import * as tg from "generic-type-guard";
|
|
||||||
|
|
||||||
export const isItemEventMessageInterface = new tg.IsInterface()
|
|
||||||
.withProperties({
|
|
||||||
itemId: tg.isNumber,
|
|
||||||
event: tg.isString,
|
|
||||||
state: tg.isUnknown,
|
|
||||||
parameters: tg.isUnknown,
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
|
9
back/src/Model/Websocket/JoinRoomMessage.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import {isPointInterface} from "./PointInterface";
|
||||||
|
|
||||||
|
export const isJoinRoomMessageInterface =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
roomId: tg.isString,
|
||||||
|
position: isPointInterface,
|
||||||
|
}).get();
|
||||||
|
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
6
back/src/Model/Websocket/MessageUserJoined.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||||
|
|
||||||
|
export class MessageUserJoined {
|
||||||
|
constructor(public userId: string, public name: string, public character: string, public position: PointInterface) {
|
||||||
|
}
|
||||||
|
}
|
6
back/src/Model/Websocket/MessageUserMoved.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import {PointInterface} from "./PointInterface";
|
||||||
|
|
||||||
|
export class MessageUserMoved {
|
||||||
|
constructor(public userId: string, public position: PointInterface) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
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
|
export class MessageUserPosition {
|
||||||
) {}
|
constructor(public userId: string, public name: string, public character: string, public position: PointInterface) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,117 +0,0 @@
|
||||||
import { PointInterface } from "./PointInterface";
|
|
||||||
import {
|
|
||||||
CharacterLayerMessage,
|
|
||||||
ItemEventMessage,
|
|
||||||
PointMessage,
|
|
||||||
PositionMessage,
|
|
||||||
} from "../../Messages/generated/messages_pb";
|
|
||||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
|
||||||
import Direction = PositionMessage.Direction;
|
|
||||||
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
|
||||||
|
|
||||||
export class ProtobufUtils {
|
|
||||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
|
||||||
let direction: Direction;
|
|
||||||
switch (point.direction) {
|
|
||||||
case "up":
|
|
||||||
direction = Direction.UP;
|
|
||||||
break;
|
|
||||||
case "down":
|
|
||||||
direction = Direction.DOWN;
|
|
||||||
break;
|
|
||||||
case "left":
|
|
||||||
direction = Direction.LEFT;
|
|
||||||
break;
|
|
||||||
case "right":
|
|
||||||
direction = Direction.RIGHT;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("unexpected direction");
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = new PositionMessage();
|
|
||||||
position.setX(point.x);
|
|
||||||
position.setY(point.y);
|
|
||||||
position.setMoving(point.moving);
|
|
||||||
position.setDirection(direction);
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toPointInterface(position: PositionMessage): PointInterface {
|
|
||||||
let direction: string;
|
|
||||||
switch (position.getDirection()) {
|
|
||||||
case Direction.UP:
|
|
||||||
direction = "up";
|
|
||||||
break;
|
|
||||||
case Direction.DOWN:
|
|
||||||
direction = "down";
|
|
||||||
break;
|
|
||||||
case Direction.LEFT:
|
|
||||||
direction = "left";
|
|
||||||
break;
|
|
||||||
case Direction.RIGHT:
|
|
||||||
direction = "right";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("Unexpected direction");
|
|
||||||
}
|
|
||||||
|
|
||||||
// sending to all clients in room except sender
|
|
||||||
return {
|
|
||||||
x: position.getX(),
|
|
||||||
y: position.getY(),
|
|
||||||
direction,
|
|
||||||
moving: position.getMoving(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toPointMessage(point: PositionInterface): PointMessage {
|
|
||||||
const position = new PointMessage();
|
|
||||||
position.setX(Math.floor(point.x));
|
|
||||||
position.setY(Math.floor(point.y));
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toItemEvent(itemEventMessage: ItemEventMessage): ItemEventMessageInterface {
|
|
||||||
return {
|
|
||||||
itemId: itemEventMessage.getItemid(),
|
|
||||||
event: itemEventMessage.getEvent(),
|
|
||||||
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
|
||||||
state: JSON.parse(itemEventMessage.getStatejson()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
|
||||||
const itemEventMessage = new ItemEventMessage();
|
|
||||||
itemEventMessage.setItemid(itemEvent.itemId);
|
|
||||||
itemEventMessage.setEvent(itemEvent.event);
|
|
||||||
itemEventMessage.setParametersjson(JSON.stringify(itemEvent.parameters));
|
|
||||||
itemEventMessage.setStatejson(JSON.stringify(itemEvent.state));
|
|
||||||
|
|
||||||
return itemEventMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
|
||||||
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
|
||||||
const message = new CharacterLayerMessage();
|
|
||||||
message.setName(characterLayer.name);
|
|
||||||
if (characterLayer.url) {
|
|
||||||
message.setUrl(characterLayer.url);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
|
||||||
return characterLayers.map(function (characterLayer): CharacterLayer {
|
|
||||||
const url = characterLayer.getUrl();
|
|
||||||
return {
|
|
||||||
name: characterLayer.getName(),
|
|
||||||
url: url ? url : undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
8
back/src/Model/Websocket/SetPlayerDetailsMessage.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isSetPlayerDetailsMessage =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
name: tg.isString,
|
||||||
|
character: tg.isString
|
||||||
|
}).get();
|
||||||
|
export type SetPlayerDetailsMessage = tg.GuardedType<typeof isSetPlayerDetailsMessage>;
|
5
back/src/Model/Websocket/UserInGroupInterface.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface UserInGroupInterface {
|
||||||
|
userId: string,
|
||||||
|
name: string,
|
||||||
|
initiator: boolean
|
||||||
|
}
|
10
back/src/Model/Websocket/WebRtcSignalMessage.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isWebRtcSignalMessageInterface =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
userId: tg.isString,
|
||||||
|
receiverId: tg.isString,
|
||||||
|
roomId: tg.isString,
|
||||||
|
signal: tg.isUnknown
|
||||||
|
}).get();
|
||||||
|
export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>;
|
287
back/src/Model/World.ts
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
import {MessageUserPosition, Point} from "./Websocket/MessageUserPosition";
|
||||||
|
import {PointInterface} from "./Websocket/PointInterface";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
import {Distance} from "./Distance";
|
||||||
|
import {UserInterface} from "./UserInterface";
|
||||||
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
import {Identificable} from "_Model/Websocket/Identificable";
|
||||||
|
|
||||||
|
export type ConnectCallback = (user: string, group: Group) => void;
|
||||||
|
export type DisconnectCallback = (user: string, group: Group) => void;
|
||||||
|
|
||||||
|
// callback called when a group is created or moved or changes users
|
||||||
|
export type GroupUpdatedCallback = (group: Group) => void;
|
||||||
|
export type GroupDeletedCallback = (uuid: string, lastUser: UserInterface) => void;
|
||||||
|
|
||||||
|
export class World {
|
||||||
|
private readonly minDistance: number;
|
||||||
|
private readonly groupRadius: number;
|
||||||
|
|
||||||
|
// Users, sorted by ID
|
||||||
|
private readonly users: Map<string, UserInterface>;
|
||||||
|
private readonly groups: Group[];
|
||||||
|
|
||||||
|
private readonly connectCallback: ConnectCallback;
|
||||||
|
private readonly disconnectCallback: DisconnectCallback;
|
||||||
|
private readonly groupUpdatedCallback: GroupUpdatedCallback;
|
||||||
|
private readonly groupDeletedCallback: GroupDeletedCallback;
|
||||||
|
|
||||||
|
constructor(connectCallback: ConnectCallback,
|
||||||
|
disconnectCallback: DisconnectCallback,
|
||||||
|
minDistance: number,
|
||||||
|
groupRadius: number,
|
||||||
|
groupUpdatedCallback: GroupUpdatedCallback,
|
||||||
|
groupDeletedCallback: GroupDeletedCallback)
|
||||||
|
{
|
||||||
|
this.users = new Map<string, UserInterface>();
|
||||||
|
this.groups = [];
|
||||||
|
this.connectCallback = connectCallback;
|
||||||
|
this.disconnectCallback = disconnectCallback;
|
||||||
|
this.minDistance = minDistance;
|
||||||
|
this.groupRadius = groupRadius;
|
||||||
|
this.groupUpdatedCallback = groupUpdatedCallback;
|
||||||
|
this.groupDeletedCallback = groupDeletedCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGroups(): Group[] {
|
||||||
|
return this.groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUsers(): Map<string, UserInterface> {
|
||||||
|
return this.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
public join(socket : Identificable, userPosition: PointInterface): void {
|
||||||
|
this.users.set(socket.userId, {
|
||||||
|
id: socket.userId,
|
||||||
|
position: userPosition
|
||||||
|
});
|
||||||
|
// Let's call update position to trigger the join / leave room
|
||||||
|
this.updatePosition(socket, userPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public leave(user : Identificable){
|
||||||
|
const userObj = this.users.get(user.userId);
|
||||||
|
if (userObj === undefined) {
|
||||||
|
console.warn('User ', user.userId, 'does not belong to world! It should!');
|
||||||
|
}
|
||||||
|
if (userObj !== undefined && typeof userObj.group !== 'undefined') {
|
||||||
|
this.leaveGroup(userObj);
|
||||||
|
}
|
||||||
|
this.users.delete(user.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePosition(socket : Identificable, userPosition: PointInterface): void {
|
||||||
|
const user = this.users.get(socket.userId);
|
||||||
|
if(typeof user === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.position = userPosition;
|
||||||
|
|
||||||
|
if (typeof user.group === 'undefined') {
|
||||||
|
// If the user is not part of a group:
|
||||||
|
// should he join a group?
|
||||||
|
const closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user);
|
||||||
|
|
||||||
|
if (closestItem !== null) {
|
||||||
|
if (closestItem instanceof Group) {
|
||||||
|
// Let's join the group!
|
||||||
|
closestItem.join(user);
|
||||||
|
} else {
|
||||||
|
const closestUser : UserInterface = closestItem;
|
||||||
|
const group: Group = new Group([
|
||||||
|
user,
|
||||||
|
closestUser
|
||||||
|
], this.connectCallback, this.disconnectCallback);
|
||||||
|
this.groups.push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If the user is part of a group:
|
||||||
|
// should he leave the group?
|
||||||
|
const distance = World.computeDistanceBetweenPositions(user.position, user.group.getPosition());
|
||||||
|
if (distance > this.groupRadius) {
|
||||||
|
this.leaveGroup(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At the very end, if the user is part of a group, let's call the callback to update group position
|
||||||
|
if (typeof user.group !== 'undefined') {
|
||||||
|
this.groupUpdatedCallback(user.group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
*/
|
||||||
|
private leaveGroup(user: UserInterface): void {
|
||||||
|
const group = user.group;
|
||||||
|
if (typeof group === 'undefined') {
|
||||||
|
throw new Error("The user is part of no group");
|
||||||
|
}
|
||||||
|
group.leave(user);
|
||||||
|
if (group.isEmpty()) {
|
||||||
|
this.groupDeletedCallback(group.getId(), user);
|
||||||
|
group.destroy();
|
||||||
|
const index = this.groups.indexOf(group, 0);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error("Could not find group");
|
||||||
|
}
|
||||||
|
this.groups.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.groupUpdatedCallback(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks for the closest user that is:
|
||||||
|
* - close enough (distance <= minDistance)
|
||||||
|
* - not in a group
|
||||||
|
* OR
|
||||||
|
* - close enough to a group (distance <= groupRadius)
|
||||||
|
*/
|
||||||
|
private searchClosestAvailableUserOrGroup(user: UserInterface): UserInterface|Group|null
|
||||||
|
{
|
||||||
|
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
||||||
|
let matchingItem: UserInterface | Group | null = null;
|
||||||
|
this.users.forEach((currentUser, userId) => {
|
||||||
|
// Let's only check users that are not part of a group
|
||||||
|
if (typeof currentUser.group !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(currentUser === user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = World.computeDistance(user, currentUser); // compute distance between peers.
|
||||||
|
|
||||||
|
if(distance <= minimumDistanceFound && distance <= this.minDistance) {
|
||||||
|
minimumDistanceFound = distance;
|
||||||
|
matchingItem = currentUser;
|
||||||
|
}
|
||||||
|
/*if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) {
|
||||||
|
// We found a user we can bind to.
|
||||||
|
return;
|
||||||
|
}*/
|
||||||
|
/*
|
||||||
|
if(context.groups.length > 0) {
|
||||||
|
|
||||||
|
context.groups.forEach(group => {
|
||||||
|
if(group.isPartOfGroup(userPosition)) { // Is the user in a group ?
|
||||||
|
if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player)
|
||||||
|
|
||||||
|
// Should we split the group? (is each player reachable from the current player?)
|
||||||
|
// This is needed if
|
||||||
|
// A <==> B <==> C <===> D
|
||||||
|
// becomes A <==> B <=====> C <> D
|
||||||
|
// If C moves right, the distance between B and C is too great and we must form 2 groups
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the user is in no group
|
||||||
|
// Is there someone in a group close enough and with room in the group ?
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Aucun groupe n'existe donc je stock les users assez proches de moi
|
||||||
|
let dist: Distance = {
|
||||||
|
distance: distance,
|
||||||
|
first: userPosition,
|
||||||
|
second: user // TODO: convertir en messageUserPosition
|
||||||
|
}
|
||||||
|
usersToBeGroupedWith.push(dist);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
this.groups.forEach((group: Group) => {
|
||||||
|
if (group.isFull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const distance = World.computeDistanceBetweenPositions(user.position, group.getPosition());
|
||||||
|
if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
||||||
|
minimumDistanceFound = distance;
|
||||||
|
matchingItem = group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static computeDistance(user1: UserInterface, user2: UserInterface): number
|
||||||
|
{
|
||||||
|
return Math.sqrt(Math.pow(user2.position.x - user1.position.x, 2) + Math.pow(user2.position.y - user1.position.y, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*getDistancesBetweenGroupUsers(group: Group): Distance[]
|
||||||
|
{
|
||||||
|
let i = 0;
|
||||||
|
let users = group.getUsers();
|
||||||
|
let distances: Distance[] = [];
|
||||||
|
users.forEach(function(user1, key1) {
|
||||||
|
users.forEach(function(user2, key2) {
|
||||||
|
if(key1 < key2) {
|
||||||
|
distances[i] = {
|
||||||
|
distance: World.computeDistance(user1, user2),
|
||||||
|
first: user1,
|
||||||
|
second: user2
|
||||||
|
};
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
distances.sort(World.compareDistances);
|
||||||
|
|
||||||
|
return distances;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterGroup(distances: Distance[], group: Group): void
|
||||||
|
{
|
||||||
|
let users = group.getUsers();
|
||||||
|
let usersToRemove = false;
|
||||||
|
let groupTmp: MessageUserPosition[] = [];
|
||||||
|
distances.forEach(dist => {
|
||||||
|
if(dist.distance <= World.MIN_DISTANCE) {
|
||||||
|
let users = [dist.first];
|
||||||
|
let usersbis = [dist.second]
|
||||||
|
groupTmp.push(dist.first);
|
||||||
|
groupTmp.push(dist.second);
|
||||||
|
} else {
|
||||||
|
usersToRemove = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(usersToRemove) {
|
||||||
|
// Detecte le ou les users qui se sont fait sortir du groupe
|
||||||
|
let difference = users.filter(x => !groupTmp.includes(x));
|
||||||
|
|
||||||
|
// TODO : Notify users un difference that they have left the group
|
||||||
|
}
|
||||||
|
|
||||||
|
let newgroup = new Group(groupTmp);
|
||||||
|
this.groups.push(newgroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static compareDistances(distA: Distance, distB: Distance): number
|
||||||
|
{
|
||||||
|
if (distA.distance < distB.distance) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (distA.distance > distB.distance) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}*/
|
||||||
|
}
|
|
@ -1,120 +0,0 @@
|
||||||
import { User } from "./User";
|
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
|
||||||
import { Movable } from "./Movable";
|
|
||||||
import { Group } from "./Group";
|
|
||||||
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 MovesCallback = (thing: Movable, position: PositionInterface, 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 {
|
|
||||||
private things: Set<Movable> = new Set<Movable>();
|
|
||||||
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private onEnters: EntersCallback,
|
|
||||||
private onMoves: MovesCallback,
|
|
||||||
private onLeaves: LeavesCallback,
|
|
||||||
private onEmote: EmoteCallback,
|
|
||||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
|
|
||||||
public readonly x: number,
|
|
||||||
public readonly y: number
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user/thing leaves the zone
|
|
||||||
*/
|
|
||||||
public leave(thing: Movable, newZone: Zone | null) {
|
|
||||||
const result = this.things.delete(thing);
|
|
||||||
if (!result) {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
throw new Error(`Could not find user in zone ${thing.id}`);
|
|
||||||
}
|
|
||||||
if (thing instanceof Group) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find group ${thing.getId()} in zone (${this.x},${this.y}). Position of group: (${
|
|
||||||
thing.getPosition().x
|
|
||||||
},${thing.getPosition().y})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.notifyLeft(thing, newZone);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify listeners of this zone that this user/thing left
|
|
||||||
*/
|
|
||||||
private notifyLeft(thing: Movable, newZone: Zone | null) {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
this.onLeaves(thing, newZone, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
|
||||||
this.things.add(thing);
|
|
||||||
this.notifyEnter(thing, oldZone, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify listeners of this zone that this user entered
|
|
||||||
*/
|
|
||||||
private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
this.onEnters(thing, oldZone, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public move(thing: Movable, position: PositionInterface) {
|
|
||||||
if (!this.things.has(thing)) {
|
|
||||||
this.things.add(thing);
|
|
||||||
this.notifyEnter(thing, null, position);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
//if (listener !== thing) {
|
|
||||||
this.onMoves(thing, position, listener);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getThings(): Set<Movable> {
|
|
||||||
return this.things;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addListener(socket: ZoneSocket): void {
|
|
||||||
this.listeners.add(socket);
|
|
||||||
// TODO: here, we should trigger in some way the sending of current items
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeListener(socket: ZoneSocket): void {
|
|
||||||
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,324 +0,0 @@
|
||||||
import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb";
|
|
||||||
import {
|
|
||||||
AdminGlobalMessage,
|
|
||||||
AdminMessage,
|
|
||||||
AdminPusherToBackMessage,
|
|
||||||
AdminRoomMessage,
|
|
||||||
BanMessage,
|
|
||||||
BanUserMessage,
|
|
||||||
BatchToPusherMessage,
|
|
||||||
BatchToPusherRoomMessage,
|
|
||||||
EmotePromptMessage,
|
|
||||||
FollowRequestMessage,
|
|
||||||
FollowConfirmationMessage,
|
|
||||||
FollowAbortMessage,
|
|
||||||
EmptyMessage,
|
|
||||||
ItemEventMessage,
|
|
||||||
JoinRoomMessage,
|
|
||||||
PlayGlobalMessage,
|
|
||||||
PusherToBackMessage,
|
|
||||||
QueryJitsiJwtMessage,
|
|
||||||
RefreshRoomPromptMessage,
|
|
||||||
RoomMessage,
|
|
||||||
SendUserMessage,
|
|
||||||
ServerToAdminClientMessage,
|
|
||||||
SetPlayerDetailsMessage,
|
|
||||||
SilentMessage,
|
|
||||||
UserMovesMessage,
|
|
||||||
VariableMessage,
|
|
||||||
WebRtcSignalToServerMessage,
|
|
||||||
WorldFullWarningToRoomMessage,
|
|
||||||
ZoneMessage,
|
|
||||||
} from "./Messages/generated/messages_pb";
|
|
||||||
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
|
||||||
import { socketManager } from "./Services/SocketManager";
|
|
||||||
import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers";
|
|
||||||
import { User, UserSocket } from "./Model/User";
|
|
||||||
import { GameRoom } from "./Model/GameRoom";
|
|
||||||
import Debug from "debug";
|
|
||||||
import { Admin } from "./Model/Admin";
|
|
||||||
|
|
||||||
const debug = Debug("roommanager");
|
|
||||||
|
|
||||||
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
|
||||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, BatchToPusherMessage>;
|
|
||||||
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
|
||||||
|
|
||||||
const roomManager: IRoomManagerServer = {
|
|
||||||
joinRoom: (call: UserSocket): void => {
|
|
||||||
console.log("joinRoom called");
|
|
||||||
|
|
||||||
let room: GameRoom | null = null;
|
|
||||||
let user: User | null = null;
|
|
||||||
|
|
||||||
call.on("data", (message: PusherToBackMessage) => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
if (room === null || user === null) {
|
|
||||||
if (message.hasJoinroommessage()) {
|
|
||||||
socketManager
|
|
||||||
.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
|
|
||||||
.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 {
|
|
||||||
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.hasVariablemessage()) {
|
|
||||||
await socketManager.handleVariableEvent(
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
message.getVariablemessage() as VariableMessage
|
|
||||||
);
|
|
||||||
} 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.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));
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("end", () => {
|
|
||||||
debug("joinRoom ended");
|
|
||||||
if (user !== null && room !== null) {
|
|
||||||
socketManager.leaveRoom(room, user);
|
|
||||||
}
|
|
||||||
call.end();
|
|
||||||
room = null;
|
|
||||||
user = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("error", (err: Error) => {
|
|
||||||
console.error("An error occurred in joinRoom stream:", err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
listenZone(call: ZoneSocket): void {
|
|
||||||
debug("listenZone called");
|
|
||||||
const zoneMessage = call.request;
|
|
||||||
|
|
||||||
socketManager
|
|
||||||
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
|
||||||
.catch((e) => {
|
|
||||||
emitErrorOnZoneSocket(call, e);
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("cancelled", () => {
|
|
||||||
debug("listenZone cancelled");
|
|
||||||
socketManager
|
|
||||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
call.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("close", () => {
|
|
||||||
debug("listenZone connection closed");
|
|
||||||
socketManager
|
|
||||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
}).on("error", (e) => {
|
|
||||||
console.error("An error occurred in listenZone stream:", e);
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
adminRoom(call: AdminSocket): void {
|
|
||||||
console.log("adminRoom called");
|
|
||||||
|
|
||||||
const admin = new Admin(call);
|
|
||||||
let room: GameRoom | null = null;
|
|
||||||
|
|
||||||
call.on("data", (message: AdminPusherToBackMessage) => {
|
|
||||||
try {
|
|
||||||
if (room === null) {
|
|
||||||
if (message.hasSubscribetoroom()) {
|
|
||||||
const roomId = message.getSubscribetoroom();
|
|
||||||
socketManager
|
|
||||||
.handleJoinAdminRoom(admin, roomId)
|
|
||||||
.then((gameRoom: GameRoom) => {
|
|
||||||
room = gameRoom;
|
|
||||||
})
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
} else {
|
|
||||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
emitError(call, e);
|
|
||||||
call.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("end", () => {
|
|
||||||
debug("joinRoom ended");
|
|
||||||
if (room !== null) {
|
|
||||||
socketManager.leaveAdminRoom(room, admin);
|
|
||||||
}
|
|
||||||
call.end();
|
|
||||||
room = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("error", (err: Error) => {
|
|
||||||
console.error("An error occurred in joinAdminRoom stream:", err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
|
||||||
socketManager
|
|
||||||
.sendAdminMessage(
|
|
||||||
call.request.getRoomid(),
|
|
||||||
call.request.getRecipientuuid(),
|
|
||||||
call.request.getMessage(),
|
|
||||||
call.request.getType()
|
|
||||||
)
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
|
||||||
},
|
|
||||||
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
|
||||||
throw new Error("Not implemented yet");
|
|
||||||
// TODO
|
|
||||||
callback(null, new EmptyMessage());
|
|
||||||
},
|
|
||||||
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());
|
|
||||||
},
|
|
||||||
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());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export { roomManager };
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { App as _App, AppOptions } from "uWebSockets.js";
|
|
||||||
import BaseApp from "./baseapp";
|
|
||||||
import { extend } from "./utils";
|
|
||||||
import { UwsApp } from "./types";
|
|
||||||
|
|
||||||
class App extends (<UwsApp>_App) {
|
|
||||||
constructor(options: AppOptions = {}) {
|
|
||||||
super(options); // eslint-disable-line constructor-super
|
|
||||||
extend(this, new BaseApp());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,111 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
import { Readable } from "stream";
|
|
||||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
|
||||||
|
|
||||||
import formData from "./formdata";
|
|
||||||
import { stob } from "./utils";
|
|
||||||
import { Handler } from "./types";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
|
||||||
const noOp = () => true;
|
|
||||||
|
|
||||||
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
const contType = req.getHeader("content-type");
|
|
||||||
|
|
||||||
res.bodyStream = function () {
|
|
||||||
const stream = new Readable();
|
|
||||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
|
||||||
|
|
||||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
|
||||||
// uint and then slicing is bit faster than slice and then uint
|
|
||||||
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
if (isLast) {
|
|
||||||
stream.push(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
res.body = () => stob(res.bodyStream());
|
|
||||||
|
|
||||||
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
|
|
||||||
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
|
|
||||||
};
|
|
||||||
|
|
||||||
class BaseApp {
|
|
||||||
_sockets = new Map();
|
|
||||||
ws!: TemplatedApp["ws"];
|
|
||||||
get!: TemplatedApp["get"];
|
|
||||||
_post!: TemplatedApp["post"];
|
|
||||||
_put!: TemplatedApp["put"];
|
|
||||||
_patch!: TemplatedApp["patch"];
|
|
||||||
_listen!: TemplatedApp["listen"];
|
|
||||||
|
|
||||||
post(pattern: string, handler: Handler) {
|
|
||||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
|
||||||
this._post(pattern, (res, req) => {
|
|
||||||
handleBody(res, req);
|
|
||||||
handler(res, req);
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
put(pattern: string, handler: Handler) {
|
|
||||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
|
||||||
this._put(pattern, (res, req) => {
|
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
patch(pattern: string, handler: Handler) {
|
|
||||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
|
||||||
this._patch(pattern, (res, req) => {
|
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
|
||||||
if (typeof p === "number" && typeof h === "string") {
|
|
||||||
this._listen(h, p, (socket) => {
|
|
||||||
this._sockets.set(p, socket);
|
|
||||||
if (cb === undefined) {
|
|
||||||
throw new Error("cb undefined");
|
|
||||||
}
|
|
||||||
cb(socket);
|
|
||||||
});
|
|
||||||
} else if (typeof h === "number" && typeof p === "function") {
|
|
||||||
this._listen(h, (socket) => {
|
|
||||||
this._sockets.set(h, socket);
|
|
||||||
p(socket);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
close(port: null | number = null) {
|
|
||||||
if (port) {
|
|
||||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
|
||||||
this._sockets.delete(port);
|
|
||||||
} else {
|
|
||||||
this._sockets.forEach((app) => {
|
|
||||||
us_listen_socket_close(app);
|
|
||||||
});
|
|
||||||
this._sockets.clear();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BaseApp;
|
|
|
@ -1,101 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
import { createWriteStream } from "fs";
|
|
||||||
import { join, dirname } from "path";
|
|
||||||
import Busboy from "busboy";
|
|
||||||
import mkdirp from "mkdirp";
|
|
||||||
|
|
||||||
function formData(
|
|
||||||
contType: string,
|
|
||||||
options: busboy.BusboyConfig & {
|
|
||||||
abortOnLimit?: boolean;
|
|
||||||
tmpDir?: string;
|
|
||||||
onFile?: (
|
|
||||||
fieldname: string,
|
|
||||||
file: NodeJS.ReadableStream,
|
|
||||||
filename: string,
|
|
||||||
encoding: string,
|
|
||||||
mimetype: string
|
|
||||||
) => string;
|
|
||||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
filename?: (oldName: string) => string;
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
console.log("Enter form data");
|
|
||||||
options.headers = {
|
|
||||||
"content-type": contType,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const busb = new Busboy(options);
|
|
||||||
const ret = {};
|
|
||||||
|
|
||||||
this.bodyStream().pipe(busb);
|
|
||||||
|
|
||||||
busb.on("limit", () => {
|
|
||||||
if (options.abortOnLimit) {
|
|
||||||
reject(Error("limit"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
|
|
||||||
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
|
|
||||||
filename,
|
|
||||||
encoding,
|
|
||||||
mimetype,
|
|
||||||
filePath: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof options.tmpDir === "string") {
|
|
||||||
if (typeof options.filename === "function") filename = options.filename(filename);
|
|
||||||
const fileToSave = join(options.tmpDir, filename);
|
|
||||||
mkdirp(dirname(fileToSave));
|
|
||||||
|
|
||||||
file.pipe(createWriteStream(fileToSave));
|
|
||||||
value.filePath = fileToSave;
|
|
||||||
}
|
|
||||||
if (typeof options.onFile === "function") {
|
|
||||||
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on("field", function (fieldname, value) {
|
|
||||||
if (typeof options.onField === "function") options.onField(fieldname, value);
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on("finish", function () {
|
|
||||||
resolve(ret);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on("error", reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRetValue(
|
|
||||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
fieldname: string,
|
|
||||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
) {
|
|
||||||
if (fieldname.endsWith("[]")) {
|
|
||||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
|
||||||
if (Array.isArray(ret[fieldname])) {
|
|
||||||
ret[fieldname].push(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;
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
|
||||||
import BaseApp from "./baseapp";
|
|
||||||
import { extend } from "./utils";
|
|
||||||
import { UwsApp } from "./types";
|
|
||||||
|
|
||||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
|
||||||
constructor(options: AppOptions) {
|
|
||||||
super(options); // eslint-disable-line constructor-super
|
|
||||||
extend(this, new BaseApp());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SSLApp;
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
|
||||||
|
|
||||||
export type UwsApp = {
|
|
||||||
(options: AppOptions): TemplatedApp;
|
|
||||||
new (options: AppOptions): TemplatedApp;
|
|
||||||
prototype: TemplatedApp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
|
||||||
|
|
||||||
export {};
|
|
|
@ -1,38 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
import { ReadStream } from "fs";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function extend(who: any, from: any, overwrite = true) {
|
|
||||||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
|
|
||||||
ownProps.forEach((prop) => {
|
|
||||||
if (prop === "constructor" || from[prop] === undefined) return;
|
|
||||||
if (who[prop] && overwrite) {
|
|
||||||
who[`_${prop}`] = who[prop];
|
|
||||||
}
|
|
||||||
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
|
|
||||||
else who[prop] = from[prop];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function stob(stream: ReadStream): Promise<Buffer> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const buffers: Buffer[] = [];
|
|
||||||
stream.on("data", buffers.push.bind(buffers));
|
|
||||||
|
|
||||||
stream.on("end", () => {
|
|
||||||
switch (buffers.length) {
|
|
||||||
case 0:
|
|
||||||
resolve(Buffer.allocUnsafe(0));
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
resolve(buffers[0]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
resolve(Buffer.concat(buffers));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { extend, stob };
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { parse } from "query-string";
|
|
||||||
import { HttpRequest } from "uWebSockets.js";
|
|
||||||
import App from "./server/app";
|
|
||||||
import SSLApp from "./server/sslapp";
|
|
||||||
import * as types from "./server/types";
|
|
||||||
|
|
||||||
const getQuery = (req: HttpRequest) => {
|
|
||||||
return parse(req.getQuery());
|
|
||||||
};
|
|
||||||
|
|
||||||
export { App, SSLApp, getQuery };
|
|
||||||
export * from "./server/types";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
App,
|
|
||||||
SSLApp,
|
|
||||||
getQuery,
|
|
||||||
...types,
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
|
||||||
import Axios from "axios";
|
|
||||||
import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData";
|
|
||||||
import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect";
|
|
||||||
|
|
||||||
class AdminApi {
|
|
||||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
|
||||||
if (!ADMIN_API_URL) {
|
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: { playUri: string } = {
|
|
||||||
playUri,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
|
||||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 +0,0 @@
|
||||||
export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
|
|
||||||
return array1.filter((value) => array2.includes(value)).length > 0;
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { EventEmitter } from "events";
|
|
||||||
|
|
||||||
const clientJoinEvent = "clientJoin";
|
|
||||||
const clientLeaveEvent = "clientLeave";
|
|
||||||
|
|
||||||
class ClientEventsEmitter extends EventEmitter {
|
|
||||||
emitClientJoin(clientUUid: string, roomId: string): void {
|
|
||||||
this.emit(clientJoinEvent, clientUUid, roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitClientLeave(clientUUid: string, roomId: string): void {
|
|
||||||
this.emit(clientLeaveEvent, clientUUid, roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.on(clientJoinEvent, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToClientLeave(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.on(clientLeaveEvent, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterFromClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.removeListener(clientJoinEvent, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterFromClientLeave(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.removeListener(clientLeaveEvent, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clientEventsEmitter = new ClientEventsEmitter();
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
function secNSec2ms(secNSec: Array<number> | number) {
|
|
||||||
if (Array.isArray(secNSec)) {
|
|
||||||
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
|
||||||
}
|
|
||||||
return secNSec / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CpuTracker {
|
|
||||||
private cpuPercent: number = 0;
|
|
||||||
private overHeating: boolean = false;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
let time = process.hrtime.bigint();
|
|
||||||
let usage = process.cpuUsage();
|
|
||||||
setInterval(() => {
|
|
||||||
const elapTime = process.hrtime.bigint();
|
|
||||||
const elapUsage = process.cpuUsage(usage);
|
|
||||||
usage = process.cpuUsage();
|
|
||||||
|
|
||||||
const elapTimeMS = elapTime - time;
|
|
||||||
const elapUserMS = secNSec2ms(elapUsage.user);
|
|
||||||
const elapSystMS = secNSec2ms(elapUsage.system);
|
|
||||||
this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
|
|
||||||
|
|
||||||
time = elapTime;
|
|
||||||
|
|
||||||
if (!this.overHeating && this.cpuPercent > CPU_OVERHEAT_THRESHOLD) {
|
|
||||||
this.overHeating = true;
|
|
||||||
console.warn('CPU high threshold alert. Going in "overheat" mode');
|
|
||||||
} else if (this.overHeating && this.cpuPercent <= CPU_OVERHEAT_THRESHOLD) {
|
|
||||||
this.overHeating = false;
|
|
||||||
console.log('CPU is back to normal. Canceling "overheat" mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*console.log('elapsed time ms: ', elapTimeMS)
|
|
||||||
console.log('elapsed user ms: ', elapUserMS)
|
|
||||||
console.log('elapsed system ms:', elapSystMS)
|
|
||||||
console.log('cpu percent: ', this.cpuPercent)*/
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCpuPercent(): number {
|
|
||||||
return this.cpuPercent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isOverHeating(): boolean {
|
|
||||||
return this.overHeating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cpuTracker = new CpuTracker();
|
|
||||||
|
|
||||||
export { cpuTracker };
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { Counter, Gauge } from "prom-client";
|
|
||||||
|
|
||||||
//this class should manage all the custom metrics used by prometheus
|
|
||||||
class GaugeManager {
|
|
||||||
private nbClientsGauge: Gauge<string>;
|
|
||||||
private nbClientsPerRoomGauge: Gauge<string>;
|
|
||||||
private nbGroupsPerRoomGauge: Gauge<string>;
|
|
||||||
private nbGroupsPerRoomCounter: Counter<string>;
|
|
||||||
private nbRoomsGauge: Gauge<string>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.nbRoomsGauge = new Gauge({
|
|
||||||
name: "workadventure_nb_rooms",
|
|
||||||
help: "Number of active rooms",
|
|
||||||
});
|
|
||||||
this.nbClientsGauge = new Gauge({
|
|
||||||
name: "workadventure_nb_sockets",
|
|
||||||
help: "Number of connected sockets",
|
|
||||||
labelNames: [],
|
|
||||||
});
|
|
||||||
this.nbClientsPerRoomGauge = new Gauge({
|
|
||||||
name: "workadventure_nb_clients_per_room",
|
|
||||||
help: "Number of clients per room",
|
|
||||||
labelNames: ["room"],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.nbGroupsPerRoomCounter = new Counter({
|
|
||||||
name: "workadventure_counter_groups_per_room",
|
|
||||||
help: "Counter of groups per room",
|
|
||||||
labelNames: ["room"],
|
|
||||||
});
|
|
||||||
this.nbGroupsPerRoomGauge = new Gauge({
|
|
||||||
name: "workadventure_nb_groups_per_room",
|
|
||||||
help: "Number of groups per room",
|
|
||||||
labelNames: ["room"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
incNbRoomGauge(): void {
|
|
||||||
this.nbRoomsGauge.inc();
|
|
||||||
}
|
|
||||||
decNbRoomGauge(): void {
|
|
||||||
this.nbRoomsGauge.dec();
|
|
||||||
}
|
|
||||||
|
|
||||||
incNbClientPerRoomGauge(roomId: string): void {
|
|
||||||
this.nbClientsGauge.inc();
|
|
||||||
this.nbClientsPerRoomGauge.inc({ room: roomId });
|
|
||||||
}
|
|
||||||
|
|
||||||
decNbClientPerRoomGauge(roomId: string): void {
|
|
||||||
this.nbClientsGauge.dec();
|
|
||||||
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,74 +0,0 @@
|
||||||
import {
|
|
||||||
BatchMessage,
|
|
||||||
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);
|
|
||||||
|
|
||||||
const errorMessage = new ErrorMessage();
|
|
||||||
errorMessage.setMessage(message);
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setErrormessage(errorMessage);
|
|
||||||
|
|
||||||
//if (!Client.disconnecting) {
|
|
||||||
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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,874 +0,0 @@
|
||||||
import { GameRoom } from "../Model/GameRoom";
|
|
||||||
import {
|
|
||||||
ItemEventMessage,
|
|
||||||
ItemStateMessage,
|
|
||||||
PlayGlobalMessage,
|
|
||||||
PointMessage,
|
|
||||||
RoomJoinedMessage,
|
|
||||||
ServerToClientMessage,
|
|
||||||
SilentMessage,
|
|
||||||
SubMessage,
|
|
||||||
UserMovedMessage,
|
|
||||||
UserMovesMessage,
|
|
||||||
WebRtcDisconnectMessage,
|
|
||||||
WebRtcSignalToClientMessage,
|
|
||||||
WebRtcSignalToServerMessage,
|
|
||||||
WebRtcStartMessage,
|
|
||||||
QueryJitsiJwtMessage,
|
|
||||||
SendJitsiJwtMessage,
|
|
||||||
SendUserMessage,
|
|
||||||
JoinRoomMessage,
|
|
||||||
Zone as ProtoZone,
|
|
||||||
BatchToPusherMessage,
|
|
||||||
SubToPusherMessage,
|
|
||||||
UserJoinedZoneMessage,
|
|
||||||
GroupUpdateZoneMessage,
|
|
||||||
GroupLeftZoneMessage,
|
|
||||||
WorldFullWarningMessage,
|
|
||||||
UserLeftZoneMessage,
|
|
||||||
EmoteEventMessage,
|
|
||||||
BanUserMessage,
|
|
||||||
RefreshRoomMessage,
|
|
||||||
EmotePromptMessage,
|
|
||||||
FollowRequestMessage,
|
|
||||||
FollowConfirmationMessage,
|
|
||||||
FollowAbortMessage,
|
|
||||||
VariableMessage,
|
|
||||||
BatchToPusherRoomMessage,
|
|
||||||
SubToPusherRoomMessage,
|
|
||||||
SetPlayerDetailsMessage,
|
|
||||||
PlayerDetailsUpdatedMessage,
|
|
||||||
} from "../Messages/generated/messages_pb";
|
|
||||||
import { User, UserSocket } from "../Model/User";
|
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
|
||||||
import { Group } from "../Model/Group";
|
|
||||||
import { cpuTracker } from "./CpuTracker";
|
|
||||||
import {
|
|
||||||
GROUP_RADIUS,
|
|
||||||
JITSI_ISS,
|
|
||||||
MINIMUM_DISTANCE,
|
|
||||||
SECRET_JITSI_KEY,
|
|
||||||
TURN_STATIC_AUTH_SECRET,
|
|
||||||
} from "../Enum/EnvironmentVariable";
|
|
||||||
import { Movable } from "../Model/Movable";
|
|
||||||
import { PositionInterface } from "../Model/PositionInterface";
|
|
||||||
import Jwt from "jsonwebtoken";
|
|
||||||
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
|
||||||
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
|
||||||
import { gaugeManager } from "./GaugeManager";
|
|
||||||
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
|
||||||
import { Zone } from "_Model/Zone";
|
|
||||||
import Debug from "debug";
|
|
||||||
import { Admin } from "_Model/Admin";
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
const debug = Debug("sockermanager");
|
|
||||||
|
|
||||||
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
|
|
||||||
// TODO: should we batch those every 100ms?
|
|
||||||
const batchMessage = new BatchToPusherMessage();
|
|
||||||
batchMessage.addPayload(subMessage);
|
|
||||||
|
|
||||||
socket.write(batchMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SocketManager {
|
|
||||||
//private rooms = new Map<string, GameRoom>();
|
|
||||||
// List of rooms in process of loading.
|
|
||||||
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
|
||||||
gaugeManager.incNbClientPerRoomGauge(roomId);
|
|
||||||
});
|
|
||||||
clientEventsEmitter.registerToClientLeave((clientUUid: string, roomId: string) => {
|
|
||||||
gaugeManager.decNbClientPerRoomGauge(roomId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleJoinRoom(
|
|
||||||
socket: UserSocket,
|
|
||||||
joinRoomMessage: JoinRoomMessage
|
|
||||||
): Promise<{ room: GameRoom; user: User }> {
|
|
||||||
//join new previous room
|
|
||||||
const { room, user } = await this.joinRoom(socket, joinRoomMessage);
|
|
||||||
|
|
||||||
if (!socket.writable) {
|
|
||||||
console.warn("Socket was aborted");
|
|
||||||
return {
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const roomJoinedMessage = new RoomJoinedMessage();
|
|
||||||
roomJoinedMessage.setTagList(joinRoomMessage.getTagList());
|
|
||||||
roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken());
|
|
||||||
|
|
||||||
for (const [itemId, item] of room.getItemsState().entries()) {
|
|
||||||
const itemStateMessage = new ItemStateMessage();
|
|
||||||
itemStateMessage.setItemid(itemId);
|
|
||||||
itemStateMessage.setStatejson(JSON.stringify(item));
|
|
||||||
|
|
||||||
roomJoinedMessage.addItem(itemStateMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const variables = await room.getVariablesForTags(user.tags);
|
|
||||||
|
|
||||||
for (const [name, value] of variables.entries()) {
|
|
||||||
const variableMessage = new VariableMessage();
|
|
||||||
variableMessage.setName(name);
|
|
||||||
variableMessage.setValue(value);
|
|
||||||
|
|
||||||
roomJoinedMessage.addVariable(variableMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
roomJoinedMessage.setCurrentuserid(user.id);
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage);
|
|
||||||
socket.write(serverToClientMessage);
|
|
||||||
|
|
||||||
return {
|
|
||||||
room,
|
|
||||||
user,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
|
||||||
const userMoves = userMovesMessage.toObject();
|
|
||||||
const position = userMovesMessage.getPosition();
|
|
||||||
|
|
||||||
// If CPU is high, let's drop messages of users moving (we will only dispatch the final position)
|
|
||||||
if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position === undefined) {
|
|
||||||
throw new Error("Position not found in message");
|
|
||||||
}
|
|
||||||
const viewport = userMoves.viewport;
|
|
||||||
if (viewport === undefined) {
|
|
||||||
throw new Error("Viewport not found in message");
|
|
||||||
}
|
|
||||||
|
|
||||||
// update position in the world
|
|
||||||
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
|
||||||
//room.setViewport(client, client.viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSetPlayerDetails(room: GameRoom, user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
|
||||||
room.updatePlayerDetails(user, playerDetailsMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
|
||||||
room.setSilent(user, silentMessage.getSilent());
|
|
||||||
}
|
|
||||||
|
|
||||||
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
|
|
||||||
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
|
||||||
|
|
||||||
const subMessage = new SubMessage();
|
|
||||||
subMessage.setItemeventmessage(itemEventMessage);
|
|
||||||
|
|
||||||
// Let's send the event without using the SocketIO room.
|
|
||||||
// TODO: move this in the GameRoom class.
|
|
||||||
for (const user of room.getUsers().values()) {
|
|
||||||
user.emitInBatch(subMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
room.setItemState(itemEvent.itemId, itemEvent.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage): Promise<void> {
|
|
||||||
return room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
|
||||||
//send only at user
|
|
||||||
const remoteUser = room.getUsers().get(data.getReceiverid());
|
|
||||||
if (remoteUser === undefined) {
|
|
||||||
console.warn(
|
|
||||||
"While exchanging a WebRTC signal: client with id ",
|
|
||||||
data.getReceiverid(),
|
|
||||||
" does not exist. This might be a race condition."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
|
||||||
webrtcSignalToClient.setUserid(user.id);
|
|
||||||
webrtcSignalToClient.setSignal(data.getSignal());
|
|
||||||
// TODO: only compute credentials if data.signal.type === "offer"
|
|
||||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
|
||||||
const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
|
|
||||||
webrtcSignalToClient.setWebrtcusername(username);
|
|
||||||
webrtcSignalToClient.setWebrtcpassword(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
|
|
||||||
|
|
||||||
//if (!client.disconnecting) {
|
|
||||||
remoteUser.socket.write(serverToClientMessage);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitScreenSharing(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
|
||||||
//send only at user
|
|
||||||
const remoteUser = room.getUsers().get(data.getReceiverid());
|
|
||||||
if (remoteUser === undefined) {
|
|
||||||
console.warn(
|
|
||||||
"While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ",
|
|
||||||
data.getReceiverid(),
|
|
||||||
" does not exist. This might be a race condition."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
|
||||||
webrtcSignalToClient.setUserid(user.id);
|
|
||||||
webrtcSignalToClient.setSignal(data.getSignal());
|
|
||||||
// TODO: only compute credentials if data.signal.type === "offer"
|
|
||||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
|
||||||
const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
|
|
||||||
webrtcSignalToClient.setWebrtcusername(username);
|
|
||||||
webrtcSignalToClient.setWebrtcpassword(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
|
|
||||||
|
|
||||||
//if (!client.disconnecting) {
|
|
||||||
remoteUser.socket.write(serverToClientMessage);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveRoom(room: GameRoom, user: User) {
|
|
||||||
// leave previous room and world
|
|
||||||
try {
|
|
||||||
//user leave previous world
|
|
||||||
room.leave(user);
|
|
||||||
if (room.isEmpty()) {
|
|
||||||
this.roomsPromises.delete(room.roomUrl);
|
|
||||||
gaugeManager.decNbRoomGauge();
|
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
|
||||||
console.log("A user left");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
|
||||||
//check and create new room
|
|
||||||
let roomPromise = this.roomsPromises.get(roomId);
|
|
||||||
if (roomPromise === undefined) {
|
|
||||||
roomPromise = GameRoom.create(
|
|
||||||
roomId,
|
|
||||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
|
||||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
|
||||||
MINIMUM_DISTANCE,
|
|
||||||
GROUP_RADIUS,
|
|
||||||
(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
|
|
||||||
this.onZoneEnter(thing, fromZone, listener),
|
|
||||||
(thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
|
|
||||||
this.onClientMove(thing, position, listener),
|
|
||||||
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
|
||||||
this.onClientLeave(thing, newZone, listener),
|
|
||||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
|
||||||
this.onEmote(emoteEventMessage, listener),
|
|
||||||
(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) =>
|
|
||||||
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener)
|
|
||||||
)
|
|
||||||
.then((gameRoom) => {
|
|
||||||
gaugeManager.incNbRoomGauge();
|
|
||||||
return gameRoom;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
this.roomsPromises.delete(roomId);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
this.roomsPromises.set(roomId, roomPromise);
|
|
||||||
}
|
|
||||||
return roomPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async joinRoom(
|
|
||||||
socket: UserSocket,
|
|
||||||
joinRoomMessage: JoinRoomMessage
|
|
||||||
): Promise<{ room: GameRoom; user: User }> {
|
|
||||||
const roomId = joinRoomMessage.getRoomid();
|
|
||||||
|
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
|
||||||
|
|
||||||
//join world
|
|
||||||
const user = room.join(socket, joinRoomMessage);
|
|
||||||
|
|
||||||
clientEventsEmitter.emitClientJoin(user.uuid, roomId);
|
|
||||||
console.log(new Date().toISOString() + " A user joined");
|
|
||||||
return { room, user };
|
|
||||||
}
|
|
||||||
|
|
||||||
private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
const userJoinedZoneMessage = new UserJoinedZoneMessage();
|
|
||||||
if (!Number.isInteger(thing.id)) {
|
|
||||||
throw new Error(`clientUser.userId is not an integer ${thing.id}`);
|
|
||||||
}
|
|
||||||
userJoinedZoneMessage.setUserid(thing.id);
|
|
||||||
userJoinedZoneMessage.setUseruuid(thing.uuid);
|
|
||||||
userJoinedZoneMessage.setName(thing.name);
|
|
||||||
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
|
||||||
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
|
||||||
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
|
|
||||||
if (thing.visitCardUrl) {
|
|
||||||
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
|
|
||||||
}
|
|
||||||
userJoinedZoneMessage.setCompanion(thing.companion);
|
|
||||||
if (thing.outlineColor === undefined) {
|
|
||||||
userJoinedZoneMessage.setHasoutline(false);
|
|
||||||
} else {
|
|
||||||
userJoinedZoneMessage.setHasoutline(true);
|
|
||||||
userJoinedZoneMessage.setOutlinecolor(thing.outlineColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setUserjoinedzonemessage(userJoinedZoneMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, listener);
|
|
||||||
//listener.emitInBatch(subMessage);
|
|
||||||
} else if (thing instanceof Group) {
|
|
||||||
this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
|
|
||||||
} else {
|
|
||||||
console.error("Unexpected type for Movable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
const userMovedMessage = new UserMovedMessage();
|
|
||||||
userMovedMessage.setUserid(thing.id);
|
|
||||||
userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setUsermovedmessage(userMovedMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, listener);
|
|
||||||
//listener.emitInBatch(subMessage);
|
|
||||||
//console.log("Sending USER_MOVED event");
|
|
||||||
} else if (thing instanceof Group) {
|
|
||||||
this.emitCreateUpdateGroupEvent(listener, null, thing);
|
|
||||||
} else {
|
|
||||||
console.error("Unexpected type for Movable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
this.emitUserLeftEvent(listener, thing.id, newZone);
|
|
||||||
} else if (thing instanceof Group) {
|
|
||||||
this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
|
|
||||||
} else {
|
|
||||||
console.error("Unexpected type for Movable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setEmoteeventmessage(emoteEventMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, client);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) {
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, client);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
|
|
||||||
const position = group.getPosition();
|
|
||||||
const pointMessage = new PointMessage();
|
|
||||||
pointMessage.setX(Math.floor(position.x));
|
|
||||||
pointMessage.setY(Math.floor(position.y));
|
|
||||||
const groupUpdateMessage = new GroupUpdateZoneMessage();
|
|
||||||
groupUpdateMessage.setGroupid(group.getId());
|
|
||||||
groupUpdateMessage.setPosition(pointMessage);
|
|
||||||
groupUpdateMessage.setGroupsize(group.getSize);
|
|
||||||
groupUpdateMessage.setFromzone(this.toProtoZone(fromZone));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setGroupupdatezonemessage(groupUpdateMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, client);
|
|
||||||
//client.emitInBatch(subMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void {
|
|
||||||
const groupDeleteMessage = new GroupLeftZoneMessage();
|
|
||||||
groupDeleteMessage.setGroupid(groupId);
|
|
||||||
groupDeleteMessage.setTozone(this.toProtoZone(newZone));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setGroupleftzonemessage(groupDeleteMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, client);
|
|
||||||
//user.emitInBatch(subMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void {
|
|
||||||
const userLeftMessage = new UserLeftZoneMessage();
|
|
||||||
userLeftMessage.setUserid(userId);
|
|
||||||
userLeftMessage.setTozone(this.toProtoZone(newZone));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setUserleftzonemessage(userLeftMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, client);
|
|
||||||
}
|
|
||||||
|
|
||||||
private toProtoZone(zone: Zone | null): ProtoZone | undefined {
|
|
||||||
if (zone !== null) {
|
|
||||||
const zoneMessage = new ProtoZone();
|
|
||||||
zoneMessage.setX(zone.x);
|
|
||||||
zoneMessage.setY(zone.y);
|
|
||||||
return zoneMessage;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private joinWebRtcRoom(user: User, group: Group) {
|
|
||||||
for (const otherUser of group.getUsers()) {
|
|
||||||
if (user === otherUser) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's send 2 messages: one to the user joining the group and one to the other user
|
|
||||||
const webrtcStartMessage1 = new WebRtcStartMessage();
|
|
||||||
webrtcStartMessage1.setUserid(otherUser.id);
|
|
||||||
webrtcStartMessage1.setInitiator(true);
|
|
||||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
|
||||||
const { username, password } = this.getTURNCredentials(
|
|
||||||
otherUser.id.toString(),
|
|
||||||
TURN_STATIC_AUTH_SECRET
|
|
||||||
);
|
|
||||||
webrtcStartMessage1.setWebrtcusername(username);
|
|
||||||
webrtcStartMessage1.setWebrtcpassword(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverToClientMessage1 = new ServerToClientMessage();
|
|
||||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
|
||||||
|
|
||||||
user.socket.write(serverToClientMessage1);
|
|
||||||
|
|
||||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
|
||||||
webrtcStartMessage2.setUserid(user.id);
|
|
||||||
webrtcStartMessage2.setInitiator(false);
|
|
||||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
|
||||||
const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
|
|
||||||
webrtcStartMessage2.setWebrtcusername(username);
|
|
||||||
webrtcStartMessage2.setWebrtcpassword(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverToClientMessage2 = new ServerToClientMessage();
|
|
||||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
|
||||||
|
|
||||||
otherUser.socket.write(serverToClientMessage2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes a unique user/password for the TURN server, using a shared secret between the WorkAdventure API server
|
|
||||||
* and the Coturn server.
|
|
||||||
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
|
|
||||||
*/
|
|
||||||
private getTURNCredentials(name: string, secret: string): { username: string; password: string } {
|
|
||||||
const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours
|
|
||||||
const username = [unixTimeStamp, name].join(":");
|
|
||||||
const hmac = crypto.createHmac("sha1", secret);
|
|
||||||
hmac.setEncoding("base64");
|
|
||||||
hmac.write(username);
|
|
||||||
hmac.end();
|
|
||||||
const password = hmac.read() as string;
|
|
||||||
return {
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//disconnect user
|
|
||||||
private disConnectedUser(user: User, group: Group) {
|
|
||||||
// Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
|
|
||||||
// which will be shut for the other player).
|
|
||||||
// However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player,
|
|
||||||
// the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing).
|
|
||||||
// So we also send the disconnect event to the other player.
|
|
||||||
for (const otherUser of group.getUsers()) {
|
|
||||||
if (user === otherUser) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webrtcDisconnectMessage1 = new WebRtcDisconnectMessage();
|
|
||||||
webrtcDisconnectMessage1.setUserid(user.id);
|
|
||||||
|
|
||||||
const serverToClientMessage1 = new ServerToClientMessage();
|
|
||||||
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
|
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
|
||||||
otherUser.socket.write(serverToClientMessage1);
|
|
||||||
//}
|
|
||||||
|
|
||||||
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
|
|
||||||
webrtcDisconnectMessage2.setUserid(otherUser.id);
|
|
||||||
|
|
||||||
const serverToClientMessage2 = new ServerToClientMessage();
|
|
||||||
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
|
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
|
||||||
user.socket.write(serverToClientMessage2);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
|
|
||||||
return this.roomsPromises;
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
|
||||||
const room = queryJitsiJwtMessage.getJitsiroom();
|
|
||||||
const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead.
|
|
||||||
|
|
||||||
if (SECRET_JITSI_KEY === "") {
|
|
||||||
throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's see if the current client has
|
|
||||||
const isAdmin = user.tags.includes(tag);
|
|
||||||
|
|
||||||
const jwt = Jwt.sign(
|
|
||||||
{
|
|
||||||
aud: "jitsi",
|
|
||||||
iss: JITSI_ISS,
|
|
||||||
sub: JITSI_URL,
|
|
||||||
room: room,
|
|
||||||
moderator: isAdmin,
|
|
||||||
},
|
|
||||||
SECRET_JITSI_KEY,
|
|
||||||
{
|
|
||||||
expiresIn: "1d",
|
|
||||||
algorithm: "HS256",
|
|
||||||
header: {
|
|
||||||
alg: "HS256",
|
|
||||||
typ: "JWT",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendJitsiJwtMessage = new SendJitsiJwtMessage();
|
|
||||||
sendJitsiJwtMessage.setJitsiroom(room);
|
|
||||||
sendJitsiJwtMessage.setJwt(jwt);
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setSendjitsijwtmessage(sendJitsiJwtMessage);
|
|
||||||
|
|
||||||
user.socket.write(serverToClientMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
|
||||||
const sendUserMessage = new SendUserMessage();
|
|
||||||
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
|
||||||
sendUserMessage.setType(sendUserMessageToSend.getType());
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setSendusermessage(sendUserMessage);
|
|
||||||
user.socket.write(serverToClientMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) {
|
|
||||||
const banUserMessage = new BanUserMessage();
|
|
||||||
banUserMessage.setMessage(banUserMessageToSend.getMessage());
|
|
||||||
banUserMessage.setType(banUserMessageToSend.getType());
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setSendusermessage(banUserMessage);
|
|
||||||
user.socket.write(serverToClientMessage);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Let's leave the room now.
|
|
||||||
room.leave(user);
|
|
||||||
// Let's close the connection when the user is banned.
|
|
||||||
user.socket.end();
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("In addZoneListener, could not find room with id '" + roomId + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
const things = room.addZoneListener(call, x, y);
|
|
||||||
|
|
||||||
const batchMessage = new BatchToPusherMessage();
|
|
||||||
|
|
||||||
for (const thing of things) {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
const userJoinedMessage = new UserJoinedZoneMessage();
|
|
||||||
userJoinedMessage.setUserid(thing.id);
|
|
||||||
userJoinedMessage.setUseruuid(thing.uuid);
|
|
||||||
userJoinedMessage.setName(thing.name);
|
|
||||||
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
|
||||||
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
|
||||||
if (thing.visitCardUrl) {
|
|
||||||
userJoinedMessage.setVisitcardurl(thing.visitCardUrl);
|
|
||||||
}
|
|
||||||
userJoinedMessage.setCompanion(thing.companion);
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setUserjoinedzonemessage(userJoinedMessage);
|
|
||||||
|
|
||||||
batchMessage.addPayload(subMessage);
|
|
||||||
} else if (thing instanceof Group) {
|
|
||||||
const groupUpdateMessage = new GroupUpdateZoneMessage();
|
|
||||||
groupUpdateMessage.setGroupid(thing.getId());
|
|
||||||
groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition()));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setGroupupdatezonemessage(groupUpdateMessage);
|
|
||||||
|
|
||||||
batchMessage.addPayload(subMessage);
|
|
||||||
} else {
|
|
||||||
console.error("Unexpected type for Movable returned by setViewport");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
call.write(batchMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
room.removeZoneListener(call, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addRoomListener(call: RoomSocket, roomId: string) {
|
|
||||||
const room = await this.getOrCreateRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("In addRoomListener, could not find room with id '" + roomId + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
room.addRoomListener(call);
|
|
||||||
|
|
||||||
const batchMessage = new BatchToPusherRoomMessage();
|
|
||||||
|
|
||||||
call.write(batchMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeRoomListener(call: RoomSocket, roomId: string) {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
room.removeRoomListener(call);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
|
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
|
||||||
|
|
||||||
room.adminJoin(admin);
|
|
||||||
|
|
||||||
return room;
|
|
||||||
}
|
|
||||||
|
|
||||||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
|
||||||
room.adminLeave(admin);
|
|
||||||
if (room.isEmpty()) {
|
|
||||||
this.roomsPromises.delete(room.roomUrl);
|
|
||||||
gaugeManager.decNbRoomGauge();
|
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string, type: string): Promise<void> {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
console.error(
|
|
||||||
"In sendAdminMessage, could not find room with id '" +
|
|
||||||
roomId +
|
|
||||||
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = room.getUsersByUuid(recipientUuid);
|
|
||||||
if (recipients.length === 0) {
|
|
||||||
console.error(
|
|
||||||
"In sendAdminMessage, could not find user with id '" +
|
|
||||||
recipientUuid +
|
|
||||||
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const recipient of recipients) {
|
|
||||||
const sendUserMessage = new SendUserMessage();
|
|
||||||
sendUserMessage.setMessage(message);
|
|
||||||
sendUserMessage.setType(type);
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setSendusermessage(sendUserMessage);
|
|
||||||
|
|
||||||
recipient.socket.write(serverToClientMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async banUser(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
console.error(
|
|
||||||
"In banUser, could not find room with id '" +
|
|
||||||
roomId +
|
|
||||||
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = room.getUsersByUuid(recipientUuid);
|
|
||||||
if (recipients.length === 0) {
|
|
||||||
console.error(
|
|
||||||
"In banUser, could not find user with id '" +
|
|
||||||
recipientUuid +
|
|
||||||
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const recipient of recipients) {
|
|
||||||
// Let's leave the room now.
|
|
||||||
room.leave(recipient);
|
|
||||||
|
|
||||||
const banUserMessage = new BanUserMessage();
|
|
||||||
banUserMessage.setMessage(message);
|
|
||||||
banUserMessage.setType("banned");
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setBanusermessage(banUserMessage);
|
|
||||||
|
|
||||||
// Let's close the connection when the user is banned.
|
|
||||||
recipient.socket.write(serverToClientMessage);
|
|
||||||
recipient.socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendAdminRoomMessage(roomId: string, message: string, type: string) {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
//todo: this should cause the http call to return a 500
|
|
||||||
console.error(
|
|
||||||
"In sendAdminRoomMessage, could not find room with id '" +
|
|
||||||
roomId +
|
|
||||||
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
room.getUsers().forEach((recipient) => {
|
|
||||||
const sendUserMessage = new SendUserMessage();
|
|
||||||
sendUserMessage.setMessage(message);
|
|
||||||
sendUserMessage.setType(type);
|
|
||||||
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
|
||||||
clientMessage.setSendusermessage(sendUserMessage);
|
|
||||||
|
|
||||||
recipient.socket.write(clientMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispatchWorldFullWarning(roomId: string): Promise<void> {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
//todo: this should cause the http call to return a 500
|
|
||||||
console.error(
|
|
||||||
"In dispatchWorldFullWarning, could not find room with id '" +
|
|
||||||
roomId +
|
|
||||||
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
room.getUsers().forEach((recipient) => {
|
|
||||||
const worldFullMessage = new WorldFullWarningMessage();
|
|
||||||
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
|
||||||
clientMessage.setWorldfullwarningmessage(worldFullMessage);
|
|
||||||
|
|
||||||
recipient.socket.write(clientMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispatchRoomRefresh(roomId: string): Promise<void> {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionNumber = room.incrementVersion();
|
|
||||||
room.getUsers().forEach((recipient) => {
|
|
||||||
const worldFullMessage = new RefreshRoomMessage();
|
|
||||||
worldFullMessage.setRoomid(roomId);
|
|
||||||
worldFullMessage.setVersionnumber(versionNumber);
|
|
||||||
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
|
||||||
clientMessage.setRefreshroommessage(worldFullMessage);
|
|
||||||
|
|
||||||
recipient.socket.write(clientMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEmoteEventMessage(room: GameRoom, user: User, emotePromptMessage: EmotePromptMessage) {
|
|
||||||
const emoteEventMessage = new EmoteEventMessage();
|
|
||||||
emoteEventMessage.setEmote(emotePromptMessage.getEmote());
|
|
||||||
emoteEventMessage.setActoruserid(user.id);
|
|
||||||
room.emitEmoteEvent(user, emoteEventMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFollowRequestMessage(room: GameRoom, user: User, message: FollowRequestMessage) {
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
|
||||||
clientMessage.setFollowrequestmessage(message);
|
|
||||||
room.sendToOthersInGroupIncludingUser(user, clientMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFollowConfirmationMessage(room: GameRoom, user: User, message: FollowConfirmationMessage) {
|
|
||||||
const leader = room.getUserById(message.getLeader());
|
|
||||||
if (!leader) {
|
|
||||||
const message = `Could not follow user "{message.getLeader()}" in room "{room.roomUrl}".`;
|
|
||||||
console.info(message, "Maybe the user just left.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// By security, we look at the group leader. If the group leader is NOT the leader in the message,
|
|
||||||
// everybody should stop following the group leader (to avoid having 2 group leaders)
|
|
||||||
if (user?.group?.leader && user?.group?.leader !== leader) {
|
|
||||||
user?.group?.leader?.stopLeading();
|
|
||||||
}
|
|
||||||
|
|
||||||
leader.addFollower(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFollowAbortMessage(room: GameRoom, user: User, message: FollowAbortMessage) {
|
|
||||||
if (user.id === message.getLeader()) {
|
|
||||||
user?.group?.leader?.stopLeading();
|
|
||||||
} else {
|
|
||||||
// Forward message
|
|
||||||
const leader = room.getUserById(message.getLeader());
|
|
||||||
leader?.delFollower(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const socketManager = new SocketManager();
|
|
|
@ -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,14 +0,0 @@
|
||||||
import {arrayIntersect} from "../src/Services/ArrayHelper";
|
|
||||||
|
|
||||||
|
|
||||||
describe("RoomIdentifier", () => {
|
|
||||||
it("should return true on intersect", () => {
|
|
||||||
expect(arrayIntersect(['admin', 'user'], ['admin', 'superAdmin'])).toBe(true);
|
|
||||||
});
|
|
||||||
it("should be reflexive", () => {
|
|
||||||
expect(arrayIntersect(['admin', 'superAdmin'], ['admin', 'user'])).toBe(true);
|
|
||||||
});
|
|
||||||
it("should return false on non intersect", () => {
|
|
||||||
expect(arrayIntersect(['admin', 'user'], ['superAdmin'])).toBe(false);
|
|
||||||
});
|
|
||||||
})
|
|
|
@ -1,148 +0,0 @@
|
||||||
import "jasmine";
|
|
||||||
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
|
|
||||||
import { Point } from "../src/Model/Websocket/MessageUserPosition";
|
|
||||||
import { Group } from "../src/Model/Group";
|
|
||||||
import { User, UserSocket } from "_Model/User";
|
|
||||||
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
|
|
||||||
import Direction = PositionMessage.Direction;
|
|
||||||
import { EmoteCallback } from "_Model/Zone";
|
|
||||||
|
|
||||||
function createMockUser(userId: number): User {
|
|
||||||
return {
|
|
||||||
userId,
|
|
||||||
} as unknown as User;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMockUserSocket(): UserSocket {
|
|
||||||
return {} as unknown as UserSocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage {
|
|
||||||
const positionMessage = new PositionMessage();
|
|
||||||
positionMessage.setX(x);
|
|
||||||
positionMessage.setY(y);
|
|
||||||
positionMessage.setDirection(Direction.DOWN);
|
|
||||||
positionMessage.setMoving(false);
|
|
||||||
const joinRoomMessage = new JoinRoomMessage();
|
|
||||||
joinRoomMessage.setUseruuid("1");
|
|
||||||
joinRoomMessage.setIpaddress("10.0.0.2");
|
|
||||||
joinRoomMessage.setName("foo");
|
|
||||||
joinRoomMessage.setRoomid("_/global/test.json");
|
|
||||||
joinRoomMessage.setPositionmessage(positionMessage);
|
|
||||||
return joinRoomMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {};
|
|
||||||
|
|
||||||
describe("GameRoom", () => {
|
|
||||||
it("should connect user1 and user2", async () => {
|
|
||||||
let connectCalledNumber: number = 0;
|
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
|
||||||
connectCalledNumber++;
|
|
||||||
};
|
|
||||||
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 user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(261, 100));
|
|
||||||
|
|
||||||
expect(connectCalledNumber).toBe(0);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(101, 100));
|
|
||||||
|
|
||||||
expect(connectCalledNumber).toBe(2);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(102, 100));
|
|
||||||
expect(connectCalledNumber).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should connect 3 users", async () => {
|
|
||||||
let connectCalled: boolean = false;
|
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
|
||||||
connectCalled = true;
|
|
||||||
};
|
|
||||||
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 user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
connectCalled = false;
|
|
||||||
|
|
||||||
// baz joins at the outer limit of the group
|
|
||||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(false);
|
|
||||||
|
|
||||||
world.updatePosition(user3, new Point(309, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should disconnect user1 and user2", async () => {
|
|
||||||
let connectCalled: boolean = false;
|
|
||||||
let disconnectCallNumber: number = 0;
|
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
|
||||||
connectCalled = true;
|
|
||||||
};
|
|
||||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
|
||||||
disconnectCallNumber++;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
expect(disconnectCallNumber).toBe(0);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
|
|
||||||
|
|
||||||
expect(disconnectCallNumber).toBe(2);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(262, 100));
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|