Heavy changes: refactoring the pusher to always send the textures (and the front to accept them)

This commit is contained in:
David Négrier 2022-02-23 21:08:21 +01:00
parent d65fe0ee26
commit 378a95962a
31 changed files with 290 additions and 270 deletions

View File

@ -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();

View File

@ -134,7 +134,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);
@ -214,32 +214,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 {
// TODO: the local store should NOT be used as a buffer for all the texture we were authorized to have. Bad idea.
// Instead, it is the responsibility of the ADMIN to return the EXACT list of textures we can have in a given context
// + this list can change over time or over rooms.
// 1- a room could forbid a particular dress code. In this case, the user MUST change its skin.
// 2- a room can allow "external skins from other maps" => important: think about fediverse! => switch to URLs? (with a whitelist mechanism?) => but what about NFTs?
// Note: stocker des URL dans le localstorage pour les utilisateurs actuels: mauvaise idée (empêche de mettre l'URL à jour dans le futur) => en même temps, problème avec le portage de user d'un serveur à l'autre
// Réfléchir à une notion de "character server" ??
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"));
@ -265,7 +239,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.
@ -275,7 +249,7 @@ class ConnectionManager {
} }
public initBenchmark(): void { public initBenchmark(): void {
this.localUser = new LocalUser("", []); this.localUser = new LocalUser("");
} }
public connectToRoomSocket( public connectToRoomSocket(
@ -352,13 +326,13 @@ class ConnectionManager {
throw new Error("No Auth code provided"); throw new Error("No Auth code provided");
} }
} }
const { authToken, userUuid, textures, email } = await Axios.get(`${PUSHER_URL}/login-callback`, { const { authToken, userUuid, email } = await Axios.get(`${PUSHER_URL}/login-callback`, {
params: { code, nonce, token, playUri: this.currentRoom?.key }, params: { code, nonce, token, playUri: this.currentRoom?.key },
}).then((res) => { }).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;

View File

@ -2,6 +2,7 @@ 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"; import { PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
import { CharacterLayer } from "../../../back/src/Model/Websocket/CharacterLayer";
export interface PointInterface { export interface PointInterface {
x: number; x: number;
@ -83,6 +84,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 {

View File

@ -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;
@ -24,9 +25,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
) {}
} }

View File

@ -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");

View File

@ -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;
@ -336,12 +340,16 @@ export class RoomConnection implements RoomConnection {
this.userId = roomJoinedMessage.currentUserId; this.userId = roomJoinedMessage.currentUserId;
this.tags = roomJoinedMessage.tag; this.tags = roomJoinedMessage.tag;
this._userRoomToken = roomJoinedMessage.userRoomToken; this._userRoomToken = roomJoinedMessage.userRoomToken;
const characterLayers = roomJoinedMessage.characterLayer.map(
this.mapCharactgerLayerToBodyResourceDescription.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 +359,15 @@ export class RoomConnection implements RoomConnection {
this.closed = true; this.closed = true;
break; break;
} }
case "invalidTextureMessage": {
menuVisiblilityStore.set(false);
menuIconVisiblilityStore.set(false);
selectCharacterSceneVisibleStore.set(true);
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
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 +608,15 @@ export class RoomConnection implements RoomConnection {
}); });
}*/ }*/
private mapCharactgerLayerToBodyResourceDescription(
characterLayer: CharacterLayerMessage
): BodyResourceDescriptionInterface {
return {
name: 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 +624,9 @@ 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(
return { this.mapCharactgerLayerToBodyResourceDescription.bind(this)
name: characterLayer.name, );
img: characterLayer.url,
};
});
const companion = message.companion; const companion = message.companion;

View File

