├── tests
├── mock
│ ├── components
│ │ └── test-component.js
│ ├── level-provider.js
│ └── templates
│ │ └── test.json
├── setup.js
├── index.test.js
├── utils
│ ├── client.js
│ ├── server.js
│ └── constants.js
├── lifecycle.js
├── messages.js
└── rooms.js
├── .npmignore
├── docs
├── templates
│ ├── index-property-item.ejs
│ ├── property-item.ejs
│ ├── index-event-item.ejs
│ ├── index.ejs
│ ├── index-function-item.ejs
│ ├── event-item.ejs
│ ├── callback-item.ejs
│ ├── function-item.ejs
│ └── class.ejs
├── client
│ ├── Levels.md
│ ├── README.md
│ ├── User.md
│ ├── InterpolateValue.md
│ ├── NetworkEntity.md
│ ├── Room.md
│ └── PlayNetwork.md
├── server
│ ├── README.md
│ ├── Users.md
│ ├── Rooms.md
│ ├── NetworkEntity.md
│ ├── Room.md
│ ├── User.md
│ └── PlayNetwork.md
└── README.md
├── .gitattributes
├── src
├── server
│ ├── core
│ │ ├── servers.js
│ │ ├── network-entities
│ │ │ ├── parsers.js
│ │ │ ├── network-entities.js
│ │ │ └── network-entity.js
│ │ ├── server.js
│ │ ├── users.js
│ │ ├── rooms.js
│ │ ├── user.js
│ │ └── room.js
│ ├── libs
│ │ ├── utils.js
│ │ ├── levels.js
│ │ ├── assets.js
│ │ ├── logger.js
│ │ ├── performance.js
│ │ ├── templates.js
│ │ └── scripts.js
│ └── index.js
└── client
│ ├── network-entities
│ └── network-entities.js
│ ├── user.js
│ ├── levels.js
│ ├── room.js
│ ├── interpolation.js
│ └── index.js
├── rollup.config.js
├── LICENSE
├── .eslintrc.json
├── .gitignore
├── package.json
├── jest.config.mjs
├── README.md
├── docs.js
└── dist
└── network-entity.js
/tests/mock/components/test-component.js:
--------------------------------------------------------------------------------
1 | pc.createScript('test');
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | /docs.js
3 | /rollup.config.js
4 | /package-lock.json
5 | /tmpl
6 | /examples
7 | /node_modules
8 | /tests
9 |
--------------------------------------------------------------------------------
/docs/templates/index-property-item.ejs:
--------------------------------------------------------------------------------
1 | .<%=name%> : <%-locals.type.names.map((i) => {
2 | return `${i}`;
3 | }).join(' | ')%>
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | src/node/libs/ammo.js linguist-vendored
2 | src/server/libs/permessage-deflate/**/* linguist-vendored
3 | package-lock.json linguist-generated
4 | dist/**/* linguist-generated
5 |
--------------------------------------------------------------------------------
/docs/templates/property-item.ejs:
--------------------------------------------------------------------------------
1 |
2 | ### .<%=name%> : <%-locals.type.names.map((i) => {
3 | return `${i}`;
4 | }).join(' | ')%>
5 | <%if(description){%><%-description%>
6 | <%}%>
7 |
--------------------------------------------------------------------------------
/docs/templates/index-event-item.ejs:
--------------------------------------------------------------------------------
1 | <%=name%><%if(locals?.params?.length){%> => (<%=locals?.params?.filter((i) => {
2 | return i.name.indexOf('.') === -1;
3 | }).map((i) => {
4 | return i.optional ? `[${i.name}]`: i.name;
5 | }).join(', ')%>)<%}%>
6 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | import { HTMLCanvasElement } from '@playcanvas/canvas-mock/src/index.mjs';
2 |
3 | import * as pc from 'playcanvas';
4 | global.pc = pc;
5 |
6 | const canvas = new HTMLCanvasElement(100, 100);
7 | canvas.id = -1;
8 | const app = new pc.Application(canvas);
9 | app.autoRender = false;
10 | app.start();
11 |
--------------------------------------------------------------------------------
/docs/templates/index.ejs:
--------------------------------------------------------------------------------
1 | # <%=title%>
2 |
3 | <%for(const [className, classItem] of classes) {%>
4 | ### <%=className%>
5 | <%-classItem.description%>
6 | <%}%>
7 |
8 | <% if (links.size) {%><% for(const [className, link] of links) { -%>
9 | [<%=className%>]: <%=link%>
10 | <% } %><% } -%>
--------------------------------------------------------------------------------
/tests/mock/level-provider.js:
--------------------------------------------------------------------------------
1 | class MockLevelProvider {
2 | mockData = new Map();
3 |
4 | save(id, data) {
5 | this.mockData.set(id, data);
6 | }
7 |
8 | load(id) {
9 | return this.mockData.get(id);
10 | }
11 |
12 | has(id) {
13 | return this.mockData.has(id);
14 | }
15 | }
16 |
17 | export default MockLevelProvider;
18 |
--------------------------------------------------------------------------------
/docs/templates/index-function-item.ejs:
--------------------------------------------------------------------------------
1 | <%=locals.kind==='constructor'?'new ':''%><%=name%>(<%=locals?.params?.filter((i) => {
2 | return i.name.indexOf('.') === -1;
3 | }).map((i) => {
4 | return i.optional ? `[${i.name}]`: i.name;
5 | }).join(', ')%>)<%=locals.async?' [async]':''%><%if(locals.returns) {%> => <%-returns[0].type.names.map((i) => {
6 | return `${i}`;
7 | }).join(' | ')%><%}%>
8 |
--------------------------------------------------------------------------------
/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { describe } from '@jest/globals';
2 |
3 | import { initialization, destruction } from './lifecycle.js';
4 | import { clientMessages, serverMessages } from './messages.js';
5 | import { levels, rooms } from './rooms.js';
6 |
7 | initialization();
8 |
9 | describe('Main tests', () => {
10 | clientMessages();
11 | serverMessages();
12 | levels();
13 | rooms();
14 | });
15 |
16 | destruction();
17 |
--------------------------------------------------------------------------------
/tests/utils/client.js:
--------------------------------------------------------------------------------
1 | import { jest } from '@jest/globals';
2 |
3 | export const createClient = (callback) => {
4 | jest.isolateModules(async () => {
5 | await import('./../../dist/pn.js');
6 | callback(window.pn);
7 | });
8 | };
9 |
10 | export const connectClient = (client, port, callback) => {
11 | const host = 'localhost';
12 | const useSSL = false;
13 | const payload = null;
14 | client.connect(host, port, useSSL, payload, callback);
15 | };
16 |
--------------------------------------------------------------------------------
/src/server/core/servers.js:
--------------------------------------------------------------------------------
1 | import pn from './../index.js';
2 | import Server from './server.js';
3 |
4 | export default class Servers extends Map {
5 | async get(id, callback) {
6 | let server = super.get(id);
7 | if (!server) {
8 | const url = await pn.redis.HGET('_route:server', id.toString());
9 | if (!url) return null;
10 |
11 | server = new Server(id, url);
12 | this.set(id, server);
13 | }
14 |
15 | callback(server);
16 | return server;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/client/network-entities/network-entities.js:
--------------------------------------------------------------------------------
1 | class NetworkEntities {
2 | constructor() {
3 | this._index = new Map();
4 | }
5 |
6 | get(id) {
7 | return this._index.get(id);
8 | }
9 |
10 | has(id) {
11 | return this._index.has(id);
12 | }
13 |
14 | add(networkEntity) {
15 | if (this.has(networkEntity.id)) return;
16 |
17 | this._index.set(networkEntity.id, networkEntity);
18 |
19 | networkEntity.entity.once('destroy', () => {
20 | this._index.delete(networkEntity.id)
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/utils/server.js:
--------------------------------------------------------------------------------
1 | import pn from './../../src/server/index.js';
2 | import { createServer as createHttpServer } from 'http';
3 | import MockLevelProvider from '../mock/level-provider.js';
4 |
5 | export const createServer = async (port) => {
6 | const server = createHttpServer();
7 | server.listen(port);
8 |
9 | await pn.start({
10 | scriptsPath: './tests/mock/components',
11 | templatesPath: './tests/mock/templates',
12 | redisUrl: 'redis://localhost:6379',
13 | server,
14 | useAmmo: false,
15 | levelProvider: new MockLevelProvider()
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/docs/client/Levels.md:
--------------------------------------------------------------------------------
1 | # Levels (client)
2 |
3 | Interface that allows to save hierarchy data to a server.
4 |
5 | ---
6 |
7 | # Index
8 |
9 |
10 | ### Functions
11 |
12 | save(sceneId, [callback])
13 |
14 |
15 | ---
16 |
17 |
18 | # Functions
19 |
20 |
21 | ### save(sceneId, [callback])
22 |
23 | Save the hierarchy data of a Scene to the server.
24 |
25 | | Param | Type | Description |
26 | | --- | --- | --- |
27 | | sceneId | `Number` | ID of a Scene. |
28 | | callback (optional) | `errorCallback` | Callback of a server response. |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/server/core/network-entities/parsers.js:
--------------------------------------------------------------------------------
1 | import { roundTo } from '../../libs/utils.js';
2 |
3 | export default new Map([
4 | [pc.Vec2, (data) => ({ x: roundTo(data.x), y: roundTo(data.y) })],
5 | [pc.Vec3, (data) => ({ x: roundTo(data.x), y: roundTo(data.y), z: roundTo(data.z) })],
6 | [pc.Vec4, (data) => ({ x: roundTo(data.x), y: roundTo(data.y), z: roundTo(data.z), w: roundTo(data.w) })],
7 | [pc.Quat, (data) => ({ x: roundTo(data.x), y: roundTo(data.y), z: roundTo(data.z), w: roundTo(data.w) })],
8 | [pc.Color, (data) => ({ r: data.r, g: data.g, b: data.b, a: data.a })],
9 | [Map, (data) => Array.from(data)],
10 | [Object, (data) => data]
11 | ]);
12 |
--------------------------------------------------------------------------------
/docs/templates/event-item.ejs:
--------------------------------------------------------------------------------
1 |
2 | ### <%=name%> [event]<%if(locals?.params?.length){%> => (<%=locals?.params?.filter((i) => {
3 | return i.name.indexOf('.') === -1;
4 | }).map((i) => {
5 | return i.optional ? `[${i.name}]`: i.name;
6 | }).join(', ')%>)<%}%>
7 | <%if(description){%><%-description%>
8 | <%}%><%paramsDescription = params.filter((p)=>{return !!p.description;}).length > 0;
9 | %>
10 | <%if(params.length){-%>| Param | Type |<%if(paramsDescription){%> Description |<%}%>
11 | | --- | --- |<%if(paramsDescription){%> --- |<%}%>
12 | <%for(const item of params){-%>
13 | | <%=item.name%> | <%-item.type.names.map((i) => {
14 | return `${i}`;
15 | }).join(' | ')%> |<%if(paramsDescription){%> <%-item.description%> |<%}%>
16 | <%}%><%}%>
17 |
18 |
--------------------------------------------------------------------------------
/docs/templates/callback-item.ejs:
--------------------------------------------------------------------------------
1 |
2 | ### <%=name%> [callback]<%if(locals?.params?.length){%> => (<%=locals?.params?.filter((i) => {
3 | return i.name.indexOf('.') === -1;
4 | }).map((i) => {
5 | return i.optional ? `[${i.name}]`: i.name;
6 | }).join(', ')%>)<%}%>
7 | <%if(description){%><%-description%>
8 | <%}%><%paramsDescription = params.filter((p)=>{return !!p.description;}).length > 0;
9 | %>
10 | <%if(params.length){-%>| Param | Type |<%if(paramsDescription){%> Description |<%}%>
11 | | --- | --- |<%if(paramsDescription){%> --- |<%}%>
12 | <%for(const item of params){-%>
13 | | <%=item.name%><%=item.optional?' (optional)':''%> | <%-item.type.names.map((i) => {
14 | return `${i}`;
15 | }).join(' | ')%> |<%if(paramsDescription){%> <%-item.description%> |<%}%>
16 | <%}%><%}%>
17 |
18 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { babel } from '@rollup/plugin-babel';
2 | import replace from '@rollup/plugin-replace';
3 |
4 | const config = [{
5 | input: 'src/client/index.js',
6 | output: {
7 | file: 'dist/pn.js',
8 | format: 'esm'
9 | },
10 | treeshake: false,
11 | plugins: [
12 | babel({
13 | babelHelpers: 'bundled',
14 | babelrc: false,
15 | comments: false,
16 | compact: false,
17 | minified: false
18 | }),
19 | replace({
20 | preventAssignment: true,
21 | $1: ''
22 | })
23 | ]
24 | }, {
25 | input: 'src/client/network-entities/network-entity.js',
26 | output: {
27 | dir: 'dist',
28 | format: 'esm'
29 | },
30 | treeshake: false,
31 | plugins: [
32 | babel({ babelHelpers: 'bundled' })
33 | ]
34 | }];
35 |
36 | export default config;
37 |
--------------------------------------------------------------------------------
/docs/templates/function-item.ejs:
--------------------------------------------------------------------------------
1 |
2 | ### <%=locals.kind==='constructor'?'new ':''%><%=name%>(<%=locals?.params?.filter((i) => {
3 | return i.name.indexOf('.') === -1;
4 | }).map((i) => {
5 | return i.optional ? `[${i.name}]`: i.name;
6 | }).join(', ')%>)<%=locals.async?' [async]':''%>
7 | <%if(locals.returns){-%>
8 | **Returns:** <%-returns[0].type.names.map((i) => {
9 | return `${i}`;
10 | }).join(' | ')%> <%}%>
11 | <%if(description){%><%-description%>
12 | <%}%>
13 | <%paramsDescription = params.filter((p)=>{return !!p.description;}).length > 0;
14 | %><%if(params.length){-%>| Param | Type |<%if(paramsDescription){%> Description |<%}%>
15 | | --- | --- |<%if(paramsDescription){%> --- |<%}%>
16 | <%for(const item of params){-%>
17 | | <%-item.name%><%=item.optional?' (optional)':''%> | <%-item.type.names.map((i) => {
18 | return `${i}`;
19 | }).join(' | ')%> |<%if(paramsDescription){%> <%-item.description%> |<%}%>
20 | <%}%><%}%>
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 meta.space
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/server/core/server.js:
--------------------------------------------------------------------------------
1 | import * as pc from 'playcanvas';
2 | import WebSocket from 'faye-websocket';
3 |
4 | export default class Server extends pc.EventHandler {
5 | constructor(id, url) {
6 | super();
7 |
8 | this.id = id;
9 | this.url = url;
10 |
11 | this._socket = new WebSocket.Client(`ws://${this.url}`, [], {
12 | headers: { 'User-Agent': 'PlayNetwork' }
13 | });
14 |
15 | this._msgId = 1;
16 | this._callbacks = new Map();
17 |
18 | this.on('_send', (user, data) => {
19 | user._send(data.name, data.data, data.scope.type, data.scope.id);
20 | });
21 | }
22 |
23 | send(name, data, scope, id, userId, callback) {
24 | const msg = {
25 | name,
26 | data,
27 | scope: {
28 | type: scope,
29 | id
30 | },
31 | userId
32 | };
33 |
34 | if (callback) {
35 | msg.id = this._msgId++;
36 | this._callbacks.set(msg.id, callback);
37 | }
38 |
39 | this._socket.send(JSON.stringify(msg));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/server/libs/utils.js:
--------------------------------------------------------------------------------
1 | import sysPath from 'path';
2 | import { BACK_SLASH_RE, DOUBLE_SLASH_RE, SLASH, SLASH_SLASH } from 'chokidar/lib/constants.js';
3 |
4 | const DEFAULT_ROUND_PRECISION = 3;
5 |
6 | export function roundTo(number, digits = DEFAULT_ROUND_PRECISION) {
7 | const pow = Math.pow(10, digits);
8 | return Math.round(number * pow) / pow;
9 | }
10 |
11 | export function guid() {
12 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
13 | const r = Math.random() * 16 | 0;
14 | const v = (c === 'x') ? r : (r & 0x3 | 0x8);
15 | return v.toString(16);
16 | });
17 | }
18 |
19 | export const unifyPath = (path) => toUnix(sysPath.normalize(toUnix(path)));
20 |
21 | // chokidar path unification
22 | const toUnix = (string) => {
23 | let str = string.replace(BACK_SLASH_RE, SLASH);
24 | let prepend = false;
25 | if (str.startsWith(SLASH_SLASH)) {
26 | prepend = true;
27 | }
28 | while (str.match(DOUBLE_SLASH_RE)) {
29 | str = str.replace(DOUBLE_SLASH_RE, SLASH);
30 | }
31 | if (prepend) {
32 | str = SLASH + str;
33 | }
34 | return str;
35 | };
36 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser":"@babel/eslint-parser",
3 | "plugins": ["jest"],
4 | "env": {
5 | "node": true,
6 | "es2021": true,
7 | "jest/globals": true
8 | },
9 | "extends": [
10 | "standard"
11 | ],
12 | "globals": {
13 | "pc": "readonly",
14 | "DEBUG": "readonly"
15 | },
16 | "parserOptions": {
17 | "requireConfigFile": false,
18 | "ecmaVersion": "latest",
19 | "sourceType": "module"
20 | },
21 | "ignorePatterns": ["scripts.js", "ammo.js", "src/client/**/*.js", "dist/**/*.js", "examples/**/components/**/*.js"],
22 | "rules": {
23 | "no-unused-vars": ["error", { "varsIgnorePattern": "_" }],
24 | "semi": ["error", "always"],
25 | "quotes": ["error", "single"],
26 | "indent": ["error", 4, { "SwitchCase": 1 }],
27 | "space-before-function-paren": ["error", { "anonymous": "never", "named": "never", "asyncArrow": "always" }],
28 | "curly": ["off", "all"]
29 | },
30 | "overrides": [
31 | {
32 | "files": ["src/components/**/*"],
33 | "rules": {
34 | "no-var": "off"
35 | }
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/src/server/libs/levels.js:
--------------------------------------------------------------------------------
1 | import pn from './../index.js';
2 |
3 | class Levels {
4 | cache = new Map();
5 | provider = null;
6 |
7 | async initialize(provider) {
8 | this.provider = provider;
9 |
10 | pn.on('_level:save', async (sender, data, callback) => {
11 | await this.save(data.scene, data);
12 | callback();
13 | });
14 | }
15 |
16 | async save(id, level) {
17 | if (!Number.isInteger(id) || isNaN(id) || !isFinite(id)) throw new Error('level id should be an integer');
18 | if (typeof (level) !== 'object') throw new Error('level should be an object');
19 |
20 | const data = JSON.stringify(level, null, 4);
21 | this.cache.set(id, data);
22 | await this.provider.save(id, data);
23 |
24 | console.log(`level ${id}, saved`);
25 | }
26 |
27 | async load(id) {
28 | if (this.cache.has(id)) return JSON.parse(this.cache.get(id));
29 |
30 | const data = await this.provider.load(id);
31 | const level = data.toString();
32 | this.cache.set(id, level);
33 | return JSON.parse(level);
34 | }
35 |
36 | async has(id) {
37 | if (this.cache.has(id)) return true;
38 | return await this.provider.has(id);
39 | }
40 | }
41 |
42 | export default new Levels();
43 |
--------------------------------------------------------------------------------
/src/client/user.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @class User
3 | * @classdesc User object that is created for each {@link User} we know,
4 | * including ourself.
5 | * @extends pc.EventHandler
6 | * @property {number} id Numerical ID of a {@link User}.
7 | * @property {boolean} mine True if {@link User} object is our own.
8 | */
9 |
10 | /**
11 | * @event User#destroy
12 | * @description Fired when {@link User} has been destroyed
13 | * (not known to client anymore).
14 | */
15 |
16 | /**
17 | * @event User#*
18 | * @description Fired when a {@link User} received named network message.
19 | * @param {object|array|string|number|boolean} [data] Message data.
20 | */
21 |
22 | class User extends pc.EventHandler {
23 | constructor(id, mine) {
24 | super();
25 |
26 | this.id = id;
27 | this.mine = mine;
28 | }
29 |
30 | /**
31 | * @method send
32 | * @description Send named message to a server User.
33 | * @param {string} name Name of a message.
34 | * @param {null|object|array|string|number|boolean} [data] JSON friendly message data.
35 | * @param {responseCallback} [callback] Callback that will be fired when response message is received.
36 | */
37 | send(name, data, callback) {
38 | pn._send(name, data, 'user', this.id, callback);
39 | }
40 |
41 | destroy() {
42 | this.fire('destroy');
43 | this.off();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/docs/server/README.md:
--------------------------------------------------------------------------------
1 | # Server
2 |
3 |
4 | ### PlayNetwork
5 | Main interface of PlayNetwork server. This class handles clients connection and communication.
6 |
7 | ### NetworkEntity
8 | NetworkEntity is a [pc.ScriptType], which is attached to a [pc.ScriptComponent] of an [pc.Entity] that needs to be synchronised between server and clients. It has unique ID, optional owner and list of properties to be synchronised. For convenience, [pc.Entity] has additional property: `entity.networkEntity`.
9 |
10 | ### Room
11 | A Room represents own [pc.Application] context, with a list of joined [User]s.
12 |
13 | ### Rooms
14 | Interface with a list of server [Room]s and an interface to create new rooms.
15 |
16 | ### User
17 | User interface which is created for each individual connection and inter-connections to a [PlayNetwork].
18 |
19 | ### Users
20 | Interface of all [User]s currently connected to a server. As well as for handling new user authentication.
21 |
22 |
23 | [pc.ScriptType]: https://developer.playcanvas.com/en/api/pc.ScriptType.html
24 | [pc.ScriptComponent]: https://developer.playcanvas.com/en/api/pc.ScriptComponent.html
25 | [pc.Entity]: https://developer.playcanvas.com/en/api/pc.Entity.html
26 | [pc.Application]: https://developer.playcanvas.com/en/api/pc.Application.html
27 | [User]: ./User.md
28 | [Room]: ./Room.md
29 | [PlayNetwork]: ./PlayNetwork.md
30 |
--------------------------------------------------------------------------------
/docs/templates/class.ejs:
--------------------------------------------------------------------------------
1 | # <%-name%> (<%=scope%>)<%if(locals.extends){%>
2 | extends <%-locals.extends-%>
3 | <%}%>
4 |
5 | <%-description%>
6 |
7 | ---
8 |
9 | # Index
10 | <% if (properties.length) { %>
11 | ### Properties
12 |
13 | <% for(const item of properties) { %><%- include('index-property-item', item) %><% } %><% } -%>
14 |
15 | <% if (locals.events && events.size) {%>### Events
16 |
17 | <% for(const item of events) { %><%- include('index-event-item', item); %><% } %><% } -%>
18 | <% if (constructor) {%>### Constructor
19 | <%- include('index-function-item', constructor) -%><%}-%>
20 |
21 | <% if (functions.size) {%>### Functions
22 |
23 | <% for(const item of functions) { %><%- include('index-function-item', item) %><% } %><% } %>
24 |
25 | ---
26 |
27 | <% if (properties.length) { %>
28 | # Properties
29 |
30 | <% for(const item of properties) { %><%- include('property-item', item) %><% } %><% } %>
31 | <% if (events.size) {%>
32 | # Events
33 |
34 | <% for(const item of events) { %><%- include('event-item', item) %><% } %><% } -%>
35 | <% if (constructor) {-%>
36 | # Constructor
37 |
38 | <%- include('function-item', constructor); %>
39 | <% } -%><% if (functions.size) {-%>
40 | # Functions
41 |
42 | <% for(const item of functions) { %><%- include('function-item', item) %><% } %><% } -%>
43 |
44 | <% if(callbacks.size) { %># Callbacks
45 | <% for(const item of callbacks) { %>
46 | <%- include('callback-item', item); %>
47 | <% } -%><% } -%>
48 |
49 | <% if (links.size) {%><% for(const [className, link] of links) { -%>
50 | [<%=className%>]: <%=link%>
51 | <% } %><% } -%>
--------------------------------------------------------------------------------
/docs/client/README.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 |
4 | ### PlayNetwork
5 | Main interface to connect to a server and interact with networked data.
6 |
7 | ### InterpolateValue
8 | Helper class to interpolate values between states. It has mechanics to smoothen unreliable intervals of state and can interpolate simple values such as `number`, as well as complex: [pc.Vec2], [pc.Vec3], [pc.Vec4], [pc.Quat], [pc.Color].
9 |
10 | ### Levels
11 | Interface that allows to save hierarchy data to a server.
12 |
13 | ### NetworkEntity
14 | NetworkEntity is a [pc.ScriptType], which is attached to a [pc.ScriptComponent] of an [pc.Entity] that needs to be synchronised between server and clients. It has unique ID, optional owner and list of properties to be synchronised. For convenience, [pc.Entity] has additional property: `entity.networkEntity`.
15 |
16 | ### Room
17 | Room to which [User] has joined.
18 |
19 | ### User
20 | User object that is created for each [User] we know, including ourself.
21 |
22 |
23 | [pc.Vec2]: https://developer.playcanvas.com/en/api/pc.Vec2.html
24 | [pc.Vec3]: https://developer.playcanvas.com/en/api/pc.Vec3.html
25 | [pc.Vec4]: https://developer.playcanvas.com/en/api/pc.Vec4.html
26 | [pc.Quat]: https://developer.playcanvas.com/en/api/pc.Quat.html
27 | [pc.Color]: https://developer.playcanvas.com/en/api/pc.Color.html
28 | [pc.ScriptType]: https://developer.playcanvas.com/en/api/pc.ScriptType.html
29 | [pc.ScriptComponent]: https://developer.playcanvas.com/en/api/pc.ScriptComponent.html
30 | [pc.Entity]: https://developer.playcanvas.com/en/api/pc.Entity.html
31 | [User]: ./User.md
32 |
--------------------------------------------------------------------------------
/src/server/libs/assets.js:
--------------------------------------------------------------------------------
1 | import https from 'https';
2 | import fs from 'fs/promises';
3 | import path from 'path';
4 | import { unifyPath } from './utils.js';
5 |
6 | async function _loadAssetById(id, token) {
7 | const options = {
8 | hostname: 'playcanvas.com',
9 | path: `/api/assets/${id}`,
10 | headers: {
11 | Authorization: `Bearer ${token}`
12 | }
13 | };
14 |
15 | return new Promise((resolve) => {
16 | https.get(options, (response) => {
17 | if (response.statusCode !== 200) {
18 | resolve(null);
19 | return;
20 | }
21 |
22 | let result = '';
23 |
24 | response.on('data', function(chunk) {
25 | result += chunk;
26 | });
27 |
28 | response.on('end', function() {
29 | resolve(result);
30 | });
31 | });
32 | });
33 | };
34 |
35 | export async function downloadAsset(saveTo, id, token) {
36 | if (!token) {
37 | console.error('No playcanvas token provided');
38 | return false;
39 | }
40 |
41 | const asset = await _loadAssetById(id, token);
42 | if (!asset) return;
43 |
44 | await fs.mkdir(path.dirname(saveTo), { recursive: true });
45 | await fs.writeFile(saveTo, asset);
46 |
47 | return true;
48 | };
49 |
50 | export async function updateAssets(directory, token) {
51 | if (!token) {
52 | console.error('No playcanvas token provided');
53 | return false;
54 | }
55 |
56 | directory = unifyPath(directory);
57 |
58 | const files = await fs.readdir(directory);
59 |
60 | for (let i = 0; i < files.length; i++) {
61 | const _path = `${directory}${path.sep}${files[i]}`;
62 | const stats = await fs.stat(_path);
63 |
64 | if (stats.isDirectory()) {
65 | await updateAssets(_path, token);
66 | } else if (stats.isFile()) {
67 | const asset = JSON.parse(await fs.readFile(_path));
68 | await downloadAsset(_path, asset.id, token);
69 | }
70 | }
71 |
72 | return true;
73 | };
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 |
84 | # Gatsby files
85 | .cache/
86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
87 | # https://nextjs.org/blog/next-9-1#public-directory-support
88 | # public
89 |
90 | # vuepress build output
91 | .vuepress/dist
92 |
93 | # Serverless directories
94 | .serverless/
95 |
96 | # FuseBox cache
97 | .fusebox/
98 |
99 | # DynamoDB Local files
100 | .dynamodb/
101 |
102 | # TernJS port file
103 | .tern-port
104 |
105 | *.pem
106 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playnetwork",
3 | "version": "0.6.1",
4 | "description": "",
5 | "exports": {
6 | ".": "./src/server/index.js",
7 | "./node": "./src/node/index.js"
8 | },
9 | "type": "module",
10 | "scripts": {
11 | "build": "npx rollup -c",
12 | "docs:server": "node docs.js Server ./docs/server/ ./src/server/index.js ./src/server/core/**/*.js",
13 | "docs:client": "node docs.js Client ./docs/client/ ./src/client/**/*.js",
14 | "docs": "npm run docs:server & npm run docs:client",
15 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --forceExit --runInBand"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/meta-space-org/playnetwork.git"
20 | },
21 | "author": "meta.space",
22 | "license": "ISC",
23 | "bugs": {
24 | "url": "https://github.com/meta-space-org/playcanvas-server-boilerplate/issues"
25 | },
26 | "homepage": "https://github.com/meta-space-org/playcanvas-server-boilerplate#readme",
27 | "dependencies": {
28 | "@playcanvas/canvas-mock": "^1.0.1",
29 | "chokidar": "^3.5.3",
30 | "fast-deep-equal": "^3.1.3",
31 | "faye-websocket": "^0.11.4",
32 | "permessage-deflate": "^0.1.7",
33 | "playcanvas": "^1.61.3",
34 | "redis": "^4.6.5"
35 | },
36 | "devDependencies": {
37 | "@babel/cli": "^7.21.0",
38 | "@babel/core": "^7.21.3",
39 | "@babel/eslint-parser": "^7.21.3",
40 | "@babel/plugin-proposal-class-properties": "^7.18.6",
41 | "@babel/preset-env": "^7.20.2",
42 | "@playcanvas/jsdoc-template": "^1.0.29",
43 | "@rollup/plugin-babel": "^6.0.3",
44 | "@rollup/plugin-replace": "^5.0.2",
45 | "babel-plugin-remove-import-export": "^1.1.1",
46 | "ejs": "^3.1.9",
47 | "eslint": "^8.37.0",
48 | "eslint-config-standard": "^17.0.0",
49 | "eslint-plugin-import": "^2.27.5",
50 | "eslint-plugin-jest": "^27.2.1",
51 | "eslint-plugin-node": "^11.1.0",
52 | "eslint-plugin-promise": "^6.1.1",
53 | "jest": "^29.5.0",
54 | "jest-environment-jsdom": "^29.5.0",
55 | "jsdoc-api": "^8.0.0",
56 | "jsdoc-parse": "^6.2.0"
57 | },
58 | "optionalDependencies": {
59 | "bufferutil": "^4.0.7"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/lifecycle.js:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from '@jest/globals';
2 |
3 | import pn from '../src/server/index.js';
4 |
5 | import { createServer } from './utils/server.js';
6 | import { createClient, connectClient } from './utils/client.js';
7 |
8 | export const initialization = () => {
9 | describe('Initialization', () => {
10 | test('Server create failed', async () => {
11 | await expect(pn.start()).rejects.toThrow();
12 | });
13 |
14 | test('Server created', async () => {
15 | await createServer(8080);
16 | expect(pn.id).toEqual(expect.anything());
17 | });
18 |
19 | test('Client initialized', (done) => {
20 | createClient((client) => {
21 | try {
22 | expect(client).toBeDefined();
23 | expect(client.levels).toBeDefined();
24 | global.client = client;
25 | } finally {
26 | done();
27 | }
28 | });
29 | });
30 |
31 | test('Client connected', (done) => {
32 | connectClient(global.client, 8080, (err, user) => {
33 | try {
34 | expect(err).toBeNull();
35 | expect(user).toEqual(expect.anything());
36 | expect(pn.users._index.has(user.id)).toBeTruthy();
37 | expect(global.client.me).toEqual(expect.anything());
38 | expect(global.client.me.mine).toBeTruthy();
39 | global.serverUser = pn.users._index.get(user.id);
40 | } finally {
41 | done();
42 | }
43 | });
44 | });
45 | });
46 | };
47 |
48 | export const destruction = () => {
49 | describe('Destruction', () => {
50 | test('Client disconnect', (done) => {
51 | pn.users.once('disconnect', (user) => {
52 | try {
53 | expect(user.id).toBe(global.serverUser.id);
54 | expect(pn.users._index.has(user.id)).toBeFalsy();
55 | } finally {
56 | done();
57 | }
58 | });
59 |
60 | global.client.socket.close();
61 | });
62 | });
63 | };
64 |
--------------------------------------------------------------------------------
/src/server/core/users.js:
--------------------------------------------------------------------------------
1 | import * as pc from 'playcanvas';
2 | import pn from './../index.js';
3 | import User from './user.js';
4 |
5 | /**
6 | * @class Users
7 | * @classdesc Interface of all {@link User}s currently connected to a server. As well as for handling new user authentication.
8 | * @extends pc.EventHandler
9 | */
10 |
11 | /**
12 | * @callback authenticateCallback
13 | * @param {Error} [error] {@link Error} object if authentication failed.
14 | * @param {number|string} userId User ID if authentication succeeded.
15 | */
16 |
17 | /**
18 | * @event Users#connect
19 | * @description Fired when new user has been connected.
20 | * @param {User} user
21 | */
22 |
23 | /**
24 | * @event Users#disconnect
25 | * @description Fired when a user has been disconnected.
26 | * @param {User} user
27 | */
28 |
29 | /**
30 | * @event Users#authenticate
31 | * @description Event to handle new connected sockets and authenticate a user. Callback should be called with an error or userId provided.
32 | * @param {object|array|string|number|boolean} payload Payload data sent from a client.
33 | * @param {authenticateCallback} callback Callback that should be called when authentication is finished. By providing userId - authentication considered successfull.
34 | */
35 |
36 | export default class Users extends pc.EventHandler {
37 | _index = new Map();
38 |
39 | add(user) {
40 | this._index.set(user.id, user);
41 |
42 | user.once('destroy', () => {
43 | this._index.delete(user.id);
44 | this.fire('disconnect', user);
45 | });
46 |
47 | this.fire('connect', user);
48 | }
49 |
50 | /**
51 | * @method get
52 | * @description Get {@link User} by ID
53 | * @param {number} id
54 | * @returns {User|null}
55 | */
56 | async get(id) {
57 | if (!this._index.has(id)) {
58 | const serverId = parseInt(await pn.redis.HGET('_route:user', id.toString()));
59 | if (!serverId) return null;
60 |
61 | const user = new User(id, null, serverId);
62 | this._index.set(user.id, user);
63 |
64 | user.once('destroy', () => {
65 | this._index.delete(user.id);
66 | });
67 | }
68 |
69 | return this._index.get(id);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/server/libs/logger.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | let relativePath = '';
4 |
5 | global.console.setRelativePath = (filePath) => {
6 | relativePath = path.dirname(filePath);
7 | };
8 |
9 | function processStack(stack) {
10 | let lines = stack.split('\n');
11 |
12 | lines = lines.filter((line, ind) => {
13 | if (ind === 0) {
14 | // remove first error message
15 | return false;
16 | } else if (line.includes('node:internal')) {
17 | // remove internal errors
18 | return false;
19 | }
20 | return true;
21 | });
22 |
23 | lines = lines.map((line) => {
24 | line = line.slice(7); // remove "at"
25 | if (relativePath && line.includes(relativePath)) {
26 | // make paths relative to project
27 | line = line.replace(relativePath, '');
28 | } if (line.includes('node_modules\\playcanvas\\build\\playcanvas.js:')) {
29 | // replace playcanvas engine path
30 | line = line.replace(/\s\([a-zA-Z:\\_]+playcanvas\.js:/, ' (playcanvas.js:');
31 | }
32 |
33 | // :line:char > :line
34 | line = line.replace(/(:[0-9]+):[0-9]+/, '$1');
35 |
36 | // name (location) > location (name)
37 | line = line.replace(/([a-zA-Z0-9\\._<>]+)\s\(([a-zA-Z\\./]+:[0-9]+)\)/, '$2 ($1)');
38 |
39 | return line;
40 | });
41 |
42 | return lines.join('\n');
43 | };
44 |
45 | const consoleLog = console.log;
46 | const consoleInfo = console.info;
47 | const consoleWarn = console.warn;
48 | const consoleError = console.error;
49 |
50 | global.console.log = function(...args) {
51 | consoleLog.call(console, ...args);
52 | };
53 |
54 | global.console.info = function(...args) {
55 | consoleInfo.call(console, '\x1b[36m%s', ...args, '\x1b[0m');
56 | };
57 |
58 | global.console.warn = function(...args) {
59 | consoleWarn.call(console, '\x1b[33m%s', ...args, '\x1b[0m');
60 | };
61 |
62 | global.console.error = function(...args) {
63 | if (args[0] instanceof Error) {
64 | const err = args[0];
65 | consoleError.call(console, '\x1b[31m%s\x1b[0m', `ERROR: ${err.message}`);
66 | consoleError.call(console, processStack(err.stack));
67 | } else {
68 | consoleError.call(console, ...args);
69 | }
70 | };
71 |
72 | export default console;
73 |
--------------------------------------------------------------------------------
/docs/client/User.md:
--------------------------------------------------------------------------------
1 | # User (client)
2 | extends [pc.EventHandler]
3 |
4 | User object that is created for each [User] we know, including ourself.
5 |
6 | ---
7 |
8 | # Index
9 |
10 | ### Properties
11 |
12 | .id : `number`
13 | .mine : `boolean`
14 |
15 | ### Events
16 |
17 | destroy
18 | * => ([data])
19 |
20 | ### Functions
21 |
22 | send(name, [data], [callback])
23 |
24 |
25 | ---
26 |
27 |
28 | # Properties
29 |
30 |
31 | ### .id : `number`
32 | Numerical ID of a [User].
33 |
34 |
35 | ### .mine : `boolean`
36 | True if [User] object is our own.
37 |
38 |
39 |
40 | # Events
41 |
42 |
43 | ### destroy [event]
44 | Fired when [User] has been destroyed (not known to client anymore).
45 |
46 |
47 |
48 |
49 | ### * [event] => ([data])
50 | Fired when a [User] received named network message.
51 |
52 | | Param | Type | Description |
53 | | --- | --- | --- |
54 | | data | `object` | `array` | `string` | `number` | `boolean` | Message data. |
55 |
56 |
57 | # Functions
58 |
59 |
60 | ### send(name, [data], [callback])
61 |
62 | Send named message to a server User.
63 |
64 | | Param | Type | Description |
65 | | --- | --- | --- |
66 | | name | `string` | Name of a message. |
67 | | data (optional) | `null` | `object` | `array` | `string` | `number` | `boolean` | JSON friendly message data. |
68 | | callback (optional) | responseCallback | Callback that will be fired when response message is received. |
69 |
70 |
71 |
72 | # Callbacks
73 |
74 |
75 | ### responseCallback [callback] => (error, data)
76 |
77 | | Param | Type | Description |
78 | | --- | --- | --- |
79 | | error | ````null```` | ```[Error]``` | Error provided with with a response. |
80 | | data | ````null```` | ````object```` | ````array```` | ````string```` | ````number```` | ````boolean```` | Data provided with a response. |
81 |
82 |
83 |
84 |
85 | [pc.EventHandler]: https://developer.playcanvas.com/en/api/pc.EventHandler.html
86 | [User]: ./User.md
87 |
--------------------------------------------------------------------------------
/src/server/core/network-entities/network-entities.js:
--------------------------------------------------------------------------------
1 | import * as pc from 'playcanvas';
2 |
3 | import pn from './../../index.js';
4 | import entityToData from './entity-parser.js';
5 |
6 | export default class NetworkEntities extends pc.EventHandler {
7 | index = new Map();
8 | entitiesInProcess = 0;
9 | nextEntityId = 0;
10 |
11 | constructor(app) {
12 | super();
13 |
14 | this.app = app;
15 | }
16 |
17 | create(script) {
18 | script.entity.forEach((e) => {
19 | if (!e.networkEntity) return;
20 |
21 | const id = `${pn.id}-${this.nextEntityId}`;
22 | this.nextEntityId++;
23 |
24 | e.networkEntity.id = id;
25 | pn.redis.HSET('_route:networkEntity', id, pn.id);
26 |
27 | this.index.set(id, e);
28 | pn.networkEntities.set(id, e.networkEntity);
29 |
30 | e.networkEntity.once('destroy', () => {
31 | if (!this.index.has(id)) return;
32 |
33 | e.forEach((x) => {
34 | if (!x.networkEntity) return;
35 | this.index.delete(id);
36 | pn.networkEntities.delete(id);
37 | pn.redis.HDEL('_route:networkEntity', id.toString());
38 | });
39 |
40 | this.app.room.send('_networkEntities:delete', id);
41 | });
42 | });
43 |
44 | if (!this.app.frame) return;
45 |
46 | this.app.room.send('_networkEntities:create', { entities: this.toData(script.entity) });
47 | }
48 |
49 | delete(id) {
50 | this.index.delete(id);
51 | pn.networkEntities.delete(id);
52 | }
53 |
54 | get(id) {
55 | return this.index.get(id) || null;
56 | }
57 |
58 | getState(force) {
59 | const state = [];
60 | for (const entity of this.index.values()) {
61 | if (!entity.script || !entity.script.networkEntity)
62 | continue;
63 |
64 | const entityState = entity.script.networkEntity.getState(force);
65 |
66 | if (entityState)
67 | state.push(entityState);
68 | }
69 | return state;
70 | }
71 |
72 | toData(entity) {
73 | const entities = {};
74 |
75 | entity.forEach((e) => {
76 | if (!(e instanceof pc.Entity))
77 | return;
78 |
79 | const entityData = entityToData(e);
80 | entities[entityData.resource_id] = entityData;
81 | });
82 |
83 | return entities;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/docs/server/Users.md:
--------------------------------------------------------------------------------
1 | # Users (server)
2 | extends [pc.EventHandler]
3 |
4 | Interface of all [User]s currently connected to a server. As well as for handling new user authentication.
5 |
6 | ---
7 |
8 | # Index
9 |
10 | ### Events
11 |
12 | connect => (user)
13 | disconnect => (user)
14 | authenticate => (payload, callback)
15 |
16 | ### Functions
17 |
18 | get(id) => [User] | `null`
19 |
20 |
21 | ---
22 |
23 |
24 |
25 | # Events
26 |
27 |
28 | ### connect [event] => (user)
29 | Fired when new user has been connected.
30 |
31 | | Param | Type |
32 | | --- | --- |
33 | | user | [User] |
34 |
35 |
36 |
37 | ### disconnect [event] => (user)
38 | Fired when a user has been disconnected.
39 |
40 | | Param | Type |
41 | | --- | --- |
42 | | user | [User] |
43 |
44 |
45 |
46 | ### authenticate [event] => (payload, callback)
47 | Event to handle new connected sockets and authenticate a user. Callback should be called with an error or userId provided.
48 |
49 | | Param | Type | Description |
50 | | --- | --- | --- |
51 | | payload | `object` | `array` | `string` | `number` | `boolean` | Payload data sent from a client. |
52 | | callback | authenticateCallback | Callback that should be called when authentication is finished. By providing userId - authentication considered successfull. |
53 |
54 |
55 | # Functions
56 |
57 |
58 | ### get(id)
59 |
60 | **Returns:** [User] | `null`
61 | Get [User] by ID
62 |
63 | | Param | Type |
64 | | --- | --- |
65 | | id | `number` |
66 |
67 |
68 |
69 | # Callbacks
70 |
71 |
72 | ### authenticateCallback [callback] => ([error], userId)
73 |
74 | | Param | Type | Description |
75 | | --- | --- | --- |
76 | | error (optional) | [Error] | [Error] object if authentication failed. |
77 | | userId | `number` | `string` | User ID if authentication succeeded. |
78 |
79 |
80 |
81 |
82 | [pc.EventHandler]: https://developer.playcanvas.com/en/api/pc.EventHandler.html
83 | [User]: ./User.md
84 | [Error]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
85 |
--------------------------------------------------------------------------------
/docs/server/Rooms.md:
--------------------------------------------------------------------------------
1 | # Rooms (server)
2 | extends [pc.EventHandler]
3 |
4 | Interface with a list of server [Room]s and an interface to create new rooms.
5 |
6 | ---
7 |
8 | # Index
9 |
10 | ### Events
11 |
12 | join => (room, user)
13 | leave => (room, user)
14 |
15 | ### Functions
16 |
17 | create(levelId, [tickrate]) [async] => [Room]
18 | get(id) => [Room] | `null`
19 | has(id) => `boolean`
20 |
21 |
22 | ---
23 |
24 |
25 |
26 | # Events
27 |
28 |
29 | ### join [event] => (room, user)
30 | Fired when a [User] successfully joined a [Room].
31 |
32 | | Param | Type | Description |
33 | | --- | --- | --- |
34 | | room | [Room] | [Room] to which a [User] joined. |
35 | | user | [User] | [User] who joined. |
36 |
37 |
38 |
39 | ### leave [event] => (room, user)
40 | Fired when a [User] left a [Room].
41 |
42 | | Param | Type | Description |
43 | | --- | --- | --- |
44 | | room | [Room] | [Room] from which a [User] has left. |
45 | | user | [User] | [User] who has left. |
46 |
47 |
48 | # Functions
49 |
50 |
51 | ### create(levelId, [tickrate]) [async]
52 |
53 | **Returns:** [Room]
54 | Function to create a new [Room]. It will load a level by provided ID and start new [Room] with a [pc.Application].
55 |
56 | | Param | Type | Description |
57 | | --- | --- | --- |
58 | | levelId | `number` | ID Number of a level. |
59 | | tickrate (optional) | `number` | Tick rate - is how many times Application will be calling `update` in a second. Defaults to 20 UPS. |
60 |
61 |
62 |
63 | ### get(id)
64 |
65 | **Returns:** [Room] | `null`
66 | Get a [Room] by ID.
67 |
68 | | Param | Type | Description |
69 | | --- | --- | --- |
70 | | id | `number` | ID of a [Room]. |
71 |
72 |
73 |
74 | ### has(id)
75 |
76 | **Returns:** `boolean`
77 | Check if a [Room] with a specific ID exists.
78 |
79 | | Param | Type | Description |
80 | | --- | --- | --- |
81 | | id | `number` | ID of a [Room]. |
82 |
83 |
84 |
85 |
86 | [pc.EventHandler]: https://developer.playcanvas.com/en/api/pc.EventHandler.html
87 | [Room]: ./Room.md
88 | [pc.Application]: https://developer.playcanvas.com/en/api/pc.Application.html
89 | [User]: ./User.md
90 |
--------------------------------------------------------------------------------
/tests/mock/templates/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Box",
3 | "tags": [],
4 | "meta": null,
5 | "data": {
6 | "entities": {
7 | "e1c81cb0-c8f0-465b-b1dc-c984a7dbb07b": {
8 | "name": "Box",
9 | "tags": [],
10 | "enabled": true,
11 | "resource_id": "e1c81cb0-c8f0-465b-b1dc-c984a7dbb07b",
12 | "parent": null,
13 | "children": [],
14 | "position": [0, 0.5, 0],
15 | "rotation": [0, 0, 0],
16 | "scale": [1, 1, 1],
17 | "components": {
18 | "render": {
19 | "enabled": true,
20 | "type": "box",
21 | "asset": null,
22 | "materialAssets": [null],
23 | "layers": [0],
24 | "batchGroupId": null,
25 | "castShadows": true,
26 | "castShadowsLightmap": true,
27 | "receiveShadows": true,
28 | "lightmapped": false,
29 | "lightmapSizeMultiplier": 1,
30 | "castShadowsLightMap": true,
31 | "lightMapped": false,
32 | "lightMapSizeMultiplier": 1,
33 | "isStatic": false,
34 | "rootBone": null
35 | },
36 | "script": {
37 | "enabled": true,
38 | "order": ["networkEntity"],
39 | "scripts": {
40 | "networkEntity": {
41 | "enabled": true,
42 | "attributes": {
43 | "id": 0,
44 | "owner": "",
45 | "properties": []
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | },
54 | "file": null,
55 | "path": [],
56 | "id": 92407117,
57 | "uniqueId": 92407117,
58 | "type": "template",
59 | "source": false,
60 | "source_asset_id": null,
61 | "createdAt": "2022-07-07T13:32:40.500Z",
62 | "scope": {
63 | "type": "project",
64 | "id": 857037
65 | },
66 | "user_id": 4373,
67 | "revision": 1,
68 | "preload": true,
69 | "exclude": false,
70 | "region": "eu-west-1",
71 | "task": null,
72 | "i18n": {}
73 | }
74 |
--------------------------------------------------------------------------------
/docs/client/InterpolateValue.md:
--------------------------------------------------------------------------------
1 | # InterpolateValue (client)
2 |
3 | Helper class to interpolate values between states. It has mechanics to smoothen unreliable intervals of state and can interpolate simple values such as `number`, as well as complex: [pc.Vec2], [pc.Vec3], [pc.Vec4], [pc.Quat], [pc.Color].
4 |
5 | ---
6 |
7 | # Index
8 |
9 | ### Properties
10 |
11 | .value : `number` | [pc.Vec2] | [pc.Vec3] | [pc.Vec4] | [pc.Quat] | [pc.Color]
12 |
13 | ### Constructor
14 | new InterpolateValue(value)
15 |
16 | ### Functions
17 |
18 | set(value)
19 | add(value)
20 | update(dt)
21 |
22 |
23 | ---
24 |
25 |
26 | # Properties
27 |
28 |
29 | ### .value : `number` | [pc.Vec2] | [pc.Vec3] | [pc.Vec4] | [pc.Quat] | [pc.Color]
30 | Current Value, that it interpolated between states on every update.
31 |
32 |
33 | # Constructor
34 |
35 |
36 | ### new InterpolateValue(value)
37 |
38 |
39 | | Param | Type | Description |
40 | | --- | --- | --- |
41 | | value | `number` | [pc.Vec2] | [pc.Vec3] | [pc.Vec4] | [pc.Quat] | [pc.Color] | Value to interpolate. Can be a simple `number`, as well as complex: [pc.Vec2], [pc.Vec3], [pc.Vec4], [pc.Quat], [pc.Color] object with `lerp` or `slerp`, `copy` and `clone` method. |
42 |
43 |
44 |
45 | # Functions
46 |
47 |
48 | ### set(value)
49 |
50 | Force a value set, ignoring an interpolation.
51 |
52 | | Param | Type |
53 | | --- | --- |
54 | | value | `number` | [pc.Vec2] | [pc.Vec3] | [pc.Vec4] | [pc.Quat] | [pc.Color] |
55 |
56 |
57 |
58 | ### add(value)
59 |
60 | Add a value to list of interpolation states.
61 |
62 | | Param | Type |
63 | | --- | --- |
64 | | value | `number` | [pc.Vec2] | [pc.Vec3] | [pc.Vec4] | [pc.Quat] | [pc.Color] |
65 |
66 |
67 |
68 | ### update(dt)
69 |
70 | Call an update, with should be called at the application update interval. This will progress interpolation through states based on Delta Time.
71 |
72 | | Param | Type | Description |
73 | | --- | --- | --- |
74 | | dt | `number` | Delta Time of an application update frequency. |
75 |
76 |
77 |
78 |
79 | [pc.Vec2]: https://developer.playcanvas.com/en/api/pc.Vec2.html
80 | [pc.Vec3]: https://developer.playcanvas.com/en/api/pc.Vec3.html
81 | [pc.Vec4]: https://developer.playcanvas.com/en/api/pc.Vec4.html
82 | [pc.Quat]: https://developer.playcanvas.com/en/api/pc.Quat.html
83 | [pc.Color]: https://developer.playcanvas.com/en/api/pc.Color.html
84 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # API Documentation
2 |
3 | # Server
4 |
5 |
6 | ### PlayNetwork
7 | Main interface of PlayNetwork server. This class handles clients connection and communication.
8 |
9 | ### NetworkEntity
10 | NetworkEntity is a [pc.ScriptType], which is attached to a [pc.ScriptComponent] of an [pc.Entity] that needs to be synchronised between server and clients. It has unique ID, optional owner and list of properties to be synchronised. For convenience, [pc.Entity] has additional property: `entity.networkEntity`.
11 |
12 | ### Room
13 | A Room represents own [pc.Application] context, with a list of joined [User](./server/User.md)s.
14 |
15 | ### Rooms
16 | Interface with a list of server [Room](./server/Room.md)s and an interface to create new rooms.
17 |
18 | ### User
19 | User interface which is created for each individual connection and inter-connections to a [PlayNetwork](./server/PlayNetwork.md).
20 |
21 | ### Users
22 | Interface of all [User](./server/User.md)s currently connected to a server. As well as for handling new user authentication.
23 |
24 |
25 |
26 |
27 | # Client
28 |
29 |
30 | ### PlayNetwork
31 | Main interface to connect to a server and interact with networked data.
32 |
33 | ### InterpolateValue
34 | Helper class to interpolate values between states. It has mechanics to smoothen unreliable intervals of state and can interpolate simple values such as `number`, as well as complex: [pc.Vec2], [pc.Vec3], [pc.Vec4], [pc.Quat], [pc.Color].
35 |
36 | ### Levels
37 | Interface that allows to save hierarchy data to a server.
38 |
39 | ### NetworkEntity
40 | NetworkEntity is a [pc.ScriptType], which is attached to a [pc.ScriptComponent] of an [pc.Entity] that needs to be synchronised between server and clients. It has unique ID, optional owner and list of properties to be synchronised. For convenience, [pc.Entity] has additional property: `entity.networkEntity`.
41 |
42 | ### Room
43 | Room to which [User](./client/User.md) has joined.
44 |
45 | ### User
46 | User object that is created for each [User](./client/User.md) we know, including ourself.
47 |
48 |
49 |
50 | [pc.ScriptType]: https://developer.playcanvas.com/en/api/pc.ScriptType.html
51 | [pc.ScriptComponent]: https://developer.playcanvas.com/en/api/pc.ScriptComponent.html
52 | [pc.Entity]: https://developer.playcanvas.com/en/api/pc.Entity.html
53 | [pc.Application]: https://developer.playcanvas.com/en/api/pc.Application.html
54 | [pc.Vec2]: https://developer.playcanvas.com/en/api/pc.Vec2.html
55 | [pc.Vec3]: https://developer.playcanvas.com/en/api/pc.Vec3.html
56 | [pc.Vec4]: https://developer.playcanvas.com/en/api/pc.Vec4.html
57 | [pc.Quat]: https://developer.playcanvas.com/en/api/pc.Quat.html
58 | [pc.Color]: https://developer.playcanvas.com/en/api/pc.Color.html
59 |
--------------------------------------------------------------------------------
/src/server/core/rooms.js:
--------------------------------------------------------------------------------
1 | import * as pc from 'playcanvas';
2 | import levels from '../libs/levels.js';
3 |
4 | import pn from './../index.js';
5 | import Room from './room.js';
6 |
7 | /**
8 | * @class Rooms
9 | * @classdesc Interface with a list of server {@link Room}s and an interface to create new rooms.
10 | * @extends pc.EventHandler
11 | */
12 |
13 | /**
14 | * @event Rooms#join
15 | * @description Fired when a {@link User} successfully joined a {@link Room}.
16 | * @async
17 | * @param {Room} room {@link Room} to which a {@link User} joined.
18 | * @param {User} user {@link User} who joined.
19 | */
20 |
21 | /**
22 | * @event Rooms#leave
23 | * @description Fired when a {@link User} left a {@link Room}.
24 | * @param {Room} room {@link Room} from which a {@link User} has left.
25 | * @param {User} user {@link User} who has left.
26 | */
27 |
28 | class Rooms extends pc.EventHandler {
29 | _index = new Map();
30 |
31 | initialize() {
32 | pn.on('_room:create', async (sender, data, callback) => {
33 | if (!data?.levelId) return callback(new Error('No levelId provided'));
34 | if (!(await levels.has(data?.levelId))) return callback(new Error('Level not found'));
35 |
36 | const room = await this.create(data.levelId, data.tickrate);
37 | callback(null, room.id);
38 | });
39 |
40 | pn.on('_room:join', async (sender, id, callback) => {
41 | if (!id) return callback(new Error('Room id is required'));
42 | // eslint-disable-next-line n/no-callback-literal
43 | callback(await sender.join(id));
44 | });
45 |
46 | pn.on('_room:leave', async (sender, _, callback) => {
47 | // eslint-disable-next-line n/no-callback-literal
48 | callback(await sender.leave());
49 | });
50 | }
51 |
52 | /**
53 | * @function create
54 | * @description Function to create a new {@link Room}.
55 | * It will load a level by provided ID and start new {@link Room} with a {@link pc.Application}.
56 | * @async
57 | * @param {number} levelId ID Number of a level.
58 | * @param {number} [tickrate=20] Tick rate - is how many times Application
59 | * will be calling `update` in a second. Defaults to 20 UPS.
60 | * @returns {Room} Room that has been created.
61 | */
62 | async create(levelId, tickrate = 20) {
63 | const roomId = await pn.generateId('room');
64 |
65 | const room = new Room(roomId, tickrate);
66 | await room.initialize(levelId);
67 |
68 | this._index.set(room.id, room);
69 |
70 | room.once('destroy', () => {
71 | this._index.delete(room.id);
72 | });
73 |
74 | return room;
75 | }
76 |
77 | /**
78 | * @method get
79 | * @description Get a {@link Room} by ID.
80 | * @param {number} id ID of a {@link Room}.
81 | * @returns {Room|null}
82 | */
83 | get(id) {
84 | return this._index.get(id) || null;
85 | }
86 |
87 | /**
88 | * @method has
89 | * @description Check if a {@link Room} with a specific ID exists.
90 | * @param {number} id ID of a {@link Room}.
91 | * @returns {boolean}
92 | */
93 | has(id) {
94 | return this._index.has(id);
95 | }
96 | }
97 |
98 | export default Rooms;
99 |
--------------------------------------------------------------------------------
/docs/client/NetworkEntity.md:
--------------------------------------------------------------------------------
1 | # NetworkEntity (client)
2 | extends [pc.ScriptType]
3 |
4 | NetworkEntity is a [pc.ScriptType], which is attached to a [pc.ScriptComponent] of an [pc.Entity] that needs to be synchronised between server and clients. It has unique ID, optional owner and list of properties to be synchronised. For convenience, [pc.Entity] has additional property: `entity.networkEntity`.
5 |
6 | ---
7 |
8 | # Index
9 |
10 | ### Properties
11 |
12 | .id : `string`
13 | .mine : `boolean`
14 | .properties : `Array.