Merge branch 'develop' into feature-admin-message-queue

This commit is contained in:
Alban Bruder 2021-12-24 12:49:08 +01:00 committed by GitHub
commit 380328079b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2376 additions and 2022 deletions

View file

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isColorEvent = new tg.IsInterface()
.withProperties({
red: tg.isNumber,
green: tg.isNumber,
blue: tg.isNumber,
})
.get();
/**
* A message sent from the iFrame to the game to dynamically set the outline of the player.
*/
export type ColorEvent = tg.GuardedType<typeof isColorEvent>;

View file

@ -29,6 +29,7 @@ import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/Trigg
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import { isColorEvent } from "./ColorEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@ -152,6 +153,14 @@ export const iframeQueryMapTypeGuards = {
query: isCreateEmbeddedWebsiteEvent,
answer: tg.isUndefined,
},
setPlayerOutline: {
query: isColorEvent,
answer: tg.isUndefined,
},
removePlayerOutline: {
query: tg.isUndefined,
answer: tg.isUndefined,
},
};
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;

View file

@ -1,4 +1,4 @@
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks";
@ -82,6 +82,24 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
}
return userRoomToken;
}
public setOutlineColor(red: number, green: number, blue: number): Promise<void> {
return queryWorkadventure({
type: "setPlayerOutline",
data: {
red,
green,
blue,
},
});
}
public removeOutlineColor(): Promise<void> {
return queryWorkadventure({
type: "removePlayerOutline",
data: undefined,
});
}
}
export default new WorkadventurePlayerCommands();

View file

