mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-08-01 12:56:03 +00:00
wip
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+28
-37
@@ -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);
|
||||
@@ -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}`);
|
||||
|
||||
+18
-20
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user