This commit is contained in:
syuilo
2026-07-30 21:30:20 +09:00
parent c35a6c01ff
commit c0ff062ccb
14 changed files with 106 additions and 218 deletions
+6 -6
View File
@@ -156,7 +156,7 @@ import { QueueService } from './QueueService.js';
import { LoggerService } from './LoggerService.js';
import { WorldRoomService } from './WorldRoomService.js';
import { WorldRoomEntityService } from './entities/WorldRoomEntityService.js';
import { WorldRoomMultiplayService } from './WorldRoomMultiplayService.js';
import { WorldMultiplayService } from './WorldMultiplayService.js';
import { WorldAvatarService } from './WorldAvatarService.js';
import { WorldAvatarEntityService } from './entities/WorldAvatarEntityService.js';
import { TelemetryService } from './telemetry/TelemetryService.js';
@@ -237,7 +237,7 @@ const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisti
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
const $PageService: Provider = { provide: 'PageService', useExisting: PageService };
const $WorldRoomService: Provider = { provide: 'WorldRoomService', useExisting: WorldRoomService };
const $WorldRoomMultiplayService: Provider = { provide: 'WorldRoomMultiplayService', useExisting: WorldRoomMultiplayService };
const $WorldMultiplayService: Provider = { provide: 'WorldMultiplayService', useExisting: WorldMultiplayService };
const $WorldAvatarService: Provider = { provide: 'WorldAvatarService', useExisting: WorldAvatarService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
@@ -395,7 +395,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ReversiService,
PageService,
WorldRoomService,
WorldRoomMultiplayService,
WorldMultiplayService,
WorldAvatarService,
ChartLoggerService,
@@ -551,7 +551,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ReversiService,
$PageService,
$WorldRoomService,
$WorldRoomMultiplayService,
$WorldMultiplayService,
$WorldAvatarService,
$ChartLoggerService,
@@ -707,7 +707,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ReversiService,
PageService,
WorldRoomService,
WorldRoomMultiplayService,
WorldMultiplayService,
WorldAvatarService,
FederationChart,
@@ -861,7 +861,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ReversiService,
$PageService,
$WorldRoomService,
$WorldRoomMultiplayService,
$WorldMultiplayService,
$WorldAvatarService,
$FederationChart,
@@ -173,7 +173,7 @@ export interface ChatEventTypes {
};
}
export interface WorldRoomEventTypes {
export interface WorldEventTypes {
enter: {
user: Packed<'UserLite'>;
avatar: Packed<'WorldAvatarLite'>['def'] | null;
@@ -325,9 +325,9 @@ export type GlobalEvents = {
name: `chatRoomStream:${MiChatRoom['id']}`;
payload: EventTypesToEventPayload<ChatEventTypes>;
};
worldRoom: {
name: `worldRoomStream:${string}`;
payload: EventTypesToEventPayload<WorldRoomEventTypes>;
world: {
name: `worldStream:${string}`;
payload: EventTypesToEventPayload<WorldEventTypes>;
};
reversi: {
name: `reversiStream:${MiUser['id']}`;
@@ -451,7 +451,7 @@ export class GlobalEventService {
}
@bindThis
public publishWorldRoomStream<K extends keyof WorldRoomEventTypes>(roomId: string, type: K, value?: WorldRoomEventTypes[K]): void {
this.publish(`worldRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
public publishWorldStream<K extends keyof WorldEventTypes>(spaceKey: string, type: K, value?: WorldEventTypes[K]): void {
this.publish(`worldStream:${spaceKey}`, type, typeof value === 'undefined' ? null : value);
}
}
@@ -7,10 +7,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, Not } from 'typeorm';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import {
MiWorldRoom,
} from '@/models/_.js';
import type { MiWorldAvatar, WorldRoomsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
@@ -30,7 +26,7 @@ type PlayerState = {
};
@Injectable()
export class WorldRoomMultiplayService {
export class WorldMultiplayService {
constructor(
@Inject(DI.db)
private db: DataSource,
@@ -38,9 +34,6 @@ export class WorldRoomMultiplayService {
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.worldRoomsRepository)
private worldRoomsRepository: WorldRoomsRepository,
private roleService: RoleService,
private queryService: QueryService,
private idService: IdService,
@@ -52,74 +45,72 @@ export class WorldRoomMultiplayService {
}
@bindThis
public async enter(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
console.log('enter', { userId, roomId });
public async enter(userId: MiUser['id'], spaceKey: string) {
console.log('enter', { userId, spaceKey: spaceKey });
// TODO: atomicにやる
const currentPlayers = await this.redisClient.hlen(`worldRoom:${roomId}:players`);
if (currentPlayers < 10) {
const currentPlayers = await this.redisClient.hlen(`world:${spaceKey}:players`);
if (currentPlayers < 50) {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.hset(`worldRoom:${roomId}:players`, userId, 1);
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
redisPipeline.hset(`world:${spaceKey}:players`, userId, 1);
redisPipeline.hexpire(`world:${spaceKey}:players`, 30, 'FIELDS', 1, userId);
await redisPipeline.exec();
} else {
throw new Error('Room is full.');
throw new Error('The group is full.');
}
// TODO: 既に入っていたらスキップ
const avatar = await this.worldAvatarService.getActiveAvatarOfUser(userId);
this.globalEventService.publishWorldRoomStream(roomId, 'enter', {
this.globalEventService.publishWorldStream(spaceKey, 'enter', {
user: await this.userEntityService.pack(userId),
avatar: avatar?.def,
});
}
@bindThis
public async heartbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
const exists = await this.redisClient.hexists(`worldRoom:${roomId}:players`, userId);
public async heartbeat(userId: MiUser['id'], spaceKey: string) {
const exists = await this.redisClient.hexists(`world:${spaceKey}:players`, userId);
if (exists) {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
redisPipeline.hexpire(`world:${spaceKey}:players`, 30, 'FIELDS', 1, userId);
redisPipeline.hexpire(`world:${spaceKey}:playerStates`, 30, 'FIELDS', 1, userId);
await redisPipeline.exec();
} else {
throw new Error('Not in room.');
throw new Error('Not in the group.');
}
}
@bindThis
public async left(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
console.log('left', { userId, roomId });
public async left(userId: MiUser['id'], spaceKey: string) {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.hdel(`worldRoom:${roomId}:players`, userId);
redisPipeline.hdel(`worldRoom:${roomId}:playerStates`, userId);
redisPipeline.hdel(`world:${spaceKey}:players`, userId);
redisPipeline.hdel(`world:${spaceKey}:playerStates`, userId);
await redisPipeline.exec();
this.globalEventService.publishWorldRoomStream(roomId, 'left', {
this.globalEventService.publishWorldStream(spaceKey, 'left', {
userId,
});
}
@bindThis
public async updatePlayerState(userId: MiUser['id'], roomId: MiWorldRoom['id'], state: PlayerState) {
public async updatePlayerState(userId: MiUser['id'], spaceKey: string, state: PlayerState) {
const redisPipeline = this.redisClient.pipeline();
redisPipeline.hset(`worldRoom:${roomId}:playerStates`, userId, JSON.stringify(state));
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
redisPipeline.hset(`world:${spaceKey}:playerStates`, userId, JSON.stringify(state));
redisPipeline.hexpire(`world:${spaceKey}:playerStates`, 30, 'FIELDS', 1, userId);
await redisPipeline.exec();
}
@bindThis
public async getPlayerStates(roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
const entries = await this.redisClient.hgetall(`worldRoom:${roomId}:playerStates`);
public async getPlayerStates(spaceKey: string): Promise<Record<string, PlayerState>> {
const entries = await this.redisClient.hgetall(`world:${spaceKey}:playerStates`);
return Object.fromEntries(Object.entries(entries).map(([userId, state]) => [userId, JSON.parse(state) as PlayerState]));
}
@bindThis
public getPlayerStatesAndHeatbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
public getPlayerStatesAndHeatbeat(userId: MiUser['id'], spaceKey: string): Promise<Record<string, PlayerState>> {
// TODO: atomicにやる
this.heartbeat(userId, roomId);
return this.getPlayerStates(roomId);
this.heartbeat(userId, spaceKey);
return this.getPlayerStates(spaceKey);
}
@bindThis
@@ -135,8 +126,8 @@ export class WorldRoomMultiplayService {
}
@bindThis
public async getPlayerProfiles(roomId: MiWorldRoom['id'], userId?: MiUser['id']): Promise<Record<string, any>> {
let playerIds = await this.redisClient.hkeys(`worldRoom:${roomId}:players`);
public async getPlayerProfiles(spaceKey: string, userId?: MiUser['id']): Promise<Record<string, any>> {
let playerIds = await this.redisClient.hkeys(`world:${spaceKey}:players`);
playerIds = playerIds.filter(id => id !== userId);
const packedUsers = await this.userEntityService.packMany(playerIds);
+2 -2
View File
@@ -49,7 +49,7 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
import { ReversiChannel } from './api/stream/channels/reversi.js';
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
import { WorldRoomChannel } from './api/stream/channels/world-room.js';
import { WorldChannel } from './api/stream/channels/world.js';
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@@ -100,7 +100,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
QueueStatsChannel,
ServerStatsChannel,
UserListChannel,
WorldRoomChannel,
WorldChannel,
NoteStreamingHidingService,
OpenApiServerService,
OAuth2ProviderService,
@@ -35,7 +35,7 @@ import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js';
import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js';
import { ReversiChannel } from '@/server/api/stream/channels/reversi.js';
import { ReversiGameChannel } from '@/server/api/stream/channels/reversi-game.js';
import { WorldRoomChannel } from '@/server/api/stream/channels/world-room.js';
import { WorldChannel } from '@/server/api/stream/channels/world.js';
import type { ChannelRequest } from './channel.js';
import type { ChannelConstructor } from './channel.js';
import type Channel from './channel.js';
@@ -339,7 +339,7 @@ export default class Connection {
case 'chatRoom': return ChatRoomChannel;
case 'reversi': return ReversiChannel;
case 'reversiGame': return ReversiGameChannel;
case 'worldRoom': return WorldRoomChannel;
case 'worldRoom': return WorldChannel;
default:
throw new Error(`no such channel: ${name}`);
@@ -10,16 +10,17 @@ import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import { WorldRoomService } from '@/core/WorldRoomService.js';
import { WorldRoomMultiplayService } from '@/core/WorldRoomMultiplayService.js';
import { WorldMultiplayService } from '@/core/WorldMultiplayService.js';
import Channel, { type ChannelRequest } from '../channel.js';
@Injectable({ scope: Scope.TRANSIENT })
export class WorldRoomChannel extends Channel {
public readonly chName = 'worldRoom';
export class WorldChannel extends Channel {
public readonly chName = 'world';
public static shouldShare = false;
public static requireCredential = true as const;
public static kind = 'read:worldRoom';
public static kind = 'read:world';
private roomId: string;
private spaceKey: string;
private intervalId: NodeJS.Timeout;
private isEntered = false;
@@ -28,20 +29,17 @@ export class WorldRoomChannel extends Channel {
request: ChannelRequest,
private worldRoomService: WorldRoomService,
private worldRoomMultiplayService: WorldRoomMultiplayService,
private worldMultiplayService: WorldMultiplayService,
) {
super(request);
}
@bindThis
public async init(params: JsonObject): Promise<boolean> {
if (typeof params.roomId !== 'string') return false;
if (typeof params.spaceKey !== 'string') return false;
if (!this.user) return false;
this.roomId = params.roomId;
const room = await this.worldRoomService.findRoomById(this.roomId);
if (room == null) return false;
this.spaceKey = params.spaceKey;
try {
await this.enter();
@@ -49,7 +47,7 @@ export class WorldRoomChannel extends Channel {
return false;
}
this.subscriber.on(`worldRoomStream:${this.roomId}`, this.onEvent);
this.subscriber.on(`worldStream:${this.spaceKey}`, this.onEvent);
return true;
}
@@ -58,29 +56,29 @@ export class WorldRoomChannel extends Channel {
private async enter() {
if (this.isEntered) return;
await this.worldRoomMultiplayService.enter(this.user!.id, this.roomId);
await this.worldMultiplayService.enter(this.user!.id, this.spaceKey);
this.isEntered = true;
this.send('entered', {
playerProfiles: await this.worldRoomMultiplayService.getPlayerProfiles(this.roomId, this.user!.id),
playerProfiles: await this.worldMultiplayService.getPlayerProfiles(this.spaceKey, this.user!.id),
});
this.intervalId = setInterval(async () => {
const states = await this.worldRoomMultiplayService.getPlayerStatesAndHeatbeat(this.user!.id, this.roomId);
const states = await this.worldMultiplayService.getPlayerStatesAndHeatbeat(this.user!.id, this.spaceKey);
delete states[this.user!.id];
this.send('sync', states);
}, 100);
}
@bindThis
private async onEvent(data: GlobalEvents['worldRoom']['payload']) {
private async onEvent(data: GlobalEvents['world']['payload']) {
switch (data.type) {
case 'enter': {
if (data.body.user.id === this.user!.id) return; // 自分の入室は無視
this.send('playerEntered', {
id: data.body.user.id,
profile: this.worldRoomMultiplayService.packPlayerProfile(data.body.user, data.body.avatar),
profile: this.worldMultiplayService.packPlayerProfile(data.body.user, data.body.avatar),
});
break;
}
@@ -98,8 +96,8 @@ export class WorldRoomChannel extends Channel {
public onMessage(type: string, body: any) {
switch (type) {
case 'update':
if (this.roomId && this.isEntered) {
this.worldRoomMultiplayService.updatePlayerState(this.user!.id, this.roomId, body);
if (this.spaceKey != null && this.isEntered) {
this.worldMultiplayService.updatePlayerState(this.user!.id, this.spaceKey, body);
}
break;
}
@@ -107,9 +105,9 @@ export class WorldRoomChannel extends Channel {
@bindThis
public dispose() {
this.subscriber.off(`worldRoomStream:${this.roomId}`, this.onEvent);
this.subscriber.off(`worldStream:${this.spaceKey}`, this.onEvent);
clearInterval(this.intervalId);
this.worldRoomMultiplayService.left(this.user!.id, this.roomId);
this.worldMultiplayService.left(this.user!.id, this.spaceKey);
}
}
@@ -7,6 +7,8 @@ import * as BABYLON from '@babylonjs/core/pure.js';
import EventEmitter from 'eventemitter3';
import { PlayerContainer, type PlayerProfile, type PlayerState } from './PlayerContainer.js';
// TODO: multiplayer関連は全ての子クラスで必要とは限らない(preview用など)ため、このクラスを継承する別のabstract classに分離
const IN_WEB_WORKER = typeof window === 'undefined';
export type EngineBaseEvents = {
@@ -189,7 +189,7 @@ import { prefer } from '@/preferences.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { miLocalStorage } from '@/local-storage.js';
import { FURNITURE_UI_DEFS } from '@/world/room/furniture-ui-defs.js';
import { RoomMultiplayer } from '@/world/room/multiplayer.js';
import { Multiplayer } from '@/world/multiplayer.js';
import { $i } from '@/i.js';
import { userPage } from '@/filters/user.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -340,7 +340,7 @@ const roomControllerOptions = computed<RoomControllerOptions>(() => ({
}));
const controller = markRaw(new RoomController(deepClone(initialRoomState), roomControllerOptions.value));
const multiplayer = markRaw(new RoomMultiplayer(props.room.id, controller));
const multiplayer = markRaw(new Multiplayer(`room:${props.room.id}`, controller));
watch(controller.roomState, () => {
controller.roomState.value.worldScale = WORLD_SCALE;
+2 -2
View File
@@ -85,7 +85,7 @@ import { prefer } from '@/preferences.js';
import { isTouchUsing } from '@/utility/touch.js';
import { deviceKind } from '@/utility/device-kind.js';
import MkProgressBar from '@/components/MkProgressBar.vue';
import { WorldMultiplayer } from '@/world/multiplayer.js';
import { Multiplayer } from '@/world/multiplayer.js';
import { $i } from '@/i.js';
import { userPage } from '@/filters/user.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -132,7 +132,7 @@ const worldControllerOptions = computed<WorldEngineControllerOptions>(() => ({
}));
const controller = markRaw(new WorldEngineController(worldControllerOptions.value));
const multiplayer = markRaw(new WorldMultiplayer('0', controller));
const multiplayer = markRaw(new Multiplayer('world:main:0', controller));
const pointedPlayerInfo = ref<PlayerProfile | null>(null);
const isMenuShowing = ref(!isNarrow);
-16
View File
@@ -70,20 +70,4 @@ export class WorldEngineController extends EngineControllerBase<WorldEngine> {
public setCameraJoystickMoveVector(vec: { x: number; y: number }) {
this.call('cameraJoystickMove', [vec]);
}
public updatePlayerProfiles(profiles: Record<string, PlayerProfile>) {
this.call('updatePlayerProfiles', [profiles]);
}
public updatePlayerStates(states: Record<string, PlayerState>) {
this.call('updatePlayerStates', [states]);
}
public clearPlayers() {
this.call('clearPlayers');
}
public updateAvatarDisplayOptions(options: { showUsername: boolean; show2dAvatar: boolean }) {
this.call('updateAvatarDisplayOptions', [options]);
}
}
@@ -6,9 +6,12 @@
import { reactive, ref, shallowRef, triggerRef, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import type { EngineBase, EngineBaseEvents } from 'frontend-misskey-world-engine/src/EngineBase.js';
import type { PlayerProfile, PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
// TODO: multiplayer関連は全ての子クラスで必要とは限らない(preview用など)ため、このクラスを継承する別のabstract classに分離
export type EngineControllerBaseOptions = {
workerMode?: boolean;
graphicsQuality: number;
@@ -414,6 +417,34 @@ export abstract class EngineControllerBase<T extends EngineBase<EngineBaseEvents
this.call('resumeRender');
}
public sit() {
this.call('sit');
}
public lyingDown() {
this.call('lyingDown');
}
public standUp() {
this.call('standUp');
}
public updatePlayerProfiles(profiles: Record<string, PlayerProfile>) {
this.call('updatePlayerProfiles', [profiles]);
}
public updatePlayerStates(states: Record<string, PlayerState>) {
this.call('updatePlayerStates', [states]);
}
public clearPlayers() {
this.call('clearPlayers');
}
public updateAvatarDisplayOptions(options: { showUsername: boolean; show2dAvatar: boolean }) {
this.call('updateAvatarDisplayOptions', [options]);
}
public takeScreenshot() {
return this.callAndWaitReturn('takeScreenshot');
}
+7 -7
View File
@@ -7,21 +7,21 @@ import { reactive, ref, shallowRef, triggerRef, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { PlayerProfile, PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
import type { WorldEngineController } from './controller.js';
import type { EngineControllerBase } from './EngineControllerBase.js';
import { useStream } from '@/stream.js';
import * as os from '@/os.js';
import { withTimeout } from '@/utility/promise-timeout.js';
import { deepEqual } from '@/utility/deep-equal.js';
export class WorldMultiplayer {
export class Multiplayer {
public isOnline = ref(false);
private controller: WorldEngineController;
private controller: EngineControllerBase<any>;
private connection: Misskey.IChannelConnection<Misskey.Channels['world']> | null = null;
private dimensionId: string;
private spaceKey: string;
public playerProfiles: Record<string, PlayerProfile> = {};
constructor(dimensionId: string, controller: WorldEngineController) {
this.dimensionId = dimensionId;
constructor(spaceKey: string, controller: EngineControllerBase<any>) {
this.spaceKey = spaceKey;
this.controller = controller;
this.onSync = this.onSync.bind(this);
@@ -32,7 +32,7 @@ export class WorldMultiplayer {
public enter() {
const p = new Promise<void>((resolve, reject) => {
this.connection = useStream().useChannel('world', {
roomId: this.dimensionId,
spaceKey: this.spaceKey,
});
this.connection.once('entered', ({ playerProfiles }) => {
console.log('entered', playerProfiles);
@@ -223,32 +223,4 @@ export class RoomController extends EngineControllerBase<RoomEngine, {
public interact(id: string) {
this.call('interact', [this.selected.value!.furnitureId, id]);
}
public sit() {
this.call('sit');
}
public lyingDown() {
this.call('lyingDown');
}
public standUp() {
this.call('standUp');
}
public updatePlayerProfiles(profiles: Record<string, PlayerProfile>) {
this.call('updatePlayerProfiles', [profiles]);
}
public updatePlayerStates(states: Record<string, PlayerState>) {
this.call('updatePlayerStates', [states]);
}
public clearPlayers() {
this.call('clearPlayers');
}
public updateAvatarDisplayOptions(options: { showUsername: boolean; show2dAvatar: boolean }) {
this.call('updateAvatarDisplayOptions', [options]);
}
}
@@ -1,90 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { reactive, ref, shallowRef, triggerRef, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { PlayerProfile, PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
import type { RoomController } from './controller.js';
import { useStream } from '@/stream.js';
import * as os from '@/os.js';
import { withTimeout } from '@/utility/promise-timeout.js';
import { deepEqual } from '@/utility/deep-equal.js';
export class RoomMultiplayer {
public isOnline = ref(false);
private controller: RoomController;
private connection: Misskey.IChannelConnection<Misskey.Channels['worldRoom']> | null = null;
private roomId: string;
public playerProfiles: Record<string, PlayerProfile> = {};
constructor(roomId: string, controller: RoomController) {
this.roomId = roomId;
this.controller = controller;
this.onSync = this.onSync.bind(this);
this.onPlayerEntered = this.onPlayerEntered.bind(this);
this.onPlayerLeft = this.onPlayerLeft.bind(this);
}
public enter() {
const p = new Promise<void>((resolve, reject) => {
this.connection = useStream().useChannel('worldRoom', {
roomId: this.roomId,
});
this.connection.once('entered', ({ playerProfiles }) => {
console.log('entered', playerProfiles);
this.playerProfiles = playerProfiles;
this.controller.updatePlayerProfiles(this.playerProfiles);
this.connection!.on('sync', this.onSync);
this.connection!.on('playerEntered', this.onPlayerEntered);
this.connection!.on('playerLeft', this.onPlayerLeft);
this.isOnline.value = true;
resolve();
});
});
return withTimeout(p, 5000).catch((err) => {
this.connection?.dispose();
this.connection = null;
throw err;
});
}
public left() {
if (this.connection == null) return;
this.connection.dispose();
this.connection = null;
this.isOnline.value = false;
}
private prevState: PlayerState | null = null;
public updateState(state: PlayerState) {
if (this.connection == null || !this.isOnline.value) return;
if (this.prevState != null && deepEqual(this.prevState, state)) return;
this.connection.send('update', state);
this.prevState = state;
}
private onSync(states: Record<string, PlayerState>) {
this.controller.updatePlayerStates(states);
}
private onPlayerEntered(data: { id: string; profile: PlayerProfile; }) {
this.playerProfiles[data.id] = data.profile;
this.controller.updatePlayerProfiles(this.playerProfiles);
}
private onPlayerLeft(data: { id: string; }) {
delete this.playerProfiles[data.id];
this.controller.updatePlayerProfiles(this.playerProfiles);
}
public dispose() {
this.left();
}
}