diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 0e8203cf..157faa4e 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"; @@ -44,7 +45,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(); @@ -66,6 +67,7 @@ export class GameRoom { onMoves: MovesCallback, onLeaves: LeavesCallback, onEmote: EmoteCallback, + onLockGroup: LockGroupCallback, onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback ) { // A zone is 10 sprites wide. @@ -76,6 +78,7 @@ export class GameRoom { onMoves, onLeaves, onEmote, + onLockGroup, onPlayerDetailsUpdated ); } @@ -90,6 +93,7 @@ export class GameRoom { onMoves: MovesCallback, onLeaves: LeavesCallback, onEmote: EmoteCallback, + onLockGroup: LockGroupCallback, onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback ): Promise { const mapDetails = await GameRoom.getMapDetails(roomUrl); @@ -105,6 +109,7 @@ export class GameRoom { onMoves, onLeaves, onEmote, + onLockGroup, onPlayerDetailsUpdated ); @@ -244,7 +249,7 @@ export class GameRoom { this.disconnectCallback, this.positionNotifier ); - this.groups.add(group); + this.groups.set(group.getId(), group); } } } else { @@ -328,7 +333,7 @@ export class GameRoom { this.disconnectCallback, this.positionNotifier ); - this.groups.add(newGroup); + this.groups.set(newGroup.getId(), newGroup); } else { this.leaveGroup(user); } @@ -375,10 +380,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(); @@ -418,7 +423,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()); @@ -544,6 +549,10 @@ export class GameRoom { this.positionNotifier.emitEmoteEvent(user, emoteEventMessage); } + public emitLockGroupEvent(user: User, groupId: number) { + this.positionNotifier.emitLockGroupEvent(user, groupId); + } + public addRoomListener(socket: RoomSocket) { this.roomListeners.add(socket); } @@ -657,4 +666,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/Model/Group.ts b/back/src/Model/Group.ts index c14d509f..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,15 +142,19 @@ export class Group implements Movable { return this.users.size >= MAX_PER_GROUP; } + isLocked(): boolean { + return this.locked; + } + isEmpty(): boolean { return this.users.size <= 1; } 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 { @@ -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/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index b059999a..38d538ec 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, @@ -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,12 @@ export class PositionNotifier { zone.emitEmoteEvent(emoteEventMessage); } + 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(groupId); + } + 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..2d0cefd5 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -13,6 +13,7 @@ export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: Z 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 = (groupId: number, listener: ZoneSocket) => void; export type PlayerDetailsUpdatedCallback = ( playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket @@ -27,6 +28,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 +110,12 @@ export class Zone { } } + public emitLockGroupEvent(groupId: number) { + for (const listener of this.listeners) { + this.onLockGroup(groupId, 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 d375fbd8..ab886f50 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -29,6 +29,7 @@ import { WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, ZoneMessage, + LockGroupPromptMessage, } from "./Messages/generated/messages_pb"; import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; import { socketManager } from "./Services/SocketManager"; @@ -135,6 +136,12 @@ const roomManager: IRoomManagerServer = { user, message.getFollowabortmessage() as FollowAbortMessage ); + } else if (message.hasLockgrouppromptmessage()) { + socketManager.handleLockGroupPromptMessage( + room, + user, + message.getLockgrouppromptmessage() as LockGroupPromptMessage + ); } else if (message.hasSendusermessage()) { const sendUserMessage = message.getSendusermessage(); socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage); diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 1d26f001..8aa7f6a4 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -38,6 +38,9 @@ import { SubToPusherRoomMessage, SetPlayerDetailsMessage, PlayerDetailsUpdatedMessage, + GroupUsersUpdateMessage, + LockGroupPromptMessage, + RoomMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -68,7 +71,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); } @@ -266,18 +268,28 @@ 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) => - 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) => this.onClientLeave(thing, newZone, listener), (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), + (groupId: number, listener: ZoneSocket) => { + void this.onLockGroup(groupId, listener, roomPromise); + }, (playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) => this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener) ) @@ -381,10 +393,24 @@ export class SocketManager { emitZoneMessage(subMessage, client); } + private async onLockGroup( + groupId: number, + client: ZoneSocket, + roomPromise: PromiseLike | undefined + ): Promise { + if (!roomPromise) { + return; + } + const group = (await roomPromise).getGroupById(groupId); + if (!group) { + return; + } + this.emitCreateUpdateGroupEvent(client, null, group); + } + private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) { const subMessage = new SubToPusherMessage(); subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage); - emitZoneMessage(subMessage, client); } @@ -398,6 +424,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); @@ -413,7 +440,6 @@ export class SocketManager { const subMessage = new SubToPusherMessage(); subMessage.setGroupleftzonemessage(groupDeleteMessage); - emitZoneMessage(subMessage, client); //user.emitInBatch(subMessage); } @@ -425,7 +451,6 @@ export class SocketManager { const subMessage = new SubToPusherMessage(); subMessage.setUserleftzonemessage(userLeftMessage); - emitZoneMessage(subMessage, client); } @@ -439,6 +464,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) { @@ -634,6 +672,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); @@ -870,6 +909,15 @@ export class SocketManager { leader?.delFollower(user); } } + + handleLockGroupPromptMessage(room: GameRoom, user: User, message: LockGroupPromptMessage) { + const group = user.group; + if (!group) { + return; + } + group.lock(message.getLock()); + room.emitLockGroupEvent(user, group.getId()); + } } export const socketManager = new SocketManager(); 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, + () => {}, () => {} ); 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/desktop/electron/yarn.lock b/desktop/electron/yarn.lock index 82034d10..32dd7fe9 100644 --- a/desktop/electron/yarn.lock +++ b/desktop/electron/yarn.lock @@ -295,9 +295,9 @@ ansi-escapes@^4.2.1: type-fest "^0.21.3" ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== ansi-regex@^5.0.1: version "5.0.1" @@ -2409,9 +2409,9 @@ pirates@^4.0.1: integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== plist@^3.0.1, plist@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.4.tgz#a62df837e3aed2bb3b735899d510c4f186019cbe" - integrity sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg== + version "3.0.5" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.5.tgz#2cbeb52d10e3cdccccf0c11a63a85d830970a987" + integrity sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA== dependencies: base64-js "^1.5.1" xmlbuilder "^9.0.7" diff --git a/desktop/local-app/yarn.lock b/desktop/local-app/yarn.lock index 03b6bbab..52bbce0a 100644 --- a/desktop/local-app/yarn.lock +++ b/desktop/local-app/yarn.lock @@ -587,9 +587,9 @@ minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^0.5.1: version "0.5.5" diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index bfe1d9d0..b9cb7801 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 { currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore"; const gameScene = gameManager.getCurrentGameScene(); @@ -70,6 +72,10 @@ } } + function lockClick() { + gameScene.connection?.emitLockGroup(!$currentPlayerGroupLockStateStore); + } + let isSilent: boolean; const unsubscribeIsSilent = isSilentStore.subscribe((value) => { isSilent = value; @@ -95,6 +101,15 @@ +
+ +
+
\ No newline at end of file diff --git a/front/src/Connexion/AdminMessagesService.ts b/front/src/Connexion/AdminMessagesService.ts index 4b7030ed..22bdd469 100644 --- a/front/src/Connexion/AdminMessagesService.ts +++ b/front/src/Connexion/AdminMessagesService.ts @@ -1,5 +1,5 @@ import { Subject } from "rxjs"; -import type { BanUserMessage, SendUserMessage } from "../Messages/ts-proto-generated/messages"; +import type { BanUserMessage, SendUserMessage } from "../Messages/ts-proto-generated/protos/messages"; export enum AdminMessageEventTypes { admin = "message", diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 09e7257d..c0c9597c 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -88,8 +88,7 @@ class ConnectionManager { * @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 { - const connexionType = urlManager.getGameConnexionType(); - this.connexionType = connexionType; + this.connexionType = urlManager.getGameConnexionType(); this._currentRoom = null; const urlParams = new URLSearchParams(window.location.search); @@ -102,14 +101,15 @@ class ConnectionManager { urlParams.delete("token"); } - if (connexionType === GameConnexionTypes.login) { + if (this.connexionType === GameConnexionTypes.login) { this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl())); const redirect = this.loadOpenIDScreen(); if (redirect !== null) { return redirect; } urlManager.pushRoomIdToUrl(this._currentRoom); - } else if (connexionType === GameConnexionTypes.jwt) { + } else if (this.connexionType === GameConnexionTypes.jwt) { + /** @deprecated */ if (!token) { const code = urlParams.get("code"); const state = urlParams.get("state"); @@ -135,8 +135,9 @@ class ConnectionManager { return redirect; } urlManager.pushRoomIdToUrl(this._currentRoom); - } else if (connexionType === GameConnexionTypes.register) { - //@deprecated + } + //@deprecated + else if (this.connexionType === GameConnexionTypes.register) { const organizationMemberToken = urlManager.getOrganizationToken(); const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then( (res) => res.data @@ -165,11 +166,11 @@ class ConnectionManager { ) ); urlManager.pushRoomIdToUrl(this._currentRoom); - } else if (connexionType === GameConnexionTypes.room || connexionType === GameConnexionTypes.empty) { + } else if (this.connexionType === GameConnexionTypes.room || this.connexionType === GameConnexionTypes.empty) { this.authToken = localUserStore.getAuthToken(); let roomPath: string; - if (connexionType === GameConnexionTypes.empty) { + if (this.connexionType === GameConnexionTypes.empty) { roomPath = localUserStore.getLastRoomUrl(); //get last room path from cache api try { diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index c681fd37..84ce60c1 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -43,7 +43,13 @@ export interface PositionInterface { export interface GroupCreatedUpdatedMessageInterface { position: PositionInterface; groupId: number; - groupSize: number; + groupSize?: number; + locked?: boolean; +} + +export interface GroupUsersUpdateMessageInterface { + groupId: number; + userIds: number[]; } export interface WebRtcDisconnectMessageInterface { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index ed982536..5cfc85c1 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -5,47 +5,35 @@ import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; import type { GroupCreatedUpdatedMessageInterface, - ItemEventMessageInterface, + GroupUsersUpdateMessageInterface, MessageUserJoined, - OnConnectInterface, - PlayerDetailsUpdatedMessageInterface, PlayGlobalMessageInterface, PositionInterface, RoomJoinedMessageInterface, ViewportInterface, - WebRtcDisconnectMessageInterface, WebRtcSignalReceivedMessageInterface, } from "./ConnexionModels"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import { adminMessagesService } from "./AdminMessagesService"; import { connectionManager } from "./ConnectionManager"; import { get } from "svelte/store"; +import { followRoleStore, followUsersStore } from "../Stores/FollowStore"; import { menuIconVisiblilityStore, menuVisiblilityStore, warningContainerStore } from "../Stores/MenuStore"; -import { followStateStore, 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, @@ -53,10 +41,8 @@ import { SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto, PingMessage as PingMessageTsProto, CharacterLayerMessage, -} from "../Messages/ts-proto-generated/messages"; +} from "../Messages/ts-proto-generated/protos/messages"; import { Subject } from "rxjs"; -import { OpenPopupEvent } from "../Api/Events/OpenPopupEvent"; -import { match } from "assert"; import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore"; import { gameManager } from "../Phaser/Game/GameManager"; import { SelectCharacterScene, SelectCharacterSceneName } from "../Phaser/Login/SelectCharacterScene"; @@ -116,6 +102,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(); @@ -443,6 +432,10 @@ export class RoomConnection implements RoomConnection { this._sendJitsiJwtMessageStream.next(message.sendJitsiJwtMessage); break; } + case "groupUsersUpdateMessage": { + this._groupUsersUpdateMessageStream.next(message.groupUsersUpdateMessage); + break; + } case "sendUserMessage": { adminMessagesService.onSendusermessage(message.sendUserMessage); break; @@ -675,6 +668,7 @@ export class RoomConnection implements RoomConnection { groupId: message.groupId, position: position, groupSize: message.groupSize, + locked: message.locked, }; } @@ -890,6 +884,19 @@ export class RoomConnection implements RoomConnection { this.socket.send(bytes); } + public emitLockGroup(lock: boolean = true): void { + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "lockGroupPromptMessage", + lockGroupPromptMessage: { + lock, + }, + }, + }).finish(); + + this.socket.send(bytes); + } + public getAllTags(): string[] { return this.tags; } diff --git a/front/src/Network/ProtobufClientUtils.ts b/front/src/Network/ProtobufClientUtils.ts index 3e172d0f..beec3d9f 100644 --- a/front/src/Network/ProtobufClientUtils.ts +++ b/front/src/Network/ProtobufClientUtils.ts @@ -1,4 +1,4 @@ -import { PositionMessage, PositionMessage_Direction } from "../Messages/ts-proto-generated/messages"; +import { PositionMessage, PositionMessage_Direction } from "../Messages/ts-proto-generated/protos/messages"; import type { PointInterface } from "../Connexion/ConnexionModels"; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index eb402e97..66a2d8eb 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -76,6 +76,7 @@ import { userIsAdminStore } from "../../Stores/GameStore"; import { contactPageStore } from "../../Stores/MenuStore"; import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; import { audioManagerFileStore } from "../../Stores/AudioManagerStore"; +import { currentPlayerGroupLockStateStore } from "../../Stores/CurrentPlayerGroupStore"; import EVENT_TYPE = Phaser.Scenes.Events; import Texture = Phaser.Textures.Texture; @@ -177,6 +178,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; @@ -218,6 +220,7 @@ export class GameScene extends DirtyScene { private loader: Loader; private lastCameraEvent: WasCameraUpdatedEvent | undefined; private firstCameraUpdateSent: boolean = false; + private currentPlayerGroupId?: number; public readonly superLoad: SuperLoaderPlugin; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { @@ -839,6 +842,10 @@ export class GameScene extends DirtyScene { }); }); + this.connection.groupUsersUpdateMessageStream.subscribe((message) => { + this.currentPlayerGroupId = message.groupId; + }); + /** * Triggered when we receive the JWT token to connect to Jitsi */ @@ -969,7 +976,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 @@ -979,7 +988,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(); } @@ -1849,12 +1860,14 @@ ${escapedMessage} 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); + currentPlayerGroupLockStateStore.set(undefined); + break; + } default: { const tmp: never = event; } @@ -2028,11 +2041,16 @@ ${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); this.groups.set(groupPositionMessage.groupId, sprite); + if (this.currentPlayerGroupId === groupPositionMessage.groupId) { + currentPlayerGroupLockStateStore.set(groupPositionMessage.locked); + } return sprite; } diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index e7f814b9..b454de56 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -280,6 +280,9 @@ export class UserInputManager { ); this.scene.input.keyboard.on("keyup-SPACE", (event: Event) => { + if (this.isInputDisabled) { + return; + } this.userInputHandler.handleSpaceKeyUpEvent(event); }); } diff --git a/front/src/Stores/CurrentPlayerGroupStore.ts b/front/src/Stores/CurrentPlayerGroupStore.ts new file mode 100644 index 00000000..91d4b50e --- /dev/null +++ b/front/src/Stores/CurrentPlayerGroupStore.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export const currentPlayerGroupLockStateStore = writable(undefined); diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index e881b167..a7abbadd 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -3,10 +3,10 @@ import { localUserStore } from "../Connexion/LocalUserStore"; export enum GameConnexionTypes { room = 1, - register, + register /*@deprecated*/, empty, unknown, - jwt, + jwt /*@deprecated*/, login, } @@ -16,11 +16,15 @@ class UrlManager { const url = window.location.pathname.toString(); if (url === "/login") { return GameConnexionTypes.login; - } else if (url === "/jwt") { + } + //@deprecated jwt url will be replace by "?token=" + else if (url === "/jwt") { return GameConnexionTypes.jwt; } else if (url.includes("_/") || url.includes("*/") || url.includes("@/")) { return GameConnexionTypes.room; - } else if (url.includes("register/")) { + } + //@deprecated register url will be replace by "?token=" + else if (url.includes("register/")) { return GameConnexionTypes.register; } else if (url === "/") { return GameConnexionTypes.empty; @@ -29,6 +33,9 @@ class UrlManager { } } + /** + * @deprecated + */ public getOrganizationToken(): string | null { const match = /\/register\/(.+)/.exec(window.location.pathname.toString()); return match ? match[1] : null; diff --git a/maps/yarn.lock b/maps/yarn.lock index 38a0f92b..97e22592 100644 --- a/maps/yarn.lock +++ b/maps/yarn.lock @@ -124,9 +124,9 @@ ansi-escapes@^4.2.1: type-fest "^0.21.3" ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== ansi-regex@^5.0.0: version "5.0.1" diff --git a/messages/package.json b/messages/package.json index 644065c9..4cef28dd 100644 --- a/messages/package.json +++ b/messages/package.json @@ -6,7 +6,7 @@ "proto": "grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --grpc_out=generated --js_out=\"import_style=commonjs,binary:generated\" --ts_out=generated -I ./protos protos/*.proto", "ts-proto": "grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=ts-proto-generated --ts_proto_opt=oneof=unions --ts_proto_opt=esModuleInterop=true protos/*.proto", "copy-to-back": "rm -rf ../back/src/Messages/generated && cp -rf generated/ ../back/src/Messages/generated", - "copy-to-front-ts-proto": "sed 's/import { Observable } from \"rxjs\";/import type { Observable } from \"rxjs\";/g' ts-proto-generated/protos/messages.ts > ../front/src/Messages/ts-proto-generated/messages.ts", + "copy-to-front-ts-proto": "cp -rf ts-proto-generated/* ../front/src/Messages/ts-proto-generated/ && sed -i 's/import { Observable } from \"rxjs\";/import type { Observable } from \"rxjs\";/g' ../front/src/Messages/ts-proto-generated/protos/messages.ts", "copy-to-pusher": "rm -rf ../pusher/src/Messages/generated && cp -rf generated/ ../pusher/src/Messages/generated", "json-copy-to-pusher": "rm -rf ../pusher/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../pusher/src/Messages/JsonMessages/", "json-copy-to-back": "rm -rf ../back/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../back/src/Messages/JsonMessages/", diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index d0768480..35db5321 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 { @@ -99,6 +101,10 @@ message FollowAbortMessage { int32 follower = 2; } +message LockGroupPromptMessage { + bool lock = 1; +} + message ClientToServerMessage { oneof message { UserMovesMessage userMovesMessage = 2; @@ -117,6 +123,7 @@ message ClientToServerMessage { FollowRequestMessage followRequestMessage = 15; FollowConfirmationMessage followConfirmationMessage = 16; FollowAbortMessage followAbortMessage = 17; + LockGroupPromptMessage lockGroupPromptMessage = 18; } } @@ -184,7 +191,8 @@ message BatchMessage { message GroupUpdateMessage { int32 groupId = 1; PointMessage position = 2; - int32 groupSize = 3; + google.protobuf.UInt32Value groupSize = 3; + google.protobuf.BoolValue locked = 4; } message GroupDeleteMessage { @@ -216,6 +224,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; @@ -316,6 +329,7 @@ message ServerToClientMessage { FollowConfirmationMessage followConfirmationMessage = 22; FollowAbortMessage followAbortMessage = 23; InvalidTextureMessage invalidTextureMessage = 24; + GroupUsersUpdateMessage groupUsersUpdateMessage = 25; } } @@ -356,8 +370,9 @@ message UserLeftZoneMessage { message GroupUpdateZoneMessage { int32 groupId = 1; PointMessage position = 2; - int32 groupSize = 3; + int32 groupSize = 3; Zone fromZone = 4; + bool locked = 5; } message GroupLeftZoneMessage { @@ -403,6 +418,7 @@ message PusherToBackMessage { FollowRequestMessage followRequestMessage = 16; FollowConfirmationMessage followConfirmationMessage = 17; FollowAbortMessage followAbortMessage = 18; + LockGroupPromptMessage lockGroupPromptMessage = 19; } } diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 689addbb..5a5f857d 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -320,10 +320,11 @@ export class AuthenticateController extends BaseHttpController { //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken: string | null = param.organizationMemberToken; + const playUri: string | null = param.playUri; try { if (typeof organizationMemberToken != "string") throw new Error("No organization token"); - const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); + const data = await adminApi.fetchMemberDataByToken(organizationMemberToken, playUri); const userUuid = data.userUuid; const email = data.email; const roomUrl = data.roomUrl; diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index a8243b49..6a2dccc1 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -21,6 +21,7 @@ import { FollowConfirmationMessage, FollowAbortMessage, VariableMessage, + LockGroupPromptMessage, } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb"; import { parse } from "query-string"; @@ -561,6 +562,11 @@ export class IoSocketController { ); } else if (message.hasFollowabortmessage()) { socketManager.handleFollowAbort(client, message.getFollowabortmessage() as FollowAbortMessage); + } 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/Controller/OpenIdProfileController.ts b/pusher/src/Controller/OpenIdProfileController.ts index 3ff4c948..589c9d54 100644 --- a/pusher/src/Controller/OpenIdProfileController.ts +++ b/pusher/src/Controller/OpenIdProfileController.ts @@ -13,14 +13,14 @@ export class OpenIdProfileController extends BaseHttpController { } try { const resCheckTokenAuth = await openIDClient.checkTokenAuth(accessToken as string); - if (!resCheckTokenAuth.email) { + if (!resCheckTokenAuth.sub) { throw new Error("Email was not found"); } res.send( this.buildHtml( OPID_CLIENT_ISSUER, - resCheckTokenAuth.email as string, - resCheckTokenAuth.picture as string | undefined + resCheckTokenAuth.sub + /*resCheckTokenAuth.picture as string | undefined*/ ) ); return; diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 2132ff39..f3b15ed2 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -22,6 +22,7 @@ import { import { ClientReadableStream } from "grpc"; import { PositionDispatcher } from "_Model/PositionDispatcher"; import Debug from "debug"; +import { BoolValue, UInt32Value } from "google-protobuf/google/protobuf/wrappers_pb"; const debug = Debug("zone"); @@ -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 | undefined, + private position: PointMessage, + private locked: boolean | undefined + ) {} 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 { @@ -144,9 +151,13 @@ export class GroupDescriptor { throw new Error("GroupDescriptor.groupId is not an integer: " + this.groupId); } groupUpdateMessage.setGroupid(this.groupId); - groupUpdateMessage.setGroupsize(this.groupSize); + if (this.groupSize !== undefined) { + groupUpdateMessage.setGroupsize(new UInt32Value().setValue(this.groupSize)); + } groupUpdateMessage.setPosition(this.position); - + if (this.locked !== undefined) { + groupUpdateMessage.setLocked(new BoolValue().setValue(this.locked)); + } return groupUpdateMessage; } } @@ -206,9 +217,7 @@ export class Zone { this.notifyGroupMove(groupDescriptor); } else { this.groups.set(groupId, groupDescriptor); - const fromZone = groupUpdateZoneMessage.getFromzone(); - this.notifyGroupEnter(groupDescriptor, fromZone?.toObject()); } } else if (message.hasUserleftzonemessage()) { diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index c31d1a9b..1e5e98e8 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -61,7 +61,7 @@ class AdminApi { async fetchMemberDataByUuid( userIdentifier: string | null, - roomId: string, + playUri: string, ipAddress: string, characterLayers: string[] ): Promise { @@ -69,7 +69,12 @@ class AdminApi { return Promise.reject(new Error("No admin backoffice set!")); } const res = await Axios.get>(ADMIN_API_URL + "/api/room/access", { - params: { userIdentifier, roomId, ipAddress, characterLayers }, + params: { + userIdentifier, + playUri, + ipAddress, + characterLayers, + }, headers: { Authorization: `${ADMIN_API_TOKEN}` }, paramsSerializer: (p) => { return qs.stringify(p, { arrayFormat: "brackets" }); @@ -84,12 +89,13 @@ class AdminApi { return res.data; } - async fetchMemberDataByToken(organizationMemberToken: string): Promise { + async fetchMemberDataByToken(organizationMemberToken: string, playUri: string | null): Promise { if (!ADMIN_API_URL) { return Promise.reject(new Error("No admin backoffice set!")); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, { + params: { playUri }, headers: { Authorization: `${ADMIN_API_TOKEN}` }, }); if (!isAdminApiData(res.data)) { diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 981c580a..37ad3689 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -38,6 +38,7 @@ import { ErrorMessage, WorldFullMessage, PlayerDetailsUpdatedMessage, + LockGroupPromptMessage, InvalidTextureMessage, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -297,6 +298,12 @@ export class SocketManager implements ZoneEventListener { client.backConnection.write(pusherToBackMessage); } + handleLockGroup(client: ExSocketInterface, message: LockGroupPromptMessage): void { + const pusherToBackMessage = new PusherToBackMessage(); + pusherToBackMessage.setLockgrouppromptmessage(message); + client.backConnection.write(pusherToBackMessage); + } + onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void { const subMessage = new SubMessage(); subMessage.setEmoteeventmessage(emoteMessage); diff --git a/uploader/yarn.lock b/uploader/yarn.lock index feab6ff4..4c36556a 100644 --- a/uploader/yarn.lock +++ b/uploader/yarn.lock @@ -174,9 +174,9 @@ ansi-escapes@^4.2.1: type-fest "^0.11.0" ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== ansi-regex@^5.0.0: version "5.0.0"