diff --git a/src/lib/components/MudConnection.svelte b/src/lib/components/MudConnection.svelte index 8c3eca7..2a785fd 100644 --- a/src/lib/components/MudConnection.svelte +++ b/src/lib/components/MudConnection.svelte @@ -1,21 +1,16 @@ {#if $connectionStatus[profileId]} @@ -607,4 +427,4 @@ white-space: nowrap; border-width: 0; } - + \ No newline at end of file diff --git a/src/lib/components/MudMdi.svelte b/src/lib/components/MudMdi.svelte index 0468544..a305469 100644 --- a/src/lib/components/MudMdi.svelte +++ b/src/lib/components/MudMdi.svelte @@ -381,6 +381,10 @@ { + // Just log the event, actual playback is handled by ClientMediaPackage + console.log(`MudMdi received playSound event for ${tab.id}:`, event.detail); + }} /> ({}); +// Simple store for active connections +export const connections = writable>({}); /** * ConnectionManager - Singleton service to manage MUD connections - * This ensures connections stay alive even when components are unmounted during tab switches + * Simplified to just maintain a central registry of active connections */ export class ConnectionManager { private static instance: ConnectionManager; @@ -29,63 +28,79 @@ export class ConnectionManager { } /** - * Get an existing connection without creating a new one + * Get all connections */ - public getExistingConnection(profileId: string): MudConnection | null { - const connections = get(activeConnections); - return connections[profileId] || null; + public getConnections(): Record { + return get(connections); } - + /** - * Create a new connection or return an existing one + * Get a connection by profile ID */ - public getConnection(profileId: string, options: { - host: string; - port: number; + public getConnection(profileId: string): MudConnection | null { + const allConnections = get(connections); + return allConnections[profileId] || null; + } + + /** + * Create or get a connection + */ + public createConnection(options: { + profileId: string; + host: string; + port: number; useSSL?: boolean; - gmcpHandler?: GmcpHandler; }): MudConnection { - // Get current connections - const connections = get(activeConnections); + const { profileId, host, port, useSSL } = options; // Check if a connection already exists for this profile - if (connections[profileId]) { - console.log(`Returning existing connection for profile ${profileId}`); - return connections[profileId]; + const existingConnection = this.getConnection(profileId); + if (existingConnection) { + console.log(`Connection already exists for profile ${profileId}`); + return existingConnection; } - // Create a new connection + // Create a new connection with the profile ID as the connection ID console.log(`Creating new connection for profile ${profileId}`); const connection = new MudConnection({ - host: options.host, - port: options.port, - useSSL: options.useSSL, - gmcpHandler: options.gmcpHandler + id: profileId, + host, + port, + useSSL }); - // Set up event handlers - this.setupConnectionEvents(connection, profileId); + // Set up event listeners + this.setupConnectionEvents(connection); - // Store the connection - activeConnections.update(connections => ({ - ...connections, + // Store in the connections registry + connections.update(conns => ({ + ...conns, [profileId]: connection })); - // Return the connection + // Update connection status + connectionStatus.update(statuses => ({ + ...statuses, + [profileId]: 'disconnected' + })); + return connection; } /** * Connect to a MUD server + * Returns the connection object */ public connect(profileId: string, options: { host: string; port: number; useSSL?: boolean; - gmcpHandler?: GmcpHandler; - }): void { - const connection = this.getConnection(profileId, options); + }): MudConnection { + // Get or create the connection + const connection = this.createConnection({ + profileId, + ...options + }); // Update connection status connectionStatus.update(statuses => ({ @@ -95,16 +110,18 @@ export class ConnectionManager { // Connect connection.connect(); + + return connection; } /** * Disconnect from a MUD server */ public disconnect(profileId: string): void { - const connections = get(activeConnections); + const connection = this.getConnection(profileId); - if (connections[profileId]) { - connections[profileId].disconnect(); + if (connection) { + connection.disconnect(); // Update connection status connectionStatus.update(statuses => ({ @@ -118,28 +135,30 @@ export class ConnectionManager { * Send text to a MUD server */ public send(profileId: string, text: string): void { - const connections = get(activeConnections); + const connection = this.getConnection(profileId); - if (connections[profileId]) { - connections[profileId].send(text); + if (connection) { + connection.send(text); } } /** - * Close a connection and remove it + * Removes a connection from the registry */ - public closeConnection(profileId: string): void { - const connections = get(activeConnections); + public removeConnection(profileId: string): void { + const connection = this.getConnection(profileId); - if (connections[profileId]) { - // Disconnect first - connections[profileId].disconnect(); + if (connection) { + // Disconnect first if needed + if (connection.isConnected()) { + connection.disconnect(); + } - // Then remove from store - activeConnections.update(connections => { - const newConnections = { ...connections }; - delete newConnections[profileId]; - return newConnections; + // Remove from store + connections.update(conns => { + const newConns = { ...conns }; + delete newConns[profileId]; + return newConns; }); // Update connection status @@ -154,10 +173,12 @@ export class ConnectionManager { /** * Set up event handlers for a connection */ - private setupConnectionEvents(connection: MudConnection, profileId: string): void { + private setupConnectionEvents(connection: MudConnection): void { + const profileId = connection.id; + // Handle connection established connection.on('connected', () => { - console.log(`ConnectionManager: Connection established for profile ${profileId}`); + console.log(`Connection established for profile ${profileId}`); // Update connection status connectionStatus.update(statuses => ({ @@ -168,7 +189,7 @@ export class ConnectionManager { // Handle connection closed connection.on('disconnected', () => { - console.log(`ConnectionManager: Connection closed for profile ${profileId}`); + console.log(`Connection closed for profile ${profileId}`); // Update connection status connectionStatus.update(statuses => ({ @@ -179,7 +200,7 @@ export class ConnectionManager { // Handle connection error connection.on('error', (error) => { - console.error(`ConnectionManager: Connection error for profile ${profileId}:`, error); + console.error(`Connection error for profile ${profileId}:`, error); // Update connection status connectionStatus.update(statuses => ({ diff --git a/src/lib/connection/MudConnection.ts b/src/lib/connection/MudConnection.ts index 177d31b..7d9ec5c 100644 --- a/src/lib/connection/MudConnection.ts +++ b/src/lib/connection/MudConnection.ts @@ -1,5 +1,5 @@ import { EventEmitter } from '$lib/utils/EventEmitter'; -import type { GmcpHandler } from '$lib/gmcp/GmcpHandler'; +import { GmcpHandler } from '$lib/gmcp/GmcpHandler'; // IAC codes for telnet negotiation enum TelnetCommand { @@ -16,38 +16,77 @@ enum TelnetCommand { interface MudConnectionOptions { host: string; port: number; - gmcpHandler?: GmcpHandler; useSSL?: boolean; + id: string; } +/** + * MudConnection - Handles a single connection to a MUD server + * Each instance has its own GMCP handler and maintains its own state + */ export class MudConnection extends EventEmitter { private host: string; private port: number; private useSSL: boolean; private webSocket: WebSocket | null = null; - private gmcpHandler: GmcpHandler | null = null; + private gmcpHandler: GmcpHandler; private buffer: number[] = []; private connected: boolean = false; private negotiationBuffer: number[] = []; private isInIAC: boolean = false; private inSubnegotiation: boolean = false; - private simulationMode: boolean = false; // Disable simulation mode to use real connections + public readonly id: string; constructor(options: MudConnectionOptions) { super(); this.host = options.host; this.port = options.port; this.useSSL = options.useSSL || false; - this.gmcpHandler = options.gmcpHandler || null; + this.id = options.id; + + // Create GMCP handler + this.gmcpHandler = new GmcpHandler(); + + // Set up GMCP event forwarding + this.setupGmcpEvents(); + + console.log(`MudConnection created for ${this.host}:${this.port} with ID ${this.id}`); + } + + /** + * Set up event forwarding from GMCP handler + */ + private setupGmcpEvents(): void { + // Forward all GMCP events to listeners of this connection + this.gmcpHandler.on('gmcp', (module, data) => { + this.emit('gmcp', module, data); + }); + + // Forward specific module events (like gmcp:Core.Ping) + this.gmcpHandler.on('*', (eventName, ...args) => { + if (eventName.startsWith('gmcp:')) { + this.emit(eventName, ...args); + } + }); + + // Handle GMCP events that need special processing + this.gmcpHandler.on('playSound', (url, volume, loop) => { + console.log(`MudConnection forwarding playSound event: ${url}`); + this.emit('playSound', { url, volume, loop }); + }); + + // Listen for sendGmcp events from the GMCP handler + this.gmcpHandler.on('sendGmcp', (module, data) => { + this.sendGmcp(module, data); + }); } /** * Connect to the MUD server */ public connect(): void { - // For development/testing purposes, we'll use a simulated connection - if (this.simulationMode) { - this.connectSimulated(); + if (this.connected) { + console.log(`Already connected to ${this.host}:${this.port}`); return; } @@ -63,24 +102,24 @@ export class MudConnection extends EventEmitter { wsUrl = `${wsProtocol}://${window.location.host}/mud-ws?host=${encodeURIComponent(this.host)}&port=${this.port}&useSSL=${this.useSSL}`; } - console.log('Connecting to WebSocket server:', wsUrl); + console.log(`Connecting to WebSocket server: ${wsUrl}`); this.webSocket = new WebSocket(wsUrl); - this.webSocket.binaryType = 'arraybuffer'; this.webSocket.onopen = () => { this.connected = true; + console.log(`Connected to ${this.host}:${this.port}`); this.emit('connected'); // Send GMCP negotiation upon connection - if (this.gmcpHandler) { - this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP); - } + console.log('Sending GMCP negotiation'); + this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP); }; this.webSocket.onclose = () => { this.connected = false; + console.log(`Disconnected from ${this.host}:${this.port}`); this.emit('disconnected'); }; @@ -109,35 +148,6 @@ export class MudConnection extends EventEmitter { }; } - /** - * Connect to a simulated MUD server for testing - */ - private connectSimulated(): void { - console.log('Using simulated MUD connection'); - - // Simulate connection delay - setTimeout(() => { - this.connected = true; - this.emit('connected'); - - // Send welcome message - const welcomeMessage = ` -============================================================ -| Welcome to ${this.host} | -============================================================ - -This is a simulated connection for testing purposes. -Commands you type will be echoed back to the terminal. - -Type 'help' for a list of available commands. -Type 'look' to see the current room. -`; - setTimeout(() => { - this.emit('received', welcomeMessage); - }, 500); - }, 1000); - } - /** * Send text to the MUD server */ @@ -146,12 +156,6 @@ Type 'look' to see the current room. throw new Error('Not connected to MUD server'); } - // In simulation mode, generate appropriate responses - if (this.simulationMode) { - this.handleSimulatedCommand(text); - return; - } - if (!this.webSocket) { throw new Error('WebSocket not initialized'); } @@ -175,76 +179,10 @@ Type 'look' to see the current room. } } - /** - * Handle commands in simulation mode - */ - private handleSimulatedCommand(command: string): void { - // Emit that we sent the command - this.emit('sent', command); - - // Process command with a small delay to simulate network latency - setTimeout(() => { - const cmd = command.trim().toLowerCase(); - - if (cmd === 'help') { - this.emit('received', ` -Available commands: -- help: Show this help message -- look: Look at the current room -- say : Say something -- who: Show who's online -- inventory: Show your inventory -- quit: Disconnect -`); - } else if (cmd === 'look') { - this.emit('received', ` -The Testing Room -================ -You are in a simple testing room. The walls are white and featureless, -with a single door to the north. A sign on the wall reads "Simulation Mode". - -Obvious exits: north - -You see a test object here. -`); - } else if (cmd.startsWith('say ')) { - const message = command.substring(4); - this.emit('received', `You say, "${message}"\n`); - } else if (cmd === 'who') { - this.emit('received', ` -Players online: -- TestPlayer1 (Idle) -- TestPlayer2 (AFK) -- You - -Total: 3 players -`); - } else if (cmd === 'inventory' || cmd === 'i') { - this.emit('received', ` -You are carrying: -- a test item -- a notebook -- some coins (15) -`); - } else if (cmd === 'quit' || cmd === 'exit') { - this.emit('received', 'Goodbye! Thanks for testing.\n'); - setTimeout(() => this.disconnect(), 1000); - } else { - this.emit('received', `Unknown command: ${command}\nType 'help' for a list of commands.\n`); - } - }, 200); - } - /** * Disconnect from the MUD server */ public disconnect(): void { - if (this.simulationMode) { - this.connected = false; - this.emit('disconnected'); - return; - } - if (this.webSocket) { this.webSocket.close(); this.webSocket = null; @@ -255,8 +193,6 @@ You are carrying: * Handle incoming data from the MUD server */ private handleIncomingData(data: Uint8Array): void { - console.log(`Received ${data.length} bytes from server`); - // Quickly check if we need to handle telnet negotiation let containsIAC = false; for (let i = 0; i < data.length; i++) { @@ -266,6 +202,12 @@ You are carrying: } } + // Debug: Log raw data for debugging if it contains IAC + if (containsIAC) { + const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' '); + console.log(`Raw data with IAC: ${hexData}`); + } + // Fast path if no IAC codes if (!containsIAC && !this.isInIAC) { const text = new TextDecoder().decode(data); @@ -273,12 +215,6 @@ You are carrying: return; } - // Debug: Log the raw bytes if IAC is present - if (containsIAC) { - const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' '); - console.log(`Data with IAC: ${hexData}`); - } - // Process each byte in the incoming data for (let i = 0; i < data.length; i++) { const byte = data[i]; @@ -294,7 +230,7 @@ You are carrying: this.negotiationBuffer.length > 0 && this.negotiationBuffer[this.negotiationBuffer.length - 2] === TelnetCommand.IAC) { - console.log('FOUND IAC SE SEQUENCE - End of subnegotiation'); + console.log('End of subnegotiation found'); // Process the complete subnegotiation this.handleCompleteSubnegotiation(); @@ -309,10 +245,8 @@ You are carrying: if (byte === TelnetCommand.SB) { // Start of subnegotiation this.inSubnegotiation = true; - console.log('Started subnegotiation - waiting for option code'); } else if (byte === TelnetCommand.WILL || byte === TelnetCommand.DO) { // Need one more byte for option - console.log(`Telnet WILL/DO command, waiting for option`); } else { // Simple 3-byte command this.processSimpleTelnetCommand(); @@ -329,7 +263,7 @@ You are carrying: // Start of telnet command this.isInIAC = true; this.negotiationBuffer = [byte]; - console.log('Found IAC - start of telnet command'); + console.log('IAC command detected'); } else { // Normal data byte, add to buffer this.buffer.push(byte); @@ -353,19 +287,15 @@ You are carrying: try { const [iac, command, option] = this.negotiationBuffer; - console.log(`Processing telnet command: IAC(${iac}) ${command} ${option}`); - // Handle specific commands if ((command === TelnetCommand.WILL || command === TelnetCommand.DO) && option === TelnetCommand.GMCP) { - console.log('Server indicates WILL/DO GMCP, responding with DO GMCP'); + console.log('Server supports GMCP, responding with DO GMCP'); // Server wants to use GMCP, we'll respond with IAC DO GMCP this.sendIAC(TelnetCommand.DO, TelnetCommand.GMCP); - // And request Core.Hello - if (this.gmcpHandler) { - console.log('Requesting GMCP capabilities'); - this.gmcpHandler.requestCapabilities(); - } + // Request GMCP capabilities + console.log('Requesting GMCP capabilities'); + this.gmcpHandler.requestCapabilities(); } } catch (error) { console.error('Error processing telnet command:', error); @@ -377,39 +307,31 @@ You are carrying: */ private handleCompleteSubnegotiation(): void { try { - console.log(`Handling subnegotiation, buffer length: ${this.negotiationBuffer.length}`); + // Debug buffer contents + const bufferHex = this.negotiationBuffer.map(b => b.toString(16).padStart(2, '0')).join(' '); + console.log(`Processing subnegotiation, buffer: ${bufferHex}`); // Check if this is a GMCP subnegotiation // IAC SB GMCP ... IAC SE // Indexes: 0 1 2 ... -2 -1 if (this.negotiationBuffer.length >= 5 && this.negotiationBuffer[2] === TelnetCommand.GMCP) { - console.log('Processing complete GMCP subnegotiation'); + console.log('Processing GMCP subnegotiation'); try { // Extract the GMCP data (skip IAC SB GMCP, and the final IAC SE) const gmcpData = this.negotiationBuffer.slice(3, -2); const gmcpText = new TextDecoder().decode(new Uint8Array(gmcpData)); - console.log('RECEIVED GMCP DATA:', gmcpText); + console.log(`GMCP message: ${gmcpText}`); - // Process the GMCP message - if (this.gmcpHandler) { - setTimeout(() => { - try { - console.log('Passing GMCP to handler:', gmcpText); - this.gmcpHandler?.handleGmcpMessage(gmcpText); - } catch (error) { - console.error('Error in GMCP handler:', error); - } - }, 0); - } else { - console.warn('No GMCP handler available for message:', gmcpText); - } + // Process the GMCP message immediately + console.log('Passing GMCP to handler:', gmcpText); + this.gmcpHandler.handleGmcpMessage(gmcpText); } catch (error) { - console.error('Error processing GMCP data:', error, 'Buffer:', this.negotiationBuffer); + console.error('Error processing GMCP data:', error); } } else { - console.log(`Non-GMCP subnegotiation complete, option: ${this.negotiationBuffer[2]}`); + console.log(`Non-GMCP subnegotiation received: ${this.negotiationBuffer[2]}`); } } catch (error) { console.error('Error handling subnegotiation:', error); @@ -432,19 +354,12 @@ You are carrying: * Send a GMCP message */ public sendGmcp(module: string, data: any): void { - if (!this.connected) { - return; - } - - if (this.simulationMode) { - console.log(`GMCP send (simulated): ${module}`, data); - return; - } - - if (!this.webSocket) { + if (!this.connected || !this.webSocket) { + console.log('Cannot send GMCP - not connected'); return; } + console.log(`Sending GMCP: ${module}`, data); const gmcpString = `${module} ${JSON.stringify(data)}`; const gmcpData = new TextEncoder().encode(gmcpString); @@ -460,4 +375,18 @@ You are carrying: this.webSocket.send(telnetSequence); } + + /** + * Get the GMCP handler associated with this connection + */ + public getGmcpHandler(): GmcpHandler { + return this.gmcpHandler; + } + + /** + * Check if the connection is active + */ + public isConnected(): boolean { + return this.connected; + } } \ No newline at end of file diff --git a/src/lib/gmcp/GmcpHandler.ts b/src/lib/gmcp/GmcpHandler.ts index a2a987d..1e04079 100644 --- a/src/lib/gmcp/GmcpHandler.ts +++ b/src/lib/gmcp/GmcpHandler.ts @@ -92,10 +92,20 @@ export class GmcpHandler extends EventEmitter { // Find the appropriate package handler let handled = false; + console.log(`Looking for handler for GMCP module: ${module}`); + console.log(`Available package handlers: ${Array.from(this.packageHandlers.keys()).join(', ')}`); + for (const [packagePrefix, handler] of this.packageHandlers.entries()) { + console.log(`Checking if ${module} starts with ${packagePrefix}`); if (module.startsWith(packagePrefix)) { - handler.handleMessage(module, data); - handled = true; + console.log(`Found handler for ${module}: ${packagePrefix}`); + try { + handler.handleMessage(module, data); + console.log(`Successfully processed ${module} with handler ${packagePrefix}`); + handled = true; + } catch (error) { + console.error(`Error in handler ${packagePrefix} for module ${module}:`, error); + } break; } } diff --git a/src/lib/gmcp/packages/ClientMediaPackage.ts b/src/lib/gmcp/packages/ClientMediaPackage.ts index 94f6238..5a8f0da 100644 --- a/src/lib/gmcp/packages/ClientMediaPackage.ts +++ b/src/lib/gmcp/packages/ClientMediaPackage.ts @@ -29,6 +29,23 @@ export class ClientMediaPackage implements GmcpPackageHandler { // Subscribe to global volume changes to update active sounds this.setupVolumeSubscription(); + + // Log that we're ready to receive GMCP messages + console.log('ClientMediaPackage ready to handle Client.Media.* GMCP messages'); + + // Double check event listeners + if (this.emitter) { + this.emitter.on('test', () => { + console.log('Test event received by ClientMediaPackage'); + }); + + // Test emit an event to confirm EventEmitter works properly + setTimeout(() => { + if (this.emitter) { + this.emitter.emit('test'); + } + }, 500); + } } /** @@ -73,10 +90,16 @@ export class ClientMediaPackage implements GmcpPackageHandler { handleMessage(module: string, data: any): void { try { + console.log(`ClientMediaPackage handling message: ${module}`, data); + if (module === 'Client.Media.Play') { + console.log('Processing Client.Media.Play message'); this.handlePlay(data); } else if (module === 'Client.Media.Stop') { + console.log('Processing Client.Media.Stop message'); this.handleStop(data); + } else { + console.log(`Unhandled Client.Media message type: ${module}`); } } catch (error) { console.error('Error handling GMCP Media message:', error); @@ -169,17 +192,45 @@ export class ClientMediaPackage implements GmcpPackageHandler { this.tagToIdsMap.get(tag)?.add(soundId); } - // Play the sound - sound.play(); + try { + // Play the sound directly + console.log(`Directly playing sound ${soundId} from ${fullUrl} with volume ${soundVolume}`); + + // Register event handlers for debugging + sound.once('load', () => { + console.log(`Sound ${soundId} loaded successfully from ${fullUrl}`); + }); + + sound.once('play', () => { + console.log(`Sound ${soundId} started playing`); + }); + + sound.once('end', () => { + console.log(`Sound ${soundId} finished playing`); + this.cleanupSound(soundId); + }); + + sound.on('loaderror', (id, error) => { + console.error(`Error loading sound ${soundId} from ${fullUrl}:`, error); + }); + + sound.on('playerror', (id, error) => { + console.error(`Error playing sound ${soundId} from ${fullUrl}:`, error); + }); + + // Play the sound + const soundId2 = sound.play(); + console.log(`Sound started with Howler id: ${soundId2}`); + } catch (error) { + console.error(`Error playing sound ${soundId}:`, error); + } - // Set up cleanup when sound ends - sound.once('end', () => { - console.log(`Sound ${soundId} finished playing`); - this.cleanupSound(soundId); - }); - - // Emit an event for other components - this.emitter?.emit('playSound', fullUrl, soundVolume, !!data.loop); + // Emit an event for informational purposes only + // We don't need other components to play the sound + if (this.emitter) { + console.log(`Emitting notification of sound playback: ${fullUrl}`); + this.emitter.emit('playSound', fullUrl, soundVolume, !!data.loop); + } } catch (error) { console.error('Error in handlePlay:', error); }