@ -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, [
{
name: "color_22",
img: "resources/customisation/character_color/character_color21.png",
},
{
name: "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);

View File

@ -1,7 +1,5 @@
//The list of all the player textures, both the default models and the partial textures used for customization //The list of all the player textures, both the default models and the partial textures used for customization
import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
export interface BodyResourceDescriptionListInterface { export interface BodyResourceDescriptionListInterface {
[key: string]: BodyResourceDescriptionInterface; [key: string]: BodyResourceDescriptionInterface;
} }
@ -12,6 +10,19 @@ export interface BodyResourceDescriptionInterface {
level?: number; level?: number;
} }
/**
* Temporary object to map layers to the old "level" concept.
*/
export const mapLayerToLevel = {
woka: -1,
body: 0,
eyes: 1,
hair: 2,
clothes: 3,
hat: 4,
accessory: 5,
};
enum PlayerTexturesKey { enum PlayerTexturesKey {
Accessory = "accessory", Accessory = "accessory",
Body = "body", Body = "body",

View File

@ -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, PlayerTextures } from "./PlayerTextures"; import { BodyResourceDescriptionInterface, mapLayerToLevel, PlayerTextures } from "./PlayerTextures";
import CancelablePromise from "cancelable-promise"; import CancelablePromise from "cancelable-promise";
export interface FrameConfig { export interface FrameConfig {
@ -28,13 +28,11 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio
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 +40,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.name)) {
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,9 +61,9 @@ 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.
@ -77,23 +74,6 @@ export const lazyLoadPlayerCharacterTextures = (
); );
}; };
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 = PlayerTextures.PLAYER_RESOURCES[textureName];
if (playerResource !== undefined) return playerResource;
for (let i = 0; i < PlayerTextures.LAYERS.length; i++) {
const playerResource = PlayerTextures.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,

View File

@ -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) {
@ -745,6 +740,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"));
@ -1689,16 +1692,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,

View File

@ -1,41 +1,45 @@
import { ResizableScene } from "./ResizableScene"; import { ResizableScene } from "./ResizableScene";
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import { loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
import type { CharacterTexture } from "../../Connexion/LocalUser"; import type { CharacterTexture } from "../../Connexion/LocalUser";
import type CancelablePromise from "cancelable-promise"; import type CancelablePromise from "cancelable-promise";
import { PlayerTextures } from "../Entity/PlayerTextures";
import { Loader } from "../Components/Loader";
import { CustomizeSceneName } from "./CustomizeScene";
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 = PlayerTextures.PLAYER_RESOURCES;
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 PlayerTextures.LAYERS) {
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;
}
} }

View File

