Merge pull request #1662 from thecodingmachine/feat/outline_api

Adding an API to control players outline
This commit is contained in:
David Négrier 2021-12-23 10:55:03 +01:00 committed by GitHub
commit 565ccb10c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 611 additions and 69 deletions

View File

@ -2,7 +2,13 @@ import { PointInterface } from "./Websocket/PointInterface";
import { Group } from "./Group"; import { Group } from "./Group";
import { User, UserSocket } from "./User"; import { User, UserSocket } from "./User";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; import {
EmoteCallback,
EntersCallback,
LeavesCallback,
MovesCallback,
PlayerDetailsUpdatedCallback,
} from "_Model/Zone";
import { PositionNotifier } from "./PositionNotifier"; import { PositionNotifier } from "./PositionNotifier";
import { Movable } from "_Model/Movable"; import { Movable } from "_Model/Movable";
import { import {
@ -11,6 +17,7 @@ import {
EmoteEventMessage, EmoteEventMessage,
ErrorMessage, ErrorMessage,
JoinRoomMessage, JoinRoomMessage,
SetPlayerDetailsMessage,
SubToPusherRoomMessage, SubToPusherRoomMessage,
VariableMessage, VariableMessage,
VariableWithTagMessage, VariableWithTagMessage,
@ -56,10 +63,19 @@ export class GameRoom {
onEnters: EntersCallback, onEnters: EntersCallback,
onMoves: MovesCallback, onMoves: MovesCallback,
onLeaves: LeavesCallback, onLeaves: LeavesCallback,
onEmote: EmoteCallback onEmote: EmoteCallback,
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
) { ) {
// A zone is 10 sprites wide. // A zone is 10 sprites wide.
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote); this.positionNotifier = new PositionNotifier(
320,
320,
onEnters,
onMoves,
onLeaves,
onEmote,
onPlayerDetailsUpdated
);
} }
public static async create( public static async create(
@ -71,7 +87,8 @@ export class GameRoom {
onEnters: EntersCallback, onEnters: EntersCallback,
onMoves: MovesCallback, onMoves: MovesCallback,
onLeaves: LeavesCallback, onLeaves: LeavesCallback,
onEmote: EmoteCallback onEmote: EmoteCallback,
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
): Promise<GameRoom> { ): Promise<GameRoom> {
const mapDetails = await GameRoom.getMapDetails(roomUrl); const mapDetails = await GameRoom.getMapDetails(roomUrl);
@ -85,7 +102,8 @@ export class GameRoom {
onEnters, onEnters,
onMoves, onMoves,
onLeaves, onLeaves,
onEmote onEmote,
onPlayerDetailsUpdated
); );
return gameRoom; return gameRoom;
@ -180,6 +198,14 @@ export class GameRoom {
this.updateUserGroup(user); this.updateUserGroup(user);
} }
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
if (playerDetailsMessage.getRemoveoutlinecolor()) {
user.outlineColor = undefined;
} else {
user.outlineColor = playerDetailsMessage.getOutlinecolor();
}
}
private updateUserGroup(user: User): void { private updateUserGroup(user: User): void {
user.group?.updatePosition(); user.group?.updatePosition();
user.group?.searchForNearbyUsers(); user.group?.searchForNearbyUsers();

View File

@ -8,12 +8,19 @@
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
* number of players around the current player. * number of players around the current player.
*/ */
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone"; import {
EmoteCallback,
EntersCallback,
LeavesCallback,
MovesCallback,
PlayerDetailsUpdatedCallback,
Zone,
} from "./Zone";
import { Movable } from "_Model/Movable"; import { Movable } from "_Model/Movable";
import { PositionInterface } from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import { ZoneSocket } from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import { User } from "../Model/User"; import { User } from "../Model/User";
import { EmoteEventMessage } from "../Messages/generated/messages_pb"; import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";
interface ZoneDescriptor { interface ZoneDescriptor {
i: number; i: number;
@ -42,7 +49,8 @@ export class PositionNotifier {
private onUserEnters: EntersCallback, private onUserEnters: EntersCallback,
private onUserMoves: MovesCallback, private onUserMoves: MovesCallback,
private onUserLeaves: LeavesCallback, private onUserLeaves: LeavesCallback,
private onEmote: EmoteCallback private onEmote: EmoteCallback,
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
) {} ) {}
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
@ -98,7 +106,15 @@ export class PositionNotifier {
let zone = this.zones[j][i]; let zone = this.zones[j][i];
if (zone === undefined) { if (zone === undefined) {
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j); zone = new Zone(
this.onUserEnters,
this.onUserMoves,
this.onUserLeaves,
this.onEmote,
this.onPlayerDetailsUpdated,
i,
j
);
this.zones[j][i] = zone; this.zones[j][i] = zone;
} }
return zone; return zone;
@ -132,4 +148,11 @@ export class PositionNotifier {
} }
} }
} }
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
const position = user.getPosition();
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
zone.updatePlayerDetails(user, playerDetails);
}
} }

