Refactoring Woka management (#1810)
* Wrap websockets with HyperExpress * Add endpoints on pusher to resolve wokas * getting textures urls from pusher * Adding OpenAPI documentation for the pusher. The pusher now exposes a "/openapi" endpoint and a "/swagger-ui/" endpoint. * revert FRONT_URL * playerTextures metadata is being loaded via Phaser.Loader * fetch textures every time character or customize scene is open * Heavy changes: refactoring the pusher to always send the textures (and the front to accept them) * Sending character layer details to admin * Cleaning commented code * Fixing regex * Fix woka endpoints on pusher * Change error wording on pusher * Working on integration of the woka-list with the new admin endpoint. * Switching from "name" to "id" in texture object + using zod for woka/list validation * Add position on default woka data * Remove async on pusher option method * Fix woka list url * add options for /register * Fxiing loading the Woka list * Actually returning something in logout-callback * Copying messages to back too * remove customize button if no body parts are available (#1952) * remove customize button if no body parts are available * remove unused position field from PlayerTexturesCollection interface * removed unused label field * fix LocalUser test * little PlayerTextures class refactor * Fixing linting * Fixing missing Openapi packages in prod * Fixing back build Co-authored-by: Hanusiak Piotr <piotr@ltmp.co> Co-authored-by: David Négrier <d.negrier@thecodingmachine.com> * Add returns on pusher endpoints Co-authored-by: Alexis Faizeau <a.faizeau@workadventu.re> Co-authored-by: Hanusiak Piotr <piotr@ltmp.co> Co-authored-by: Piotr Hanusiak <wacneg@gmail.com>
This commit is contained in:
parent
d3862a3afd
commit
6540f15c5b
2
.github/workflows/build-and-deploy.yml
vendored
2
.github/workflows/build-and-deploy.yml
vendored
@ -81,7 +81,7 @@ jobs:
|
|||||||
working-directory: messages
|
working-directory: messages
|
||||||
|
|
||||||
- name: Build proto messages
|
- name: Build proto messages
|
||||||
run: yarn run proto && yarn run copy-to-back
|
run: yarn run proto && yarn run copy-to-back && yarn run json-copy-to-back
|
||||||
working-directory: messages
|
working-directory: messages
|
||||||
|
|
||||||
# docker
|
# docker
|
||||||
|
2
.github/workflows/continuous_integration.yml
vendored
2
.github/workflows/continuous_integration.yml
vendored
@ -155,7 +155,7 @@ jobs:
|
|||||||
working-directory: "messages"
|
working-directory: "messages"
|
||||||
|
|
||||||
- name: "Build proto messages"
|
- name: "Build proto messages"
|
||||||
run: yarn run proto && yarn run copy-to-back
|
run: yarn run proto && yarn run copy-to-back && yarn run json-copy-to-back
|
||||||
working-directory: "messages"
|
working-directory: "messages"
|
||||||
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
|
2
back/src/Messages/JsonMessages/.gitignore
vendored
Normal file
2
back/src/Messages/JsonMessages/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
@ -27,7 +27,7 @@ import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
|||||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||||
import { Admin } from "../Model/Admin";
|
import { Admin } from "../Model/Admin";
|
||||||
import { adminApi } from "../Services/AdminApi";
|
import { adminApi } from "../Services/AdminApi";
|
||||||
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
||||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
import { mapFetcher } from "../Services/MapFetcher";
|
import { mapFetcher } from "../Services/MapFetcher";
|
||||||
import { VariablesManager } from "../Services/VariablesManager";
|
import { VariablesManager } from "../Services/VariablesManager";
|
||||||
@ -35,7 +35,7 @@ import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
|||||||
import { LocalUrlError } from "../Services/LocalUrlError";
|
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||||
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
||||||
import { VariableError } from "../Services/VariableError";
|
import { VariableError } from "../Services/VariableError";
|
||||||
import { isRoomRedirect } from "../Services/AdminApi/RoomRedirect";
|
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||||
|
|
||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
@ -571,8 +571,11 @@ export class GameRoom {
|
|||||||
return {
|
return {
|
||||||
mapUrl,
|
mapUrl,
|
||||||
policy_type: 1,
|
policy_type: 1,
|
||||||
textures: [],
|
|
||||||
tags: [],
|
tags: [],
|
||||||
|
authenticationMandatory: null,
|
||||||
|
roomSlug: null,
|
||||||
|
contactPage: null,
|
||||||
|
group: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData";
|
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
||||||
import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect";
|
import { isRoomRedirect, RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||||
|
|
||||||
class AdminApi {
|
class AdminApi {
|
||||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||||
|
@ -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>;
|
|
@ -103,6 +103,7 @@ export class SocketManager {
|
|||||||
const roomJoinedMessage = new RoomJoinedMessage();
|
const roomJoinedMessage = new RoomJoinedMessage();
|
||||||
roomJoinedMessage.setTagList(joinRoomMessage.getTagList());
|
roomJoinedMessage.setTagList(joinRoomMessage.getTagList());
|
||||||
roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken());
|
roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken());
|
||||||
|
roomJoinedMessage.setCharacterlayerList(joinRoomMessage.getCharacterlayerList());
|
||||||
|
|
||||||
for (const [itemId, item] of room.getItemsState().entries()) {
|
for (const [itemId, item] of room.getItemsState().entries()) {
|
||||||
const itemStateMessage = new ItemStateMessage();
|
const itemStateMessage = new ItemStateMessage();
|
||||||
|
@ -28,6 +28,8 @@ services:
|
|||||||
dockerfile: pusher/Dockerfile
|
dockerfile: pusher/Dockerfile
|
||||||
command: yarn run runprod
|
command: yarn run runprod
|
||||||
volumes: []
|
volumes: []
|
||||||
|
environment:
|
||||||
|
ENABLE_OPENAPI_ENDPOINT: "false"
|
||||||
|
|
||||||
back:
|
back:
|
||||||
image: 'wa-back-e2e'
|
image: 'wa-back-e2e'
|
||||||
|
@ -89,6 +89,7 @@ services:
|
|||||||
OPID_USERNAME_CLAIM: $OPID_USERNAME_CLAIM
|
OPID_USERNAME_CLAIM: $OPID_USERNAME_CLAIM
|
||||||
OPID_LOCALE_CLAIM: $OPID_LOCALE_CLAIM
|
OPID_LOCALE_CLAIM: $OPID_LOCALE_CLAIM
|
||||||
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
|
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
|
||||||
|
ENABLE_OPENAPI_ENDPOINT: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
@ -13,4 +13,11 @@ Check out the [contributing guide](../../CONTRIBUTING.md)
|
|||||||
|
|
||||||
## Front documentation
|
## Front documentation
|
||||||
|
|
||||||
|
- [How to add translations](how-to-translate.md)
|
||||||
- [How to add new functions in the scripting API](contributing-to-scripting-api.md)
|
- [How to add new functions in the scripting API](contributing-to-scripting-api.md)
|
||||||
|
- [About Wokas](wokas.md)
|
||||||
|
|
||||||
|
## Pusher documentation
|
||||||
|
|
||||||
|
The Pusher is exposing its HTTP API as "OpenAPI" endpoint.
|
||||||
|
You can browse this API at `http://pusher.workadventure.localhost/swagger-ui/`.
|
||||||
|
30
docs/dev/wokas.md
Normal file
30
docs/dev/wokas.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# About Wokas
|
||||||
|
|
||||||
|
Wokas are made of a set of layers (for custom wokas), or of only 1 layers (if selected from the first screen)
|
||||||
|
|
||||||
|
Internally, each layer has:
|
||||||
|
|
||||||
|
- a name
|
||||||
|
- a URL
|
||||||
|
|
||||||
|
## Connection to a map
|
||||||
|
|
||||||
|
When a user connects to a map, it sends, as a web-socket parameter, the list of layer **names**.
|
||||||
|
|
||||||
|
The pusher is in charge of converting those layer names into the URLs. This way, a client cannot send any random
|
||||||
|
URL to the pusher.
|
||||||
|
|
||||||
|
When the pusher receives the layer names, it validates these names and sends back the URLs + sends the names+urls to the back.
|
||||||
|
If the layers cannot be validated, the websocket connections sends an error message and closes. The user is sent back to the "choose your Woka" screen.
|
||||||
|
|
||||||
|
## Getting the list of available Wokas
|
||||||
|
|
||||||
|
The pusher can send the list of available Wokas to the user.
|
||||||
|
It can actually query the admin for this list, if needed (= if an admin is configured)
|
||||||
|
|
||||||
|
## In the pusher
|
||||||
|
|
||||||
|
The pusher contains a classes in charge of managing the Wokas:
|
||||||
|
|
||||||
|
- `LocalWokaService`: used when no admin is connected. Returns a hard-coded list of Wokas (stored in `pusher/data/woka.json`).
|
||||||
|
- `AdminWokaService`: used to delegate the list of Wokas to the admin.
|
@ -2,6 +2,7 @@
|
|||||||
import type { Game } from "../../Phaser/Game/Game";
|
import type { Game } from "../../Phaser/Game/Game";
|
||||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
|
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
|
||||||
import LL from "../../i18n/i18n-svelte";
|
import LL from "../../i18n/i18n-svelte";
|
||||||
|
import { customizeAvailableStore } from "../../Stores/SelectCharacterSceneStore";
|
||||||
|
|
||||||
export let game: Game;
|
export let game: Game;
|
||||||
|
|
||||||
@ -40,11 +41,13 @@
|
|||||||
class="selectCharacterSceneFormSubmit nes-btn is-primary"
|
class="selectCharacterSceneFormSubmit nes-btn is-primary"
|
||||||
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
|
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
|
||||||
>
|
>
|
||||||
<button
|
{#if $customizeAvailableStore}
|
||||||
type="submit"
|
<button
|
||||||
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
|
type="submit"
|
||||||
on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
|
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
|
||||||
>
|
on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ class ConnectionManager {
|
|||||||
console.error("Invalid data received from /register route. Data: ", data);
|
console.error("Invalid data received from /register route. Data: ", data);
|
||||||
throw new Error("Invalid data received from /register route.");
|
throw new Error("Invalid data received from /register route.");
|
||||||
}
|
}
|
||||||
this.localUser = new LocalUser(data.userUuid, data.textures, data.email);
|
this.localUser = new LocalUser(data.userUuid, data.email);
|
||||||
this.authToken = data.authToken;
|
this.authToken = data.authToken;
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
localUserStore.setAuthToken(this.authToken);
|
localUserStore.setAuthToken(this.authToken);
|
||||||
@ -218,22 +218,6 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
|
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
|
||||||
|
|
||||||
if (this._currentRoom.textures != undefined && this._currentRoom.textures.length > 0) {
|
|
||||||
//check if texture was changed
|
|
||||||
if (this.localUser.textures.length === 0) {
|
|
||||||
this.localUser.textures = this._currentRoom.textures;
|
|
||||||
} else {
|
|
||||||
this._currentRoom.textures.forEach((newTexture) => {
|
|
||||||
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
|
|
||||||
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.localUser.textures.push(newTexture);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
localUserStore.saveUser(this.localUser);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this._currentRoom == undefined) {
|
if (this._currentRoom == undefined) {
|
||||||
return Promise.reject(new Error("Invalid URL"));
|
return Promise.reject(new Error("Invalid URL"));
|
||||||
@ -259,7 +243,7 @@ class ConnectionManager {
|
|||||||
|
|
||||||
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
||||||
const data = await axiosWithRetry.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
const data = await axiosWithRetry.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
||||||
this.localUser = new LocalUser(data.userUuid, [], data.email);
|
this.localUser = new LocalUser(data.userUuid, data.email);
|
||||||
this.authToken = data.authToken;
|
this.authToken = data.authToken;
|
||||||
if (!isBenchmark) {
|
if (!isBenchmark) {
|
||||||
// In benchmark, we don't have a local storage.
|
// In benchmark, we don't have a local storage.
|
||||||
@ -269,7 +253,7 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public initBenchmark(): void {
|
public initBenchmark(): void {
|
||||||
this.localUser = new LocalUser("", []);
|
this.localUser = new LocalUser("");
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectToRoomSocket(
|
public connectToRoomSocket(
|
||||||
@ -346,16 +330,13 @@ class ConnectionManager {
|
|||||||
throw new Error("No Auth code provided");
|
throw new Error("No Auth code provided");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { authToken, userUuid, textures, email, username, locale } = await Axios.get(
|
const { authToken, userUuid, email, username, locale } = await Axios.get(`${PUSHER_URL}/login-callback`, {
|
||||||
`${PUSHER_URL}/login-callback`,
|
params: { code, nonce, token, playUri: this.currentRoom?.key },
|
||||||
{
|
}).then((res) => {
|
||||||
params: { code, nonce, token, playUri: this.currentRoom?.key },
|
|
||||||
}
|
|
||||||
).then((res) => {
|
|
||||||
return res.data;
|
return res.data;
|
||||||
});
|
});
|
||||||
localUserStore.setAuthToken(authToken);
|
localUserStore.setAuthToken(authToken);
|
||||||
this.localUser = new LocalUser(userUuid, textures, email);
|
this.localUser = new LocalUser(userUuid, email);
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { SignalData } from "simple-peer";
|
import type { SignalData } from "simple-peer";
|
||||||
import type { RoomConnection } from "./RoomConnection";
|
import type { RoomConnection } from "./RoomConnection";
|
||||||
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
||||||
import { PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
|
|
||||||
|
|
||||||
export interface PointInterface {
|
export interface PointInterface {
|
||||||
x: number;
|
x: number;
|
||||||
@ -83,6 +82,7 @@ export interface RoomJoinedMessageInterface {
|
|||||||
//groups: GroupCreatedUpdatedMessageInterface[],
|
//groups: GroupCreatedUpdatedMessageInterface[],
|
||||||
items: { [itemId: number]: unknown };
|
items: { [itemId: number]: unknown };
|
||||||
variables: Map<string, unknown>;
|
variables: Map<string, unknown>;
|
||||||
|
characterLayers: BodyResourceDescriptionInterface[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayGlobalMessageInterface {
|
export interface PlayGlobalMessageInterface {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
|
import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
export type LayerNames = "woka" | "body" | "eyes" | "hair" | "clothes" | "hat" | "accessory";
|
||||||
|
|
||||||
export interface CharacterTexture {
|
export interface CharacterTexture {
|
||||||
id: number;
|
id: string;
|
||||||
level: number;
|
layer: LayerNames;
|
||||||
url: string;
|
url: string;
|
||||||
rights: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
|
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
|
||||||
@ -14,9 +15,11 @@ export function isUserNameValid(value: unknown): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function areCharacterLayersValid(value: string[] | null): boolean {
|
export function areCharacterLayersValid(value: string[] | null): boolean {
|
||||||
if (!value || !value.length) return false;
|
if (!value || !value.length) {
|
||||||
for (let i = 0; i < value.length; i++) {
|
return false;
|
||||||
if (/^\w+$/.exec(value[i]) === null) {
|
}
|
||||||
|
for (const layerName of value) {
|
||||||
|
if (layerName.length === 0 || layerName === " ") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,9 +27,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LocalUser {
|
export class LocalUser {
|
||||||
constructor(
|
constructor(public readonly uuid: string, public email: string | null = null) {}
|
||||||
public readonly uuid: string,
|
|
||||||
public textures: CharacterTexture[],
|
|
||||||
public email: string | null = null
|
|
||||||
) {}
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import { isMapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
|||||||
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||||
|
|
||||||
export class MapDetail {
|
export class MapDetail {
|
||||||
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
|
constructor(public readonly mapUrl: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomRedirect {
|
export interface RoomRedirect {
|
||||||
@ -25,7 +25,6 @@ export class Room {
|
|||||||
private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
|
private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
|
||||||
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
|
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
|
||||||
private _mapUrl: string | undefined;
|
private _mapUrl: string | undefined;
|
||||||
private _textures: CharacterTexture[] | undefined;
|
|
||||||
private instance: string | undefined;
|
private instance: string | undefined;
|
||||||
private readonly _search: URLSearchParams;
|
private readonly _search: URLSearchParams;
|
||||||
private _contactPage: string | undefined;
|
private _contactPage: string | undefined;
|
||||||
@ -118,7 +117,6 @@ export class Room {
|
|||||||
} else if (isMapDetailsData(data)) {
|
} else if (isMapDetailsData(data)) {
|
||||||
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
|
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
|
||||||
this._mapUrl = data.mapUrl;
|
this._mapUrl = data.mapUrl;
|
||||||
this._textures = data.textures;
|
|
||||||
this._group = data.group;
|
this._group = data.group;
|
||||||
this._authenticationMandatory =
|
this._authenticationMandatory =
|
||||||
data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS;
|
data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS;
|
||||||
@ -128,7 +126,7 @@ export class Room {
|
|||||||
this._expireOn = new Date(data.expireOn);
|
this._expireOn = new Date(data.expireOn);
|
||||||
}
|
}
|
||||||
this._canReport = data.canReport ?? false;
|
this._canReport = data.canReport ?? false;
|
||||||
return new MapDetail(data.mapUrl, data.textures);
|
return new MapDetail(data.mapUrl);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format.");
|
throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format.");
|
||||||
}
|
}
|
||||||
@ -205,10 +203,6 @@ export class Room {
|
|||||||
return this.roomUrl.toString();
|
return this.roomUrl.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
get textures(): CharacterTexture[] | undefined {
|
|
||||||
return this._textures;
|
|
||||||
}
|
|
||||||
|
|
||||||
get mapUrl(): string {
|
get mapUrl(): string {
|
||||||
if (!this._mapUrl) {
|
if (!this._mapUrl) {
|
||||||
throw new Error("Map URL not fetched yet");
|
throw new Error("Map URL not fetched yet");
|
||||||
|
@ -20,7 +20,7 @@ import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTe
|
|||||||
import { adminMessagesService } from "./AdminMessagesService";
|
import { adminMessagesService } from "./AdminMessagesService";
|
||||||
import { connectionManager } from "./ConnectionManager";
|
import { connectionManager } from "./ConnectionManager";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { warningContainerStore } from "../Stores/MenuStore";
|
import { menuIconVisiblilityStore, menuVisiblilityStore, warningContainerStore } from "../Stores/MenuStore";
|
||||||
import { followStateStore, followRoleStore, followUsersStore } from "../Stores/FollowStore";
|
import { followStateStore, followRoleStore, followUsersStore } from "../Stores/FollowStore";
|
||||||
import { localUserStore } from "./LocalUserStore";
|
import { localUserStore } from "./LocalUserStore";
|
||||||
import {
|
import {
|
||||||
@ -52,10 +52,14 @@ import {
|
|||||||
PositionMessage_Direction,
|
PositionMessage_Direction,
|
||||||
SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto,
|
SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto,
|
||||||
PingMessage as PingMessageTsProto,
|
PingMessage as PingMessageTsProto,
|
||||||
|
CharacterLayerMessage,
|
||||||
} from "../Messages/ts-proto-generated/messages";
|
} from "../Messages/ts-proto-generated/messages";
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
import { OpenPopupEvent } from "../Api/Events/OpenPopupEvent";
|
import { OpenPopupEvent } from "../Api/Events/OpenPopupEvent";
|
||||||
import { match } from "assert";
|
import { match } from "assert";
|
||||||
|
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
|
||||||
|
import { gameManager } from "../Phaser/Game/GameManager";
|
||||||
|
import { SelectCharacterScene, SelectCharacterSceneName } from "../Phaser/Login/SelectCharacterScene";
|
||||||
|
|
||||||
const manualPingDelay = 20000;
|
const manualPingDelay = 20000;
|
||||||
|
|
||||||
@ -337,11 +341,28 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.tags = roomJoinedMessage.tag;
|
this.tags = roomJoinedMessage.tag;
|
||||||
this._userRoomToken = roomJoinedMessage.userRoomToken;
|
this._userRoomToken = roomJoinedMessage.userRoomToken;
|
||||||
|
|
||||||
|
// If one of the URLs sent to us does not exist, let's go to the Woka selection screen.
|
||||||
|
for (const characterLayer of roomJoinedMessage.characterLayer) {
|
||||||
|
if (!characterLayer.url) {
|
||||||
|
this.goToSelectYourWokaScene();
|
||||||
|
this.closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.closed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characterLayers = roomJoinedMessage.characterLayer.map(
|
||||||
|
this.mapCharacterLayerToBodyResourceDescription.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
this._roomJoinedMessageStream.next({
|
this._roomJoinedMessageStream.next({
|
||||||
connection: this,
|
connection: this,
|
||||||
room: {
|
room: {
|
||||||
items,
|
items,
|
||||||
variables,
|
variables,
|
||||||
|
characterLayers,
|
||||||
} as RoomJoinedMessageInterface,
|
} as RoomJoinedMessageInterface,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -351,6 +372,12 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.closed = true;
|
this.closed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "invalidTextureMessage": {
|
||||||
|
this.goToSelectYourWokaScene();
|
||||||
|
|
||||||
|
this.closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "tokenExpiredMessage": {
|
case "tokenExpiredMessage": {
|
||||||
connectionManager.logout().catch((e) => console.error(e));
|
connectionManager.logout().catch((e) => console.error(e));
|
||||||
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
|
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
|
||||||
@ -591,6 +618,15 @@ export class RoomConnection implements RoomConnection {
|
|||||||
});
|
});
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
private mapCharacterLayerToBodyResourceDescription(
|
||||||
|
characterLayer: CharacterLayerMessage
|
||||||
|
): BodyResourceDescriptionInterface {
|
||||||
|
return {
|
||||||
|
id: characterLayer.name,
|
||||||
|
img: characterLayer.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: move this to protobuf utils
|
// TODO: move this to protobuf utils
|
||||||
private toMessageUserJoined(message: UserJoinedMessageTsProto): MessageUserJoined {
|
private toMessageUserJoined(message: UserJoinedMessageTsProto): MessageUserJoined {
|
||||||
const position = message.position;
|
const position = message.position;
|
||||||
@ -598,12 +634,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
throw new Error("Invalid JOIN_ROOM message");
|
throw new Error("Invalid JOIN_ROOM message");
|
||||||
}
|
}
|
||||||
|
|
||||||
const characterLayers = message.characterLayers.map((characterLayer): BodyResourceDescriptionInterface => {
|
const characterLayers = message.characterLayers.map(this.mapCharacterLayerToBodyResourceDescription.bind(this));
|
||||||
return {
|
|
||||||
name: characterLayer.name,
|
|
||||||
img: characterLayer.url,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const companion = message.companion;
|
const companion = message.companion;
|
||||||
|
|
||||||
@ -863,4 +894,11 @@ export class RoomConnection implements RoomConnection {
|
|||||||
public get userRoomToken(): string | undefined {
|
public get userRoomToken(): string | undefined {
|
||||||
return this._userRoomToken;
|
return this._userRoomToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private goToSelectYourWokaScene(): void {
|
||||||
|
menuVisiblilityStore.set(false);
|
||||||
|
menuIconVisiblilityStore.set(false);
|
||||||
|
selectCharacterSceneVisibleStore.set(true);
|
||||||
|
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import type { GameScene } from "../Game/GameScene";
|
|||||||
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
|
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
|
||||||
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
|
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
|
||||||
import { isSilentStore } from "../../Stores/MediaStore";
|
import { isSilentStore } from "../../Stores/MediaStore";
|
||||||
import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
|
import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager";
|
||||||
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
||||||
import type { PictureStore } from "../../Stores/PictureStore";
|
import type { PictureStore } from "../../Stores/PictureStore";
|
||||||
import { Unsubscriber, Writable, writable } from "svelte/store";
|
import { Unsubscriber, Writable, writable } from "svelte/store";
|
||||||
@ -83,7 +83,16 @@ export abstract class Character extends Container implements OutlineableInterfac
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => {
|
return lazyLoadPlayerCharacterTextures(scene.load, [
|
||||||
|
{
|
||||||
|
id: "color_22",
|
||||||
|
img: "resources/customisation/character_color/character_color21.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "eyes_23",
|
||||||
|
img: "resources/customisation/character_eyes/character_eyes23.png",
|
||||||
|
},
|
||||||
|
]).then((textures) => {
|
||||||
this.addTextures(textures, frame);
|
this.addTextures(textures, frame);
|
||||||
this.invisible = false;
|
this.invisible = false;
|
||||||
this.playAnimation(direction, moving);
|
this.playAnimation(direction, moving);
|
||||||
|
@ -5,446 +5,122 @@ export interface BodyResourceDescriptionListInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BodyResourceDescriptionInterface {
|
export interface BodyResourceDescriptionInterface {
|
||||||
name: string;
|
id: string;
|
||||||
img: string;
|
img: string;
|
||||||
level?: number;
|
level?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {
|
/**
|
||||||
male1: { name: "male1", img: "resources/characters/pipoya/Male 01-1.png" },
|
* Temporary object to map layers to the old "level" concept.
|
||||||
male2: { name: "male2", img: "resources/characters/pipoya/Male 02-2.png" },
|
*/
|
||||||
male3: { name: "male3", img: "resources/characters/pipoya/Male 03-4.png" },
|
export const mapLayerToLevel = {
|
||||||
male4: { name: "male4", img: "resources/characters/pipoya/Male 09-1.png" },
|
woka: -1,
|
||||||
male5: { name: "male5", img: "resources/characters/pipoya/Male 10-3.png" },
|
body: 0,
|
||||||
male6: { name: "male6", img: "resources/characters/pipoya/Male 17-2.png" },
|
eyes: 1,
|
||||||
male7: { name: "male7", img: "resources/characters/pipoya/Male 18-1.png" },
|
hair: 2,
|
||||||
male8: { name: "male8", img: "resources/characters/pipoya/Male 16-4.png" },
|
clothes: 3,
|
||||||
male9: { name: "male9", img: "resources/characters/pipoya/Male 07-2.png" },
|
hat: 4,
|
||||||
male10: { name: "male10", img: "resources/characters/pipoya/Male 05-3.png" },
|
accessory: 5,
|
||||||
male11: { name: "male11", img: "resources/characters/pipoya/Teacher male 02.png" },
|
|
||||||
male12: { name: "male12", img: "resources/characters/pipoya/su4 Student male 12.png" },
|
|
||||||
|
|
||||||
Female1: { name: "Female1", img: "resources/characters/pipoya/Female 01-1.png" },
|
|
||||||
Female2: { name: "Female2", img: "resources/characters/pipoya/Female 02-2.png" },
|
|
||||||
Female3: { name: "Female3", img: "resources/characters/pipoya/Female 03-4.png" },
|
|
||||||
Female4: { name: "Female4", img: "resources/characters/pipoya/Female 09-1.png" },
|
|
||||||
Female5: { name: "Female5", img: "resources/characters/pipoya/Female 10-3.png" },
|
|
||||||
Female6: { name: "Female6", img: "resources/characters/pipoya/Female 17-2.png" },
|
|
||||||
Female7: { name: "Female7", img: "resources/characters/pipoya/Female 18-1.png" },
|
|
||||||
Female8: { name: "Female8", img: "resources/characters/pipoya/Female 16-4.png" },
|
|
||||||
Female9: { name: "Female9", img: "resources/characters/pipoya/Female 07-2.png" },
|
|
||||||
Female10: { name: "Female10", img: "resources/characters/pipoya/Female 05-3.png" },
|
|
||||||
Female11: { name: "Female11", img: "resources/characters/pipoya/Teacher fmale 02.png" },
|
|
||||||
Female12: { name: "Female12", img: "resources/characters/pipoya/su4 Student fmale 12.png" },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COLOR_RESOURCES: BodyResourceDescriptionListInterface = {
|
export enum PlayerTexturesKey {
|
||||||
color_1: { name: "color_1", img: "resources/customisation/character_color/character_color0.png" },
|
Accessory = "accessory",
|
||||||
color_2: { name: "color_2", img: "resources/customisation/character_color/character_color1.png" },
|
Body = "body",
|
||||||
color_3: { name: "color_3", img: "resources/customisation/character_color/character_color2.png" },
|
Clothes = "clothes",
|
||||||
color_4: { name: "color_4", img: "resources/customisation/character_color/character_color3.png" },
|
Eyes = "eyes",
|
||||||
color_5: { name: "color_5", img: "resources/customisation/character_color/character_color4.png" },
|
Hair = "hair",
|
||||||
color_6: { name: "color_6", img: "resources/customisation/character_color/character_color5.png" },
|
Hat = "hat",
|
||||||
color_7: { name: "color_7", img: "resources/customisation/character_color/character_color6.png" },
|
Woka = "woka",
|
||||||
color_8: { name: "color_8", img: "resources/customisation/character_color/character_color7.png" },
|
}
|
||||||
color_9: { name: "color_9", img: "resources/customisation/character_color/character_color8.png" },
|
|
||||||
color_10: { name: "color_10", img: "resources/customisation/character_color/character_color9.png" },
|
|
||||||
color_11: { name: "color_11", img: "resources/customisation/character_color/character_color10.png" },
|
|
||||||
color_12: { name: "color_12", img: "resources/customisation/character_color/character_color11.png" },
|
|
||||||
color_13: { name: "color_13", img: "resources/customisation/character_color/character_color12.png" },
|
|
||||||
color_14: { name: "color_14", img: "resources/customisation/character_color/character_color13.png" },
|
|
||||||
color_15: { name: "color_15", img: "resources/customisation/character_color/character_color14.png" },
|
|
||||||
color_16: { name: "color_16", img: "resources/customisation/character_color/character_color15.png" },
|
|
||||||
color_17: { name: "color_17", img: "resources/customisation/character_color/character_color16.png" },
|
|
||||||
color_18: { name: "color_18", img: "resources/customisation/character_color/character_color17.png" },
|
|
||||||
color_19: { name: "color_19", img: "resources/customisation/character_color/character_color18.png" },
|
|
||||||
color_20: { name: "color_20", img: "resources/customisation/character_color/character_color19.png" },
|
|
||||||
color_21: { name: "color_21", img: "resources/customisation/character_color/character_color20.png" },
|
|
||||||
color_22: { name: "color_22", img: "resources/customisation/character_color/character_color21.png" },
|
|
||||||
color_23: { name: "color_23", img: "resources/customisation/character_color/character_color22.png" },
|
|
||||||
color_24: { name: "color_24", img: "resources/customisation/character_color/character_color23.png" },
|
|
||||||
color_25: { name: "color_25", img: "resources/customisation/character_color/character_color24.png" },
|
|
||||||
color_26: { name: "color_26", img: "resources/customisation/character_color/character_color25.png" },
|
|
||||||
color_27: { name: "color_27", img: "resources/customisation/character_color/character_color26.png" },
|
|
||||||
color_28: { name: "color_28", img: "resources/customisation/character_color/character_color27.png" },
|
|
||||||
color_29: { name: "color_29", img: "resources/customisation/character_color/character_color28.png" },
|
|
||||||
color_30: { name: "color_30", img: "resources/customisation/character_color/character_color29.png" },
|
|
||||||
color_31: { name: "color_31", img: "resources/customisation/character_color/character_color30.png" },
|
|
||||||
color_32: { name: "color_32", img: "resources/customisation/character_color/character_color31.png" },
|
|
||||||
color_33: { name: "color_33", img: "resources/customisation/character_color/character_color32.png" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EYES_RESOURCES: BodyResourceDescriptionListInterface = {
|
type PlayerTexturesMetadata = Record<PlayerTexturesKey, PlayerTexturesCategory>;
|
||||||
eyes_1: { name: "eyes_1", img: "resources/customisation/character_eyes/character_eyes1.png" },
|
|
||||||
eyes_2: { name: "eyes_2", img: "resources/customisation/character_eyes/character_eyes2.png" },
|
|
||||||
eyes_3: { name: "eyes_3", img: "resources/customisation/character_eyes/character_eyes3.png" },
|
|
||||||
eyes_4: { name: "eyes_4", img: "resources/customisation/character_eyes/character_eyes4.png" },
|
|
||||||
eyes_5: { name: "eyes_5", img: "resources/customisation/character_eyes/character_eyes5.png" },
|
|
||||||
eyes_6: { name: "eyes_6", img: "resources/customisation/character_eyes/character_eyes6.png" },
|
|
||||||
eyes_7: { name: "eyes_7", img: "resources/customisation/character_eyes/character_eyes7.png" },
|
|
||||||
eyes_8: { name: "eyes_8", img: "resources/customisation/character_eyes/character_eyes8.png" },
|
|
||||||
eyes_9: { name: "eyes_9", img: "resources/customisation/character_eyes/character_eyes9.png" },
|
|
||||||
eyes_10: { name: "eyes_10", img: "resources/customisation/character_eyes/character_eyes10.png" },
|
|
||||||
eyes_11: { name: "eyes_11", img: "resources/customisation/character_eyes/character_eyes11.png" },
|
|
||||||
eyes_12: { name: "eyes_12", img: "resources/customisation/character_eyes/character_eyes12.png" },
|
|
||||||
eyes_13: { name: "eyes_13", img: "resources/customisation/character_eyes/character_eyes13.png" },
|
|
||||||
eyes_14: { name: "eyes_14", img: "resources/customisation/character_eyes/character_eyes14.png" },
|
|
||||||
eyes_15: { name: "eyes_15", img: "resources/customisation/character_eyes/character_eyes15.png" },
|
|
||||||
eyes_16: { name: "eyes_16", img: "resources/customisation/character_eyes/character_eyes16.png" },
|
|
||||||
eyes_17: { name: "eyes_17", img: "resources/customisation/character_eyes/character_eyes17.png" },
|
|
||||||
eyes_18: { name: "eyes_18", img: "resources/customisation/character_eyes/character_eyes18.png" },
|
|
||||||
eyes_19: { name: "eyes_19", img: "resources/customisation/character_eyes/character_eyes19.png" },
|
|
||||||
eyes_20: { name: "eyes_20", img: "resources/customisation/character_eyes/character_eyes20.png" },
|
|
||||||
eyes_21: { name: "eyes_21", img: "resources/customisation/character_eyes/character_eyes21.png" },
|
|
||||||
eyes_22: { name: "eyes_22", img: "resources/customisation/character_eyes/character_eyes22.png" },
|
|
||||||
eyes_23: { name: "eyes_23", img: "resources/customisation/character_eyes/character_eyes23.png" },
|
|
||||||
eyes_24: { name: "eyes_24", img: "resources/customisation/character_eyes/character_eyes24.png" },
|
|
||||||
eyes_25: { name: "eyes_25", img: "resources/customisation/character_eyes/character_eyes25.png" },
|
|
||||||
eyes_26: { name: "eyes_26", img: "resources/customisation/character_eyes/character_eyes26.png" },
|
|
||||||
eyes_27: { name: "eyes_27", img: "resources/customisation/character_eyes/character_eyes27.png" },
|
|
||||||
eyes_28: { name: "eyes_28", img: "resources/customisation/character_eyes/character_eyes28.png" },
|
|
||||||
eyes_29: { name: "eyes_29", img: "resources/customisation/character_eyes/character_eyes29.png" },
|
|
||||||
eyes_30: { name: "eyes_30", img: "resources/customisation/character_eyes/character_eyes30.png" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HAIR_RESOURCES: BodyResourceDescriptionListInterface = {
|
interface PlayerTexturesCategory {
|
||||||
hair_1: { name: "hair_1", img: "resources/customisation/character_hairs/character_hairs0.png" },
|
collections: PlayerTexturesCollection[];
|
||||||
hair_2: { name: "hair_2", img: "resources/customisation/character_hairs/character_hairs1.png" },
|
required?: boolean;
|
||||||
hair_3: { name: "hair_3", img: "resources/customisation/character_hairs/character_hairs2.png" },
|
}
|
||||||
hair_4: { name: "hair_4", img: "resources/customisation/character_hairs/character_hairs3.png" },
|
|
||||||
hair_5: { name: "hair_5", img: "resources/customisation/character_hairs/character_hairs4.png" },
|
|
||||||
hair_6: { name: "hair_6", img: "resources/customisation/character_hairs/character_hairs5.png" },
|
|
||||||
hair_7: { name: "hair_7", img: "resources/customisation/character_hairs/character_hairs6.png" },
|
|
||||||
hair_8: { name: "hair_8", img: "resources/customisation/character_hairs/character_hairs7.png" },
|
|
||||||
hair_9: { name: "hair_9", img: "resources/customisation/character_hairs/character_hairs8.png" },
|
|
||||||
hair_10: { name: "hair_10", img: "resources/customisation/character_hairs/character_hairs9.png" },
|
|
||||||
hair_11: { name: "hair_11", img: "resources/customisation/character_hairs/character_hairs10.png" },
|
|
||||||
hair_12: { name: "hair_12", img: "resources/customisation/character_hairs/character_hairs11.png" },
|
|
||||||
hair_13: { name: "hair_13", img: "resources/customisation/character_hairs/character_hairs12.png" },
|
|
||||||
hair_14: { name: "hair_14", img: "resources/customisation/character_hairs/character_hairs13.png" },
|
|
||||||
hair_15: { name: "hair_15", img: "resources/customisation/character_hairs/character_hairs14.png" },
|
|
||||||
hair_16: { name: "hair_16", img: "resources/customisation/character_hairs/character_hairs15.png" },
|
|
||||||
hair_17: { name: "hair_17", img: "resources/customisation/character_hairs/character_hairs16.png" },
|
|
||||||
hair_18: { name: "hair_18", img: "resources/customisation/character_hairs/character_hairs17.png" },
|
|
||||||
hair_19: { name: "hair_19", img: "resources/customisation/character_hairs/character_hairs18.png" },
|
|
||||||
hair_20: { name: "hair_20", img: "resources/customisation/character_hairs/character_hairs19.png" },
|
|
||||||
hair_21: { name: "hair_21", img: "resources/customisation/character_hairs/character_hairs20.png" },
|
|
||||||
hair_22: { name: "hair_22", img: "resources/customisation/character_hairs/character_hairs21.png" },
|
|
||||||
hair_23: { name: "hair_23", img: "resources/customisation/character_hairs/character_hairs22.png" },
|
|
||||||
hair_24: { name: "hair_24", img: "resources/customisation/character_hairs/character_hairs23.png" },
|
|
||||||
hair_25: { name: "hair_25", img: "resources/customisation/character_hairs/character_hairs24.png" },
|
|
||||||
hair_26: { name: "hair_26", img: "resources/customisation/character_hairs/character_hairs25.png" },
|
|
||||||
hair_27: { name: "hair_27", img: "resources/customisation/character_hairs/character_hairs26.png" },
|
|
||||||
hair_28: { name: "hair_28", img: "resources/customisation/character_hairs/character_hairs27.png" },
|
|
||||||
hair_29: { name: "hair_29", img: "resources/customisation/character_hairs/character_hairs28.png" },
|
|
||||||
hair_30: { name: "hair_30", img: "resources/customisation/character_hairs/character_hairs29.png" },
|
|
||||||
hair_31: { name: "hair_31", img: "resources/customisation/character_hairs/character_hairs30.png" },
|
|
||||||
hair_32: { name: "hair_32", img: "resources/customisation/character_hairs/character_hairs31.png" },
|
|
||||||
hair_33: { name: "hair_33", img: "resources/customisation/character_hairs/character_hairs32.png" },
|
|
||||||
hair_34: { name: "hair_34", img: "resources/customisation/character_hairs/character_hairs33.png" },
|
|
||||||
hair_35: { name: "hair_35", img: "resources/customisation/character_hairs/character_hairs34.png" },
|
|
||||||
hair_36: { name: "hair_36", img: "resources/customisation/character_hairs/character_hairs35.png" },
|
|
||||||
hair_37: { name: "hair_37", img: "resources/customisation/character_hairs/character_hairs36.png" },
|
|
||||||
hair_38: { name: "hair_38", img: "resources/customisation/character_hairs/character_hairs37.png" },
|
|
||||||
hair_39: { name: "hair_39", img: "resources/customisation/character_hairs/character_hairs38.png" },
|
|
||||||
hair_40: { name: "hair_40", img: "resources/customisation/character_hairs/character_hairs39.png" },
|
|
||||||
hair_41: { name: "hair_41", img: "resources/customisation/character_hairs/character_hairs40.png" },
|
|
||||||
hair_42: { name: "hair_42", img: "resources/customisation/character_hairs/character_hairs41.png" },
|
|
||||||
hair_43: { name: "hair_43", img: "resources/customisation/character_hairs/character_hairs42.png" },
|
|
||||||
hair_44: { name: "hair_44", img: "resources/customisation/character_hairs/character_hairs43.png" },
|
|
||||||
hair_45: { name: "hair_45", img: "resources/customisation/character_hairs/character_hairs44.png" },
|
|
||||||
hair_46: { name: "hair_46", img: "resources/customisation/character_hairs/character_hairs45.png" },
|
|
||||||
hair_47: { name: "hair_47", img: "resources/customisation/character_hairs/character_hairs46.png" },
|
|
||||||
hair_48: { name: "hair_48", img: "resources/customisation/character_hairs/character_hairs47.png" },
|
|
||||||
hair_49: { name: "hair_49", img: "resources/customisation/character_hairs/character_hairs48.png" },
|
|
||||||
hair_50: { name: "hair_50", img: "resources/customisation/character_hairs/character_hairs49.png" },
|
|
||||||
hair_51: { name: "hair_51", img: "resources/customisation/character_hairs/character_hairs50.png" },
|
|
||||||
hair_52: { name: "hair_52", img: "resources/customisation/character_hairs/character_hairs51.png" },
|
|
||||||
hair_53: { name: "hair_53", img: "resources/customisation/character_hairs/character_hairs52.png" },
|
|
||||||
hair_54: { name: "hair_54", img: "resources/customisation/character_hairs/character_hairs53.png" },
|
|
||||||
hair_55: { name: "hair_55", img: "resources/customisation/character_hairs/character_hairs54.png" },
|
|
||||||
hair_56: { name: "hair_56", img: "resources/customisation/character_hairs/character_hairs55.png" },
|
|
||||||
hair_57: { name: "hair_57", img: "resources/customisation/character_hairs/character_hairs56.png" },
|
|
||||||
hair_58: { name: "hair_58", img: "resources/customisation/character_hairs/character_hairs57.png" },
|
|
||||||
hair_59: { name: "hair_59", img: "resources/customisation/character_hairs/character_hairs58.png" },
|
|
||||||
hair_60: { name: "hair_60", img: "resources/customisation/character_hairs/character_hairs59.png" },
|
|
||||||
hair_61: { name: "hair_61", img: "resources/customisation/character_hairs/character_hairs60.png" },
|
|
||||||
hair_62: { name: "hair_62", img: "resources/customisation/character_hairs/character_hairs61.png" },
|
|
||||||
hair_63: { name: "hair_63", img: "resources/customisation/character_hairs/character_hairs62.png" },
|
|
||||||
hair_64: { name: "hair_64", img: "resources/customisation/character_hairs/character_hairs63.png" },
|
|
||||||
hair_65: { name: "hair_65", img: "resources/customisation/character_hairs/character_hairs64.png" },
|
|
||||||
hair_66: { name: "hair_66", img: "resources/customisation/character_hairs/character_hairs65.png" },
|
|
||||||
hair_67: { name: "hair_67", img: "resources/customisation/character_hairs/character_hairs66.png" },
|
|
||||||
hair_68: { name: "hair_68", img: "resources/customisation/character_hairs/character_hairs67.png" },
|
|
||||||
hair_69: { name: "hair_69", img: "resources/customisation/character_hairs/character_hairs68.png" },
|
|
||||||
hair_70: { name: "hair_70", img: "resources/customisation/character_hairs/character_hairs69.png" },
|
|
||||||
hair_71: { name: "hair_71", img: "resources/customisation/character_hairs/character_hairs70.png" },
|
|
||||||
hair_72: { name: "hair_72", img: "resources/customisation/character_hairs/character_hairs71.png" },
|
|
||||||
hair_73: { name: "hair_73", img: "resources/customisation/character_hairs/character_hairs72.png" },
|
|
||||||
hair_74: { name: "hair_74", img: "resources/customisation/character_hairs/character_hairs73.png" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = {
|
interface PlayerTexturesCollection {
|
||||||
clothes_1: { name: "clothes_1", img: "resources/customisation/character_clothes/character_clothes0.png" },
|
name: string;
|
||||||
clothes_2: { name: "clothes_2", img: "resources/customisation/character_clothes/character_clothes1.png" },
|
textures: PlayerTexturesRecord[];
|
||||||
clothes_3: { name: "clothes_3", img: "resources/customisation/character_clothes/character_clothes2.png" },
|
}
|
||||||
clothes_4: { name: "clothes_4", img: "resources/customisation/character_clothes/character_clothes3.png" },
|
|
||||||
clothes_5: { name: "clothes_5", img: "resources/customisation/character_clothes/character_clothes4.png" },
|
|
||||||
clothes_6: { name: "clothes_6", img: "resources/customisation/character_clothes/character_clothes5.png" },
|
|
||||||
clothes_7: { name: "clothes_7", img: "resources/customisation/character_clothes/character_clothes6.png" },
|
|
||||||
clothes_8: { name: "clothes_8", img: "resources/customisation/character_clothes/character_clothes7.png" },
|
|
||||||
clothes_9: { name: "clothes_9", img: "resources/customisation/character_clothes/character_clothes8.png" },
|
|
||||||
clothes_10: { name: "clothes_10", img: "resources/customisation/character_clothes/character_clothes9.png" },
|
|
||||||
clothes_11: { name: "clothes_11", img: "resources/customisation/character_clothes/character_clothes10.png" },
|
|
||||||
clothes_12: { name: "clothes_12", img: "resources/customisation/character_clothes/character_clothes11.png" },
|
|
||||||
clothes_13: { name: "clothes_13", img: "resources/customisation/character_clothes/character_clothes12.png" },
|
|
||||||
clothes_14: { name: "clothes_14", img: "resources/customisation/character_clothes/character_clothes13.png" },
|
|
||||||
clothes_15: { name: "clothes_15", img: "resources/customisation/character_clothes/character_clothes14.png" },
|
|
||||||
clothes_16: { name: "clothes_16", img: "resources/customisation/character_clothes/character_clothes15.png" },
|
|
||||||
clothes_17: { name: "clothes_17", img: "resources/customisation/character_clothes/character_clothes16.png" },
|
|
||||||
clothes_18: { name: "clothes_18", img: "resources/customisation/character_clothes/character_clothes17.png" },
|
|
||||||
clothes_19: { name: "clothes_19", img: "resources/customisation/character_clothes/character_clothes18.png" },
|
|
||||||
clothes_20: { name: "clothes_20", img: "resources/customisation/character_clothes/character_clothes19.png" },
|
|
||||||
clothes_21: { name: "clothes_21", img: "resources/customisation/character_clothes/character_clothes20.png" },
|
|
||||||
clothes_22: { name: "clothes_22", img: "resources/customisation/character_clothes/character_clothes21.png" },
|
|
||||||
clothes_23: { name: "clothes_23", img: "resources/customisation/character_clothes/character_clothes22.png" },
|
|
||||||
clothes_24: { name: "clothes_24", img: "resources/customisation/character_clothes/character_clothes23.png" },
|
|
||||||
clothes_25: { name: "clothes_25", img: "resources/customisation/character_clothes/character_clothes24.png" },
|
|
||||||
clothes_26: { name: "clothes_26", img: "resources/customisation/character_clothes/character_clothes25.png" },
|
|
||||||
clothes_27: { name: "clothes_27", img: "resources/customisation/character_clothes/character_clothes26.png" },
|
|
||||||
clothes_28: { name: "clothes_28", img: "resources/customisation/character_clothes/character_clothes27.png" },
|
|
||||||
clothes_29: { name: "clothes_29", img: "resources/customisation/character_clothes/character_clothes28.png" },
|
|
||||||
clothes_30: { name: "clothes_30", img: "resources/customisation/character_clothes/character_clothes29.png" },
|
|
||||||
clothes_31: { name: "clothes_31", img: "resources/customisation/character_clothes/character_clothes30.png" },
|
|
||||||
clothes_32: { name: "clothes_32", img: "resources/customisation/character_clothes/character_clothes31.png" },
|
|
||||||
clothes_33: { name: "clothes_33", img: "resources/customisation/character_clothes/character_clothes32.png" },
|
|
||||||
clothes_34: { name: "clothes_34", img: "resources/customisation/character_clothes/character_clothes33.png" },
|
|
||||||
clothes_35: { name: "clothes_35", img: "resources/customisation/character_clothes/character_clothes34.png" },
|
|
||||||
clothes_36: { name: "clothes_36", img: "resources/customisation/character_clothes/character_clothes35.png" },
|
|
||||||
clothes_37: { name: "clothes_37", img: "resources/customisation/character_clothes/character_clothes36.png" },
|
|
||||||
clothes_38: { name: "clothes_38", img: "resources/customisation/character_clothes/character_clothes37.png" },
|
|
||||||
clothes_39: { name: "clothes_39", img: "resources/customisation/character_clothes/character_clothes38.png" },
|
|
||||||
clothes_40: { name: "clothes_40", img: "resources/customisation/character_clothes/character_clothes39.png" },
|
|
||||||
clothes_41: { name: "clothes_41", img: "resources/customisation/character_clothes/character_clothes40.png" },
|
|
||||||
clothes_42: { name: "clothes_42", img: "resources/customisation/character_clothes/character_clothes41.png" },
|
|
||||||
clothes_43: { name: "clothes_43", img: "resources/customisation/character_clothes/character_clothes42.png" },
|
|
||||||
clothes_44: { name: "clothes_44", img: "resources/customisation/character_clothes/character_clothes43.png" },
|
|
||||||
clothes_45: { name: "clothes_45", img: "resources/customisation/character_clothes/character_clothes44.png" },
|
|
||||||
clothes_46: { name: "clothes_46", img: "resources/customisation/character_clothes/character_clothes45.png" },
|
|
||||||
clothes_47: { name: "clothes_47", img: "resources/customisation/character_clothes/character_clothes46.png" },
|
|
||||||
clothes_48: { name: "clothes_48", img: "resources/customisation/character_clothes/character_clothes47.png" },
|
|
||||||
clothes_49: { name: "clothes_49", img: "resources/customisation/character_clothes/character_clothes48.png" },
|
|
||||||
clothes_50: { name: "clothes_50", img: "resources/customisation/character_clothes/character_clothes49.png" },
|
|
||||||
clothes_51: { name: "clothes_51", img: "resources/customisation/character_clothes/character_clothes50.png" },
|
|
||||||
clothes_52: { name: "clothes_52", img: "resources/customisation/character_clothes/character_clothes51.png" },
|
|
||||||
clothes_53: { name: "clothes_53", img: "resources/customisation/character_clothes/character_clothes52.png" },
|
|
||||||
clothes_54: { name: "clothes_54", img: "resources/customisation/character_clothes/character_clothes53.png" },
|
|
||||||
clothes_55: { name: "clothes_55", img: "resources/customisation/character_clothes/character_clothes54.png" },
|
|
||||||
clothes_56: { name: "clothes_56", img: "resources/customisation/character_clothes/character_clothes55.png" },
|
|
||||||
clothes_57: { name: "clothes_57", img: "resources/customisation/character_clothes/character_clothes56.png" },
|
|
||||||
clothes_58: { name: "clothes_58", img: "resources/customisation/character_clothes/character_clothes57.png" },
|
|
||||||
clothes_59: { name: "clothes_59", img: "resources/customisation/character_clothes/character_clothes58.png" },
|
|
||||||
clothes_60: { name: "clothes_60", img: "resources/customisation/character_clothes/character_clothes59.png" },
|
|
||||||
clothes_61: { name: "clothes_61", img: "resources/customisation/character_clothes/character_clothes60.png" },
|
|
||||||
clothes_62: { name: "clothes_62", img: "resources/customisation/character_clothes/character_clothes61.png" },
|
|
||||||
clothes_63: { name: "clothes_63", img: "resources/customisation/character_clothes/character_clothes62.png" },
|
|
||||||
clothes_64: { name: "clothes_64", img: "resources/customisation/character_clothes/character_clothes63.png" },
|
|
||||||
clothes_65: { name: "clothes_65", img: "resources/customisation/character_clothes/character_clothes64.png" },
|
|
||||||
clothes_66: { name: "clothes_66", img: "resources/customisation/character_clothes/character_clothes65.png" },
|
|
||||||
clothes_67: { name: "clothes_67", img: "resources/customisation/character_clothes/character_clothes66.png" },
|
|
||||||
clothes_68: { name: "clothes_68", img: "resources/customisation/character_clothes/character_clothes67.png" },
|
|
||||||
clothes_69: { name: "clothes_69", img: "resources/customisation/character_clothes/character_clothes68.png" },
|
|
||||||
clothes_70: { name: "clothes_70", img: "resources/customisation/character_clothes/character_clothes69.png" },
|
|
||||||
clothes_pride_shirt: {
|
|
||||||
name: "clothes_pride_shirt",
|
|
||||||
img: "resources/customisation/character_clothes/pride_shirt.png",
|
|
||||||
},
|
|
||||||
clothes_black_hoodie: {
|
|
||||||
name: "clothes_black_hoodie",
|
|
||||||
img: "resources/customisation/character_clothes/black_hoodie.png",
|
|
||||||
},
|
|
||||||
clothes_white_hoodie: {
|
|
||||||
name: "clothes_white_hoodie",
|
|
||||||
img: "resources/customisation/character_clothes/white_hoodie.png",
|
|
||||||
},
|
|
||||||
clothes_engelbert: { name: "clothes_engelbert", img: "resources/customisation/character_clothes/engelbert.png" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HATS_RESOURCES: BodyResourceDescriptionListInterface = {
|
interface PlayerTexturesRecord {
|
||||||
hats_1: { name: "hats_1", img: "resources/customisation/character_hats/character_hats1.png" },
|
id: string;
|
||||||
hats_2: { name: "hats_2", img: "resources/customisation/character_hats/character_hats2.png" },
|
name: string;
|
||||||
hats_3: { name: "hats_3", img: "resources/customisation/character_hats/character_hats3.png" },
|
url: string;
|
||||||
hats_4: { name: "hats_4", img: "resources/customisation/character_hats/character_hats4.png" },
|
}
|
||||||
hats_5: { name: "hats_5", img: "resources/customisation/character_hats/character_hats5.png" },
|
|
||||||
hats_6: { name: "hats_6", img: "resources/customisation/character_hats/character_hats6.png" },
|
|
||||||
hats_7: { name: "hats_7", img: "resources/customisation/character_hats/character_hats7.png" },
|
|
||||||
hats_8: { name: "hats_8", img: "resources/customisation/character_hats/character_hats8.png" },
|
|
||||||
hats_9: { name: "hats_9", img: "resources/customisation/character_hats/character_hats9.png" },
|
|
||||||
hats_10: { name: "hats_10", img: "resources/customisation/character_hats/character_hats10.png" },
|
|
||||||
hats_11: { name: "hats_11", img: "resources/customisation/character_hats/character_hats11.png" },
|
|
||||||
hats_12: { name: "hats_12", img: "resources/customisation/character_hats/character_hats12.png" },
|
|
||||||
hats_13: { name: "hats_13", img: "resources/customisation/character_hats/character_hats13.png" },
|
|
||||||
hats_14: { name: "hats_14", img: "resources/customisation/character_hats/character_hats14.png" },
|
|
||||||
hats_15: { name: "hats_15", img: "resources/customisation/character_hats/character_hats15.png" },
|
|
||||||
hats_16: { name: "hats_16", img: "resources/customisation/character_hats/character_hats16.png" },
|
|
||||||
hats_17: { name: "hats_17", img: "resources/customisation/character_hats/character_hats17.png" },
|
|
||||||
hats_18: { name: "hats_18", img: "resources/customisation/character_hats/character_hats18.png" },
|
|
||||||
hats_19: { name: "hats_19", img: "resources/customisation/character_hats/character_hats19.png" },
|
|
||||||
hats_20: { name: "hats_20", img: "resources/customisation/character_hats/character_hats20.png" },
|
|
||||||
hats_21: { name: "hats_21", img: "resources/customisation/character_hats/character_hats21.png" },
|
|
||||||
hats_22: { name: "hats_22", img: "resources/customisation/character_hats/character_hats22.png" },
|
|
||||||
hats_23: { name: "hats_23", img: "resources/customisation/character_hats/character_hats23.png" },
|
|
||||||
hats_24: { name: "hats_24", img: "resources/customisation/character_hats/character_hats24.png" },
|
|
||||||
hats_25: { name: "hats_25", img: "resources/customisation/character_hats/character_hats25.png" },
|
|
||||||
hats_26: { name: "hats_26", img: "resources/customisation/character_hats/character_hats26.png" },
|
|
||||||
tinfoil_hat1: { name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.png" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {
|
export class PlayerTextures {
|
||||||
accessory_1: {
|
private PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {};
|
||||||
name: "accessory_1",
|
private COLOR_RESOURCES: BodyResourceDescriptionListInterface = {};
|
||||||
img: "resources/customisation/character_accessories/character_accessories1.png",
|
private EYES_RESOURCES: BodyResourceDescriptionListInterface = {};
|
||||||
},
|
private HAIR_RESOURCES: BodyResourceDescriptionListInterface = {};
|
||||||
accessory_2: {
|
private CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = {};
|
||||||
name: "accessory_2",
|
private HATS_RESOURCES: BodyResourceDescriptionListInterface = {};
|
||||||
img: "resources/customisation/character_accessories/character_accessories2.png",
|
private ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {};
|
||||||
},
|
private LAYERS: BodyResourceDescriptionListInterface[] = [];
|
||||||
accessory_3: {
|
|
||||||
name: "accessory_3",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories3.png",
|
|
||||||
},
|
|
||||||
accessory_4: {
|
|
||||||
name: "accessory_4",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories4.png",
|
|
||||||
},
|
|
||||||
accessory_5: {
|
|
||||||
name: "accessory_5",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories5.png",
|
|
||||||
},
|
|
||||||
accessory_6: {
|
|
||||||
name: "accessory_6",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories6.png",
|
|
||||||
},
|
|
||||||
accessory_7: {
|
|
||||||
name: "accessory_7",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories7.png",
|
|
||||||
},
|
|
||||||
accessory_8: {
|
|
||||||
name: "accessory_8",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories8.png",
|
|
||||||
},
|
|
||||||
accessory_9: {
|
|
||||||
name: "accessory_9",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories9.png",
|
|
||||||
},
|
|
||||||
accessory_10: {
|
|
||||||
name: "accessory_10",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories10.png",
|
|
||||||
},
|
|
||||||
accessory_11: {
|
|
||||||
name: "accessory_11",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories11.png",
|
|
||||||
},
|
|
||||||
accessory_12: {
|
|
||||||
name: "accessory_12",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories12.png",
|
|
||||||
},
|
|
||||||
accessory_13: {
|
|
||||||
name: "accessory_13",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories13.png",
|
|
||||||
},
|
|
||||||
accessory_14: {
|
|
||||||
name: "accessory_14",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories14.png",
|
|
||||||
},
|
|
||||||
accessory_15: {
|
|
||||||
name: "accessory_15",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories15.png",
|
|
||||||
},
|
|
||||||
accessory_16: {
|
|
||||||
name: "accessory_16",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories16.png",
|
|
||||||
},
|
|
||||||
accessory_17: {
|
|
||||||
name: "accessory_17",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories17.png",
|
|
||||||
},
|
|
||||||
accessory_18: {
|
|
||||||
name: "accessory_18",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories18.png",
|
|
||||||
},
|
|
||||||
accessory_19: {
|
|
||||||
name: "accessory_19",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories19.png",
|
|
||||||
},
|
|
||||||
accessory_20: {
|
|
||||||
name: "accessory_20",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories20.png",
|
|
||||||
},
|
|
||||||
accessory_21: {
|
|
||||||
name: "accessory_21",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories21.png",
|
|
||||||
},
|
|
||||||
accessory_22: {
|
|
||||||
name: "accessory_22",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories22.png",
|
|
||||||
},
|
|
||||||
accessory_23: {
|
|
||||||
name: "accessory_23",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories23.png",
|
|
||||||
},
|
|
||||||
accessory_24: {
|
|
||||||
name: "accessory_24",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories24.png",
|
|
||||||
},
|
|
||||||
accessory_25: {
|
|
||||||
name: "accessory_25",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories25.png",
|
|
||||||
},
|
|
||||||
accessory_26: {
|
|
||||||
name: "accessory_26",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories26.png",
|
|
||||||
},
|
|
||||||
accessory_27: {
|
|
||||||
name: "accessory_27",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories27.png",
|
|
||||||
},
|
|
||||||
accessory_28: {
|
|
||||||
name: "accessory_28",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories28.png",
|
|
||||||
},
|
|
||||||
accessory_29: {
|
|
||||||
name: "accessory_29",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories29.png",
|
|
||||||
},
|
|
||||||
accessory_30: {
|
|
||||||
name: "accessory_30",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories30.png",
|
|
||||||
},
|
|
||||||
accessory_31: {
|
|
||||||
name: "accessory_31",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories31.png",
|
|
||||||
},
|
|
||||||
accessory_32: {
|
|
||||||
name: "accessory_32",
|
|
||||||
img: "resources/customisation/character_accessories/character_accessories32.png",
|
|
||||||
},
|
|
||||||
accessory_mate_bottle: {
|
|
||||||
name: "accessory_mate_bottle",
|
|
||||||
img: "resources/customisation/character_accessories/mate_bottle1.png",
|
|
||||||
},
|
|
||||||
accessory_mask: { name: "accessory_mask", img: "resources/customisation/character_accessories/mask.png" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LAYERS: BodyResourceDescriptionListInterface[] = [
|
public loadPlayerTexturesMetadata(metadata: PlayerTexturesMetadata): void {
|
||||||
COLOR_RESOURCES,
|
this.mapTexturesMetadataIntoResources(metadata);
|
||||||
EYES_RESOURCES,
|
}
|
||||||
HAIR_RESOURCES,
|
|
||||||
CLOTHES_RESOURCES,
|
public getTexturesResources(key: PlayerTexturesKey): BodyResourceDescriptionListInterface {
|
||||||
HATS_RESOURCES,
|
switch (key) {
|
||||||
ACCESSORIES_RESOURCES,
|
case PlayerTexturesKey.Accessory:
|
||||||
];
|
return this.ACCESSORIES_RESOURCES;
|
||||||
|
case PlayerTexturesKey.Body:
|
||||||
|
return this.COLOR_RESOURCES;
|
||||||
|
case PlayerTexturesKey.Clothes:
|
||||||
|
return this.CLOTHES_RESOURCES;
|
||||||
|
case PlayerTexturesKey.Eyes:
|
||||||
|
return this.EYES_RESOURCES;
|
||||||
|
case PlayerTexturesKey.Hair:
|
||||||
|
return this.HAIR_RESOURCES;
|
||||||
|
case PlayerTexturesKey.Hat:
|
||||||
|
return this.HATS_RESOURCES;
|
||||||
|
case PlayerTexturesKey.Woka:
|
||||||
|
return this.PLAYER_RESOURCES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLayers(): BodyResourceDescriptionListInterface[] {
|
||||||
|
return this.LAYERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapTexturesMetadataIntoResources(metadata: PlayerTexturesMetadata): void {
|
||||||
|
this.PLAYER_RESOURCES = this.getMappedResources(metadata.woka);
|
||||||
|
this.COLOR_RESOURCES = this.getMappedResources(metadata.body);
|
||||||
|
this.EYES_RESOURCES = this.getMappedResources(metadata.eyes);
|
||||||
|
this.HAIR_RESOURCES = this.getMappedResources(metadata.hair);
|
||||||
|
this.CLOTHES_RESOURCES = this.getMappedResources(metadata.clothes);
|
||||||
|
this.HATS_RESOURCES = this.getMappedResources(metadata.hat);
|
||||||
|
this.ACCESSORIES_RESOURCES = this.getMappedResources(metadata.accessory);
|
||||||
|
|
||||||
|
this.LAYERS = [
|
||||||
|
this.COLOR_RESOURCES,
|
||||||
|
this.EYES_RESOURCES,
|
||||||
|
this.HAIR_RESOURCES,
|
||||||
|
this.CLOTHES_RESOURCES,
|
||||||
|
this.HATS_RESOURCES,
|
||||||
|
this.ACCESSORIES_RESOURCES,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMappedResources(category: PlayerTexturesCategory): BodyResourceDescriptionListInterface {
|
||||||
|
const resources: BodyResourceDescriptionListInterface = {};
|
||||||
|
if (!category) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
for (const collection of category.collections) {
|
||||||
|
for (const texture of collection.textures) {
|
||||||
|
resources[texture.id] = { id: texture.id, img: texture.url };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const OBJECTS: BodyResourceDescriptionInterface[] = [
|
export const OBJECTS: BodyResourceDescriptionInterface[] = [
|
||||||
{ name: "teleportation", img: "resources/objects/teleportation.png" },
|
{ id: "teleportation", img: "resources/objects/teleportation.png" },
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
||||||
import type { CharacterTexture } from "../../Connexion/LocalUser";
|
import type { CharacterTexture } from "../../Connexion/LocalUser";
|
||||||
import { BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES } from "./PlayerTextures";
|
import { BodyResourceDescriptionInterface, mapLayerToLevel, PlayerTextures, PlayerTexturesKey } from "./PlayerTextures";
|
||||||
import CancelablePromise from "cancelable-promise";
|
import CancelablePromise from "cancelable-promise";
|
||||||
|
|
||||||
export interface FrameConfig {
|
export interface FrameConfig {
|
||||||
@ -8,33 +8,37 @@ export interface FrameConfig {
|
|||||||
frameHeight: number;
|
frameHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => {
|
export const loadAllLayers = (
|
||||||
|
load: LoaderPlugin,
|
||||||
|
playerTextures: PlayerTextures
|
||||||
|
): BodyResourceDescriptionInterface[][] => {
|
||||||
const returnArray: BodyResourceDescriptionInterface[][] = [];
|
const returnArray: BodyResourceDescriptionInterface[][] = [];
|
||||||
LAYERS.forEach((layer) => {
|
playerTextures.getLayers().forEach((layer) => {
|
||||||
const layerArray: BodyResourceDescriptionInterface[] = [];
|
const layerArray: BodyResourceDescriptionInterface[] = [];
|
||||||
Object.values(layer).forEach((textureDescriptor) => {
|
Object.values(layer).forEach((textureDescriptor) => {
|
||||||
layerArray.push(textureDescriptor);
|
layerArray.push(textureDescriptor);
|
||||||
load.spritesheet(textureDescriptor.name, textureDescriptor.img, { frameWidth: 32, frameHeight: 32 });
|
load.spritesheet(textureDescriptor.id, textureDescriptor.img, { frameWidth: 32, frameHeight: 32 });
|
||||||
});
|
});
|
||||||
returnArray.push(layerArray);
|
returnArray.push(layerArray);
|
||||||
});
|
});
|
||||||
return returnArray;
|
return returnArray;
|
||||||
};
|
};
|
||||||
export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptionInterface[] => {
|
export const loadAllDefaultModels = (
|
||||||
const returnArray = Object.values(PLAYER_RESOURCES);
|
load: LoaderPlugin,
|
||||||
|
playerTextures: PlayerTextures
|
||||||
|
): BodyResourceDescriptionInterface[] => {
|
||||||
|
const returnArray = Object.values(playerTextures.getTexturesResources(PlayerTexturesKey.Woka));
|
||||||
returnArray.forEach((playerResource: BodyResourceDescriptionInterface) => {
|
returnArray.forEach((playerResource: BodyResourceDescriptionInterface) => {
|
||||||
load.spritesheet(playerResource.name, playerResource.img, { frameWidth: 32, frameHeight: 32 });
|
load.spritesheet(playerResource.id, playerResource.img, { frameWidth: 32, frameHeight: 32 });
|
||||||
});
|
});
|
||||||
return returnArray;
|
return returnArray;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadCustomTexture = (
|
export const loadWokaTexture = (
|
||||||
loaderPlugin: LoaderPlugin,
|
loaderPlugin: LoaderPlugin,
|
||||||
texture: CharacterTexture
|
texture: BodyResourceDescriptionInterface
|
||||||
): CancelablePromise<BodyResourceDescriptionInterface> => {
|
): CancelablePromise<BodyResourceDescriptionInterface> => {
|
||||||
const name = "customCharacterTexture" + texture.id;
|
return createLoadingPromise(loaderPlugin, texture, {
|
||||||
const playerResourceDescriptor: BodyResourceDescriptionInterface = { name, img: texture.url, level: texture.level };
|
|
||||||
return createLoadingPromise(loaderPlugin, playerResourceDescriptor, {
|
|
||||||
frameWidth: 32,
|
frameWidth: 32,
|
||||||
frameHeight: 32,
|
frameHeight: 32,
|
||||||
});
|
});
|
||||||
@ -42,16 +46,15 @@ export const loadCustomTexture = (
|
|||||||
|
|
||||||
export const lazyLoadPlayerCharacterTextures = (
|
export const lazyLoadPlayerCharacterTextures = (
|
||||||
loadPlugin: LoaderPlugin,
|
loadPlugin: LoaderPlugin,
|
||||||
texturekeys: Array<string | BodyResourceDescriptionInterface>
|
textures: BodyResourceDescriptionInterface[]
|
||||||
): CancelablePromise<string[]> => {
|
): CancelablePromise<string[]> => {
|
||||||
const promisesList: CancelablePromise<unknown>[] = [];
|
const promisesList: CancelablePromise<unknown>[] = [];
|
||||||
texturekeys.forEach((textureKey: string | BodyResourceDescriptionInterface) => {
|
textures.forEach((texture) => {
|
||||||
try {
|
try {
|
||||||
//TODO refactor
|
//TODO refactor
|
||||||
const playerResourceDescriptor = getRessourceDescriptor(textureKey);
|
if (!loadPlugin.textureManager.exists(texture.id)) {
|
||||||
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
|
||||||
promisesList.push(
|
promisesList.push(
|
||||||
createLoadingPromise(loadPlugin, playerResourceDescriptor, {
|
createLoadingPromise(loadPlugin, texture, {
|
||||||
frameWidth: 32,
|
frameWidth: 32,
|
||||||
frameHeight: 32,
|
frameHeight: 32,
|
||||||
})
|
})
|
||||||
@ -64,58 +67,41 @@ export const lazyLoadPlayerCharacterTextures = (
|
|||||||
let returnPromise: CancelablePromise<Array<string | BodyResourceDescriptionInterface>>;
|
let returnPromise: CancelablePromise<Array<string | BodyResourceDescriptionInterface>>;
|
||||||
if (promisesList.length > 0) {
|
if (promisesList.length > 0) {
|
||||||
loadPlugin.start();
|
loadPlugin.start();
|
||||||
returnPromise = CancelablePromise.all(promisesList).then(() => texturekeys);
|
returnPromise = CancelablePromise.all(promisesList).then(() => textures);
|
||||||
} else {
|
} else {
|
||||||
returnPromise = CancelablePromise.resolve(texturekeys);
|
returnPromise = CancelablePromise.resolve(textures);
|
||||||
}
|
}
|
||||||
|
|
||||||
//If the loading fail, we render the default model instead.
|
//If the loading fail, we render the default model instead.
|
||||||
return returnPromise.then((keys) =>
|
return returnPromise.then((keys) =>
|
||||||
keys.map((key) => {
|
keys.map((key) => {
|
||||||
return typeof key !== "string" ? key.name : key;
|
return typeof key !== "string" ? key.id : key;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRessourceDescriptor = (
|
|
||||||
textureKey: string | BodyResourceDescriptionInterface
|
|
||||||
): BodyResourceDescriptionInterface => {
|
|
||||||
if (typeof textureKey !== "string" && textureKey.img) {
|
|
||||||
return textureKey;
|
|
||||||
}
|
|
||||||
const textureName: string = typeof textureKey === "string" ? textureKey : textureKey.name;
|
|
||||||
const playerResource = PLAYER_RESOURCES[textureName];
|
|
||||||
if (playerResource !== undefined) return playerResource;
|
|
||||||
|
|
||||||
for (let i = 0; i < LAYERS.length; i++) {
|
|
||||||
const playerResource = LAYERS[i][textureName];
|
|
||||||
if (playerResource !== undefined) return playerResource;
|
|
||||||
}
|
|
||||||
throw new Error("Could not find a data for texture " + textureName);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createLoadingPromise = (
|
export const createLoadingPromise = (
|
||||||
loadPlugin: LoaderPlugin,
|
loadPlugin: LoaderPlugin,
|
||||||
playerResourceDescriptor: BodyResourceDescriptionInterface,
|
playerResourceDescriptor: BodyResourceDescriptionInterface,
|
||||||
frameConfig: FrameConfig
|
frameConfig: FrameConfig
|
||||||
) => {
|
) => {
|
||||||
return new CancelablePromise<BodyResourceDescriptionInterface>((res, rej, cancel) => {
|
return new CancelablePromise<BodyResourceDescriptionInterface>((res, rej, cancel) => {
|
||||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
if (loadPlugin.textureManager.exists(playerResourceDescriptor.id)) {
|
||||||
return res(playerResourceDescriptor);
|
return res(playerResourceDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(() => {
|
cancel(() => {
|
||||||
loadPlugin.off("loaderror");
|
loadPlugin.off("loaderror");
|
||||||
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name);
|
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.id);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
|
loadPlugin.spritesheet(playerResourceDescriptor.id, playerResourceDescriptor.img, frameConfig);
|
||||||
const errorCallback = (file: { src: string }) => {
|
const errorCallback = (file: { src: string }) => {
|
||||||
if (file.src !== playerResourceDescriptor.img) return;
|
if (file.src !== playerResourceDescriptor.img) return;
|
||||||
console.error("failed loading player resource: ", playerResourceDescriptor);
|
console.error("failed loading player resource: ", playerResourceDescriptor);
|
||||||
rej(playerResourceDescriptor);
|
rej(playerResourceDescriptor);
|
||||||
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback);
|
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.id, successCallback);
|
||||||
loadPlugin.off("loaderror", errorCallback);
|
loadPlugin.off("loaderror", errorCallback);
|
||||||
};
|
};
|
||||||
const successCallback = () => {
|
const successCallback = () => {
|
||||||
@ -123,7 +109,7 @@ export const createLoadingPromise = (
|
|||||||
res(playerResourceDescriptor);
|
res(playerResourceDescriptor);
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPlugin.once("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback);
|
loadPlugin.once("filecomplete-spritesheet-" + playerResourceDescriptor.id, successCallback);
|
||||||
loadPlugin.on("loaderror", errorCallback);
|
loadPlugin.on("loaderror", errorCallback);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@ import { soundManager } from "./SoundManager";
|
|||||||
import { SharedVariablesManager } from "./SharedVariablesManager";
|
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||||
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
|
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
|
||||||
|
|
||||||
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
|
import { lazyLoadPlayerCharacterTextures, loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
|
||||||
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
|
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
|
||||||
import { iframeListener } from "../../Api/IframeListener";
|
import { iframeListener } from "../../Api/IframeListener";
|
||||||
import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
|
import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
|
||||||
@ -97,6 +97,8 @@ import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
|
|||||||
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
|
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
|
||||||
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
|
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
|
||||||
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
|
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
|
||||||
|
import { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
|
||||||
|
import CancelablePromise from "cancelable-promise";
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null;
|
initPosition: PointInterface | null;
|
||||||
reconnecting: boolean;
|
reconnecting: boolean;
|
||||||
@ -244,13 +246,6 @@ export class GameScene extends DirtyScene {
|
|||||||
//initialize frame event of scripting API
|
//initialize frame event of scripting API
|
||||||
this.listenToIframeEvents();
|
this.listenToIframeEvents();
|
||||||
|
|
||||||
const localUser = localUserStore.getLocalUser();
|
|
||||||
const textures = localUser?.textures;
|
|
||||||
if (textures) {
|
|
||||||
for (const texture of textures) {
|
|
||||||
loadCustomTexture(this.load, texture).catch((e) => console.error(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.load.image("iconTalk", "/resources/icons/icon_talking.png");
|
this.load.image("iconTalk", "/resources/icons/icon_talking.png");
|
||||||
|
|
||||||
if (touchScreenManager.supportTouchScreen) {
|
if (touchScreenManager.supportTouchScreen) {
|
||||||
@ -744,6 +739,14 @@ export class GameScene extends DirtyScene {
|
|||||||
.then((onConnect: OnConnectInterface) => {
|
.then((onConnect: OnConnectInterface) => {
|
||||||
this.connection = onConnect.connection;
|
this.connection = onConnect.connection;
|
||||||
|
|
||||||
|
lazyLoadPlayerCharacterTextures(this.load, onConnect.room.characterLayers)
|
||||||
|
.then((layers) => {
|
||||||
|
this.currentPlayerTexturesResolve(layers);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.currentPlayerTexturesReject(e);
|
||||||
|
});
|
||||||
|
|
||||||
playersStore.connectToRoomConnection(this.connection);
|
playersStore.connectToRoomConnection(this.connection);
|
||||||
userIsAdminStore.set(this.connection.hasTag("admin"));
|
userIsAdminStore.set(this.connection.hasTag("admin"));
|
||||||
|
|
||||||
@ -1531,7 +1534,7 @@ ${escapedMessage}
|
|||||||
this.messageSubscription?.unsubscribe();
|
this.messageSubscription?.unsubscribe();
|
||||||
this.userInputManager.destroy();
|
this.userInputManager.destroy();
|
||||||
this.pinchManager?.destroy();
|
this.pinchManager?.destroy();
|
||||||
this.emoteManager.destroy();
|
this.emoteManager?.destroy();
|
||||||
this.cameraManager.destroy();
|
this.cameraManager.destroy();
|
||||||
this.peerStoreUnsubscribe();
|
this.peerStoreUnsubscribe();
|
||||||
this.emoteUnsubscribe();
|
this.emoteUnsubscribe();
|
||||||
@ -1690,16 +1693,23 @@ ${escapedMessage}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The promise that will resolve to the current player texture. This will be available only after connection is established.
|
||||||
|
private currentPlayerTexturesResolve!: (value: string[]) => void;
|
||||||
|
private currentPlayerTexturesReject!: (reason: unknown) => void;
|
||||||
|
private currentPlayerTexturesPromise: CancelablePromise<string[]> = new CancelablePromise((resolve, reject) => {
|
||||||
|
this.currentPlayerTexturesResolve = resolve;
|
||||||
|
this.currentPlayerTexturesReject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
private createCurrentPlayer() {
|
private createCurrentPlayer() {
|
||||||
//TODO create animation moving between exit and start
|
//TODO create animation moving between exit and start
|
||||||
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers);
|
|
||||||
try {
|
try {
|
||||||
this.CurrentPlayer = new Player(
|
this.CurrentPlayer = new Player(
|
||||||
this,
|
this,
|
||||||
this.startPositionCalculator.startPosition.x,
|
this.startPositionCalculator.startPosition.x,
|
||||||
this.startPositionCalculator.startPosition.y,
|
this.startPositionCalculator.startPosition.y,
|
||||||
this.playerName,
|
this.playerName,
|
||||||
texturesPromise,
|
this.currentPlayerTexturesPromise,
|
||||||
PlayerAnimationDirections.Down,
|
PlayerAnimationDirections.Down,
|
||||||
false,
|
false,
|
||||||
this.companion,
|
this.companion,
|
||||||
|
@ -1,41 +1,41 @@
|
|||||||
import { ResizableScene } from "./ResizableScene";
|
import { ResizableScene } from "./ResizableScene";
|
||||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
import { BodyResourceDescriptionInterface, PlayerTexturesKey } from "../Entity/PlayerTextures";
|
||||||
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
|
import { loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
|
||||||
import { loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
|
|
||||||
import type { CharacterTexture } from "../../Connexion/LocalUser";
|
|
||||||
import type CancelablePromise from "cancelable-promise";
|
import type CancelablePromise from "cancelable-promise";
|
||||||
|
import { PlayerTextures } from "../Entity/PlayerTextures";
|
||||||
|
|
||||||
export abstract class AbstractCharacterScene extends ResizableScene {
|
export abstract class AbstractCharacterScene extends ResizableScene {
|
||||||
|
protected playerTextures: PlayerTextures;
|
||||||
|
|
||||||
|
constructor(params: { key: string }) {
|
||||||
|
super(params);
|
||||||
|
this.playerTextures = new PlayerTextures();
|
||||||
|
}
|
||||||
|
|
||||||
loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
||||||
const textures = this.getTextures();
|
const textures = this.playerTextures.getTexturesResources(PlayerTexturesKey.Woka);
|
||||||
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
|
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
|
||||||
if (textures) {
|
if (textures) {
|
||||||
for (const texture of textures) {
|
for (const texture of Object.values(textures)) {
|
||||||
if (texture.level === -1) {
|
if (texture.level === -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
promises.push(loadCustomTexture(this.load, texture));
|
promises.push(loadWokaTexture(this.load, texture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
||||||
const textures = this.getTextures();
|
|
||||||
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
|
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
|
||||||
if (textures) {
|
for (const textures of this.playerTextures.getLayers()) {
|
||||||
for (const texture of textures) {
|
for (const texture of Object.values(textures)) {
|
||||||
if (texture.level !== -1) {
|
if (texture.level !== -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
promises.push(loadCustomTexture(this.load, texture));
|
promises.push(loadWokaTexture(this.load, texture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTextures(): CharacterTexture[] | undefined {
|
|
||||||
const localUser = localUserStore.getLocalUser();
|
|
||||||
return localUser?.textures;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import { CustomizedCharacter } from "../Entity/CustomizedCharacter";
|
|||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||||
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
|
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
|
||||||
|
import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
export const CustomizeSceneName = "CustomizeScene";
|
export const CustomizeSceneName = "CustomizeScene";
|
||||||
|
|
||||||
@ -40,27 +41,45 @@ export class CustomizeScene extends AbstractCharacterScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.loadCustomSceneSelectCharacters()
|
const wokaMetadataKey = "woka-list";
|
||||||
.then((bodyResourceDescriptions) => {
|
this.cache.json.remove(wokaMetadataKey);
|
||||||
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
// FIXME: window.location.href is wrong. We need the URL of the main room (so we need to apply any redirect before!)
|
||||||
if (
|
this.load.json(
|
||||||
bodyResourceDescription.level == undefined ||
|
wokaMetadataKey,
|
||||||
bodyResourceDescription.level < 0 ||
|
`${PUSHER_URL}/woka/list/` + encodeURIComponent(window.location.href),
|
||||||
bodyResourceDescription.level > 5
|
undefined,
|
||||||
) {
|
{
|
||||||
throw new Error("Texture level is null");
|
responseType: "text",
|
||||||
}
|
headers: {
|
||||||
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
|
Authorization: localUserStore.getAuthToken() ?? "",
|
||||||
});
|
},
|
||||||
this.lazyloadingAttempt = true;
|
withCredentials: true,
|
||||||
})
|
}
|
||||||
.catch((e) => console.error(e));
|
);
|
||||||
|
this.load.once(`filecomplete-json-${wokaMetadataKey}`, () => {
|
||||||
|
this.playerTextures.loadPlayerTexturesMetadata(this.cache.json.get(wokaMetadataKey));
|
||||||
|
this.loadCustomSceneSelectCharacters()
|
||||||
|
.then((bodyResourceDescriptions) => {
|
||||||
|
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
||||||
|
if (
|
||||||
|
bodyResourceDescription.level == undefined ||
|
||||||
|
bodyResourceDescription.level < 0 ||
|
||||||
|
bodyResourceDescription.level > 5
|
||||||
|
) {
|
||||||
|
throw new Error("Texture level is null");
|
||||||
|
}
|
||||||
|
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
|
||||||
|
});
|
||||||
|
this.lazyloadingAttempt = true;
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
|
||||||
this.layers = loadAllLayers(this.load);
|
this.layers = loadAllLayers(this.load, this.playerTextures);
|
||||||
this.lazyloadingAttempt = false;
|
this.lazyloadingAttempt = false;
|
||||||
|
|
||||||
//this function must stay at the end of preload function
|
//this function must stay at the end of preload function
|
||||||
this.loader.addLoader();
|
this.loader.addLoader();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
@ -192,13 +211,13 @@ export class CustomizeScene extends AbstractCharacterScene {
|
|||||||
const children: Array<string> = new Array<string>();
|
const children: Array<string> = new Array<string>();
|
||||||
for (let j = 0; j <= layerNumber; j++) {
|
for (let j = 0; j <= layerNumber; j++) {
|
||||||
if (j === layerNumber) {
|
if (j === layerNumber) {
|
||||||
children.push(this.layers[j][selectedItem].name);
|
children.push(this.layers[j][selectedItem].id);
|
||||||
} else {
|
} else {
|
||||||
const layer = this.selectedLayers[j];
|
const layer = this.selectedLayers[j];
|
||||||
if (layer === undefined) {
|
if (layer === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
children.push(this.layers[j][layer].name);
|
children.push(this.layers[j][layer].id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
@ -276,7 +295,7 @@ export class CustomizeScene extends AbstractCharacterScene {
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
for (const layerItem of this.selectedLayers) {
|
for (const layerItem of this.selectedLayers) {
|
||||||
if (layerItem !== undefined) {
|
if (layerItem !== undefined) {
|
||||||
layers.push(this.layers[i][layerItem].name);
|
layers.push(this.layers[i][layerItem].id);
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@ -287,14 +306,14 @@ export class CustomizeScene extends AbstractCharacterScene {
|
|||||||
analyticsClient.validationWoka("CustomizeWoka");
|
analyticsClient.validationWoka("CustomizeWoka");
|
||||||
|
|
||||||
gameManager.setCharacterLayers(layers);
|
gameManager.setCharacterLayers(layers);
|
||||||
this.scene.sleep(CustomizeSceneName);
|
this.scene.stop(CustomizeSceneName);
|
||||||
waScaleManager.restoreZoom();
|
waScaleManager.restoreZoom();
|
||||||
gameManager.tryResumingGame(EnableCameraSceneName);
|
gameManager.tryResumingGame(EnableCameraSceneName);
|
||||||
customCharacterSceneVisibleStore.set(false);
|
customCharacterSceneVisibleStore.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public backToPreviousScene() {
|
public backToPreviousScene() {
|
||||||
this.scene.sleep(CustomizeSceneName);
|
this.scene.stop(CustomizeSceneName);
|
||||||
waScaleManager.restoreZoom();
|
waScaleManager.restoreZoom();
|
||||||
this.scene.run(SelectCharacterSceneName);
|
this.scene.run(SelectCharacterSceneName);
|
||||||
customCharacterSceneVisibleStore.set(false);
|
customCharacterSceneVisibleStore.set(false);
|
||||||
|
@ -7,6 +7,8 @@ import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
|
|||||||
import LL from "../../i18n/i18n-svelte";
|
import LL from "../../i18n/i18n-svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { localeDetector } from "../../i18n/locales";
|
import { localeDetector } from "../../i18n/locales";
|
||||||
|
import { PlayerTextures } from "../Entity/PlayerTextures";
|
||||||
|
import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
export const EntrySceneName = "EntryScene";
|
export const EntrySceneName = "EntryScene";
|
||||||
|
|
||||||
@ -15,6 +17,8 @@ export const EntrySceneName = "EntryScene";
|
|||||||
* and to route to the next correct scene.
|
* and to route to the next correct scene.
|
||||||
*/
|
*/
|
||||||
export class EntryScene extends Scene {
|
export class EntryScene extends Scene {
|
||||||
|
private localeLoaded: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
key: EntrySceneName,
|
key: EntrySceneName,
|
||||||
@ -30,6 +34,10 @@ export class EntryScene extends Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
this.loadLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadLocale(): void {
|
||||||
localeDetector()
|
localeDetector()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
gameManager
|
gameManager
|
||||||
|
@ -5,7 +5,7 @@ import { CustomizeSceneName } from "./CustomizeScene";
|
|||||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||||
import { loadAllDefaultModels } from "../Entity/PlayerTexturesLoadingManager";
|
import { loadAllDefaultModels } from "../Entity/PlayerTexturesLoadingManager";
|
||||||
import { Loader } from "../Components/Loader";
|
import { Loader } from "../Components/Loader";
|
||||||
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
|
import { BodyResourceDescriptionInterface, PlayerTextures } from "../Entity/PlayerTextures";
|
||||||
import { AbstractCharacterScene } from "./AbstractCharacterScene";
|
import { AbstractCharacterScene } from "./AbstractCharacterScene";
|
||||||
import { areCharacterLayersValid } from "../../Connexion/LocalUser";
|
import { areCharacterLayersValid } from "../../Connexion/LocalUser";
|
||||||
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
||||||
@ -14,6 +14,8 @@ import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterSt
|
|||||||
import { waScaleManager } from "../Services/WaScaleManager";
|
import { waScaleManager } from "../Services/WaScaleManager";
|
||||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||||
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
|
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
|
||||||
|
import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
|
||||||
|
import { customizeAvailableStore } from "../../Stores/SelectCharacterSceneStore";
|
||||||
|
|
||||||
//todo: put this constants in a dedicated file
|
//todo: put this constants in a dedicated file
|
||||||
export const SelectCharacterSceneName = "SelectCharacterScene";
|
export const SelectCharacterSceneName = "SelectCharacterScene";
|
||||||
@ -38,25 +40,46 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
key: SelectCharacterSceneName,
|
key: SelectCharacterSceneName,
|
||||||
});
|
});
|
||||||
this.loader = new Loader(this);
|
this.loader = new Loader(this);
|
||||||
|
this.playerTextures = new PlayerTextures();
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.loadSelectSceneCharacters()
|
const wokaMetadataKey = "woka-list";
|
||||||
.then((bodyResourceDescriptions) => {
|
this.cache.json.remove(wokaMetadataKey);
|
||||||
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
|
||||||
this.playerModels.push(bodyResourceDescription);
|
|
||||||
});
|
|
||||||
this.lazyloadingAttempt = true;
|
|
||||||
})
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
this.playerModels = loadAllDefaultModels(this.load);
|
|
||||||
this.lazyloadingAttempt = false;
|
|
||||||
|
|
||||||
//this function must stay at the end of preload function
|
// FIXME: window.location.href is wrong. We need the URL of the main room (so we need to apply any redirect before!)
|
||||||
this.loader.addLoader();
|
this.load.json(
|
||||||
|
wokaMetadataKey,
|
||||||
|
`${PUSHER_URL}/woka/list/` + encodeURIComponent(window.location.href),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
responseType: "text",
|
||||||
|
headers: {
|
||||||
|
Authorization: localUserStore.getAuthToken() ?? "",
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.load.once(`filecomplete-json-${wokaMetadataKey}`, () => {
|
||||||
|
this.playerTextures.loadPlayerTexturesMetadata(this.cache.json.get(wokaMetadataKey));
|
||||||
|
this.loadSelectSceneCharacters()
|
||||||
|
.then((bodyResourceDescriptions) => {
|
||||||
|
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
|
||||||
|
this.playerModels.push(bodyResourceDescription);
|
||||||
|
});
|
||||||
|
this.lazyloadingAttempt = true;
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
this.playerModels = loadAllDefaultModels(this.load, this.playerTextures);
|
||||||
|
this.lazyloadingAttempt = false;
|
||||||
|
|
||||||
|
//this function must stay at the end of preload function
|
||||||
|
this.loader.addLoader();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
customizeAvailableStore.set(this.isCustomizationAvailable());
|
||||||
selectCharacterSceneVisibleStore.set(true);
|
selectCharacterSceneVisibleStore.set(true);
|
||||||
this.events.addListener("wake", () => {
|
this.events.addListener("wake", () => {
|
||||||
waScaleManager.saveZoom();
|
waScaleManager.saveZoom();
|
||||||
@ -130,16 +153,16 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
const playerResource = this.playerModels[i];
|
const playerResource = this.playerModels[i];
|
||||||
|
|
||||||
//check already exist texture
|
//check already exist texture
|
||||||
if (this.players.find((c) => c.texture.key === playerResource.name)) {
|
if (this.players.find((c) => c.texture.key === playerResource.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [middleX, middleY] = this.getCharacterPosition();
|
const [middleX, middleY] = this.getCharacterPosition();
|
||||||
const player = this.physics.add.sprite(middleX, middleY, playerResource.name, 0);
|
const player = this.physics.add.sprite(middleX, middleY, playerResource.id, 0);
|
||||||
this.setUpPlayer(player, i);
|
this.setUpPlayer(player, i);
|
||||||
this.anims.create({
|
this.anims.create({
|
||||||
key: playerResource.name,
|
key: playerResource.id,
|
||||||
frames: this.anims.generateFrameNumbers(playerResource.name, { start: 0, end: 11 }),
|
frames: this.anims.generateFrameNumbers(playerResource.id, { start: 0, end: 11 }),
|
||||||
frameRate: 8,
|
frameRate: 8,
|
||||||
repeat: -1,
|
repeat: -1,
|
||||||
});
|
});
|
||||||
@ -164,7 +187,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
this.currentSelectUser = 0;
|
this.currentSelectUser = 0;
|
||||||
}
|
}
|
||||||
this.selectedPlayer = this.players[this.currentSelectUser];
|
this.selectedPlayer = this.players[this.currentSelectUser];
|
||||||
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
|
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected moveUser() {
|
protected moveUser() {
|
||||||
@ -247,9 +270,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected updateSelectedPlayer(): void {
|
protected updateSelectedPlayer(): void {
|
||||||
this.selectedPlayer?.anims.pause(this.selectedPlayer?.anims.currentAnim.frames[0]);
|
this.selectedPlayer?.anims?.pause(this.selectedPlayer?.anims.currentAnim.frames[0]);
|
||||||
const player = this.players[this.currentSelectUser];
|
const player = this.players[this.currentSelectUser];
|
||||||
player.play(this.playerModels[this.currentSelectUser].name);
|
player?.play(this.playerModels[this.currentSelectUser].id);
|
||||||
this.selectedPlayer = player;
|
this.selectedPlayer = player;
|
||||||
localUserStore.setPlayerCharacterIndex(this.currentSelectUser);
|
localUserStore.setPlayerCharacterIndex(this.currentSelectUser);
|
||||||
}
|
}
|
||||||
@ -274,4 +297,13 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
|||||||
//move position of user
|
//move position of user
|
||||||
this.moveUser();
|
this.moveUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isCustomizationAvailable(): boolean {
|
||||||
|
for (const layer of this.playerTextures.getLayers()) {
|
||||||
|
if (Object.keys(layer).length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
front/src/Stores/SelectCharacterSceneStore.ts
Normal file
3
front/src/Stores/SelectCharacterSceneStore.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
export const customizeAvailableStore = writable(false);
|
@ -1,28 +0,0 @@
|
|||||||
import "jasmine";
|
|
||||||
import { getRessourceDescriptor } from "../../../src/Phaser/Entity/PlayerTexturesLoadingManager";
|
|
||||||
|
|
||||||
describe("getRessourceDescriptor()", () => {
|
|
||||||
it(", if given a valid descriptor as parameter, should return it", () => {
|
|
||||||
const desc = getRessourceDescriptor({ name: "name", img: "url" });
|
|
||||||
expect(desc.name).toEqual("name");
|
|
||||||
expect(desc.img).toEqual("url");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(", if given a string as parameter, should search through hardcoded values", () => {
|
|
||||||
const desc = getRessourceDescriptor("male1");
|
|
||||||
expect(desc.name).toEqual("male1");
|
|
||||||
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(", if given a string as parameter, should search through hardcoded values (bis)", () => {
|
|
||||||
const desc = getRessourceDescriptor("color_2");
|
|
||||||
expect(desc.name).toEqual("color_2");
|
|
||||||
expect(desc.img).toEqual("resources/customisation/character_color/character_color1.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(", if given a descriptor without url as parameter, should search through hardcoded values", () => {
|
|
||||||
const desc = getRessourceDescriptor({ name: "male1", img: "" });
|
|
||||||
expect(desc.name).toEqual("male1");
|
|
||||||
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,5 +1,4 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
import { isCharacterTexture } from "./CharacterTexture";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* WARNING! The original file is in /messages/JsonMessages.
|
* WARNING! The original file is in /messages/JsonMessages.
|
||||||
@ -12,7 +11,6 @@ export const isAdminApiData = new tg.IsInterface()
|
|||||||
email: tg.isNullable(tg.isString),
|
email: tg.isNullable(tg.isString),
|
||||||
roomUrl: tg.isString,
|
roomUrl: tg.isString,
|
||||||
mapUrlStart: tg.isString,
|
mapUrlStart: tg.isString,
|
||||||
textures: tg.isArray(isCharacterTexture),
|
|
||||||
})
|
})
|
||||||
.withOptionalProperties({
|
.withOptionalProperties({
|
||||||
messages: tg.isArray(tg.isUnknown),
|
messages: tg.isArray(tg.isUnknown),
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import * as tg from "generic-type-guard";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* WARNING! The original file is in /messages/JsonMessages.
|
|
||||||
* All other files are automatically copied from this file on container startup / build
|
|
||||||
*/
|
|
||||||
|
|
||||||
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,5 +1,4 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
import { isCharacterTexture } from "./CharacterTexture";
|
|
||||||
import { isNumber } from "generic-type-guard";
|
import { isNumber } from "generic-type-guard";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -12,7 +11,6 @@ export const isMapDetailsData = new tg.IsInterface()
|
|||||||
mapUrl: tg.isString,
|
mapUrl: tg.isString,
|
||||||
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
||||||
tags: tg.isArray(tg.isString),
|
tags: tg.isArray(tg.isString),
|
||||||
textures: tg.isArray(isCharacterTexture),
|
|
||||||
authenticationMandatory: tg.isUnion(tg.isNullable(tg.isBoolean), tg.isUndefined),
|
authenticationMandatory: tg.isUnion(tg.isNullable(tg.isBoolean), tg.isUndefined),
|
||||||
roomSlug: tg.isNullable(tg.isString), // deprecated
|
roomSlug: tg.isNullable(tg.isString), // deprecated
|
||||||
contactPage: tg.isNullable(tg.isString),
|
contactPage: tg.isNullable(tg.isString),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
import { isCharacterTexture } from "./CharacterTexture";
|
//import { isCharacterTexture } from "./CharacterTexture";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* WARNING! The original file is in /messages/JsonMessages.
|
* WARNING! The original file is in /messages/JsonMessages.
|
||||||
@ -13,7 +13,6 @@ export const isRegisterData = new tg.IsInterface()
|
|||||||
organizationMemberToken: tg.isNullable(tg.isString),
|
organizationMemberToken: tg.isNullable(tg.isString),
|
||||||
mapUrlStart: tg.isString,
|
mapUrlStart: tg.isString,
|
||||||
userUuid: tg.isString,
|
userUuid: tg.isString,
|
||||||
textures: tg.isArray(isCharacterTexture),
|
|
||||||
authToken: tg.isString,
|
authToken: tg.isString,
|
||||||
})
|
})
|
||||||
.withOptionalProperties({
|
.withOptionalProperties({
|
||||||
|
@ -9,9 +9,10 @@
|
|||||||
"copy-to-front-ts-proto": "sed 's/import { Observable } from \"rxjs\";/import type { Observable } from \"rxjs\";/g' ts-proto-generated/protos/messages.ts > ../front/src/Messages/ts-proto-generated/messages.ts",
|
"copy-to-front-ts-proto": "sed 's/import { Observable } from \"rxjs\";/import type { Observable } from \"rxjs\";/g' ts-proto-generated/protos/messages.ts > ../front/src/Messages/ts-proto-generated/messages.ts",
|
||||||
"copy-to-pusher": "rm -rf ../pusher/src/Messages/generated && cp -rf generated/ ../pusher/src/Messages/generated",
|
"copy-to-pusher": "rm -rf ../pusher/src/Messages/generated && cp -rf generated/ ../pusher/src/Messages/generated",
|
||||||
"json-copy-to-pusher": "rm -rf ../pusher/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../pusher/src/Messages/JsonMessages/",
|
"json-copy-to-pusher": "rm -rf ../pusher/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../pusher/src/Messages/JsonMessages/",
|
||||||
|
"json-copy-to-back": "rm -rf ../back/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../back/src/Messages/JsonMessages/",
|
||||||
"json-copy-to-front": "rm -rf ../front/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../front/src/Messages/JsonMessages/",
|
"json-copy-to-front": "rm -rf ../front/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../front/src/Messages/JsonMessages/",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"proto-all": "yarn run proto && yarn run ts-proto && yarn run copy-to-back && yarn run copy-to-front-ts-proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher && yarn run json-copy-to-front",
|
"proto-all": "yarn run proto && yarn run ts-proto && yarn run copy-to-back && yarn run copy-to-front-ts-proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher && yarn run json-copy-to-back && yarn run json-copy-to-front",
|
||||||
"proto:watch": "yarn run proto-all; inotifywait -q -m -e close_write protos/messages.proto JsonMessages/ | while read -r filename event; do yarn run proto-all; done",
|
"proto:watch": "yarn run proto-all; inotifywait -q -m -e close_write protos/messages.proto JsonMessages/ | while read -r filename event; do yarn run proto-all; done",
|
||||||
"pretty": "yarn prettier --write 'JsonMessages/**/*.ts'",
|
"pretty": "yarn prettier --write 'JsonMessages/**/*.ts'",
|
||||||
"pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'"
|
"pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'"
|
||||||
|
@ -34,6 +34,7 @@ message SilentMessage {
|
|||||||
message CharacterLayerMessage {
|
message CharacterLayerMessage {
|
||||||
string url = 1;
|
string url = 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
|
string layer = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CompanionMessage {
|
message CompanionMessage {
|
||||||
@ -223,6 +224,8 @@ message RoomJoinedMessage {
|
|||||||
repeated string tag = 5;
|
repeated string tag = 5;
|
||||||
repeated VariableMessage variable = 6;
|
repeated VariableMessage variable = 6;
|
||||||
string userRoomToken = 7;
|
string userRoomToken = 7;
|
||||||
|
// We send the current skin of the current player.
|
||||||
|
repeated CharacterLayerMessage characterLayer = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WebRtcStartMessage {
|
message WebRtcStartMessage {
|
||||||
@ -274,6 +277,8 @@ message WorldFullMessage{
|
|||||||
}
|
}
|
||||||
message TokenExpiredMessage{
|
message TokenExpiredMessage{
|
||||||
}
|
}
|
||||||
|
message InvalidTextureMessage{
|
||||||
|
}
|
||||||
|
|
||||||
message WorldConnexionMessage{
|
message WorldConnexionMessage{
|
||||||
string message = 2;
|
string message = 2;
|
||||||
@ -310,6 +315,7 @@ message ServerToClientMessage {
|
|||||||
FollowRequestMessage followRequestMessage = 21;
|
FollowRequestMessage followRequestMessage = 21;
|
||||||
FollowConfirmationMessage followConfirmationMessage = 22;
|
FollowConfirmationMessage followConfirmationMessage = 22;
|
||||||
FollowAbortMessage followAbortMessage = 23;
|
FollowAbortMessage followAbortMessage = 23;
|
||||||
|
InvalidTextureMessage invalidTextureMessage = 24;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1851
pusher/data/woka.json
Normal file
1851
pusher/data/woka.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,9 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc",
|
"tsc": "tsc && cp -rf ./data ./dist/",
|
||||||
"dev": "ts-node-dev --respawn ./server.ts",
|
"dev": "ts-node-dev --respawn ./server.ts",
|
||||||
"prod": "tsc && node --max-old-space-size=4096 ./dist/server.js",
|
"prod": "tsc && cp -rf ./data ./dist/ && node --max-old-space-size=4096 ./dist/server.js",
|
||||||
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
||||||
"profile": "tsc && node --prof ./dist/server.js",
|
"profile": "tsc && node --prof ./dist/server.js",
|
||||||
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||||
@ -41,22 +41,22 @@
|
|||||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
"busboy": "^0.3.1",
|
|
||||||
"circular-json": "^0.5.9",
|
"circular-json": "^0.5.9",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"generic-type-guard": "^3.2.0",
|
"generic-type-guard": "^3.2.0",
|
||||||
"google-protobuf": "^3.13.0",
|
"google-protobuf": "^3.13.0",
|
||||||
"grpc": "^1.24.4",
|
"grpc": "^1.24.4",
|
||||||
|
"hyper-express": "^5.8.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"openid-client": "^4.7.4",
|
"openid-client": "^4.7.4",
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
|
"qs": "^6.10.3",
|
||||||
"query-string": "^6.13.3",
|
"query-string": "^6.13.3",
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.4.0",
|
"uuidv4": "^6.0.7",
|
||||||
"uuidv4": "^6.0.7"
|
"zod": "^3.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/busboy": "^0.2.3",
|
|
||||||
"@types/circular-json": "^0.4.0",
|
"@types/circular-json": "^0.4.0",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/google-protobuf": "^3.7.3",
|
"@types/google-protobuf": "^3.7.3",
|
||||||
@ -64,13 +64,17 @@
|
|||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.10",
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
"@types/mkdirp": "^1.0.1",
|
"@types/mkdirp": "^1.0.1",
|
||||||
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/uuidv4": "^5.0.0",
|
"@types/uuidv4": "^5.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@typescript-eslint/parser": "^2.26.0",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"jasmine": "^3.5.0",
|
"jasmine": "^3.5.0",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
|
"live-directory": "^2.3.2",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
|
"swagger-jsdoc": "^6.1.0",
|
||||||
|
"swagger-ui-dist": "^4.5.1",
|
||||||
"ts-node-dev": "^1.1.8",
|
"ts-node-dev": "^1.1.8",
|
||||||
"typescript": "^4.5.2"
|
"typescript": "^4.5.2"
|
||||||
},
|
},
|
||||||
|
@ -4,31 +4,38 @@ import { AuthenticateController } from "./Controller/AuthenticateController"; //
|
|||||||
import { MapController } from "./Controller/MapController";
|
import { MapController } from "./Controller/MapController";
|
||||||
import { PrometheusController } from "./Controller/PrometheusController";
|
import { PrometheusController } from "./Controller/PrometheusController";
|
||||||
import { DebugController } from "./Controller/DebugController";
|
import { DebugController } from "./Controller/DebugController";
|
||||||
import { App as uwsApp } from "./Server/sifrr.server";
|
|
||||||
import { AdminController } from "./Controller/AdminController";
|
import { AdminController } from "./Controller/AdminController";
|
||||||
import { OpenIdProfileController } from "./Controller/OpenIdProfileController";
|
import { OpenIdProfileController } from "./Controller/OpenIdProfileController";
|
||||||
|
import { WokaListController } from "./Controller/WokaListController";
|
||||||
|
import { SwaggerController } from "./Controller/SwaggerController";
|
||||||
|
import HyperExpress from "hyper-express";
|
||||||
|
import { cors } from "./Middleware/Cors";
|
||||||
|
import { ENABLE_OPENAPI_ENDPOINT } from "./Enum/EnvironmentVariable";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: uwsApp;
|
public app: HyperExpress.compressors.TemplatedApp;
|
||||||
public ioSocketController: IoSocketController;
|
|
||||||
public authenticateController: AuthenticateController;
|
|
||||||
public mapController: MapController;
|
|
||||||
public prometheusController: PrometheusController;
|
|
||||||
private debugController: DebugController;
|
|
||||||
private adminController: AdminController;
|
|
||||||
private openIdProfileController: OpenIdProfileController;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = new uwsApp();
|
const webserver = new HyperExpress.Server();
|
||||||
|
this.app = webserver.uws_instance;
|
||||||
|
|
||||||
//create socket controllers
|
// Global middlewares
|
||||||
this.ioSocketController = new IoSocketController(this.app);
|
webserver.use(cors);
|
||||||
this.authenticateController = new AuthenticateController(this.app);
|
|
||||||
this.mapController = new MapController(this.app);
|
// Socket controllers
|
||||||
this.prometheusController = new PrometheusController(this.app);
|
new IoSocketController(this.app);
|
||||||
this.debugController = new DebugController(this.app);
|
|
||||||
this.adminController = new AdminController(this.app);
|
// Http controllers
|
||||||
this.openIdProfileController = new OpenIdProfileController(this.app);
|
new AuthenticateController(webserver);
|
||||||
|
new MapController(webserver);
|
||||||
|
new PrometheusController(webserver);
|
||||||
|
new DebugController(webserver);
|
||||||
|
new AdminController(webserver);
|
||||||
|
new OpenIdProfileController(webserver);
|
||||||
|
new WokaListController(webserver);
|
||||||
|
if (ENABLE_OPENAPI_ENDPOINT) {
|
||||||
|
new SwaggerController(webserver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,45 +1,43 @@
|
|||||||
import { BaseController } from "./BaseController";
|
|
||||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
|
||||||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
|
||||||
import { apiClientRepository } from "../Services/ApiClientRepository";
|
import { apiClientRepository } from "../Services/ApiClientRepository";
|
||||||
import {
|
import {
|
||||||
AdminRoomMessage,
|
AdminRoomMessage,
|
||||||
WorldFullWarningToRoomMessage,
|
WorldFullWarningToRoomMessage,
|
||||||
RefreshRoomPromptMessage,
|
RefreshRoomPromptMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
|
import { adminToken } from "../Middleware/AdminToken";
|
||||||
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
|
|
||||||
export class AdminController extends BaseController {
|
export class AdminController extends BaseHttpController {
|
||||||
constructor(private App: TemplatedApp) {
|
routes() {
|
||||||
super();
|
|
||||||
this.App = App;
|
|
||||||
this.receiveGlobalMessagePrompt();
|
this.receiveGlobalMessagePrompt();
|
||||||
this.receiveRoomEditionPrompt();
|
this.receiveRoomEditionPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /room/refresh:
|
||||||
|
* post:
|
||||||
|
* description: Forces anyone out of the room. The request must be authenticated with the "admin-token" header.
|
||||||
|
* parameters:
|
||||||
|
* - name: "admin-token"
|
||||||
|
* in: "header"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* description: TODO - move this to a classic "Authorization" header!
|
||||||
|
* - name: "roomId"
|
||||||
|
* in: "body"
|
||||||
|
* description: "The ID (full URL) to the room"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Will always return "ok".
|
||||||
|
* example: "ok"
|
||||||
|
*/
|
||||||
receiveRoomEditionPrompt() {
|
receiveRoomEditionPrompt() {
|
||||||
this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => {
|
this.app.post("/room/refresh", { middlewares: [adminToken] }, async (req, res) => {
|
||||||
res.onAborted(() => {
|
const body = await req.json();
|
||||||
console.warn("/message request was aborted");
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = req.getHeader("admin-token");
|
|
||||||
const body = await res.json();
|
|
||||||
|
|
||||||
if (ADMIN_API_TOKEN === "") {
|
|
||||||
res.writeStatus("401 Unauthorized").end("No token configured!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (token !== ADMIN_API_TOKEN) {
|
|
||||||
console.error("Admin access refused for token: " + token);
|
|
||||||
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof body.roomId !== "string") {
|
if (typeof body.roomId !== "string") {
|
||||||
@ -58,41 +56,53 @@ export class AdminController extends BaseController {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.errorToResponse(err, res);
|
this.castErrorToResponse(err, res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.writeStatus("200");
|
res.send("ok");
|
||||||
res.end("ok");
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /message:
|
||||||
|
* post:
|
||||||
|
* description: Sends a message (or a world full message) to a number of rooms.
|
||||||
|
* parameters:
|
||||||
|
* - name: "admin-token"
|
||||||
|
* in: "header"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* description: TODO - move this to a classic "Authorization" header!
|
||||||
|
* - name: "text"
|
||||||
|
* in: "body"
|
||||||
|
* description: "The text of the message"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* - name: "type"
|
||||||
|
* in: "body"
|
||||||
|
* description: Either "capacity" or "message
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* - name: "targets"
|
||||||
|
* in: "body"
|
||||||
|
* description: The list of room IDs to target
|
||||||
|
* required: true
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: "https://play.workadventu.re/@/foo/bar/baz"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Will always return "ok".
|
||||||
|
* example: "ok"
|
||||||
|
*/
|
||||||
receiveGlobalMessagePrompt() {
|
receiveGlobalMessagePrompt() {
|
||||||
this.App.options("/message", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => {
|
this.app.post("/message", { middlewares: [adminToken] }, async (req, res) => {
|
||||||
res.onAborted(() => {
|
const body = await req.json();
|
||||||
console.warn("/message request was aborted");
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = req.getHeader("admin-token");
|
|
||||||
const body = await res.json();
|
|
||||||
|
|
||||||
if (ADMIN_API_TOKEN === "") {
|
|
||||||
res.writeStatus("401 Unauthorized").end("No token configured!");
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (token !== ADMIN_API_TOKEN) {
|
|
||||||
console.error("Admin access refused for token: " + token);
|
|
||||||
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof body.text !== "string") {
|
if (typeof body.text !== "string") {
|
||||||
@ -133,13 +143,11 @@ export class AdminController extends BaseController {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.errorToResponse(err, res);
|
this.castErrorToResponse(err, res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.writeStatus("200");
|
res.send("ok");
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end("ok");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
import { BaseController } from "./BaseController";
|
|
||||||
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
||||||
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { openIDClient } from "../Services/OpenIDClient";
|
import { openIDClient } from "../Services/OpenIDClient";
|
||||||
import { DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable";
|
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
|
||||||
import { RegisterData } from "../Messages/JsonMessages/RegisterData";
|
import { RegisterData } from "../Messages/JsonMessages/RegisterData";
|
||||||
|
|
||||||
export interface TokenInterface {
|
export interface TokenInterface {
|
||||||
userUuid: string;
|
userUuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthenticateController extends BaseController {
|
export class AuthenticateController extends BaseHttpController {
|
||||||
constructor(private App: TemplatedApp) {
|
routes() {
|
||||||
super();
|
|
||||||
this.openIDLogin();
|
this.openIDLogin();
|
||||||
this.openIDCallback();
|
this.openIDCallback();
|
||||||
this.register();
|
this.register();
|
||||||
@ -23,14 +21,41 @@ export class AuthenticateController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openIDLogin() {
|
openIDLogin() {
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /login-screen:
|
||||||
|
* get:
|
||||||
|
* description: Redirects the user to the OpenID login screen
|
||||||
|
* parameters:
|
||||||
|
* - name: "nonce"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* - name: "state"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* - name: "playUri"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: false
|
||||||
|
* type: "string"
|
||||||
|
* - name: "redirect"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: false
|
||||||
|
* type: "string"
|
||||||
|
* responses:
|
||||||
|
* 302:
|
||||||
|
* description: Redirects the user to the OpenID login screen
|
||||||
|
*
|
||||||
|
*/
|
||||||
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.get("/login-screen", async (res: HttpResponse, req: HttpRequest) => {
|
this.app.get("/login-screen", async (req, res) => {
|
||||||
res.onAborted(() => {
|
|
||||||
console.warn("/message request was aborted");
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { nonce, state, playUri, redirect } = parse(req.getQuery());
|
const { nonce, state, playUri, redirect } = parse(req.path_query);
|
||||||
if (!state || !nonce) {
|
if (!state || !nonce) {
|
||||||
throw new Error("missing state and nonce URL parameters");
|
throw new Error("missing state and nonce URL parameters");
|
||||||
}
|
}
|
||||||
@ -41,24 +66,53 @@ export class AuthenticateController extends BaseController {
|
|||||||
playUri as string | undefined,
|
playUri as string | undefined,
|
||||||
redirect as string | undefined
|
redirect as string | undefined
|
||||||
);
|
);
|
||||||
res.writeStatus("302");
|
res.status(302);
|
||||||
res.writeHeader("Location", loginUri);
|
res.setHeader("Location", loginUri);
|
||||||
return res.end();
|
return res.send("");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("openIDLogin => e", e);
|
console.error("openIDLogin => e", e);
|
||||||
return this.errorToResponse(e, res);
|
this.castErrorToResponse(e, res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openIDCallback() {
|
openIDCallback() {
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /login-callback:
|
||||||
|
* get:
|
||||||
|
* description: TODO
|
||||||
|
* parameters:
|
||||||
|
* - name: "nonce"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* - name: "state"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* - name: "playUri"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: false
|
||||||
|
* type: "string"
|
||||||
|
* - name: "redirect"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: false
|
||||||
|
* type: "string"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: TODO
|
||||||
|
*
|
||||||
|
*/
|
||||||
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.get("/login-callback", async (res: HttpResponse, req: HttpRequest) => {
|
this.app.get("/login-callback", async (req, res) => {
|
||||||
res.onAborted(() => {
|
const IPAddress = req.header("x-forwarded-for");
|
||||||
console.warn("/message request was aborted");
|
const { code, nonce, token, playUri } = parse(req.path_query);
|
||||||
});
|
|
||||||
const IPAddress = req.getHeader("x-forwarded-for");
|
|
||||||
const { code, nonce, token, playUri } = parse(req.getQuery());
|
|
||||||
try {
|
try {
|
||||||
//verify connected by token
|
//verify connected by token
|
||||||
if (token != undefined) {
|
if (token != undefined) {
|
||||||
@ -77,31 +131,22 @@ export class AuthenticateController extends BaseController {
|
|||||||
//if not nonce and code, user connected in anonymous
|
//if not nonce and code, user connected in anonymous
|
||||||
//get data with identifier and return token
|
//get data with identifier and return token
|
||||||
if (!code && !nonce) {
|
if (!code && !nonce) {
|
||||||
res.writeStatus("200");
|
return res.json({ ...resUserData, authToken: token });
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.writeHeader("Content-Type", "application/json");
|
|
||||||
return res.end(JSON.stringify({ ...resUserData, authToken: token }));
|
|
||||||
}
|
}
|
||||||
console.error("Token cannot to be check on OpenId provider");
|
console.error("Token cannot to be check on OpenId provider");
|
||||||
res.writeStatus("500");
|
res.status(500);
|
||||||
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL);
|
res.send("User cannot to be connected on openid provider");
|
||||||
res.end("User cannot to be connected on openid provider");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
|
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
|
||||||
res.writeStatus("200");
|
return res.json({
|
||||||
this.addCorsHeaders(res);
|
...resCheckTokenAuth,
|
||||||
res.writeHeader("Content-Type", "application/json");
|
...resUserData,
|
||||||
return res.end(
|
authToken: token,
|
||||||
JSON.stringify({
|
username: authTokenData?.username,
|
||||||
...resCheckTokenAuth,
|
locale: authTokenData?.locale,
|
||||||
...resUserData,
|
});
|
||||||
authToken: token,
|
|
||||||
username: authTokenData?.username,
|
|
||||||
locale: authTokenData?.locale,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.info("User was not connected", err);
|
console.info("User was not connected", err);
|
||||||
}
|
}
|
||||||
@ -114,9 +159,8 @@ export class AuthenticateController extends BaseController {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
//if no access on openid provider, return error
|
//if no access on openid provider, return error
|
||||||
console.error("User cannot to be connected on OpenId provider => ", err);
|
console.error("User cannot to be connected on OpenId provider => ", err);
|
||||||
res.writeStatus("500");
|
res.status(500);
|
||||||
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL);
|
res.send("User cannot to be connected on openid provider");
|
||||||
res.end("User cannot to be connected on openid provider");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const email = userInfo.email || userInfo.sub;
|
const email = userInfo.email || userInfo.sub;
|
||||||
@ -134,25 +178,32 @@ export class AuthenticateController extends BaseController {
|
|||||||
//This is very important to create User Local in LocalStorage in WorkAdventure
|
//This is very important to create User Local in LocalStorage in WorkAdventure
|
||||||
const data = await this.getUserByUserIdentifier(email, playUri as string, IPAddress);
|
const data = await this.getUserByUserIdentifier(email, playUri as string, IPAddress);
|
||||||
|
|
||||||
res.writeStatus("200");
|
return res.json({ ...data, authToken, username: userInfo?.username, locale: userInfo?.locale });
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.writeHeader("Content-Type", "application/json");
|
|
||||||
return res.end(
|
|
||||||
JSON.stringify({ ...data, authToken, username: userInfo?.username, locale: userInfo?.locale })
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("openIDCallback => ERROR", e);
|
console.error("openIDCallback => ERROR", e);
|
||||||
return this.errorToResponse(e, res);
|
return this.castErrorToResponse(e, res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /logout-callback:
|
||||||
|
* get:
|
||||||
|
* description: TODO
|
||||||
|
* parameters:
|
||||||
|
* - name: "token"
|
||||||
|
* in: "query"
|
||||||
|
* description: "todo"
|
||||||
|
* required: false
|
||||||
|
* type: "string"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: TODO
|
||||||
|
*
|
||||||
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.get("/logout-callback", async (res: HttpResponse, req: HttpRequest) => {
|
this.app.get("/logout-callback", async (req, res) => {
|
||||||
res.onAborted(() => {
|
const { token } = parse(req.path_query);
|
||||||
console.warn("/message request was aborted");
|
|
||||||
});
|
|
||||||
|
|
||||||
const { token } = parse(req.getQuery());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
|
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
|
||||||
@ -162,29 +213,65 @@ export class AuthenticateController extends BaseController {
|
|||||||
await openIDClient.logoutUser(authTokenData.accessToken);
|
await openIDClient.logoutUser(authTokenData.accessToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("openIDCallback => logout-callback", error);
|
console.error("openIDCallback => logout-callback", error);
|
||||||
} finally {
|
|
||||||
res.writeStatus("200");
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
// eslint-disable-next-line no-unsafe-finally
|
|
||||||
return res.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res.status(200).send("");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Try to login with an admin token
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /register:
|
||||||
|
* post:
|
||||||
|
* description: Try to login with an admin token
|
||||||
|
* parameters:
|
||||||
|
* - name: "organizationMemberToken"
|
||||||
|
* in: "body"
|
||||||
|
* description: "A token allowing a user to connect to a given world"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: The details of the logged user
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* authToken:
|
||||||
|
* type: string
|
||||||
|
* description: A unique identification JWT token
|
||||||
|
* userUuid:
|
||||||
|
* type: string
|
||||||
|
* description: Unique user ID
|
||||||
|
* email:
|
||||||
|
* type: string|null
|
||||||
|
* description: The email of the user
|
||||||
|
* example: john.doe@example.com
|
||||||
|
* roomUrl:
|
||||||
|
* type: string
|
||||||
|
* description: The room URL to connect to
|
||||||
|
* example: https://play.workadventu.re/@/foo/bar/baz
|
||||||
|
* organizationMemberToken:
|
||||||
|
* type: string|null
|
||||||
|
* description: TODO- unclear. It seems to be sent back from the request?
|
||||||
|
* example: ???
|
||||||
|
* mapUrlStart:
|
||||||
|
* type: string
|
||||||
|
* description: TODO- unclear. I cannot find any use of this
|
||||||
|
* example: ???
|
||||||
|
* messages:
|
||||||
|
* type: array
|
||||||
|
* description: The list of messages to be displayed when the user logs?
|
||||||
|
* example: ???
|
||||||
|
*/
|
||||||
private register() {
|
private register() {
|
||||||
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
this.app.options("/register", {}, (req, res) => {
|
||||||
this.addCorsHeaders(res);
|
res.status(200).send("");
|
||||||
|
|
||||||
res.end();
|
|
||||||
});
|
});
|
||||||
|
this.app.post("/register", (req, res) => {
|
||||||
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
(async () => {
|
(async () => {
|
||||||
res.onAborted(() => {
|
const param = await req.json();
|
||||||
console.warn("Login request was aborted");
|
|
||||||
});
|
|
||||||
const param = await res.json();
|
|
||||||
|
|
||||||
//todo: what to do if the organizationMemberToken is already used?
|
//todo: what to do if the organizationMemberToken is already used?
|
||||||
const organizationMemberToken: string | null = param.organizationMemberToken;
|
const organizationMemberToken: string | null = param.organizationMemberToken;
|
||||||
@ -196,71 +283,81 @@ export class AuthenticateController extends BaseController {
|
|||||||
const email = data.email;
|
const email = data.email;
|
||||||
const roomUrl = data.roomUrl;
|
const roomUrl = data.roomUrl;
|
||||||
const mapUrlStart = data.mapUrlStart;
|
const mapUrlStart = data.mapUrlStart;
|
||||||
const textures = data.textures;
|
|
||||||
|
|
||||||
const authToken = jwtTokenManager.createAuthToken(email || userUuid);
|
const authToken = jwtTokenManager.createAuthToken(email || userUuid);
|
||||||
res.writeStatus("200 OK");
|
res.json({
|
||||||
this.addCorsHeaders(res);
|
authToken,
|
||||||
res.writeHeader("Content-Type", "application/json");
|
userUuid,
|
||||||
res.end(
|
email,
|
||||||
JSON.stringify({
|
roomUrl,
|
||||||
authToken,
|
mapUrlStart,
|
||||||
userUuid,
|
organizationMemberToken,
|
||||||
email,
|
} as RegisterData);
|
||||||
roomUrl,
|
|
||||||
mapUrlStart,
|
|
||||||
organizationMemberToken,
|
|
||||||
textures,
|
|
||||||
} as RegisterData)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("register => ERROR", e);
|
console.error("register => ERROR", e);
|
||||||
this.errorToResponse(e, res);
|
this.castErrorToResponse(e, res);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//permit to login on application. Return token to connect on Websocket IO.
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /anonymLogin:
|
||||||
|
* post:
|
||||||
|
* description: Generates an "anonymous" JWT token allowing to connect to WorkAdventure anonymously.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: The details of the logged user
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* authToken:
|
||||||
|
* type: string
|
||||||
|
* description: A unique identification JWT token
|
||||||
|
* userUuid:
|
||||||
|
* type: string
|
||||||
|
* description: Unique user ID
|
||||||
|
* 403:
|
||||||
|
* description: Anonymous login is disabled at the configuration level (environment variable DISABLE_ANONYMOUS = true)
|
||||||
|
*/
|
||||||
private anonymLogin() {
|
private anonymLogin() {
|
||||||
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
this.app.post("/anonymLogin", (req, res) => {
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
res.onAborted(() => {
|
|
||||||
console.warn("Login request was aborted");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (DISABLE_ANONYMOUS) {
|
if (DISABLE_ANONYMOUS) {
|
||||||
res.writeStatus("403 FORBIDDEN");
|
res.status(403);
|
||||||
res.end();
|
return res;
|
||||||
} else {
|
} else {
|
||||||
const userUuid = v4();
|
const userUuid = v4();
|
||||||
const authToken = jwtTokenManager.createAuthToken(userUuid);
|
const authToken = jwtTokenManager.createAuthToken(userUuid);
|
||||||
res.writeStatus("200 OK");
|
return res.json({
|
||||||
this.addCorsHeaders(res);
|
authToken,
|
||||||
res.writeHeader("Content-Type", "application/json");
|
userUuid,
|
||||||
res.end(
|
});
|
||||||
JSON.stringify({
|
|
||||||
authToken,
|
|
||||||
userUuid,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /profile-callback:
|
||||||
|
* get:
|
||||||
|
* description: ???
|
||||||
|
* parameters:
|
||||||
|
* - name: "token"
|
||||||
|
* in: "query"
|
||||||
|
* description: "A JWT authentication token ???"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* responses:
|
||||||
|
* 302:
|
||||||
|
* description: Redirects the user to the profile screen of the admin
|
||||||
|
*/
|
||||||
profileCallback() {
|
profileCallback() {
|
||||||
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.get("/profile-callback", async (res: HttpResponse, req: HttpRequest) => {
|
this.app.get("/profile-callback", async (req, res) => {
|
||||||
res.onAborted(() => {
|
const { token } = parse(req.path_query);
|
||||||
console.warn("/message request was aborted");
|
|
||||||
});
|
|
||||||
const { token } = parse(req.getQuery());
|
|
||||||
try {
|
try {
|
||||||
//verify connected by token
|
//verify connected by token
|
||||||
if (token != undefined) {
|
if (token != undefined) {
|
||||||
@ -272,18 +369,18 @@ export class AuthenticateController extends BaseController {
|
|||||||
await openIDClient.checkTokenAuth(authTokenData.accessToken);
|
await openIDClient.checkTokenAuth(authTokenData.accessToken);
|
||||||
|
|
||||||
//get login profile
|
//get login profile
|
||||||
res.writeStatus("302");
|
res.status(302);
|
||||||
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
|
res.setHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
|
||||||
this.addCorsHeaders(res);
|
res.send("");
|
||||||
// eslint-disable-next-line no-unsafe-finally
|
return;
|
||||||
return res.end();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.errorToResponse(error, res);
|
this.castErrorToResponse(error, res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("profileCallback => ERROR", error);
|
console.error("profileCallback => ERROR", error);
|
||||||
this.errorToResponse(error, res);
|
this.castErrorToResponse(error, res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -311,7 +408,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
userRoomToken: undefined,
|
userRoomToken: undefined,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress);
|
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress, []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("openIDCallback => fetchMemberDataByUuid", err);
|
console.error("openIDCallback => fetchMemberDataByUuid", err);
|
||||||
}
|
}
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
import { HttpResponse } from "uWebSockets.js";
|
|
||||||
import { FRONT_URL } from "../Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
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", FRONT_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns any exception into a HTTP response (and logs the error)
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
protected errorToResponse(e: any, res: HttpResponse): void {
|
|
||||||
if (e && e.message) {
|
|
||||||
let url = e?.config?.url;
|
|
||||||
if (url !== undefined) {
|
|
||||||
url = " for URL: " + url;
|
|
||||||
} else {
|
|
||||||
url = "";
|
|
||||||
}
|
|
||||||
console.error("ERROR: " + e.message + url);
|
|
||||||
} else if (typeof e === "string") {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
if (e.stack) {
|
|
||||||
console.error(e.stack);
|
|
||||||
}
|
|
||||||
if (e.response) {
|
|
||||||
res.writeStatus(e.response.status + " " + e.response.statusText);
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end(
|
|
||||||
"An error occurred: " +
|
|
||||||
e.response.status +
|
|
||||||
" " +
|
|
||||||
(e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.writeStatus("500 Internal Server Error");
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end("An error occurred");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
47
pusher/src/Controller/BaseHttpController.ts
Normal file
47
pusher/src/Controller/BaseHttpController.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Server } from "hyper-express";
|
||||||
|
import Response from "hyper-express/types/components/http/Response";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export class BaseHttpController {
|
||||||
|
constructor(protected app: Server) {
|
||||||
|
this.routes();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected routes() {
|
||||||
|
/* Define routes on children */
|
||||||
|
}
|
||||||
|
|
||||||
|
protected castErrorToResponse(e: unknown, res: Response): void {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
let url: string | undefined;
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
url = e.config.url;
|
||||||
|
if (url !== undefined) {
|
||||||
|
url = " for URL: " + url;
|
||||||
|
} else {
|
||||||
|
url = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("ERROR: " + e.message + url);
|
||||||
|
console.error(e.stack);
|
||||||
|
} else if (typeof e === "string") {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axios.isAxiosError(e) && e.response) {
|
||||||
|
res.status(e.response.status);
|
||||||
|
res.send(
|
||||||
|
"An error occurred: " +
|
||||||
|
e.response.status +
|
||||||
|
" " +
|
||||||
|
(e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res.status(500);
|
||||||
|
res.send("An error occurred");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,51 +1,42 @@
|
|||||||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||||
import { IoSocketController } from "_Controller/IoSocketController";
|
|
||||||
import { stringify } from "circular-json";
|
import { stringify } from "circular-json";
|
||||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { App } from "../Server/sifrr.server";
|
|
||||||
import { socketManager } from "../Services/SocketManager";
|
import { socketManager } from "../Services/SocketManager";
|
||||||
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
|
|
||||||
export class DebugController {
|
export class DebugController extends BaseHttpController {
|
||||||
constructor(private App: App) {
|
routes() {
|
||||||
this.getDump();
|
this.app.get("/dump", (req, res) => {
|
||||||
}
|
const query = parse(req.path_query);
|
||||||
|
|
||||||
getDump() {
|
|
||||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
const query = parse(req.getQuery());
|
|
||||||
|
|
||||||
if (ADMIN_API_TOKEN === "") {
|
if (ADMIN_API_TOKEN === "") {
|
||||||
return res.writeStatus("401 Unauthorized").end("No token configured!");
|
return res.status(401).send("No token configured!");
|
||||||
}
|
}
|
||||||
if (query.token !== ADMIN_API_TOKEN) {
|
if (query.token !== ADMIN_API_TOKEN) {
|
||||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
return res.status(401).send("Invalid token sent!");
|
||||||
}
|
}
|
||||||
|
|
||||||
const worlds = Object.fromEntries(socketManager.getWorlds().entries());
|
const worlds = Object.fromEntries(socketManager.getWorlds().entries());
|
||||||
|
|
||||||
return res
|
return res.json(
|
||||||
.writeStatus("200 OK")
|
stringify(worlds, (key: unknown, value: unknown) => {
|
||||||
.writeHeader("Content-Type", "application/json")
|
if (value instanceof Map) {
|
||||||
.end(
|
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
stringify(worlds, (key: unknown, value: unknown) => {
|
for (const [mapKey, mapValue] of value.entries()) {
|
||||||
if (value instanceof Map) {
|
obj[mapKey] = mapValue;
|
||||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
for (const [mapKey, mapValue] of value.entries()) {
|
|
||||||
obj[mapKey] = mapValue;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
} else if (value instanceof Set) {
|
|
||||||
const obj: Array<unknown> = [];
|
|
||||||
for (const [setKey, setValue] of value.entries()) {
|
|
||||||
obj.push(setValue);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
})
|
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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
import { ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||||
import { GameRoomPolicyTypes, PusherRoom } from "../Model/PusherRoom";
|
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
||||||
import { PointInterface } from "../Model/Websocket/PointInterface";
|
import { PointInterface } from "../Model/Websocket/PointInterface";
|
||||||
import {
|
import {
|
||||||
SetPlayerDetailsMessage,
|
SetPlayerDetailsMessage,
|
||||||
@ -23,7 +23,6 @@ import {
|
|||||||
VariableMessage,
|
VariableMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
||||||
import { TemplatedApp } from "uWebSockets.js";
|
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { AdminSocketTokenData, jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
|
import { AdminSocketTokenData, jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
|
||||||
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
||||||
@ -32,15 +31,51 @@ import { emitInBatch } from "../Services/IoSocketHelpers";
|
|||||||
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||||
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
|
||||||
import { isAdminMessageInterface } from "../Model/Websocket/Admin/AdminMessages";
|
import { isAdminMessageInterface } from "../Model/Websocket/Admin/AdminMessages";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import { InvalidTokenError } from "../Controller/InvalidTokenError";
|
import { InvalidTokenError } from "../Controller/InvalidTokenError";
|
||||||
|
import HyperExpress from "hyper-express";
|
||||||
|
import { localWokaService } from "../Services/LocalWokaService";
|
||||||
|
import { WebSocket } from "uWebSockets.js";
|
||||||
|
import { WokaDetail } from "../Enum/PlayerTextures";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object passed between the "open" and the "upgrade" methods when opening a websocket
|
||||||
|
*/
|
||||||
|
interface UpgradeData {
|
||||||
|
// Data passed here is accessible on the "websocket" socket object.
|
||||||
|
rejected: false;
|
||||||
|
token: string;
|
||||||
|
userUuid: string;
|
||||||
|
IPAddress: string;
|
||||||
|
roomId: string;
|
||||||
|
name: string;
|
||||||
|
companion: CompanionMessage | undefined;
|
||||||
|
characterLayers: WokaDetail[];
|
||||||
|
messages: unknown[];
|
||||||
|
tags: string[];
|
||||||
|
visitCardUrl: string | null;
|
||||||
|
userRoomToken: string | undefined;
|
||||||
|
position: PointInterface;
|
||||||
|
viewport: {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpgradeFailedData {
|
||||||
|
rejected: true;
|
||||||
|
reason: "tokenInvalid" | "textureInvalid" | null;
|
||||||
|
message: string;
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class IoSocketController {
|
export class IoSocketController {
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
|
|
||||||
constructor(private readonly app: TemplatedApp) {
|
constructor(private readonly app: HyperExpress.compressors.TemplatedApp) {
|
||||||
this.ioConnection();
|
this.ioConnection();
|
||||||
if (ADMIN_SOCKETS_TOKEN) {
|
if (ADMIN_SOCKETS_TOKEN) {
|
||||||
this.adminRoomSocket();
|
this.adminRoomSocket();
|
||||||
@ -244,7 +279,7 @@ export class IoSocketController {
|
|||||||
let memberVisitCardUrl: string | null = null;
|
let memberVisitCardUrl: string | null = null;
|
||||||
let memberMessages: unknown;
|
let memberMessages: unknown;
|
||||||
let memberUserRoomToken: string | undefined;
|
let memberUserRoomToken: string | undefined;
|
||||||
let memberTextures: CharacterTexture[] = [];
|
let memberTextures: WokaDetail[] = [];
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
let userData: FetchMemberDataByUuidResponse = {
|
let userData: FetchMemberDataByUuidResponse = {
|
||||||
email: userIdentifier,
|
email: userIdentifier,
|
||||||
@ -256,10 +291,18 @@ export class IoSocketController {
|
|||||||
anonymous: true,
|
anonymous: true,
|
||||||
userRoomToken: undefined,
|
userRoomToken: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let characterLayerObjs: WokaDetail[];
|
||||||
|
|
||||||
if (ADMIN_API_URL) {
|
if (ADMIN_API_URL) {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
userData = await adminApi.fetchMemberDataByUuid(userIdentifier, roomId, IPAddress);
|
userData = await adminApi.fetchMemberDataByUuid(
|
||||||
|
userIdentifier,
|
||||||
|
roomId,
|
||||||
|
IPAddress,
|
||||||
|
characterLayers
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (Axios.isAxiosError(err)) {
|
if (Axios.isAxiosError(err)) {
|
||||||
if (err?.response?.status == 404) {
|
if (err?.response?.status == 404) {
|
||||||
@ -308,6 +351,8 @@ export class IoSocketController {
|
|||||||
) {
|
) {
|
||||||
throw new Error("Use the login URL to connect");
|
throw new Error("Use the login URL to connect");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
characterLayerObjs = memberTextures;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(
|
console.log(
|
||||||
"access not granted for user " +
|
"access not granted for user " +
|
||||||
@ -318,11 +363,31 @@ export class IoSocketController {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
throw new Error("User cannot access this world");
|
throw new Error("User cannot access this world");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const fetchedTextures = await localWokaService.fetchWokaDetails(characterLayers);
|
||||||
|
if (fetchedTextures === undefined) {
|
||||||
|
// The textures we want to use do not exist!
|
||||||
|
// We need to go in error.
|
||||||
|
res.upgrade(
|
||||||
|
{
|
||||||
|
rejected: true,
|
||||||
|
reason: "textureInvalid",
|
||||||
|
message: "",
|
||||||
|
roomId,
|
||||||
|
} as UpgradeFailedData,
|
||||||
|
websocketKey,
|
||||||
|
websocketProtocol,
|
||||||
|
websocketExtensions,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
characterLayerObjs = fetchedTextures;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate characterLayers objects from characterLayers string[]
|
// Generate characterLayers objects from characterLayers string[]
|
||||||
const characterLayerObjs: CharacterLayer[] =
|
/*const characterLayerObjs: CharacterLayer[] =
|
||||||
SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
|
SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);*/
|
||||||
|
|
||||||
if (upgradeAborted.aborted) {
|
if (upgradeAborted.aborted) {
|
||||||
console.log("Ouch! Client disconnected before we could upgrade it!");
|
console.log("Ouch! Client disconnected before we could upgrade it!");
|
||||||
@ -334,7 +399,7 @@ export class IoSocketController {
|
|||||||
res.upgrade(
|
res.upgrade(
|
||||||
{
|
{
|
||||||
// Data passed here is accessible on the "websocket" socket object.
|
// Data passed here is accessible on the "websocket" socket object.
|
||||||
url,
|
rejected: false,
|
||||||
token,
|
token,
|
||||||
userUuid: userData.userUuid,
|
userUuid: userData.userUuid,
|
||||||
IPAddress,
|
IPAddress,
|
||||||
@ -346,7 +411,6 @@ export class IoSocketController {
|
|||||||
tags: memberTags,
|
tags: memberTags,
|
||||||
visitCardUrl: memberVisitCardUrl,
|
visitCardUrl: memberVisitCardUrl,
|
||||||
userRoomToken: memberUserRoomToken,
|
userRoomToken: memberUserRoomToken,
|
||||||
textures: memberTextures,
|
|
||||||
position: {
|
position: {
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
@ -359,7 +423,7 @@ export class IoSocketController {
|
|||||||
bottom,
|
bottom,
|
||||||
left,
|
left,
|
||||||
},
|
},
|
||||||
},
|
} as UpgradeData,
|
||||||
/* Spell these correctly */
|
/* Spell these correctly */
|
||||||
websocketKey,
|
websocketKey,
|
||||||
websocketProtocol,
|
websocketProtocol,
|
||||||
@ -374,7 +438,7 @@ export class IoSocketController {
|
|||||||
reason: e instanceof InvalidTokenError ? tokenInvalidException : null,
|
reason: e instanceof InvalidTokenError ? tokenInvalidException : null,
|
||||||
message: e.message,
|
message: e.message,
|
||||||
roomId,
|
roomId,
|
||||||
},
|
} as UpgradeFailedData,
|
||||||
websocketKey,
|
websocketKey,
|
||||||
websocketProtocol,
|
websocketProtocol,
|
||||||
websocketExtensions,
|
websocketExtensions,
|
||||||
@ -387,7 +451,7 @@ export class IoSocketController {
|
|||||||
reason: null,
|
reason: null,
|
||||||
message: "500 Internal Server Error",
|
message: "500 Internal Server Error",
|
||||||
roomId,
|
roomId,
|
||||||
},
|
} as UpgradeFailedData,
|
||||||
websocketKey,
|
websocketKey,
|
||||||
websocketProtocol,
|
websocketProtocol,
|
||||||
websocketExtensions,
|
websocketExtensions,
|
||||||
@ -398,20 +462,23 @@ export class IoSocketController {
|
|||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
/* Handlers */
|
/* Handlers */
|
||||||
open: (ws) => {
|
open: (_ws: WebSocket) => {
|
||||||
|
const ws = _ws as WebSocket & (UpgradeData | UpgradeFailedData);
|
||||||
if (ws.rejected === true) {
|
if (ws.rejected === true) {
|
||||||
// If there is a room in the error, let's check if we need to clean it.
|
// If there is a room in the error, let's check if we need to clean it.
|
||||||
if (ws.roomId) {
|
if (ws.roomId) {
|
||||||
socketManager.deleteRoomIfEmptyFromId(ws.roomId as string);
|
socketManager.deleteRoomIfEmptyFromId(ws.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//FIX ME to use status code
|
//FIX ME to use status code
|
||||||
if (ws.reason === tokenInvalidException) {
|
if (ws.reason === tokenInvalidException) {
|
||||||
socketManager.emitTokenExpiredMessage(ws);
|
socketManager.emitTokenExpiredMessage(ws);
|
||||||
|
} else if (ws.reason === "textureInvalid") {
|
||||||
|
socketManager.emitInvalidTextureMessage(ws);
|
||||||
} else if (ws.message === "World is full") {
|
} else if (ws.message === "World is full") {
|
||||||
socketManager.emitWorldFullMessage(ws);
|
socketManager.emitWorldFullMessage(ws);
|
||||||
} else {
|
} else {
|
||||||
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
|
socketManager.emitConnexionErrorMessage(ws, ws.message);
|
||||||
}
|
}
|
||||||
setTimeout(() => ws.close(), 0);
|
setTimeout(() => ws.close(), 0);
|
||||||
return;
|
return;
|
||||||
@ -535,7 +602,6 @@ export class IoSocketController {
|
|||||||
client.name = ws.name;
|
client.name = ws.name;
|
||||||
client.tags = ws.tags;
|
client.tags = ws.tags;
|
||||||
client.visitCardUrl = ws.visitCardUrl;
|
client.visitCardUrl = ws.visitCardUrl;
|
||||||
client.textures = ws.textures;
|
|
||||||
client.characterLayers = ws.characterLayers;
|
client.characterLayers = ws.characterLayers;
|
||||||
client.companion = ws.companion;
|
client.companion = ws.companion;
|
||||||
client.roomId = ws.roomId;
|
client.roomId = ws.roomId;
|
||||||
|
@ -1,41 +1,101 @@
|
|||||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
|
||||||
import { BaseController } from "./BaseController";
|
|
||||||
import { parse } from "query-string";
|
|
||||||
import { adminApi } from "../Services/AdminApi";
|
import { adminApi } from "../Services/AdminApi";
|
||||||
import { ADMIN_API_URL, DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
|
||||||
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
||||||
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
||||||
import { socketManager } from "../Services/SocketManager";
|
|
||||||
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
import { v4 } from "uuid";
|
|
||||||
import { InvalidTokenError } from "./InvalidTokenError";
|
import { InvalidTokenError } from "./InvalidTokenError";
|
||||||
|
import { parse } from "query-string";
|
||||||
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
|
|
||||||
export class MapController extends BaseController {
|
export class MapController extends BaseHttpController {
|
||||||
constructor(private App: TemplatedApp) {
|
|
||||||
super();
|
|
||||||
this.App = App;
|
|
||||||
this.getMapUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a map mapping map name to file name of the map
|
// Returns a map mapping map name to file name of the map
|
||||||
getMapUrl() {
|
routes() {
|
||||||
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
|
/**
|
||||||
this.addCorsHeaders(res);
|
* @openapi
|
||||||
res.end();
|
* /map:
|
||||||
});
|
* get:
|
||||||
|
* description: Returns a map mapping map name to file name of the map
|
||||||
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
|
* produces:
|
||||||
res.onAborted(() => {
|
* - "application/json"
|
||||||
console.warn("/map request was aborted");
|
* parameters:
|
||||||
});
|
* - name: "playUri"
|
||||||
|
* in: "query"
|
||||||
const query = parse(req.getQuery());
|
* description: "The full URL of WorkAdventure to load this map"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* - name: "authToken"
|
||||||
|
* in: "query"
|
||||||
|
* description: "The authentication token"
|
||||||
|
* required: true
|
||||||
|
* type: "string"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: The details of the map
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - mapUrl
|
||||||
|
* - policy_type
|
||||||
|
* - tags
|
||||||
|
* - textures
|
||||||
|
* - authenticationMandatory
|
||||||
|
* - roomSlug
|
||||||
|
* - contactPage
|
||||||
|
* - group
|
||||||
|
* properties:
|
||||||
|
* mapUrl:
|
||||||
|
* type: string
|
||||||
|
* description: The full URL to the JSON map file
|
||||||
|
* example: https://myuser.github.io/myrepo/map.json
|
||||||
|
* policy_type:
|
||||||
|
* type: integer
|
||||||
|
* description: ANONYMOUS_POLICY = 1, MEMBERS_ONLY_POLICY = 2, USE_TAGS_POLICY= 3
|
||||||
|
* example: 1
|
||||||
|
* tags:
|
||||||
|
* type: array
|
||||||
|
* description: The list of tags required to enter this room
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* example: speaker
|
||||||
|
* authenticationMandatory:
|
||||||
|
* type: boolean|null
|
||||||
|
* description: Whether the authentication is mandatory or not for this map.
|
||||||
|
* example: true
|
||||||
|
* roomSlug:
|
||||||
|
* type: string
|
||||||
|
* description: The slug of the room
|
||||||
|
* deprecated: true
|
||||||
|
* example: foo
|
||||||
|
* contactPage:
|
||||||
|
* type: string|null
|
||||||
|
* description: The URL to the contact page
|
||||||
|
* example: https://mycompany.com/contact-us
|
||||||
|
* group:
|
||||||
|
* type: string|null
|
||||||
|
* description: The group this room is part of (maps the notion of "world" in WorkAdventure SAAS)
|
||||||
|
* example: myorg/myworld
|
||||||
|
* iframeAuthentication:
|
||||||
|
* type: string|null
|
||||||
|
* description: The URL of the authentication Iframe
|
||||||
|
* example: https://mycompany.com/authc
|
||||||
|
* expireOn:
|
||||||
|
* type: string|undefined
|
||||||
|
* description: The date (in ISO 8601 format) at which the room will expire
|
||||||
|
* example: 2022-11-05T08:15:30-05:00
|
||||||
|
* canReport:
|
||||||
|
* type: boolean|undefined
|
||||||
|
* description: Whether the "report" feature is enabled or not on this room
|
||||||
|
* example: true
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
this.app.get("/map", (req, res) => {
|
||||||
|
const query = parse(req.path_query);
|
||||||
if (typeof query.playUri !== "string") {
|
if (typeof query.playUri !== "string") {
|
||||||
console.error("Expected playUri parameter in /map endpoint");
|
console.error("Expected playUri parameter in /map endpoint");
|
||||||
res.writeStatus("400 Bad request");
|
res.status(400);
|
||||||
this.addCorsHeaders(res);
|
res.send("Expected playUri parameter");
|
||||||
res.end("Expected playUri parameter");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,30 +105,22 @@ export class MapController extends BaseController {
|
|||||||
|
|
||||||
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
|
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
res.writeStatus("404 Not Found");
|
res.status(404);
|
||||||
this.addCorsHeaders(res);
|
res.json({});
|
||||||
res.writeHeader("Content-Type", "application/json");
|
|
||||||
res.end(JSON.stringify({}));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapUrl = roomUrl.protocol + "//" + match[1];
|
const mapUrl = roomUrl.protocol + "//" + match[1];
|
||||||
|
|
||||||
res.writeStatus("200 OK");
|
res.json({
|
||||||
this.addCorsHeaders(res);
|
mapUrl,
|
||||||
res.writeHeader("Content-Type", "application/json");
|
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
|
||||||
res.end(
|
roomSlug: null, // Deprecated
|
||||||
JSON.stringify({
|
group: null,
|
||||||
mapUrl,
|
tags: [],
|
||||||
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
|
contactPage: null,
|
||||||
roomSlug: null, // Deprecated
|
authenticationMandatory: DISABLE_ANONYMOUS,
|
||||||
group: null,
|
} as MapDetailsData);
|
||||||
tags: [],
|
|
||||||
textures: [],
|
|
||||||
contactPage: null,
|
|
||||||
authenticationMandatory: DISABLE_ANONYMOUS,
|
|
||||||
} as MapDetailsData)
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -90,12 +142,12 @@ export class MapController extends BaseController {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof InvalidTokenError) {
|
if (e instanceof InvalidTokenError) {
|
||||||
// The token was not good, redirect user on login page
|
// The token was not good, redirect user on login page
|
||||||
res.writeStatus("401 Unauthorized");
|
res.status(401);
|
||||||
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL);
|
res.send("Token decrypted error");
|
||||||
res.end("Token decrypted error");
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
return this.errorToResponse(e, res);
|
this.castErrorToResponse(e, res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,12 +158,10 @@ export class MapController extends BaseController {
|
|||||||
mapDetails.authenticationMandatory = true;
|
mapDetails.authenticationMandatory = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.writeStatus("200 OK");
|
res.json(mapDetails);
|
||||||
this.addCorsHeaders(res);
|
return;
|
||||||
res.writeHeader("Content-Type", "application/json");
|
|
||||||
res.end(JSON.stringify(mapDetails));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToResponse(e, res);
|
this.castErrorToResponse(e, res);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
@ -1,26 +1,13 @@
|
|||||||
import { BaseController } from "./BaseController";
|
|
||||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { openIDClient } from "../Services/OpenIDClient";
|
import { openIDClient } from "../Services/OpenIDClient";
|
||||||
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
|
||||||
import { adminApi } from "../Services/AdminApi";
|
|
||||||
import { OPID_CLIENT_ISSUER } from "../Enum/EnvironmentVariable";
|
import { OPID_CLIENT_ISSUER } from "../Enum/EnvironmentVariable";
|
||||||
import { IntrospectionResponse } from "openid-client";
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
|
|
||||||
export class OpenIdProfileController extends BaseController {
|
export class OpenIdProfileController extends BaseHttpController {
|
||||||
constructor(private App: TemplatedApp) {
|
routes() {
|
||||||
super();
|
|
||||||
this.profileOpenId();
|
|
||||||
}
|
|
||||||
|
|
||||||
profileOpenId() {
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.get("/profile", async (res: HttpResponse, req: HttpRequest) => {
|
this.app.get("/profile", async (req, res) => {
|
||||||
res.onAborted(() => {
|
const { accessToken } = parse(req.path_query);
|
||||||
console.warn("/message request was aborted");
|
|
||||||
});
|
|
||||||
|
|
||||||
const { accessToken } = parse(req.getQuery());
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw Error("Access token expected cannot to be check on Hydra");
|
throw Error("Access token expected cannot to be check on Hydra");
|
||||||
}
|
}
|
||||||
@ -29,16 +16,17 @@ export class OpenIdProfileController extends BaseController {
|
|||||||
if (!resCheckTokenAuth.email) {
|
if (!resCheckTokenAuth.email) {
|
||||||
throw new Error("Email was not found");
|
throw new Error("Email was not found");
|
||||||
}
|
}
|
||||||
res.end(
|
res.send(
|
||||||
this.buildHtml(
|
this.buildHtml(
|
||||||
OPID_CLIENT_ISSUER,
|
OPID_CLIENT_ISSUER,
|
||||||
resCheckTokenAuth.email as string,
|
resCheckTokenAuth.email as string,
|
||||||
resCheckTokenAuth.picture as string | undefined
|
resCheckTokenAuth.picture as string | undefined
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("profileCallback => ERROR", error);
|
console.error("profileCallback => ERROR", error);
|
||||||
this.errorToResponse(error, res);
|
this.castErrorToResponse(error, res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -64,13 +52,13 @@ export class OpenIdProfileController extends BaseController {
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section>
|
<section>
|
||||||
<img src="${pictureUrl ? pictureUrl : "/images/profile"}">
|
<img src="${pictureUrl ? pictureUrl : "/images/profile"}">
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
Profile validated by domain: <span style="font-weight: bold">${domain}</span>
|
Profile validated by domain: <span style="font-weight: bold">${domain}</span>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
Your email: <span style="font-weight: bold">${email}</span>
|
Your email: <span style="font-weight: bold">${email}</span>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
import { App } from "../Server/sifrr.server";
|
|
||||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
|
||||||
import { register, collectDefaultMetrics } from "prom-client";
|
import { register, collectDefaultMetrics } from "prom-client";
|
||||||
|
import { Server } from "hyper-express";
|
||||||
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
|
import Request from "hyper-express/types/components/http/Request";
|
||||||
|
import Response from "hyper-express/types/components/http/Response";
|
||||||
|
|
||||||
export class PrometheusController {
|
export class PrometheusController extends BaseHttpController {
|
||||||
constructor(private App: App) {
|
constructor(app: Server) {
|
||||||
|
super(app);
|
||||||
collectDefaultMetrics({
|
collectDefaultMetrics({
|
||||||
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
||||||
});
|
});
|
||||||
|
|
||||||
this.App.get("/metrics", this.metrics.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
routes() {
|
||||||
res.writeHeader("Content-Type", register.contentType);
|
this.app.get("/metrics", this.metrics.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private metrics(req: Request, res: Response): void {
|
||||||
|
res.setHeader("Content-Type", register.contentType);
|
||||||
res.end(register.metrics());
|
res.end(register.metrics());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
pusher/src/Controller/SwaggerController.ts
Normal file
64
pusher/src/Controller/SwaggerController.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
export class SwaggerController extends BaseHttpController {
|
||||||
|
routes() {
|
||||||
|
this.app.get("/openapi", (req, res) => {
|
||||||
|
// Let's load the module dynamically (it may not exist in prod because part of the -dev packages)
|
||||||
|
const swaggerJsdoc = require("swagger-jsdoc");
|
||||||
|
const options = {
|
||||||
|
swaggerDefinition: {
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: {
|
||||||
|
title: "WorkAdventure Pusher",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apis: ["./src/Controller/*.ts"],
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(swaggerJsdoc(options));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a LiveDirectory instance to virtualize directory with our assets
|
||||||
|
// @ts-ignore
|
||||||
|
const LiveDirectory = require("live-directory");
|
||||||
|
const LiveAssets = new LiveDirectory({
|
||||||
|
path: __dirname + "/../../node_modules/swagger-ui-dist", // We want to provide the system path to the folder. Avoid using relative paths.
|
||||||
|
keep: {
|
||||||
|
extensions: [".css", ".js", ".json", ".png", ".jpg", ".jpeg", ".html"], // We only want to serve files with these extensions
|
||||||
|
},
|
||||||
|
ignore: (path: string) => {
|
||||||
|
return path.startsWith("."); // We want to ignore dotfiles for safety
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create static serve route to serve index.html
|
||||||
|
this.app.get("/swagger-ui/", (request, response) => {
|
||||||
|
fs.readFile(__dirname + "/../../node_modules/swagger-ui-dist/index.html", "utf8", function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
return response.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
const result = data.replace(/https:\/\/petstore\.swagger\.io\/v2\/swagger.json/g, "/openapi");
|
||||||
|
|
||||||
|
response.send(result);
|
||||||
|
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create static serve route to serve frontend assets
|
||||||
|
this.app.get("/swagger-ui/*", (request, response) => {
|
||||||
|
// Strip away '/assets' from the request path to get asset relative path
|
||||||
|
// Lookup LiveFile instance from our LiveDirectory instance.
|
||||||
|
const path = request.path.replace("/swagger-ui", "");
|
||||||
|
const file = LiveAssets.get(path);
|
||||||
|
|
||||||
|
// Return a 404 if no asset/file exists on the derived path
|
||||||
|
if (file === undefined) return response.status(404).send("");
|
||||||
|
|
||||||
|
// Set appropriate mime-type and serve file buffer as response body
|
||||||
|
return response.type(file.extension).send(file.buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
52
pusher/src/Controller/WokaListController.ts
Normal file
52
pusher/src/Controller/WokaListController.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { BaseHttpController } from "./BaseHttpController";
|
||||||
|
import { wokaService } from "../Services/WokaService";
|
||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
|
|
||||||
|
export class WokaListController extends BaseHttpController {
|
||||||
|
routes() {
|
||||||
|
this.app.options("/woka/list/:roomUrl", {}, (req, res) => {
|
||||||
|
res.status(200).send("");
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
this.app.get("/woka/list/:roomUrl", {}, async (req, res) => {
|
||||||
|
const token = req.header("Authorization");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
res.status(401).send("Undefined authorization header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jwtData = jwtTokenManager.verifyJWTToken(token);
|
||||||
|
// Let's set the "uuid" param
|
||||||
|
req.params["uuid"] = jwtData.identifier;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Connection refused for token: " + token, e);
|
||||||
|
res.status(401).send("Invalid token sent");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isParameters = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
roomUrl: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!isParameters(req.path_parameters)) {
|
||||||
|
return res.status(400).send("Unknown parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomUrl = decodeURIComponent(req.path_parameters.roomUrl);
|
||||||
|
const wokaList = await wokaService.getWokaList(roomUrl, req.params["uuid"]);
|
||||||
|
|
||||||
|
if (!wokaList) {
|
||||||
|
return res.status(500).send("Error on getting woka list");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(wokaList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,9 @@ export const OPID_USERNAME_CLAIM = process.env.OPID_USERNAME_CLAIM || "username"
|
|||||||
export const OPID_LOCALE_CLAIM = process.env.OPID_LOCALE_CLAIM || "locale";
|
export const OPID_LOCALE_CLAIM = process.env.OPID_LOCALE_CLAIM || "locale";
|
||||||
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
|
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
|
||||||
|
|
||||||
|
// If set to the string "true", the /openapi route will return the OpenAPI definition and the swagger-ui/ route will display the documentation
|
||||||
|
export const ENABLE_OPENAPI_ENDPOINT = process.env.ENABLE_OPENAPI_ENDPOINT === "true";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SECRET_KEY,
|
SECRET_KEY,
|
||||||
API_URL,
|
API_URL,
|
||||||
|
48
pusher/src/Enum/PlayerTextures.ts
Normal file
48
pusher/src/Enum/PlayerTextures.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
//The list of all the player textures, both the default models and the partial textures used for customization
|
||||||
|
|
||||||
|
const wokaTexture = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
tintable: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WokaTexture = z.infer<typeof wokaTexture>;
|
||||||
|
|
||||||
|
const wokaTextureCollection = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
textures: z.array(wokaTexture),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WokaTextureCollection = z.infer<typeof wokaTextureCollection>;
|
||||||
|
|
||||||
|
const wokaPartType = z.object({
|
||||||
|
collections: z.array(wokaTextureCollection),
|
||||||
|
required: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WokaPartType = z.infer<typeof wokaPartType>;
|
||||||
|
|
||||||
|
export const wokaList = z.record(wokaPartType);
|
||||||
|
|
||||||
|
export type WokaList = z.infer<typeof wokaList>;
|
||||||
|
|
||||||
|
export const wokaPartNames = ["woka", "body", "eyes", "hair", "clothes", "hat", "accessory"];
|
||||||
|
|
||||||
|
export const isWokaDetail = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
id: tg.isString,
|
||||||
|
})
|
||||||
|
.withOptionalProperties({
|
||||||
|
url: tg.isString,
|
||||||
|
layer: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
|
export type WokaDetail = tg.GuardedType<typeof isWokaDetail>;
|
||||||
|
|
||||||
|
export type WokaDetailsResult = WokaDetail[];
|
22
pusher/src/Middleware/AdminToken.ts
Normal file
22
pusher/src/Middleware/AdminToken.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Request from "hyper-express/types/components/http/Request";
|
||||||
|
import Response from "hyper-express/types/components/http/Response";
|
||||||
|
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
|
||||||
|
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
export function adminToken(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
|
||||||
|
const token = req.header("admin-token");
|
||||||
|
|
||||||
|
if (ADMIN_API_TOKEN === "") {
|
||||||
|
res.status(401).end("No token configured!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token !== ADMIN_API_TOKEN) {
|
||||||
|
console.error("Admin access refused for token: " + token);
|
||||||
|
res.status(401).end("Incorrect token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
18
pusher/src/Middleware/Cors.ts
Normal file
18
pusher/src/Middleware/Cors.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Request from "hyper-express/types/components/http/Request";
|
||||||
|
import Response from "hyper-express/types/components/http/Response";
|
||||||
|
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
|
||||||
|
import { FRONT_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
export function cors(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
|
||||||
|
res.setHeader(
|
||||||
|
"access-control-allow-headers",
|
||||||
|
"Origin, X-Requested-With, Content-Type, Accept, Authorization, Pragma, Cache-Control"
|
||||||
|
);
|
||||||
|
res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
||||||
|
res.setHeader("access-control-allow-origin", FRONT_URL);
|
||||||
|
res.setHeader("access-control-allow-credentials", "true");
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
16
pusher/src/Middleware/HasToken.ts
Normal file
16
pusher/src/Middleware/HasToken.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Request from "hyper-express/types/components/http/Request";
|
||||||
|
import Response from "hyper-express/types/components/http/Response";
|
||||||
|
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
|
||||||
|
|
||||||
|
export function hasToken(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
|
||||||
|
const authorizationHeader = req.header("Authorization");
|
||||||
|
|
||||||
|
if (!authorizationHeader) {
|
||||||
|
res.status(401).send("Undefined authorization header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
@ -9,13 +9,13 @@ import {
|
|||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
SubMessage,
|
SubMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import { WebSocket } from "uWebSockets.js";
|
import { compressors } from "hyper-express";
|
||||||
import { ClientDuplexStream } from "grpc";
|
import { ClientDuplexStream } from "grpc";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
|
|
||||||
export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||||
|
|
||||||
export interface ExAdminSocketInterface extends WebSocket {
|
export interface ExAdminSocketInterface extends compressors.WebSocket {
|
||||||
adminConnection: AdminConnection;
|
adminConnection: AdminConnection;
|
||||||
disconnecting: boolean;
|
disconnecting: boolean;
|
||||||
}
|
}
|
||||||
|
@ -8,26 +8,21 @@ import {
|
|||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
SubMessage,
|
SubMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import { WebSocket } from "uWebSockets.js";
|
|
||||||
import { ClientDuplexStream } from "grpc";
|
import { ClientDuplexStream } from "grpc";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import { CharacterTexture } from "../../Messages/JsonMessages/CharacterTexture";
|
import { compressors } from "hyper-express";
|
||||||
|
import { WokaDetail } from "_Enum/PlayerTextures";
|
||||||
|
|
||||||
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||||
|
|
||||||
export interface CharacterLayer {
|
export interface ExSocketInterface extends compressors.WebSocket, Identificable {
|
||||||
name: string;
|
|
||||||
url: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExSocketInterface extends WebSocket, Identificable {
|
|
||||||
token: string;
|
token: string;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
//userId: number; // A temporary (autoincremented) identifier for this user
|
//userId: number; // A temporary (autoincremented) identifier for this user
|
||||||
userUuid: string; // A unique identifier for this user
|
userUuid: string; // A unique identifier for this user
|
||||||
IPAddress: string; // IP address
|
IPAddress: string; // IP address
|
||||||
name: string;
|
name: string;
|
||||||
characterLayers: CharacterLayer[];
|
characterLayers: WokaDetail[];
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
viewport: ViewportInterface;
|
viewport: ViewportInterface;
|
||||||
companion?: CompanionMessage;
|
companion?: CompanionMessage;
|
||||||
@ -41,7 +36,6 @@ export interface ExSocketInterface extends WebSocket, Identificable {
|
|||||||
messages: unknown;
|
messages: unknown;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
visitCardUrl: string | null;
|
visitCardUrl: string | null;
|
||||||
textures: CharacterTexture[];
|
|
||||||
backConnection: BackConnection;
|
backConnection: BackConnection;
|
||||||
listenedZones: Set<Zone>;
|
listenedZones: Set<Zone>;
|
||||||
userRoomToken: string | undefined;
|
userRoomToken: string | undefined;
|
||||||
|
@ -5,10 +5,11 @@ import {
|
|||||||
PointMessage,
|
PointMessage,
|
||||||
PositionMessage,
|
PositionMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import { CharacterLayer, ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
|
import { WokaDetail } from "_Enum/PlayerTextures";
|
||||||
|
|
||||||
export class ProtobufUtils {
|
export class ProtobufUtils {
|
||||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||||
@ -94,13 +95,16 @@ export class ProtobufUtils {
|
|||||||
return itemEventMessage;
|
return itemEventMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
public static toCharacterLayerMessages(characterLayers: WokaDetail[]): CharacterLayerMessage[] {
|
||||||
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
||||||
const message = new CharacterLayerMessage();
|
const message = new CharacterLayerMessage();
|
||||||
message.setName(characterLayer.name);
|
message.setName(characterLayer.id);
|
||||||
if (characterLayer.url) {
|
if (characterLayer.url) {
|
||||||
message.setUrl(characterLayer.url);
|
message.setUrl(characterLayer.url);
|
||||||
}
|
}
|
||||||
|
if (characterLayer.layer) {
|
||||||
|
message.setLayer(characterLayer.layer);
|
||||||
|
}
|
||||||
return message;
|
return message;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,109 +0,0 @@
|
|||||||
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,99 +0,0 @@
|
|||||||
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,36 +0,0 @@
|
|||||||
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,26 +1,34 @@
|
|||||||
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
|
||||||
import Axios from "axios";
|
import Axios, { AxiosResponse } from "axios";
|
||||||
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
|
|
||||||
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
|
||||||
import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
||||||
import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||||
import { AdminApiData, isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
|
import { AdminApiData, isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
|
||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import { isNumber } from "generic-type-guard";
|
||||||
|
import { isWokaDetail } from "../Enum/PlayerTextures";
|
||||||
|
import qs from "qs";
|
||||||
|
|
||||||
export interface AdminBannedData {
|
export interface AdminBannedData {
|
||||||
is_banned: boolean;
|
is_banned: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchMemberDataByUuidResponse {
|
const isFetchMemberDataByUuidResponse = new tg.IsInterface()
|
||||||
email: string;
|
.withProperties({
|
||||||
userUuid: string;
|
email: tg.isString,
|
||||||
tags: string[];
|
userUuid: tg.isString,
|
||||||
visitCardUrl: string | null;
|
tags: tg.isArray(tg.isString),
|
||||||
textures: CharacterTexture[];
|
visitCardUrl: tg.isNullable(tg.isString),
|
||||||
messages: unknown[];
|
textures: tg.isArray(isWokaDetail),
|
||||||
anonymous?: boolean;
|
messages: tg.isArray(tg.isUnknown),
|
||||||
userRoomToken: string | undefined;
|
})
|
||||||
}
|
.withOptionalProperties({
|
||||||
|
anonymous: tg.isBoolean,
|
||||||
|
userRoomToken: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
|
||||||
|
export type FetchMemberDataByUuidResponse = tg.GuardedType<typeof isFetchMemberDataByUuidResponse>;
|
||||||
|
|
||||||
class AdminApi {
|
class AdminApi {
|
||||||
/**
|
/**
|
||||||
@ -48,15 +56,25 @@ class AdminApi {
|
|||||||
async fetchMemberDataByUuid(
|
async fetchMemberDataByUuid(
|
||||||
userIdentifier: string | null,
|
userIdentifier: string | null,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
ipAddress: string
|
ipAddress: string,
|
||||||
|
characterLayers: string[]
|
||||||
): Promise<FetchMemberDataByUuidResponse> {
|
): Promise<FetchMemberDataByUuidResponse> {
|
||||||
if (!ADMIN_API_URL) {
|
if (!ADMIN_API_URL) {
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
}
|
}
|
||||||
const res = await Axios.get(ADMIN_API_URL + "/api/room/access", {
|
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/room/access", {
|
||||||
params: { userIdentifier, roomId, ipAddress },
|
params: { userIdentifier, roomId, ipAddress, characterLayers },
|
||||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||||
|
paramsSerializer: (p) => {
|
||||||
|
return qs.stringify(p, { arrayFormat: "brackets" });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
if (!isFetchMemberDataByUuidResponse(res.data)) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid answer received from the admin for the /api/room/access endpoint. Received: " +
|
||||||
|
JSON.stringify(res.data)
|
||||||
|
);
|
||||||
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
29
pusher/src/Services/AdminWokaService.ts
Normal file
29
pusher/src/Services/AdminWokaService.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import { wokaList, WokaList } from "../Enum/PlayerTextures";
|
||||||
|
import { WokaServiceInterface } from "./WokaServiceInterface";
|
||||||
|
|
||||||
|
class AdminWokaService implements WokaServiceInterface {
|
||||||
|
/**
|
||||||
|
* Returns the list of all available Wokas for the current user.
|
||||||
|
*/
|
||||||
|
getWokaList(roomUrl: string, token: string): Promise<WokaList | undefined> {
|
||||||
|
return axios
|
||||||
|
.get<unknown, AxiosResponse<unknown>>(`${ADMIN_API_URL}/api/woka/list`, {
|
||||||
|
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||||
|
params: {
|
||||||
|
roomUrl,
|
||||||
|
uuid: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
return wokaList.parse(res.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`Cannot get woka list from admin API with token: ${token}`, err);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adminWokaService = new AdminWokaService();
|
74
pusher/src/Services/LocalWokaService.ts
Normal file
74
pusher/src/Services/LocalWokaService.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { WokaDetail, WokaDetailsResult, WokaList, wokaPartNames } from "../Enum/PlayerTextures";
|
||||||
|
import { WokaServiceInterface } from "./WokaServiceInterface";
|
||||||
|
|
||||||
|
class LocalWokaService implements WokaServiceInterface {
|
||||||
|
/**
|
||||||
|
* Returns the list of all available Wokas & Woka Parts for the current user.
|
||||||
|
*/
|
||||||
|
async getWokaList(roomId: string, token: string): Promise<WokaList | undefined> {
|
||||||
|
const wokaData: WokaList = await require("../../data/woka.json");
|
||||||
|
if (!wokaData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return wokaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL of all the images for the given texture ids.
|
||||||
|
*
|
||||||
|
* Key: texture id
|
||||||
|
* Value: URL
|
||||||
|
*
|
||||||
|
* If one of the textures cannot be found, undefined is returned (and the user should be redirected to Woka choice page!)
|
||||||
|
*/
|
||||||
|
async fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> {
|
||||||
|
const wokaData: WokaList = await require("../../data/woka.json");
|
||||||
|
const textures = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
url: string;
|
||||||
|
layer: string;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
const searchIds = new Set(textureIds);
|
||||||
|
|
||||||
|
for (const part of wokaPartNames) {
|
||||||
|
const wokaPartType = wokaData[part];
|
||||||
|
if (!wokaPartType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const collection of wokaPartType.collections) {
|
||||||
|
for (const id of searchIds) {
|
||||||
|
const texture = collection.textures.find((texture) => texture.id === id);
|
||||||
|
|
||||||
|
if (texture) {
|
||||||
|
textures.set(id, {
|
||||||
|
url: texture.url,
|
||||||
|
layer: part,
|
||||||
|
});
|
||||||
|
searchIds.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textureIds.length !== textures.size) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const details: WokaDetail[] = [];
|
||||||
|
|
||||||
|
textures.forEach((value, key) => {
|
||||||
|
details.push({
|
||||||
|
id: key,
|
||||||
|
url: value.url,
|
||||||
|
layer: value.layer,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const localWokaService = new LocalWokaService();
|
@ -1,5 +1,5 @@
|
|||||||
import { PusherRoom } from "../Model/PusherRoom";
|
import { PusherRoom } from "../Model/PusherRoom";
|
||||||
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
|
import { ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
|
||||||
import {
|
import {
|
||||||
AdminMessage,
|
AdminMessage,
|
||||||
AdminPusherToBackMessage,
|
AdminPusherToBackMessage,
|
||||||
@ -38,6 +38,7 @@ import {
|
|||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
WorldFullMessage,
|
WorldFullMessage,
|
||||||
PlayerDetailsUpdatedMessage,
|
PlayerDetailsUpdatedMessage,
|
||||||
|
InvalidTextureMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
||||||
@ -52,7 +53,8 @@ import Debug from "debug";
|
|||||||
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||||
import { WebSocket } from "uWebSockets.js";
|
import { WebSocket } from "uWebSockets.js";
|
||||||
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||||
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
//import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
||||||
|
import { compressors } from "hyper-express";
|
||||||
|
|
||||||
const debug = Debug("socket");
|
const debug = Debug("socket");
|
||||||
|
|
||||||
@ -174,10 +176,13 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
|
|
||||||
for (const characterLayer of client.characterLayers) {
|
for (const characterLayer of client.characterLayers) {
|
||||||
const characterLayerMessage = new CharacterLayerMessage();
|
const characterLayerMessage = new CharacterLayerMessage();
|
||||||
characterLayerMessage.setName(characterLayer.name);
|
characterLayerMessage.setName(characterLayer.id);
|
||||||
if (characterLayer.url !== undefined) {
|
if (characterLayer.url !== undefined) {
|
||||||
characterLayerMessage.setUrl(characterLayer.url);
|
characterLayerMessage.setUrl(characterLayer.url);
|
||||||
}
|
}
|
||||||
|
if (characterLayer.layer !== undefined) {
|
||||||
|
characterLayerMessage.setLayer(characterLayer.layer);
|
||||||
|
}
|
||||||
|
|
||||||
joinRoomMessage.addCharacterlayer(characterLayerMessage);
|
joinRoomMessage.addCharacterlayer(characterLayerMessage);
|
||||||
}
|
}
|
||||||
@ -544,36 +549,6 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges the characterLayers received from the front (as an array of string) with the custom textures from the back.
|
|
||||||
*/
|
|
||||||
static mergeCharacterLayersAndCustomTextures(
|
|
||||||
characterLayers: string[],
|
|
||||||
memberTextures: CharacterTexture[]
|
|
||||||
): CharacterLayer[] {
|
|
||||||
const characterLayerObjs: CharacterLayer[] = [];
|
|
||||||
for (const characterLayer of characterLayers) {
|
|
||||||
if (characterLayer.startsWith("customCharacterTexture")) {
|
|
||||||
const customCharacterLayerId: number = +characterLayer.substr(22);
|
|
||||||
for (const memberTexture of memberTextures) {
|
|
||||||
if (memberTexture.id == customCharacterLayerId) {
|
|
||||||
characterLayerObjs.push({
|
|
||||||
name: characterLayer,
|
|
||||||
url: memberTexture.url,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
characterLayerObjs.push({
|
|
||||||
name: characterLayer,
|
|
||||||
url: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return characterLayerObjs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public onUserEnters(user: UserDescriptor, listener: ExSocketInterface): void {
|
public onUserEnters(user: UserDescriptor, listener: ExSocketInterface): void {
|
||||||
const subMessage = new SubMessage();
|
const subMessage = new SubMessage();
|
||||||
subMessage.setUserjoinedmessage(user.toUserJoinedMessage());
|
subMessage.setUserjoinedmessage(user.toUserJoinedMessage());
|
||||||
@ -619,7 +594,7 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
emitInBatch(listener, subMessage);
|
emitInBatch(listener, subMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitWorldFullMessage(client: WebSocket) {
|
public emitWorldFullMessage(client: compressors.WebSocket) {
|
||||||
const errorMessage = new WorldFullMessage();
|
const errorMessage = new WorldFullMessage();
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
@ -630,7 +605,7 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitTokenExpiredMessage(client: WebSocket) {
|
public emitTokenExpiredMessage(client: compressors.WebSocket) {
|
||||||
const errorMessage = new TokenExpiredMessage();
|
const errorMessage = new TokenExpiredMessage();
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
@ -641,7 +616,18 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitConnexionErrorMessage(client: WebSocket, message: string) {
|
public emitInvalidTextureMessage(client: compressors.WebSocket) {
|
||||||
|
const errorMessage = new InvalidTextureMessage();
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setInvalidtexturemessage(errorMessage);
|
||||||
|
|
||||||
|
if (!client.disconnecting) {
|
||||||
|
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public emitConnexionErrorMessage(client: compressors.WebSocket, message: string) {
|
||||||
const errorMessage = new WorldConnexionMessage();
|
const errorMessage = new WorldConnexionMessage();
|
||||||
errorMessage.setMessage(message);
|
errorMessage.setMessage(message);
|
||||||
|
|
||||||
|
5
pusher/src/Services/WokaService.ts
Normal file
5
pusher/src/Services/WokaService.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import { adminWokaService } from "./AdminWokaService";
|
||||||
|
import { localWokaService } from "./LocalWokaService";
|
||||||
|
|
||||||
|
export const wokaService = ADMIN_API_URL ? adminWokaService : localWokaService;
|
8
pusher/src/Services/WokaServiceInterface.ts
Normal file
8
pusher/src/Services/WokaServiceInterface.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { WokaDetailsResult, WokaList } from "../Enum/PlayerTextures";
|
||||||
|
|
||||||
|
export interface WokaServiceInterface {
|
||||||
|
/**
|
||||||
|
* Returns the list of all available Wokas for the current user.
|
||||||
|
*/
|
||||||
|
getWokaList(roomId: string, token: string): Promise<WokaList | undefined>;
|
||||||
|
}
|
1035
pusher/yarn.lock
1035
pusher/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user