@ -18,6 +18,7 @@ export enum EventMessage {
GROUP_DELETE = "group-delete",
SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id.
ITEM_EVENT = "item-event",
USER_DETAILS_UPDATED = "user-details-updated",
CONNECT_ERROR = "connect_error",
CONNECTING_ERROR = "connecting_error",
@ -64,6 +65,7 @@ export interface MessageUserJoined {
visitCardUrl: string | null;
companion: string | null;
userUuid: string;
outlineColor: number | undefined;
}
export interface PositionInterface {
@ -102,6 +104,12 @@ export interface ItemEventMessageInterface {
parameters: unknown;
}
export interface PlayerDetailsUpdatedMessageInterface {
userId: number;
outlineColor: number;
removeOutlineColor: boolean;
}
export interface RoomJoinedMessageInterface {
//users: MessageUserPositionInterface[],
//groups: GroupCreatedUpdatedMessageInterface[],

View file

@ -82,7 +82,7 @@ export class Room {
const currentRoom = new Room(baseUrl);
let instance: string = "global";
if (currentRoom.isPublic) {
instance = currentRoom.instance as string;
instance = currentRoom.getInstance();
}
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
@ -104,9 +104,9 @@ export class Room {
const data = result.data;
if (isRoomRedirect(data.redirectUrl)) {
if (isRoomRedirect(data)) {
return {
redirectUrl: data.redirectUrl as string,
redirectUrl: data.redirectUrl,
};
} else if (isMapDetailsData(data)) {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);

View file

@ -34,6 +34,7 @@ import {
BanUserMessage,
VariableMessage,
ErrorMessage,
PlayerDetailsUpdatedMessage,
} from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
@ -45,6 +46,7 @@ import {
ItemEventMessageInterface,
MessageUserJoined,
OnConnectInterface,
PlayerDetailsUpdatedMessageInterface,
PlayGlobalMessageInterface,
PositionInterface,
RoomJoinedMessageInterface,
@ -172,6 +174,9 @@ export class RoomConnection implements RoomConnection {
} else if (subMessage.hasEmoteeventmessage()) {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else if (subMessage.hasPlayerdetailsupdatedmessage()) {
event = EventMessage.USER_DETAILS_UPDATED;
payload = subMessage.getPlayerdetailsupdatedmessage();
} else if (subMessage.hasErrormessage()) {
const errorMessage = subMessage.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
@ -276,7 +281,7 @@ export class RoomConnection implements RoomConnection {
}
}
public emitPlayerDetailsMessage(userName: string, characterLayersSelected: BodyResourceDescriptionInterface[]) {
/*public emitPlayerDetailsMessage(userName: string, characterLayersSelected: BodyResourceDescriptionInterface[]) {
const message = new SetPlayerDetailsMessage();
message.setName(userName);
message.setCharacterlayersList(characterLayersSelected.map((characterLayer) => characterLayer.name));
@ -284,6 +289,20 @@ export class RoomConnection implements RoomConnection {
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setSetplayerdetailsmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}*/
public emitPlayerOutlineColor(color: number | null) {
const message = new SetPlayerDetailsMessage();
if (color === null) {
message.setRemoveoutlinecolor(true);
} else {
message.setOutlinecolor(color);
}
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setSetplayerdetailsmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
@ -404,6 +423,7 @@ export class RoomConnection implements RoomConnection {
position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null,
userUuid: message.getUseruuid(),
outlineColor: message.getHasoutline() ? message.getOutlinecolor() : undefined,
};
}
@ -596,6 +616,20 @@ export class RoomConnection implements RoomConnection {
});
}
onPlayerDetailsUpdated(callback: (message: PlayerDetailsUpdatedMessageInterface) => void): void {
this.onMessage(EventMessage.USER_DETAILS_UPDATED, (message: PlayerDetailsUpdatedMessage) => {
const details = message.getDetails();
if (details === undefined) {
throw new Error("Malformed message. Missing details in PlayerDetailsUpdatedMessage");
}
callback({
userId: message.getUserid(),
outlineColor: details.getOutlinecolor(),
removeOutlineColor: details.getRemoveoutlinecolor(),
});
});
}
public uploadAudio(file: FormData) {
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file)
.then((res: { data: {} }) => {

View file

@ -13,7 +13,8 @@ import { isSilentStore } from "../../Stores/MediaStore";
import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
import { TexturesHelper } from "../Helpers/TexturesHelper";
import type { PictureStore } from "../../Stores/PictureStore";
import { Writable, writable } from "svelte/store";
import { Unsubscriber, Writable, writable } from "svelte/store";
import { createColorStore } from "../../Stores/OutlineColorStore";
const playerNameY = -25;
@ -40,6 +41,8 @@ export abstract class Character extends Container {
private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene;
private readonly _pictureStore: Writable<string | undefined>;
private readonly outlineColorStore = createColorStore();
private readonly outlineColorStoreUnsubscribe: Unsubscriber;
constructor(
scene: GameScene,
@ -97,18 +100,26 @@ export abstract class Character extends Container {
});
this.on("pointerover", () => {
this.getOutlinePlugin()?.add(this.playerName, {
thickness: 2,
outlineColor: 0xffff00,
});
this.scene.markDirty();
this.outlineColorStore.pointerOver();
});
this.on("pointerout", () => {
this.getOutlinePlugin()?.remove(this.playerName);
this.scene.markDirty();
this.outlineColorStore.pointerOut();
});
}
this.outlineColorStoreUnsubscribe = this.outlineColorStore.subscribe((color) => {
if (color === undefined) {
this.getOutlinePlugin()?.remove(this.playerName);
} else {
this.getOutlinePlugin()?.remove(this.playerName);
this.getOutlinePlugin()?.add(this.playerName, {
thickness: 2,
outlineColor: color,
});
}
this.scene.markDirty();
});
scene.add.existing(this);
this.scene.physics.world.enableBody(this);
@ -315,6 +326,7 @@ export abstract class Character extends Container {
}
}
this.list.forEach((objectContaining) => objectContaining.destroy());
this.outlineColorStoreUnsubscribe();
super.destroy();
}
@ -401,4 +413,12 @@ export abstract class Character extends Container {
public get pictureStore(): PictureStore {
return this._pictureStore;
}
public setOutlineColor(color: number): void {
this.outlineColorStore.setColor(color);
}
public removeOutlineColor(): void {
this.outlineColorStore.removeColor();
}
}

View file

@ -55,6 +55,7 @@ import type {
MessageUserMovedInterface,
MessageUserPositionInterface,
OnConnectInterface,
PlayerDetailsUpdatedMessageInterface,
PointInterface,
PositionInterface,
RoomJoinedMessageInterface,
@ -86,8 +87,11 @@ import GameObject = Phaser.GameObjects.GameObject;
import DOMElement = Phaser.GameObjects.DOMElement;
import Tileset = Phaser.Tilemaps.Tileset;
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
import { deepCopy } from "deep-copy-ts";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore";
import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@ -123,6 +127,11 @@ interface DeleteGroupEventInterface {
groupId: number;
}
interface PlayerDetailsUpdatedInterface {
type: "PlayerDetailsUpdated";
details: PlayerDetailsUpdatedMessageInterface;
}
export class GameScene extends DirtyScene {
Terrains: Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: Player;
@ -135,20 +144,14 @@ export class GameScene extends DirtyScene {
groups: Map<number, Sprite>;
circleTexture!: CanvasTexture;
circleRedTexture!: CanvasTexture;
pendingEvents: Queue<
| InitUserPositionEventInterface
| AddPlayerEventInterface
| RemovePlayerEventInterface
| UserMovedEventInterface
| GroupCreatedUpdatedEventInterface
| DeleteGroupEventInterface
> = new Queue<
pendingEvents = new Queue<
| InitUserPositionEventInterface
| AddPlayerEventInterface
| RemovePlayerEventInterface
| UserMovedEventInterface
| GroupCreatedUpdatedEventInterface
| DeleteGroupEventInterface
| PlayerDetailsUpdatedInterface
>();
private initPosition: PositionInterface | null = null;
private playersPositionInterpolator = new PlayersPositionInterpolator();
@ -341,7 +344,10 @@ export class GameScene extends DirtyScene {
private async onMapLoad(data: any): Promise<void> {
// Triggered when the map is loaded
// Load tiles attached to the map recursively
this.mapFile = data.data;
// The map file can be modified by the scripting API and we don't want to tamper the Phaser cache (in case we come back on the map after visiting other maps)
// So we are doing a deep copy
this.mapFile = deepCopy(data.data);
const url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
this.mapFile.tilesets.forEach((tileset) => {
if (typeof tileset.name === "undefined" || typeof tileset.image === "undefined") {
@ -682,6 +688,7 @@ export class GameScene extends DirtyScene {
visitCardUrl: message.visitCardUrl,
companion: message.companion,
userUuid: message.userUuid,
outlineColor: message.outlineColor,
};
this.addPlayer(userMessage);
});
@ -735,6 +742,13 @@ export class GameScene extends DirtyScene {
item.fire(message.event, message.state, message.parameters);
});
this.connection.onPlayerDetailsUpdated((message) => {
this.pendingEvents.enqueue({
type: "PlayerDetailsUpdated",
details: message,
});
});
/**
* Triggered when we receive the JWT token to connect to Jitsi
*/
@ -1300,6 +1314,21 @@ ${escapedMessage}
iframeListener.registerAnswerer("removeActionMessage", (message) => {
layoutManagerActionStore.removeAction(message.uuid);
});
iframeListener.registerAnswerer("setPlayerOutline", (message) => {
const normalizeColor = (color: number) => Math.min(Math.max(0, Math.round(color)), 255);
const red = normalizeColor(message.red);
const green = normalizeColor(message.green);
const blue = normalizeColor(message.blue);
const color = (red << 16) | (green << 8) | blue;
this.CurrentPlayer.setOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color);
});
iframeListener.registerAnswerer("removePlayerOutline", (message) => {
this.CurrentPlayer.removeOutlineColor();
this.connection?.emitPlayerOutlineColor(null);
});
}
private setPropertyLayer(
@ -1422,6 +1451,7 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("removeActionMessage");
iframeListener.unregisterAnswerer("openCoWebsite");
iframeListener.unregisterAnswerer("getCoWebsites");
iframeListener.unregisterAnswerer("setPlayerOutline");
this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close();
@ -1676,6 +1706,12 @@ ${escapedMessage}
case "DeleteGroupEvent":
this.doDeleteGroup(event.groupId);
break;
case "PlayerDetailsUpdated":
this.doUpdatePlayerDetails(event.details);
break;
default: {
const tmp: never = event;
}
}
}
// Let's move all users
@ -1749,6 +1785,9 @@ ${escapedMessage}
addPlayerData.companion,
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
);
if (addPlayerData.outlineColor !== undefined) {
player.setOutlineColor(addPlayerData.outlineColor);
}
this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player);
player.updatePosition(addPlayerData.position);
@ -1852,6 +1891,23 @@ ${escapedMessage}
this.groups.delete(groupId);
}
doUpdatePlayerDetails(message: PlayerDetailsUpdatedMessageInterface): void {
const character = this.MapPlayersByKey.get(message.userId);
if (character === undefined) {
console.log(
"Could not set new details to character with ID ",
message.userId,
". Did he/she left before te message was received?"
);
return;
}
if (message.removeOutlineColor) {
character.removeOutlineColor();
} else {
character.setOutlineColor(message.outlineColor);
}
}
/**
* Sends to the server an event emitted by one of the ActionableItems.
*/

View file

@ -8,4 +8,5 @@ export interface PlayerInterface {
companion: string | null;
userUuid: string;
color?: string;
outlineColor?: number;
}

View file

@ -365,7 +365,9 @@ function applyCameraConstraints(currentStream: MediaStream | null, constraints:
return;
}
for (const track of currentStream.getVideoTracks()) {
toggleConstraints(track, constraints);
toggleConstraints(track, constraints).catch((e) =>
console.error("Error while setting new camera constraints:", e)
);
}
}
@ -380,19 +382,21 @@ function applyMicrophoneConstraints(
return;
}
for (const track of currentStream.getAudioTracks()) {
toggleConstraints(track, constraints);
toggleConstraints(track, constraints).catch((e) =>
console.error("Error while setting new audio constraints:", e)
);
}
}
function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): void {
async function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): Promise<void> {
if (implementCorrectTrackBehavior) {
track.enabled = constraints !== false;
} else if (constraints === false) {
track.stop();
}
// @ts-ignore
if (typeof constraints !== "boolean" && constraints !== true) {
track.applyConstraints(constraints);
return track.applyConstraints(constraints);
}
}
@ -484,7 +488,12 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
type: "success",
stream: null,
});
initStream(constraints);
initStream(constraints).catch((e) => {
set({
type: "error",
error: e instanceof Error ? e : new Error("An unknown error happened"),
});
});
}
} else {
//on bad navigators like chrome, we have to stop the tracks when we mute and reinstantiate the stream when we need to unmute
@ -496,7 +505,12 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
});
} //we reemit the stream if it was muted just to be sure
else if (constraints.audio /* && !oldConstraints.audio*/ || (!oldConstraints.video && constraints.video)) {
initStream(constraints);
initStream(constraints).catch((e) => {
set({
type: "error",
error: e instanceof Error ? e : new Error("An unknown error happened"),
});
});
}
oldConstraints = {
video: !!constraints.video,

View file

@ -0,0 +1,40 @@
import { writable } from "svelte/store";
export function createColorStore() {
const { subscribe, set } = writable<number | undefined>(undefined);
let color: number | undefined = undefined;
let focused: boolean = false;
const updateColor = () => {
if (focused) {
set(0xffff00);
} else {
set(color);
}
};
return {
subscribe,
pointerOver() {
focused = true;
updateColor();
},
pointerOut() {
focused = false;
updateColor();
},
setColor(newColor: number) {
color = newColor;
updateColor();
},
removeColor() {
color = undefined;
updateColor();
},
};
}

View file

@ -98,7 +98,7 @@ export class SimplePeer {
private receiveWebrtcStart(user: UserSimplePeerInterface): void {
this.Users.push(user);
// Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group)
// Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joins a group)
// So we can receive a request we already had before. (which will abort at the first line of createPeerConnection)
// This would be symmetrical to the way we handle disconnection.