View File

@ -9,6 +9,7 @@ import {
CompanionMessage, CompanionMessage,
PusherToBackMessage, PusherToBackMessage,
ServerToClientMessage, ServerToClientMessage,
SetPlayerDetailsMessage,
SubMessage, SubMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
@ -31,7 +32,8 @@ export class User implements Movable {
public readonly visitCardUrl: string | null, public readonly visitCardUrl: string | null,
public readonly name: string, public readonly name: string,
public readonly characterLayers: CharacterLayer[], public readonly characterLayers: CharacterLayer[],
public readonly companion?: CompanionMessage public readonly companion?: CompanionMessage,
private _outlineColor?: number | undefined
) { ) {
this.listenedZones = new Set<Zone>(); this.listenedZones = new Set<Zone>();
@ -69,4 +71,17 @@ export class User implements Movable {
}, 100); }, 100);
} }
} }
public set outlineColor(value: number | undefined) {
this._outlineColor = value;
const playerDetails = new SetPlayerDetailsMessage();
if (value === undefined) {
playerDetails.setRemoveoutlinecolor(true);
} else {
playerDetails.setOutlinecolor(value);
}
this.positionNotifier.updatePlayerDetails(this, playerDetails);
}
} }

View File

@ -3,12 +3,20 @@ import { PositionInterface } from "_Model/PositionInterface";
import { Movable } from "./Movable"; import { Movable } from "./Movable";
import { Group } from "./Group"; import { Group } from "./Group";
import { ZoneSocket } from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import { EmoteEventMessage } from "../Messages/generated/messages_pb"; import {
EmoteEventMessage,
SetPlayerDetailsMessage,
PlayerDetailsUpdatedMessage,
} from "../Messages/generated/messages_pb";
export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void; export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
export type MovesCallback = (thing: Movable, position: PositionInterface, 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 LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
export type PlayerDetailsUpdatedCallback = (
playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage,
listener: ZoneSocket
) => void;
export class Zone { export class Zone {
private things: Set<Movable> = new Set<Movable>(); private things: Set<Movable> = new Set<Movable>();
@ -19,6 +27,7 @@ export class Zone {
private onMoves: MovesCallback, private onMoves: MovesCallback,
private onLeaves: LeavesCallback, private onLeaves: LeavesCallback,
private onEmote: EmoteCallback, private onEmote: EmoteCallback,
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
public readonly x: number, public readonly x: number,
public readonly y: number public readonly y: number
) {} ) {}
@ -106,4 +115,14 @@ export class Zone {
this.onEmote(emoteEventMessage, listener); this.onEmote(emoteEventMessage, listener);
} }
} }
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
const playerDetailsUpdatedMessage = new PlayerDetailsUpdatedMessage();
playerDetailsUpdatedMessage.setUserid(user.id);
playerDetailsUpdatedMessage.setDetails(playerDetails);
for (const listener of this.listeners) {
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener);
}
}
} }

View File

