Merge pull request #2139 from thecodingmachine/develop

Deploy 2022-04-26 (1.10)
This commit is contained in:
David Négrier 2022-04-27 18:45:51 +02:00 committed by GitHub
commit a662096e1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
312 changed files with 6693 additions and 4985 deletions

View File

@ -60,6 +60,10 @@ jobs:
run: yarn build
working-directory: "desktop/local-app"
- name: "Set desktop app version"
run: node helpers/set-version.js
working-directory: "desktop/electron"
- name: "Install dependencies"
run: yarn install --froze-lockfile
working-directory: "desktop/electron"
@ -68,15 +72,19 @@ jobs:
run: yarn build
working-directory: "desktop/electron"
- name: "Build app"
run: yarn bundle --publish never
- name: "Install electron tools"
run: yarn electron-builder install-app-deps
working-directory: "desktop/electron"
- name: "Build app for testing"
run: yarn electron-builder --publish never
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: "desktop/electron"
if: ${{ github.event_name != 'release' }}
- name: "Build & publish App"
run: yarn release
- name: "Build & release app"
run: yarn electron-builder --publish always
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: "desktop/electron"

View File

@ -26,6 +26,9 @@
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error", { "args": "none", "caughtErrors": "all", "varsIgnorePattern": "_exhaustiveCheck" }
],
"no-throw-literal": "error"
}
}

View File

