From 4fd6aba8d8687c716e4f0ed561cd4e2fd7c5ef79 Mon Sep 17 00:00:00 2001 From: Alex Bates Date: Mon, 28 Sep 2026 22:44:16 +0100 Subject: [PATCH] play songs through mamar-editor's DxMamar --- extensions/diorama64/package.json | 2 +- .../src/browser/ares/ares-connection.ts | 14 -- .../src/browser/bgm-editor/music-preview.ts | 140 +++++------------- yarn.lock | 8 +- 4 files changed, 43 insertions(+), 121 deletions(-) diff --git a/extensions/diorama64/package.json b/extensions/diorama64/package.json index 9f8762a..8e30646 100644 --- a/extensions/diorama64/package.json +++ b/extensions/diorama64/package.json @@ -25,7 +25,7 @@ "@theia/workspace": "1.69.0", "electron-updater": "7.0.0-alpha.7", "js-yaml": "^4.1.0", - "mamar-editor": "^0.1.0" + "mamar-editor": "^0.2.0" }, "devDependencies": { "@types/js-yaml": "^4.0.9", diff --git a/extensions/diorama64/src/browser/ares/ares-connection.ts b/extensions/diorama64/src/browser/ares/ares-connection.ts index aaed6ec..0755b99 100644 --- a/extensions/diorama64/src/browser/ares/ares-connection.ts +++ b/extensions/diorama64/src/browser/ares/ares-connection.ts @@ -79,12 +79,6 @@ export class AresConnection { this.send(message); } - writeU32(address: number, value: number): void { - const data = new Uint8Array(4); - new DataView(data.buffer).setUint32(0, value >>> 0, false); - this.writeMemory(address, data); - } - /** Reads bytes from the emulated system's memory, in memory order. */ readMemory(address: number, size: number): Promise { const id = this.nextReadId++ >>> 0; @@ -105,14 +99,6 @@ export class AresConnection { }); } - async readU32(address: number): Promise { - const data = await this.readMemory(address, 4); - if (data.length < 4) { - throw new Error(`ares couldn't read 0x${address.toString(16)}`); - } - return new DataView(data.buffer, data.byteOffset).getUint32(0, false); - } - /** * Accumulates raw socket bytes (chunked arbitrarily by the OS/TCP, not by * message boundary) and slices out complete messages as they arrive. diff --git a/extensions/diorama64/src/browser/bgm-editor/music-preview.ts b/extensions/diorama64/src/browser/bgm-editor/music-preview.ts index 477f518..4df6556 100644 --- a/extensions/diorama64/src/browser/bgm-editor/music-preview.ts +++ b/extensions/diorama64/src/browser/bgm-editor/music-preview.ts @@ -7,53 +7,24 @@ import { Emitter } from "@theia/core/lib/common"; import { MessageService } from "@theia/core/lib/common/message-service"; import { FileService } from "@theia/filesystem/lib/browser/file-service"; import { WorkspaceService } from "@theia/workspace/lib/browser"; -import type { PlayerStatus, SongPlayer, TrackMute } from "mamar-editor"; +import { + DxMamar, + MAMAR_SYMBOL_NAMES, + type EmulatorMemory, + type MamarSymbols, + type PlayerStatus, + type SongPlayer, + type TrackMute, +} from "mamar-editor"; import { ELF_PATH, PaperMarioDxDebugServer } from "../../common/debug-protocol"; import { AresConnection } from "../ares/ares-connection"; import { AresSession } from "../ares/ares-session"; import { BuildRunner } from "../c-language/build-runner"; -/** The globals dx reads and writes to play Mamar's song (see src/dx/mamar.h). */ -const SYMBOLS = [ - "MamarEnabled", - "MamarReady", - "MamarBGM", - "MamarBGMSize", - "MamarVariation", - "MamarBankSong", - "MamarRequest", - "MamarPaused", - "MamarAmbience", - "MamarTrackMute", - "MamarTempo", -] as const; - -type Symbols = Record<(typeof SYMBOLS)[number], number>; - -/** Largest encoded BGM dx's buffer for Mamar's song holds (MAMAR_BGM_MAX_SIZE). */ -const MAX_BGM_SIZE = 0x5000; - -/** MamarReady's value once the game has booted (MAMAR_READY). */ -const READY = 0x4d414d52; - -const TRACK_MUTE_VALUES: Record = { - none: 0, - mute: 1, - solo: 2, -}; - /** How long the game gets to boot and get ready for Mamar's songs. */ const BOOT_TIMEOUT_MS = 20_000; -const BOOT_POLL_MS = 100; const STATUS_POLL_MS = 100; -interface Running { - symbols: Symbols; - /** Where the game put its buffer for the song. */ - bgmAddress: number; - request: number; -} - /** * Plays songs from open BGM editors in the mod's built ROM, running in ares. * There's one ares, so one editor's player controls it at a time: the last to @@ -82,7 +53,7 @@ export class MusicPreview { @inject(MessageService) protected readonly messageService!: MessageService; - protected running?: Promise; + protected running?: Promise; protected controller?: PreviewSongPlayer; protected statusTimer?: ReturnType; @@ -138,7 +109,7 @@ export class MusicPreview { } } - protected ensureRunning(): Promise { + protected ensureRunning(): Promise { if (!this.running || this.aresSession.owner !== "music") { const running = this.boot(); this.running = running; @@ -161,7 +132,7 @@ export class MusicPreview { return this.running; } - protected async boot(): Promise { + protected async boot(): Promise { const root = this.workspaceService.tryGetRoots()[0]; if (!root) { throw new Error("No mod is open."); @@ -173,83 +144,49 @@ export class MusicPreview { } const found = await this.debugServer.readSymbols(root.resource.toString(), [ - ...SYMBOLS, + ...MAMAR_SYMBOL_NAMES, ]); - const missing = SYMBOLS.filter((name) => found[name] === undefined); + const missing = MAMAR_SYMBOL_NAMES.filter( + (name) => found[name] === undefined, + ); if (missing.length > 0) { throw new Error( "This mod's version of dx can't preview music. Update dx to play songs from the editor.", ); } - const symbols = found as Symbols; await this.aresSession.startMusic(); - - // Writing memory while the game boots can stop it booting, so only read - // until it's ready. Reads fail until ares opens its connection. - const deadline = Date.now() + BOOT_TIMEOUT_MS; - const readUntil = async ( - address: number, - done: (value: number) => boolean, - ): Promise => { - while (Date.now() < deadline) { - try { - const value = await this.ares.readU32(address); - if (done(value)) { - return value; - } - } catch { - // Not connected yet. - } - await new Promise((resolve) => setTimeout(resolve, BOOT_POLL_MS)); - } - return undefined; + const memory: EmulatorMemory = { + read: (address, size) => this.ares.readMemory(address, size), + write: (address, data) => this.ares.writeMemory(address, data), }; - - if ( - (await readUntil(symbols.MamarReady, (v) => v === READY)) !== undefined - ) { - this.ares.setVideoEnabled(false); - this.ares.writeU32(symbols.MamarEnabled, 1); - const bgmAddress = await readUntil(symbols.MamarBGM, (v) => v !== 0); - if (bgmAddress !== undefined) { - return { symbols, bgmAddress, request: 0 }; - } - } - throw new Error("The game didn't get ready to play the song."); + const mamar = await DxMamar.connect( + memory, + found as MamarSymbols, + BOOT_TIMEOUT_MS, + ); + this.ares.setVideoEnabled(false); + return mamar; } - protected writeAll(running: Running, player: PreviewSongPlayer): void { - const { symbols } = running; - this.ares.writeU32(symbols.MamarPaused, player.paused ? 1 : 0); - this.ares.writeU32(symbols.MamarAmbience, player.ambience); - const mutes = new Uint8Array(player.trackMutes.length * 4); - const view = new DataView(mutes.buffer); - player.trackMutes.forEach((mute, i) => - view.setUint32(i * 4, TRACK_MUTE_VALUES[mute], false), - ); - this.ares.writeMemory(symbols.MamarTrackMute, mutes); + protected writeAll(mamar: DxMamar, player: PreviewSongPlayer): void { + mamar.setPaused(player.paused); + mamar.setAmbientSound(player.ambience); + mamar.setTrackMutes(player.trackMutes); } /** Plays the player's song from its start. */ - protected writeSong(running: Running, player: PreviewSongPlayer): void { - const { bgm } = player; - if (!bgm) { + protected writeSong(mamar: DxMamar, player: PreviewSongPlayer): void { + if (!player.bgm) { return; } - if (bgm.length > MAX_BGM_SIZE) { + try { + mamar.play(player.bgm, player.variation, player.bankSong); + } catch (error) { this.messageService.error( - `The song is too large to play: ${bgm.length} bytes, but the game holds ${MAX_BGM_SIZE}.`, + error instanceof Error ? error.message : String(error), ); - return; } - const { symbols } = running; - this.ares.writeMemory(running.bgmAddress, bgm); - this.ares.writeU32(symbols.MamarBGMSize, bgm.length); - this.ares.writeU32(symbols.MamarVariation, player.variation); - this.ares.writeU32(symbols.MamarBankSong, player.bankSong); - running.request++; - this.ares.writeU32(symbols.MamarRequest, running.request); } protected startStatusPolling(): void { @@ -265,10 +202,9 @@ export class MusicPreview { } reading = true; try { - const { symbols } = await running; - const tempo = await this.ares.readU32(symbols.MamarTempo); + const tempo = await (await running).readTempo(); if (this.controller === controller) { - controller.status({ tempo: tempo / 100 }); + controller.status({ tempo }); } } catch { // ares is restarting or has exited. diff --git a/yarn.lock b/yarn.lock index db252c1..29c02fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6133,10 +6133,10 @@ make-fetch-happen@^10.2.1: socks-proxy-agent "^7.0.0" ssri "^9.0.0" -mamar-editor@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/mamar-editor/-/mamar-editor-0.1.0.tgz#1c692055a939d804714171cb9da89dcdbd6d6a03" - integrity sha512-McKrngy0VE07ySsWdVcZOsC0uFqnNHy981G6VXNhzYcF6DigqzUrJtZAE/0+Zcv65hJZbDEGb4wqu1jPvhPq5Q== +mamar-editor@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/mamar-editor/-/mamar-editor-0.2.0.tgz#65243a9d32f72a3df33d86772fc03e498ffbb4b8" + integrity sha512-C31SiY1tniCTqIN5TZALZ2n/RXZdOlyqTNy7QwfrZXyDtxF/cbAOJEyDf4CU/hlNL+dRFSzaeNWD+5vlUW/1jg== markdown-it-anchor@^9.2.0: version "9.2.0" -- 2.51.2