@ -5,7 +5,7 @@ import Sprite = Phaser.GameObjects.Sprite;
import { gameManager } from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import { Loader } from "../Components/Loader"; import { Loader } from "../Components/Loader";
import { BodyResourceDescriptionInterface, PlayerTextures } from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import { AbstractCharacterScene } from "./AbstractCharacterScene"; import { AbstractCharacterScene } from "./AbstractCharacterScene";
import { areCharacterLayersValid } from "../../Connexion/LocalUser"; import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import { SelectCharacterSceneName } from "./SelectCharacterScene"; import { SelectCharacterSceneName } from "./SelectCharacterScene";
@ -32,14 +32,12 @@ export class CustomizeScene extends AbstractCharacterScene {
private moveVertically: number = 0; private moveVertically: number = 0;
private loader: Loader; private loader: Loader;
private playerTextures: PlayerTextures;
constructor() { constructor() {
super({ super({
key: CustomizeSceneName, key: CustomizeSceneName,
}); });
this.loader = new Loader(this); this.loader = new Loader(this);
this.playerTextures = new PlayerTextures();
} }
preload() { preload() {

View File

@ -33,7 +33,6 @@ export class SelectCharacterScene extends AbstractCharacterScene {
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
private loader: Loader; private loader: Loader;
private playerTextures: PlayerTextures;
constructor() { constructor() {
super({ super({

View File

@ -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");
});
});

View File

@ -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.
@ -12,7 +12,7 @@ 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), // textures: tg.isArray(isCharacterTexture),
}) })
.withOptionalProperties({ .withOptionalProperties({
messages: tg.isArray(tg.isUnknown), messages: tg.isArray(tg.isUnknown),

View File

@ -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>;

View File

@ -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";
import { isNumber } from "generic-type-guard"; import { isNumber } from "generic-type-guard";
/* /*
@ -12,7 +12,7 @@ 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), // 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),

View File

@ -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,7 @@ 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), // textures: tg.isArray(isCharacterTexture),
authToken: tg.isString, authToken: tg.isString,
}) })
.withOptionalProperties({ .withOptionalProperties({

View File

@ -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;
} }
} }

View File

@ -273,7 +273,6 @@ export class AuthenticateController extends BaseHttpController {
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.json({ res.json({
@ -283,7 +282,6 @@ export class AuthenticateController extends BaseHttpController {
roomUrl, roomUrl,
mapUrlStart, mapUrlStart,
organizationMemberToken, organizationMemberToken,
textures,
} as RegisterData); } as RegisterData);
} catch (e) { } catch (e) {
console.error("register => ERROR", e); console.error("register => ERROR", e);

View File

@ -1,4 +1,4 @@
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 } from "../Model/PusherRoom"; import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { PointInterface } from "../Model/Websocket/PointInterface"; import { PointInterface } from "../Model/Websocket/PointInterface";
import { import {
@ -31,11 +31,46 @@ 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 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;
@ -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,6 +291,9 @@ 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 {
@ -308,6 +346,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 +358,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 +394,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 +406,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 +418,7 @@ export class IoSocketController {
bottom, bottom,
left, left,
}, },
}, } as UpgradeData,
/* Spell these correctly */ /* Spell these correctly */
websocketKey, websocketKey,
websocketProtocol, websocketProtocol,
@ -374,7 +433,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 +446,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 +457,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 +597,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;

View File

@ -1,24 +1,13 @@
import { hasToken } from "../Middleware/HasToken"; import { hasToken } from "../Middleware/HasToken";
import { BaseHttpController } from "./BaseHttpController"; import { BaseHttpController } from "./BaseHttpController";
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable"; import { wokaService } from "../Services/WokaService";
import { adminWokaService } from "..//Services/AdminWokaService";
import { localWokaService } from "..//Services/LocalWokaService";
import { WokaServiceInterface } from "src/Services/WokaServiceInterface";
import { Server } from "hyper-express";
export class WokaListController extends BaseHttpController { export class WokaListController extends BaseHttpController {
private wokaService: WokaServiceInterface;
constructor(app: Server) {
super(app);
this.wokaService = ADMIN_API_URL ? adminWokaService : localWokaService;
}
routes() { routes() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.get("/woka-list", {}, async (req, res) => { this.app.get("/woka-list", {}, async (req, res) => {
const token = req.header("Authorization"); const token = req.header("Authorization");
const wokaList = await this.wokaService.getWokaList(token); const wokaList = await wokaService.getWokaList(token);
if (!wokaList) { if (!wokaList) {
return res.status(500).send("Error on getting woka list"); return res.status(500).send("Error on getting woka list");
@ -28,20 +17,20 @@ export class WokaListController extends BaseHttpController {
}); });
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.post("/woka-details", async (req, res) => { /*this.app.post("/woka-details", async (req, res) => {
const body = await req.json(); const body = await req.json();
if (!body || !body.textureIds) { if (!body || !body.textureIds) {
return res.status(400); return res.status(400);
} }
const textureIds = body.textureIds; const textureIds = body.textureIds;
const wokaDetails = await this.wokaService.fetchWokaDetails(textureIds); const wokaDetails = await wokaService.fetchWokaDetails(textureIds);
if (!wokaDetails) { if (!wokaDetails) {
return res.json({ details: [] }); return res.json({ details: [] });
} }
return res.json(wokaDetails); return res.json(wokaDetails);
}); });*/
} }
} }

View File

@ -49,16 +49,11 @@ export const isWokaDetail = new tg.IsInterface()
id: tg.isString, id: tg.isString,
}) })
.withOptionalProperties({ .withOptionalProperties({
texture: tg.isString, url: tg.isString,
layer: tg.isString,
}) })
.get(); .get();
export type WokaDetail = tg.GuardedType<typeof isWokaDetail>; export type WokaDetail = tg.GuardedType<typeof isWokaDetail>;
export const isWokaDetailsResult = new tg.IsInterface() export type WokaDetailsResult = WokaDetail[];
.withProperties({
details: tg.isArray(isWokaDetail),
})
.get();
export type WokaDetailsResult = tg.GuardedType<typeof isWokaDetailsResult>;

View File

@ -13,14 +13,10 @@ import { ClientDuplexStream } from "grpc";
import { Zone } from "_Model/Zone"; import { Zone } from "_Model/Zone";
import { CharacterTexture } from "../../Messages/JsonMessages/CharacterTexture"; import { CharacterTexture } from "../../Messages/JsonMessages/CharacterTexture";
import { compressors } from "hyper-express"; 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 {
name: string;
url: string | undefined;
}
export interface ExSocketInterface extends compressors.WebSocket, Identificable { export interface ExSocketInterface extends compressors.WebSocket, Identificable {
token: string; token: string;
roomId: string; roomId: string;
@ -28,7 +24,7 @@ export interface ExSocketInterface extends compressors.WebSocket, Identificable
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;
@ -42,7 +38,6 @@ export interface ExSocketInterface extends compressors.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;

View File

@ -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;
}); });
} }

View File

@ -1,25 +1,33 @@
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 { 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";
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 {
/** /**
@ -52,10 +60,16 @@ class AdminApi {
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 },
headers: { Authorization: `${ADMIN_API_TOKEN}` }, headers: { Authorization: `${ADMIN_API_TOKEN}` },
}); });
if (!isFetchMemberDataByUuidResponse(res.data)) {
throw new Error(
"Invalid answer received from the admin for the /api/map endpoint. Received: " +
JSON.stringify(res.data)
);
}
return res.data; return res.data;
} }

View File

@ -1,6 +1,6 @@
import axios from "axios"; import axios from "axios";
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { isWokaDetailsResult, isWokaList, WokaDetailsResult, WokaList } from "../Enum/PlayerTextures"; import { isWokaList, WokaList } from "../Enum/PlayerTextures";
import { WokaServiceInterface } from "./WokaServiceInterface"; import { WokaServiceInterface } from "./WokaServiceInterface";
class AdminWokaService implements WokaServiceInterface { class AdminWokaService implements WokaServiceInterface {
@ -32,7 +32,7 @@ class AdminWokaService implements WokaServiceInterface {
* *
* If one of the textures cannot be found, undefined is returned * If one of the textures cannot be found, undefined is returned
*/ */
fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> { /*fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> {
return axios return axios
.post( .post(
`${ADMIN_API_URL}/api/woka-details`, `${ADMIN_API_URL}/api/woka-details`,
@ -49,11 +49,11 @@ class AdminWokaService implements WokaServiceInterface {
} }
const result: WokaDetailsResult = res.data; const result: WokaDetailsResult = res.data;
if (result.details.length !== textureIds.length) { if (result.length !== textureIds.length) {
return undefined; return undefined;
} }
for (const detail of result.details) { for (const detail of result) {
if (!detail.texture) { if (!detail.texture) {
return undefined; return undefined;
} }
@ -65,7 +65,7 @@ class AdminWokaService implements WokaServiceInterface {
console.error(`Cannot get woka details from admin API with ids: ${textureIds}`, err); console.error(`Cannot get woka details from admin API with ids: ${textureIds}`, err);
return undefined; return undefined;
}); });
} }*/
} }
export const adminWokaService = new AdminWokaService(); export const adminWokaService = new AdminWokaService();

View File

@ -23,7 +23,13 @@ class LocalWokaService implements WokaServiceInterface {
*/ */
async fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> { async fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> {
const wokaData: WokaList = await require("../../data/woka.json"); const wokaData: WokaList = await require("../../data/woka.json");
const textures = new Map<string, string>(); const textures = new Map<
string,
{
url: string;
layer: string;
}
>();
const searchIds = new Set(textureIds); const searchIds = new Set(textureIds);
for (const part of wokaPartNames) { for (const part of wokaPartNames) {
@ -37,7 +43,10 @@ class LocalWokaService implements WokaServiceInterface {
const texture = collection.textures.find((texture) => texture.id === id); const texture = collection.textures.find((texture) => texture.id === id);
if (texture) { if (texture) {
textures.set(id, texture.url); textures.set(id, {
url: texture.url,
layer: part,
});
searchIds.delete(id); searchIds.delete(id);
} }
} }
@ -53,11 +62,12 @@ class LocalWokaService implements WokaServiceInterface {
textures.forEach((value, key) => { textures.forEach((value, key) => {
details.push({ details.push({
id: key, id: key,
texture: value, url: value.url,
layer: value.layer,
}); });
}); });
return { details }; return details;
} }
} }

View File

@ -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,7 @@ 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"; import { compressors } from "hyper-express";
const debug = Debug("socket"); const debug = Debug("socket");
@ -175,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);
} }
@ -545,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());
@ -642,6 +616,17 @@ export class SocketManager implements ZoneEventListener {
} }
} }
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) { public emitConnexionErrorMessage(client: compressors.WebSocket, message: string) {
const errorMessage = new WorldConnexionMessage(); const errorMessage = new WorldConnexionMessage();
errorMessage.setMessage(message); errorMessage.setMessage(message);

View 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;

View File

@ -14,5 +14,5 @@ export interface WokaServiceInterface {
* *
* If one of the textures cannot be found, undefined is returned (and the user should be redirected to Woka choice page!) * If one of the textures cannot be found, undefined is returned (and the user should be redirected to Woka choice page!)
*/ */
fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined>; //fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined>;
} }