@ -5,6 +5,7 @@ import {
AdminPusherToBackMessage, AdminPusherToBackMessage,
AdminRoomMessage, AdminRoomMessage,
BanMessage, BanMessage,
BanUserMessage,
BatchToPusherMessage, BatchToPusherMessage,
BatchToPusherRoomMessage, BatchToPusherRoomMessage,
EmotePromptMessage, EmotePromptMessage,
@ -16,7 +17,9 @@ import {
QueryJitsiJwtMessage, QueryJitsiJwtMessage,
RefreshRoomPromptMessage, RefreshRoomPromptMessage,
RoomMessage, RoomMessage,
SendUserMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
SetPlayerDetailsMessage,
SilentMessage, SilentMessage,
UserMovesMessage, UserMovesMessage,
VariableMessage, VariableMessage,
@ -118,14 +121,17 @@ const roomManager: IRoomManagerServer = {
); );
} else if (message.hasSendusermessage()) { } else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage(); const sendUserMessage = message.getSendusermessage();
if (sendUserMessage !== undefined) { socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage);
socketManager.handlerSendUserMessage(user, sendUserMessage);
}
} else if (message.hasBanusermessage()) { } else if (message.hasBanusermessage()) {
const banUserMessage = message.getBanusermessage(); const banUserMessage = message.getBanusermessage();
if (banUserMessage !== undefined) { socketManager.handlerBanUserMessage(room, user, banUserMessage as BanUserMessage);
socketManager.handlerBanUserMessage(room, user, banUserMessage); } else if (message.hasSetplayerdetailsmessage()) {
} const setPlayerDetailsMessage = message.getSetplayerdetailsmessage();
socketManager.handleSetPlayerDetails(
room,
user,
setPlayerDetailsMessage as SetPlayerDetailsMessage
);
} else { } else {
throw new Error("Unhandled message type"); throw new Error("Unhandled message type");
} }

View File

@ -33,6 +33,8 @@ import {
VariableMessage, VariableMessage,
BatchToPusherRoomMessage, BatchToPusherRoomMessage,
SubToPusherRoomMessage, SubToPusherRoomMessage,
SetPlayerDetailsMessage,
PlayerDetailsUpdatedMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { User, UserSocket } from "../Model/User"; import { User, UserSocket } from "../Model/User";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
@ -151,20 +153,9 @@ export class SocketManager {
//room.setViewport(client, client.viewport); //room.setViewport(client, client.viewport);
} }
// Useless now, will be useful again if we allow editing details in game handleSetPlayerDetails(room: GameRoom, user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
/*handleSetPlayerDetails(client: UserSocket, playerDetailsMessage: SetPlayerDetailsMessage) { room.updatePlayerDetails(user, playerDetailsMessage);
const playerDetails = {
name: playerDetailsMessage.getName(),
characterLayers: playerDetailsMessage.getCharacterlayersList()
};
//console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails);
if (!isSetPlayerDetailsMessage(playerDetails)) {
emitError(client, 'Invalid SET_PLAYER_DETAILS message received: ');
return;
} }
client.name = playerDetails.name;
client.characterLayers = SocketManager.mergeCharacterLayersAndCustomTextures(playerDetails.characterLayers, client.textures);
}*/
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) { handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
room.setSilent(user, silentMessage.getSilent()); room.setSilent(user, silentMessage.getSilent());
@ -282,7 +273,9 @@ export class SocketManager {
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) => (thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
this.onClientLeave(thing, newZone, listener), this.onClientLeave(thing, newZone, listener),
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
this.onEmote(emoteEventMessage, listener) this.onEmote(emoteEventMessage, listener),
(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) =>
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener)
) )
.then((gameRoom) => { .then((gameRoom) => {
gaugeManager.incNbRoomGauge(); gaugeManager.incNbRoomGauge();
@ -329,6 +322,12 @@ export class SocketManager {
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl); userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
} }
userJoinedZoneMessage.setCompanion(thing.companion); userJoinedZoneMessage.setCompanion(thing.companion);
if (thing.outlineColor === undefined) {
userJoinedZoneMessage.setHasoutline(false);
} else {
userJoinedZoneMessage.setHasoutline(true);
userJoinedZoneMessage.setOutlinecolor(thing.outlineColor);
}
const subMessage = new SubToPusherMessage(); const subMessage = new SubToPusherMessage();
subMessage.setUserjoinedzonemessage(userJoinedZoneMessage); subMessage.setUserjoinedzonemessage(userJoinedZoneMessage);
@ -378,6 +377,13 @@ export class SocketManager {
emitZoneMessage(subMessage, client); emitZoneMessage(subMessage, client);
} }
private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) {
const subMessage = new SubToPusherMessage();
subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage);
emitZoneMessage(subMessage, client);
}
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void { private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
const position = group.getPosition(); const position = group.getPosition();
const pointMessage = new PointMessage(); const pointMessage = new PointMessage();
@ -572,7 +578,7 @@ export class SocketManager {
user.socket.write(serverToClientMessage); user.socket.write(serverToClientMessage);
} }
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) { public handleSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
sendUserMessage.setType(sendUserMessageToSend.getType()); sendUserMessage.setType(sendUserMessageToSend.getType());

View File

@ -51,7 +51,8 @@ describe("GameRoom", () => {
() => {}, () => {},
() => {}, () => {},
() => {}, () => {},
emote emote,
() => {}
); );
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100)); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
@ -86,7 +87,8 @@ describe("GameRoom", () => {
() => {}, () => {},
() => {}, () => {},
() => {}, () => {},
emote emote,
() => {}
); );
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100)); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
@ -125,7 +127,8 @@ describe("GameRoom", () => {
() => {}, () => {},
() => {}, () => {},
() => {}, () => {},
emote emote,
() => {}
); );
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100)); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));

