Skip to content

Commit

Permalink
Only store beatmap in serialized state in multiplayer server
Browse files Browse the repository at this point in the history
  • Loading branch information
Marvin Schürz committed Jan 22, 2025
1 parent 3d1e47c commit e8bbc22
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 47 deletions.
44 changes: 44 additions & 0 deletions packages/core/src/editor/multiplayer/BoxedBeatmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { ISummary, MutationContext } from '@osucad/multiplayer';
import type { Beatmap } from '../../beatmap/Beatmap';
import { SharedStructure } from '@osucad/multiplayer';
import { RulesetStore } from '../../rulesets/RulesetStore';

export interface BoxedBeatmapSummary extends ISummary {
ruleset: string;
beatmap: any;
}

export class BoxedBeatmap extends SharedStructure<never, BoxedBeatmapSummary> {
constructor(
public beatmap?: Beatmap<any>,
) {
super();
}

override handle(mutation: never, ctx: MutationContext): void | null {
}

get rulesetId() {
return this.beatmap!.beatmapInfo.ruleset.shortName;
}

override createSummary(): BoxedBeatmapSummary {
return {
id: this.id,
ruleset: this.rulesetId,
beatmap: this.beatmap!.createSummary(),
};
}

override initializeFromSummary(summary: BoxedBeatmapSummary): void {
const rulesetInfo = RulesetStore.getByShortName(summary.ruleset);
if (!rulesetInfo || !rulesetInfo.available)
throw new Error(`Ruleset "${summary.ruleset}" is not supported`);

const ruleset = rulesetInfo.createInstance();
const beatmap = this.beatmap = ruleset.createBeatmap();

beatmap.beatmapInfo.ruleset = rulesetInfo;
beatmap.initializeFromSummary(summary.beatmap);
}
}
25 changes: 17 additions & 8 deletions packages/core/src/editor/multiplayer/MultiplayerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { MutationSource, UpdateHandler } from '@osucad/multiplayer';
import { io } from 'socket.io-client';
import { SimpleFile } from '../../beatmap/io/SimpleFile';
import { StaticFileStore } from '../../beatmap/io/StaticFileStore';
import { RulesetStore } from '../../rulesets/RulesetStore';
import { BoxedBeatmap } from './BoxedBeatmap';
import { ConnectedUsers } from './ConnectedUsers';

