Implement on enter/leave layer events
This commit is contained in:
parent
0b08d9251e
commit
934e24f837
@ -14,6 +14,7 @@ The list of functions below is **deprecated**. You should not use those but. use
|
||||
- Method `WA.goToRoom` is deprecated. It has been renamed to `WA.nav.goToRoom`.
|
||||
- Method `WA.openCoWebSite` is deprecated. It has been renamed to `WA.nav.openCoWebSite`.
|
||||
- Method `WA.closeCoWebSite` is deprecated. It has been renamed to `WA.nav.closeCoWebSite`.
|
||||
- Method `WA.closeCoWebsite` is deprecated. It has been renamed to `WA.nav.closeCoWebsite`.
|
||||
- Method `WA.openPopup` is deprecated. It has been renamed to `WA.ui.openPopup`.
|
||||
- Method `WA.onChatMessage` is deprecated. It has been renamed to `WA.chat.onChatMessage`.
|
||||
- Method `WA.onEnterZone` is deprecated. It has been renamed to `WA.room.onEnterZone`.
|
||||
|
@ -69,6 +69,8 @@ The event has the following attributes :
|
||||
* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving.
|
||||
* **x (number):** coordinate X of the current player.
|
||||
* **y (number):** coordinate Y of the current player.
|
||||
* **oldX (number):** old coordinate X of the current player.
|
||||
* **oldY (number):** old coordinate Y of the current player.
|
||||
|
||||
**callback:** the function that will be called when the current player is moving. It contains the event.
|
||||
|
||||
|
11
front/src/Api/Events/ChangeLayerEvent.ts
Normal file
11
front/src/Api/Events/ChangeLayerEvent.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
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>;
|
@ -6,6 +6,8 @@ export const isHasPlayerMovedEvent = new tg.IsInterface()
|
||||
moving: tg.isBoolean,
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber,
|
||||
oldX: tg.isOptional(tg.isNumber),
|
||||
oldY: tg.isOptional(tg.isNumber),
|
||||
})
|
||||
.get();
|
||||
|
||||
|
@ -29,6 +29,7 @@ import type {
|
||||
} from "./ui/TriggerActionMessageEvent";
|
||||
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
|
||||
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
|
||||
import type { ChangeLayerEvent } from "./ChangeLayerEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
@ -75,6 +76,8 @@ export interface IframeResponseEventMap {
|
||||
userInputChat: UserInputChatEvent;
|
||||
enterEvent: EnterLeaveEvent;
|
||||
leaveEvent: EnterLeaveEvent;
|
||||
enterLayerEvent: ChangeLayerEvent;
|
||||
leaveLayerEvent: ChangeLayerEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
|
@ -30,6 +30,7 @@ import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
|
||||
import type { SetVariableEvent } from "./Events/SetVariableEvent";
|
||||
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
|
||||
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
|
||||
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
|
||||
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||
query: IframeQueryMap[T]["query"],
|
||||
@ -395,6 +396,24 @@ class IframeListener {
|
||||
});
|
||||
}
|
||||
|
||||
sendEnterLayerEvent(layerName: string) {
|
||||
this.postMessage({
|
||||
type: "enterLayerEvent",
|
||||
data: {
|
||||
name: layerName,
|
||||
} as ChangeLayerEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendLeaveLayerEvent(layerName: string) {
|
||||
this.postMessage({
|
||||
type: "leaveLayerEvent",
|
||||
data: {
|
||||
name: layerName,
|
||||
} as ChangeLayerEvent,
|
||||
});
|
||||
}
|
||||
|
||||
hasPlayerMoved(event: HasPlayerMovedEvent) {
|
||||
if (this.sendPlayerMove) {
|
||||
this.postMessage({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||
import { ChangeLayerEvent, isChangeLayerEvent } from "../Events/ChangeLayerEvent";
|
||||
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
@ -12,6 +13,9 @@ import website from "./website";
|
||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
|
||||
const enterLayerStreams: Map<string, Subject<void>> = new Map<string, Subject<void>>();
|
||||
const leaveLayerStreams: Map<string, Subject<void>> = new Map<string, Subject<void>>();
|
||||
|
||||
interface TileDescriptor {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -47,8 +51,25 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
leaveStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "enterLayerEvent",
|
||||
typeChecker: isChangeLayerEvent,
|
||||
callback: (payloadData: ChangeLayerEvent) => {
|
||||
enterLayerStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "leaveLayerEvent",
|
||||
typeChecker: isChangeLayerEvent,
|
||||
callback: (payloadData) => {
|
||||
leaveLayerStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
* @deprecated Use onEnterLayer instead
|
||||
*/
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
let subject = enterStreams.get(name);
|
||||
if (subject === undefined) {
|
||||
@ -57,6 +78,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use onLeaveLayer instead
|
||||
*/
|
||||
onLeaveZone(name: string, callback: () => void): void {
|
||||
let subject = leaveStreams.get(name);
|
||||
if (subject === undefined) {
|
||||
@ -65,12 +90,35 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
}
|
||||
|
||||
onEnterLayer(layerName: string): Subject<void> {
|
||||
let subject = enterLayerStreams.get(layerName);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<ChangeLayerEvent>();
|
||||
enterLayerStreams.set(layerName, subject);
|
||||
}
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
onLeaveLayer(layerName: string): Subject<void> {
|
||||
let subject = leaveLayerStreams.get(layerName);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<ChangeLayerEvent>();
|
||||
leaveLayerStreams.set(layerName, subject);
|
||||
}
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
showLayer(layerName: string): void {
|
||||
sendToWorkadventure({ type: "showLayer", data: { name: layerName } });
|
||||
}
|
||||
|
||||
hideLayer(layerName: string): void {
|
||||
sendToWorkadventure({ type: "hideLayer", data: { name: layerName } });
|
||||
}
|
||||
|
||||
setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void {
|
||||
sendToWorkadventure({
|
||||
type: "setProperty",
|
||||
@ -81,10 +129,12 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTiledMap(): Promise<ITiledMap> {
|
||||
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
|
||||
return event.data as ITiledMap;
|
||||
}
|
||||
|
||||
setTiles(tiles: TileDescriptor[]) {
|
||||
sendToWorkadventure({
|
||||
type: "setTiles",
|
||||
|
@ -2,6 +2,7 @@ import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiled
|
||||
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
|
||||
export type PropertyChangeCallback = (
|
||||
newValue: string | number | boolean | undefined,
|
||||
@ -9,14 +10,25 @@ export type PropertyChangeCallback = (
|
||||
allProps: Map<string, string | boolean | number>
|
||||
) => void;
|
||||
|
||||
export type layerChangeCallback = (
|
||||
layersChangedByAction: Array<ITiledMapLayer>,
|
||||
allLayersOnNewPosition: Array<ITiledMapLayer>,
|
||||
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* A wrapper around a ITiledMap interface to provide additional capabilities.
|
||||
* It is used to handle layer properties.
|
||||
*/
|
||||
export class GameMap {
|
||||
// oldKey is the index of the previous tile.
|
||||
private oldKey: number | undefined;
|
||||
// key is the index of the current tile.
|
||||
private key: number | undefined;
|
||||
private lastProperties = new Map<string, string | boolean | number>();
|
||||
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||
private propertiesChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||
private enterLayerCallbacks = Array<layerChangeCallback>();
|
||||
private leaveLayerCallbacks = Array<layerChangeCallback>();
|
||||
private tileNameMap = new Map<string, number>();
|
||||
|
||||
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
|
||||
@ -68,22 +80,32 @@ export class GameMap {
|
||||
return [];
|
||||
}
|
||||
|
||||
private getLayersByKey(key: number): Array<ITiledMapLayer> {
|
||||
return this.flatLayers.filter(flatLayer => flatLayer.type === 'tilelayer' && flatLayer.data[key] !== 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of the current player (in pixels)
|
||||
* This will trigger events if properties are changing.
|
||||
*/
|
||||
public setPosition(x: number, y: number) {
|
||||
this.oldKey = this.key;
|
||||
|
||||
const xMap = Math.floor(x / this.map.tilewidth);
|
||||
const yMap = Math.floor(y / this.map.tileheight);
|
||||
const key = xMap + yMap * this.map.width;
|
||||
|
||||
if (key === this.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.key = key;
|
||||
this.triggerAll();
|
||||
|
||||
this.triggerAllProperties();
|
||||
this.triggerLayersChange();
|
||||
}
|
||||
|
||||
private triggerAll(): void {
|
||||
private triggerAllProperties(): void {
|
||||
const newProps = this.getProperties(this.key ?? 0);
|
||||
const oldProps = this.lastProperties;
|
||||
this.lastProperties = newProps;
|
||||
@ -105,6 +127,36 @@ export class GameMap {
|
||||
}
|
||||
}
|
||||
|
||||
private triggerLayersChange() {
|
||||
const layersByOldKey = this.oldKey ? this.getLayersByKey(this.oldKey) : [];
|
||||
const layersByNewKey = this.key ? this.getLayersByKey(this.key) : [];
|
||||
|
||||
const enterLayers = new Set(layersByNewKey);
|
||||
const leaveLayers = new Set(layersByOldKey);
|
||||
|
||||
enterLayers.forEach(layer => {
|
||||
if (leaveLayers.has(layer)) {
|
||||
leaveLayers.delete(layer);
|
||||
enterLayers.delete(layer);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (enterLayers.size > 0) {
|
||||
const layerArray = Array.from(enterLayers);
|
||||
for (const callback of this.enterLayerCallbacks) {
|
||||
callback(layerArray, layersByNewKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (leaveLayers.size > 0) {
|
||||
const layerArray = Array.from(leaveLayers);
|
||||
for (const callback of this.leaveLayerCallbacks) {
|
||||
callback(layerArray, layersByNewKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentProperties(): Map<string, string | boolean | number> {
|
||||
return this.lastProperties;
|
||||
}
|
||||
@ -167,7 +219,7 @@ export class GameMap {
|
||||
newValue: string | number | boolean | undefined,
|
||||
allProps: Map<string, string | boolean | number>
|
||||
) {
|
||||
const callbacksArray = this.callbacks.get(propName);
|
||||
const callbacksArray = this.propertiesChangeCallbacks.get(propName);
|
||||
if (callbacksArray !== undefined) {
|
||||
for (const callback of callbacksArray) {
|
||||
callback(newValue, oldValue, allProps);
|
||||
@ -179,14 +231,28 @@ export class GameMap {
|
||||
* Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on.
|
||||
*/
|
||||
public onPropertyChange(propName: string, callback: PropertyChangeCallback) {
|
||||
let callbacksArray = this.callbacks.get(propName);
|
||||
let callbacksArray = this.propertiesChangeCallbacks.get(propName);
|
||||
if (callbacksArray === undefined) {
|
||||
callbacksArray = new Array<PropertyChangeCallback>();
|
||||
this.callbacks.set(propName, callbacksArray);
|
||||
this.propertiesChangeCallbacks.set(propName, callbacksArray);
|
||||
}
|
||||
callbacksArray.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback called when the user moves inside another layer.
|
||||
*/
|
||||
public onEnterLayer(callback: layerChangeCallback) {
|
||||
this.enterLayerCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback called when the user moves outside another layer.
|
||||
*/
|
||||
public onLeaveLayer(callback: layerChangeCallback) {
|
||||
this.leaveLayerCallbacks.push(callback);
|
||||
}
|
||||
|
||||
public findLayer(layerName: string): ITiledMapLayer | undefined {
|
||||
return this.flatLayers.find((layer) => layer.name === layerName);
|
||||
}
|
||||
@ -284,7 +350,8 @@ export class GameMap {
|
||||
}
|
||||
property.value = propertyValue;
|
||||
|
||||
this.triggerAll();
|
||||
this.triggerAllProperties();
|
||||
this.triggerLayersChange();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -186,6 +186,8 @@ export class GameScene extends DirtyScene {
|
||||
moving: false,
|
||||
x: -1000,
|
||||
y: -1000,
|
||||
oldX: -1000,
|
||||
oldY: -1000,
|
||||
};
|
||||
|
||||
private gameMap!: GameMap;
|
||||
@ -764,6 +766,19 @@ export class GameScene extends DirtyScene {
|
||||
|
||||
//init user position and play trigger to check layers properties
|
||||
this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y);
|
||||
|
||||
// Init layer change listener
|
||||
this.gameMap.onEnterLayer(layers => {
|
||||
layers.forEach(layer => {
|
||||
iframeListener.sendEnterLayerEvent(layer.name);
|
||||
});
|
||||
});
|
||||
|
||||
this.gameMap.onLeaveLayer(layers => {
|
||||
layers.forEach(layer => {
|
||||
iframeListener.sendLeaveLayerEvent(layer.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -895,6 +910,7 @@ export class GameScene extends DirtyScene {
|
||||
audioManagerVisibilityStore.set(!(newValue === undefined));
|
||||
});
|
||||
|
||||
// TODO: Legacy functionnality replace by layer change
|
||||
this.gameMap.onPropertyChange("zone", (newValue, oldValue) => {
|
||||
if (oldValue) {
|
||||
iframeListener.sendLeaveEvent(oldValue as string);
|
||||
@ -1749,7 +1765,11 @@ ${escapedMessage}
|
||||
const playerMovement = new PlayerMovement(
|
||||
{ x: player.x, y: player.y },
|
||||
this.currentTick,
|
||||
message.position,
|
||||
{
|
||||
...message.position,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
},
|
||||
this.currentTick + POSITION_DELAY
|
||||
);
|
||||
this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement);
|
||||
|
@ -38,6 +38,8 @@ export class PlayerMovement {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
oldX: this.startPosition.x,
|
||||
oldY: this.startPosition.y,
|
||||
direction: this.endPosition.direction,
|
||||
moving: true,
|
||||
};
|
||||
|
@ -64,14 +64,14 @@ export class Player extends Character {
|
||||
|
||||
if (x !== 0 || y !== 0) {
|
||||
this.move(x, y);
|
||||
this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y });
|
||||
this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y, oldX: x, oldY: y });
|
||||
} else if (this.wasMoving && moving) {
|
||||
// slow joystick movement
|
||||
this.move(0, 0);
|
||||
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
|
||||
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y, oldX: x, oldY: y });
|
||||
} else if (this.wasMoving && !moving) {
|
||||
this.stop();
|
||||
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
|
||||
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y, oldX: x, oldY: y });
|
||||
}
|
||||
|
||||
if (direction !== null) {
|
||||
|
@ -9,7 +9,9 @@ export interface LayoutManagerAction {
|
||||
userInputManager: UserInputManager | undefined;
|
||||
}
|
||||
|
||||
|
||||
function createLayoutManagerAction() {
|
||||
|
||||
const { subscribe, set, update } = writable<LayoutManagerAction[]>([]);
|
||||
|
||||
return {
|
||||
|
@ -7,7 +7,12 @@ describe("Interpolation / Extrapolation", () => {
|
||||
x: 100, y: 200
|
||||
}, 42000,
|
||||
{
|
||||
x: 200, y: 100, moving: true, direction: "up"
|
||||
x: 200,
|
||||
y: 100,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
moving: true,
|
||||
direction: "up"
|
||||
},
|
||||
42200
|
||||
);
|
||||
@ -19,6 +24,8 @@ describe("Interpolation / Extrapolation", () => {
|
||||
expect(playerMovement.getPosition(42100)).toEqual({
|
||||
x: 150,
|
||||
y: 150,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
direction: 'up',
|
||||
moving: true
|
||||
});
|
||||
@ -26,6 +33,8 @@ describe("Interpolation / Extrapolation", () => {
|
||||
expect(playerMovement.getPosition(42200)).toEqual({
|
||||
x: 200,
|
||||
y: 100,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
direction: 'up',
|
||||
moving: true
|
||||
});
|
||||
@ -33,6 +42,8 @@ describe("Interpolation / Extrapolation", () => {
|
||||
expect(playerMovement.getPosition(42300)).toEqual({
|
||||
x: 250,
|
||||
y: 50,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
direction: 'up',
|
||||
moving: true
|
||||
});
|
||||
@ -43,7 +54,12 @@ describe("Interpolation / Extrapolation", () => {
|
||||
x: 100, y: 200
|
||||
}, 42000,
|
||||
{
|
||||
x: 200, y: 100, moving: false, direction: "up"
|
||||
x: 200,
|
||||
y: 100,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
moving: false,
|
||||
direction: "up"
|
||||
},
|
||||
42200
|
||||
);
|
||||
@ -51,6 +67,8 @@ describe("Interpolation / Extrapolation", () => {
|
||||
expect(playerMovement.getPosition(42300)).toEqual({
|
||||
x: 200,
|
||||
y: 100,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
direction: 'up',
|
||||
moving: false
|
||||
});
|
||||
@ -61,7 +79,12 @@ describe("Interpolation / Extrapolation", () => {
|
||||
x: 100, y: 200
|
||||
}, 42000,
|
||||
{
|
||||
x: 200, y: 100, moving: false, direction: "up"
|
||||
x: 200,
|
||||
y: 100,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
moving: false,
|
||||
direction: "up"
|
||||
},
|
||||
42200
|
||||
);
|
||||
@ -69,6 +92,8 @@ describe("Interpolation / Extrapolation", () => {
|
||||
expect(playerMovement.getPosition(42100)).toEqual({
|
||||
x: 150,
|
||||
y: 150,
|
||||
oldX: undefined,
|
||||
oldY: undefined,
|
||||
direction: 'up',
|
||||
moving: true
|
||||
});
|
||||
|
@ -231,12 +231,12 @@ export class SocketManager implements ZoneEventListener {
|
||||
try {
|
||||
client.viewport = viewport;
|
||||
|
||||
const world = this.rooms.get(client.roomId);
|
||||
if (!world) {
|
||||
const room = this.rooms.get(client.roomId);
|
||||
if (!room) {
|
||||
console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'");
|
||||
return;
|
||||
}
|
||||
world.setViewport(client, client.viewport);
|
||||
room.setViewport(client, client.viewport);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "SET_VIEWPORT" event');
|
||||
console.error(e);
|
||||
|
Loading…
Reference in New Issue
Block a user