From 07d399403be2107a077110fac8d55e7045c42efd Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Wed, 23 Feb 2022 00:27:07 +0100 Subject: [PATCH 01/64] Refactor how to use new Jitsi icon Signed-off-by: Gregoire Parant --- .../EmbedScreens/CoWebsiteThumbnailSlot.svelte | 9 ++++++++- .../logos => src/Components/images}/jitsi.png | Bin 2 files changed, 8 insertions(+), 1 deletion(-) rename front/{public/resources/logos => src/Components/images}/jitsi.png (100%) diff --git a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte index d8431704..a21d8389 100644 --- a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte +++ b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte @@ -9,6 +9,8 @@ import { iframeStates } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; + import uploadFile from "../images/jitsi.png"; + export let index: number; export let coWebsite: CoWebsite; export let vertical: boolean; @@ -21,7 +23,7 @@ onMount(() => { icon.src = isJitsi - ? "/resources/logos/jitsi.png" + ? uploadFile : `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`; icon.alt = coWebsite.getUrl().hostname; icon.onload = () => { @@ -350,9 +352,14 @@ color: white; padding: 4px; border-radius: 4px; + p { margin-bottom: 0; } + + &.hide { + display: none; + } } } diff --git a/front/public/resources/logos/jitsi.png b/front/src/Components/images/jitsi.png similarity index 100% rename from front/public/resources/logos/jitsi.png rename to front/src/Components/images/jitsi.png From 8d8857011ea9fae0933af2802ef7f040db987843 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Wed, 23 Feb 2022 14:40:19 +0100 Subject: [PATCH 02/64] removed unused imports --- front/src/Connexion/RoomConnection.ts | 17 +---------------- front/src/Phaser/Game/GameScene.ts | 1 + 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 4e2f8397..584a1a51 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -5,15 +5,11 @@ import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; import type { GroupCreatedUpdatedMessageInterface, - ItemEventMessageInterface, MessageUserJoined, - OnConnectInterface, - PlayerDetailsUpdatedMessageInterface, PlayGlobalMessageInterface, PositionInterface, RoomJoinedMessageInterface, ViewportInterface, - WebRtcDisconnectMessageInterface, WebRtcSignalReceivedMessageInterface, } from "./ConnexionModels"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; @@ -21,31 +17,22 @@ import { adminMessagesService } from "./AdminMessagesService"; import { connectionManager } from "./ConnectionManager"; import { get } from "svelte/store"; import { warningContainerStore } from "../Stores/MenuStore"; -import { followStateStore, followRoleStore, followUsersStore } from "../Stores/FollowStore"; +import { followRoleStore, followUsersStore } from "../Stores/FollowStore"; import { localUserStore } from "./LocalUserStore"; import { - RefreshRoomMessage, ServerToClientMessage as ServerToClientMessageTsProto, TokenExpiredMessage, WorldConnexionMessage, - WorldFullMessage, ErrorMessage as ErrorMessageTsProto, UserMovedMessage as UserMovedMessageTsProto, GroupUpdateMessage as GroupUpdateMessageTsProto, GroupDeleteMessage as GroupDeleteMessageTsProto, UserJoinedMessage as UserJoinedMessageTsProto, UserLeftMessage as UserLeftMessageTsProto, - ItemEventMessage as ItemEventMessageTsProto, EmoteEventMessage as EmoteEventMessageTsProto, - VariableMessage as VariableMessageTsProto, PlayerDetailsUpdatedMessage as PlayerDetailsUpdatedMessageTsProto, - WorldFullWarningMessage, WebRtcDisconnectMessage as WebRtcDisconnectMessageTsProto, - PlayGlobalMessage as PlayGlobalMessageTsProto, - StopGlobalMessage as StopGlobalMessageTsProto, SendJitsiJwtMessage as SendJitsiJwtMessageTsProto, - SendUserMessage as SendUserMessageTsProto, - BanUserMessage as BanUserMessageTsProto, ClientToServerMessage as ClientToServerMessageTsProto, PositionMessage as PositionMessageTsProto, ViewportMessage as ViewportMessageTsProto, @@ -54,8 +41,6 @@ import { PingMessage as PingMessageTsProto, } from "../Messages/ts-proto-generated/messages"; import { Subject } from "rxjs"; -import { OpenPopupEvent } from "../Api/Events/OpenPopupEvent"; -import { match } from "assert"; const manualPingDelay = 20000; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 0a44cea3..274ae573 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1976,6 +1976,7 @@ ${escapedMessage} } private doShareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { + console.log(groupPositionMessage); //delete previous group this.doDeleteGroup(groupPositionMessage.groupId); From 724dbc8efac7a8eea78260e7787c5d8f4be9ab81 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Mon, 28 Feb 2022 13:47:08 +0100 Subject: [PATCH 03/64] send group userIds to players --- back/src/Model/Group.ts | 2 +- back/src/Services/SocketManager.ts | 24 ++++++++++++++++++++++-- front/src/Connexion/ConnexionModels.ts | 5 +++++ front/src/Connexion/RoomConnection.ts | 10 ++++++++++ front/src/Phaser/Game/GameScene.ts | 1 - messages/protos/messages.proto | 6 ++++++ 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index c14d509f..65e15c40 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -147,9 +147,9 @@ export class Group implements Movable { join(user: User): void { // Broadcast on the right event - this.connectCallback(user, this); this.users.add(user); user.group = this; + this.connectCallback(user, this); } leave(user: User): void { diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 9233811b..c396893d 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -38,6 +38,7 @@ import { SubToPusherRoomMessage, SetPlayerDetailsMessage, PlayerDetailsUpdatedMessage, + GroupUsersUpdateMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -265,8 +266,14 @@ export class SocketManager { if (roomPromise === undefined) { roomPromise = GameRoom.create( roomId, - (user: User, group: Group) => this.joinWebRtcRoom(user, group), - (user: User, group: Group) => this.disConnectedUser(user, group), + (user: User, group: Group) => { + this.joinWebRtcRoom(user, group); + this.sendGroupUsersUpdateToGroupMembers(group); + }, + (user: User, group: Group) => { + this.disConnectedUser(user, group); + this.sendGroupUsersUpdateToGroupMembers(group); + }, MINIMUM_DISTANCE, GROUP_RADIUS, (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => @@ -438,6 +445,19 @@ export class SocketManager { return undefined; } + private sendGroupUsersUpdateToGroupMembers(group: Group) { + const groupUserUpdateMessage = new GroupUsersUpdateMessage(); + groupUserUpdateMessage.setGroupid(group.getId()); + groupUserUpdateMessage.setUseridsList(group.getUsers().map((user) => user.id)); + + const clientMessage = new ServerToClientMessage(); + clientMessage.setGroupusersupdatemessage(groupUserUpdateMessage); + + group.getUsers().forEach((currentUser: User) => { + currentUser.socket.write(clientMessage); + }); + } + private joinWebRtcRoom(user: User, group: Group) { for (const otherUser of group.getUsers()) { if (user === otherUser) { diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index bf834a02..3d8893a2 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -47,6 +47,11 @@ export interface GroupCreatedUpdatedMessageInterface { groupSize: number; } +export interface GroupUsersUpdateMessageInterface { + groupId: number; + userIds: number[]; +} + export interface WebRtcDisconnectMessageInterface { userId: number; } diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 584a1a51..95c91cd5 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -5,6 +5,7 @@ import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; import type { GroupCreatedUpdatedMessageInterface, + GroupUsersUpdateMessageInterface, MessageUserJoined, PlayGlobalMessageInterface, PositionInterface, @@ -97,6 +98,9 @@ export class RoomConnection implements RoomConnection { private readonly _groupUpdateMessageStream = new Subject(); public readonly groupUpdateMessageStream = this._groupUpdateMessageStream.asObservable(); + private readonly _groupUsersUpdateMessageStream = new Subject(); + public readonly groupUsersUpdateMessageStream = this._groupUsersUpdateMessageStream.asObservable(); + private readonly _groupDeleteMessageStream = new Subject(); public readonly groupDeleteMessageStream = this._groupDeleteMessageStream.asObservable(); @@ -398,6 +402,12 @@ export class RoomConnection implements RoomConnection { this._sendJitsiJwtMessageStream.next(message.sendJitsiJwtMessage); break; } + case "groupUsersUpdateMessage": { + console.log("GOT GROUP USERS UPDATE MESSAGE"); + console.log(message.groupUsersUpdateMessage); + this._groupUsersUpdateMessageStream.next(message.groupUsersUpdateMessage); + break; + } case "sendUserMessage": { adminMessagesService.onSendusermessage(message.sendUserMessage); break; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 274ae573..0a44cea3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1976,7 +1976,6 @@ ${escapedMessage} } private doShareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { - console.log(groupPositionMessage); //delete previous group this.doDeleteGroup(groupPositionMessage.groupId); diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 8ac7bbf0..5c9c95b7 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -215,6 +215,11 @@ message ItemStateMessage { string stateJson = 2; } +message GroupUsersUpdateMessage { + int32 groupId = 1; + repeated int32 userIds = 2; +} + message RoomJoinedMessage { //repeated UserJoinedMessage user = 1; //repeated GroupUpdateMessage group = 2; @@ -310,6 +315,7 @@ message ServerToClientMessage { FollowRequestMessage followRequestMessage = 21; FollowConfirmationMessage followConfirmationMessage = 22; FollowAbortMessage followAbortMessage = 23; + GroupUsersUpdateMessage groupUsersUpdateMessage = 24; } } From d43c8d181af71542abe1c268de8a0bcaf4aaaca6 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Tue, 1 Mar 2022 11:36:45 +0100 Subject: [PATCH 04/64] sending info about group lock state. wip --- back/src/Model/GameRoom.ts | 2 +- back/src/Model/Group.ts | 9 +++++++++ back/src/RoomManager.ts | 8 ++++++++ back/src/Services/SocketManager.ts | 7 +++++++ front/src/Connexion/ConnexionModels.ts | 1 + front/src/Connexion/RoomConnection.ts | 15 +++++++++++++++ front/src/Phaser/Game/GameScene.ts | 20 +++++++++++++++++++- messages/protos/messages.proto | 11 +++++++++++ pusher/src/Controller/IoSocketController.ts | 3 +++ pusher/src/Model/Zone.ts | 15 +++++++++++++-- pusher/src/Services/SocketManager.ts | 7 +++++++ 11 files changed, 94 insertions(+), 4 deletions(-) diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 7d7b24a5..b55c6079 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -418,7 +418,7 @@ export class GameRoom { }); this.groups.forEach((group: Group) => { - if (group.isFull()) { + if (group.isFull() || group.isLocked()) { return; } const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index 65e15c40..a960e7b3 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -14,6 +14,7 @@ export class Group implements Movable { private x!: number; private y!: number; private wasDestroyed: boolean = false; + private locked: boolean = false; private roomId: string; private currentZone: Zone | null = null; /** @@ -141,6 +142,10 @@ export class Group implements Movable { return this.users.size >= MAX_PER_GROUP; } + isLocked(): boolean { + return this.locked; + } + isEmpty(): boolean { return this.users.size <= 1; } @@ -167,6 +172,10 @@ export class Group implements Movable { this.disconnectCallback(user, this); } + lock(lock: boolean = true): void { + this.locked = lock; + } + /** * Let's kick everybody out. * Usually used when there is only one user left. diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index d375fbd8..b5e27982 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -29,6 +29,7 @@ import { WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, ZoneMessage, + LockGroupMessage, } from "./Messages/generated/messages_pb"; import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; import { socketManager } from "./Services/SocketManager"; @@ -135,6 +136,11 @@ const roomManager: IRoomManagerServer = { user, message.getFollowabortmessage() as FollowAbortMessage ); + } else if (message.hasLockgroupmessage()) { + socketManager.handleLockGroupMessage( + user, + message.getLockgroupmessage() as LockGroupMessage + ); } else if (message.hasSendusermessage()) { const sendUserMessage = message.getSendusermessage(); socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage); @@ -148,6 +154,8 @@ const roomManager: IRoomManagerServer = { user, setPlayerDetailsMessage as SetPlayerDetailsMessage ); + } else if (message.hasLockgroupmessage()) { + console.log("===== GOT LOCK GROUP MESSAGE FROM CLIENT ====="); } else { throw new Error("Unhandled message type"); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index c396893d..af0ec6cf 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -39,6 +39,7 @@ import { SetPlayerDetailsMessage, PlayerDetailsUpdatedMessage, GroupUsersUpdateMessage, + LockGroupMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -404,6 +405,7 @@ export class SocketManager { groupUpdateMessage.setPosition(pointMessage); groupUpdateMessage.setGroupsize(group.getSize); groupUpdateMessage.setFromzone(this.toProtoZone(fromZone)); + groupUpdateMessage.setLocked(group.isLocked()); const subMessage = new SubToPusherMessage(); subMessage.setGroupupdatezonemessage(groupUpdateMessage); @@ -889,6 +891,11 @@ export class SocketManager { leader?.delFollower(user); } } + + handleLockGroupMessage(user: User, message: LockGroupMessage) { + console.log(`lock group: ${message.getLock()}`); + user.group?.lock(message.getLock()); + } } export const socketManager = new SocketManager(); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 3d8893a2..04282abc 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -45,6 +45,7 @@ export interface GroupCreatedUpdatedMessageInterface { position: PositionInterface; groupId: number; groupSize: number; + locked: boolean; } export interface GroupUsersUpdateMessageInterface { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 95c91cd5..0f932901 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -636,6 +636,7 @@ export class RoomConnection implements RoomConnection { groupId: message.groupId, position: position, groupSize: message.groupSize, + locked: message.locked, }; } @@ -851,6 +852,20 @@ export class RoomConnection implements RoomConnection { this.socket.send(bytes); } + public emitLockGroup(groupId: number, lock: boolean = true): void { + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "lockGroupMessage", + lockGroupMessage: { + groupId, + lock, + }, + }, + }).finish(); + + this.socket.send(bytes); + } + public getAllTags(): string[] { return this.tags; } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 0a44cea3..c0d82d24 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -718,6 +718,16 @@ export class GameScene extends DirtyScene { e ) ); + + this.input.keyboard.on("keydown-L", (event: Event) => { + console.log("group locked"); + this.connection?.emitLockGroup(1, true); + }); + + this.input.keyboard.on("keydown-U", (event: Event) => { + console.log("group unlocked"); + this.connection?.emitLockGroup(1, false); + }); } /** @@ -782,6 +792,7 @@ export class GameScene extends DirtyScene { this.connection.groupUpdateMessageStream.subscribe( (groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { + console.log(groupPositionMessage); this.shareGroupPosition(groupPositionMessage); } ); @@ -1809,8 +1820,13 @@ ${escapedMessage} break; } case "GroupCreatedUpdatedEvent": + console.log("CREATE OR UPDATE GROUP"); this.doShareGroupPosition(event.event); break; + // TODO: CALL THIS ON GROUP LOCK CHANGE + // case "GroupCreatedUpdatedEvent": + // this.doShareGroupPosition(event.event); + // break; case "DeleteGroupEvent": this.doDeleteGroup(event.groupId); break; @@ -1985,7 +2001,9 @@ ${escapedMessage} this, Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y), - groupPositionMessage.groupSize === MAX_PER_GROUP ? "circleSprite-red" : "circleSprite-white" + groupPositionMessage.groupSize === MAX_PER_GROUP || groupPositionMessage.locked + ? "circleSprite-red" + : "circleSprite-white" ); sprite.setDisplayOrigin(48, 48); this.add.existing(sprite); diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 5c9c95b7..4ccaed4b 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -98,6 +98,11 @@ message FollowAbortMessage { int32 follower = 2; } +message LockGroupMessage { + int32 groupId = 1; + bool lock = 2; +} + message ClientToServerMessage { oneof message { UserMovesMessage userMovesMessage = 2; @@ -116,6 +121,7 @@ message ClientToServerMessage { FollowRequestMessage followRequestMessage = 15; FollowConfirmationMessage followConfirmationMessage = 16; FollowAbortMessage followAbortMessage = 17; + LockGroupMessage lockGroupMessage = 18; } } @@ -172,6 +178,7 @@ message SubMessage { VariableMessage variableMessage = 8; ErrorMessage errorMessage = 9; PlayerDetailsUpdatedMessage playerDetailsUpdatedMessage = 10; + LockGroupMessage lockGroupMessage = 11; } } @@ -184,6 +191,7 @@ message GroupUpdateMessage { int32 groupId = 1; PointMessage position = 2; int32 groupSize = 3; + bool locked = 4; } message GroupDeleteMessage { @@ -316,6 +324,7 @@ message ServerToClientMessage { FollowConfirmationMessage followConfirmationMessage = 22; FollowAbortMessage followAbortMessage = 23; GroupUsersUpdateMessage groupUsersUpdateMessage = 24; + LockGroupMessage lockGroupMessage = 25; } } @@ -358,6 +367,7 @@ message GroupUpdateZoneMessage { PointMessage position = 2; int32 groupSize = 3; Zone fromZone = 4; + bool locked = 5; } message GroupLeftZoneMessage { @@ -403,6 +413,7 @@ message PusherToBackMessage { FollowRequestMessage followRequestMessage = 16; FollowConfirmationMessage followConfirmationMessage = 17; FollowAbortMessage followAbortMessage = 18; + LockGroupMessage lockGroupMessage = 19; } } diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 6db53403..617da341 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -21,6 +21,7 @@ import { FollowConfirmationMessage, FollowAbortMessage, VariableMessage, + LockGroupMessage, } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb"; import { TemplatedApp } from "uWebSockets.js"; @@ -494,6 +495,8 @@ export class IoSocketController { ); } else if (message.hasFollowabortmessage()) { socketManager.handleFollowAbort(client, message.getFollowabortmessage() as FollowAbortMessage); + } else if (message.hasLockgroupmessage()) { + socketManager.handleLockGroup(client, message.getLockgroupmessage() as LockGroupMessage); } /* Ok is false if backpressure was built up, wait for drain */ diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 2132ff39..1bf1090a 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -32,6 +32,7 @@ export interface ZoneEventListener { onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupLeaves(groupId: number, listener: ExSocketInterface): void; + // onGroupLock(groupId: number, listener: ExSocketInterface): void; onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void; onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void; onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ExSocketInterface): void; @@ -123,19 +124,25 @@ export class UserDescriptor { } export class GroupDescriptor { - private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) {} + private constructor( + public readonly groupId: number, + private groupSize: number, + private position: PointMessage, + private locked: boolean + ) {} public static createFromGroupUpdateZoneMessage(message: GroupUpdateZoneMessage): GroupDescriptor { const position = message.getPosition(); if (position === undefined) { throw new Error("Missing position"); } - return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position); + return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position, message.getLocked()); } public update(groupDescriptor: GroupDescriptor) { this.groupSize = groupDescriptor.groupSize; this.position = groupDescriptor.position; + this.locked = groupDescriptor.locked; } public toGroupUpdateMessage(): GroupUpdateMessage { @@ -146,6 +153,7 @@ export class GroupDescriptor { groupUpdateMessage.setGroupid(this.groupId); groupUpdateMessage.setGroupsize(this.groupSize); groupUpdateMessage.setPosition(this.position); + groupUpdateMessage.setLocked(this.locked); return groupUpdateMessage; } @@ -238,6 +246,9 @@ export class Zone { } else if (message.hasEmoteeventmessage()) { const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; this.notifyEmote(emoteEventMessage); + } else if (message.hasEmoteeventmessage()) { + const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; + this.notifyEmote(emoteEventMessage); } else if (message.hasPlayerdetailsupdatedmessage()) { const playerDetailsUpdatedMessage = message.getPlayerdetailsupdatedmessage() as PlayerDetailsUpdatedMessage; diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 30fe761f..b1006bf8 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -38,6 +38,7 @@ import { ErrorMessage, WorldFullMessage, PlayerDetailsUpdatedMessage, + LockGroupMessage, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; @@ -292,6 +293,12 @@ export class SocketManager implements ZoneEventListener { client.backConnection.write(pusherToBackMessage); } + handleLockGroup(client: ExSocketInterface, message: LockGroupMessage): void { + const pusherToBackMessage = new PusherToBackMessage(); + pusherToBackMessage.setLockgroupmessage(message); + client.backConnection.write(pusherToBackMessage); + } + onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void { const subMessage = new SubMessage(); subMessage.setEmoteeventmessage(emoteMessage); From ef22d4ba209972b61f64810c9ffd0d1701323397 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Wed, 2 Mar 2022 10:44:26 +0100 Subject: [PATCH 05/64] listening to lockGroupMessage on front --- back/src/Model/GameRoom.ts | 12 +++++ back/src/Model/PositionNotifier.ts | 13 ++++- back/src/Model/Zone.ts | 10 ++++ back/src/RoomManager.ts | 1 + back/src/Services/SocketManager.ts | 26 ++++++++-- back/tests/PositionNotifierTest.ts | 2 + front/src/Connexion/ConnexionModels.ts | 5 ++ front/src/Connexion/RoomConnection.ts | 10 +++- front/src/Phaser/Game/GameScene.ts | 66 +++++++++++++++++++++----- messages/protos/messages.proto | 1 + pusher/src/Model/Zone.ts | 15 ++++-- pusher/src/Services/SocketManager.ts | 7 +++ 12 files changed, 145 insertions(+), 23 deletions(-) diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index b55c6079..d6a6c7c2 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -6,6 +6,7 @@ import { EmoteCallback, EntersCallback, LeavesCallback, + LockGroupCallback, MovesCallback, PlayerDetailsUpdatedCallback, } from "_Model/Zone"; @@ -22,6 +23,7 @@ import { VariableMessage, VariableWithTagMessage, ServerToClientMessage, + LockGroupMessage, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { RoomSocket, ZoneSocket } from "src/RoomManager"; @@ -66,6 +68,7 @@ export class GameRoom { onMoves: MovesCallback, onLeaves: LeavesCallback, onEmote: EmoteCallback, + onLockGroup: LockGroupCallback, onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback ) { // A zone is 10 sprites wide. @@ -76,6 +79,7 @@ export class GameRoom { onMoves, onLeaves, onEmote, + onLockGroup, onPlayerDetailsUpdated ); } @@ -90,6 +94,7 @@ export class GameRoom { onMoves: MovesCallback, onLeaves: LeavesCallback, onEmote: EmoteCallback, + onLockGroup: LockGroupCallback, onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback ): Promise { const mapDetails = await GameRoom.getMapDetails(roomUrl); @@ -105,6 +110,7 @@ export class GameRoom { onMoves, onLeaves, onEmote, + onLockGroup, onPlayerDetailsUpdated ); @@ -544,6 +550,12 @@ export class GameRoom { this.positionNotifier.emitEmoteEvent(user, emoteEventMessage); } + public emitLockGroupMessage(user: User, lockGroupMessage: LockGroupMessage) { + console.log("D2 GAME ROOM EMIT LOCK GROUP MESSAGE"); + console.log(lockGroupMessage.getGroupid()); + this.positionNotifier.emitLockGroupEvent(user, lockGroupMessage); + } + public addRoomListener(socket: RoomSocket) { this.roomListeners.add(socket); } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index b059999a..ad7334f9 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -12,6 +12,7 @@ import { EmoteCallback, EntersCallback, LeavesCallback, + LockGroupCallback, MovesCallback, PlayerDetailsUpdatedCallback, Zone, @@ -20,7 +21,7 @@ import { Movable } from "_Model/Movable"; import { PositionInterface } from "_Model/PositionInterface"; import { ZoneSocket } from "../RoomManager"; import { User } from "../Model/User"; -import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb"; +import { EmoteEventMessage, LockGroupMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb"; interface ZoneDescriptor { i: number; @@ -50,6 +51,7 @@ export class PositionNotifier { private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback, + private onLockGroup: LockGroupCallback, private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback ) {} @@ -111,6 +113,7 @@ export class PositionNotifier { this.onUserMoves, this.onUserLeaves, this.onEmote, + this.onLockGroup, this.onPlayerDetailsUpdated, i, j @@ -137,6 +140,14 @@ export class PositionNotifier { zone.emitEmoteEvent(emoteEventMessage); } + public emitLockGroupEvent(user: User, lockGroupMessage: LockGroupMessage) { + const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); + const zone = this.getZone(zoneDesc.i, zoneDesc.j); + console.log("D3 emit from ZONE"); + console.log(lockGroupMessage.getGroupid()); + zone.emitLockGroupEvent(lockGroupMessage); + } + public *getAllUsersInSquareAroundZone(zone: Zone): Generator { const zoneDescriptor = this.getZoneDescriptorFromCoordinates(zone.x, zone.y); for (const d of getNearbyDescriptorsMatrix(zoneDescriptor)) { diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index 5c3e92ba..35bb4660 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -7,12 +7,14 @@ import { EmoteEventMessage, SetPlayerDetailsMessage, PlayerDetailsUpdatedMessage, + LockGroupMessage, } from "../Messages/generated/messages_pb"; export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void; export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; +export type LockGroupCallback = (lockGroupMessage: LockGroupMessage, listener: ZoneSocket) => void; export type PlayerDetailsUpdatedCallback = ( playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket @@ -27,6 +29,7 @@ export class Zone { private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, + private onLockGroup: LockGroupCallback, private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback, public readonly x: number, public readonly y: number @@ -108,6 +111,13 @@ export class Zone { } } + public emitLockGroupEvent(lockGroupMessage: LockGroupMessage) { + console.log("D4 ZONE ON LOCK GROUP CALLBACK"); + for (const listener of this.listeners) { + this.onLockGroup(lockGroupMessage, listener); + } + } + public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) { const playerDetailsUpdatedMessage = new PlayerDetailsUpdatedMessage(); playerDetailsUpdatedMessage.setUserid(user.id); diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index b5e27982..6a091054 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -138,6 +138,7 @@ const roomManager: IRoomManagerServer = { ); } else if (message.hasLockgroupmessage()) { socketManager.handleLockGroupMessage( + room, user, message.getLockgroupmessage() as LockGroupMessage ); diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index af0ec6cf..aaa373fb 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -285,6 +285,8 @@ export class SocketManager { this.onClientLeave(thing, newZone, listener), (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), + (lockGroupMessage: LockGroupMessage, listener: ZoneSocket) => + this.onLockGroup(lockGroupMessage, listener), (playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) => this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener) ) @@ -388,6 +390,16 @@ export class SocketManager { emitZoneMessage(subMessage, client); } + private onLockGroup(lockGroupMessage: LockGroupMessage, client: ZoneSocket) { + const subMessage = new SubToPusherMessage(); + subMessage.setLockgroupmessage(lockGroupMessage); + console.log("D5 SOCKET MANAGER ON LOCK GROUP"); + console.log(lockGroupMessage.getGroupid()); + console.log(lockGroupMessage.getLock()); + + emitZoneMessage(subMessage, client); + } + private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) { const subMessage = new SubToPusherMessage(); subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage); @@ -892,9 +904,17 @@ export class SocketManager { } } - handleLockGroupMessage(user: User, message: LockGroupMessage) { - console.log(`lock group: ${message.getLock()}`); - user.group?.lock(message.getLock()); + handleLockGroupMessage(room: GameRoom, user: User, message: LockGroupMessage) { + console.log("D1 HANDLE LOCK GROUP MESSAGE"); + const group = user.group; + if (!group) { + return; + } + group.lock(message.getLock()); + const lockGroupMessage = new LockGroupMessage(); + lockGroupMessage.setLock(message.getLock()); + lockGroupMessage.setGroupid(message.getGroupid()); + room.emitLockGroupMessage(user, lockGroupMessage); } } diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index bf7ddd6e..f21bb4b2 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -25,6 +25,7 @@ describe("PositionNotifier", () => { leaveTriggered = true; }, () => {}, + () => {}, () => {} ); @@ -132,6 +133,7 @@ describe("PositionNotifier", () => { leaveTriggered = true; }, () => {}, + () => {}, () => {} ); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 04282abc..5b41e602 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -48,6 +48,11 @@ export interface GroupCreatedUpdatedMessageInterface { locked: boolean; } +export interface LockGroupMessageInterface { + groupId: number; + locked: boolean; +} + export interface GroupUsersUpdateMessageInterface { groupId: number; userIds: number[]; diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 0f932901..3d69f38a 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -40,6 +40,7 @@ import { PositionMessage_Direction, SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto, PingMessage as PingMessageTsProto, + LockGroupMessage, } from "../Messages/ts-proto-generated/messages"; import { Subject } from "rxjs"; @@ -101,6 +102,9 @@ export class RoomConnection implements RoomConnection { private readonly _groupUsersUpdateMessageStream = new Subject(); public readonly groupUsersUpdateMessageStream = this._groupUsersUpdateMessageStream.asObservable(); + private readonly _lockGroupMessageStream = new Subject(); + public readonly lockGroupMessageStream = this._lockGroupMessageStream.asObservable(); + private readonly _groupDeleteMessageStream = new Subject(); public readonly groupDeleteMessageStream = this._groupDeleteMessageStream.asObservable(); @@ -264,6 +268,10 @@ export class RoomConnection implements RoomConnection { this._emoteEventMessageStream.next(subMessage.emoteEventMessage); break; } + case "lockGroupMessage": { + this._lockGroupMessageStream.next(subMessage.lockGroupMessage); + break; + } case "playerDetailsUpdatedMessage": { this._playerDetailsUpdatedMessageStream.next(subMessage.playerDetailsUpdatedMessage); break; @@ -403,8 +411,6 @@ export class RoomConnection implements RoomConnection { break; } case "groupUsersUpdateMessage": { - console.log("GOT GROUP USERS UPDATE MESSAGE"); - console.log(message.groupUsersUpdateMessage); this._groupUsersUpdateMessageStream.next(message.groupUsersUpdateMessage); break; } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index c0d82d24..7b92a5d1 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -51,6 +51,7 @@ import { PathfindingManager } from "../../Utils/PathfindingManager"; import { ActivatablesManager } from "./ActivatablesManager"; import type { GroupCreatedUpdatedMessageInterface, + LockGroupMessageInterface, MessageUserMovedInterface, MessageUserPositionInterface, OnConnectInterface, @@ -132,6 +133,11 @@ interface DeleteGroupEventInterface { groupId: number; } +interface LockGroupEventInterface { + type: "LockGroupEvent"; + event: LockGroupMessageInterface; +} + interface PlayerDetailsUpdatedInterface { type: "PlayerDetailsUpdated"; details: PlayerDetailsUpdatedMessageInterface; @@ -147,6 +153,7 @@ export class GameScene extends DirtyScene { mapFile!: ITiledMap; animatedTiles!: AnimatedTiles; groups: Map; + currentPlayerGroupId?: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; pendingEvents = new Queue< @@ -156,6 +163,7 @@ export class GameScene extends DirtyScene { | UserMovedEventInterface | GroupCreatedUpdatedEventInterface | DeleteGroupEventInterface + | LockGroupEventInterface | PlayerDetailsUpdatedInterface >(); private initPosition: PositionInterface | null = null; @@ -225,6 +233,7 @@ export class GameScene extends DirtyScene { }); this.Terrains = []; this.groups = new Map(); + this.currentPlayerGroupId = undefined; this.instance = room.getInstance(); this.MapUrlFile = MapUrlFile; @@ -720,13 +729,17 @@ export class GameScene extends DirtyScene { ); this.input.keyboard.on("keydown-L", (event: Event) => { - console.log("group locked"); - this.connection?.emitLockGroup(1, true); + if (this.currentPlayerGroupId !== undefined) { + console.log("group locked"); + this.connection?.emitLockGroup(this.currentPlayerGroupId, true); + } }); this.input.keyboard.on("keydown-U", (event: Event) => { - console.log("group unlocked"); - this.connection?.emitLockGroup(1, false); + if (this.currentPlayerGroupId !== undefined) { + console.log("group unlocked"); + this.connection?.emitLockGroup(this.currentPlayerGroupId, false); + } }); } @@ -792,7 +805,6 @@ export class GameScene extends DirtyScene { this.connection.groupUpdateMessageStream.subscribe( (groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { - console.log(groupPositionMessage); this.shareGroupPosition(groupPositionMessage); } ); @@ -805,6 +817,10 @@ export class GameScene extends DirtyScene { } }); + this.connection.lockGroupMessageStream.subscribe((message) => { + this.lockGroup(message); + }); + this.connection.onServerDisconnected(() => { console.log("Player disconnected from server. Reloading scene."); this.cleanupClosingScene(); @@ -838,6 +854,12 @@ export class GameScene extends DirtyScene { }); }); + this.connection.groupUsersUpdateMessageStream.subscribe((message) => { + this.currentPlayerGroupId = message.groupId; + console.log("GOT GROUP USERS UPDATE MESSAGE"); + console.log(message); + }); + /** * Triggered when we receive the JWT token to connect to Jitsi */ @@ -1820,19 +1842,22 @@ ${escapedMessage} break; } case "GroupCreatedUpdatedEvent": - console.log("CREATE OR UPDATE GROUP"); this.doShareGroupPosition(event.event); break; - // TODO: CALL THIS ON GROUP LOCK CHANGE - // case "GroupCreatedUpdatedEvent": - // this.doShareGroupPosition(event.event); - // break; - case "DeleteGroupEvent": - this.doDeleteGroup(event.groupId); - break; case "PlayerDetailsUpdated": this.doUpdatePlayerDetails(event.details); break; + case "DeleteGroupEvent": { + this.doDeleteGroup(event.groupId); + if (this.currentPlayerGroupId === event.groupId) { + this.currentPlayerGroupId = undefined; + } + break; + } + case "LockGroupEvent": { + this.doLockGroup(event.event); + break; + } default: { const tmp: never = event; } @@ -2027,6 +2052,21 @@ ${escapedMessage} this.groups.delete(groupId); } + lockGroup(event: LockGroupMessageInterface): void { + this.pendingEvents.enqueue({ + type: "LockGroupEvent", + event, + }); + } + + doLockGroup(event: LockGroupMessageInterface): void { + const group = this.groups.get(event.groupId); + if (!group) { + return; + } + group.setTexture(event.lock ? "circleSprite-red" : "circleSprite-white"); + } + doUpdatePlayerDetails(message: PlayerDetailsUpdatedMessageInterface): void { const character = this.MapPlayersByKey.get(message.userId); if (character === undefined) { diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 4ccaed4b..4708f37d 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -434,6 +434,7 @@ message SubToPusherMessage { EmoteEventMessage emoteEventMessage = 9; ErrorMessage errorMessage = 10; PlayerDetailsUpdatedMessage playerDetailsUpdatedMessage = 11; + LockGroupMessage lockGroupMessage = 12; } } diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 1bf1090a..50d684a1 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -18,6 +18,7 @@ import { ErrorMessage, PlayerDetailsUpdatedMessage, SetPlayerDetailsMessage, + LockGroupMessage, } from "../Messages/generated/messages_pb"; import { ClientReadableStream } from "grpc"; import { PositionDispatcher } from "_Model/PositionDispatcher"; @@ -32,7 +33,7 @@ export interface ZoneEventListener { onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupLeaves(groupId: number, listener: ExSocketInterface): void; - // onGroupLock(groupId: number, listener: ExSocketInterface): void; + onLockGroup(lockGroupMessage: LockGroupMessage, listener: ExSocketInterface): void; onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void; onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void; onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ExSocketInterface): void; @@ -246,9 +247,9 @@ export class Zone { } else if (message.hasEmoteeventmessage()) { const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; this.notifyEmote(emoteEventMessage); - } else if (message.hasEmoteeventmessage()) { - const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; - this.notifyEmote(emoteEventMessage); + } else if (message.hasLockgroupmessage()) { + const lockGroupMessage = message.getLockgroupmessage() as LockGroupMessage; + this.notifyLockGroup(lockGroupMessage); } else if (message.hasPlayerdetailsupdatedmessage()) { const playerDetailsUpdatedMessage = message.getPlayerdetailsupdatedmessage() as PlayerDetailsUpdatedMessage; @@ -359,6 +360,12 @@ export class Zone { } } + private notifyLockGroup(lockGroupMessage: LockGroupMessage) { + for (const listener of this.listeners) { + this.socketListener.onLockGroup(lockGroupMessage, listener); + } + } + private notifyPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage) { for (const listener of this.listeners) { if (listener.userId === playerDetailsUpdatedMessage.getUserid()) { diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index b1006bf8..96e599e0 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -306,6 +306,13 @@ export class SocketManager implements ZoneEventListener { emitInBatch(listener, subMessage); } + onLockGroup(lockGroupMessage: LockGroupMessage, listener: ExSocketInterface): void { + const subMessage = new SubMessage(); + subMessage.setLockgroupmessage(lockGroupMessage); + + emitInBatch(listener, subMessage); + } + onPlayerDetailsUpdated( playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ExSocketInterface From a0205bf682ef301922c99c587aa493bd4b7c68ea Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Wed, 2 Mar 2022 17:00:43 +0100 Subject: [PATCH 06/64] cleanup --- back/src/Model/GameRoom.ts | 2 - back/src/Model/PositionNotifier.ts | 2 - back/src/Model/Zone.ts | 1 - back/src/RoomManager.ts | 2 - back/src/Services/SocketManager.ts | 15 ++------ front/src/Components/CameraControls.svelte | 30 ++++++++++++++- front/src/Components/images/lock.svg | 1 + .../images/lock.svg:Zone.Identifier | 3 ++ front/src/Connexion/ConnexionModels.ts | 2 +- front/src/Phaser/Game/GameScene.ts | 37 ++++++++----------- front/src/Stores/CurrentPlayerGroupStore.ts | 4 ++ messages/protos/messages.proto | 1 - pusher/src/Model/Zone.ts | 1 - 13 files changed, 57 insertions(+), 44 deletions(-) create mode 100644 front/src/Components/images/lock.svg create mode 100644 front/src/Components/images/lock.svg:Zone.Identifier create mode 100644 front/src/Stores/CurrentPlayerGroupStore.ts diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index d6a6c7c2..74284ab2 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -551,8 +551,6 @@ export class GameRoom { } public emitLockGroupMessage(user: User, lockGroupMessage: LockGroupMessage) { - console.log("D2 GAME ROOM EMIT LOCK GROUP MESSAGE"); - console.log(lockGroupMessage.getGroupid()); this.positionNotifier.emitLockGroupEvent(user, lockGroupMessage); } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index ad7334f9..6e1fb195 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -143,8 +143,6 @@ export class PositionNotifier { public emitLockGroupEvent(user: User, lockGroupMessage: LockGroupMessage) { const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); const zone = this.getZone(zoneDesc.i, zoneDesc.j); - console.log("D3 emit from ZONE"); - console.log(lockGroupMessage.getGroupid()); zone.emitLockGroupEvent(lockGroupMessage); } diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index 35bb4660..4f49ff55 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -112,7 +112,6 @@ export class Zone { } public emitLockGroupEvent(lockGroupMessage: LockGroupMessage) { - console.log("D4 ZONE ON LOCK GROUP CALLBACK"); for (const listener of this.listeners) { this.onLockGroup(lockGroupMessage, listener); } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 6a091054..8a840c15 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -155,8 +155,6 @@ const roomManager: IRoomManagerServer = { user, setPlayerDetailsMessage as SetPlayerDetailsMessage ); - } else if (message.hasLockgroupmessage()) { - console.log("===== GOT LOCK GROUP MESSAGE FROM CLIENT ====="); } else { throw new Error("Unhandled message type"); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index aaa373fb..2bfe7cf2 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -70,7 +70,6 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo // TODO: should we batch those every 100ms? const batchMessage = new BatchToPusherMessage(); batchMessage.addPayload(subMessage); - socket.write(batchMessage); } @@ -277,8 +276,9 @@ export class SocketManager { }, MINIMUM_DISTANCE, GROUP_RADIUS, - (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => - this.onZoneEnter(thing, fromZone, listener), + (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => { + this.onZoneEnter(thing, fromZone, listener); + }, (thing: Movable, position: PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => @@ -393,17 +393,12 @@ export class SocketManager { private onLockGroup(lockGroupMessage: LockGroupMessage, client: ZoneSocket) { const subMessage = new SubToPusherMessage(); subMessage.setLockgroupmessage(lockGroupMessage); - console.log("D5 SOCKET MANAGER ON LOCK GROUP"); - console.log(lockGroupMessage.getGroupid()); - console.log(lockGroupMessage.getLock()); - emitZoneMessage(subMessage, client); } private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) { const subMessage = new SubToPusherMessage(); subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage); - emitZoneMessage(subMessage, client); } @@ -433,7 +428,6 @@ export class SocketManager { const subMessage = new SubToPusherMessage(); subMessage.setGroupleftzonemessage(groupDeleteMessage); - emitZoneMessage(subMessage, client); //user.emitInBatch(subMessage); } @@ -445,7 +439,6 @@ export class SocketManager { const subMessage = new SubToPusherMessage(); subMessage.setUserleftzonemessage(userLeftMessage); - emitZoneMessage(subMessage, client); } @@ -667,6 +660,7 @@ export class SocketManager { const groupUpdateMessage = new GroupUpdateZoneMessage(); groupUpdateMessage.setGroupid(thing.getId()); groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition())); + groupUpdateMessage.setLocked(thing.isLocked()); const subMessage = new SubToPusherMessage(); subMessage.setGroupupdatezonemessage(groupUpdateMessage); @@ -905,7 +899,6 @@ export class SocketManager { } handleLockGroupMessage(room: GameRoom, user: User, message: LockGroupMessage) { - console.log("D1 HANDLE LOCK GROUP MESSAGE"); const group = user.group; if (!group) { return; diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index bfe1d9d0..7b189aff 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -10,12 +10,14 @@ import layoutPresentationImg from "./images/layout-presentation.svg"; import layoutChatImg from "./images/layout-chat.svg"; import followImg from "./images/follow.svg"; + import lockImg from "./images/lock.svg"; import { LayoutMode } from "../WebRtc/LayoutManager"; import { peerStore } from "../Stores/PeerStore"; import { onDestroy } from "svelte"; import { embedScreenLayout } from "../Stores/EmbedScreensStore"; import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore"; import { gameManager } from "../Phaser/Game/GameManager"; + import { currentPlayerGroupIdStore, currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore"; const gameScene = gameManager.getCurrentGameScene(); @@ -70,6 +72,15 @@ } } + function lockClick() { + console.log($currentPlayerGroupIdStore); + console.log($currentPlayerGroupLockStateStore); + if ($currentPlayerGroupIdStore === undefined) { + return; + } + gameScene.connection?.emitLockGroup($currentPlayerGroupIdStore, !$currentPlayerGroupLockStateStore); + } + let isSilent: boolean; const unsubscribeIsSilent = isSilentStore.subscribe((value) => { isSilent = value; @@ -95,6 +106,15 @@ +
+ +
+
\ No newline at end of file diff --git a/front/src/Components/images/lock.svg:Zone.Identifier b/front/src/Components/images/lock.svg:Zone.Identifier new file mode 100644 index 00000000..053d1127 --- /dev/null +++ b/front/src/Components/images/lock.svg:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=about:internet diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 5b41e602..167624e6 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -50,7 +50,7 @@ export interface GroupCreatedUpdatedMessageInterface { export interface LockGroupMessageInterface { groupId: number; - locked: boolean; + lock: boolean; } export interface GroupUsersUpdateMessageInterface { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7b92a5d1..8b2112b9 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -77,6 +77,7 @@ import { userIsAdminStore } from "../../Stores/GameStore"; import { contactPageStore } from "../../Stores/MenuStore"; import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; import { audioManagerFileStore } from "../../Stores/AudioManagerStore"; +import { currentPlayerGroupIdStore, currentPlayerGroupLockStateStore } from "../../Stores/CurrentPlayerGroupStore"; import EVENT_TYPE = Phaser.Scenes.Events; import Texture = Phaser.Textures.Texture; @@ -153,7 +154,6 @@ export class GameScene extends DirtyScene { mapFile!: ITiledMap; animatedTiles!: AnimatedTiles; groups: Map; - currentPlayerGroupId?: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; pendingEvents = new Queue< @@ -185,6 +185,7 @@ export class GameScene extends DirtyScene { private volumeStoreUnsubscribers: Map = new Map(); private localVolumeStoreUnsubscriber: Unsubscriber | undefined; private followUsersColorStoreUnsubscribe!: Unsubscriber; + private currentPlayerGroupIdStoreUnsubscribe!: Unsubscriber; private biggestAvailableAreaStoreUnsubscribe!: () => void; MapUrlFile: string; @@ -226,6 +227,7 @@ export class GameScene extends DirtyScene { private loader: Loader; private lastCameraEvent: WasCameraUpdatedEvent | undefined; private firstCameraUpdateSent: boolean = false; + private currentPlayerGroupId?: number; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -233,7 +235,6 @@ export class GameScene extends DirtyScene { }); this.Terrains = []; this.groups = new Map(); - this.currentPlayerGroupId = undefined; this.instance = room.getInstance(); this.MapUrlFile = MapUrlFile; @@ -717,6 +718,10 @@ export class GameScene extends DirtyScene { } }); + this.currentPlayerGroupIdStoreUnsubscribe = currentPlayerGroupIdStore.subscribe((groupId) => { + this.currentPlayerGroupId = groupId; + }); + Promise.all([this.connectionAnswerPromise as Promise, ...scriptPromises]) .then(() => { this.scene.wake(); @@ -727,20 +732,6 @@ export class GameScene extends DirtyScene { e ) ); - - this.input.keyboard.on("keydown-L", (event: Event) => { - if (this.currentPlayerGroupId !== undefined) { - console.log("group locked"); - this.connection?.emitLockGroup(this.currentPlayerGroupId, true); - } - }); - - this.input.keyboard.on("keydown-U", (event: Event) => { - if (this.currentPlayerGroupId !== undefined) { - console.log("group unlocked"); - this.connection?.emitLockGroup(this.currentPlayerGroupId, false); - } - }); } /** @@ -855,9 +846,8 @@ export class GameScene extends DirtyScene { }); this.connection.groupUsersUpdateMessageStream.subscribe((message) => { - this.currentPlayerGroupId = message.groupId; - console.log("GOT GROUP USERS UPDATE MESSAGE"); - console.log(message); + // TODO: how else can we deduce our current group? + currentPlayerGroupIdStore.set(message.groupId); }); /** @@ -1849,9 +1839,8 @@ ${escapedMessage} break; case "DeleteGroupEvent": { this.doDeleteGroup(event.groupId); - if (this.currentPlayerGroupId === event.groupId) { - this.currentPlayerGroupId = undefined; - } + currentPlayerGroupIdStore.set(undefined); + currentPlayerGroupLockStateStore.set(undefined); break; } case "LockGroupEvent": { @@ -2033,6 +2022,9 @@ ${escapedMessage} sprite.setDisplayOrigin(48, 48); this.add.existing(sprite); this.groups.set(groupPositionMessage.groupId, sprite); + if (this.currentPlayerGroupId === groupPositionMessage.groupId) { + currentPlayerGroupLockStateStore.set(groupPositionMessage.locked); + } return sprite; } @@ -2064,6 +2056,7 @@ ${escapedMessage} if (!group) { return; } + currentPlayerGroupLockStateStore.set(event.lock); group.setTexture(event.lock ? "circleSprite-red" : "circleSprite-white"); } diff --git a/front/src/Stores/CurrentPlayerGroupStore.ts b/front/src/Stores/CurrentPlayerGroupStore.ts new file mode 100644 index 00000000..cda46325 --- /dev/null +++ b/front/src/Stores/CurrentPlayerGroupStore.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; + +export const currentPlayerGroupIdStore = writable(undefined); +export const currentPlayerGroupLockStateStore = writable(undefined); diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 4708f37d..3db0da95 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -324,7 +324,6 @@ message ServerToClientMessage { FollowConfirmationMessage followConfirmationMessage = 22; FollowAbortMessage followAbortMessage = 23; GroupUsersUpdateMessage groupUsersUpdateMessage = 24; - LockGroupMessage lockGroupMessage = 25; } } diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 50d684a1..451597b9 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -155,7 +155,6 @@ export class GroupDescriptor { groupUpdateMessage.setGroupsize(this.groupSize); groupUpdateMessage.setPosition(this.position); groupUpdateMessage.setLocked(this.locked); - return groupUpdateMessage; } } From a0535ed4a84cff750f7e23f8ae27dcd306723085 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 3 Mar 2022 12:22:16 +0100 Subject: [PATCH 07/64] Updating GroupDescriptors on LockGroupMessage --- back/src/RoomManager.ts | 6 +++--- back/src/Services/SocketManager.ts | 5 +++-- front/src/Components/CameraControls.svelte | 9 ++------- front/src/Connexion/RoomConnection.ts | 7 +++---- messages/protos/messages.proto | 8 ++++++-- pusher/src/Controller/IoSocketController.ts | 8 ++++++-- pusher/src/Model/PositionDispatcher.ts | 1 + pusher/src/Model/Zone.ts | 14 ++++++++++++-- pusher/src/Services/SocketManager.ts | 6 ++++-- 9 files changed, 40 insertions(+), 24 deletions(-) diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 8a840c15..2705787e 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -29,7 +29,7 @@ import { WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, ZoneMessage, - LockGroupMessage, + LockGroupPromptMessage, } from "./Messages/generated/messages_pb"; import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; import { socketManager } from "./Services/SocketManager"; @@ -136,11 +136,11 @@ const roomManager: IRoomManagerServer = { user, message.getFollowabortmessage() as FollowAbortMessage ); - } else if (message.hasLockgroupmessage()) { + } else if (message.hasLockgrouppromptmessage()) { socketManager.handleLockGroupMessage( room, user, - message.getLockgroupmessage() as LockGroupMessage + message.getLockgrouppromptmessage() as LockGroupPromptMessage ); } else if (message.hasSendusermessage()) { const sendUserMessage = message.getSendusermessage(); diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 2bfe7cf2..0f78427f 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -40,6 +40,7 @@ import { PlayerDetailsUpdatedMessage, GroupUsersUpdateMessage, LockGroupMessage, + LockGroupPromptMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -898,7 +899,7 @@ export class SocketManager { } } - handleLockGroupMessage(room: GameRoom, user: User, message: LockGroupMessage) { + handleLockGroupMessage(room: GameRoom, user: User, message: LockGroupPromptMessage) { const group = user.group; if (!group) { return; @@ -906,7 +907,7 @@ export class SocketManager { group.lock(message.getLock()); const lockGroupMessage = new LockGroupMessage(); lockGroupMessage.setLock(message.getLock()); - lockGroupMessage.setGroupid(message.getGroupid()); + lockGroupMessage.setGroupid(group.getId()); room.emitLockGroupMessage(user, lockGroupMessage); } } diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index 7b189aff..da207df1 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -17,7 +17,7 @@ import { embedScreenLayout } from "../Stores/EmbedScreensStore"; import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore"; import { gameManager } from "../Phaser/Game/GameManager"; - import { currentPlayerGroupIdStore, currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore"; + import { currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore"; const gameScene = gameManager.getCurrentGameScene(); @@ -73,12 +73,7 @@ } function lockClick() { - console.log($currentPlayerGroupIdStore); - console.log($currentPlayerGroupLockStateStore); - if ($currentPlayerGroupIdStore === undefined) { - return; - } - gameScene.connection?.emitLockGroup($currentPlayerGroupIdStore, !$currentPlayerGroupLockStateStore); + gameScene.connection?.emitLockGroup(!$currentPlayerGroupLockStateStore); } let isSilent: boolean; diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 3d69f38a..d0cc5c48 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -858,12 +858,11 @@ export class RoomConnection implements RoomConnection { this.socket.send(bytes); } - public emitLockGroup(groupId: number, lock: boolean = true): void { + public emitLockGroup(lock: boolean = true): void { const bytes = ClientToServerMessageTsProto.encode({ message: { - $case: "lockGroupMessage", - lockGroupMessage: { - groupId, + $case: "lockGroupPromptMessage", + lockGroupPromptMessage: { lock, }, }, diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 3db0da95..b6e8d98f 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -98,6 +98,10 @@ message FollowAbortMessage { int32 follower = 2; } +message LockGroupPromptMessage { + bool lock = 1; +} + message LockGroupMessage { int32 groupId = 1; bool lock = 2; @@ -121,7 +125,7 @@ message ClientToServerMessage { FollowRequestMessage followRequestMessage = 15; FollowConfirmationMessage followConfirmationMessage = 16; FollowAbortMessage followAbortMessage = 17; - LockGroupMessage lockGroupMessage = 18; + LockGroupPromptMessage lockGroupPromptMessage = 18; } } @@ -412,7 +416,7 @@ message PusherToBackMessage { FollowRequestMessage followRequestMessage = 16; FollowConfirmationMessage followConfirmationMessage = 17; FollowAbortMessage followAbortMessage = 18; - LockGroupMessage lockGroupMessage = 19; + LockGroupPromptMessage lockGroupPromptMessage = 19; } } diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 617da341..7c7928d6 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -22,6 +22,7 @@ import { FollowAbortMessage, VariableMessage, LockGroupMessage, + LockGroupPromptMessage, } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb"; import { TemplatedApp } from "uWebSockets.js"; @@ -495,8 +496,11 @@ export class IoSocketController { ); } else if (message.hasFollowabortmessage()) { socketManager.handleFollowAbort(client, message.getFollowabortmessage() as FollowAbortMessage); - } else if (message.hasLockgroupmessage()) { - socketManager.handleLockGroup(client, message.getLockgroupmessage() as LockGroupMessage); + } else if (message.hasLockgrouppromptmessage()) { + socketManager.handleLockGroup( + client, + message.getLockgrouppromptmessage() as LockGroupPromptMessage + ); } /* Ok is false if backpressure was built up, wait for drain */ diff --git a/pusher/src/Model/PositionDispatcher.ts b/pusher/src/Model/PositionDispatcher.ts index f868cd2c..b0eea0ac 100644 --- a/pusher/src/Model/PositionDispatcher.ts +++ b/pusher/src/Model/PositionDispatcher.ts @@ -63,6 +63,7 @@ export class PositionDispatcher { const addedZones = [...newZones].filter((x) => !oldZones.has(x)); const removedZones = [...oldZones].filter((x) => !newZones.has(x)); + console.log("START LISTENING"); for (const zone of addedZones) { zone.startListening(socket); } diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 451597b9..92f82222 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -140,6 +140,10 @@ export class GroupDescriptor { return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position, message.getLocked()); } + public updateFromLockGroupMessage(message: LockGroupMessage): void { + this.locked = message.getLock(); + } + public update(groupDescriptor: GroupDescriptor) { this.groupSize = groupDescriptor.groupSize; this.position = groupDescriptor.position; @@ -214,11 +218,17 @@ export class Zone { this.notifyGroupMove(groupDescriptor); } else { this.groups.set(groupId, groupDescriptor); - const fromZone = groupUpdateZoneMessage.getFromzone(); - this.notifyGroupEnter(groupDescriptor, fromZone?.toObject()); } + } else if (message.hasLockgroupmessage()) { + const lockGroupMessage = message.getLockgroupmessage() as LockGroupMessage; + const groupId = lockGroupMessage.getGroupid(); + const oldGroupDescriptor = this.groups.get(groupId); + if (oldGroupDescriptor !== undefined) { + oldGroupDescriptor.updateFromLockGroupMessage(lockGroupMessage); + this.notifyLockGroup(lockGroupMessage); + } } else if (message.hasUserleftzonemessage()) { const userLeftMessage = message.getUserleftzonemessage() as UserLeftZoneMessage; this.users.delete(userLeftMessage.getUserid()); diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 96e599e0..30f41f2a 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -39,6 +39,7 @@ import { WorldFullMessage, PlayerDetailsUpdatedMessage, LockGroupMessage, + LockGroupPromptMessage, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; @@ -293,9 +294,9 @@ export class SocketManager implements ZoneEventListener { client.backConnection.write(pusherToBackMessage); } - handleLockGroup(client: ExSocketInterface, message: LockGroupMessage): void { + handleLockGroup(client: ExSocketInterface, message: LockGroupPromptMessage): void { const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setLockgroupmessage(message); + pusherToBackMessage.setLockgrouppromptmessage(message); client.backConnection.write(pusherToBackMessage); } @@ -614,6 +615,7 @@ export class SocketManager implements ZoneEventListener { public onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void { const subMessage = new SubMessage(); + console.log("ON GROUP ENTERS"); subMessage.setGroupupdatemessage(group.toGroupUpdateMessage()); emitInBatch(listener, subMessage); From 5706e5c41631f6e038f36ae7d8941219b0e80ce7 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 3 Mar 2022 12:31:17 +0100 Subject: [PATCH 08/64] remove console logs --- pusher/src/Model/PositionDispatcher.ts | 1 - pusher/src/Services/SocketManager.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/pusher/src/Model/PositionDispatcher.ts b/pusher/src/Model/PositionDispatcher.ts index b0eea0ac..f868cd2c 100644 --- a/pusher/src/Model/PositionDispatcher.ts +++ b/pusher/src/Model/PositionDispatcher.ts @@ -63,7 +63,6 @@ export class PositionDispatcher { const addedZones = [...newZones].filter((x) => !oldZones.has(x)); const removedZones = [...oldZones].filter((x) => !newZones.has(x)); - console.log("START LISTENING"); for (const zone of addedZones) { zone.startListening(socket); } diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 30f41f2a..436ecd39 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -615,7 +615,6 @@ export class SocketManager implements ZoneEventListener { public onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void { const subMessage = new SubMessage(); - console.log("ON GROUP ENTERS"); subMessage.setGroupupdatemessage(group.toGroupUpdateMessage()); emitInBatch(listener, subMessage); From 363b906cb825d9019f36463ead8e445f9ec5e9c5 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 3 Mar 2022 14:36:16 +0100 Subject: [PATCH 09/64] group circles with color fill --- front/src/Components/CameraControls.svelte | 2 +- front/src/Phaser/Game/GameScene.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index da207df1..c22900ff 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -103,7 +103,7 @@
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8b2112b9..8b0793e0 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -980,7 +980,9 @@ export class GameScene extends DirtyScene { context.arc(48, 48, 48, 0, 2 * Math.PI, false); // context.lineWidth = 5; context.strokeStyle = "#ffffff"; + context.fillStyle = "#ffffff44"; context.stroke(); + context.fill(); this.circleTexture.refresh(); //create red circle canvas use to create sprite @@ -990,7 +992,9 @@ export class GameScene extends DirtyScene { contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); //context.lineWidth = 5; contextRed.strokeStyle = "#ff0000"; + contextRed.fillStyle = "#ff000044"; contextRed.stroke(); + contextRed.fill(); this.circleRedTexture.refresh(); } From ba612c49ed482e2af89ed96e7058e4c62d31d0a0 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 3 Mar 2022 14:47:42 +0100 Subject: [PATCH 10/64] fix GameRoom text --- back/tests/GameRoomTest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/back/tests/GameRoomTest.ts b/back/tests/GameRoomTest.ts index d4e83daf..fb9f09fb 100644 --- a/back/tests/GameRoomTest.ts +++ b/back/tests/GameRoomTest.ts @@ -52,6 +52,7 @@ describe("GameRoom", () => { () => {}, () => {}, emote, + () => {}, () => {} ); @@ -88,6 +89,7 @@ describe("GameRoom", () => { () => {}, () => {}, emote, + () => {}, () => {} ); @@ -128,6 +130,7 @@ describe("GameRoom", () => { () => {}, () => {}, emote, + () => {}, () => {} ); From 3c74b2b554fec3dc221ffde8b766917657371296 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 3 Mar 2022 15:21:44 +0100 Subject: [PATCH 11/64] remove obsolete check --- front/src/Components/CameraControls.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index c22900ff..b9cb7801 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -103,7 +103,7 @@
From 3a5aa3c3c55979208839659aaf57d00900910ab6 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Wed, 9 Mar 2022 16:51:11 +0100 Subject: [PATCH 12/64] cr fixes #1 wip --- back/src/Model/GameRoom.ts | 14 ++++++++----- back/src/Services/SocketManager.ts | 20 ++++++++++++++----- .../images/lock.svg:Zone.Identifier | 3 --- front/src/Phaser/Game/GameScene.ts | 1 + 4 files changed, 25 insertions(+), 13 deletions(-) delete mode 100644 front/src/Components/images/lock.svg:Zone.Identifier diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 74284ab2..a073f0e5 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -46,7 +46,7 @@ export class GameRoom { // Users, sorted by ID private readonly users = new Map(); private readonly usersByUuid = new Map(); - private readonly groups = new Set(); + private readonly groups: Map = new Map(); private readonly admins = new Set(); private itemsState = new Map(); @@ -250,7 +250,7 @@ export class GameRoom { this.disconnectCallback, this.positionNotifier ); - this.groups.add(group); + this.groups.set(group.getId(), group); } } } else { @@ -334,7 +334,7 @@ export class GameRoom { this.disconnectCallback, this.positionNotifier ); - this.groups.add(newGroup); + this.groups.set(newGroup.getId(), newGroup); } else { this.leaveGroup(user); } @@ -381,10 +381,10 @@ export class GameRoom { group.leave(user); if (group.isEmpty()) { group.destroy(); - if (!this.groups.has(group)) { + if (!this.groups.has(group.getId())) { throw new Error(`Could not find group ${group.getId()} referenced by user ${user.id} in World.`); } - this.groups.delete(group); + this.groups.delete(group.getId()); //todo: is the group garbage collected? } else { group.updatePosition(); @@ -664,4 +664,8 @@ export class GameRoom { const variablesManager = await this.getVariableManager(); return variablesManager.getVariablesForTags(tags); } + + public getGroupById(id: number): Group | undefined { + return this.groups.get(id); + } } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 0f78427f..f3f34f9b 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -41,6 +41,7 @@ import { GroupUsersUpdateMessage, LockGroupMessage, LockGroupPromptMessage, + RoomMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -287,7 +288,7 @@ export class SocketManager { (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), (lockGroupMessage: LockGroupMessage, listener: ZoneSocket) => - this.onLockGroup(lockGroupMessage, listener), + this.onLockGroup(lockGroupMessage, listener, roomPromise), (playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) => this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener) ) @@ -391,10 +392,19 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private onLockGroup(lockGroupMessage: LockGroupMessage, client: ZoneSocket) { - const subMessage = new SubToPusherMessage(); - subMessage.setLockgroupmessage(lockGroupMessage); - emitZoneMessage(subMessage, client); + private async onLockGroup( + lockGroupMessage: LockGroupMessage, + client: ZoneSocket, + roomPromise: PromiseLike | undefined + ) { + if (!roomPromise) { + return; + } + const group = (await roomPromise).getGroupById(lockGroupMessage.getGroupid()); + if (!group) { + return; + } + this.emitCreateUpdateGroupEvent(client, null, group); } private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) { diff --git a/front/src/Components/images/lock.svg:Zone.Identifier b/front/src/Components/images/lock.svg:Zone.Identifier deleted file mode 100644 index 053d1127..00000000 --- a/front/src/Components/images/lock.svg:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -HostUrl=about:internet diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8b0793e0..e795979f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -2010,6 +2010,7 @@ ${escapedMessage} } private doShareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { + console.log(groupPositionMessage); //delete previous group this.doDeleteGroup(groupPositionMessage.groupId); From 5a8cba24fa423727ea3f98466d67007d84fb57b4 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 10 Mar 2022 12:20:53 +0100 Subject: [PATCH 13/64] much cleaner approach to group lock update --- back/src/Model/GameRoom.ts | 5 ++-- back/src/Model/PositionNotifier.ts | 6 ++-- back/src/Model/Zone.ts | 7 ++--- back/src/RoomManager.ts | 2 +- back/src/Services/SocketManager.ts | 17 +++-------- front/src/Connexion/ConnexionModels.ts | 5 ---- front/src/Connexion/RoomConnection.ts | 8 ------ front/src/Phaser/Game/GameScene.ts | 31 --------------------- messages/protos/messages.proto | 7 ----- pusher/src/Controller/IoSocketController.ts | 1 - pusher/src/Model/Zone.ts | 23 --------------- pusher/src/Services/SocketManager.ts | 8 ------ 12 files changed, 13 insertions(+), 107 deletions(-) diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index a073f0e5..c1c1c346 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -23,7 +23,6 @@ import { VariableMessage, VariableWithTagMessage, ServerToClientMessage, - LockGroupMessage, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { RoomSocket, ZoneSocket } from "src/RoomManager"; @@ -550,8 +549,8 @@ export class GameRoom { this.positionNotifier.emitEmoteEvent(user, emoteEventMessage); } - public emitLockGroupMessage(user: User, lockGroupMessage: LockGroupMessage) { - this.positionNotifier.emitLockGroupEvent(user, lockGroupMessage); + public emitLockGroupEvent(user: User, groupId: number) { + this.positionNotifier.emitLockGroupEvent(user, groupId); } public addRoomListener(socket: RoomSocket) { diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index 6e1fb195..38d538ec 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -21,7 +21,7 @@ import { Movable } from "_Model/Movable"; import { PositionInterface } from "_Model/PositionInterface"; import { ZoneSocket } from "../RoomManager"; import { User } from "../Model/User"; -import { EmoteEventMessage, LockGroupMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb"; +import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb"; interface ZoneDescriptor { i: number; @@ -140,10 +140,10 @@ export class PositionNotifier { zone.emitEmoteEvent(emoteEventMessage); } - public emitLockGroupEvent(user: User, lockGroupMessage: LockGroupMessage) { + public emitLockGroupEvent(user: User, groupId: number) { const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); const zone = this.getZone(zoneDesc.i, zoneDesc.j); - zone.emitLockGroupEvent(lockGroupMessage); + zone.emitLockGroupEvent(groupId); } public *getAllUsersInSquareAroundZone(zone: Zone): Generator { diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index 4f49ff55..2d0cefd5 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -7,14 +7,13 @@ import { EmoteEventMessage, SetPlayerDetailsMessage, PlayerDetailsUpdatedMessage, - LockGroupMessage, } from "../Messages/generated/messages_pb"; export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void; export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; -export type LockGroupCallback = (lockGroupMessage: LockGroupMessage, listener: ZoneSocket) => void; +export type LockGroupCallback = (groupId: number, listener: ZoneSocket) => void; export type PlayerDetailsUpdatedCallback = ( playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket @@ -111,9 +110,9 @@ export class Zone { } } - public emitLockGroupEvent(lockGroupMessage: LockGroupMessage) { + public emitLockGroupEvent(groupId: number) { for (const listener of this.listeners) { - this.onLockGroup(lockGroupMessage, listener); + this.onLockGroup(groupId, listener); } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 2705787e..ab886f50 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -137,7 +137,7 @@ const roomManager: IRoomManagerServer = { message.getFollowabortmessage() as FollowAbortMessage ); } else if (message.hasLockgrouppromptmessage()) { - socketManager.handleLockGroupMessage( + socketManager.handleLockGroupPromptMessage( room, user, message.getLockgrouppromptmessage() as LockGroupPromptMessage diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index f3f34f9b..d0151e30 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -39,7 +39,6 @@ import { SetPlayerDetailsMessage, PlayerDetailsUpdatedMessage, GroupUsersUpdateMessage, - LockGroupMessage, LockGroupPromptMessage, RoomMessage, } from "../Messages/generated/messages_pb"; @@ -287,8 +286,7 @@ export class SocketManager { this.onClientLeave(thing, newZone, listener), (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), - (lockGroupMessage: LockGroupMessage, listener: ZoneSocket) => - this.onLockGroup(lockGroupMessage, listener, roomPromise), + (groupId: number, listener: ZoneSocket) => this.onLockGroup(groupId, listener, roomPromise), (playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) => this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener) ) @@ -392,15 +390,11 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private async onLockGroup( - lockGroupMessage: LockGroupMessage, - client: ZoneSocket, - roomPromise: PromiseLike | undefined - ) { + private async onLockGroup(groupId: number, client: ZoneSocket, roomPromise: PromiseLike | undefined) { if (!roomPromise) { return; } - const group = (await roomPromise).getGroupById(lockGroupMessage.getGroupid()); + const group = (await roomPromise).getGroupById(groupId); if (!group) { return; } @@ -915,10 +909,7 @@ export class SocketManager { return; } group.lock(message.getLock()); - const lockGroupMessage = new LockGroupMessage(); - lockGroupMessage.setLock(message.getLock()); - lockGroupMessage.setGroupid(group.getId()); - room.emitLockGroupMessage(user, lockGroupMessage); + room.emitLockGroupEvent(user, group.getId()); } } diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 167624e6..04282abc 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -48,11 +48,6 @@ export interface GroupCreatedUpdatedMessageInterface { locked: boolean; } -export interface LockGroupMessageInterface { - groupId: number; - lock: boolean; -} - export interface GroupUsersUpdateMessageInterface { groupId: number; userIds: number[]; diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index d0cc5c48..48827bc5 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -40,7 +40,6 @@ import { PositionMessage_Direction, SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto, PingMessage as PingMessageTsProto, - LockGroupMessage, } from "../Messages/ts-proto-generated/messages"; import { Subject } from "rxjs"; @@ -102,9 +101,6 @@ export class RoomConnection implements RoomConnection { private readonly _groupUsersUpdateMessageStream = new Subject(); public readonly groupUsersUpdateMessageStream = this._groupUsersUpdateMessageStream.asObservable(); - private readonly _lockGroupMessageStream = new Subject(); - public readonly lockGroupMessageStream = this._lockGroupMessageStream.asObservable(); - private readonly _groupDeleteMessageStream = new Subject(); public readonly groupDeleteMessageStream = this._groupDeleteMessageStream.asObservable(); @@ -268,10 +264,6 @@ export class RoomConnection implements RoomConnection { this._emoteEventMessageStream.next(subMessage.emoteEventMessage); break; } - case "lockGroupMessage": { - this._lockGroupMessageStream.next(subMessage.lockGroupMessage); - break; - } case "playerDetailsUpdatedMessage": { this._playerDetailsUpdatedMessageStream.next(subMessage.playerDetailsUpdatedMessage); break; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index e795979f..15570bb9 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -51,7 +51,6 @@ import { PathfindingManager } from "../../Utils/PathfindingManager"; import { ActivatablesManager } from "./ActivatablesManager"; import type { GroupCreatedUpdatedMessageInterface, - LockGroupMessageInterface, MessageUserMovedInterface, MessageUserPositionInterface, OnConnectInterface, @@ -134,11 +133,6 @@ interface DeleteGroupEventInterface { groupId: number; } -interface LockGroupEventInterface { - type: "LockGroupEvent"; - event: LockGroupMessageInterface; -} - interface PlayerDetailsUpdatedInterface { type: "PlayerDetailsUpdated"; details: PlayerDetailsUpdatedMessageInterface; @@ -163,7 +157,6 @@ export class GameScene extends DirtyScene { | UserMovedEventInterface | GroupCreatedUpdatedEventInterface | DeleteGroupEventInterface - | LockGroupEventInterface | PlayerDetailsUpdatedInterface >(); private initPosition: PositionInterface | null = null; @@ -808,10 +801,6 @@ export class GameScene extends DirtyScene { } }); - this.connection.lockGroupMessageStream.subscribe((message) => { - this.lockGroup(message); - }); - this.connection.onServerDisconnected(() => { console.log("Player disconnected from server. Reloading scene."); this.cleanupClosingScene(); @@ -1847,10 +1836,6 @@ ${escapedMessage} currentPlayerGroupLockStateStore.set(undefined); break; } - case "LockGroupEvent": { - this.doLockGroup(event.event); - break; - } default: { const tmp: never = event; } @@ -2049,22 +2034,6 @@ ${escapedMessage} this.groups.delete(groupId); } - lockGroup(event: LockGroupMessageInterface): void { - this.pendingEvents.enqueue({ - type: "LockGroupEvent", - event, - }); - } - - doLockGroup(event: LockGroupMessageInterface): void { - const group = this.groups.get(event.groupId); - if (!group) { - return; - } - currentPlayerGroupLockStateStore.set(event.lock); - group.setTexture(event.lock ? "circleSprite-red" : "circleSprite-white"); - } - doUpdatePlayerDetails(message: PlayerDetailsUpdatedMessageInterface): void { const character = this.MapPlayersByKey.get(message.userId); if (character === undefined) { diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index b6e8d98f..78858499 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -102,11 +102,6 @@ message LockGroupPromptMessage { bool lock = 1; } -message LockGroupMessage { - int32 groupId = 1; - bool lock = 2; -} - message ClientToServerMessage { oneof message { UserMovesMessage userMovesMessage = 2; @@ -182,7 +177,6 @@ message SubMessage { VariableMessage variableMessage = 8; ErrorMessage errorMessage = 9; PlayerDetailsUpdatedMessage playerDetailsUpdatedMessage = 10; - LockGroupMessage lockGroupMessage = 11; } } @@ -437,7 +431,6 @@ message SubToPusherMessage { EmoteEventMessage emoteEventMessage = 9; ErrorMessage errorMessage = 10; PlayerDetailsUpdatedMessage playerDetailsUpdatedMessage = 11; - LockGroupMessage lockGroupMessage = 12; } } diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 7c7928d6..781b4fc0 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -21,7 +21,6 @@ import { FollowConfirmationMessage, FollowAbortMessage, VariableMessage, - LockGroupMessage, LockGroupPromptMessage, } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb"; diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 92f82222..02a01d8d 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -18,7 +18,6 @@ import { ErrorMessage, PlayerDetailsUpdatedMessage, SetPlayerDetailsMessage, - LockGroupMessage, } from "../Messages/generated/messages_pb"; import { ClientReadableStream } from "grpc"; import { PositionDispatcher } from "_Model/PositionDispatcher"; @@ -33,7 +32,6 @@ export interface ZoneEventListener { onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void; onGroupLeaves(groupId: number, listener: ExSocketInterface): void; - onLockGroup(lockGroupMessage: LockGroupMessage, listener: ExSocketInterface): void; onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void; onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void; onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ExSocketInterface): void; @@ -140,10 +138,6 @@ export class GroupDescriptor { return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position, message.getLocked()); } - public updateFromLockGroupMessage(message: LockGroupMessage): void { - this.locked = message.getLock(); - } - public update(groupDescriptor: GroupDescriptor) { this.groupSize = groupDescriptor.groupSize; this.position = groupDescriptor.position; @@ -221,14 +215,6 @@ export class Zone { const fromZone = groupUpdateZoneMessage.getFromzone(); this.notifyGroupEnter(groupDescriptor, fromZone?.toObject()); } - } else if (message.hasLockgroupmessage()) { - const lockGroupMessage = message.getLockgroupmessage() as LockGroupMessage; - const groupId = lockGroupMessage.getGroupid(); - const oldGroupDescriptor = this.groups.get(groupId); - if (oldGroupDescriptor !== undefined) { - oldGroupDescriptor.updateFromLockGroupMessage(lockGroupMessage); - this.notifyLockGroup(lockGroupMessage); - } } else if (message.hasUserleftzonemessage()) { const userLeftMessage = message.getUserleftzonemessage() as UserLeftZoneMessage; this.users.delete(userLeftMessage.getUserid()); @@ -256,9 +242,6 @@ export class Zone { } else if (message.hasEmoteeventmessage()) { const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; this.notifyEmote(emoteEventMessage); - } else if (message.hasLockgroupmessage()) { - const lockGroupMessage = message.getLockgroupmessage() as LockGroupMessage; - this.notifyLockGroup(lockGroupMessage); } else if (message.hasPlayerdetailsupdatedmessage()) { const playerDetailsUpdatedMessage = message.getPlayerdetailsupdatedmessage() as PlayerDetailsUpdatedMessage; @@ -369,12 +352,6 @@ export class Zone { } } - private notifyLockGroup(lockGroupMessage: LockGroupMessage) { - for (const listener of this.listeners) { - this.socketListener.onLockGroup(lockGroupMessage, listener); - } - } - private notifyPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage) { for (const listener of this.listeners) { if (listener.userId === playerDetailsUpdatedMessage.getUserid()) { diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 436ecd39..f6e2feb0 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -38,7 +38,6 @@ import { ErrorMessage, WorldFullMessage, PlayerDetailsUpdatedMessage, - LockGroupMessage, LockGroupPromptMessage, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -307,13 +306,6 @@ export class SocketManager implements ZoneEventListener { emitInBatch(listener, subMessage); } - onLockGroup(lockGroupMessage: LockGroupMessage, listener: ExSocketInterface): void { - const subMessage = new SubMessage(); - subMessage.setLockgroupmessage(lockGroupMessage); - - emitInBatch(listener, subMessage); - } - onPlayerDetailsUpdated( playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ExSocketInterface From 767e3b94e3026088db18d4e1537c7e79b646eb62 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 10 Mar 2022 13:19:23 +0100 Subject: [PATCH 14/64] fix compilation error --- back/src/Services/SocketManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index d0151e30..087843b1 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -903,7 +903,7 @@ export class SocketManager { } } - handleLockGroupMessage(room: GameRoom, user: User, message: LockGroupPromptMessage) { + handleLockGroupPromptMessage(room: GameRoom, user: User, message: LockGroupPromptMessage) { const group = user.group; if (!group) { return; From a8cd37bcf9a02936faec07945520fd9cb50d5388 Mon Sep 17 00:00:00 2001 From: Hanusiak Piotr Date: Thu, 10 Mar 2022 15:34:19 +0100 Subject: [PATCH 15/64] well-known values wip --- messages/protos/messages.proto | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 78858499..9d211855 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -1,5 +1,7 @@ syntax = "proto3"; +import "google/protobuf/wrappers.proto"; + /*********** PARTIAL MESSAGES **************/ message PositionMessage { @@ -362,9 +364,9 @@ message UserLeftZoneMessage { message GroupUpdateZoneMessage { int32 groupId = 1; PointMessage position = 2; - int32 groupSize = 3; + google.protobuf.UInt32Value groupSize = 3; Zone fromZone = 4; - bool locked = 5; + google.protobuf.BoolValue locked = 5; } message GroupLeftZoneMessage { From 52b502770252c52794a39379389652cad24a3c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 14 Mar 2022 13:55:30 +0100 Subject: [PATCH 16/64] Adding source maps to production build. --- front/vite.config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/front/vite.config.ts b/front/vite.config.ts index 5b9e259d..ce7ab4cc 100644 --- a/front/vite.config.ts +++ b/front/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; import { envConfig } from "@geprog/vite-plugin-env-config"; import sveltePreprocess from "svelte-preprocess"; -import pluginRewriteAll from 'vite-plugin-rewrite-all'; +import pluginRewriteAll from "vite-plugin-rewrite-all"; export default defineConfig({ server: { @@ -10,7 +10,10 @@ export default defineConfig({ hmr: { // workaround for development in docker clientPort: 80, - } + }, + }, + build: { + sourcemap: true, }, plugins: [ svelte({ From b6b6c7f15fcc9cf6c86f3e1be72e0115f00682b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 14 Mar 2022 14:28:42 +0100 Subject: [PATCH 17/64] Adding error case when texture is empty --- front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts | 6 ++++++ front/src/Phaser/Game/GameScene.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts index f3c9d273..ad78f183 100644 --- a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts +++ b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts @@ -96,6 +96,12 @@ export const createLoadingPromise = ( return; }); + // If for some reason, the "img" is empty, let's reject the promise. + if (!playerResourceDescriptor.img) { + console.warn("Tried to load an empty texture for a Woka"); + rej(playerResourceDescriptor); + return; + } loadPlugin.spritesheet(playerResourceDescriptor.id, playerResourceDescriptor.img, frameConfig); const errorCallback = (file: { src: string }) => { if (file.src !== playerResourceDescriptor.img) return; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4d4c84f5..02c6101a 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -18,7 +18,7 @@ import { soundManager } from "./SoundManager"; import { SharedVariablesManager } from "./SharedVariablesManager"; import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; -import { lazyLoadPlayerCharacterTextures, loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager"; +import { lazyLoadPlayerCharacterTextures } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { iframeListener } from "../../Api/IframeListener"; import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; From 153bffd521216e8293abfed35422571a6541e72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 11:21:48 +0100 Subject: [PATCH 18/64] Wait for main character to be loaded to display the GameScene This change makes sure the character of the current player is fully loaded before we display the game scene. Otherwise, you could have a glitch for 0.5-2 seconds between the GameScene being displayed and the actual character being displayed. --- front/package.json | 1 + front/src/Phaser/Entity/Character.ts | 35 ++++++++++++++++++++++++---- front/src/Phaser/Game/GameScene.ts | 6 ++++- front/yarn.lock | 5 ++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/front/package.json b/front/package.json index 4a0ea649..a52193b2 100644 --- a/front/package.json +++ b/front/package.json @@ -57,6 +57,7 @@ "simple-peer": "^9.11.0", "socket.io-client": "^2.3.0", "standardized-audio-context": "^25.2.4", + "ts-deferred": "^1.0.4", "ts-proto": "^1.96.0", "typesafe-i18n": "^2.59.0", "uuidv4": "^6.2.10", diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 37d92b89..65e4c190 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -18,6 +18,7 @@ import { createColorStore } from "../../Stores/OutlineColorStore"; import type { OutlineableInterface } from "../Game/OutlineableInterface"; import type CancelablePromise from "cancelable-promise"; import { TalkIcon } from "../Components/TalkIcon"; +import { Deferred } from "ts-deferred"; const playerNameY = -25; @@ -50,6 +51,11 @@ export abstract class Character extends Container implements OutlineableInterfac private readonly outlineColorStoreUnsubscribe: Unsubscriber; private texturePromise: CancelablePromise | undefined; + /** + * A deferred promise that resolves when the texture of the character is actually displayed. + */ + private textureLoadedDeferred = new Deferred(); + constructor( scene: GameScene, x: number, @@ -78,6 +84,7 @@ export abstract class Character extends Container implements OutlineableInterfac this.addTextures(textures, frame); this.invisible = false; this.playAnimation(direction, moving); + this.textureLoadedDeferred.resolve(); return this.getSnapshot().then((htmlImageElementSrc) => { this._pictureStore.set(htmlImageElementSrc); }); @@ -92,11 +99,20 @@ export abstract class Character extends Container implements OutlineableInterfac id: "eyes_23", img: "resources/customisation/character_eyes/character_eyes23.png", }, - ]).then((textures) => { - this.addTextures(textures, frame); - this.invisible = false; - this.playAnimation(direction, moving); - }); + ]) + .then((textures) => { + this.addTextures(textures, frame); + this.invisible = false; + this.playAnimation(direction, moving); + this.textureLoadedDeferred.resolve(); + return this.getSnapshot().then((htmlImageElementSrc) => { + this._pictureStore.set(htmlImageElementSrc); + }); + }) + .catch((e) => { + this.textureLoadedDeferred.reject(e); + throw e; + }); }) .finally(() => { this.texturePromise = undefined; @@ -517,4 +533,13 @@ export abstract class Character extends Container implements OutlineableInterfac public characterFarAwayOutline(): void { this.outlineColorStore.characterFarAway(); } + + /** + * Returns a promise that resolves as soon as a texture is displayed for the user. + * The promise will return when the required texture is loaded OR when the fallback texture is loaded (in case + * the required texture could not be loaded). + */ + public getTextureLoadedPromise(): PromiseLike { + return this.textureLoadedDeferred.promise; + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 02c6101a..4ce35372 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -702,7 +702,11 @@ export class GameScene extends DirtyScene { } }); - Promise.all([this.connectionAnswerPromise as Promise, ...scriptPromises]) + Promise.all([ + this.connectionAnswerPromise as Promise, + ...scriptPromises, + this.CurrentPlayer.getTextureLoadedPromise() as Promise, + ]) .then(() => { this.scene.wake(); }) diff --git a/front/yarn.lock b/front/yarn.lock index c7263531..4c6d813d 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2900,6 +2900,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +ts-deferred@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ts-deferred/-/ts-deferred-1.0.4.tgz#58145ebaeef5b8f2a290b8cec3d060839f9489c7" + integrity sha1-WBReuu71uPKikLjOw9Bgg5+Uicc= + ts-node@^10.4.0: version "10.4.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" From b959ce7a6dd2db72f868f6b12b525dccc9d6ff88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 11:32:04 +0100 Subject: [PATCH 19/64] Migrating some promises to Deferred objects to simplify the code. --- front/src/Phaser/Game/GameScene.ts | 31 ++++++++++++------------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4ce35372..fea70920 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -99,6 +99,7 @@ import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures"; import CancelablePromise from "cancelable-promise"; +import { Deferred } from "ts-deferred"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -164,13 +165,9 @@ export class GameScene extends DirtyScene { private playersPositionInterpolator = new PlayersPositionInterpolator(); public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; - private connectionAnswerPromise: Promise; - private connectionAnswerPromiseResolve!: ( - value: RoomJoinedMessageInterface | PromiseLike - ) => void; + private connectionAnswerPromiseDeferred: Deferred; // A promise that will resolve when the "create" method is called (signaling loading is ended) - private createPromise: Promise; - private createPromiseResolve!: (value?: void | PromiseLike) => void; + private createPromiseDeferred: Deferred; private iframeSubscriptionList!: Array; private peerStoreUnsubscribe!: Unsubscriber; private emoteUnsubscribe!: Unsubscriber; @@ -232,12 +229,8 @@ export class GameScene extends DirtyScene { this.MapUrlFile = MapUrlFile; this.roomUrl = room.key; - this.createPromise = new Promise((resolve, reject): void => { - this.createPromiseResolve = resolve; - }); - this.connectionAnswerPromise = new Promise((resolve, reject): void => { - this.connectionAnswerPromiseResolve = resolve; - }); + this.createPromiseDeferred = new Deferred(); + this.connectionAnswerPromiseDeferred = new Deferred(); this.loader = new Loader(this); } @@ -408,11 +401,11 @@ export class GameScene extends DirtyScene { this.load.on("complete", () => { // FIXME: the factory might fail because the resources might not be loaded yet... // We would need to add a loader ended event in addition to the createPromise - this.createPromise + this.createPromiseDeferred.promise .then(async () => { itemFactory.create(this); - const roomJoinedAnswer = await this.connectionAnswerPromise; + const roomJoinedAnswer = await this.connectionAnswerPromiseDeferred.promise; for (const object of objectsOfType) { // TODO: we should pass here a factory to create sprites (maybe?) @@ -609,7 +602,7 @@ export class GameScene extends DirtyScene { } } - this.createPromiseResolve(); + this.createPromiseDeferred.resolve(); // Now, let's load the script, if any const scripts = this.getScriptUrls(this.mapFile); const disableModuleMode = this.getProperty(this.mapFile, GameMapProperties.SCRIPT_DISABLE_MODULE_SUPPORT) as @@ -703,7 +696,7 @@ export class GameScene extends DirtyScene { }); Promise.all([ - this.connectionAnswerPromise as Promise, + this.connectionAnswerPromiseDeferred.promise as Promise, ...scriptPromises, this.CurrentPlayer.getTextureLoadedPromise() as Promise, ]) @@ -872,7 +865,7 @@ export class GameScene extends DirtyScene { ); //this.initUsersPosition(roomJoinedMessage.users); - this.connectionAnswerPromiseResolve(onConnect.room); + this.connectionAnswerPromiseDeferred.resolve(onConnect.room); // Analyze tags to find if we are admin. If yes, show console. if (this.scene.isSleeping()) { @@ -1287,7 +1280,7 @@ ${escapedMessage} iframeListener.registerAnswerer("getState", async () => { // The sharedVariablesManager is not instantiated before the connection is established. So we need to wait // for the connection to send back the answer. - await this.connectionAnswerPromise; + await this.connectionAnswerPromiseDeferred.promise; return { mapUrl: this.MapUrlFile, startLayerName: this.startPositionCalculator.startLayerName, @@ -1310,7 +1303,7 @@ ${escapedMessage} }) ); iframeListener.registerAnswerer("loadTileset", (eventTileset) => { - return this.connectionAnswerPromise.then(() => { + return this.connectionAnswerPromiseDeferred.promise.then(() => { const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/")); //Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1 let newFirstgid = 1; From 79db6c8f3bc72b0d7978a693058127ef1113c332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 17:36:48 +0100 Subject: [PATCH 20/64] Fixing a race condition in Jitsi When setting the name, in rare cases, Jitsi was not initialized yet and setting the name would cause a JS error. We are now waiting for Jitsi to be properly initialized before setting the name. --- front/src/WebRtc/JitsiFactory.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index d328a72d..3d58d9c2 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -197,7 +197,10 @@ class JitsiFactory { options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations. this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); - this.jitsiApi.executeCommand("displayName", playerName); + + this.jitsiApi.addListener("videoConferenceJoined", () => { + this.jitsiApi?.executeCommand("displayName", playerName); + }); this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); From 53b184e82bb0f0bb7aabe16ba2631297a5974a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 15:50:25 +0100 Subject: [PATCH 21/64] Adding support for custom logos The admin can now set custom logos for the login scene and for the loading screen. --- front/src/Components/Login/LoginScene.svelte | 4 +++- front/src/Connexion/Room.ts | 12 +++++++++++ front/src/Phaser/Components/Loader.ts | 21 +++++++++++--------- messages/JsonMessages/MapDetailsData.ts | 4 ++++ pusher/src/Controller/MapController.ts | 8 ++++++++ pusher/src/Services/AdminApi.ts | 12 ++++++++--- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/front/src/Components/Login/LoginScene.svelte b/front/src/Components/Login/LoginScene.svelte index 6b26e659..63a76342 100644 --- a/front/src/Components/Login/LoginScene.svelte +++ b/front/src/Components/Login/LoginScene.svelte @@ -13,6 +13,8 @@ let name = gameManager.getPlayerName() || ""; let startValidating = false; + let logo = gameManager.currentStartedRoom.loginSceneLogo ?? logoImg; + function submit() { startValidating = true; @@ -25,7 +27,7 @@
- WorkAdventure logo +

{$LL.login.input.name.placeholder()}

diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 778f7e35..bf95e48d 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -31,6 +31,8 @@ export class Room { private _group: string | null = null; private _expireOn: Date | undefined; private _canReport: boolean = false; + private _loadingLogo: string | undefined; + private _loginSceneLogo: string | undefined; private constructor(private roomUrl: URL) { this.id = roomUrl.pathname; @@ -126,6 +128,8 @@ export class Room { this._expireOn = new Date(data.expireOn); } this._canReport = data.canReport ?? false; + this._loadingLogo = data.loadingLogo ?? undefined; + this._loginSceneLogo = data.loginSceneLogo ?? undefined; return new MapDetail(data.mapUrl); } else { throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format."); @@ -233,4 +237,12 @@ export class Room { get canReport(): boolean { return this._canReport; } + + get loadingLogo(): string | undefined { + return this._loadingLogo; + } + + get loginSceneLogo(): string | undefined { + return this._loginSceneLogo; + } } diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts index 7eb08e6d..e13272e7 100644 --- a/front/src/Phaser/Components/Loader.ts +++ b/front/src/Phaser/Components/Loader.ts @@ -1,9 +1,8 @@ import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig; import { DirtyScene } from "../Game/DirtyScene"; +import { gameManager } from "../Game/GameManager"; -const LogoNameIndex: string = "logoLoading"; const TextName: string = "Loading..."; -const LogoResource: string = "static/images/logo.png"; const LogoFrame: ImageFrameConfig = { frameWidth: 310, frameHeight: 60 }; const loadingBarHeight: number = 16; @@ -15,6 +14,7 @@ export class Loader { private progressAmount: number = 0; private logo: Phaser.GameObjects.Image | undefined; private loadingText: Phaser.GameObjects.Text | null = null; + private logoNameIndex!: string; public constructor(private scene: Phaser.Scene) {} @@ -24,15 +24,18 @@ export class Loader { return; } + const logoResource = gameManager.currentStartedRoom.loadingLogo ?? "static/images/logo.png"; + this.logoNameIndex = "logoLoading" + logoResource; + const loadingBarWidth: number = Math.floor(this.scene.game.renderer.width / 3); const promiseLoadLogoTexture = new Promise((res) => { - if (this.scene.load.textureManager.exists(LogoNameIndex)) { + if (this.scene.load.textureManager.exists(this.logoNameIndex)) { return res( (this.logo = this.scene.add.image( this.scene.game.renderer.width / 2, this.scene.game.renderer.height / 2 - 150, - LogoNameIndex + this.logoNameIndex )) ); } else { @@ -43,8 +46,8 @@ export class Loader { TextName ); } - this.scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame); - this.scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => { + this.scene.load.spritesheet(this.logoNameIndex, logoResource, LogoFrame); + this.scene.load.once(`filecomplete-spritesheet-${this.logoNameIndex}`, () => { if (this.loadingText) { this.loadingText.destroy(); } @@ -52,7 +55,7 @@ export class Loader { (this.logo = this.scene.add.image( this.scene.game.renderer.width / 2, this.scene.game.renderer.height / 2 - 150, - LogoNameIndex + this.logoNameIndex )) ); }); @@ -86,8 +89,8 @@ export class Loader { } public removeLoader(): void { - if (this.scene.load.textureManager.exists(LogoNameIndex)) { - this.scene.load.textureManager.remove(LogoNameIndex); + if (this.scene.load.textureManager.exists(this.logoNameIndex)) { + this.scene.load.textureManager.remove(this.logoNameIndex); } } diff --git a/messages/JsonMessages/MapDetailsData.ts b/messages/JsonMessages/MapDetailsData.ts index 09500b80..2dbf88ea 100644 --- a/messages/JsonMessages/MapDetailsData.ts +++ b/messages/JsonMessages/MapDetailsData.ts @@ -22,6 +22,10 @@ export const isMapDetailsData = new tg.IsInterface() expireOn: tg.isString, // Whether the "report" feature is enabled or not on this room canReport: tg.isBoolean, + // The URL of the logo image on the loading screen + loadingLogo: tg.isNullable(tg.isString), + // The URL of the logo image on "LoginScene" + loginSceneLogo: tg.isNullable(tg.isString), }) .get(); diff --git a/pusher/src/Controller/MapController.ts b/pusher/src/Controller/MapController.ts index c6243713..bbab821d 100644 --- a/pusher/src/Controller/MapController.ts +++ b/pusher/src/Controller/MapController.ts @@ -88,6 +88,14 @@ export class MapController extends BaseHttpController { * type: boolean|undefined * description: Whether the "report" feature is enabled or not on this room * example: true + * loadingLogo: + * type: string + * description: The URL of the image to be used on the loading page + * example: https://example.com/logo.png + * loginSceneLogo: + * type: string + * description: The URL of the image to be used on the LoginScene + * example: https://example.com/logo_login.png * */ this.app.get("/map", (req, res) => { diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index 9ea84b2d..59df89f9 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -1,7 +1,7 @@ import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable"; import Axios, { AxiosResponse } from "axios"; -import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; -import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect"; +import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; +import { isRoomRedirect, RoomRedirect } from "../Messages/JsonMessages/RoomRedirect"; import { AdminApiData, isAdminApiData } from "../Messages/JsonMessages/AdminApiData"; import * as tg from "generic-type-guard"; import { isNumber } from "generic-type-guard"; @@ -46,10 +46,16 @@ class AdminApi { userId, }; - const res = await Axios.get(ADMIN_API_URL + "/api/map", { + const res = await Axios.get>(ADMIN_API_URL + "/api/map", { headers: { Authorization: `${ADMIN_API_TOKEN}` }, params, }); + if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) { + throw new Error( + "Invalid answer received from the admin for the /api/map endpoint. Received: " + + JSON.stringify(res.data) + ); + } return res.data; } From 37e824c494dc1cf6765ebf7befd779e4d73c7640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 16 Mar 2022 15:27:58 +0100 Subject: [PATCH 22/64] Fixing setInterval not freed for some volume analyzers Also: using a 256 fftSize instead of 2048 to save on CPU cycles. --- front/src/Phaser/Components/SoundMeter.ts | 4 ++-- front/src/Stores/MediaStore.ts | 9 ++++++++- front/src/WebRtc/VideoPeer.ts | 10 +++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/front/src/Phaser/Components/SoundMeter.ts b/front/src/Phaser/Components/SoundMeter.ts index 6e12912f..86ea610f 100644 --- a/front/src/Phaser/Components/SoundMeter.ts +++ b/front/src/Phaser/Components/SoundMeter.ts @@ -57,8 +57,8 @@ export class SoundMeter { this.context = context; this.analyser = this.context.createAnalyser(); - this.analyser.fftSize = 2048; - const bufferLength = this.analyser.fftSize; + this.analyser.fftSize = 256; + const bufferLength = this.analyser.frequencyBinCount; this.dataArray = new Uint8Array(bufferLength); } diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index 9494eb7e..b86a97ce 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -545,8 +545,12 @@ export const obtainedMediaConstraintStore = derived(undefined, (set) => { let timeout: ReturnType; + let soundMeter: SoundMeter; const unsubscribe = localStreamStore.subscribe((localStreamStoreValue) => { clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } if (localStreamStoreValue.type === "error") { set(undefined); return; @@ -557,7 +561,7 @@ export const localVolumeStore = readable(undefined, (set) => set(undefined); return; } - const soundMeter = new SoundMeter(mediaStream); + soundMeter = new SoundMeter(mediaStream); let error = false; timeout = setInterval(() => { @@ -575,6 +579,9 @@ export const localVolumeStore = readable(undefined, (set) => return () => { unsubscribe(); clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } }; }); diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index a1ffa14c..50c3e19f 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -74,12 +74,17 @@ export class VideoPeer extends Peer { this.volumeStore = readable(undefined, (set) => { let timeout: ReturnType; + let soundMeter: SoundMeter; const unsubscribe = this.streamStore.subscribe((mediaStream) => { + clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } if (mediaStream === null || mediaStream.getAudioTracks().length <= 0) { set(undefined); return; } - const soundMeter = new SoundMeter(mediaStream); + soundMeter = new SoundMeter(mediaStream); let error = false; timeout = setInterval(() => { @@ -97,6 +102,9 @@ export class VideoPeer extends Peer { return () => { unsubscribe(); clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } }; }); From d9407a34285cfde6f25970d9936ebb2a088b4aa4 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 15 Mar 2022 14:56:11 +0100 Subject: [PATCH 23/64] Add * in access location of nginx configuration Signed-off-by: Gregoire Parant --- front/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/nginx.conf b/front/nginx.conf index 9521137c..876ee916 100644 --- a/front/nginx.conf +++ b/front/nginx.conf @@ -45,7 +45,7 @@ server { rewrite ^/jwt /index.html break; } - location ~ ^/[@_]/ { + location ~ ^/[@_*]/ { try_files $uri $uri/ /index.html; } } From 33e78060d4267170a3c4baf35c22db4c4e478481 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Thu, 17 Mar 2022 17:50:39 +0100 Subject: [PATCH 24/64] Stabilize screensharing (#1982) Co-authored-by: Alexis Faizeau --- front/src/WebRtc/SimplePeer.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index fdd95748..c2d17241 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -125,7 +125,6 @@ export class SimplePeer { if (!peerConnexionDeleted) { throw new Error("Error to delete peer connection"); } - //return this.createPeerConnection(user, localStream); } else { peerConnection.toClose = false; return null; @@ -171,6 +170,7 @@ export class SimplePeer { stream: MediaStream | null ): ScreenSharingPeer | null { const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId); + if (peerConnection) { if (peerConnection.destroyed) { peerConnection.toClose = true; @@ -182,8 +182,8 @@ export class SimplePeer { this.createPeerConnection(user); } else { peerConnection.toClose = false; + return null; } - return null; } // Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing) @@ -201,6 +201,9 @@ export class SimplePeer { this.Connection, stream ); + + peer.toClose = false; + this.PeerScreenSharingConnectionArray.set(user.userId, peer); screenSharingPeerStore.pushNewPeer(peer); @@ -265,10 +268,13 @@ export class SimplePeer { } // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. + peer.toClose = true; peer.destroy(); } catch (err) { console.error("closeConnection", err); } + + screenSharingPeerStore.removePeer(userId); } public closeAllConnections() { @@ -376,11 +382,6 @@ export class SimplePeer { private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void { const uuid = playersStore.getPlayerById(userId)?.userUuid || ""; if (blackListManager.isBlackListed(uuid)) return; - // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) - if (this.PeerScreenSharingConnectionArray.has(userId)) { - this.pushScreenSharingToRemoteUser(userId, localScreenCapture); - return; - } const screenSharingUser: UserSimplePeerInterface = { userId, From 9c99d760f79869a1c6289a0a6039c525e3709005 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Fri, 18 Mar 2022 11:44:25 +0100 Subject: [PATCH 25/64] Close socket connection on not authorized woka --- front/src/Connexion/RoomConnection.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 20b69d62..b2ac2e22 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -349,7 +349,9 @@ export class RoomConnection implements RoomConnection { break; } } + if (this.closed) { + this.closeConnection(); break; } From c51fe1cfed7732abf181bc20faffcede8bf4e96e Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Fri, 18 Mar 2022 11:50:48 +0100 Subject: [PATCH 26/64] Translate camera help (#1983) Co-authored-by: Alexis Faizeau --- .../de-DE-chrome.png | Bin 0 -> 42434 bytes .../en-US-firefox.png} | Bin .../fr-FR-chrome.png} | Bin .../HelpCameraSettingsPopup.svelte | 6 ++---- front/src/i18n/de-DE/camera.ts | 4 ++++ front/src/i18n/en-US/camera.ts | 4 ++++ front/src/i18n/fr-FR/camera.ts | 4 ++++ 7 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 front/public/resources/help-setting-camera-permission/de-DE-chrome.png rename front/{src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png => public/resources/help-setting-camera-permission/en-US-firefox.png} (100%) rename front/{src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png => public/resources/help-setting-camera-permission/fr-FR-chrome.png} (100%) diff --git a/front/public/resources/help-setting-camera-permission/de-DE-chrome.png b/front/public/resources/help-setting-camera-permission/de-DE-chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..9903c308135af54dc09227bf6c87e913594d2aed GIT binary patch literal 42434 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%0D|RfE^sU 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 literal 0 HcmV?d00001 diff --git a/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png b/front/public/resources/help-setting-camera-permission/en-US-firefox.png similarity index 100% rename from front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png rename to front/public/resources/help-setting-camera-permission/en-US-firefox.png diff --git a/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png b/front/public/resources/help-setting-camera-permission/fr-FR-chrome.png similarity index 100% rename from front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png rename to front/public/resources/help-setting-camera-permission/fr-FR-chrome.png diff --git a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte index 34d174c6..f28d05ca 100644 --- a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte +++ b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte @@ -1,8 +1,6 @@
-
-

{$LL.menu.settings.gameQuality.title()}

-
- -
-
-
-

{$LL.menu.settings.videoQuality.title()}

-
- -
-
-
-

{$LL.menu.settings.language.title()}

-
- -
-
-
-

{$LL.menu.settings.save.warning()}

- +

{$LL.menu.settings.gameQuality.title()}

+
+ - {$LL.menu.settings.fullscreen()} - - - - -
+ + + + +
+
+
+

{$LL.menu.settings.videoQuality.title()}

+
+ +
+
+
+

{$LL.menu.settings.language.title()}

+
+ +
+
+ +
+

{$LL.menu.settings.privacySettings.title()}

+

{$LL.menu.settings.privacySettings.explaination()}

+
+ +
+
+
+

{$LL.menu.settings.save.warning()}

+ +
+
+ + + + +
diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 24d4ab42..23e0bcb0 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -24,6 +24,7 @@ const code = "code"; const cameraSetup = "cameraSetup"; const cacheAPIIndex = "workavdenture-cache"; const userProperties = "user-properties"; +const privacySettings = "privacySettings"; class LocalUserStore { saveUser(localUser: LocalUser) { @@ -224,6 +225,14 @@ class LocalUserStore { return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; } + setPrivacySettings(option: string) { + localStorage.setItem(privacySettings, option) + } + + getPrivacySettings() { + return localStorage.getItem(privacySettings); + } + getAllUserProperties(): Map { const result = new Map(); for (let i = 0; i < localStorage.length; i++) { diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index 9494eb7e..f1681615 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -12,6 +12,7 @@ import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { AudioContext } from "standardized-audio-context"; +import { visibilityStore } from "./VisibilityStore"; /** * A store that contains the camera state requested by the user (on or off). @@ -242,6 +243,8 @@ export const mediaStreamConstraintsStore = derived( privacyShutdownStore, cameraEnergySavingStore, isSilentStore, + visibilityStore, + //TODO: optionState ], ( [ @@ -254,6 +257,7 @@ export const mediaStreamConstraintsStore = derived( $privacyShutdownStore, $cameraEnergySavingStore, $isSilentStore, + $visibilityStore ], set ) => { @@ -308,6 +312,11 @@ export const mediaStreamConstraintsStore = derived( currentAudioConstraint = false; } + // if ($visibilityStore === false && $option) { + // + // } + //TODO + // Let's make the changes only if the new value is different from the old one. if ( previousComputedVideoConstraint != currentVideoConstraint || diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index c1b7bc18..dc19e481 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -57,6 +57,14 @@ const menu: NonNullable = { language: { title: "Sprache", }, + privacySettings: { + title: "Datenschutzeinstellungen", //TODO: confirm & complete translation + explaination: "", + allEnabled: "", + onlyCameraEnabled: "", + onlyMicrophoneEnabled: "", + allDisabled: "" + }, save: { warning: "(Das Spiel wird nach dem Speichern neugestartet)", button: "Speichern", diff --git a/front/src/i18n/en-US/menu.ts b/front/src/i18n/en-US/menu.ts index 0883fb15..04f2f377 100644 --- a/front/src/i18n/en-US/menu.ts +++ b/front/src/i18n/en-US/menu.ts @@ -57,6 +57,14 @@ const menu: BaseTranslation = { language: { title: "Language", }, + privacySettings: { + title: "Privacy settings", + explaination: "Here you can set an option to keep your microphone/camera enabled when switching active tabs.", + allEnabled: "Camera and microphone always enabled", + onlyCameraEnabled: "Microphone disabled when the WA tab is not focused", + onlyMicrophoneEnabled: "Camera disabled when the WA tab is not focused", + allDisabled: "Both disabled when the WA tab is not focused" + }, save: { warning: "(Saving these settings will restart the game)", button: "Save", diff --git a/front/src/i18n/fr-FR/menu.ts b/front/src/i18n/fr-FR/menu.ts index f8c58990..9e32e9ba 100644 --- a/front/src/i18n/fr-FR/menu.ts +++ b/front/src/i18n/fr-FR/menu.ts @@ -57,6 +57,14 @@ const menu: NonNullable = { language: { title: "Langage", }, + privacySettings: { + title: "Paramètres de confidentialité", + explaination: "Vous pouvez définir ici si vous souhaitez conserver ou non l'activation du microphone/de la caméra au passage sur un autre onglet.", + allEnabled: "Camera et microphone toujours actifs", + onlyCameraEnabled: "Seul le microphone est activé quand l'onglet WA n'est pas sélectionné", + onlyMicrophoneEnabled: "Seule la caméra est activé quand l'onglet WA n'est pas sélectionné", + allDisabled: "Tout désactiver quand l'onglet WA n'est pas sélectionné" + }, save: { warning: "(La sauvegarde de ces paramètres redémarre le jeu)", button: "Sauvegarder", From 9442c9c9f13b45f211d33a4bec3261b826cbb51c Mon Sep 17 00:00:00 2001 From: Benedicte Quimbert Date: Wed, 9 Mar 2022 17:46:11 +0100 Subject: [PATCH 41/64] PrivacyShutdownStore modifies constraints according to the user's setting --- .../Components/Menu/SettingsSubMenu.svelte | 3 +-- front/src/Stores/MediaStore.ts | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/front/src/Components/Menu/SettingsSubMenu.svelte b/front/src/Components/Menu/SettingsSubMenu.svelte index d5bcab56..67a0bdc3 100644 --- a/front/src/Components/Menu/SettingsSubMenu.svelte +++ b/front/src/Components/Menu/SettingsSubMenu.svelte @@ -20,7 +20,7 @@ let previewValueGame = valueGame; let previewValueVideo = valueVideo; let previewValueLocale = valueLocale; - let previewPrivacySettings = valuePrivacySettings; // TODO: retreive from local storage + let previewPrivacySettings = valuePrivacySettings; function saveSetting() { let change = false; @@ -42,7 +42,6 @@ } if (valuePrivacySettings !== previewPrivacySettings) { - console.log(`was: ${previewPrivacySettings} | is: ${valuePrivacySettings}`) localUserStore.setPrivacySettings(valuePrivacySettings); } diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index f1681615..f73174c9 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -11,7 +11,6 @@ import { peerStore } from "./PeerStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; -import { AudioContext } from "standardized-audio-context"; import { visibilityStore } from "./VisibilityStore"; /** @@ -244,7 +243,6 @@ export const mediaStreamConstraintsStore = derived( cameraEnergySavingStore, isSilentStore, visibilityStore, - //TODO: optionState ], ( [ @@ -257,7 +255,7 @@ export const mediaStreamConstraintsStore = derived( $privacyShutdownStore, $cameraEnergySavingStore, $isSilentStore, - $visibilityStore + $visibilityStore, ], set ) => { @@ -296,7 +294,19 @@ export const mediaStreamConstraintsStore = derived( // Disable webcam for privacy reasons (the game is not visible and we were talking to no one) if ($privacyShutdownStore === true) { - currentVideoConstraint = false; + const userSetting = localUserStore.getPrivacySettings(); + switch (userSetting) { + case "cameraEnabled": + currentAudioConstraint = false; + break; + case "microphoneEnabled": + currentVideoConstraint = false; + break; + case "noneEnabled": + currentVideoConstraint = false; + currentAudioConstraint = false; + break; + } } // Disable webcam for energy reasons (the user is not moving and we are talking to no one) @@ -312,10 +322,6 @@ export const mediaStreamConstraintsStore = derived( currentAudioConstraint = false; } - // if ($visibilityStore === false && $option) { - // - // } - //TODO // Let's make the changes only if the new value is different from the old one. if ( From 6d9a3f45dc4691fceb5561723998fc336161f077 Mon Sep 17 00:00:00 2001 From: Valdo Date: Mon, 21 Mar 2022 15:22:51 +0000 Subject: [PATCH 42/64] Allow audio (#1991) * Removing old unused images Just a bit of cleanup! * Allow audio feature * Pretty * Ignored screenshot folder * Added manual test * Simplified test map Co-authored-by: Valdo Romao --- .../AudioManager/AudioManager.svelte | 118 ++++----- .../Components/Menu/SettingsSubMenu.svelte | 17 ++ front/src/Connexion/LocalUserStore.ts | 7 + front/src/i18n/de-DE/audio.ts | 3 +- front/src/i18n/en-US/audio.ts | 3 +- front/src/i18n/fr-FR/audio.ts | 3 +- maps/assets/audio/campfire.ogg | Bin 0 -> 1778217 bytes maps/tests/AllowAudio/map.json | 241 ++++++++++++++++++ maps/tests/index.html | 8 + tests/.gitignore | 1 + 10 files changed, 341 insertions(+), 60 deletions(-) create mode 100755 maps/assets/audio/campfire.ogg create mode 100644 maps/tests/AllowAudio/map.json diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte index eec51572..786c5693 100644 --- a/front/src/Components/AudioManager/AudioManager.svelte +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -13,20 +13,20 @@ let unsubscriberFileStore: Unsubscriber | null = null; let unsubscriberVolumeStore: Unsubscriber | null = null; - let decreaseWhileTalking: boolean = true; + let isAudioAllowed: boolean = true; onMount(() => { let volume = Math.min(localUserStore.getAudioPlayerVolume(), get(audioManagerVolumeStore).volume); audioManagerVolumeStore.setVolume(volume); audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted()); - unsubscriberFileStore = audioManagerFileStore.subscribe((src) => { + unsubscriberFileStore = audioManagerFileStore.subscribe((src: string) => { HTMLAudioPlayer.pause(); HTMLAudioPlayer.src = src; HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop; HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume; HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted; - void HTMLAudioPlayer.play(); + tryPlay(); }); unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => { const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking; @@ -52,6 +52,16 @@ } }); + function tryPlay() { + void HTMLAudioPlayer.play() + .then(() => { + isAudioAllowed = true; + }) + .catch(() => { + isAudioAllowed = false; + }); + } + function updateVolumeUI() { if (get(audioManagerVolumeStore).muted) { audioPlayerVolumeIcon.classList.add("muted"); @@ -90,73 +100,67 @@ audioPlayerVol.blur(); return false; } - - function setDecrease() { - audioManagerVolumeStore.setDecreaseWhileTalking(decreaseWhileTalking); - }
-
- - - - +
+
+ + - - - - - - - - - - -
-
- + + + + + + + + + + + + +
+
+ +
diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 23e0bcb0..091d3232 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -24,12 +24,14 @@ const code = "code"; const cameraSetup = "cameraSetup"; const cacheAPIIndex = "workavdenture-cache"; const userProperties = "user-properties"; -const privacySettings = "privacySettings"; +const cameraPrivacySettings = "cameraPrivacySettings"; +const microphonePrivacySettings = "microphonePrivacySettings"; class LocalUserStore { saveUser(localUser: LocalUser) { localStorage.setItem("localUser", JSON.stringify(localUser)); } + getLocalUser(): LocalUser | null { const data = localStorage.getItem("localUser"); return data ? JSON.parse(data) : null; @@ -38,6 +40,7 @@ class LocalUserStore { setName(name: string): void { localStorage.setItem(playerNameKey, name); } + getName(): string | null { const value = localStorage.getItem(playerNameKey) || ""; return isUserNameValid(value) ? value : null; @@ -46,6 +49,7 @@ class LocalUserStore { setPlayerCharacterIndex(playerCharacterIndex: number): void { localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex); } + getPlayerCharacterIndex(): number { return parseInt(localStorage.getItem(selectedPlayerKey) || ""); } @@ -53,6 +57,7 @@ class LocalUserStore { setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void { localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers })); } + getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null { return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null"); } @@ -60,6 +65,7 @@ class LocalUserStore { setCharacterLayers(layers: string[]): void { localStorage.setItem(characterLayersKey, JSON.stringify(layers)); } + getCharacterLayers(): string[] | null { const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null"); return areCharacterLayersValid(value) ? value : null; @@ -68,6 +74,7 @@ class LocalUserStore { setCompanion(companion: string | null): void { return localStorage.setItem(companionKey, JSON.stringify(companion)); } + getCompanion(): string | null { const companion = JSON.parse(localStorage.getItem(companionKey) || "null"); @@ -77,6 +84,7 @@ class LocalUserStore { return companion; } + wasCompanionSet(): boolean { return localStorage.getItem(companionKey) ? true : false; } @@ -84,6 +92,7 @@ class LocalUserStore { setGameQualityValue(value: number): void { localStorage.setItem(gameQualityKey, "" + value); } + getGameQualityValue(): number { return parseInt(localStorage.getItem(gameQualityKey) || "60"); } @@ -91,6 +100,7 @@ class LocalUserStore { setVideoQualityValue(value: number): void { localStorage.setItem(videoQualityKey, "" + value); } + getVideoQualityValue(): number { return parseInt(localStorage.getItem(videoQualityKey) || "20"); } @@ -98,6 +108,7 @@ class LocalUserStore { setAudioPlayerVolume(value: number): void { localStorage.setItem(audioPlayerVolumeKey, "" + value); } + getAudioPlayerVolume(): number { return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1"); } @@ -105,6 +116,7 @@ class LocalUserStore { setAudioPlayerMuted(value: boolean): void { localStorage.setItem(audioPlayerMuteKey, value.toString()); } + getAudioPlayerMuted(): boolean { return localStorage.getItem(audioPlayerMuteKey) === "true"; } @@ -112,6 +124,7 @@ class LocalUserStore { setHelpCameraSettingsShown(): void { localStorage.setItem(helpCameraSettingsShown, "1"); } + getHelpCameraSettingsShown(): boolean { return localStorage.getItem(helpCameraSettingsShown) === "1"; } @@ -119,6 +132,7 @@ class LocalUserStore { setFullscreen(value: boolean): void { localStorage.setItem(fullscreenKey, value.toString()); } + getFullscreen(): boolean { return localStorage.getItem(fullscreenKey) === "true"; } @@ -126,6 +140,7 @@ class LocalUserStore { setForceCowebsiteTrigger(value: boolean): void { localStorage.setItem(forceCowebsiteTriggerKey, value.toString()); } + getForceCowebsiteTrigger(): boolean { return localStorage.getItem(forceCowebsiteTriggerKey) === "true"; } @@ -133,6 +148,7 @@ class LocalUserStore { setIgnoreFollowRequests(value: boolean): void { localStorage.setItem(ignoreFollowRequests, value.toString()); } + getIgnoreFollowRequests(): boolean { return localStorage.getItem(ignoreFollowRequests) === "true"; } @@ -149,11 +165,13 @@ class LocalUserStore { } } } + getLastRoomUrl(): string { return ( localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL ); } + getLastRoomUrlCacheApi(): Promise { if (!("caches" in window)) { return Promise.resolve(undefined); @@ -170,6 +188,7 @@ class LocalUserStore { setAuthToken(value: string | null) { value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken); } + getAuthToken(): string | null { return localStorage.getItem(authToken); } @@ -196,23 +215,29 @@ class LocalUserStore { } return oldValue === value; } + setState(value: string) { localStorage.setItem(state, value); } + getState(): string | null { return localStorage.getItem(state); } + generateNonce(): string { const newNonce = uuidv4(); localStorage.setItem(nonce, newNonce); return newNonce; } + getNonce(): string | null { return localStorage.getItem(nonce); } + setCode(value: string): void { localStorage.setItem(code, value); } + getCode(): string | null { return localStorage.getItem(code); } @@ -220,17 +245,26 @@ class LocalUserStore { setCameraSetup(cameraId: string) { localStorage.setItem(cameraSetup, cameraId); } + getCameraSetup(): { video: unknown; audio: unknown } | undefined { const cameraSetupValues = localStorage.getItem(cameraSetup); return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; } - setPrivacySettings(option: string) { - localStorage.setItem(privacySettings, option) + setCameraPrivacySettings(option: boolean) { + localStorage.setItem(cameraPrivacySettings, option.toString()); } - getPrivacySettings() { - return localStorage.getItem(privacySettings); + getCameraPrivacySettings() { + return localStorage.getItem(cameraPrivacySettings) === "true"; + } + + setMicrophonePrivacySettings(option: boolean) { + localStorage.setItem(microphonePrivacySettings, option.toString()); + } + + getMicrophonePrivacySettings() { + return localStorage.getItem(microphonePrivacySettings) === "true"; } getAllUserProperties(): Map { diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index f73174c9..c662a28f 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -294,18 +294,13 @@ export const mediaStreamConstraintsStore = derived( // Disable webcam for privacy reasons (the game is not visible and we were talking to no one) if ($privacyShutdownStore === true) { - const userSetting = localUserStore.getPrivacySettings(); - switch (userSetting) { - case "cameraEnabled": - currentAudioConstraint = false; - break; - case "microphoneEnabled": - currentVideoConstraint = false; - break; - case "noneEnabled": - currentVideoConstraint = false; - currentAudioConstraint = false; - break; + const userMicrophonePrivacySetting = localUserStore.getMicrophonePrivacySettings(); + const userCameraPrivacySetting = localUserStore.getCameraPrivacySettings(); + if (!userMicrophonePrivacySetting) { + currentAudioConstraint = false; + } + if (!userCameraPrivacySetting) { + currentVideoConstraint = false; } } @@ -322,7 +317,6 @@ export const mediaStreamConstraintsStore = derived( currentAudioConstraint = false; } - // Let's make the changes only if the new value is different from the old one. if ( previousComputedVideoConstraint != currentVideoConstraint || diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index dc19e481..3d785b6f 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -57,13 +57,12 @@ const menu: NonNullable = { language: { title: "Sprache", }, + //TODO: complete translation privacySettings: { - title: "Datenschutzeinstellungen", //TODO: confirm & complete translation - explaination: "", - allEnabled: "", - onlyCameraEnabled: "", - onlyMicrophoneEnabled: "", - allDisabled: "" + title: "", + explanation: "", + cameraToggle: "", + microphoneToggle: "", }, save: { warning: "(Das Spiel wird nach dem Speichern neugestartet)", diff --git a/front/src/i18n/en-US/menu.ts b/front/src/i18n/en-US/menu.ts index 04f2f377..6eb6de21 100644 --- a/front/src/i18n/en-US/menu.ts +++ b/front/src/i18n/en-US/menu.ts @@ -58,12 +58,11 @@ const menu: BaseTranslation = { title: "Language", }, privacySettings: { - title: "Privacy settings", - explaination: "Here you can set an option to keep your microphone/camera enabled when switching active tabs.", - allEnabled: "Camera and microphone always enabled", - onlyCameraEnabled: "Microphone disabled when the WA tab is not focused", - onlyMicrophoneEnabled: "Camera disabled when the WA tab is not focused", - allDisabled: "Both disabled when the WA tab is not focused" + title: "Away mode settings", + explanation: + 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', + cameraToggle: "Camera", + microphoneToggle: "Microphone", }, save: { warning: "(Saving these settings will restart the game)", diff --git a/front/src/i18n/fr-FR/menu.ts b/front/src/i18n/fr-FR/menu.ts index 9e32e9ba..1515ea8f 100644 --- a/front/src/i18n/fr-FR/menu.ts +++ b/front/src/i18n/fr-FR/menu.ts @@ -58,12 +58,11 @@ const menu: NonNullable = { title: "Langage", }, privacySettings: { - title: "Paramètres de confidentialité", - explaination: "Vous pouvez définir ici si vous souhaitez conserver ou non l'activation du microphone/de la caméra au passage sur un autre onglet.", - allEnabled: "Camera et microphone toujours actifs", - onlyCameraEnabled: "Seul le microphone est activé quand l'onglet WA n'est pas sélectionné", - onlyMicrophoneEnabled: "Seule la caméra est activé quand l'onglet WA n'est pas sélectionné", - allDisabled: "Tout désactiver quand l'onglet WA n'est pas sélectionné" + title: "Paramètres du mode absent", + explanation: + "Quand l'onglet WorkAdventure n'est pas visible, vous passez en \"mode absent\". Lorsque ce mode est actif, vous pouvez décider de garder vos webcam et/ou micro désactivés tant que vous ne revenez pas sur l'onglet", + cameraToggle: "Camera", + microphoneToggle: "Microphone", }, save: { warning: "(La sauvegarde de ces paramètres redémarre le jeu)", From 66aeb21fbb65df8e36988c06ac78745266d1e17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Mar 2022 09:14:22 +0100 Subject: [PATCH 44/64] Restoring icons deleted by error --- front/public/resources/logos/cowebsite-swipe.svg | 1 + front/public/resources/logos/fullscreen-exit.svg | 3 +++ front/public/resources/logos/fullscreen.svg | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 front/public/resources/logos/cowebsite-swipe.svg create mode 100644 front/public/resources/logos/fullscreen-exit.svg create mode 100644 front/public/resources/logos/fullscreen.svg diff --git a/front/public/resources/logos/cowebsite-swipe.svg b/front/public/resources/logos/cowebsite-swipe.svg new file mode 100644 index 00000000..1d4f9ebc --- /dev/null +++ b/front/public/resources/logos/cowebsite-swipe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/public/resources/logos/fullscreen-exit.svg b/front/public/resources/logos/fullscreen-exit.svg new file mode 100644 index 00000000..1d15cd8b --- /dev/null +++ b/front/public/resources/logos/fullscreen-exit.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/front/public/resources/logos/fullscreen.svg b/front/public/resources/logos/fullscreen.svg new file mode 100644 index 00000000..2c518466 --- /dev/null +++ b/front/public/resources/logos/fullscreen.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From e20a8caa26944bcddb360db329aaa074dc1ae90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Mar 2022 11:05:52 +0100 Subject: [PATCH 45/64] Allowing iframeAuthentication URL (stored in OPID_LOGIN_SCREEN_PROVIDER) to be non-absolute. --- front/src/Connexion/ConnectionManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 7578a942..3e314e50 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -55,7 +55,7 @@ class ConnectionManager { loginSceneVisibleIframeStore.set(false); return null; } - const redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`); + const redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`, window.location.href); redirectUrl.searchParams.append("state", state); redirectUrl.searchParams.append("nonce", nonce); redirectUrl.searchParams.append("playUri", this._currentRoom.key); From f1549a0ee0eb2faf2775f5f1409e87b555b08bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Mar 2022 12:14:23 +0100 Subject: [PATCH 46/64] Removing the temporary error message when moving to open id screen --- front/src/Connexion/ConnectionManager.ts | 28 ++++++++++++++++-------- front/src/Phaser/Game/GameManager.ts | 10 ++++++++- front/src/Phaser/Login/EmptyScene.ts | 17 ++++++++++++++ front/src/Phaser/Login/LoginScene.ts | 5 ++++- 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 front/src/Phaser/Login/EmptyScene.ts diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 3e314e50..09e7257d 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -45,8 +45,10 @@ class ConnectionManager { /** * TODO fix me to be move in game manager + * + * Returns the URL that we need to redirect to to load the OpenID screen, or "null" if no redirection needs to happen. */ - public loadOpenIDScreen() { + public loadOpenIDScreen(): URL | null { const state = localUserStore.generateState(); const nonce = localUserStore.generateNonce(); localUserStore.setAuthToken(null); @@ -59,7 +61,6 @@ class ConnectionManager { redirectUrl.searchParams.append("state", state); redirectUrl.searchParams.append("nonce", nonce); redirectUrl.searchParams.append("playUri", this._currentRoom.key); - window.location.assign(redirectUrl.toString()); return redirectUrl; } @@ -83,8 +84,10 @@ class ConnectionManager { /** * Tries to login to the node server and return the starting map url to be loaded + * + * @return returns a promise to the Room we are going to load OR a pointer to the URL we must redirect to if authentication is needed. */ - public async initGameConnexion(): Promise { + public async initGameConnexion(): Promise { const connexionType = urlManager.getGameConnexionType(); this.connexionType = connexionType; this._currentRoom = null; @@ -101,8 +104,9 @@ class ConnectionManager { if (connexionType === GameConnexionTypes.login) { this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl())); - if (this.loadOpenIDScreen() !== null) { - return Promise.reject(new Error("You will be redirect on login page")); + const redirect = this.loadOpenIDScreen(); + if (redirect !== null) { + return redirect; } urlManager.pushRoomIdToUrl(this._currentRoom); } else if (connexionType === GameConnexionTypes.jwt) { @@ -124,8 +128,11 @@ class ConnectionManager { analyticsClient.loggedWithSso(); } catch (err) { console.error(err); - this.loadOpenIDScreen(); - return Promise.reject(new Error("You will be redirect on login page")); + const redirect = this.loadOpenIDScreen(); + if (redirect === null) { + throw new Error("Unable to redirect on login page."); + } + return redirect; } urlManager.pushRoomIdToUrl(this._currentRoom); } else if (connexionType === GameConnexionTypes.register) { @@ -212,8 +219,11 @@ class ConnectionManager { err.response?.data && err.response.data !== "User cannot to be connected on openid provider") ) { - this.loadOpenIDScreen(); - return Promise.reject(new Error("You will be redirect on login page")); + const redirect = this.loadOpenIDScreen(); + if (redirect === null) { + throw new Error("Unable to redirect on login page."); + } + return redirect; } } } diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 520929eb..0220fca1 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -9,6 +9,7 @@ import { EnableCameraSceneName } from "../Login/EnableCameraScene"; import { LoginSceneName } from "../Login/LoginScene"; import { SelectCharacterSceneName } from "../Login/SelectCharacterScene"; import { GameScene } from "./GameScene"; +import { EmptySceneName } from "../Login/EmptyScene"; /** * This class should be responsible for any scene starting/stopping @@ -32,7 +33,14 @@ export class GameManager { public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise { this.scenePlugin = scenePlugin; - this.startRoom = await connectionManager.initGameConnexion(); + const result = await connectionManager.initGameConnexion(); + if (result instanceof URL) { + window.location.assign(result.toString()); + // window.location.assign is not immediate and Javascript keeps running after. + // so we need to redirect to an empty Phaser scene, waiting for the redirection to take place + return EmptySceneName; + } + this.startRoom = result; this.loadMap(this.startRoom); //If player name was not set show login scene with player name diff --git a/front/src/Phaser/Login/EmptyScene.ts b/front/src/Phaser/Login/EmptyScene.ts new file mode 100644 index 00000000..4511a160 --- /dev/null +++ b/front/src/Phaser/Login/EmptyScene.ts @@ -0,0 +1,17 @@ +import { Scene } from "phaser"; + +export const EmptySceneName = "EmptyScene"; + +export class EmptyScene extends Scene { + constructor() { + super({ + key: EmptySceneName, + }); + } + + preload() {} + + create() {} + + update(time: number, delta: number): void {} +} diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 94f04db9..14cee6a1 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -28,7 +28,10 @@ export class LoginScene extends ResizableScene { gameManager.currentStartedRoom && gameManager.currentStartedRoom.authenticationMandatory ) { - connectionManager.loadOpenIDScreen(); + const redirect = connectionManager.loadOpenIDScreen(); + if (redirect !== null) { + window.location.assign(redirect.toString()); + } loginSceneVisibleIframeStore.set(true); } loginSceneVisibleStore.set(true); From a80f7ced4413ee0e6e24dc6b4462e4757af82143 Mon Sep 17 00:00:00 2001 From: Benedicte Quimbert Date: Tue, 22 Mar 2022 16:48:02 +0100 Subject: [PATCH 47/64] Adds the todo about German translation --- front/src/i18n/de-DE/menu.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index 3d785b6f..8b1faff3 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -57,13 +57,15 @@ const menu: NonNullable = { language: { title: "Sprache", }, - //TODO: complete translation + //TODO: German translation privacySettings: { - title: "", - explanation: "", - cameraToggle: "", - microphoneToggle: "", + title: "Away mode settings", + explanation: + 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', + cameraToggle: "Camera", + microphoneToggle: "Microphone", }, + //END-TODO: German translation save: { warning: "(Das Spiel wird nach dem Speichern neugestartet)", button: "Speichern", From 1681373d887cfa46690f1117f2ad028b6524dbb8 Mon Sep 17 00:00:00 2001 From: Benedicte Quimbert Date: Tue, 22 Mar 2022 17:25:22 +0100 Subject: [PATCH 48/64] Adds test map --- front/src/i18n/de-DE/menu.ts | 2 +- .../AwayModeSettings/away_mode_settings.json | 97 +++++++++++++++++++ maps/tests/index.html | 8 ++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 maps/tests/AwayModeSettings/away_mode_settings.json diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index 8b1faff3..9b5fd460 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -61,7 +61,7 @@ const menu: NonNullable = { privacySettings: { title: "Away mode settings", explanation: - 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', + 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', cameraToggle: "Camera", microphoneToggle: "Microphone", }, diff --git a/maps/tests/AwayModeSettings/away_mode_settings.json b/maps/tests/AwayModeSettings/away_mode_settings.json new file mode 100644 index 00000000..62cc3c01 --- /dev/null +++ b/maps/tests/AwayModeSettings/away_mode_settings.json @@ -0,0 +1,97 @@ +{ "compressionlevel":-1, + "height":20, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":20, + "id":42, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":20, + "id":39, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":43, + "name":"Test", + "objects":[ + { + "height":225.333333333333, + "id":13, + "name":"", + "rotation":0, + "text": + { + "text":"Test: \n- Open two windows (you can use Private Mode) so that you control two Wokas.\n\n- On woka A window: go to Menu > Settings and set your away mode options\n- On woka A window: open a new tab in the browser, so that your WA tab is not visible\n\n- On woka B window: move to woka A to check that the options were applied", + "wrap":true + }, + "type":"", + "visible":true, + "width":434.773333333333, + "x":97.9466666666667, + "y":33.8366666666667 + }, + { + "height":155, + "id":16, + "name":"", + "rotation":0, + "text": + { + "color":"#00007f", + "text":"Reminder: \nThere are 4 cases to test for your away mode (WA tab hidden) settings. \nCamera and microphone stay enabled\nOnly camera stays enabled\nOnly microphone stays enabled\nBoth are disabled", + "wrap":true + }, + "type":"", + "visible":true, + "width":407.4375, + "x":96.9479166666667, + "y":322.5 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":44, + "nextobjectid":17, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.7.2", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/Test\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.6", + "width":20 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 26a1795f..1ba1cbb2 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -455,6 +455,14 @@ Testing zoom via mouse wheel + + + Success Failure Pending + + + Away mode settings + +
+
diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 7753fd7b..fd3a5931 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -25,6 +25,7 @@ const code = "code"; const cameraSetup = "cameraSetup"; const cacheAPIIndex = "workavdenture-cache"; const userProperties = "user-properties"; +const privacySettings = "privacySettings"; class LocalUserStore { saveUser(localUser: LocalUser) { @@ -231,6 +232,14 @@ class LocalUserStore { return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; } + setPrivacySettings(option: string) { + localStorage.setItem(privacySettings, option) + } + + getPrivacySettings() { + return localStorage.getItem(privacySettings); + } + getAllUserProperties(): Map { const result = new Map(); for (let i = 0; i < localStorage.length; i++) { diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index b86a97ce..a12469af 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -12,6 +12,7 @@ import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { AudioContext } from "standardized-audio-context"; +import { visibilityStore } from "./VisibilityStore"; /** * A store that contains the camera state requested by the user (on or off). @@ -242,6 +243,8 @@ export const mediaStreamConstraintsStore = derived( privacyShutdownStore, cameraEnergySavingStore, isSilentStore, + visibilityStore, + //TODO: optionState ], ( [ @@ -254,6 +257,7 @@ export const mediaStreamConstraintsStore = derived( $privacyShutdownStore, $cameraEnergySavingStore, $isSilentStore, + $visibilityStore ], set ) => { @@ -308,6 +312,11 @@ export const mediaStreamConstraintsStore = derived( currentAudioConstraint = false; } + // if ($visibilityStore === false && $option) { + // + // } + //TODO + // Let's make the changes only if the new value is different from the old one. if ( previousComputedVideoConstraint != currentVideoConstraint || diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index c1b7bc18..dc19e481 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -57,6 +57,14 @@ const menu: NonNullable = { language: { title: "Sprache", }, + privacySettings: { + title: "Datenschutzeinstellungen", //TODO: confirm & complete translation + explaination: "", + allEnabled: "", + onlyCameraEnabled: "", + onlyMicrophoneEnabled: "", + allDisabled: "" + }, save: { warning: "(Das Spiel wird nach dem Speichern neugestartet)", button: "Speichern", diff --git a/front/src/i18n/en-US/menu.ts b/front/src/i18n/en-US/menu.ts index 0883fb15..04f2f377 100644 --- a/front/src/i18n/en-US/menu.ts +++ b/front/src/i18n/en-US/menu.ts @@ -57,6 +57,14 @@ const menu: BaseTranslation = { language: { title: "Language", }, + privacySettings: { + title: "Privacy settings", + explaination: "Here you can set an option to keep your microphone/camera enabled when switching active tabs.", + allEnabled: "Camera and microphone always enabled", + onlyCameraEnabled: "Microphone disabled when the WA tab is not focused", + onlyMicrophoneEnabled: "Camera disabled when the WA tab is not focused", + allDisabled: "Both disabled when the WA tab is not focused" + }, save: { warning: "(Saving these settings will restart the game)", button: "Save", diff --git a/front/src/i18n/fr-FR/menu.ts b/front/src/i18n/fr-FR/menu.ts index f8c58990..9e32e9ba 100644 --- a/front/src/i18n/fr-FR/menu.ts +++ b/front/src/i18n/fr-FR/menu.ts @@ -57,6 +57,14 @@ const menu: NonNullable = { language: { title: "Langage", }, + privacySettings: { + title: "Paramètres de confidentialité", + explaination: "Vous pouvez définir ici si vous souhaitez conserver ou non l'activation du microphone/de la caméra au passage sur un autre onglet.", + allEnabled: "Camera et microphone toujours actifs", + onlyCameraEnabled: "Seul le microphone est activé quand l'onglet WA n'est pas sélectionné", + onlyMicrophoneEnabled: "Seule la caméra est activé quand l'onglet WA n'est pas sélectionné", + allDisabled: "Tout désactiver quand l'onglet WA n'est pas sélectionné" + }, save: { warning: "(La sauvegarde de ces paramètres redémarre le jeu)", button: "Sauvegarder", From 40c0f06c8a7734b241eda0f28cf75f2f45433dc0 Mon Sep 17 00:00:00 2001 From: Benedicte Quimbert Date: Wed, 9 Mar 2022 17:46:11 +0100 Subject: [PATCH 56/64] PrivacyShutdownStore modifies constraints according to the user's setting --- .../Components/Menu/SettingsSubMenu.svelte | 28 +++++++++---------- front/src/Stores/MediaStore.ts | 22 +++++++++------ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/front/src/Components/Menu/SettingsSubMenu.svelte b/front/src/Components/Menu/SettingsSubMenu.svelte index 69857504..67a3073e 100644 --- a/front/src/Components/Menu/SettingsSubMenu.svelte +++ b/front/src/Components/Menu/SettingsSubMenu.svelte @@ -9,19 +9,20 @@ import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils"; import { audioManagerVolumeStore } from "../../Stores/AudioManagerStore"; - let fullscreen: boolean = localUserStore.getFullscreen(); - let notification: boolean = localUserStore.getNotification() === "granted"; - let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger(); - let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests(); - let decreaseAudioPlayerVolumeWhileTalking: boolean = localUserStore.getDecreaseAudioPlayerVolumeWhileTalking(); - let valueGame: number = localUserStore.getGameQualityValue(); - let valueVideo: number = localUserStore.getVideoQualityValue(); - let valueLocale: string = $locale; - let valuePrivacySettings = localUserStore.getPrivacySettings(); - let previewValueGame = valueGame; - let previewValueVideo = valueVideo; - let previewValueLocale = valueLocale; - let previewPrivacySettings = valuePrivacySettings; // TODO: retreive from local storage + let fullscreen: boolean = localUserStore.getFullscreen(); + let notification: boolean = localUserStore.getNotification() === "granted"; + let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger(); + let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests(); + let decreaseAudioPlayerVolumeWhileTalking: boolean = localUserStore.getDecreaseAudioPlayerVolumeWhileTalking(); + let valueGame: number = localUserStore.getGameQualityValue(); + let valueVideo: number = localUserStore.getVideoQualityValue(); + let valueLocale: string = $locale; + let valuePrivacySettings = localUserStore.getPrivacySettings(); + + let previewValueGame = valueGame; + let previewValueVideo = valueVideo; + let previewValueLocale = valueLocale; + let previewPrivacySettings = valuePrivacySettings; function saveSetting() { let change = false; @@ -43,7 +44,6 @@ } if (valuePrivacySettings !== previewPrivacySettings) { - console.log(`was: ${previewPrivacySettings} | is: ${valuePrivacySettings}`) localUserStore.setPrivacySettings(valuePrivacySettings); } diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index a12469af..f94a3c55 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -11,7 +11,6 @@ import { peerStore } from "./PeerStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; -import { AudioContext } from "standardized-audio-context"; import { visibilityStore } from "./VisibilityStore"; /** @@ -244,7 +243,6 @@ export const mediaStreamConstraintsStore = derived( cameraEnergySavingStore, isSilentStore, visibilityStore, - //TODO: optionState ], ( [ @@ -257,7 +255,7 @@ export const mediaStreamConstraintsStore = derived( $privacyShutdownStore, $cameraEnergySavingStore, $isSilentStore, - $visibilityStore + $visibilityStore, ], set ) => { @@ -296,7 +294,19 @@ export const mediaStreamConstraintsStore = derived( // Disable webcam for privacy reasons (the game is not visible and we were talking to no one) if ($privacyShutdownStore === true) { - currentVideoConstraint = false; + const userSetting = localUserStore.getPrivacySettings(); + switch (userSetting) { + case "cameraEnabled": + currentAudioConstraint = false; + break; + case "microphoneEnabled": + currentVideoConstraint = false; + break; + case "noneEnabled": + currentVideoConstraint = false; + currentAudioConstraint = false; + break; + } } // Disable webcam for energy reasons (the user is not moving and we are talking to no one) @@ -312,10 +322,6 @@ export const mediaStreamConstraintsStore = derived( currentAudioConstraint = false; } - // if ($visibilityStore === false && $option) { - // - // } - //TODO // Let's make the changes only if the new value is different from the old one. if ( From 4e5fb735bddf75081f7f63e33700752f20aa4953 Mon Sep 17 00:00:00 2001 From: Benedicte Quimbert Date: Mon, 21 Mar 2022 18:39:20 +0100 Subject: [PATCH 57/64] switches dropdown for checkboxes and adjusts wording --- .../Components/Menu/SettingsSubMenu.svelte | 490 +++++++++--------- front/src/Connexion/LocalUserStore.ts | 44 +- front/src/Stores/MediaStore.ts | 20 +- front/src/i18n/de-DE/menu.ts | 11 +- front/src/i18n/en-US/menu.ts | 11 +- front/src/i18n/fr-FR/menu.ts | 11 +- 6 files changed, 306 insertions(+), 281 deletions(-) diff --git a/front/src/Components/Menu/SettingsSubMenu.svelte b/front/src/Components/Menu/SettingsSubMenu.svelte index 67a3073e..6623ea44 100644 --- a/front/src/Components/Menu/SettingsSubMenu.svelte +++ b/front/src/Components/Menu/SettingsSubMenu.svelte @@ -9,84 +9,92 @@ import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils"; import { audioManagerVolumeStore } from "../../Stores/AudioManagerStore"; - let fullscreen: boolean = localUserStore.getFullscreen(); - let notification: boolean = localUserStore.getNotification() === "granted"; - let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger(); - let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests(); - let decreaseAudioPlayerVolumeWhileTalking: boolean = localUserStore.getDecreaseAudioPlayerVolumeWhileTalking(); - let valueGame: number = localUserStore.getGameQualityValue(); - let valueVideo: number = localUserStore.getVideoQualityValue(); - let valueLocale: string = $locale; - let valuePrivacySettings = localUserStore.getPrivacySettings(); + let fullscreen: boolean = localUserStore.getFullscreen(); + let notification: boolean = localUserStore.getNotification() === "granted"; + let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger(); + let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests(); + let decreaseAudioPlayerVolumeWhileTalking: boolean = localUserStore.getDecreaseAudioPlayerVolumeWhileTalking(); + let valueGame: number = localUserStore.getGameQualityValue(); + let valueVideo: number = localUserStore.getVideoQualityValue(); + let valueLocale: string = $locale; + let valueCameraPrivacySettings = localUserStore.getCameraPrivacySettings(); + let valueMicrophonePrivacySettings = localUserStore.getMicrophonePrivacySettings(); - let previewValueGame = valueGame; - let previewValueVideo = valueVideo; - let previewValueLocale = valueLocale; - let previewPrivacySettings = valuePrivacySettings; + let previewValueGame = valueGame; + let previewValueVideo = valueVideo; + let previewValueLocale = valueLocale; + let previewCameraPrivacySettings = valueCameraPrivacySettings; + let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings; - function saveSetting() { - let change = false; + function saveSetting() { + let change = false; - if (valueLocale !== previewValueLocale) { - previewValueLocale = valueLocale; - setCurrentLocale(valueLocale as Locales); - } + if (valueLocale !== previewValueLocale) { + previewValueLocale = valueLocale; + setCurrentLocale(valueLocale as Locales); + } - if (valueVideo !== previewValueVideo) { - previewValueVideo = valueVideo; - videoConstraintStore.setFrameRate(valueVideo); - } + if (valueVideo !== previewValueVideo) { + previewValueVideo = valueVideo; + videoConstraintStore.setFrameRate(valueVideo); + } - if (valueGame !== previewValueGame) { - previewValueGame = valueGame; - localUserStore.setGameQualityValue(valueGame); - change = true; - } + if (valueGame !== previewValueGame) { + previewValueGame = valueGame; + localUserStore.setGameQualityValue(valueGame); + change = true; + } - if (valuePrivacySettings !== previewPrivacySettings) { - localUserStore.setPrivacySettings(valuePrivacySettings); - } + if (valueCameraPrivacySettings !== previewCameraPrivacySettings) { + previewCameraPrivacySettings = valueCameraPrivacySettings; + localUserStore.setCameraPrivacySettings(valueCameraPrivacySettings); + } + + if (valueMicrophonePrivacySettings !== previewMicrophonePrivacySettings) { + previewMicrophonePrivacySettings = valueMicrophonePrivacySettings; + localUserStore.setMicrophonePrivacySettings(valueMicrophonePrivacySettings); + } if (change) { window.location.reload(); } audioManagerVolumeStore.setDecreaseWhileTalking(decreaseAudioPlayerVolumeWhileTalking); - closeMenu(); - } - - function changeFullscreen() { - const body = HtmlUtils.querySelectorOrFail("body"); - if (body) { - if (document.fullscreenElement !== null && !fullscreen) { - document.exitFullscreen().catch((e) => console.error(e)); - } else { - body.requestFullscreen().catch((e) => console.error(e)); - } - localUserStore.setFullscreen(fullscreen); + closeMenu(); } - } - function changeNotification() { - if (Notification.permission === "granted") { - localUserStore.setNotification(notification ? "granted" : "denied"); - } else { - Notification.requestPermission() - .then((response) => { - if (response === "granted") { + function changeFullscreen() { + const body = HtmlUtils.querySelectorOrFail("body"); + if (body) { + if (document.fullscreenElement !== null && !fullscreen) { + document.exitFullscreen().catch((e) => console.error(e)); + } else { + body.requestFullscreen().catch((e) => console.error(e)); + } + localUserStore.setFullscreen(fullscreen); + } + } + + function changeNotification() { + if (Notification.permission === "granted") { localUserStore.setNotification(notification ? "granted" : "denied"); - } else { - localUserStore.setNotification("denied"); - notification = false; - } - }) - .catch((e) => console.error(e)); + } else { + Notification.requestPermission() + .then((response) => { + if (response === "granted") { + localUserStore.setNotification(notification ? "granted" : "denied"); + } else { + localUserStore.setNotification("denied"); + notification = false; + } + }) + .catch((e) => console.error(e)); + } } - } - function changeForceCowebsiteTrigger() { - localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger); - } + function changeForceCowebsiteTrigger() { + localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger); + } function changeIgnoreFollowRequests() { localUserStore.setIgnoreFollowRequests(ignoreFollowRequests); @@ -99,198 +107,190 @@ localUserStore.setDecreaseAudioPlayerVolumeWhileTalking(decreaseAudioPlayerVolumeWhileTalking); } - const isMobile = isMediaBreakpointUp("md"); + const isMobile = isMediaBreakpointUp("md");
-
-

{$LL.menu.settings.gameQuality.title()}

-
- -
-
-
-

{$LL.menu.settings.videoQuality.title()}

-
- -
-
-
-

{$LL.menu.settings.language.title()}

-
- -
-
+
+

{$LL.menu.settings.gameQuality.title()}

+
+ +
+
+
+

{$LL.menu.settings.videoQuality.title()}

+
+ +
+
+
+

{$LL.menu.settings.language.title()}

+
+ +
+
-
-

{$LL.menu.settings.privacySettings.title()}

-

{$LL.menu.settings.privacySettings.explaination()}

-
- -
-
-
-

{$LL.menu.settings.save.warning()}

- -
-
- - - - -
+
+

{$LL.menu.settings.privacySettings.title()}

+

{$LL.menu.settings.privacySettings.explanation()}

+ + +
+
+

{$LL.menu.settings.save.warning()}

+ +
+
+ + + + +
diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index fd3a5931..8e0d6f07 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -25,12 +25,14 @@ const code = "code"; const cameraSetup = "cameraSetup"; const cacheAPIIndex = "workavdenture-cache"; const userProperties = "user-properties"; -const privacySettings = "privacySettings"; +const cameraPrivacySettings = "cameraPrivacySettings"; +const microphonePrivacySettings = "microphonePrivacySettings"; class LocalUserStore { saveUser(localUser: LocalUser) { localStorage.setItem("localUser", JSON.stringify(localUser)); } + getLocalUser(): LocalUser | null { const data = localStorage.getItem("localUser"); return data ? JSON.parse(data) : null; @@ -39,6 +41,7 @@ class LocalUserStore { setName(name: string): void { localStorage.setItem(playerNameKey, name); } + getName(): string | null { const value = localStorage.getItem(playerNameKey) || ""; return isUserNameValid(value) ? value : null; @@ -47,6 +50,7 @@ class LocalUserStore { setPlayerCharacterIndex(playerCharacterIndex: number): void { localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex); } + getPlayerCharacterIndex(): number { return parseInt(localStorage.getItem(selectedPlayerKey) || ""); } @@ -54,6 +58,7 @@ class LocalUserStore { setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void { localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers })); } + getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null { return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null"); } @@ -61,6 +66,7 @@ class LocalUserStore { setCharacterLayers(layers: string[]): void { localStorage.setItem(characterLayersKey, JSON.stringify(layers)); } + getCharacterLayers(): string[] | null { const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null"); return areCharacterLayersValid(value) ? value : null; @@ -69,6 +75,7 @@ class LocalUserStore { setCompanion(companion: string | null): void { return localStorage.setItem(companionKey, JSON.stringify(companion)); } + getCompanion(): string | null { const companion = JSON.parse(localStorage.getItem(companionKey) || "null"); @@ -78,6 +85,7 @@ class LocalUserStore { return companion; } + wasCompanionSet(): boolean { return localStorage.getItem(companionKey) ? true : false; } @@ -85,6 +93,7 @@ class LocalUserStore { setGameQualityValue(value: number): void { localStorage.setItem(gameQualityKey, "" + value); } + getGameQualityValue(): number { return parseInt(localStorage.getItem(gameQualityKey) || "60"); } @@ -92,6 +101,7 @@ class LocalUserStore { setVideoQualityValue(value: number): void { localStorage.setItem(videoQualityKey, "" + value); } + getVideoQualityValue(): number { return parseInt(localStorage.getItem(videoQualityKey) || "20"); } @@ -99,6 +109,7 @@ class LocalUserStore { setAudioPlayerVolume(value: number): void { localStorage.setItem(audioPlayerVolumeKey, "" + value); } + getAudioPlayerVolume(): number { return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1"); } @@ -106,6 +117,7 @@ class LocalUserStore { setAudioPlayerMuted(value: boolean): void { localStorage.setItem(audioPlayerMuteKey, value.toString()); } + getAudioPlayerMuted(): boolean { return localStorage.getItem(audioPlayerMuteKey) === "true"; } @@ -113,6 +125,7 @@ class LocalUserStore { setHelpCameraSettingsShown(): void { localStorage.setItem(helpCameraSettingsShown, "1"); } + getHelpCameraSettingsShown(): boolean { return localStorage.getItem(helpCameraSettingsShown) === "1"; } @@ -120,6 +133,7 @@ class LocalUserStore { setFullscreen(value: boolean): void { localStorage.setItem(fullscreenKey, value.toString()); } + getFullscreen(): boolean { return localStorage.getItem(fullscreenKey) === "true"; } @@ -127,6 +141,7 @@ class LocalUserStore { setForceCowebsiteTrigger(value: boolean): void { localStorage.setItem(forceCowebsiteTriggerKey, value.toString()); } + getForceCowebsiteTrigger(): boolean { return localStorage.getItem(forceCowebsiteTriggerKey) === "true"; } @@ -134,6 +149,7 @@ class LocalUserStore { setIgnoreFollowRequests(value: boolean): void { localStorage.setItem(ignoreFollowRequests, value.toString()); } + getIgnoreFollowRequests(): boolean { return localStorage.getItem(ignoreFollowRequests) === "true"; } @@ -156,11 +172,13 @@ class LocalUserStore { } } } + getLastRoomUrl(): string { return ( localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL ); } + getLastRoomUrlCacheApi(): Promise { if (!("caches" in window)) { return Promise.resolve(undefined); @@ -177,6 +195,7 @@ class LocalUserStore { setAuthToken(value: string | null) { value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken); } + getAuthToken(): string | null { return localStorage.getItem(authToken); } @@ -203,23 +222,29 @@ class LocalUserStore { } return oldValue === value; } + setState(value: string) { localStorage.setItem(state, value); } + getState(): string | null { return localStorage.getItem(state); } + generateNonce(): string { const newNonce = uuidv4(); localStorage.setItem(nonce, newNonce); return newNonce; } + getNonce(): string | null { return localStorage.getItem(nonce); } + setCode(value: string): void { localStorage.setItem(code, value); } + getCode(): string | null { return localStorage.getItem(code); } @@ -227,17 +252,26 @@ class LocalUserStore { setCameraSetup(cameraId: string) { localStorage.setItem(cameraSetup, cameraId); } + getCameraSetup(): { video: unknown; audio: unknown } | undefined { const cameraSetupValues = localStorage.getItem(cameraSetup); return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; } - setPrivacySettings(option: string) { - localStorage.setItem(privacySettings, option) + setCameraPrivacySettings(option: boolean) { + localStorage.setItem(cameraPrivacySettings, option.toString()); } - getPrivacySettings() { - return localStorage.getItem(privacySettings); + getCameraPrivacySettings() { + return localStorage.getItem(cameraPrivacySettings) === "true"; + } + + setMicrophonePrivacySettings(option: boolean) { + localStorage.setItem(microphonePrivacySettings, option.toString()); + } + + getMicrophonePrivacySettings() { + return localStorage.getItem(microphonePrivacySettings) === "true"; } getAllUserProperties(): Map { diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index f94a3c55..557cbcd8 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -294,18 +294,13 @@ export const mediaStreamConstraintsStore = derived( // Disable webcam for privacy reasons (the game is not visible and we were talking to no one) if ($privacyShutdownStore === true) { - const userSetting = localUserStore.getPrivacySettings(); - switch (userSetting) { - case "cameraEnabled": - currentAudioConstraint = false; - break; - case "microphoneEnabled": - currentVideoConstraint = false; - break; - case "noneEnabled": - currentVideoConstraint = false; - currentAudioConstraint = false; - break; + const userMicrophonePrivacySetting = localUserStore.getMicrophonePrivacySettings(); + const userCameraPrivacySetting = localUserStore.getCameraPrivacySettings(); + if (!userMicrophonePrivacySetting) { + currentAudioConstraint = false; + } + if (!userCameraPrivacySetting) { + currentVideoConstraint = false; } } @@ -322,7 +317,6 @@ export const mediaStreamConstraintsStore = derived( currentAudioConstraint = false; } - // Let's make the changes only if the new value is different from the old one. if ( previousComputedVideoConstraint != currentVideoConstraint || diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index dc19e481..3d785b6f 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -57,13 +57,12 @@ const menu: NonNullable = { language: { title: "Sprache", }, + //TODO: complete translation privacySettings: { - title: "Datenschutzeinstellungen", //TODO: confirm & complete translation - explaination: "", - allEnabled: "", - onlyCameraEnabled: "", - onlyMicrophoneEnabled: "", - allDisabled: "" + title: "", + explanation: "", + cameraToggle: "", + microphoneToggle: "", }, save: { warning: "(Das Spiel wird nach dem Speichern neugestartet)", diff --git a/front/src/i18n/en-US/menu.ts b/front/src/i18n/en-US/menu.ts index 04f2f377..6eb6de21 100644 --- a/front/src/i18n/en-US/menu.ts +++ b/front/src/i18n/en-US/menu.ts @@ -58,12 +58,11 @@ const menu: BaseTranslation = { title: "Language", }, privacySettings: { - title: "Privacy settings", - explaination: "Here you can set an option to keep your microphone/camera enabled when switching active tabs.", - allEnabled: "Camera and microphone always enabled", - onlyCameraEnabled: "Microphone disabled when the WA tab is not focused", - onlyMicrophoneEnabled: "Camera disabled when the WA tab is not focused", - allDisabled: "Both disabled when the WA tab is not focused" + title: "Away mode settings", + explanation: + 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', + cameraToggle: "Camera", + microphoneToggle: "Microphone", }, save: { warning: "(Saving these settings will restart the game)", diff --git a/front/src/i18n/fr-FR/menu.ts b/front/src/i18n/fr-FR/menu.ts index 9e32e9ba..1515ea8f 100644 --- a/front/src/i18n/fr-FR/menu.ts +++ b/front/src/i18n/fr-FR/menu.ts @@ -58,12 +58,11 @@ const menu: NonNullable = { title: "Langage", }, privacySettings: { - title: "Paramètres de confidentialité", - explaination: "Vous pouvez définir ici si vous souhaitez conserver ou non l'activation du microphone/de la caméra au passage sur un autre onglet.", - allEnabled: "Camera et microphone toujours actifs", - onlyCameraEnabled: "Seul le microphone est activé quand l'onglet WA n'est pas sélectionné", - onlyMicrophoneEnabled: "Seule la caméra est activé quand l'onglet WA n'est pas sélectionné", - allDisabled: "Tout désactiver quand l'onglet WA n'est pas sélectionné" + title: "Paramètres du mode absent", + explanation: + "Quand l'onglet WorkAdventure n'est pas visible, vous passez en \"mode absent\". Lorsque ce mode est actif, vous pouvez décider de garder vos webcam et/ou micro désactivés tant que vous ne revenez pas sur l'onglet", + cameraToggle: "Camera", + microphoneToggle: "Microphone", }, save: { warning: "(La sauvegarde de ces paramètres redémarre le jeu)", From 5c6a74df4cbe479fed48590f5729634fba0edb35 Mon Sep 17 00:00:00 2001 From: Benedicte Quimbert Date: Tue, 22 Mar 2022 16:48:02 +0100 Subject: [PATCH 58/64] Adds the todo about German translation --- front/src/i18n/de-DE/menu.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index 3d785b6f..8b1faff3 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -57,13 +57,15 @@ const menu: NonNullable = { language: { title: "Sprache", }, - //TODO: complete translation + //TODO: German translation privacySettings: { - title: "", - explanation: "", - cameraToggle: "", - microphoneToggle: "", + title: "Away mode settings", + explanation: + 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', + cameraToggle: "Camera", + microphoneToggle: "Microphone", }, + //END-TODO: German translation save: { warning: "(Das Spiel wird nach dem Speichern neugestartet)", button: "Speichern", From 421592514193b814c852c965fef0e4d05aa51b88 Mon Sep 17 00:00:00 2001 From: Benedicte Quimbert Date: Tue, 22 Mar 2022 17:25:22 +0100 Subject: [PATCH 59/64] Adds test map --- front/src/i18n/de-DE/menu.ts | 2 +- .../AwayModeSettings/away_mode_settings.json | 97 +++++++++++++++++++ maps/tests/index.html | 8 ++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 maps/tests/AwayModeSettings/away_mode_settings.json diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index 8b1faff3..9b5fd460 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -61,7 +61,7 @@ const menu: NonNullable = { privacySettings: { title: "Away mode settings", explanation: - 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', + 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', cameraToggle: "Camera", microphoneToggle: "Microphone", }, diff --git a/maps/tests/AwayModeSettings/away_mode_settings.json b/maps/tests/AwayModeSettings/away_mode_settings.json new file mode 100644 index 00000000..62cc3c01 --- /dev/null +++ b/maps/tests/AwayModeSettings/away_mode_settings.json @@ -0,0 +1,97 @@ +{ "compressionlevel":-1, + "height":20, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":20, + "id":42, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":20, + "id":39, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":43, + "name":"Test", + "objects":[ + { + "height":225.333333333333, + "id":13, + "name":"", + "rotation":0, + "text": + { + "text":"Test: \n- Open two windows (you can use Private Mode) so that you control two Wokas.\n\n- On woka A window: go to Menu > Settings and set your away mode options\n- On woka A window: open a new tab in the browser, so that your WA tab is not visible\n\n- On woka B window: move to woka A to check that the options were applied", + "wrap":true + }, + "type":"", + "visible":true, + "width":434.773333333333, + "x":97.9466666666667, + "y":33.8366666666667 + }, + { + "height":155, + "id":16, + "name":"", + "rotation":0, + "text": + { + "color":"#00007f", + "text":"Reminder: \nThere are 4 cases to test for your away mode (WA tab hidden) settings. \nCamera and microphone stay enabled\nOnly camera stays enabled\nOnly microphone stays enabled\nBoth are disabled", + "wrap":true + }, + "type":"", + "visible":true, + "width":407.4375, + "x":96.9479166666667, + "y":322.5 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":44, + "nextobjectid":17, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.7.2", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/Test\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.6", + "width":20 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 3a1ef520..e625aa6d 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -463,6 +463,14 @@ Testing zoom via mouse wheel + + + Success Failure Pending + + + Away mode settings + +