View File

@ -19,7 +19,8 @@ describe("PositionNotifier", () => {
moveTriggered = true; moveTriggered = true;
}, (thing: Movable) => { }, (thing: Movable) => {
leaveTriggered = true; leaveTriggered = true;
}, () => {}); }, () => {},
() => {});
const user1 = new User(1, 'test', '10.0.0.2', { const user1 = new User(1, 'test', '10.0.0.2', {
x: 500, x: 500,
@ -94,7 +95,8 @@ describe("PositionNotifier", () => {
moveTriggered = true; moveTriggered = true;
}, (thing: Movable) => { }, (thing: Movable) => {
leaveTriggered = true; leaveTriggered = true;
}, () => {}); }, () => {},
() => {});
const user1 = new User(1, 'test', '10.0.0.2', { const user1 = new User(1, 'test', '10.0.0.2', {
x: 500, x: 500,

View File

@ -106,3 +106,25 @@ Example :
```javascript ```javascript
WA.player.onPlayerMove(console.log); WA.player.onPlayerMove(console.log);
``` ```
### Set the outline color of the player
```
WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;
WA.player.removeOutlineColor(): Promise<void>;
```
You can display a thin line around your player's name (the "outline").
Use `setOutlineColor` to set the outline and `removeOutlineColor` to remove it.
Colors are expressed in RGB. Each parameter is an integer between 0 and 255.
```typescript
// Let's add a red outline to our player
WA.player.setOutlineColor(255, 0, 0);
```
When you set the outline on your player, other players will see the outline too (the outline color is shared across
browsers automatically).
![](images/outlines.png)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,6 +55,7 @@ import type {
MessageUserMovedInterface, MessageUserMovedInterface,
MessageUserPositionInterface, MessageUserPositionInterface,
OnConnectInterface, OnConnectInterface,
PlayerDetailsUpdatedMessageInterface,
PointInterface, PointInterface,
PositionInterface, PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
@ -88,6 +89,7 @@ import Tileset = Phaser.Tilemaps.Tileset;
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile; import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore"; import { MapStore } from "../../Stores/Utils/MapStore";
import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
reconnecting: boolean; reconnecting: boolean;
@ -123,6 +125,11 @@ interface DeleteGroupEventInterface {
groupId: number; groupId: number;
} }
interface PlayerDetailsUpdatedInterface {
type: "PlayerDetailsUpdated";
details: PlayerDetailsUpdatedMessageInterface;
}
export class GameScene extends DirtyScene { export class GameScene extends DirtyScene {
Terrains: Array<Phaser.Tilemaps.Tileset>; Terrains: Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: Player; CurrentPlayer!: Player;
@ -135,20 +142,14 @@ export class GameScene extends DirtyScene {
groups: Map<number, Sprite>; groups: Map<number, Sprite>;
circleTexture!: CanvasTexture; circleTexture!: CanvasTexture;
circleRedTexture!: CanvasTexture; circleRedTexture!: CanvasTexture;
pendingEvents: Queue< pendingEvents = new Queue<
| InitUserPositionEventInterface
| AddPlayerEventInterface
| RemovePlayerEventInterface
| UserMovedEventInterface
| GroupCreatedUpdatedEventInterface
| DeleteGroupEventInterface
> = new Queue<
| InitUserPositionEventInterface | InitUserPositionEventInterface
| AddPlayerEventInterface | AddPlayerEventInterface
| RemovePlayerEventInterface | RemovePlayerEventInterface
| UserMovedEventInterface | UserMovedEventInterface
| GroupCreatedUpdatedEventInterface | GroupCreatedUpdatedEventInterface
| DeleteGroupEventInterface | DeleteGroupEventInterface
| PlayerDetailsUpdatedInterface
>(); >();
private initPosition: PositionInterface | null = null; private initPosition: PositionInterface | null = null;
private playersPositionInterpolator = new PlayersPositionInterpolator(); private playersPositionInterpolator = new PlayersPositionInterpolator();
@ -682,6 +683,7 @@ export class GameScene extends DirtyScene {
visitCardUrl: message.visitCardUrl, visitCardUrl: message.visitCardUrl,
companion: message.companion, companion: message.companion,
userUuid: message.userUuid, userUuid: message.userUuid,
outlineColor: message.outlineColor,
}; };
this.addPlayer(userMessage); this.addPlayer(userMessage);
}); });
@ -735,6 +737,13 @@ export class GameScene extends DirtyScene {
item.fire(message.event, message.state, message.parameters); item.fire(message.event, message.state, message.parameters);
}); });
this.connection.onPlayerDetailsUpdated((message) => {
this.pendingEvents.enqueue({
type: "PlayerDetailsUpdated",
details: message,
});
});
/** /**
* Triggered when we receive the JWT token to connect to Jitsi * Triggered when we receive the JWT token to connect to Jitsi
*/ */
@ -1300,6 +1309,21 @@ ${escapedMessage}
iframeListener.registerAnswerer("removeActionMessage", (message) => { iframeListener.registerAnswerer("removeActionMessage", (message) => {
layoutManagerActionStore.removeAction(message.uuid); layoutManagerActionStore.removeAction(message.uuid);
}); });
iframeListener.registerAnswerer("setPlayerOutline", (message) => {
const normalizeColor = (color: number) => Math.min(Math.max(0, Math.round(color)), 255);
const red = normalizeColor(message.red);
const green = normalizeColor(message.green);
const blue = normalizeColor(message.blue);
const color = (red << 16) | (green << 8) | blue;
this.CurrentPlayer.setOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color);
});
iframeListener.registerAnswerer("removePlayerOutline", (message) => {
this.CurrentPlayer.removeOutlineColor();
this.connection?.emitPlayerOutlineColor(null);
});
} }
private setPropertyLayer( private setPropertyLayer(
@ -1422,6 +1446,7 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("removeActionMessage"); iframeListener.unregisterAnswerer("removeActionMessage");
iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("openCoWebsite");
iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("getCoWebsites");
iframeListener.unregisterAnswerer("setPlayerOutline");
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close(); this.embeddedWebsiteManager?.close();
@ -1676,6 +1701,12 @@ ${escapedMessage}
case "DeleteGroupEvent": case "DeleteGroupEvent":
this.doDeleteGroup(event.groupId); this.doDeleteGroup(event.groupId);
break; break;
case "PlayerDetailsUpdated":
this.doUpdatePlayerDetails(event.details);
break;
default: {
const tmp: never = event;
}
} }
} }
// Let's move all users // Let's move all users
@ -1749,6 +1780,9 @@ ${escapedMessage}
addPlayerData.companion, addPlayerData.companion,
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
); );
if (addPlayerData.outlineColor !== undefined) {
player.setOutlineColor(addPlayerData.outlineColor);
}
this.MapPlayers.add(player); this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player); this.MapPlayersByKey.set(player.userId, player);
player.updatePosition(addPlayerData.position); player.updatePosition(addPlayerData.position);
@ -1852,6 +1886,23 @@ ${escapedMessage}
this.groups.delete(groupId); this.groups.delete(groupId);
} }
doUpdatePlayerDetails(message: PlayerDetailsUpdatedMessageInterface): void {
const character = this.MapPlayersByKey.get(message.userId);
if (character === undefined) {
console.log(
"Could not set new details to character with ID ",
message.userId,
". Did he/she left before te message was received?"
);
return;
}
if (message.removeOutlineColor) {
character.removeOutlineColor();
} else {
character.setOutlineColor(message.outlineColor);
}
}
/** /**
* Sends to the server an event emitted by one of the ActionableItems. * Sends to the server an event emitted by one of the ActionableItems.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,93 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"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],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"outline.php"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"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, 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],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":342.082007343941,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"Test:\nPlay with the colors and the limits in the form\n\nResult:\nThe outline should be displayed. A mouse over displays the yellow outline but the normal outline comes back on mouse out.\n\nTest:\nClick the remove outline\n\nResult:\nThe outline is removed\n\nTest:\nClick with many players\n\nResult:\nThe outline is correctly shared",
"wrap":true
},
"type":"",
"visible":true,
"width":274.96422378621,
"x":35.7623688177162,
"y":8.73391812865529
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":3,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":10
}

View File

@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<script src="<?php echo $_SERVER["FRONT_URL"] ?>/iframe_api.js"></script>
<script>
WA.onInit().then(() => {
console.log('After WA init');
const setOutlineButton = document.getElementById('setOutline');
const removeOutlineButton = document.getElementById('removeOutline');
const redField = document.getElementById('red');
const greenField = document.getElementById('green');
const blueField = document.getElementById('blue');
setOutlineButton.addEventListener('click', () => {
console.log('SETTING OUTLINE');
WA.player.setOutlineColor(parseInt(redField.value), parseInt(greenField.value), parseInt(blueField.value));
});
removeOutlineButton.addEventListener('click', () => {
console.log('REMOVING OUTLINE');
WA.player.removeOutlineColor();
});
});
</script>
</head>
<body>
red: <input type="text" id="red" value="0" /><br/>
green: <input type="text" id="green" value="0" /><br/>
blue: <input type="text" id="blue" value="0" /><br/>
<button id="setOutline">Set outline</button>
<button id="removeOutline">Remove outline</button>
</body>
</html>

View File

@ -251,6 +251,14 @@
<a href="#" class="testLink" data-testmap="ChangeLayerApi/change_layer_api.json" target="_blank">Testing scripting API for enters/leaves layer</a> <a href="#" class="testLink" data-testmap="ChangeLayerApi/change_layer_api.json" target="_blank">Testing scripting API for enters/leaves layer</a>
</td> </td>
</tr> </tr>
<tr>
<td>
<input type="radio" name="test-outline-api"> Success <input type="radio" name="test-outline-api"> Failure <input type="radio" name="test-outline-api" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Outline/outline.json" target="_blank">Testing scripting API for outline on players</a>
</td>
</tr>
</table> </table>
<h2>CoWebsite</h2> <h2>CoWebsite</h2>
<table class="table"> <table class="table">

View File

@ -47,8 +47,12 @@ message PingMessage {
} }
message SetPlayerDetailsMessage { message SetPlayerDetailsMessage {
string name = 1; //string name = 1;
repeated string characterLayers = 2; //repeated string characterLayers = 2;
// TODO: switch to google.protobuf.Int32Value when we migrate to ts-proto
uint32 outlineColor = 3;
bool removeOutlineColor = 4;
} }
message UserMovesMessage { message UserMovesMessage {
@ -150,6 +154,7 @@ message SubMessage {
EmoteEventMessage emoteEventMessage = 7; EmoteEventMessage emoteEventMessage = 7;
VariableMessage variableMessage = 8; VariableMessage variableMessage = 8;
ErrorMessage errorMessage = 9; ErrorMessage errorMessage = 9;
PlayerDetailsUpdatedMessage playerDetailsUpdatedMessage = 10;
} }
} }
@ -176,6 +181,8 @@ message UserJoinedMessage {
CompanionMessage companion = 5; CompanionMessage companion = 5;
string visitCardUrl = 6; string visitCardUrl = 6;
string userUuid = 7; string userUuid = 7;
uint32 outlineColor = 8;
bool hasOutline = 9;
} }
message UserLeftMessage { message UserLeftMessage {
@ -313,6 +320,8 @@ message UserJoinedZoneMessage {
CompanionMessage companion = 6; CompanionMessage companion = 6;
string visitCardUrl = 7; string visitCardUrl = 7;
string userUuid = 8; string userUuid = 8;
uint32 outlineColor = 9;
bool hasOutline = 10;
} }
message UserLeftZoneMessage { message UserLeftZoneMessage {
@ -332,6 +341,10 @@ message GroupLeftZoneMessage {
Zone toZone = 2; Zone toZone = 2;
} }
message PlayerDetailsUpdatedMessage {
int32 userId = 1;
SetPlayerDetailsMessage details = 2;
}
message Zone { message Zone {
int32 x = 1; int32 x = 1;
@ -384,6 +397,7 @@ message SubToPusherMessage {
BanUserMessage banUserMessage = 8; BanUserMessage banUserMessage = 8;
EmoteEventMessage emoteEventMessage = 9; EmoteEventMessage emoteEventMessage = 9;
ErrorMessage errorMessage = 10; ErrorMessage errorMessage = 10;
PlayerDetailsUpdatedMessage playerDetailsUpdatedMessage = 11;
} }
} }

View File

@ -16,6 +16,8 @@ import {
EmoteEventMessage, EmoteEventMessage,
CompanionMessage, CompanionMessage,
ErrorMessage, ErrorMessage,
PlayerDetailsUpdatedMessage,
SetPlayerDetailsMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { ClientReadableStream } from "grpc"; import { ClientReadableStream } from "grpc";
import { PositionDispatcher } from "_Model/PositionDispatcher"; import { PositionDispatcher } from "_Model/PositionDispatcher";
@ -32,6 +34,7 @@ export interface ZoneEventListener {
onGroupLeaves(groupId: number, listener: ExSocketInterface): void; onGroupLeaves(groupId: number, listener: ExSocketInterface): void;
onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void; onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void;
onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void; onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void;
onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ExSocketInterface): void;
} }
/*export type EntersCallback = (thing: Movable, listener: User) => void; /*export type EntersCallback = (thing: Movable, listener: User) => void;
@ -46,7 +49,8 @@ export class UserDescriptor {
private characterLayers: CharacterLayerMessage[], private characterLayers: CharacterLayerMessage[],
private position: PositionMessage, private position: PositionMessage,
private visitCardUrl: string | null, private visitCardUrl: string | null,
private companion?: CompanionMessage private companion?: CompanionMessage,
private outlineColor?: number
) { ) {
if (!Number.isInteger(this.userId)) { if (!Number.isInteger(this.userId)) {
throw new Error("UserDescriptor.userId is not an integer: " + this.userId); throw new Error("UserDescriptor.userId is not an integer: " + this.userId);
@ -65,7 +69,8 @@ export class UserDescriptor {
message.getCharacterlayersList(), message.getCharacterlayersList(),
position, position,
message.getVisitcardurl(), message.getVisitcardurl(),
message.getCompanion() message.getCompanion(),
message.getHasoutline() ? message.getOutlinecolor() : undefined
); );
} }
@ -77,6 +82,14 @@ export class UserDescriptor {
this.position = position; this.position = position;
} }
public updateDetails(playerDetails: SetPlayerDetailsMessage) {
if (playerDetails.getRemoveoutlinecolor()) {
this.outlineColor = undefined;
} else {
this.outlineColor = playerDetails.getOutlinecolor();
}
}
public toUserJoinedMessage(): UserJoinedMessage { public toUserJoinedMessage(): UserJoinedMessage {
const userJoinedMessage = new UserJoinedMessage(); const userJoinedMessage = new UserJoinedMessage();
@ -89,6 +102,12 @@ export class UserDescriptor {
} }
userJoinedMessage.setCompanion(this.companion); userJoinedMessage.setCompanion(this.companion);
userJoinedMessage.setUseruuid(this.userUuid); userJoinedMessage.setUseruuid(this.userUuid);
if (this.outlineColor !== undefined) {
userJoinedMessage.setOutlinecolor(this.outlineColor);
userJoinedMessage.setHasoutline(true);
} else {
userJoinedMessage.setHasoutline(false);
}
return userJoinedMessage; return userJoinedMessage;
} }
@ -209,7 +228,7 @@ export class Zone {
const userDescriptor = this.users.get(userId); const userDescriptor = this.users.get(userId);
if (userDescriptor === undefined) { if (userDescriptor === undefined) {
console.error('Unexpected move message received for user "' + userId + '"'); console.error('Unexpected move message received for unknown user "' + userId + '"');
return; return;
} }
@ -219,6 +238,27 @@ export class Zone {
} else if (message.hasEmoteeventmessage()) { } else if (message.hasEmoteeventmessage()) {
const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage;
this.notifyEmote(emoteEventMessage); this.notifyEmote(emoteEventMessage);
} else if (message.hasPlayerdetailsupdatedmessage()) {
const playerDetailsUpdatedMessage =
message.getPlayerdetailsupdatedmessage() as PlayerDetailsUpdatedMessage;
const userId = playerDetailsUpdatedMessage.getUserid();
const userDescriptor = this.users.get(userId);
if (userDescriptor === undefined) {
console.error('Unexpected details message received for unknown user "' + userId + '"');
return;
}
const details = playerDetailsUpdatedMessage.getDetails();
if (details === undefined) {
console.error('Unexpected details message without details received for user "' + userId + '"');
return;
}
userDescriptor.updateDetails(details);
this.notifyPlayerDetailsUpdated(playerDetailsUpdatedMessage);
} else if (message.hasErrormessage()) { } else if (message.hasErrormessage()) {
const errorMessage = message.getErrormessage() as ErrorMessage; const errorMessage = message.getErrormessage() as ErrorMessage;
this.notifyError(errorMessage); this.notifyError(errorMessage);
@ -308,6 +348,15 @@ export class Zone {
} }
} }
private notifyPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage) {
for (const listener of this.listeners) {
if (listener.userId === playerDetailsUpdatedMessage.getUserid()) {
continue;
}
this.socketListener.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener);
}
}
private notifyError(errorMessage: ErrorMessage) { private notifyError(errorMessage: ErrorMessage) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.socketListener.onError(errorMessage, listener); this.socketListener.onError(errorMessage, listener);

View File

@ -34,6 +34,7 @@ import {
VariableMessage, VariableMessage,
ErrorMessage, ErrorMessage,
WorldFullMessage, WorldFullMessage,
PlayerDetailsUpdatedMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
@ -55,6 +56,7 @@ const debug = Debug("socket");
interface AdminSocketRoomsList { interface AdminSocketRoomsList {
[index: string]: number; [index: string]: number;
} }
interface AdminSocketUsersList { interface AdminSocketUsersList {
[index: string]: boolean; [index: string]: boolean;
} }
@ -276,6 +278,16 @@ export class SocketManager implements ZoneEventListener {
emitInBatch(listener, subMessage); emitInBatch(listener, subMessage);
} }
onPlayerDetailsUpdated(
playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage,
listener: ExSocketInterface
): void {
const subMessage = new SubMessage();
subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage);
emitInBatch(listener, subMessage);
}
onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void { onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void {
const subMessage = new SubMessage(); const subMessage = new SubMessage();
subMessage.setErrormessage(errorMessage); subMessage.setErrormessage(errorMessage);