Compare commits
1 commit
develop
...
enter-anim
Author | SHA1 | Date | |
---|---|---|---|
a6e25ffc35 |
|
@ -1,2 +0,0 @@
|
|||
**/node_modules/**
|
||||
**/Dockerfile
|
|
@ -1,31 +1 @@
|
|||
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=
|
||||
DEBUG_MODE=false
|
177
.github/workflows/build-and-deploy.yml
vendored
|
@ -1,13 +1,7 @@
|
|||
name: Build, push and deploy Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
release:
|
||||
types: [created]
|
||||
pull_request:
|
||||
types: [ labeled, synchronize ]
|
||||
|
||||
- push
|
||||
|
||||
# Enables BuildKit
|
||||
env:
|
||||
|
@ -16,7 +10,7 @@ env:
|
|||
jobs:
|
||||
|
||||
build-front:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -26,21 +20,21 @@ jobs:
|
|||
|
||||
|
||||
# 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"
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
dockerfile: front/Dockerfile
|
||||
path: ./
|
||||
path: front/
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
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
|
||||
|
||||
build-back:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -49,156 +43,73 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
|
||||
# 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"
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
dockerfile: back/Dockerfile
|
||||
path: ./
|
||||
path: back/
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: thecodingmachine/workadventure-back
|
||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
build-pusher:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- 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: pusher/Dockerfile
|
||||
path: ./
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: thecodingmachine/workadventure-pusher
|
||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
build-uploader:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- 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 }}
|
||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
deeploy:
|
||||
needs:
|
||||
- build-front
|
||||
- build-back
|
||||
- build-pusher
|
||||
- build-maps
|
||||
- build-uploader
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
|
||||
- name: Write certificate
|
||||
run: echo "${CERTS_PRIVATE_KEY}" > secret.key && chmod 0600 secret.key
|
||||
env:
|
||||
CERTS_PRIVATE_KEY: ${{ secrets.CERTS_PRIVATE_KEY }}
|
||||
|
||||
- name: Download certificate
|
||||
run: mkdir secrets && scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i secret.key ubuntu@cert.workadventu.re:./config/live/workadventu.re/* secrets/
|
||||
|
||||
- name: Create namespace
|
||||
uses: steebchen/kubectl@v1.0.0
|
||||
env:
|
||||
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||
with:
|
||||
args: create namespace workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Delete old certificates in namespace
|
||||
uses: steebchen/kubectl@v1.0.0
|
||||
env:
|
||||
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||
with:
|
||||
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} delete secret certificate-tls
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install certificates in namespace
|
||||
uses: steebchen/kubectl@v1.0.0
|
||||
env:
|
||||
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||
with:
|
||||
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} create secret tls certificate-tls --key="secrets/privkey.pem" --cert="secrets/fullchain.pem"
|
||||
- uses: rlespinasse/github-slug-action@1.1.0
|
||||
|
||||
- name: Deploy
|
||||
uses: thecodingmachine/deeployer-action@master
|
||||
uses: thecodingmachine/deeployer@master
|
||||
env:
|
||||
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:
|
||||
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
|
||||
uses: unsplash/comment-on-pr@v1.2.0
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||
with:
|
||||
env: host=${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com,port=80
|
||||
spec: cypress/integration/spec.js
|
||||
wait-on: https://${{ 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://workadventu.re
|
||||
with:
|
||||
env: host=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
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
- delete
|
||||
|
||||
# Enables BuildKit
|
||||
env:
|
||||
|
@ -15,12 +14,13 @@ jobs:
|
|||
|
||||
steps:
|
||||
# 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
|
||||
continue-on-error: true
|
||||
uses: thecodingmachine/deeployer-cleanup-action@master
|
||||
env:
|
||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
||||
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
|
110
.github/workflows/continuous_integration.yml
vendored
|
@ -3,13 +3,11 @@
|
|||
name: "Continuous Integration"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
- "pull_request"
|
||||
- "push"
|
||||
|
||||
jobs:
|
||||
|
||||
continuous-integration-front:
|
||||
name: "Continuous Integration Front"
|
||||
|
||||
|
@ -22,103 +20,22 @@ jobs:
|
|||
- 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'
|
||||
node-version: '12.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
|
||||
env:
|
||||
PUSHER_URL: "//localhost:8080"
|
||||
ADMIN_URL: "//localhost:80"
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Svelte check"
|
||||
run: yarn run svelte-check
|
||||
API_URL: "http://localhost:8080"
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Lint"
|
||||
run: yarn run lint
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Pretty"
|
||||
run: yarn run pretty-check
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Jasmine"
|
||||
run: yarn test
|
||||
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:
|
||||
name: "Continuous Integration Back"
|
||||
|
||||
|
@ -133,23 +50,10 @@ jobs:
|
|||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.x'
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: yarn install
|
||||
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"
|
||||
run: yarn run tsc
|
||||
working-directory: "back"
|
||||
|
@ -162,7 +66,3 @@ jobs:
|
|||
run: yarn test
|
||||
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
|
||||
.vagrant
|
||||
Vagrantfile
|
||||
docker-compose.override.yaml
|
||||
*.DS_Store
|
||||
maps/yarn.lock
|
||||
maps/dist/computer.js
|
||||
maps/dist/computer.js.map
|
||||
node_modules
|
||||
_
|
||||
docker-compose.override.yaml
|
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 |
60
README.md
|
@ -1,48 +1,60 @@
|
|||
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/YGtngdh9gt)
|
||||
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg)
|
||||
|
||||
![WorkAdventure logo](README-LOGO.svg)
|
||||
![WorkAdventure office image](README-MAP.png)
|
||||
# Work Adventure
|
||||
|
||||
Live demo [here](https://play.workadventu.re/@/tcm/workadventure/wa-village).
|
||||
## Work in progress
|
||||
|
||||
# WorkAdventure
|
||||
|
||||
WorkAdventure is a web-based collaborative workspace presented in the form of a
|
||||
Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
|
||||
16-bit video game.
|
||||
|
||||
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
|
||||
|
||||
Check out resources developed by the WorkAdventure community at [awesome-workadventure](https://github.com/workadventure/awesome-workadventure)
|
||||
|
||||
## Setting up a development environment
|
||||
## Getting started
|
||||
|
||||
Install Docker.
|
||||
|
||||
Run:
|
||||
|
||||
```
|
||||
cp .env.template .env
|
||||
docker-compose up -d
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
The environment will start.
|
||||
|
||||
You should now be able to browse to http://play.workadventure.localhost/ and see the application.
|
||||
You can view the dashboard at http://workadventure.localhost:8080/
|
||||
You should now be able to browse to http://workadventure.localhost/ and see the application.
|
||||
|
||||
Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
||||
|
||||
**/etc/hosts**
|
||||
```
|
||||
127.0.0.1 workadventure.localhost
|
||||
workadventure.localhost 127.0.0.1
|
||||
```
|
||||
|
||||
Note: If on the first run you get a page with "network error". Try to ``docker-compose stop`` , then ``docker-compose start``.
|
||||
Note 2: If you are still getting "network error". Make sure you are authorizing the self-signed certificate by entering https://pusher.workadventure.localhost and accepting them.
|
||||
## Designing a map
|
||||
|
||||
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 your 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 can 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)
|
||||
|
||||
In order to place an 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)
|
||||
|
||||
### MacOS developers, your environment with Vagrant
|
||||
|
||||
|
@ -109,7 +121,5 @@ Vagrant destroy
|
|||
* `Vagrant halt`: stop your VM Vagrant.
|
||||
* `Vagrant destroy`: delete your VM Vagrant.
|
||||
|
||||
## Setting up a production environment
|
||||
|
||||
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.
|
||||
## Features developed
|
||||
You have more details of features developed in back [README.md](./back/README.md).
|
||||
|
|
20
SECURITY.md
|
@ -1,20 +0,0 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
First things first: **Do NOT report security vulnerabilities in public issues!**
|
||||
|
||||
Please disclose responsibly by sending
|
||||
a mail at security@workadventu.re (you can also ping us in the GitHub issues, but please, no details in the issues!)
|
||||
|
||||
We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix
|
||||
and release available for an eventual public disclosure.
|
||||
|
||||
We do not have a bug bounty program.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We only apply security patches on the latest tagged release and on the `master` and `develop` branches
|
||||
|
||||
Unless specified otherwise, do not expect us to fix security issues on past releases. We are only maintaining one release:
|
||||
the latest one, which is online at https://play.workadventu.re.
|
|
@ -2,7 +2,7 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
# Box / OS
|
||||
VAGRANT_BOX = 'bento/ubuntu-20.04'
|
||||
VAGRANT_BOX = 'bento/ubuntu-19.10'
|
||||
|
||||
# VM User — 'vagrant' by default
|
||||
VM_USER = 'vagrant'
|
||||
|
@ -58,7 +58,7 @@ Vagrant.configure(2) do |config|
|
|||
apt-get update -y
|
||||
apt-get install -y git
|
||||
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 gnupg-agent
|
||||
apt-get install -y software-properties-common
|
||||
|
@ -66,8 +66,8 @@ Vagrant.configure(2) do |config|
|
|||
apt-key fingerprint 0EBFCD88
|
||||
add-apt-repository \
|
||||
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
|
||||
$(lsb_release -cs) \
|
||||
stable"
|
||||
$(lsb_release -cs) \
|
||||
stable"
|
||||
apt-get update -y
|
||||
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
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||
"plugin:@typescript-eslint/eslint-recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
|
@ -17,15 +16,12 @@
|
|||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"no-throw-literal": "error"
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
src/Messages/generated
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4
|
||||
}
|
|
@ -1,26 +1,9 @@
|
|||
# protobuf build
|
||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
|
||||
WORKDIR /usr/src
|
||||
COPY messages .
|
||||
RUN yarn install && yarn proto
|
||||
FROM thecodingmachine/nodejs:12
|
||||
|
||||
# typescript build
|
||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2
|
||||
WORKDIR /usr/src
|
||||
COPY back/yarn.lock back/package.json ./
|
||||
COPY --chown=docker:docker . .
|
||||
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
|
||||
RUN yarn install --production
|
||||
|
||||
USER node
|
||||
CMD ["yarn", "run", "runprod"]
|
||||
CMD ["yarn", "run", "prod"]
|
||||
|
||||
|
|
691
back/LICENSE.txt
|
@ -1,691 +0,0 @@
|
|||
NOTICE
|
||||
This package contains software licensed under different
|
||||
licenses, please refer to the NOTICE.txt file for further
|
||||
information and LICENSES.txt for full license texts.
|
||||
|
||||
WorkAdventure Enterprise edition can be licensed independently from
|
||||
the source under separate commercial terms.
|
||||
|
||||
The software ("Software") is developed and owned by TheCodingMachine
|
||||
and is subject to the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, with the Commons Clause as follows:
|
||||
|
||||
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license
|
||||
for software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are
|
||||
designed to take away your freedom to share and change the works. By
|
||||
contrast, our General Public Licenses are intended to guarantee your
|
||||
freedom to share and change all versions of a program--to make sure it
|
||||
remains free software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public
|
||||
License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds
|
||||
of works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further restriction,
|
||||
you may remove that term. If a license document contains a further
|
||||
restriction but permits relicensing or conveying under this License, you
|
||||
may add to a covered work material governed by the terms of that license
|
||||
document, provided that the further restriction does not survive such
|
||||
relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have permission
|
||||
to link or combine any covered work with a work licensed under version 3
|
||||
of the GNU General Public License into a single combined work, and to
|
||||
convey the resulting work. The terms of this License will continue to
|
||||
apply to the part which is the covered work, but the work with which it is
|
||||
combined will remain governed by version 3 of the GNU General Public
|
||||
License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may differ
|
||||
in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero
|
||||
General Public License "or any later version" applies to it, you have
|
||||
the option of following the terms and conditions either of that
|
||||
numbered version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number
|
||||
of the GNU Affero General Public License, you may choose any version
|
||||
ever published by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that
|
||||
proxy's public statement of acceptance of a version permanently
|
||||
authorizes you to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"Commons Clause" License Condition
|
||||
|
||||
The Software is provided to you by the Licensor under the License, as
|
||||
defined below, subject to the following condition. Without limiting
|
||||
other conditions in the License, the grant of rights under the License
|
||||
will not include, and the License does not grant to you, the right to
|
||||
Sell the Software. For purposes of the foregoing, "Sell" means
|
||||
practicing any or all of the rights granted to you under the License
|
||||
to provide to third parties, for a fee or other consideration,
|
||||
a product or service that consists, entirely or substantially,
|
||||
of the Software or the functionality of the Software. Any license
|
||||
notice or attribution required by the License must also include
|
||||
this Commons Cause License Condition notice.
|
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,81 +5,43 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
"dev": "ts-node-dev --respawn ./server.ts",
|
||||
"prod": "tsc && node --max-old-space-size=4096 ./dist/server.js",
|
||||
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
||||
"profile": "tsc && node --prof ./dist/server.js",
|
||||
"dev": "ts-node-dev --respawn --transpileOnly ./server.ts",
|
||||
"prod": "tsc && node ./dist/server.js",
|
||||
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||
"lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
|
||||
"fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||
"precommit": "lint-staged",
|
||||
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
||||
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thecodingmachine/workadventure.git"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Grégoire Parant",
|
||||
"email": "g.parant@thecodingmachine.com"
|
||||
},
|
||||
{
|
||||
"name": "David Négrier",
|
||||
"email": "d.negrier@thecodingmachine.com"
|
||||
},
|
||||
{
|
||||
"name": "Arthmaël Poly",
|
||||
"email": "a.poly@thecodingmachine.com"
|
||||
}
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"author": "g.parant@thecodingmachine.com",
|
||||
"license": "AGPL",
|
||||
"bugs": {
|
||||
"url": "https://github.com/thecodingmachine/workadventure/issues"
|
||||
},
|
||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||
"dependencies": {
|
||||
"@workadventure/tiled-map-type-guard": "^1.0.3",
|
||||
"axios": "^0.21.2",
|
||||
"busboy": "^0.3.1",
|
||||
"circular-json": "^0.5.9",
|
||||
"debug": "^4.3.1",
|
||||
"generic-type-guard": "^3.2.0",
|
||||
"google-protobuf": "^3.13.0",
|
||||
"grpc": "^1.24.4",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"@types/express": "^4.17.4",
|
||||
"@types/http-status-codes": "^1.2.0",
|
||||
"@types/jsonwebtoken": "^8.3.8",
|
||||
"@types/socket.io": "^2.1.4",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"http-status-codes": "^1.4.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"prom-client": "^12.0.0",
|
||||
"query-string": "^6.13.3",
|
||||
"redis": "^3.1.2",
|
||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"systeminformation": "^4.26.5",
|
||||
"ts-node-dev": "^1.0.0-pre.44",
|
||||
"typescript": "^3.8.3",
|
||||
"uuidv4": "^6.0.7"
|
||||
},
|
||||
"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/jsonwebtoken": "^8.3.8",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/redis": "^2.8.31",
|
||||
"@types/uuidv4": "^5.0.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"
|
||||
]
|
||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||
"@typescript-eslint/parser": "^2.26.0",
|
||||
"eslint": "^6.8.0",
|
||||
"jasmine": "^3.5.0"
|
||||
}
|
||||
}
|
||||
|
|
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
|
||||
import App from "./src/App";
|
||||
import grpc from "grpc";
|
||||
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);
|
||||
App.listen(8080, () => console.log(`Example app listening on port 8080!`))
|
|
@ -1,19 +1,52 @@
|
|||
// lib/app.ts
|
||||
import { PrometheusController } from "./Controller/PrometheusController";
|
||||
import { DebugController } from "./Controller/DebugController";
|
||||
import { App as uwsApp } from "./Server/sifrr.server";
|
||||
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
||||
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
||||
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";
|
||||
|
||||
class App {
|
||||
public app: uwsApp;
|
||||
public prometheusController: PrometheusController;
|
||||
private debugController: DebugController;
|
||||
public app: Application;
|
||||
public server: http.Server;
|
||||
public ioSocketController: IoSocketController;
|
||||
public authenticateController: AuthenticateController;
|
||||
public mapController: MapController;
|
||||
|
||||
constructor() {
|
||||
this.app = new uwsApp();
|
||||
this.app = express();
|
||||
|
||||
this.prometheusController = new PrometheusController(this.app);
|
||||
this.debugController = new DebugController(this.app);
|
||||
//config server http
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
932
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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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"
|
||||
}],
|
||||
"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,
|
||||
"height":10,
|
||||
"editorsettings":
|
||||
{
|
||||
"chunksize":
|
||||
{
|
||||
"height":32,
|
||||
"width":32
|
||||
},
|
||||
"export":
|
||||
{
|
||||
"format":"json",
|
||||
"target":"map.json"
|
||||
}
|
||||
},
|
||||
"height":16,
|
||||
"infinite":false,
|
||||
"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],
|
||||
"height":10,
|
||||
"id":1,
|
||||
"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,
|
||||
"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":16,
|
||||
"id":8,
|
||||
"name":"start",
|
||||
"opacity":1,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":10,
|
||||
"width":15,
|
||||
"x":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],
|
||||
"height":10,
|
||||
"id":5,
|
||||
"name":"first_cowebsite",
|
||||
"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",
|
||||
"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":16,
|
||||
"id":9,
|
||||
"name":"exit",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"jitsiRoom",
|
||||
"name":"exitSceneUrl",
|
||||
"type":"string",
|
||||
"value":"ChillZone"
|
||||
},
|
||||
{
|
||||
"name":"jitsiTrigger",
|
||||
"type":"string",
|
||||
"value":"onaction"
|
||||
"value":"..\/Floor0\/floor0.json"
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"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,
|
||||
"y":0
|
||||
},
|
||||
|
@ -65,25 +73,7 @@
|
|||
"draworder":"topdown",
|
||||
"id":3,
|
||||
"name":"floorLayer",
|
||||
"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
|
||||
}],
|
||||
"objects":[],
|
||||
"opacity":1,
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
|
@ -91,96 +81,54 @@
|
|||
"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],
|
||||
"height":10,
|
||||
"id":8,
|
||||
"name":"objects",
|
||||
"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":16,
|
||||
"id":7,
|
||||
"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,
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":10,
|
||||
"width":15,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":9,
|
||||
"nextobjectid":3,
|
||||
"nextlayerid":10,
|
||||
"nextobjectid":1,
|
||||
"orientation":"orthogonal",
|
||||
"properties":[
|
||||
{
|
||||
"name":"script",
|
||||
"type":"string",
|
||||
"value":"script.js"
|
||||
}],
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.7.2",
|
||||
"tiledversion":"1.3.3",
|
||||
"tileheight":32,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":11,
|
||||
"columns":16,
|
||||
"firstgid":1,
|
||||
"image":"tileset1.png",
|
||||
"imageheight":352,
|
||||
"imagewidth":352,
|
||||
"image":"tilesets_deviant_milkian_1.png",
|
||||
"imageheight":512,
|
||||
"imagewidth":512,
|
||||
"margin":0,
|
||||
"name":"tileset1",
|
||||
"name":"office_1",
|
||||
"spacing":0,
|
||||
"tilecount":121,
|
||||
"tilecount":256,
|
||||
"tileheight":32,
|
||||
"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,
|
||||
"properties":[
|
||||
|
@ -190,44 +138,26 @@
|
|||
"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,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
"value":false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"id":16,
|
||||
"id":13,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"id":14,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -236,43 +166,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":17,
|
||||
"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,
|
||||
"id":15,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -289,42 +183,6 @@
|
|||
"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,
|
||||
"properties":[
|
||||
|
@ -362,7 +220,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":32,
|
||||
"id":39,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -371,39 +229,12 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":34,
|
||||
"id":44,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}]
|
||||
},
|
||||
{
|
||||
"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
|
||||
"value":false
|
||||
}]
|
||||
},
|
||||
{
|
||||
|
@ -412,11 +243,11 @@
|
|||
{
|
||||
"name":"collides",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
"value":false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"id":46,
|
||||
"id":48,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -425,7 +256,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":59,
|
||||
"id":49,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -434,7 +265,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":60,
|
||||
"id":50,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -443,7 +274,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":70,
|
||||
"id":51,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -452,7 +283,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":71,
|
||||
"id":52,
|
||||
"properties":[
|
||||
{
|
||||
"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":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -470,7 +319,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":81,
|
||||
"id":65,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -479,7 +328,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":89,
|
||||
"id":66,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -488,7 +337,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":91,
|
||||
"id":67,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -497,7 +346,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":93,
|
||||
"id":68,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -506,7 +355,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":94,
|
||||
"id":72,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -515,7 +364,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":95,
|
||||
"id":73,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -524,7 +373,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":96,
|
||||
"id":84,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -533,7 +382,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":97,
|
||||
"id":152,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -542,7 +391,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":100,
|
||||
"id":153,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -551,7 +400,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":102,
|
||||
"id":154,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -560,7 +409,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":103,
|
||||
"id":155,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -569,7 +418,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":104,
|
||||
"id":156,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -578,7 +427,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":105,
|
||||
"id":157,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -587,7 +436,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":106,
|
||||
"id":161,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -596,7 +445,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":107,
|
||||
"id":162,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -605,7 +454,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":108,
|
||||
"id":168,
|
||||
"properties":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -614,7 +463,7 @@
|
|||
}]
|
||||
},
|
||||
{
|
||||
"id":114,
|
||||
"id":169,
|
||||
"properties":[
|
||||
{
|
||||
"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":[
|
||||
{
|
||||
"name":"collides",
|
||||
|
@ -635,6 +624,6 @@
|
|||
}],
|
||||
"tilewidth":32,
|
||||
"type":"map",
|
||||
"version":"1.6",
|
||||
"width":10
|
||||
"version":1.2,
|
||||
"width":15
|
||||
}
|
BIN
back/src/Assets/Maps/Lyon/tilesets_deviant_milkian_1.png
Normal file
After Width: | Height: | Size: 33 KiB |
35
back/src/Controller/AuthenticateController.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
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 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) => {
|
||||
let 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
|
||||
let userId = uuid();
|
||||
let token = Jwt.sign({name: param.name, userId: userId}, 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");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
398
back/src/Controller/IoSocketController.ts
Normal file
|
@ -0,0 +1,398 @@
|
|||
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 {SetPlayerDetailsMessage} from "_Model/Websocket/SetPlayerDetailsMessage";
|
||||
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
||||
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
||||
import si from "systeminformation";
|
||||
|
||||
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_OFFER = "webrtc-offer",
|
||||
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 {
|
||||
Io: socketIO.Server;
|
||||
Worlds: Map<string, World> = new Map<string, World>();
|
||||
sockets: Map<string, ExSocketInterface> = new Map<string, ExSocketInterface>();
|
||||
|
||||
constructor(server: http.Server) {
|
||||
this.Io = socketIO(server);
|
||||
|
||||
// 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) {
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
if(this.searchClientByToken(socket.handshake.query.token)){
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: any) => {
|
||||
if (err) {
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
(socket as ExSocketInterface).token = tokenDecoded;
|
||||
(socket as ExSocketInterface).userId = tokenDecoded.userId;
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
this.ioConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
searchClientByToken(token: string): ExSocketInterface | null {
|
||||
let clients: Array<any> = Object.values(this.Io.sockets.sockets);
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
let client: ExSocketInterface = 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
|
||||
let userId = group.getUsers()[0].id;
|
||||
let client: ExSocketInterface = this.searchClientByIdOrFail(userId);
|
||||
let 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.
|
||||
let userId = lastUser.id;
|
||||
let client: ExSocketInterface = this.searchClientByIdOrFail(userId);
|
||||
let roomId = client.roomId;
|
||||
this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid);
|
||||
}
|
||||
|
||||
ioConnection() {
|
||||
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
||||
let client : ExSocketInterface = socket as ExSocketInterface;
|
||||
this.sockets.set(client.userId, client);
|
||||
|
||||
// Let's log server load when a user joins
|
||||
let srvSockets = this.Io.sockets.sockets;
|
||||
console.log('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: any, answerFn): void => {
|
||||
try {
|
||||
let roomId = message.roomId;
|
||||
|
||||
if (typeof(roomId) !== 'string') {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Expected roomId as a string.'});
|
||||
return;
|
||||
}
|
||||
let position = this.hydratePositionReceive(message.position);
|
||||
if (position instanceof Error) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message});
|
||||
return;
|
||||
}
|
||||
|
||||
let Client = (socket as ExSocketInterface);
|
||||
|
||||
if (Client.roomId === roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
//leave previous room
|
||||
this.leaveRoom(Client);
|
||||
|
||||
//join new previous room
|
||||
let world = this.joinRoom(Client, roomId, position);
|
||||
|
||||
//add function to refresh position user in real time.
|
||||
//this.refreshUserPosition(Client);
|
||||
|
||||
let 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:
|
||||
let listOfUsers = Array.from(world.getUsers(), ([key, user]) => {
|
||||
let 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, (message: any): void => {
|
||||
try {
|
||||
let position = this.hydratePositionReceive(message);
|
||||
if (position instanceof Error) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message});
|
||||
return;
|
||||
}
|
||||
|
||||
let Client = (socket as ExSocketInterface);
|
||||
|
||||
// sending to all clients in room except sender
|
||||
Client.position = position;
|
||||
|
||||
// update position in the world
|
||||
let 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: any) => {
|
||||
//send only at user
|
||||
let 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.WEBRTC_OFFER, (data: any) => {
|
||||
//send only at user
|
||||
let client = this.sockets.get(data.receiverId);
|
||||
if (client === undefined) {
|
||||
console.warn("While exchanging a WebRTC offer: client with id ", data.receiverId, " does not exist. This might be a race condition.");
|
||||
return;
|
||||
}
|
||||
client.emit(SockerIoEvent.WEBRTC_OFFER, data);
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.DISCONNECT, () => {
|
||||
let 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
|
||||
let srvSockets = this.Io.sockets.sockets;
|
||||
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: SetPlayerDetailsMessage, answerFn) => {
|
||||
let Client = (socket as ExSocketInterface);
|
||||
Client.name = playerDetails.name;
|
||||
Client.character = playerDetails.character;
|
||||
answerFn(Client.userId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
searchClientByIdOrFail(userId: string): ExSocketInterface {
|
||||
let 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
|
||||
let world : World|undefined = this.Worlds.get(Client.roomId);
|
||||
if(world){
|
||||
world.leave(Client);
|
||||
}
|
||||
//user leave previous room
|
||||
Client.leave(Client.roomId);
|
||||
delete Client.roomId;
|
||||
}
|
||||
}
|
||||
|
||||
private joinRoom(Client : ExSocketInterface, roomId: string, position: Point): World {
|
||||
//join user in room
|
||||
Client.join(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;
|
||||
}
|
||||
let 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) => {
|
||||
|
||||
let clientsId = clients.reduce((tabs: Array<any>, 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});
|
||||
});
|
||||
}
|
||||
|
||||
//Hydrate and manage error
|
||||
hydratePositionReceive(message: any): Point | Error {
|
||||
try {
|
||||
if (!message.x || !message.y || !message.direction || message.moving === undefined) {
|
||||
return new Error("invalid point message sent");
|
||||
}
|
||||
return new Point(message.x, message.y, message.direction, message.moving);
|
||||
} catch (err) {
|
||||
//TODO log error
|
||||
return new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}*/
|
||||
let Client = this.searchClientByIdOrFail(userId);
|
||||
this.joinWebRtcRoom(Client, group.getId());
|
||||
}
|
||||
|
||||
//disconnect user
|
||||
disConnectedUser(userId: string, group: Group) {
|
||||
let Client = this.searchClientByIdOrFail(userId);
|
||||
Client.to(group.getId()).emit(SockerIoEvent.WEBRTC_DISCONNECT, {
|
||||
userId: userId
|
||||
});
|
||||
|
||||
//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) => {
|
||||
return res.status(OK).send({
|
||||
mapUrlStart: req.headers.host + "/map/files" + URL_ROOM_STARTED,
|
||||
startInstance: "global"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import { App } from "../Server/sifrr.server";
|
||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||
import { register, collectDefaultMetrics } from "prom-client";
|
||||
|
||||
export class PrometheusController {
|
||||
constructor(private App: App) {
|
||||
collectDefaultMetrics({
|
||||
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.json", this.metricsAsJSON.bind(this));
|
||||
}
|
||||
|
||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||
res.writeHeader("Content-Type", register.contentType);
|
||||
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 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 {
|
||||
SECRET_KEY,
|
||||
URL_ROOM_STARTED,
|
||||
MINIMUM_DISTANCE,
|
||||
ADMIN_API_URL,
|
||||
ADMIN_API_TOKEN,
|
||||
HTTP_PORT,
|
||||
GRPC_PORT,
|
||||
GROUP_RADIUS,
|
||||
ALLOW_ARTILLERY,
|
||||
CPU_OVERHEAT_THRESHOLD,
|
||||
JITSI_URL,
|
||||
JITSI_ISS,
|
||||
SECRET_JITSI_KEY,
|
||||
};
|
||||
GROUP_RADIUS
|
||||
}
|
||||
|
|
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 { User } from "./User";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionNotifier } from "_Model/PositionNotifier";
|
||||
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
|
||||
import type { Zone } from "../Model/Zone";
|
||||
import { World, ConnectCallback, DisconnectCallback } from "./World";
|
||||
import { UserInterface } from "./UserInterface";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {uuid} from "uuidv4";
|
||||
|
||||
export class Group implements Movable {
|
||||
private static nextId: number = 1;
|
||||
export class Group {
|
||||
static readonly MAX_PER_GROUP = 4;
|
||||
|
||||
private id: number;
|
||||
private users: Set<User>;
|
||||
private x!: number;
|
||||
private y!: number;
|
||||
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;
|
||||
private id: string;
|
||||
private users: UserInterface[];
|
||||
private connectCallback: ConnectCallback;
|
||||
private disconnectCallback: DisconnectCallback;
|
||||
|
||||
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.updatePosition();
|
||||
}
|
||||
|
||||
getUsers(): User[] {
|
||||
return Array.from(this.users.values());
|
||||
getUsers(): UserInterface[] {
|
||||
return this.users;
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
getId() : string{
|
||||
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)
|
||||
*/
|
||||
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 {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
x,
|
||||
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 {
|
||||
return this.users.size >= MAX_PER_GROUP;
|
||||
return this.users.length >= Group.MAX_PER_GROUP;
|
||||
}
|
||||
|
||||
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
|
||||
this.connectCallback(user, this);
|
||||
this.users.add(user);
|
||||
this.connectCallback(user.id, this);
|
||||
this.users.push(user);
|
||||
user.group = this;
|
||||
}
|
||||
|
||||
leave(user: User): void {
|
||||
const success = this.users.delete(user);
|
||||
if (success === false) {
|
||||
throw new Error(`Could not find user ${user.id} in the group ${this.id}`);
|
||||
isPartOfGroup(user: UserInterface): boolean
|
||||
{
|
||||
return this.users.indexOf(user) !== -1;
|
||||
}
|
||||
|
||||
/*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;
|
||||
|
||||
if (this.users.size !== 0) {
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
// Broadcast on the right event
|
||||
this.disconnectCallback(user, this);
|
||||
this.disconnectCallback(user.id, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Let's kick everybody out.
|
||||
* Usually used when there is only one user left.
|
||||
*/
|
||||
destroy(): void {
|
||||
if (!this.outOfBounds) {
|
||||
this.positionNotifier.leave(this);
|
||||
}
|
||||
|
||||
for (const user of this.users) {
|
||||
destroy(): void
|
||||
{
|
||||
this.users.forEach((user: UserInterface) => {
|
||||
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 {
|
||||
x: number;
|
||||
y: number;
|
||||
x: 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;
|
||||
}
|
13
back/src/Model/Websocket/ExSocketInterface.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {Socket} from "socket.io";
|
||||
import {PointInterface} from "./PointInterface";
|
||||
import {Identificable} from "./Identificable";
|
||||
|
||||
export interface ExSocketInterface extends Socket, Identificable {
|
||||
token: any;
|
||||
roomId: string;
|
||||
webRtcRoomId: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
character: string;
|
||||
position: PointInterface;
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
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>;
|
7
back/src/Model/Websocket/MessageUserJoined.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {Point} from "./MessageUserPosition";
|
||||
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 {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
public direction: string = "none",
|
||||
public moving: boolean = false
|
||||
) {}
|
||||
export class Point implements PointInterface{
|
||||
constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageUserPosition {
|
||||
constructor(public userId: string, public name: string, public character: string, public position: PointInterface) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,5 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
/*export interface PointInterface {
|
||||
export interface PointInterface {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly direction: string;
|
||||
readonly moving: boolean;
|
||||
}*/
|
||||
|
||||
export const isPointInterface = new tg.IsInterface()
|
||||
.withProperties({
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber,
|
||||
direction: tg.isString,
|
||||
moving: tg.isBoolean,
|
||||
})
|
||||
.get();
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
4
back/src/Model/Websocket/SetPlayerDetailsMessage.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface SetPlayerDetailsMessage {
|
||||
name: string,
|
||||
character: string
|
||||
}
|
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){
|
||||
let 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 {
|
||||
let 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?
|
||||
let closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user);
|
||||
|
||||
if (closestItem !== null) {
|
||||
if (closestItem instanceof Group) {
|
||||
// Let's join the group!
|
||||
closestItem.join(user);
|
||||
} else {
|
||||
let closestUser : UserInterface = closestItem;
|
||||
let 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?
|
||||
let 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 {
|
||||
let 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;
|
||||
}
|
||||
|
||||
let 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;
|
||||
}
|
||||
let 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();
|
||||
});
|
||||
});
|
|
@ -1,246 +0,0 @@
|
|||
import "jasmine";
|
||||
import { PositionNotifier } from "../src/Model/PositionNotifier";
|
||||
import { User, UserSocket } from "../src/Model/User";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { ZoneSocket } from "../src/RoomManager";
|
||||
|
||||
describe("PositionNotifier", () => {
|
||||
it("should receive notifications when player moves", () => {
|
||||
let enterTriggered = false;
|
||||
let moveTriggered = false;
|
||||
let leaveTriggered = false;
|
||||
|
||||
const positionNotifier = new PositionNotifier(
|
||||
300,
|
||||
300,
|
||||
(thing: Movable) => {
|
||||
enterTriggered = true;
|
||||
},
|
||||
(thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
},
|
||||
(thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = new User(
|
||||
1,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
const user2 = new User(
|
||||
2,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: -9999,
|
||||
y: -9999,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 1, 1);
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 1, 0);
|
||||
/*positionNotifier.setViewport(user1, {
|
||||
left: 200,
|
||||
right: 600,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
enterTriggered = false;
|
||||
|
||||
// Move inside the zone
|
||||
user2.setPosition({ x: 501, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(true);
|
||||
moveTriggered = false;
|
||||
|
||||
// Move out of the zone in a zone that we don't track
|
||||
user2.setPosition({ x: 901, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(true);
|
||||
leaveTriggered = false;
|
||||
|
||||
// Move back in
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
enterTriggered = false;
|
||||
|
||||
// Leave the room
|
||||
positionNotifier.leave(user2);
|
||||
//positionNotifier.removeViewport(user2);
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(true);
|
||||
leaveTriggered = false;
|
||||
});
|
||||
|
||||
it("should receive notifications when camera moves", () => {
|
||||
let enterTriggered = false;
|
||||
let moveTriggered = false;
|
||||
let leaveTriggered = false;
|
||||
|
||||
const positionNotifier = new PositionNotifier(
|
||||
300,
|
||||
300,
|
||||
(thing: Movable, fromZone: Zone | null) => {
|
||||
enterTriggered = true;
|
||||
},
|
||||
(thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
},
|
||||
(thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = new User(
|
||||
1,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
const user2 = new User(
|
||||
2,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
|
||||
const listener = {} as ZoneSocket;
|
||||
positionNotifier.addZoneListener(listener, 0, 0);
|
||||
positionNotifier.addZoneListener(listener, 0, 1);
|
||||
positionNotifier.addZoneListener(listener, 1, 1);
|
||||
positionNotifier.addZoneListener(listener, 1, 0);
|
||||
/*let newUsers = positionNotifier.setViewport(user1, {
|
||||
left: 200,
|
||||
right: 600,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
positionNotifier.enter(user1);
|
||||
positionNotifier.enter(user2);
|
||||
|
||||
//expect(newUsers.length).toBe(2);
|
||||
expect(enterTriggered).toBe(true);
|
||||
enterTriggered = false;
|
||||
|
||||
//positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0})
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(true);
|
||||
enterTriggered = false;
|
||||
leaveTriggered = false;
|
||||
|
||||
// Add a listener, but the user in not in this zone.
|
||||
positionNotifier.addZoneListener(listener, 10, 10);
|
||||
|
||||
/*positionNotifier.setViewport(user1, {
|
||||
left: 201,
|
||||
right: 601,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
|
||||
// Stop listening to zone
|
||||
positionNotifier.removeZoneListener(listener, 1, 1);
|
||||
// Move the viewport out of the user.
|
||||
/*positionNotifier.setViewport(user1, {
|
||||
left: 901,
|
||||
right: 1001,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
|
||||
// Move the viewport back on the user.
|
||||
positionNotifier.addZoneListener(listener, 1, 1);
|
||||
/*newUsers = positionNotifier.setViewport(user1, {
|
||||
left: 200,
|
||||
right: 600,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
enterTriggered = false;
|
||||
//expect(newUsers.length).toBe(2);
|
||||
});
|
||||
});
|
89
back/tests/WorldTest.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import "jasmine";
|
||||
import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World";
|
||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||
import { Group } from "../src/Model/Group";
|
||||
|
||||
describe("World", () => {
|
||||
it("should connect user1 and user2", () => {
|
||||
let connectCalledNumber: number = 0;
|
||||
let connect: ConnectCallback = (user: string, group: Group): void => {
|
||||
connectCalledNumber++;
|
||||
}
|
||||
let disconnect: DisconnectCallback = (user: string, group: Group): void => {
|
||||
|
||||
}
|
||||
|
||||
let world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
||||
|
||||
world.join({ userId: "foo" }, new Point(100, 100));
|
||||
|
||||
world.join({ userId: "bar" }, new Point(500, 100));
|
||||
|
||||
world.updatePosition({ userId: "bar" }, new Point(261, 100));
|
||||
|
||||
expect(connectCalledNumber).toBe(0);
|
||||
|
||||
world.updatePosition({ userId: "bar" }, new Point(101, 100));
|
||||
|
||||
expect(connectCalledNumber).toBe(2);
|
||||
|
||||
world.updatePosition({ userId: "bar" }, new Point(102, 100));
|
||||
expect(connectCalledNumber).toBe(2);
|
||||
});
|
||||
|
||||
it("should connect 3 users", () => {
|
||||
let connectCalled: boolean = false;
|
||||
let connect: ConnectCallback = (user: string, group: Group): void => {
|
||||
connectCalled = true;
|
||||
}
|
||||
let disconnect: DisconnectCallback = (user: string, group: Group): void => {
|
||||
|
||||
}
|
||||
|
||||
let world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
||||
|
||||
world.join({ userId: "foo" }, new Point(100, 100));
|
||||
|
||||
world.join({ userId: "bar" }, new Point(200, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
connectCalled = false;
|
||||
|
||||
// baz joins at the outer limit of the group
|
||||
world.join({ userId: "baz" }, new Point(311, 100));
|
||||
|
||||
expect(connectCalled).toBe(false);
|
||||
|
||||
world.updatePosition({ userId: "baz" }, new Point(309, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disconnect user1 and user2", () => {
|
||||
let connectCalled: boolean = false;
|
||||
let disconnectCallNumber: number = 0;
|
||||
let connect: ConnectCallback = (user: string, group: Group): void => {
|
||||
connectCalled = true;
|
||||
}
|
||||
let disconnect: DisconnectCallback = (user: string, group: Group): void => {
|
||||
disconnectCallNumber++;
|
||||
}
|
||||
|
||||
let world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
||||
|
||||
world.join({ userId: "foo" }, new Point(100, 100));
|
||||
|
||||
world.join({ userId: "bar" }, new Point(259, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
expect(disconnectCallNumber).toBe(0);
|
||||
|
||||
world.updatePosition({ userId: "bar" }, new Point(100+160+160+1, 100));
|
||||
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
|
||||
world.updatePosition({ userId: "bar" }, new Point(262, 100));
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
});
|
||||
|
||||
})
|