@ -45,7 +45,6 @@
"busboy": "^0.3.1",
"circular-json": "^0.5.9",
"debug": "^4.3.1",
"generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0",
"grpc": "^1.24.4",
"ipaddr.js": "^2.0.1",
@ -56,7 +55,7 @@
"redis": "^3.1.2",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
"uuidv4": "^6.0.7",
"zod": "^3.12.0"
"zod": "^3.14.3"
},
"devDependencies": {
"@types/busboy": "^0.2.3",

View File

@ -48,7 +48,7 @@ export class DebugController {
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
for (const setValue of value.values()) {
obj.push(setValue);
}
return obj;

View File

@ -1,5 +1,5 @@
import { App } from "../Server/sifrr.server";
import { HttpRequest, HttpResponse } from "uWebSockets.js";
import { HttpResponse } from "uWebSockets.js";
import { register, collectDefaultMetrics } from "prom-client";
export class PrometheusController {
@ -11,7 +11,7 @@ export class PrometheusController {
this.App.get("/metrics", this.metrics.bind(this));
}
private metrics(res: HttpResponse, req: HttpRequest): void {
private metrics(res: HttpResponse): void {
res.writeHeader("Content-Type", register.contentType);
res.end(register.metrics());
}

View File

@ -1,7 +1,7 @@
import { PointInterface } from "./Websocket/PointInterface";
import { Group } from "./Group";
import { User, UserSocket } from "./User";
import { PositionInterface } from "_Model/PositionInterface";
import { PositionInterface } from "../Model/PositionInterface";
import {
EmoteCallback,
EntersCallback,
@ -9,23 +9,20 @@ import {
LockGroupCallback,
MovesCallback,
PlayerDetailsUpdatedCallback,
} from "_Model/Zone";
} from "../Model/Zone";
import { PositionNotifier } from "./PositionNotifier";
import { Movable } from "_Model/Movable";
import { Movable } from "../Model/Movable";
import {
BatchToPusherMessage,
BatchToPusherRoomMessage,
EmoteEventMessage,
ErrorMessage,
JoinRoomMessage,
SetPlayerDetailsMessage,
SubToPusherRoomMessage,
VariableMessage,
VariableWithTagMessage,
ServerToClientMessage,
} from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { RoomSocket, ZoneSocket } from "src/RoomManager";
import { RoomSocket, ZoneSocket } from "../RoomManager";
import { Admin } from "../Model/Admin";
import { adminApi } from "../Services/AdminApi";
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
@ -36,7 +33,6 @@ import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { LocalUrlError } from "../Services/LocalUrlError";
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
import { VariableError } from "../Services/VariableError";
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void;
@ -148,8 +144,8 @@ export class GameRoom {
joinRoomMessage.getUseruuid(),
joinRoomMessage.getIpaddress(),
position,
false,
this.positionNotifier,
joinRoomMessage.getStatus(),
socket,
joinRoomMessage.getTagList(),
joinRoomMessage.getVisitcardurl(),
@ -210,10 +206,9 @@ export class GameRoom {
}
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
if (playerDetailsMessage.getRemoveoutlinecolor()) {
user.outlineColor = undefined;
} else {
user.outlineColor = playerDetailsMessage.getOutlinecolor();
user.updateDetails(playerDetailsMessage);
if (user.group !== undefined && user.silent) {
this.leaveGroup(user);
}
}
@ -352,21 +347,6 @@ export class GameRoom {
});
}
setSilent(user: User, silent: boolean) {
if (user.silent === silent) {
return;
}
user.silent = silent;
if (silent && user.group !== undefined) {
this.leaveGroup(user);
}
if (!silent) {
// If we are back to life, let's trigger a position update to see if we can join some group.
this.updatePosition(user, user.getPosition());
}
}
/**
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
*
@ -402,7 +382,7 @@ export class GameRoom {
private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
let matchingItem: User | Group | null = null;
this.users.forEach((currentUser, userId) => {
this.users.forEach((currentUser) => {
// Let's only check users that are not part of a group
if (typeof currentUser.group !== "undefined") {
return;
@ -579,21 +559,20 @@ export class GameRoom {
return {
mapUrl,
policy_type: 1,
tags: [],
authenticationMandatory: null,
roomSlug: null,
contactPage: null,
group: null,
};
}
const result = await adminApi.fetchMapDetails(roomUrl);
if (isRoomRedirect(result)) {
console.error("Unexpected room redirect received while querying map details", result);
throw new Error("Unexpected room redirect received while querying map details");
const result = isMapDetailsData.safeParse(await adminApi.fetchMapDetails(roomUrl));
if (result.success) {
return result.data;
}
return result;
console.error(result.error.issues);
console.error("Unexpected room redirect received while querying map details", result);
throw new Error("Unexpected room redirect received while querying map details");
}
private mapPromise: Promise<ITiledMap> | undefined;

View File

@ -1,8 +1,8 @@
import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom";
import { User } from "./User";
import { PositionInterface } from "_Model/PositionInterface";
import { Movable } from "_Model/Movable";
import { PositionNotifier } from "_Model/PositionNotifier";
import { PositionInterface } from "../Model/PositionInterface";
import { Movable } from "../Model/Movable";
import { PositionNotifier } from "../Model/PositionNotifier";
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
import type { Zone } from "../Model/Zone";

View File

@ -1,4 +1,4 @@
import { PositionInterface } from "_Model/PositionInterface";
import { PositionInterface } from "../Model/PositionInterface";
/**
* A physical object that can be placed into a Zone

View File

@ -17,8 +17,8 @@ import {
PlayerDetailsUpdatedCallback,
Zone,
} from "./Zone";
import { Movable } from "_Model/Movable";
import { PositionInterface } from "_Model/PositionInterface";
import { Movable } from "../Model/Movable";
import { PositionInterface } from "../Model/PositionInterface";
import { ZoneSocket } from "../RoomManager";
import { User } from "../Model/User";
import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";
@ -45,8 +45,8 @@ export class PositionNotifier {
private zones: Zone[][] = [];
constructor(
private zoneWidth: number,
private zoneHeight: number,
private readonly zoneWidth: number,
private readonly zoneHeight: number,
private onUserEnters: EntersCallback,
private onUserMoves: MovesCallback,
private onUserLeaves: LeavesCallback,

View File

@ -1,10 +1,11 @@
import { Group } from "./Group";
import { PointInterface } from "./Websocket/PointInterface";
import { Zone } from "_Model/Zone";
import { Movable } from "_Model/Movable";
import { PositionNotifier } from "_Model/PositionNotifier";
import { Zone } from "../Model/Zone";
import { Movable } from "../Model/Movable";
import { PositionNotifier } from "../Model/PositionNotifier";
import { ServerDuplexStream } from "grpc";
import {
AvailabilityStatus,
BatchMessage,
CompanionMessage,
FollowAbortMessage,
@ -14,7 +15,8 @@ import {
SetPlayerDetailsMessage,
SubMessage,
} from "../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
import { CharacterLayer } from "../Model/Websocket/CharacterLayer";
import { BoolValue, UInt32Value } from "google-protobuf/google/protobuf/wrappers_pb";
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
@ -29,15 +31,16 @@ export class User implements Movable {
public readonly uuid: string,
public readonly IPAddress: string,
private position: PointInterface,
public silent: boolean,
private positionNotifier: PositionNotifier,
private status: AvailabilityStatus,
public readonly socket: UserSocket,
public readonly tags: string[],
public readonly visitCardUrl: string | null,
public readonly name: string,
public readonly characterLayers: CharacterLayer[],
public readonly companion?: CompanionMessage,
private _outlineColor?: number | undefined
private outlineColor?: number,
private voiceIndicatorShown?: boolean
) {
this.listenedZones = new Set<Zone>();
@ -83,6 +86,18 @@ export class User implements Movable {
return this.followedBy.size !== 0;
}
public getOutlineColor(): number | undefined {
return this.outlineColor;
}
public getStatus(): AvailabilityStatus {
return this.status;
}
public get silent(): boolean {
return this.status === AvailabilityStatus.SILENT || this.status === AvailabilityStatus.JITSI;
}
get following(): User | undefined {
return this._following;
}
@ -115,14 +130,31 @@ export class User implements Movable {
}
}
public set outlineColor(value: number | undefined) {
this._outlineColor = value;
public updateDetails(details: SetPlayerDetailsMessage) {
if (details.getRemoveoutlinecolor()) {
this.outlineColor = undefined;
} else if (details.getOutlinecolor()?.getValue() !== undefined) {
this.outlineColor = details.getOutlinecolor()?.getValue();
}
this.voiceIndicatorShown = details.getShowvoiceindicator()?.getValue();
const status = details.getStatus();
let sendStatusUpdate = false;
if (status && status !== this.status) {
this.status = status;
sendStatusUpdate = true;
}
const playerDetails = new SetPlayerDetailsMessage();
if (value === undefined) {
playerDetails.setRemoveoutlinecolor(true);
} else {
playerDetails.setOutlinecolor(value);
if (this.outlineColor !== undefined) {
playerDetails.setOutlinecolor(new UInt32Value().setValue(this.outlineColor));
}
if (this.voiceIndicatorShown !== undefined) {
playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown));
}
if (sendStatusUpdate) {
playerDetails.setStatus(details.getStatus());
}
this.positionNotifier.updatePlayerDetails(this, playerDetails);

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isItemEventMessageInterface = new tg.IsInterface()
.withProperties({
itemId: tg.isNumber,
event: tg.isString,
state: tg.isUnknown,
parameters: tg.isUnknown,
})
.get();
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
export const isItemEventMessageInterface = z.object({
itemId: z.number(),
event: z.string(),
state: z.unknown(),
parameters: z.unknown(),
});
export type ItemEventMessageInterface = z.infer<typeof isItemEventMessageInterface>;

View File

@ -1,18 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
/*export interface PointInterface {
readonly x: number;
readonly y: number;
readonly direction: string;
readonly moving: boolean;
}*/
export const isPointInterface = z.object({
x: z.number(),
y: z.number(),
direction: z.string(),
moving: z.boolean(),
});
export const isPointInterface = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
direction: tg.isString,
moving: tg.isBoolean,
})
.get();
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
export type PointInterface = z.infer<typeof isPointInterface>;

View File

@ -5,10 +5,10 @@ import {
PointMessage,
PositionMessage,
} from "../../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
import { CharacterLayer } from "../../Model/Websocket/CharacterLayer";
import Direction = PositionMessage.Direction;
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
import { PositionInterface } from "_Model/PositionInterface";
import { ItemEventMessageInterface } from "../../Model/Websocket/ItemEventMessage";
import { PositionInterface } from "../../Model/PositionInterface";
export class ProtobufUtils {
public static toPositionMessage(point: PointInterface): PositionMessage {

View File

@ -1,5 +1,5 @@
import { User } from "./User";
import { PositionInterface } from "_Model/PositionInterface";
import { PositionInterface } from "../Model/PositionInterface";
import { Movable } from "./Movable";
import { Group } from "./Group";
import { ZoneSocket } from "../RoomManager";

View File

@ -15,7 +15,6 @@ import {
EmptyMessage,
ItemEventMessage,
JoinRoomMessage,
PlayGlobalMessage,
PusherToBackMessage,
QueryJitsiJwtMessage,
RefreshRoomPromptMessage,
@ -23,7 +22,6 @@ import {
SendUserMessage,
ServerToAdminClientMessage,
SetPlayerDetailsMessage,
SilentMessage,
UserMovesMessage,
VariableMessage,
WebRtcSignalToServerMessage,
@ -81,8 +79,6 @@ const roomManager: IRoomManagerServer = {
user,
message.getUsermovesmessage() as UserMovesMessage
);
} else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) {
socketManager.handleItemEvent(
room,

View File

@ -18,12 +18,21 @@ class AdminApi {
params,
});
if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) {
console.error("Unexpected answer from the /api/map admin endpoint.", res.data);
throw new Error("Unexpected answer from the /api/map admin endpoint.");
const mapDetailData = isMapDetailsData.safeParse(res.data);
const roomRedirect = isRoomRedirect.safeParse(res.data);
if (mapDetailData.success) {
return mapDetailData.data;
}
return res.data;
if (roomRedirect.success) {
return roomRedirect.data;
}
console.error(mapDetailData.error.issues);
console.error(roomRedirect.error.issues);
console.error("Unexpected answer from the /api/map admin endpoint.", res.data);
throw new Error("Unexpected answer from the /api/map admin endpoint.");
}
}

View File

@ -1,5 +1,4 @@
import {
BatchMessage,
BatchToPusherMessage,
BatchToPusherRoomMessage,
ErrorMessage,
@ -7,7 +6,7 @@ import {
SubToPusherMessage,
SubToPusherRoomMessage,
} from "../Messages/generated/messages_pb";
import { UserSocket } from "_Model/User";
import { UserSocket } from "../Model/User";
import { RoomSocket, ZoneSocket } from "../RoomManager";
function getMessageFromError(error: unknown): string {

View File

@ -2,11 +2,9 @@ import { GameRoom } from "../Model/GameRoom";
import {
ItemEventMessage,
ItemStateMessage,
PlayGlobalMessage,
PointMessage,
RoomJoinedMessage,
ServerToClientMessage,
SilentMessage,
SubMessage,
UserMovedMessage,
UserMovesMessage,
@ -35,12 +33,10 @@ import {
FollowAbortMessage,
VariableMessage,
BatchToPusherRoomMessage,
SubToPusherRoomMessage,
SetPlayerDetailsMessage,
PlayerDetailsUpdatedMessage,
GroupUsersUpdateMessage,
LockGroupPromptMessage,
RoomMessage,
} from "../Messages/generated/messages_pb";
import { User, UserSocket } from "../Model/User";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
@ -60,9 +56,9 @@ import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { clientEventsEmitter } from "./ClientEventsEmitter";
import { gaugeManager } from "./GaugeManager";
import { RoomSocket, ZoneSocket } from "../RoomManager";
import { Zone } from "_Model/Zone";
import { Zone } from "../Model/Zone";
import Debug from "debug";
import { Admin } from "_Model/Admin";
import { Admin } from "../Model/Admin";
import crypto from "crypto";
const debug = Debug("sockermanager");
@ -163,10 +159,6 @@ export class SocketManager {
room.updatePlayerDetails(user, playerDetailsMessage);
}
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
room.setSilent(user, silentMessage.getSilent());
}
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
@ -331,6 +323,7 @@ export class SocketManager {
userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setUseruuid(thing.uuid);
userJoinedZoneMessage.setName(thing.name);
userJoinedZoneMessage.setStatus(thing.getStatus());
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
@ -338,11 +331,12 @@ export class SocketManager {
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
}
userJoinedZoneMessage.setCompanion(thing.companion);
if (thing.outlineColor === undefined) {
const outlineColor = thing.getOutlineColor();
if (outlineColor === undefined) {
userJoinedZoneMessage.setHasoutline(false);
} else {
userJoinedZoneMessage.setHasoutline(true);
userJoinedZoneMessage.setOutlinecolor(thing.outlineColor);
userJoinedZoneMessage.setOutlinecolor(outlineColor);
}
const subMessage = new SubToPusherMessage();
@ -657,6 +651,7 @@ export class SocketManager {
userJoinedMessage.setUserid(thing.id);
userJoinedMessage.setUseruuid(thing.uuid);
userJoinedMessage.setName(thing.name);
userJoinedMessage.setStatus(thing.getStatus());
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
if (thing.visitCardUrl) {

View File

@ -2,7 +2,7 @@
* Handles variables shared between the scripting API and the server.
*/
import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist";
import { User } from "_Model/User";
import { User } from "../Model/User";
import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient";
import { VariableError } from "./VariableError";

View File

@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import "jasmine";
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
import { Point } from "../src/Model/Websocket/MessageUserPosition";
import { Group } from "../src/Model/Group";
import { User, UserSocket } from "_Model/User";
import { User, UserSocket } from "../src/Model/User";
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
import Direction = PositionMessage.Direction;
import { EmoteCallback } from "_Model/Zone";
import { EmoteCallback } from "../src/Model/Zone";
function createMockUser(userId: number): User {
return {

View File

@ -1,4 +1,3 @@
import { arrayIntersect } from "../src/Services/ArrayHelper";
import { mapFetcher } from "../src/Services/MapFetcher";
describe("MapFetcher", () => {

View File

@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import "jasmine";
import { PositionNotifier } from "../src/Model/PositionNotifier";
import { User, UserSocket } from "../src/Model/User";
import { Zone } from "_Model/Zone";
import { Movable } from "_Model/Movable";
import { PositionInterface } from "_Model/PositionInterface";
import { Zone } from "../src/Model/Zone";
import { Movable } from "../src/Model/Movable";
import { PositionInterface } from "../src/Model/PositionInterface";
import { ZoneSocket } from "../src/RoomManager";
import { AvailabilityStatus } from "../src/Messages/generated/messages_pb";
describe("PositionNotifier", () => {
it("should receive notifications when player moves", () => {
@ -39,8 +41,8 @@ describe("PositionNotifier", () => {
moving: false,
direction: "down",
},
false,
positionNotifier,
AvailabilityStatus.ONLINE,
{} as UserSocket,
[],
null,
@ -58,8 +60,8 @@ describe("PositionNotifier", () => {
moving: false,
direction: "down",
},
false,
positionNotifier,
AvailabilityStatus.ONLINE,
{} as UserSocket,
[],
null,
@ -147,8 +149,8 @@ describe("PositionNotifier", () => {
moving: false,
direction: "down",
},
false,
positionNotifier,
AvailabilityStatus.ONLINE,
{} as UserSocket,
[],
null,
@ -166,8 +168,8 @@ describe("PositionNotifier", () => {
moving: false,
direction: "down",
},
false,
positionNotifier,
AvailabilityStatus.ONLINE,
{} as UserSocket,
[],
null,

View File

@ -3,18 +3,18 @@
"experimentalDecorators": true,
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"downlevelIteration": true,
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
"allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
@ -23,50 +23,50 @@
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ // Disabled because of sifrr server that is monkey patching HttpResponse
"noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ // Disabled because of sifrr server that is monkey patching HttpResponse
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
"paths": {
"_Controller/*": ["src/Controller/*"],
"_Model/*": ["src/Model/*"],
"_Enum/*": ["src/Enum/*"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": ".", /* Base directory to resolve non-absolute module names. */
// "paths": {
// "_Controller/*": [
// "src/Controller/*"
// ],
// "_Model/*": [
// "src/Model/*"
// ],
// "_Enum/*": [
// "src/Enum/*"
// ]
// }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
}

View File

@ -981,7 +981,7 @@ gauge@^3.0.0:
strip-ansi "^6.0.1"
wide-align "^1.1.2"
generic-type-guard@^3.2.0, generic-type-guard@^3.4.1:
generic-type-guard@^3.4.1:
version "3.5.0"
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.5.0.tgz#39de9f8fceee65d79e7540959f0e7b23210c07b6"
integrity sha512-OpgXv/sbRobhFboaSyN/Tsh97Sxt5pcfLLxCiYZgYIIWFFp+kn2EzAXiaQZKEVRlq1rOE/zh8cYhJXEwplbJiQ==
@ -1459,9 +1459,9 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7"
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==
minipass@^3.0.0:
version "3.1.6"
@ -2259,7 +2259,7 @@ yn@3.1.1:
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
zod@^3.12.0:
version "3.14.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.2.tgz#0b4ed79085c471adce0e7f2c0a4fbb5ddc516ba2"
integrity sha512-iF+wrtzz7fQfkmn60PG6XFxaWBhYYKzp2i+nv24WbLUWb2JjymdkHlzBwP0erpc78WotwP5g9AAu7Sk8GWVVNw==
zod@^3.14.3:
version "3.14.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.3.tgz#60e86341c05883c281fe96a0e79acea48a09f123"
integrity sha512-OzwRCSXB1+/8F6w6HkYHdbuWysYWnAF4fkRgKDcSFc54CE+Sv0rHXKfeNUReGCrHukm1LNpi6AYeXotznhYJbQ==

View File

@ -369,9 +369,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"mkdirp": {
"version": "1.0.4",

View File

@ -268,8 +268,8 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7"
minimist@^1.1.3, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
mkdirp@^1.0.4:
version "1.0.4"

View File

@ -30,7 +30,7 @@ ADMIN_API_URL=
DATA_DIR=./wa
# The URL used by default, in the form: "/_/global/map/url.json"
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json
START_ROOM_URL=/_/global/thecodingmachine.github.io/workadventure-map-starter-kit/map.json
# If you want to have a contact page in your menu,
# you MUST set CONTACT_URL to the URL of the page that you want

34
contrib/docker/README.md Normal file
View File

@ -0,0 +1,34 @@
# Deploying WorkAdventure in production
This directory contains a sample production deployment of WorkAdventure using docker-compose.
Every production environment is different and this docker-compose file will not
fit all use cases. But it is intended to be a good starting point for you
to build your own deployment.
In this docker-compose file, you will find:
- A reverse-proxy (Traefik) that dispatches requests to the WorkAdventure containers and handles HTTPS certificates using LetsEncrypt
- A front container (nginx) that servers static files (HTML/JS/CSS)
- A pusher container (NodeJS) that is the point of entry for users (you can start many if you want to increase performance)
- A back container (NodeJS) that shares your rooms information
- An icon container to fetch the favicon of sites imported in iframes
- A Redis server to store values from variables originating from the Scripting API
```mermaid
graph LR
A[Browser] --> B(Traefik)
subgraph docker-compose
B --> C(Front)
B --> D(Pusher)
B --> E(Icon)
D --> F(Back)
F --> G(Redis)
end
A .-> H(Map)
F .-> H
```
**Important**: the default docker-compose file does **not** contain a container dedicated to hosting maps. The documentation and
tutorials are relying on GitHub Pages to host the maps. If you want to self-host your maps, you will need to add a simple
HTTP server (nginx / Apache, ...) and properly configure the [CORS settings as explained in the documentation](../../docs/maps/hosting.md).

View File

@ -12,7 +12,7 @@ services:
- --entryPoints.websecure.address=:${HTTPS_PORT}
# HTTP challenge
- --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL}
- --certificatesresolvers.myresolver.acme.storage=/acme.json
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
# Let's Encrypt's staging server
# uncomment during testing to avoid rate limiting
@ -22,7 +22,7 @@ services:
- "${HTTPS_PORT}:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${DATA_DIR}/letsencrypt/acme.json:/acme.json
- ${DATA_DIR}/letsencrypt/:/letsencrypt/
restart: ${RESTART_POLICY}
@ -95,6 +95,7 @@ services:
- JITSI_ISS
- MAX_PER_GROUP
- STORE_VARIABLES_FOR_LOCAL_MAPS
- REDIS_HOST=redis
labels:
- "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back.entryPoints=web"
@ -117,3 +118,11 @@ services:
- "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"
redis:
image: redis:6
volumes:
- redisdata:/data
volumes:
redisdata:

View File

@ -64,6 +64,11 @@
"ADMIN_API_URL": adminUrl,
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
"ADMIN_SOCKETS_TOKEN": env.ADMIN_SOCKETS_TOKEN,
"OPID_CLIENT_ID": "auth-code-client",
"OPID_CLIENT_SECRET": "mySecretHydraWA2022",
"OPID_CLIENT_ISSUER": "https://publichydra-"+url,
"OPID_CLIENT_REDIRECT_URL": "https://"+url+"/oauth/hydra",
"OPID_LOGIN_SCREEN_PROVIDER": "https://pusher-"+url+"/login-screen",
} else {})
},
"front": {

View File

@ -28,5 +28,4 @@ publish:
provider: github
owner: thecodingmachine
repo: workadventure
vPrefixedTagName: false
releaseType: draft
releaseType: release

View File

@ -0,0 +1,18 @@
const path = require('path');
const fs = require('fs');
let version = '0.0.0';
if (process.env.GITHUB_REF.startsWith('refs/tags/v')) {
version = process.env.GITHUB_REF.replace('refs/tags/v', '');
}
console.log('Version:', version);
const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
let data = fs.readFileSync(packageJsonPath, 'utf8');
data = data.replace('managedbyci', version);
fs.writeFileSync(packageJsonPath, data);

View File

@ -1,6 +1,6 @@
{
"name": "workadventure-desktop",
"version": "1.0.0",
"version": "managedbyci",
"description": "Desktop application for WorkAdventure",
"author": "thecodingmachine",
"main": "dist/main.js",
@ -11,7 +11,6 @@
"dev": "yarn build --watch --onSuccess 'yarn electron dist/main.js'",
"dev:local-app": "cd ../local-app && yarn && yarn dev",
"bundle": "yarn build:local-app && yarn build && electron-builder install-app-deps && electron-builder",
"release": "yarn bundle",
"typecheck": "tsc --noEmit",
"test": "exit 0",
"lint": "yarn eslint src/ . --ext .ts",
@ -32,13 +31,13 @@
},
"devDependencies": {
"@types/auto-launch": "^5.0.2",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"electron": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"electron": "^18.0.3",
"electron-builder": "^22.14.13",
"eslint": "^6.8.0",
"prettier": "^2.5.1",
"tsup": "^5.11.13",
"typescript": "^3.8.3"
"eslint": "^8.12.0",
"prettier": "^2.6.2",
"tsup": "^5.12.4",
"typescript": "^4.6.3"
}
}

View File

@ -10,7 +10,7 @@ import { setLogLevel } from "./log";
import "./serve"; // prepare custom url scheme
import { loadShortcuts } from "./shortcuts";
function init() {
async function init() {
const appLock = app.requestSingleInstanceLock();
if (!appLock) {
@ -21,7 +21,7 @@ function init() {
app.on("second-instance", () => {
// re-create window if closed
createWindow();
void createWindow();
const mainWindow = getWindow();
@ -36,15 +36,15 @@ function init() {
});
// This method will be called when Electron has finished loading
app.whenReady().then(async () => {
await app.whenReady().then(async () => {
await settings.init();
setLogLevel(settings.get("log_level") || "info");
autoUpdater.init();
await autoUpdater.init();
// enable auto launch
updateAutoLaunch();
await updateAutoLaunch();
// load ipc handler
ipc();
@ -72,7 +72,7 @@ function init() {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
void createWindow();
}
});

View File

@ -38,32 +38,35 @@ export async function manualRequestUpdateCheck() {
isManualRequestedUpdate = false;
}
function init() {
async function init() {
autoUpdater.logger = log;
autoUpdater.on("update-downloaded", ({ releaseNotes, releaseName }) => {
(async () => {
const dialogOpts = {
type: "question",
buttons: ["Install and Restart", "Install Later"],
defaultId: 0,
title: "WorkAdventure - Update",
message: process.platform === "win32" ? releaseNotes : releaseName,
detail: "A new version has been downloaded. Restart the application to apply the updates.",
};
autoUpdater.on(
"update-downloaded",
({ releaseNotes, releaseName }: { releaseNotes: string; releaseName: string }) => {
void (async () => {
const dialogOpts = {
type: "question",
buttons: ["Install and Restart", "Install Later"],
defaultId: 0,
title: "WorkAdventure - Update",
message: process.platform === "win32" ? releaseNotes : releaseName,
detail: "A new version has been downloaded. Restart the application to apply the updates.",
};
const { response } = await dialog.showMessageBox(dialogOpts);
if (response === 0) {
await sleep(1000);
const { response } = await dialog.showMessageBox(dialogOpts);
if (response === 0) {
await sleep(1000);
autoUpdater.quitAndInstall();
autoUpdater.quitAndInstall();
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
// app.confirmedExitPrompt = true;
app.quit();
}
})();
});
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
// app.confirmedExitPrompt = true;
app.quit();
}
})();
}
);
if (process.platform === "linux" && !process.env.APPIMAGE) {
autoUpdater.autoDownload = false;
@ -85,7 +88,7 @@ function init() {
}
});
checkForUpdates();
await checkForUpdates();
// run update check every hour again
setInterval(() => checkForUpdates, 1000 * 60 * 1);

View File

@ -1,4 +1,4 @@
import { ipcMain, app } from "electron";
import { ipcMain, app, desktopCapturer } from "electron";
import electronIsDev from "electron-is-dev";
import { createAndShowNotification } from "./notification";
import { Server } from "./preload-local-app/types";
@ -30,10 +30,18 @@ export default () => {
ipcMain.handle("get-version", () => (electronIsDev ? "dev" : app.getVersion()));
// app ipc
ipcMain.on("app:notify", (event, txt) => {
ipcMain.on("app:notify", (event, txt: string) => {
createAndShowNotification({ body: txt });
});
ipcMain.handle("app:getDesktopCapturerSources", async (event, options: Electron.SourcesOptions) => {
return (await desktopCapturer.getSources(options)).map((source) => ({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
}));
});
// local-app ipc
ipcMain.handle("local-app:showLocalApp", () => {
hideAppView();
@ -43,7 +51,7 @@ export default () => {
return settings.get("servers");
});
ipcMain.handle("local-app:selectServer", (event, serverId: string) => {
ipcMain.handle("local-app:selectServer", async (event, serverId: string) => {
const servers = settings.get("servers") || [];
const selectedServer = servers.find((s) => s._id === serverId);
@ -51,7 +59,7 @@ export default () => {
return new Error("Server not found");
}
showAppView(selectedServer.url);
await showAppView(selectedServer.url);
return true;
});

View File

@ -15,6 +15,7 @@ function onError(e: Error) {
function onRejection(reason: Error) {
if (reason instanceof Error) {
let _reason = reason;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const errPrototype = Object.getPrototypeOf(reason);
const nameProperty = Object.getOwnPropertyDescriptor(errPrototype, "name");

View File

@ -2,4 +2,4 @@ import app from "./app";
import log from "./log";
log.init();
app.init();
void app.init();

View File

@ -8,6 +8,7 @@ const api: WorkAdventureDesktopApi = {
notify: (txt) => ipcRenderer.send("app:notify", txt),
onMuteToggle: (callback) => ipcRenderer.on("app:on-mute-toggle", callback),
onCameraToggle: (callback) => ipcRenderer.on("app:on-camera-toggle", callback),
getDesktopCapturerSources: (options) => ipcRenderer.invoke("app:getDesktopCapturerSources", options),
};
contextBridge.exposeInMainWorld("WAD", api);

View File

@ -1,3 +1,15 @@
// copy of Electron.SourcesOptions to avoid Electron dependency in front
export interface SourcesOptions {
types: string[];
thumbnailSize?: { height: number; width: number };
}
export interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
}
export type WorkAdventureDesktopApi = {
desktop: boolean;
isDevelopment: () => Promise<boolean>;
@ -5,4 +17,5 @@ export type WorkAdventureDesktopApi = {
notify: (txt: string) => void;
onMuteToggle: (callback: () => void) => void;
onCameraToggle: (callback: () => void) => void;
getDesktopCapturerSources: (options: SourcesOptions) => Promise<DesktopCapturerSource[]>;
};

View File

@ -36,14 +36,14 @@ export function createTray() {
},
{
label: "Check for updates",
async click() {
await autoUpdater.manualRequestUpdateCheck();
click() {
void autoUpdater.manualRequestUpdateCheck();
},
},
{
label: "Open Logs",
click() {
log.openLog();
void log.openLog();
},
},
{

View File

@ -115,7 +115,7 @@ export async function createWindow() {
}
}
export function showAppView(url?: string) {
export async function showAppView(url?: string) {
if (!appView) {
throw new Error("App view not found");
}
@ -130,7 +130,7 @@ export function showAppView(url?: string) {
mainWindow.addBrowserView(appView);
if (url && url !== appViewUrl) {
appView.webContents.loadURL(url);
await appView.webContents.loadURL(url);
appViewUrl = url;
}

View File

@ -12,10 +12,10 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {},
//"baseUrl": ".",
//"paths": {},
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"useDefineForClassFields": true,
"module": "esnext",
"resolveJsonModule": true,
"baseUrl": ".",
//"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
@ -15,10 +15,23 @@
"allowJs": true,
"checkJs": true,
"paths": {
"~/*": ["./src/*"],
"@wa-preload-local-app": ["../electron/src/preload-local-app/types.ts"],
"~/*": [
"./src/*"
],
"@wa-preload-local-app": [
"../electron/src/preload-local-app/types.ts"
],
}
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}
"include": [
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.js",
"src/**/*.svelte"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -114,7 +114,6 @@ services:
#APACHE_EXTENSION_HEADERS: 1
STARTUP_COMMAND_0: sudo a2enmod headers
STARTUP_COMMAND_1: yarn install
STARTUP_COMMAND_2: yarn run dev &
volumes:
- ./maps:/var/www/html
labels:

View File

@ -9,7 +9,7 @@ The [scripting API](https://workadventu.re/map-building/scripting.md) allows map
The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to
offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance,
you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!)
you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!)
We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that
a feature is missing in the API. This article is here to explain to you how to add this feature.
@ -35,7 +35,7 @@ directly access Phaser objects (Phaser is the game engine used in WorkAdventure)
can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be
a huge XSS security flaw).
Instead, the only way the script can interact with WorkAdventure is by sending messages using the
Instead, the only way the script can interact with WorkAdventure is by sending messages using the
[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
![](images/scripting_2.svg)
@ -103,14 +103,14 @@ All the other files dedicated to the iframe API are located in the `src/Api/ifra
## Utility functions to exchange messages
In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the
In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the
[`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function.
Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes.
We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this.
Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function
from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a
from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a
[`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function.
## Types
@ -122,7 +122,7 @@ Indeed, Typescript interfaces only exist at compilation time but cannot be enfor
is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could
send specially crafted JSON packages to try to hack WA).
In WorkAdventure, we use the [generic-type-guard](https://github.com/mscharley/generic-type-guard) package. This package
In WorkAdventure, we use the [zod](https://github.com/colinhacks/zod) package. This package
allows us to create interfaces AND custom type guards in one go.
Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API:
@ -140,21 +140,20 @@ sendToWorkadventure({
The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`:
```typescript
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isChatEvent = z.object({
message: z.string(),
author: z.string(),
});
export const isChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
author: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
export type ChatEvent = z.infer<typeof isChatEvent>;
```
Using the generic-type-guard library, we start by writing a type guard function (`isChatEvent`).
Using the zod library, we start by writing a type guard function (`isChatEvent`).
From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code.
The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received
@ -212,7 +211,7 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to
define the type of the query and the type of the response.
The signature of `queryWorkadventure` is:
The signature of `queryWorkadventure` is:
```typescript
function queryWorkadventure<T extends keyof IframeQueryMap>(
@ -250,12 +249,12 @@ Here is a sample:
```typescript
iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => {
// ...
return /*...*/;
});
```
The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format
The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format
(the one you defined in the `answer` key of `iframeQueryMapTypeGuards`).
Important:

View File

@ -1,15 +1,15 @@
{.section-title.accent.text-primary}
# Working with camera
## Focusable Zones
## Focusable Area
It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Zones". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined zone, like this:
It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Area". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined area, like this:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/0_focusable_zone.png" alt="" />
</div>
### Adding new **Focusable Zone**:
### Adding new **Focusable Area**:
1. Make sure you are editing an **Object Layer**
@ -29,7 +29,7 @@ It is possible to define special regions on the map that can make the camera zoo
<img class="document-img" src="images/camera/3_define_new_zone.png" alt="" />
</div>
4. Make sure your object is of type "zone"!
4. Make sure your object is of type "area"!
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="images/camera/4_add_zone_type.png" alt="" />
@ -53,11 +53,11 @@ It is possible to define special regions on the map that can make the camera zoo
<img class="document-img" src="images/camera/7_make_sure_checked.png" alt="" />
</div>
All should be set up now and your new **Focusable Zone** should be working fine!
All should be set up now and your new **Focusable Area** should be working fine!
### Defining custom zoom margin:
If you want, you can add an additional property to control how much should the camera zoom onto focusable zone.
If you want, you can add an additional property to control how much should the camera zoom onto focusable area.
1. Like before, click on **Add Property**
@ -77,7 +77,7 @@ If you want, you can add an additional property to control how much should the c
<img class="document-img" src="images/camera/9_optional_zoom_margin_defined.png" alt="" />
</div>
For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300.
For example, if you define your area as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire area + margin of 50% of its dimensions, so 450x300.
- No margin defined

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -9,7 +9,7 @@ In order to build your own map for WorkAdventure, you need:
* "tiles" (i.e. images) to create your map
* a web-server to serve your map
WorkAdventure comes with a "map starter kit" that we recommend using to start designing your map quickly. It contains **a good default tileset** for building an office and it proposes to **use Github static pages as a web-server** which is both free and performant. It also comes with a local webserver for testing purpose and with Typescript support (if you are looking to use the [map scripting API]({{url('/map-building/scripting')}}).
WorkAdventure comes with a "map starter kit" that we recommend using to start designing your map quickly. It contains **a good default tileset** for building an office and it proposes to **use Github static pages as a web-server** which is both free and performant. It also comes with a local webserver for testing purpose and with Typescript support (if you are looking to use the [map scripting API](scripting.md).
{.alert.alert-info}
If you are looking to host your maps on your own webserver, be sure to read the [Self-hosting your map](hosting.md) guide.

View File

@ -9,19 +9,24 @@ On your map, you can define special zones (meeting rooms) that will trigger the
In order to create Jitsi meet zones:
* You must create a specific layer.
* In layer properties, you MUST add a "`jitsiRoom`" property (of type "`string`"). The value of the property is the name of the room in Jitsi. Note: the name of the room will be "slugified" and prepended with the name of the instance of the map (so that different instances of the map have different rooms)
* You must create a specific object.
* Object must be of type "`area`"
* In object properties, you MUST add a "`jitsiRoom`" property (of type "`string`"). The value of the property is the name of the room in Jitsi. Note: the name of the room will be "slugified" and prepended with a hash of the room URL
* You may also use "jitsiWidth" property (of type "number" between 0 and 100) to control the width of the iframe containing the meeting room.
You can have this layer (i.e. your meeting area) to be selectable as the precise location for your meeting using the [Google Calendar integration for Work Adventure](/integrations/google-calendar). To do so, you must set the `meetingRoomLabel` property. You can provide any name that you would like your meeting room to have (as a string).
You can have this object (i.e. your meeting area) to be selectable as the precise location for your meeting using the [Google Calendar integration for Work Adventure](/integrations/google-calendar). To do so, you must set the `meetingRoomLabel` property. You can provide any name that you would like your meeting room to have (as a string).
{.alert.alert-info}
As an alternative, you may also put the `jitsiRoom` properties on a layer (rather than putting them on an "area" object)
but we advise to stick with "area" objects for better performance!
## Triggering of the "Jitsi meet" action
By default, Jitsi meet will open when a user enters the zone defined on the map.
By default, Jitsi meet will open when a user enters the area defined on the map.
It is however possible to trigger Jitsi only on user action. You can do this with the `jitsiTrigger` property.
If you set `jitsiTrigger: onaction`, when the user walks on the layer, an alert message will be displayed at the bottom of the screen:
If you set `jitsiTrigger: onaction`, when the user walks on the area, an alert message will be displayed at the bottom of the screen:
<figure class="figure">
<img src="images/click_space_jitsi.png" class="figure-img img-fluid rounded" alt="" />
@ -32,7 +37,7 @@ If you set `jitsiTriggerMessage: your message action` you can edit alert message
## Customizing your "Jitsi meet"
Your Jitsi meet experience can be customized using Jitsi specific config options. The `jitsiConfig` and `jitsiInterfaceConfig` properties can be used on the Jitsi layer to change the way Jitsi looks and behaves. Those 2 properties are accepting a JSON string.
Your Jitsi meet experience can be customized using Jitsi specific config options. The `jitsiConfig` and `jitsiInterfaceConfig` properties can be used on the Jitsi object to change the way Jitsi looks and behaves. Those 2 properties are accepting a JSON string.
For instance, use `jitsiConfig: { "startWithAudioMuted": true }` to automatically mute the microphone when someone enters a room. Or use `jitsiInterfaceConfig: { "DEFAULT_BACKGROUND": "#77ee77" }` to change the background color of Jitsi.
@ -60,7 +65,7 @@ You can grant moderator rights to some of your members. Jitsi moderators can:
* Mute everybody expect one speaker
* Kick users out of the meeting
In order to grant moderator rights to a given user, you can add a `jitsiRoomAdminTag` property to your Jitsi layer. For instance, if you write a property:
In order to grant moderator rights to a given user, you can add a `jitsiRoomAdminTag` property to your Jitsi object. For instance, if you write a property:
jitsiRoomAdminTag: speaker
@ -74,7 +79,7 @@ WorkAdventure usually comes with a default Jitsi meet installation. If you are u
You have the possibility, in your map, to override the Jitsi meet instance that will be used by default. This can be useful for regulatory reasons. Maybe your company wants to keep control on the video streams and therefore, wants to self-host a Jitsi instance? Or maybe you want to use a very special configuration or very special version of Jitsi?
Use the `jitsiUrl` property to in the Jitsi layer to specify the Jitsi instance that should be used. Beware, `jitsiUrl` takes in parameter a **domain name**, without the protocol. So you should use:
Use the `jitsiUrl` property to in the Jitsi object to specify the Jitsi instance that should be used. Beware, `jitsiUrl` takes in parameter a **domain name**, without the protocol. So you should use:
`jitsiUrl: meet.jit.si`
and not
`jitsiUrl: https://meet.jit.si`
@ -82,3 +87,15 @@ and not
{.alert.alert-info}
When you use `jitsiUrl`, the targeted Jitsi instance must be public. You cannot use moderation features or the JWT
tokens authentication with maps configured using the `jitsiUrl` property.
## Full control over the Jitsi room name
By default, the name of the room will be "slugified" and prepended with a hash of the room URL.
This is what you want most of the time. Indeed, different maps with the same Jitsi room name (the same `jitsiRoom` property) will not share the same Jitsi room instance.
However, sometimes, you may actually want to have different WorkAdventure meeting rooms that are actually sharing
the same Jitsi meet meeting room. Or if you are pointing to a custom Jitsi server (using the `jitsiUrl` property),
you may want to point to a specific existing room.
For all those use cases, you can use `jitsiNoPrefix: true`. This will remove the automatic prefixing
of the hash and will give you full control on the Jitsi room name.

View File

@ -166,7 +166,8 @@ return [
],
[
'title' => 'Troubleshooting',
'url' => '/map-building/troubleshooting',
'view' => 'content.map.troubleshooting'
'url' => '/map-building/troubleshooting.md',
'markdown' => 'maps.troubleshooting',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/troubleshooting.md',
],
];

View File

@ -10,14 +10,26 @@ on the right side of the screen)
In order to create a zone that opens websites:
* You must create a specific layer.
* In layer properties, you MUST add a "`openWebsite`" property (of type "`string`"). The value of the property is the URL of the website to open (the URL must start with "https://")
* You must create a specific object.
* Object must be of type "`area`"
* In object properties, you MUST add a "`openWebsite`" property (of type "`string`"). The value of the property is the URL of the website to open (the URL must start with "https://")
* You may also use "`openWebsiteWidth`" property (of type "`int`" or "`float`" between 0 and 100) to control the width of the iframe.
* You may also use "`openTab`" property (of type "`string`") to open in a new tab instead.
{.alert.alert-warning}
A website can explicitly forbid another website from loading it in an iFrame using
the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options).
the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). You can
read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#embedding-an-iframe-is-forbidden).
{.alert.alert-info}
As an alternative, you may also put the `openWebsite` properties on a layer (rather than putting them on an "area" object)
but we advise sticking with "area" objects for better performance!
{.alert.alert-warning}
If the website you are embedding is using cookies, those cookies must be configured with the `SameSite=none` attribute. Otherwise,
they will be ignored by the browser. If you manage to see the website you embed but cannot log into it, the `SameSite` attribute is most
likely the culprit. You can read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#i-cannot-log-into-my-embedded-website).
## Integrating a Youtube video
@ -43,7 +55,7 @@ By default, the iFrame will open when a user enters the zone defined on the map.
It is however possible to trigger the iFrame only on user action. You can do this with the `openWebsiteTrigger` property.
If you set `openWebsiteTrigger: onaction`, when the user walks on the layer, an alert message will be displayed at the bottom of the screen:
If you set `openWebsiteTrigger: onaction`, when the user walks on the area, an alert message will be displayed at the bottom of the screen:
<figure class="figure">
<img src="images/click_space_open_website.jpg" class="figure-img img-fluid rounded" alt="" />
@ -52,7 +64,7 @@ If you set `openWebsiteTrigger: onaction`, when the user walks on the layer, an
If you set `openWebsiteTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to open the web site'.
If you set `openWebsiteTrigger: onicon`, when the user walks on the layer, an icon will be displayed at the bottom of the screen:
If you set `openWebsiteTrigger: onicon`, when the user walks on the area, an icon will be displayed at the bottom of the screen:
<figure class="figure">
<img src="images/icon_open_website.png" class="figure-img img-fluid rounded" alt="" />
@ -78,6 +90,6 @@ Cowebsites allow you to have several sites open at the same time.
If you want to open a Jitsi and another page it's easy!
You have just to [add a Jitsi to the map](meeting-rooms.md) and [add a co-website](opening-a-website.md#the-openwebsite-property) on the same layer.
You have just to [add a Jitsi to the map](meeting-rooms.md) and [add a co-website](opening-a-website.md#the-openwebsite-property) on the same object.
It's done!

View File

@ -9,8 +9,13 @@ On your map, you can define special silent zones where nobody is allowed to talk
In order to create a silent zone:
* You must create a specific layer.
* In layer properties, you MUST add a boolean "`silent`" property. If the silent property is checked, the users are entering the silent zone when they walk on any tile of the layer.
* You must create a specific object.
* Object must be of type "`area`"
* In object properties, you MUST add a boolean "`silent`" property. If the silent property is checked, the users are entering the silent zone when they walk on the area.
{.alert.alert-info}
As an alternative, you may also put the `silent` property on a layer (rather than putting them on an "area" object)
but we advise to stick with "area" objects for better performance!
## Playing sounds or background music
@ -18,10 +23,15 @@ Your map can define special zones where a sound or background music will automat
In order to create a zone that triggers sounds/music:
* You must create a specific layer.
* In layer properties, you MUST add a "`playAudio`" property. The value of the property is a URL to an MP3 file that will be played. The URL can be relative to the URL of the map.
* You must create a specific object.
* Object must be of type "`area`"
* In object properties, you MUST add a "`playAudio`" property. The value of the property is a URL to an MP3 file that will be played. The URL can be relative to the URL of the map.
* You may use the boolean property "`audioLoop`" to make the sound loop (thanks captain obvious).
* If the "`audioVolume`" property is set, the audio player uses either the value of the property or the last volume set by the user - whichever is smaller. This property is a float from 0 to 1.0
{.alert.alert-info}
"`playAudioLoop`" is deprecated and should not be used anymore.
{.alert.alert-info}
As an alternative, you may also put the `playAudio` properties on a layer (rather than putting them on an "area" object)
but we advise to stick with "area" objects for better performance!

View File

@ -0,0 +1,94 @@
{.section-title.accent.text-primary}
# Troubleshooting
## Look at the browser console
If your map is not displayed correctly (most notably if you are getting a black screen), open your browser console.
This is usually done by pressing the F12 key and selecting the "console" tab.
Scan the output. Towards the end, you might see a message explaining why your map cannot be loaded.
## Check webserver CORS settings
If you are hosting the map you built on your own webserver and if the map does not load, please check that
[your webserver CORS settings are correctly configured](hosting.md).
## Issues embedding a website
When you are embedding a website in WorkAdventure (whether it is using the [`openWebsite` property](opening-a-website.md) or
the [integrated website in a map](website-in-map.md) feature or the [Scripting API](scripting.md)), WorkAdventure
will open your website using an iFrame.
Browsers have various security measures in place, and website owners can use those measures to prevent websites from
being used inside iFrames (either partially or completely).
In the chapters below, we will list what can possibly prevent you from embedding a website, and see what are your options.
### Embedding an iFrame is forbidden
The worst that can happen is that the website you are trying to embed completely denies you the authorisation.
A website owner can do that using the [`X-Frame-Options` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options),
or the newer [`Content-Security-Policy` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
Take a look at the headers of the page you are trying to load.
{.alert.alert-info}
You can view the headers of the web page you try to load in the developer tools of your browser (usually accessible using the F12 key
of your keyboard), in the network tab. Click on the top-most request and check the "Response Headers".
Below is what you can see when opening a Youtube video page:
![](images/x-frame-options.png)
`X-Frame-Options: DENY` or `X-Frame-Options: SAMEORIGIN` will prevent WorkAdventure from loading the page.
`Content-Security-Policy` header have also the potential to prevent WorkAdventure from loading the page.
If the website you are trying to embed has one of these headers set, here are your options:
- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask for an exception
- otherwise, you can look for an "embed" option. Some websites have special pages that can be embedded. For instance,
YouTube has special "embed" links that can be used to embed a video in your website. A lot of websites have the same feature (you
can usually find those links in the "share" section)
If none of these options are available to you, as a last resort, you can use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property.
It will open your webpage in another tab instead of opening it in an iFrame.
### I cannot log into my embedded website
When you log into a website, the website is issuing a "cookie". The cookie is a unique identifier that allows the website
to recognize you and to identify you. To improve the privacy of their users, browsers can sometimes treat cookies
inside iFrames as "third-party cookies" and discard them.
Cookies can come with a `SameSite` attribute.
The `SameSite` attribute can take these values: "Lax", "Strict" or "None". The only value that allows using the
cookie inside an iFrame is "None".
{.alert.alert-info}
The `SameSite` attribute of your cookie MUST be set to "None" if you want to be able to use this cookie from an iFrame inside WorkAdventure.
**Default values**:
If the "SameSite" attribute is not explicitly set, [the behaviour depends on the browser](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#browser_compatibility).
Chrome, Edge and Opera will default to "Lax".
Firefox and Safari will default to "None" (as of 2022/04/25).
As a result, a website that does not set the `SameSite` attribute on cookies will work correctly in Firefox and Safari but
login will fail on Chrome, Edge and Opera.
If the website you are trying to embed has the `SameSite` attribute set to a value other than "None", here are your options:
- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask
the owner/administrator to change the `SameSite` settings.
- otherwise, you will have to use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property.
It will open your webpage in another tab instead of in an iFrame.
## Need some help?
<div class="card bg-red text-white"><div class="card-body">
<p>WorkAdventure is a constantly evolving project and there is plenty of room for improvement regarding map editing.</p>
<p>If you are facing any troubles, do not hesitate to seek help in
<a href="https://discord.gg/G6Xh9ZM9aR">our Discord server</a> or open an "issue" in the
<a href="https://github.com/thecodingmachine/workadventure/issues" target="_blank">GitHub WorkAdventure account</a>.
</p>
</div></div>

View File

@ -87,11 +87,11 @@ Repeat for every tile that should be "collidable".
In the next sections, you will see how you can add behaviour on your map by adding "properties".
You can add properties for a variety of features: putting exits, opening websites, meeting rooms, silent zones, etc...
You can add properties either on individual tiles of a tileset OR on a complete layer.
You can add properties either on individual tiles of a tileset, on Tiled object OR on a complete layer.
If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer.
If you put a property on a object or layer, it will be triggered if your Woka walks on object area / any tile of the layer.
The exception is the "collides" property that can only be set on tiles, but not on a complete layer.
The exception is the "collides" property that can only be set on tiles, but not on an object or on complete layer.
## Insert helpful information in your map

View File

@ -27,8 +27,8 @@ module.exports = {
],
"overrides": [
{
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
}
],
"rules": {
@ -36,6 +36,7 @@ module.exports = {
"eol-last": ["error", "always"],
"@typescript-eslint/no-explicit-any": "error",
"no-throw-literal": "error",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none", "caughtErrors": "all", "varsIgnorePattern": "_exhaustiveCheck" }],
// TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/restrict-plus-operands": "off",

View File

@ -1,5 +1,3 @@
src/Messages/generated
src/Messages/JsonMessages
src/i18n/i18n-svelte.ts
src/i18n/i18n-types.ts
src/i18n/i18n-util.ts
src/i18n/i18n-*.ts

View File

@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json",
"$schema": "https://unpkg.com/typesafe-i18n@5.4.0/schema/typesafe-i18n.json",
"baseLocale": "en-US",
"adapter": "svelte"
}

View File

@ -4,7 +4,8 @@
"main": "index.js",
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"@geprog/vite-plugin-env-config": "^4.0.0",
"@geprog/vite-plugin-env-config": "^4.0.3",
"@home-based-studio/phaser3-utils": "^0.4.7",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.36",
"@tsconfig/svelte": "^1.0.10",
"@types/google-protobuf": "^3.7.3",
@ -41,12 +42,11 @@
"buffer": "^6.0.3",
"cancelable-promise": "^4.2.1",
"cross-env": "^7.0.3",
"deep-copy-ts": "^0.5.0",
"deep-copy-ts": "^0.5.4",
"easystarjs": "^0.4.4",
"fast-deep-equal": "^3.1.3",
"generic-type-guard": "^3.4.2",
"google-protobuf": "^3.13.0",
"phaser": "^3.54.0",
"phaser": "3.55.1",
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",
"phaser3-rex-plugins": "^1.1.42",
"posthog-js": "^1.14.1",
@ -60,9 +60,9 @@
"standardized-audio-context": "^25.2.4",
"ts-deferred": "^1.0.4",
"ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0",
"typesafe-i18n": "^5.4.0",
"uuidv4": "^6.2.10",
"zod": "^3.11.6"
"zod": "^3.14.3"
},
"scripts": {
"start": "run-p templater serve watch-iframe-api svelte-check-watch typesafe-i18n-watch",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -9,7 +9,7 @@ class AnalyticsClient {
constructor() {
if (POSTHOG_API_KEY && POSTHOG_URL) {
this.posthogPromise = import("posthog-js").then(({ default: posthog }) => {
posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_URL, disable_cookie: true });
posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_URL });
//the posthog toolbar need a reference in window to be able to work
window.posthog = posthog;
return posthog;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isActionsMenuActionClickedEvent = new tg.IsInterface()
.withProperties({
id: tg.isNumber,
actionName: tg.isString,
})
.get();
export const isActionsMenuActionClickedEvent = z.object({
id: z.number(),
actionName: z.string(),
});
export type ActionsMenuActionClickedEvent = tg.GuardedType<typeof isActionsMenuActionClickedEvent>;
export type ActionsMenuActionClickedEvent = z.infer<typeof isActionsMenuActionClickedEvent>;
export type ActionsMenuActionClickedEventCallback = (event: ActionsMenuActionClickedEvent) => void;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isAddActionsMenuKeyToRemotePlayerEvent = new tg.IsInterface()
.withProperties({
id: tg.isNumber,
actionKey: tg.isString,
})
.get();
export const isAddActionsMenuKeyToRemotePlayerEvent = z.object({
id: z.number(),
actionKey: z.string(),
});
export type AddActionsMenuKeyToRemotePlayerEvent = tg.GuardedType<typeof isAddActionsMenuKeyToRemotePlayerEvent>;
export type AddActionsMenuKeyToRemotePlayerEvent = z.infer<typeof isAddActionsMenuKeyToRemotePlayerEvent>;
export type AddActionsMenuKeyToRemotePlayerEventCallback = (event: AddActionsMenuKeyToRemotePlayerEvent) => void;

View File

@ -1,12 +1,11 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isButtonClickedEvent = z.object({
popupId: z.number(),
buttonId: z.number(),
});
export const isButtonClickedEvent = new tg.IsInterface()
.withProperties({
popupId: tg.isNumber,
buttonId: tg.isNumber,
})
.get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type ButtonClickedEvent = tg.GuardedType<typeof isButtonClickedEvent>;
export type ButtonClickedEvent = z.infer<typeof isButtonClickedEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isCameraFollowPlayerEvent = z.object({
smooth: z.boolean(),
});
export const isCameraFollowPlayerEvent = new tg.IsInterface()
.withProperties({
smooth: tg.isBoolean,
})
.get();
/**
* A message sent from the iFrame to the game to make the camera follow player.
*/
export type CameraFollowPlayerEvent = tg.GuardedType<typeof isCameraFollowPlayerEvent>;
export type CameraFollowPlayerEvent = z.infer<typeof isCameraFollowPlayerEvent>;

View File

@ -1,16 +1,15 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isCameraSetEvent = z.object({
x: z.number(),
y: z.number(),
width: z.optional(z.number()),
height: z.optional(z.number()),
lock: z.boolean(),
smooth: z.boolean(),
});
export const isCameraSetEvent = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isOptional(tg.isNumber),
height: tg.isOptional(tg.isNumber),
lock: tg.isBoolean,
smooth: tg.isBoolean,
})
.get();
/**
* A message sent from the iFrame to the game to change the camera position.
*/
export type CameraSetEvent = tg.GuardedType<typeof isCameraSetEvent>;
export type CameraSetEvent = z.infer<typeof isCameraSetEvent>;

View File

@ -0,0 +1,10 @@
import { z } from "zod";
export const isChangeZoneEvent = z.object({
name: z.string(),
});
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone.
*/
export type ChangeAreaEvent = z.infer<typeof isChangeZoneEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isChangeLayerEvent = z.object({
name: z.string(),
});
export const isChangeLayerEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a layer.
*/
export type ChangeLayerEvent = tg.GuardedType<typeof isChangeLayerEvent>;
export type ChangeLayerEvent = z.infer<typeof isChangeLayerEvent>;

View File

@ -1,11 +0,0 @@
import * as tg from "generic-type-guard";
export const isChangeZoneEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone.
*/
export type ChangeZoneEvent = tg.GuardedType<typeof isChangeZoneEvent>;

View File

@ -1,12 +1,11 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isChatEvent = z.object({
message: z.string(),
author: z.string(),
});
export const isChatEvent = new tg.IsInterface()
.withProperties({
message: tg.isString,
author: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
export type ChatEvent = z.infer<typeof isChatEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isCloseCoWebsite = new tg.IsInterface()
.withProperties({
id: tg.isOptional(tg.isString),
})
.get();
export const isCloseCoWebsite = z.object({
id: z.optional(z.string()),
});
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type CloseCoWebsiteEvent = tg.GuardedType<typeof isCloseCoWebsite>;
export type CloseCoWebsiteEvent = z.infer<typeof isCloseCoWebsite>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isClosePopupEvent = new tg.IsInterface()
.withProperties({
popupId: tg.isNumber,
})
.get();
export const isClosePopupEvent = z.object({
popupId: z.number(),
});
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ClosePopupEvent = tg.GuardedType<typeof isClosePopupEvent>;
export type ClosePopupEvent = z.infer<typeof isClosePopupEvent>;

View File

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isColorEvent = z.object({
red: z.number(),
green: z.number(),
blue: z.number(),
});
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>;
export type ColorEvent = z.infer<typeof isColorEvent>;

View File

@ -1,52 +1,43 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isRectangle = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
})
.get();
export const isRectangle = z.object({
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number(),
});
export const isEmbeddedWebsiteEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.withOptionalProperties({
url: tg.isString,
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
})
.get();
// TODO: make a variation that is all optional (except for the name)
export type Rectangle = z.infer<typeof isRectangle>;
export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
url: tg.isString,
position: isRectangle,
})
.withOptionalProperties({
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
origin: tg.isSingletonStringUnion("player", "map"),
scale: tg.isNumber,
})
.get();
export const isEmbeddedWebsiteEvent = z.object({
name: z.string(),
url: z.optional(z.string()),
visible: z.optional(z.boolean()),
allowApi: z.optional(z.boolean()),
allow: z.optional(z.string()),
x: z.optional(z.number()),
y: z.optional(z.number()),
width: z.optional(z.number()),
height: z.optional(z.number()),
origin: z.optional(z.enum(["player", "map"])),
scale: z.optional(z.number()),
});
/**
* A message sent from the iFrame to the game to modify an embedded website
*/
export type ModifyEmbeddedWebsiteEvent = tg.GuardedType<typeof isEmbeddedWebsiteEvent>;
export type ModifyEmbeddedWebsiteEvent = z.infer<typeof isEmbeddedWebsiteEvent>;
export type CreateEmbeddedWebsiteEvent = tg.GuardedType<typeof isCreateEmbeddedWebsiteEvent>;
// TODO: make a variation that is all optional (except for the name)
export type Rectangle = tg.GuardedType<typeof isRectangle>;
export const isCreateEmbeddedWebsiteEvent = z.object({
name: z.string(),
url: z.string(),
position: isRectangle,
visible: z.optional(z.boolean()),
allowApi: z.optional(z.boolean()),
allow: z.optional(z.string()),
origin: z.optional(z.enum(["player", "map"])),
scale: z.optional(z.number()),
});
export type CreateEmbeddedWebsiteEvent = z.infer<typeof isCreateEmbeddedWebsiteEvent>;

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isEnterLeaveEvent = z.object({
name: z.string(),
});
export const isEnterLeaveEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type EnterLeaveEvent = tg.GuardedType<typeof isEnterLeaveEvent>;
export type EnterLeaveEvent = z.infer<typeof isEnterLeaveEvent>;

View File

@ -1,20 +1,19 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isGameStateEvent = z.object({
roomId: z.string(),
mapUrl: z.string(),
nickname: z.string(),
language: z.optional(z.string()),
uuid: z.optional(z.string()),
startLayerName: z.optional(z.string()),
tags: z.array(z.string()),
variables: z.unknown(), // Todo : Typing
playerVariables: z.unknown(), // Todo : Typing
userRoomToken: z.optional(z.string()),
});
export const isGameStateEvent = new tg.IsInterface()
.withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isString,
language: tg.isUnion(tg.isString, tg.isUndefined),
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString),
variables: tg.isObject,
playerVariables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
})
.get();
/**
* A message sent from the game to the iFrame when the gameState is received by the script
*/
export type GameStateEvent = tg.GuardedType<typeof isGameStateEvent>;
export type GameStateEvent = z.infer<typeof isGameStateEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isGoToPageEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
})
.get();
export const isGoToPageEvent = z.object({
url: z.string(),
});
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type GoToPageEvent = tg.GuardedType<typeof isGoToPageEvent>;
export type GoToPageEvent = z.infer<typeof isGoToPageEvent>;

View File

@ -1,19 +1,17 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isHasPlayerMovedEvent = new tg.IsInterface()
.withProperties({
direction: tg.isElementOf("right", "left", "up", "down"),
moving: tg.isBoolean,
x: tg.isNumber,
y: tg.isNumber,
oldX: tg.isOptional(tg.isNumber),
oldY: tg.isOptional(tg.isNumber),
})
.get();
export const isHasPlayerMovedEvent = z.object({
direction: z.enum(["right", "left", "up", "down"]),
moving: z.boolean(),
x: z.number(),
y: z.number(),
oldX: z.optional(z.number()),
oldY: z.optional(z.number()),
});
/**
* A message sent from the game to the iFrame to notify a movement from the current player.
*/
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEvent = z.infer<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;

View File

@ -1,45 +1,44 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent";
import { isChatEvent } from "./ChatEvent";
import { isClosePopupEvent } from "./ClosePopupEvent";
import type { EnterLeaveEvent } from "./EnterLeaveEvent";
import type { GoToPageEvent } from "./GoToPageEvent";
import type { LoadPageEvent } from "./LoadPageEvent";
import { isGoToPageEvent } from "./GoToPageEvent";
import { isLoadPageEvent } from "./LoadPageEvent";
import { isCoWebsite, isOpenCoWebsiteEvent } from "./OpenCoWebsiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent";
import { isOpenPopupEvent } from "./OpenPopupEvent";
import { isOpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
import { isLayerEvent } from "./LayerEvent";
import { isSetPropertyEvent } from "./setPropertyEvent";
import { isLoadSoundEvent } from "./LoadSoundEvent";
import { isPlaySoundEvent } from "./PlaySoundEvent";
import { isStopSoundEvent } from "./StopSoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent";
import { isSetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent";
import type { EmbeddedWebsite } from "../iframe/Room/EmbeddedWebsite";
import { isCreateEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent";
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isCreateEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./ui/MenuRegisterEvent";
import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import { isPlayerPosition } from "./PlayerPosition";
import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import type { CameraSetEvent } from "./CameraSetEvent";
import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
import type { ChangeAreaEvent } from "./ChangeAreaEvent";
import { isCameraSetEvent } from "./CameraSetEvent";
import { isCameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
import { isColorEvent } from "./ColorEvent";
import { isMovePlayerToEventConfig } from "./MovePlayerToEvent";
import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer";
import type { RemotePlayerClickedEvent } from "./RemotePlayerClickedEvent";
import type { AddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRemotePlayerEvent";
import { isAddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRemotePlayerEvent";
import type { ActionsMenuActionClickedEvent } from "./ActionsMenuActionClickedEvent";
import type { RemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent";
import { isRemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@ -48,45 +47,114 @@ export interface TypedMessageEvent<T> extends MessageEvent {
/**
* List event types sent from an iFrame to WorkAdventure
*/
export type IframeEventMap = {
addActionsMenuKeyToRemotePlayer: AddActionsMenuKeyToRemotePlayerEvent;
removeActionsMenuKeyFromRemotePlayer: RemoveActionsMenuKeyFromRemotePlayerEvent;
loadPage: LoadPageEvent;
chat: ChatEvent;
cameraFollowPlayer: CameraFollowPlayerEvent;
cameraSet: CameraSetEvent;
openPopup: OpenPopupEvent;
closePopup: ClosePopupEvent;
openTab: OpenTabEvent;
goToPage: GoToPageEvent;
disablePlayerControls: null;
restorePlayerControls: null;
displayBubble: null;
removeBubble: null;
onPlayerMove: undefined;
onOpenActionMenu: undefined;
onCameraUpdate: undefined;
showLayer: LayerEvent;
hideLayer: LayerEvent;
setProperty: SetPropertyEvent;
loadSound: LoadSoundEvent;
playSound: PlaySoundEvent;
stopSound: null;
getState: undefined;
loadTileset: LoadTilesetEvent;
registerMenu: MenuRegisterEvent;
unregisterMenu: UnregisterMenuEvent;
setTiles: SetTilesEvent;
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact
};
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;
data: IframeEventMap[T];
}
export const isIframeEventWrapper = z.union([
z.object({
type: z.literal("addActionsMenuKeyToRemotePlayer"),
data: isAddActionsMenuKeyToRemotePlayerEvent,
}),
z.object({
type: z.literal("removeActionsMenuKeyFromRemotePlayer"),
data: isRemoveActionsMenuKeyFromRemotePlayerEvent,
}),
z.object({
type: z.literal("loadPage"),
data: isLoadPageEvent,
}),
z.object({
type: z.literal("chat"),
data: isChatEvent,
}),
z.object({
type: z.literal("cameraFollowPlayer"),
data: isCameraFollowPlayerEvent,
}),
z.object({
type: z.literal("cameraSet"),
data: isCameraSetEvent,
}),
z.object({
type: z.literal("openPopup"),
data: isOpenPopupEvent,
}),
z.object({
type: z.literal("closePopup"),
data: isClosePopupEvent,
}),
z.object({
type: z.literal("openTab"),
data: isOpenTabEvent,
}),
z.object({
type: z.literal("goToPage"),
data: isGoToPageEvent,
}),
z.object({
type: z.literal("disablePlayerControls"),
data: z.undefined(),
}),
z.object({
type: z.literal("restorePlayerControls"),
data: z.undefined(),
}),
z.object({
type: z.literal("displayBubble"),
data: z.undefined(),
}),
z.object({
type: z.literal("removeBubble"),
data: z.undefined(),
}),
z.object({
type: z.literal("onPlayerMove"),
data: z.undefined(),
}),
z.object({
type: z.literal("onCameraUpdate"),
data: z.undefined(),
}),
z.object({
type: z.literal("showLayer"),
data: isLayerEvent,
}),
z.object({
type: z.literal("hideLayer"),
data: isLayerEvent,
}),
z.object({
type: z.literal("setProperty"),
data: isSetPropertyEvent,
}),
z.object({
type: z.literal("loadSound"),
data: isLoadSoundEvent,
}),
z.object({
type: z.literal("playSound"),
data: isPlaySoundEvent,
}),
z.object({
type: z.literal("stopSound"),
data: isStopSoundEvent,
}),
z.object({
type: z.literal("registerMenu"),
data: isMenuRegisterEvent,
}),
z.object({
type: z.literal("unregisterMenu"),
data: isUnregisterMenuEvent,
}),
z.object({
type: z.literal("setTiles"),
data: isSetTilesEvent,
}),
z.object({
type: z.literal("modifyEmbeddedWebsite"),
data: isEmbeddedWebsiteEvent,
}),
]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
typeof event.type === "string";
export type IframeEvent = z.infer<typeof isIframeEventWrapper>;
export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent;
@ -94,8 +162,8 @@ export interface IframeResponseEventMap {
leaveEvent: EnterLeaveEvent;
enterLayerEvent: ChangeLayerEvent;
leaveLayerEvent: ChangeLayerEvent;
enterZoneEvent: ChangeZoneEvent;
leaveZoneEvent: ChangeZoneEvent;
enterAreaEvent: ChangeAreaEvent;
leaveAreaEvent: ChangeAreaEvent;
buttonClickedEvent: ButtonClickedEvent;
remotePlayerClickedEvent: RemotePlayerClickedEvent;
actionsMenuActionClickedEvent: ActionsMenuActionClickedEvent;
@ -115,73 +183,78 @@ export const isIframeResponseEventWrapper = (event: {
type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
export const isLookingLikeIframeEventWrapper = z.object({
type: z.string(),
data: z.unknown().optional(),
});
/**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame.
* Types are defined using Type guards that will actually bused to enforce and check types.
*/
export const iframeQueryMapTypeGuards = {
getState: {
query: tg.isUndefined,
query: z.undefined(),
answer: isGameStateEvent,
},
getMapData: {
query: tg.isUndefined,
query: z.undefined(),
answer: isMapDataEvent,
},
setVariable: {
query: isSetVariableEvent,
answer: tg.isUndefined,
answer: z.undefined(),
},
loadTileset: {
query: isLoadTilesetEvent,
answer: tg.isNumber,
answer: z.number(),
},
openCoWebsite: {
query: isOpenCoWebsiteEvent,
answer: isCoWebsite,
},
getCoWebsites: {
query: tg.isUndefined,
answer: tg.isArray(isCoWebsite),
query: z.undefined(),
answer: z.array(isCoWebsite),
},
closeCoWebsite: {
query: tg.isString,
answer: tg.isUndefined,
query: z.string(),
answer: z.undefined(),
},
closeCoWebsites: {
query: tg.isUndefined,
answer: tg.isUndefined,
query: z.undefined(),
answer: z.undefined(),
},
triggerActionMessage: {
query: isTriggerActionMessageEvent,
answer: tg.isUndefined,
answer: z.undefined(),
},
removeActionMessage: {
query: isMessageReferenceEvent,
answer: tg.isUndefined,
answer: z.undefined(),
},
getEmbeddedWebsite: {
query: tg.isString,
query: z.string(),
answer: isCreateEmbeddedWebsiteEvent,
},
deleteEmbeddedWebsite: {
query: tg.isString,
answer: tg.isUndefined,
query: z.string(),
answer: z.undefined(),
},
createEmbeddedWebsite: {
query: isCreateEmbeddedWebsiteEvent,
answer: tg.isUndefined,
answer: z.undefined(),
},
setPlayerOutline: {
query: isColorEvent,
answer: tg.isUndefined,
answer: z.undefined(),
},
removePlayerOutline: {
query: tg.isUndefined,
answer: tg.isUndefined,
query: z.undefined(),
answer: z.undefined(),
},
getPlayerPosition: {
query: tg.isUndefined,
query: z.undefined(),
answer: isPlayerPosition,
},
movePlayerTo: {
@ -190,14 +263,13 @@ export const iframeQueryMapTypeGuards = {
},
};
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
type UnknownToVoid<T> = undefined extends T ? void : T;
export type IframeQueryMap = {
[key in keyof IframeQueryMapTypeGuardsType]: {
query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>;
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>;
query: z.infer<typeof iframeQueryMapTypeGuards[key]["query"]>;
answer: UnknownToVoid<z.infer<typeof iframeQueryMapTypeGuards[key]["answer"]>>;
};
};
@ -225,11 +297,18 @@ export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQuer
return false;
}
const result = iframeQueryMapTypeGuards[type].query(event.data);
if (!result) {
try {
iframeQueryMapTypeGuards[type].query.parse(event.data);
} catch (err) {
if (err instanceof z.ZodError) {
console.error(err.issues);
}
console.warn('Received a query with type "' + type + '" but the payload is invalid.');
return false;
}
return result;
return true;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isLayerEvent = z.object({
name: z.string(),
});
export const isLayerEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to show/hide a layer.
*/
export type LayerEvent = tg.GuardedType<typeof isLayerEvent>;
export type LayerEvent = z.infer<typeof isLayerEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isLoadPageEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
})
.get();
export const isLoadPageEvent = z.object({
url: z.string(),
});
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>;
export type LoadPageEvent = z.infer<typeof isLoadPageEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isLoadSoundEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
})
.get();
export const isLoadSoundEvent = z.object({
url: z.string(),
});
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadSoundEvent = tg.GuardedType<typeof isLoadSoundEvent>;
export type LoadSoundEvent = z.infer<typeof isLoadSoundEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isLoadTilesetEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
})
.get();
export const isLoadTilesetEvent = z.object({
url: z.string(),
});
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadTilesetEvent = tg.GuardedType<typeof isLoadTilesetEvent>;
export type LoadTilesetEvent = z.infer<typeof isLoadTilesetEvent>;

View File

@ -1,12 +1,10 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isMapDataEvent = new tg.IsInterface()
.withProperties({
data: tg.isObject,
})
.get();
export const isMapDataEvent = z.object({
data: z.unknown(), // Todo : Typing
});
/**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
*/
export type MapDataEvent = tg.GuardedType<typeof isMapDataEvent>;
export type MapDataEvent = z.infer<typeof isMapDataEvent>;

View File

@ -1,11 +1,9 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isMovePlayerToEventConfig = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
speed: tg.isOptional(tg.isNumber),
})
.get();
export const isMovePlayerToEventConfig = z.object({
x: z.number(),
y: z.number(),
speed: z.optional(z.number()),
});
export type MovePlayerToEvent = tg.GuardedType<typeof isMovePlayerToEventConfig>;
export type MovePlayerToEvent = z.infer<typeof isMovePlayerToEventConfig>;

View File

@ -1,11 +1,9 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isMovePlayerToEventAnswer = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
cancelled: tg.isBoolean,
})
.get();
export const isMovePlayerToEventAnswer = z.object({
x: z.number(),
y: z.number(),
cancelled: z.boolean(),
});
export type MovePlayerToEventAnswer = tg.GuardedType<typeof isMovePlayerToEventAnswer>;
export type ActionsMenuActionClickedEvent = z.infer<typeof isMovePlayerToEventAnswer>;

View File

@ -1,24 +1,20 @@
import * as tg from "generic-type-guard";
import { z } from "zod";
export const isOpenCoWebsiteEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
allowApi: tg.isOptional(tg.isBoolean),
allowPolicy: tg.isOptional(tg.isString),
widthPercent: tg.isOptional(tg.isNumber),
position: tg.isOptional(tg.isNumber),
closable: tg.isOptional(tg.isBoolean),
lazy: tg.isOptional(tg.isBoolean),
})
.get();
export const isOpenCoWebsiteEvent = z.object({
url: z.string(),
allowApi: z.optional(z.boolean()),
allowPolicy: z.optional(z.string()),
widthPercent: z.optional(z.number()),
position: z.optional(z.number()),
closable: z.optional(z.boolean()),
lazy: z.optional(z.boolean()),
});
export const isCoWebsite = new tg.IsInterface()
.withProperties({
id: tg.isString,
})
.get();
export const isCoWebsite = z.object({
id: z.string(),
});
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenCoWebsiteEvent = tg.GuardedType<typeof isOpenCoWebsiteEvent>;
export type OpenCoWebsiteEvent = z.infer<typeof isOpenCoWebsiteEvent>;

Some files were not shown because too many files have changed in this diff Show More