export class MultiplayerClient extends Component {
Expand Down Expand Up @@ -50,19 +50,28 @@ export class MultiplayerClient extends Component {

this.clientId = initialState.clientId;

const rulesetInfo = RulesetStore.getByShortName(initialState.beatmap.ruleset);
if (!rulesetInfo || !rulesetInfo.available)
throw new Error(`Ruleset "${initialState.beatmap.ruleset}" not available`);
const beatmap = new BoxedBeatmap();

const ruleset = rulesetInfo.createInstance();
beatmap.initializeFromSummary(initialState.document.summary);

this.beatmap = ruleset.createBeatmap();
this.beatmap.beatmapInfo.ruleset = rulesetInfo;
this.beatmap.initializeFromSummary(initialState.beatmap.data);
if (!beatmap.beatmap)
throw new Error('Beatmap failed to initialize');

this.beatmap = beatmap.beatmap;

this.updateHandler = new UpdateHandler(this.beatmap);
this.updateHandler.attach(this.beatmap);

for (const op of initialState.document.ops) {
const ctx: MutationContext = {
source: MutationSource.Remote,
version: op.version,
};

for (const mutation of op.mutations)
this.updateHandler.apply(mutation, ctx);
}

const assets = initialState.assets.map(it => new SimpleFile(
it.path,
() => fetch(`/api/assets/${it.id}`).then(it => it.arrayBuffer()),
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/editor/multiplayer/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './BoxedBeatmap';
export * from './MultiplayerClient';
export * from './MultiplayerEditor';
export * from './MultiplayerEditorBeatmap';
4 changes: 1 addition & 3 deletions packages/multiplayer/src/dataStructures/SharedObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ export abstract class SharedObject extends SharedStructure<ObjectMutation, Objec
}

handle(command: ObjectMutation, ctx: MutationContext): ObjectMutation | null {
const undoMutation: ObjectMutation = {
data: {},
};
const undoMutation: ObjectMutation = {};

for (const key in command) {
const property = this.#properties.get(key);
Expand Down
7 changes: 4 additions & 3 deletions packages/multiplayer/src/protocol/ServerMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export interface ServerMessages {

export interface InitialStateServerMessage {
clientId: number;
beatmap: {
ruleset: string;
data: unknown;
document: {
summary: any;
ops: MutationsSubmittedMessage[];
};
assets: AssetInfo[];
connectedUsers: ClientInfo[];
Expand All @@ -53,4 +53,5 @@ export interface MutationsSubmittedMessage {
version: number;
clientId: number;
mutations: IMutation[];
sequenceNumber: number;
}
18 changes: 10 additions & 8 deletions packages/server/src/Gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import type { OsuBeatmap } from '@osucad/ruleset-osu';
import type { Server } from 'socket.io';
import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import { RulesetStore, StableBeatmapParser } from '@osucad/core';
import { BoxedBeatmap, RulesetStore, StableBeatmapParser } from '@osucad/core';
import { OsuRuleset } from '@osucad/ruleset-osu';
import { getAssets } from './assets';
import { Room } from './multiplayer/Room';
import { OrderingService } from './services/OrderingService';

const beatmapFilePath = './beatmap/Suzukaze Aoba (CV. Yuki Takada) - Rainbow Days!! (Maarvin) [Expert].osu';

Expand All @@ -18,18 +19,19 @@ export class Gateway {
async init() {
RulesetStore.register(new OsuRuleset().rulesetInfo);

const ruleset = new OsuRuleset();

const conversionBeatmap = new StableBeatmapParser().parse(await fs.readFile(beatmapFilePath, 'utf-8'));

const beatmap = new OsuRuleset().createBeatmapConverter(conversionBeatmap as any).convert() as OsuBeatmap;
const beatmap = ruleset.createBeatmapConverter(conversionBeatmap as any).convert() as OsuBeatmap;

const roomId = randomUUID();

this.room = new Room(
roomId,
this.io.to(roomId),
beatmap,
getAssets(),
);
const orderingService = new OrderingService(new BoxedBeatmap(beatmap).createSummary());

const broadcast = this.io.to(roomId);

this.room = new Room(roomId, broadcast, orderingService, getAssets());

this.io.on('connect', socket => this.room.accept(socket));
}
Expand Down
36 changes: 11 additions & 25 deletions packages/server/src/multiplayer/Room.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Beatmap } from '@osucad/core';
import type { AssetInfo, ClientMessages, MutationContext, ServerMessages, SignalKey, SubmitMutationsMessage, UserPresence } from '@osucad/multiplayer';
import type { AssetInfo, ClientMessages, ServerMessages, SignalKey, SubmitMutationsMessage, UserPresence } from '@osucad/multiplayer';
import type { BroadcastOperator, Socket } from 'socket.io';
import { MutationSource, UpdateHandler } from '@osucad/multiplayer';
import type { OrderingService } from '../services/OrderingService';
import { Json } from '@osucad/serialization';
import { nextClientId } from './clientId';
import { RoomUser } from './RoomUser';
Expand All @@ -10,15 +9,11 @@ export class Room {
constructor(
private readonly id: string,
private readonly broadcast: BroadcastOperator<ServerMessages, ClientMessages>,
private readonly beatmap: Beatmap<any>,
private readonly orderingService: OrderingService,
private readonly assets: AssetInfo[],
) {
this.updateHandler = new UpdateHandler(beatmap);
this.updateHandler.attach(beatmap);
}

private readonly updateHandler: UpdateHandler;

private readonly json = new Json();

private readonly users = new Map<number, RoomUser>();
Expand Down Expand Up @@ -53,11 +48,13 @@ export class Room {
color,
);

const { summary, ops } = this.orderingService.getMessagesSinceLastSummary();

socket.emit('initialData', {
clientId,
beatmap: {
ruleset: this.beatmap.beatmapInfo.ruleset.shortName,
data: this.beatmap.createSummary(),
document: {
summary: summary.summary,
ops,
},
assets: this.assets,
connectedUsers: [...this.users.values()].map(it => it.getInfo()),
Expand Down Expand Up @@ -99,19 +96,8 @@ export class Room {
}

private handleSubmitMutations(user: RoomUser, message: SubmitMutationsMessage) {
const ctx: MutationContext = {
// pretending that all mutations are local on server side so it doesn't try to do any conflict resolution
source: MutationSource.Local,
version: message.version,
};

for (const mutation of message.mutations)
this.updateHandler.apply(mutation, ctx);

this.broadcast.emit('mutationsSubmitted', {
version: message.version,
clientId: user.clientId,
mutations: message.mutations,
});
const sequencedMessage = this.orderingService.appendOps(user.clientId, message);

this.broadcast.emit('mutationsSubmitted', sequencedMessage);
}
}
54 changes: 54 additions & 0 deletions packages/server/src/services/OrderingService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { MutationsSubmittedMessage, SubmitMutationsMessage } from '@osucad/multiplayer';

export class OrderingService {
constructor(initialSummary: any) {
this.#latestSummary = {
clientId: -1,
sequenceNumber: 0,
summary: initialSummary,
};
}

#latestSummary: {
clientId: number;
sequenceNumber: number;
summary: any;
};

#ops: MutationsSubmittedMessage[] = [];

#sequenceNumber = 0;

get sequenceNumber() {
return this.#sequenceNumber;
}

appendOps(clientId: number, message: SubmitMutationsMessage) {
const sequencedMessage: MutationsSubmittedMessage = {
mutations: message.mutations,
clientId,
version: message.version,
sequenceNumber: ++this.#sequenceNumber,
};

this.#ops.push(sequencedMessage);

return sequencedMessage;
}

appendSummary(clientId: number, summary: any) {
return this.#latestSummary = {
clientId,
summary,
sequenceNumber: ++this.#sequenceNumber,
};
}

getMessagesSinceLastSummary() {
return { summary: this.#latestSummary, ops: [...this.#ops] };
}

get opCount() {
return this.#ops.length;
}
}

0 comments on commit e8bbc22

Please sign in to comment.