This commit is contained in:
2025-04-22 18:21:10 +02:00
parent 9309509858
commit 4205af4da6
6 changed files with 386 additions and 551 deletions

View File

@@ -1,21 +1,16 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from 'svelte'; import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { MudConnection } from '$lib/connection/MudConnection'; import { ConnectionManager, connections } from '$lib/connection/ConnectionManager';
import { ConnectionManager, activeConnections } from '$lib/connection/ConnectionManager';
import { GmcpHandler } from '$lib/gmcp/GmcpHandler';
import { TriggerSystem } from '$lib/triggers/TriggerSystem'; import { TriggerSystem } from '$lib/triggers/TriggerSystem';
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager'; import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
import { import {
connections,
connectionStatus, connectionStatus,
activeProfileId, activeProfileId,
activeProfile, activeProfile,
profiles, profiles,
addToOutputHistory, addToOutputHistory,
updateGmcpData, updateGmcpData,
accessibilitySettings, accessibilitySettings
uiSettings,
outputHistory
} from '$lib/stores/mudStore'; } from '$lib/stores/mudStore';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
@@ -26,19 +21,15 @@
export let autoConnect = false; export let autoConnect = false;
// Local state // Local state
let connection: MudConnection | null = null; let connectionManager: ConnectionManager;
let connectionManager: ConnectionManager | null = null; let connection = null;
let gmcpHandler: GmcpHandler | null = null; let triggerSystem: TriggerSystem;
let triggerSystem: TriggerSystem | null = null; let accessibilityManager: AccessibilityManager;
let accessibilityManager: AccessibilityManager | null = null;
/** // Speech control
* Handle keyboard events for speech control
*/
function handleKeyDown(event: KeyboardEvent): void { function handleKeyDown(event: KeyboardEvent): void {
// Control key stops speech - the key code for Control is 17 // Control key stops speech
if (event.key === 'Control' || event.ctrlKey || event.keyCode === 17) { if (event.key === 'Control' || event.ctrlKey) {
// Stop propagation to ensure no other handlers interfere
event.stopPropagation(); event.stopPropagation();
if (accessibilityManager && accessibilityManager.isSpeaking()) { if (accessibilityManager && accessibilityManager.isSpeaking()) {
@@ -48,7 +39,7 @@
} }
// Enter key stops speech if the setting is enabled // Enter key stops speech if the setting is enabled
if ((event.key === 'Enter' || event.keyCode === 13) && $accessibilitySettings.interruptSpeechOnEnter) { if (event.key === 'Enter' && $accessibilitySettings.interruptSpeechOnEnter) {
if (accessibilityManager && accessibilityManager.isSpeaking()) { if (accessibilityManager && accessibilityManager.isSpeaking()) {
console.log('Enter key pressed - interrupting speech'); console.log('Enter key pressed - interrupting speech');
accessibilityManager.stopSpeech(); accessibilityManager.stopSpeech();
@@ -56,6 +47,7 @@
} }
} }
// Cleanup functions
let unsubscribeFunctions = []; let unsubscribeFunctions = [];
onMount(() => { onMount(() => {
@@ -64,60 +56,11 @@
// Initialize components // Initialize components
initializeComponents(); initializeComponents();
// Set up keyboard listener for speech control - use capture phase and make it top-level // Set up keyboard listener for speech control
document.addEventListener('keydown', handleKeyDown, true); // Using document instead of window document.addEventListener('keydown', handleKeyDown, true);
// Get the connection status from the store // Auto-connect if enabled
const currentStatus = get(connectionStatus)[profileId]; if (autoConnect) {
// Check if we already have a connection in the ConnectionManager
if (currentStatus === 'connected' || currentStatus === 'connecting') {
console.log(`Connection exists for ${profileId}, getting from manager`);
const connectionManager = ConnectionManager.getInstance();
const existingConnection = connectionManager.getExistingConnection(profileId);
if (existingConnection) {
console.log(`Found existing connection for ${profileId}, updating local ref`);
// Update the connections store
connections.update(conns => ({
...conns,
[profileId]: existingConnection
}));
// Update local reference
connection = existingConnection;
// Re-attach listeners
setupConnectionListeners(existingConnection);
}
} else {
// Register this connection in the connections store with component instance
connections.update(conns => {
console.log(`Registering connection component for profile: ${profileId}`);
return {
...conns,
[profileId]: this
};
});
// Update connection status - ensure UI shows status as 'disconnected' at start
if (!currentStatus) {
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'disconnected'`);
return {
...statuses,
[profileId]: 'disconnected'
};
});
}
}
// Dump the current connection status for debugging
setTimeout(() => {
console.log('Current connection status at initialization:', get(connectionStatus));
}, 100);
// Auto-connect if enabled and not already connected - with a slight delay
if (autoConnect && currentStatus !== 'connected') {
console.log('Auto-connecting profile:', profileId); console.log('Auto-connecting profile:', profileId);
setTimeout(() => connect(), 100); setTimeout(() => connect(), 100);
} }
@@ -125,25 +68,19 @@
onDestroy(() => { onDestroy(() => {
console.log(`MudConnection component being destroyed for profile: ${profileId}`); console.log(`MudConnection component being destroyed for profile: ${profileId}`);
// Remove keyboard listener
document.removeEventListener('keydown', handleKeyDown, true); // Match document and capture phase
// Don't disconnect the connection - it should persist even when the component is unmounted // Remove keyboard listener
// Only remove this component instance from the local connections store (not the ConnectionManager) document.removeEventListener('keydown', handleKeyDown, true);
connections.update(conns => {
const newConns = { ...conns }; // Remove event listeners from connection
delete newConns[profileId]; if (connection) {
return newConns; removeConnectionListeners(connection);
}); }
// Clean up subscriptions // Clean up subscriptions
unsubscribeFunctions.forEach(unsubFn => { unsubscribeFunctions.forEach(unsubFn => {
if (typeof unsubFn === 'function') { if (typeof unsubFn === 'function') {
try {
unsubFn(); unsubFn();
} catch (error) {
console.error('Error during unsubscribe:', error);
}
} }
}); });
}); });
@@ -155,141 +92,96 @@
// Get singleton instance of ConnectionManager // Get singleton instance of ConnectionManager
connectionManager = ConnectionManager.getInstance(); connectionManager = ConnectionManager.getInstance();
// Create GMCP handler // Initialize trigger system
gmcpHandler = new GmcpHandler();
// Create trigger system
triggerSystem = new TriggerSystem(); triggerSystem = new TriggerSystem();
console.log('Trigger system created');
// Create accessibility manager // Initialize accessibility manager
accessibilityManager = new AccessibilityManager(); accessibilityManager = new AccessibilityManager();
console.log('Accessibility manager created');
// Set up event listeners // Set up connection from manager if it exists
setupEventListeners(); const existingConnection = connectionManager.getConnection(profileId);
if (existingConnection) {
// Get or create connection from ConnectionManager console.log(`Found existing connection for ${profileId}`);
updateConnectionFromStore(); connection = existingConnection;
setupConnectionListeners(connection);
} }
/** // Subscribe to connections to get updates
* Update local connection reference from store const unsubscribe = connections.subscribe(conns => {
*/ const conn = conns[profileId];
function updateConnectionFromStore() { if (conn && conn !== connection) {
// Subscribe to activeConnections to get updates console.log(`Connection updated for ${profileId}`);
const unsubscribe = activeConnections.subscribe(connections => {
connection = connections[profileId] || null;
// Clean up old connection listeners
if (connection) { if (connection) {
// Re-attach event listeners when connection changes removeConnectionListeners(connection);
}
// Set new connection and listeners
connection = conn;
setupConnectionListeners(connection); setupConnectionListeners(connection);
} }
}); });
// Add to unsubscribe functions
unsubscribeFunctions.push(unsubscribe); unsubscribeFunctions.push(unsubscribe);
} }
/** /**
* Set up listeners for a specific connection * Set up listeners for a specific connection
*/ */
function setupConnectionListeners(conn: MudConnection) { function setupConnectionListeners(conn) {
// First remove ALL event listeners to prevent duplicates console.log('Setting up connection listeners');
console.log('Setting up connection listeners and removing old ones');
conn.off('received');
conn.off('sent');
conn.off('connected');
conn.off('disconnected');
conn.off('error');
// Add new listeners // Remove any existing listeners to prevent duplicates
removeConnectionListeners(conn);
// Add new event listeners
conn.on('received', handleReceived); conn.on('received', handleReceived);
conn.on('sent', handleSent); conn.on('sent', handleSent);
conn.on('connected', handleConnected); conn.on('connected', handleConnected);
conn.on('disconnected', handleDisconnected); conn.on('disconnected', handleDisconnected);
conn.on('error', handleError); conn.on('error', handleError);
// Listen for GMCP events
conn.on('gmcp', handleGmcp);
// Listen for sound play events
conn.on('playSound', handlePlaySound);
console.log('Connection listeners attached successfully');
} }
/** /**
* Set up event listeners for components * Remove listeners from a connection
*/ */
function setupEventListeners() { function removeConnectionListeners(conn) {
if (!gmcpHandler || !triggerSystem || !accessibilityManager) return; if (!conn) return;
// GMCP handler events conn.off('received', handleReceived);
gmcpHandler.on('gmcp', (module, data) => { conn.off('sent', handleSent);
updateGmcpData(module, data); conn.off('connected', handleConnected);
}); conn.off('disconnected', handleDisconnected);
conn.off('error', handleError);
gmcpHandler.on('sendGmcp', (module, data) => { conn.off('gmcp', handleGmcp);
if (connection) { conn.off('playSound', handlePlaySound);
connection.sendGmcp(module, data);
}
});
gmcpHandler.on('playSound', (url, volume, loop) => {
dispatch('playSound', { url, volume, loop });
});
// Trigger system events
triggerSystem.on('sendText', (text) => {
if (connection) {
connection.send(text);
}
});
triggerSystem.on('highlight', (text, pattern, color, isRegex) => {
dispatch('highlight', { text, pattern, color, isRegex });
});
// Set up TTS subscription - use direct subscription to avoid circular updates
const unsubscribeTts = accessibilitySettings.subscribe(settings => {
try {
// Set speech enabled directly based on the store value
if (accessibilityManager) {
console.log('Setting TTS from store:', settings.textToSpeech);
accessibilityManager.setSpeechEnabled(settings.textToSpeech);
// Update speech options when settings change
accessibilityManager.updateSpeechOptions({
rate: settings.speechRate,
pitch: settings.speechPitch,
volume: settings.speechVolume
});
}
} catch (error) {
console.error('Error updating TTS setting:', error);
}
});
unsubscribeFunctions.push(unsubscribeTts);
// Set up only GMCP debugging subscription
let previousGmcpDebug = $uiSettings.debugGmcp;
const unsubscribeUiSettings = uiSettings.subscribe(settings => {
if (settings.debugGmcp !== previousGmcpDebug) {
previousGmcpDebug = settings.debugGmcp;
}
});
unsubscribeFunctions.push(unsubscribeUiSettings);
} }
/** /**
* Connect to the MUD server * Connect to the MUD server
*/ */
export function connect() { export function connect() {
try { // Don't connect if already connected
// First check if we are already connected if (connection && connection.isConnected()) {
const currentStatus = get(connectionStatus)[profileId]; console.log(`Already connected for profile ${profileId}`);
if (currentStatus === 'connected' || currentStatus === 'connecting') {
console.log(`Already ${currentStatus} for profile ${profileId}, not connecting again`);
return; return;
} }
// Find the profile by ID - don't rely on activeProfile // Find the profile
const allProfiles = get(profiles); const allProfiles = get(profiles);
const profile = allProfiles.find(p => p.id === profileId); const profile = allProfiles.find(p => p.id === profileId);
console.log(`Connecting to profile ${profileId}:`, profile);
if (!profile) { if (!profile) {
addToOutputHistory(`Error: Profile ${profileId} not found.`); addToOutputHistory(`Error: Profile ${profileId} not found.`);
console.error(`Profile ${profileId} not found`); console.error(`Profile ${profileId} not found`);
@@ -297,53 +189,31 @@
} }
// Update connection status // Update connection status
connectionStatus.update(statuses => { connectionStatus.update(statuses => ({
console.log(`Setting connection status for ${profileId} to 'connecting'`);
return {
...statuses, ...statuses,
[profileId]: 'connecting' [profileId]: 'connecting'
}; }));
});
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) { if (get(activeProfileId) === profileId) {
addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`); addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`);
} }
// Use the connection manager to get or create a connection
if (!connectionManager) {
connectionManager = ConnectionManager.getInstance();
}
// Connect using the connection manager // Connect using the connection manager
try {
connectionManager.connect(profileId, { connectionManager.connect(profileId, {
host: profile.host, host: profile.host,
port: profile.port, port: profile.port,
useSSL: profile.useSSL, useSSL: profile.useSSL
gmcpHandler: gmcpHandler || undefined
}); });
// Update local connection reference
setTimeout(() => {
updateConnectionFromStore();
}, 100);
} catch (error) { } catch (error) {
console.error(`Failed to connect to ${profileId}:`, error); console.error(`Failed to connect to ${profileId}:`, error);
connectionStatus.update(statuses => ({
connectionStatus.update(statuses => {
return {
...statuses, ...statuses,
[profileId]: 'error' [profileId]: 'error'
}; }));
});
const errorMsg = `Error connecting to profile ${profileId}: ${error}`;
console.error(errorMsg);
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) { if (get(activeProfileId) === profileId) {
addToOutputHistory(errorMsg); addToOutputHistory(`Error connecting: ${error.message}`);
} }
} }
} }
@@ -352,42 +222,18 @@
* Disconnect from the MUD server * Disconnect from the MUD server
*/ */
export function disconnect() { export function disconnect() {
if (!connectionManager) {
connectionManager = ConnectionManager.getInstance();
}
connectionManager.disconnect(profileId); connectionManager.disconnect(profileId);
console.log(`Disconnected profile ${profileId}`);
} }
/** /**
* Handle connection established * Handle connection established
*/ */
function handleConnected() { function handleConnected() {
try { // Find the profile
// Find the profile by ID const profile = get(profiles).find(p => p.id === profileId);
const allProfiles = get(profiles);
const profile = allProfiles.find(p => p.id === profileId);
console.log(`Profile ${profileId} connected:`, profile); console.log(`Profile ${profileId} connected:`, profile);
// Update connection status GLOBALLY, not just for this profile
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'connected'`);
console.log('Previous connection statuses:', statuses);
const newStatuses = {
...statuses,
[profileId]: 'connected'
};
console.log('New connection statuses:', newStatuses);
return newStatuses;
});
// Dump the current connection status for debugging
setTimeout(() => {
console.log('Current connection status after connect:', get(connectionStatus));
}, 100);
// Only add to output history if this is the active profile // Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) { if (get(activeProfileId) === profileId) {
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`); addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`);
@@ -397,8 +243,8 @@
if (profile?.autoLogin?.enabled) { if (profile?.autoLogin?.enabled) {
setTimeout(() => { setTimeout(() => {
// Send username // Send username
if (profile.autoLogin?.username) { if (profile.autoLogin?.username && connection) {
if (connection) connection.send(profile.autoLogin.username); connection.send(profile.autoLogin.username);
} }
// Send password after a delay // Send password after a delay
@@ -422,9 +268,6 @@
} }
dispatch('connected'); dispatch('connected');
} catch (error) {
console.error(`Error handling connection for profile ${profileId}:`, error);
}
} }
/** /**
@@ -433,14 +276,6 @@
function handleDisconnected() { function handleDisconnected() {
console.log(`Profile ${profileId} disconnected`); console.log(`Profile ${profileId} disconnected`);
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'disconnected'`);
return {
...statuses,
[profileId]: 'disconnected'
};
});
// Only add to output history if this is the active profile // Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) { if (get(activeProfileId) === profileId) {
addToOutputHistory('Disconnected from server.'); addToOutputHistory('Disconnected from server.');
@@ -452,17 +287,9 @@
/** /**
* Handle connection error * Handle connection error
*/ */
function handleError(error: any) { function handleError(error) {
console.log(`Profile ${profileId} connection error:`, error); console.log(`Profile ${profileId} connection error:`, error);
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'error'`);
return {
...statuses,
[profileId]: 'error'
};
});
// Format the error message for display // Format the error message for display
const errorMessage = typeof error === 'object' ? const errorMessage = typeof error === 'object' ?
(error.message || JSON.stringify(error)) : (error.message || JSON.stringify(error)) :
@@ -481,8 +308,8 @@
/** /**
* Handle received data * Handle received data
*/ */
function handleReceived(text: string) { function handleReceived(text) {
console.log(`Profile ${profileId} received data, active profile: ${get(activeProfileId)}`); console.log(`Profile ${profileId} received data`);
try { try {
// First check if this is the active profile - only handle messages for the active profile // First check if this is the active profile - only handle messages for the active profile
@@ -491,36 +318,10 @@
return; return;
} }
// First, ensure this profile has an output history entry // Add to output history
const history = get(outputHistory); addToOutputHistory(text);
if (!history[profileId] || !Array.isArray(history[profileId])) {
console.log(`Creating fresh output history for profile ${profileId}`);
outputHistory.update(h => ({
...h,
[profileId]: []
}));
}
// Create the output item // Process triggers if available
const outputItem = {
id: `output-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
text,
timestamp: Date.now(),
isInput: false,
highlights: []
};
// Add directly to this profile's history
outputHistory.update(history => {
console.log(`Adding text to history for profile ${profileId}`);
const profileHistory = history[profileId] || [];
return {
...history,
[profileId]: [...profileHistory, outputItem]
};
});
// Process triggers with safe error handling
if (triggerSystem) { if (triggerSystem) {
try { try {
triggerSystem.processTriggers(text); triggerSystem.processTriggers(text);
@@ -530,7 +331,7 @@
} }
// Try to use text-to-speech if enabled // Try to use text-to-speech if enabled
if (accessibilityManager && $accessibilitySettings.textToSpeech && get(activeProfileId) === profileId) { if (accessibilityManager && $accessibilitySettings.textToSpeech) {
// Use a small timeout to avoid UI blocking // Use a small timeout to avoid UI blocking
setTimeout(() => { setTimeout(() => {
try { try {
@@ -550,9 +351,28 @@
/** /**
* Handle sent data * Handle sent data
*/ */
function handleSent(text: string) { function handleSent(text) {
dispatch('sent', { text }); dispatch('sent', { text });
} }
/**
* Handle GMCP message
*/
function handleGmcp(module, data) {
console.log(`GMCP message received for ${profileId}: ${module}`, data);
updateGmcpData(module, data);
// Forward GMCP events
dispatch('gmcp', { module, data });
}
/**
* Handle play sound event
*/
function handlePlaySound(options) {
console.log(`Play sound event for ${profileId}:`, options);
dispatch('playSound', options);
}
</script> </script>
{#if $connectionStatus[profileId]} {#if $connectionStatus[profileId]}

View File

@@ -381,6 +381,10 @@
<MudConnection <MudConnection
profileId={tab.id} profileId={tab.id}
on:playSound={(event) => {
// Just log the event, actual playback is handled by ClientMediaPackage
console.log(`MudMdi received playSound event for ${tab.id}:`, event.detail);
}}
/> />
<MudTerminal <MudTerminal
autofocus={activeTab === tab.id} autofocus={activeTab === tab.id}

View File

@@ -1,14 +1,13 @@
import { writable, get } from 'svelte/store'; import { writable, get } from 'svelte/store';
import { MudConnection } from './MudConnection'; import { MudConnection } from './MudConnection';
import type { GmcpHandler } from '$lib/gmcp/GmcpHandler';
import { connectionStatus } from '$lib/stores/mudStore'; import { connectionStatus } from '$lib/stores/mudStore';
// Store for active MUD connections (persistent across tab switches) // Simple store for active connections
export const activeConnections = writable<{ [key: string]: MudConnection }>({}); export const connections = writable<Record<string, MudConnection>>({});
/** /**
* ConnectionManager - Singleton service to manage MUD connections * 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 { export class ConnectionManager {
private static instance: 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 { public getConnections(): Record<string, MudConnection> {
const connections = get(activeConnections); return get(connections);
return connections[profileId] || null;
} }
/** /**
* Create a new connection or return an existing one * Get a connection by profile ID
*/ */
public getConnection(profileId: string, options: { 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; host: string;
port: number; port: number;
useSSL?: boolean; useSSL?: boolean;
gmcpHandler?: GmcpHandler;
}): MudConnection { }): MudConnection {
// Get current connections const { profileId, host, port, useSSL } = options;
const connections = get(activeConnections);
// Check if a connection already exists for this profile // Check if a connection already exists for this profile
if (connections[profileId]) { const existingConnection = this.getConnection(profileId);
console.log(`Returning existing connection for profile ${profileId}`); if (existingConnection) {
return connections[profileId]; 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}`); console.log(`Creating new connection for profile ${profileId}`);
const connection = new MudConnection({ const connection = new MudConnection({
host: options.host, id: profileId,
port: options.port, host,
useSSL: options.useSSL, port,
gmcpHandler: options.gmcpHandler useSSL
}); });
// Set up event handlers // Set up event listeners
this.setupConnectionEvents(connection, profileId); this.setupConnectionEvents(connection);
// Store the connection // Store in the connections registry
activeConnections.update(connections => ({ connections.update(conns => ({
...connections, ...conns,
[profileId]: connection [profileId]: connection
})); }));
// Return the connection // Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'disconnected'
}));
return connection; return connection;
} }
/** /**
* Connect to a MUD server * Connect to a MUD server
* Returns the connection object
*/ */
public connect(profileId: string, options: { public connect(profileId: string, options: {
host: string; host: string;
port: number; port: number;
useSSL?: boolean; useSSL?: boolean;
gmcpHandler?: GmcpHandler; }): MudConnection {
}): void { // Get or create the connection
const connection = this.getConnection(profileId, options); const connection = this.createConnection({
profileId,
...options
});
// Update connection status // Update connection status
connectionStatus.update(statuses => ({ connectionStatus.update(statuses => ({
@@ -95,16 +110,18 @@ export class ConnectionManager {
// Connect // Connect
connection.connect(); connection.connect();
return connection;
} }
/** /**
* Disconnect from a MUD server * Disconnect from a MUD server
*/ */
public disconnect(profileId: string): void { public disconnect(profileId: string): void {
const connections = get(activeConnections); const connection = this.getConnection(profileId);
if (connections[profileId]) { if (connection) {
connections[profileId].disconnect(); connection.disconnect();
// Update connection status // Update connection status
connectionStatus.update(statuses => ({ connectionStatus.update(statuses => ({
@@ -118,28 +135,30 @@ export class ConnectionManager {
* Send text to a MUD server * Send text to a MUD server
*/ */
public send(profileId: string, text: string): void { public send(profileId: string, text: string): void {
const connections = get(activeConnections); const connection = this.getConnection(profileId);
if (connections[profileId]) { if (connection) {
connections[profileId].send(text); connection.send(text);
} }
} }
/** /**
* Close a connection and remove it * Removes a connection from the registry
*/ */
public closeConnection(profileId: string): void { public removeConnection(profileId: string): void {
const connections = get(activeConnections); const connection = this.getConnection(profileId);
if (connections[profileId]) { if (connection) {
// Disconnect first // Disconnect first if needed
connections[profileId].disconnect(); if (connection.isConnected()) {
connection.disconnect();
}
// Then remove from store // Remove from store
activeConnections.update(connections => { connections.update(conns => {
const newConnections = { ...connections }; const newConns = { ...conns };
delete newConnections[profileId]; delete newConns[profileId];
return newConnections; return newConns;
}); });
// Update connection status // Update connection status
@@ -154,10 +173,12 @@ export class ConnectionManager {
/** /**
* Set up event handlers for a connection * 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 // Handle connection established
connection.on('connected', () => { connection.on('connected', () => {
console.log(`ConnectionManager: Connection established for profile ${profileId}`); console.log(`Connection established for profile ${profileId}`);
// Update connection status // Update connection status
connectionStatus.update(statuses => ({ connectionStatus.update(statuses => ({
@@ -168,7 +189,7 @@ export class ConnectionManager {
// Handle connection closed // Handle connection closed
connection.on('disconnected', () => { connection.on('disconnected', () => {
console.log(`ConnectionManager: Connection closed for profile ${profileId}`); console.log(`Connection closed for profile ${profileId}`);
// Update connection status // Update connection status
connectionStatus.update(statuses => ({ connectionStatus.update(statuses => ({
@@ -179,7 +200,7 @@ export class ConnectionManager {
// Handle connection error // Handle connection error
connection.on('error', (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 // Update connection status
connectionStatus.update(statuses => ({ connectionStatus.update(statuses => ({

View File

@@ -1,5 +1,5 @@
import { EventEmitter } from '$lib/utils/EventEmitter'; import { EventEmitter } from '$lib/utils/EventEmitter';
import type { GmcpHandler } from '$lib/gmcp/GmcpHandler'; import { GmcpHandler } from '$lib/gmcp/GmcpHandler';
// IAC codes for telnet negotiation // IAC codes for telnet negotiation
enum TelnetCommand { enum TelnetCommand {
@@ -16,38 +16,77 @@ enum TelnetCommand {
interface MudConnectionOptions { interface MudConnectionOptions {
host: string; host: string;
port: number; port: number;
gmcpHandler?: GmcpHandler;
useSSL?: boolean; 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 { export class MudConnection extends EventEmitter {
private host: string; private host: string;
private port: number; private port: number;
private useSSL: boolean; private useSSL: boolean;
private webSocket: WebSocket | null = null; private webSocket: WebSocket | null = null;
private gmcpHandler: GmcpHandler | null = null; private gmcpHandler: GmcpHandler;
private buffer: number[] = []; private buffer: number[] = [];
private connected: boolean = false; private connected: boolean = false;
private negotiationBuffer: number[] = []; private negotiationBuffer: number[] = [];
private isInIAC: boolean = false; private isInIAC: boolean = false;
private inSubnegotiation: boolean = false; private inSubnegotiation: boolean = false;
private simulationMode: boolean = false; // Disable simulation mode to use real connections public readonly id: string;
constructor(options: MudConnectionOptions) { constructor(options: MudConnectionOptions) {
super(); super();
this.host = options.host; this.host = options.host;
this.port = options.port; this.port = options.port;
this.useSSL = options.useSSL || false; 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 * Connect to the MUD server
*/ */
public connect(): void { public connect(): void {
// For development/testing purposes, we'll use a simulated connection if (this.connected) {
if (this.simulationMode) { console.log(`Already connected to ${this.host}:${this.port}`);
this.connectSimulated();
return; 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}`; 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 = new WebSocket(wsUrl);
this.webSocket.binaryType = 'arraybuffer'; this.webSocket.binaryType = 'arraybuffer';
this.webSocket.onopen = () => { this.webSocket.onopen = () => {
this.connected = true; this.connected = true;
console.log(`Connected to ${this.host}:${this.port}`);
this.emit('connected'); this.emit('connected');
// Send GMCP negotiation upon connection // Send GMCP negotiation upon connection
if (this.gmcpHandler) { console.log('Sending GMCP negotiation');
this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP); this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP);
}
}; };
this.webSocket.onclose = () => { this.webSocket.onclose = () => {
this.connected = false; this.connected = false;
console.log(`Disconnected from ${this.host}:${this.port}`);
this.emit('disconnected'); 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 * 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'); throw new Error('Not connected to MUD server');
} }
// In simulation mode, generate appropriate responses
if (this.simulationMode) {
this.handleSimulatedCommand(text);
return;
}
if (!this.webSocket) { if (!this.webSocket) {
throw new Error('WebSocket not initialized'); 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 <message>: 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 * Disconnect from the MUD server
*/ */
public disconnect(): void { public disconnect(): void {
if (this.simulationMode) {
this.connected = false;
this.emit('disconnected');
return;
}
if (this.webSocket) { if (this.webSocket) {
this.webSocket.close(); this.webSocket.close();
this.webSocket = null; this.webSocket = null;
@@ -255,8 +193,6 @@ You are carrying:
* Handle incoming data from the MUD server * Handle incoming data from the MUD server
*/ */
private handleIncomingData(data: Uint8Array): void { private handleIncomingData(data: Uint8Array): void {
console.log(`Received ${data.length} bytes from server`);
// Quickly check if we need to handle telnet negotiation // Quickly check if we need to handle telnet negotiation
let containsIAC = false; let containsIAC = false;
for (let i = 0; i < data.length; i++) { 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 // Fast path if no IAC codes
if (!containsIAC && !this.isInIAC) { if (!containsIAC && !this.isInIAC) {
const text = new TextDecoder().decode(data); const text = new TextDecoder().decode(data);
@@ -273,12 +215,6 @@ You are carrying:
return; 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 // Process each byte in the incoming data
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const byte = data[i]; const byte = data[i];
@@ -294,7 +230,7 @@ You are carrying:
this.negotiationBuffer.length > 0 && this.negotiationBuffer.length > 0 &&
this.negotiationBuffer[this.negotiationBuffer.length - 2] === TelnetCommand.IAC) { 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 // Process the complete subnegotiation
this.handleCompleteSubnegotiation(); this.handleCompleteSubnegotiation();
@@ -309,10 +245,8 @@ You are carrying:
if (byte === TelnetCommand.SB) { if (byte === TelnetCommand.SB) {
// Start of subnegotiation // Start of subnegotiation
this.inSubnegotiation = true; this.inSubnegotiation = true;
console.log('Started subnegotiation - waiting for option code');
} else if (byte === TelnetCommand.WILL || byte === TelnetCommand.DO) { } else if (byte === TelnetCommand.WILL || byte === TelnetCommand.DO) {
// Need one more byte for option // Need one more byte for option
console.log(`Telnet WILL/DO command, waiting for option`);
} else { } else {
// Simple 3-byte command // Simple 3-byte command
this.processSimpleTelnetCommand(); this.processSimpleTelnetCommand();
@@ -329,7 +263,7 @@ You are carrying:
// Start of telnet command // Start of telnet command
this.isInIAC = true; this.isInIAC = true;
this.negotiationBuffer = [byte]; this.negotiationBuffer = [byte];
console.log('Found IAC - start of telnet command'); console.log('IAC command detected');
} else { } else {
// Normal data byte, add to buffer // Normal data byte, add to buffer
this.buffer.push(byte); this.buffer.push(byte);
@@ -353,20 +287,16 @@ You are carrying:
try { try {
const [iac, command, option] = this.negotiationBuffer; const [iac, command, option] = this.negotiationBuffer;
console.log(`Processing telnet command: IAC(${iac}) ${command} ${option}`);
// Handle specific commands // Handle specific commands
if ((command === TelnetCommand.WILL || command === TelnetCommand.DO) && option === TelnetCommand.GMCP) { 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 // Server wants to use GMCP, we'll respond with IAC DO GMCP
this.sendIAC(TelnetCommand.DO, TelnetCommand.GMCP); this.sendIAC(TelnetCommand.DO, TelnetCommand.GMCP);
// And request Core.Hello // Request GMCP capabilities
if (this.gmcpHandler) {
console.log('Requesting GMCP capabilities'); console.log('Requesting GMCP capabilities');
this.gmcpHandler.requestCapabilities(); this.gmcpHandler.requestCapabilities();
} }
}
} catch (error) { } catch (error) {
console.error('Error processing telnet command:', error); console.error('Error processing telnet command:', error);
} }
@@ -377,39 +307,31 @@ You are carrying:
*/ */
private handleCompleteSubnegotiation(): void { private handleCompleteSubnegotiation(): void {
try { 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 // Check if this is a GMCP subnegotiation
// IAC SB GMCP ... IAC SE // IAC SB GMCP ... IAC SE
// Indexes: 0 1 2 ... -2 -1 // Indexes: 0 1 2 ... -2 -1
if (this.negotiationBuffer.length >= 5 && this.negotiationBuffer[2] === TelnetCommand.GMCP) { if (this.negotiationBuffer.length >= 5 && this.negotiationBuffer[2] === TelnetCommand.GMCP) {
console.log('Processing complete GMCP subnegotiation'); console.log('Processing GMCP subnegotiation');
try { try {
// Extract the GMCP data (skip IAC SB GMCP, and the final IAC SE) // Extract the GMCP data (skip IAC SB GMCP, and the final IAC SE)
const gmcpData = this.negotiationBuffer.slice(3, -2); const gmcpData = this.negotiationBuffer.slice(3, -2);
const gmcpText = new TextDecoder().decode(new Uint8Array(gmcpData)); const gmcpText = new TextDecoder().decode(new Uint8Array(gmcpData));
console.log('RECEIVED GMCP DATA:', gmcpText); console.log(`GMCP message: ${gmcpText}`);
// Process the GMCP message // Process the GMCP message immediately
if (this.gmcpHandler) {
setTimeout(() => {
try {
console.log('Passing GMCP to handler:', gmcpText); console.log('Passing GMCP to handler:', gmcpText);
this.gmcpHandler?.handleGmcpMessage(gmcpText); this.gmcpHandler.handleGmcpMessage(gmcpText);
} catch (error) { } catch (error) {
console.error('Error in GMCP handler:', error); console.error('Error processing GMCP data:', error);
}
}, 0);
} else {
console.warn('No GMCP handler available for message:', gmcpText);
}
} catch (error) {
console.error('Error processing GMCP data:', error, 'Buffer:', this.negotiationBuffer);
} }
} else { } else {
console.log(`Non-GMCP subnegotiation complete, option: ${this.negotiationBuffer[2]}`); console.log(`Non-GMCP subnegotiation received: ${this.negotiationBuffer[2]}`);
} }
} catch (error) { } catch (error) {
console.error('Error handling subnegotiation:', error); console.error('Error handling subnegotiation:', error);
@@ -432,19 +354,12 @@ You are carrying:
* Send a GMCP message * Send a GMCP message
*/ */
public sendGmcp(module: string, data: any): void { public sendGmcp(module: string, data: any): void {
if (!this.connected) { if (!this.connected || !this.webSocket) {
return; console.log('Cannot send GMCP - not connected');
}
if (this.simulationMode) {
console.log(`GMCP send (simulated): ${module}`, data);
return;
}
if (!this.webSocket) {
return; return;
} }
console.log(`Sending GMCP: ${module}`, data);
const gmcpString = `${module} ${JSON.stringify(data)}`; const gmcpString = `${module} ${JSON.stringify(data)}`;
const gmcpData = new TextEncoder().encode(gmcpString); const gmcpData = new TextEncoder().encode(gmcpString);
@@ -460,4 +375,18 @@ You are carrying:
this.webSocket.send(telnetSequence); 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;
}
} }

View File

@@ -92,10 +92,20 @@ export class GmcpHandler extends EventEmitter {
// Find the appropriate package handler // Find the appropriate package handler
let handled = false; 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()) { for (const [packagePrefix, handler] of this.packageHandlers.entries()) {
console.log(`Checking if ${module} starts with ${packagePrefix}`);
if (module.startsWith(packagePrefix)) { if (module.startsWith(packagePrefix)) {
console.log(`Found handler for ${module}: ${packagePrefix}`);
try {
handler.handleMessage(module, data); handler.handleMessage(module, data);
console.log(`Successfully processed ${module} with handler ${packagePrefix}`);
handled = true; handled = true;
} catch (error) {
console.error(`Error in handler ${packagePrefix} for module ${module}:`, error);
}
break; break;
} }
} }

View File

@@ -29,6 +29,23 @@ export class ClientMediaPackage implements GmcpPackageHandler {
// Subscribe to global volume changes to update active sounds // Subscribe to global volume changes to update active sounds
this.setupVolumeSubscription(); 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 { handleMessage(module: string, data: any): void {
try { try {
console.log(`ClientMediaPackage handling message: ${module}`, data);
if (module === 'Client.Media.Play') { if (module === 'Client.Media.Play') {
console.log('Processing Client.Media.Play message');
this.handlePlay(data); this.handlePlay(data);
} else if (module === 'Client.Media.Stop') { } else if (module === 'Client.Media.Stop') {
console.log('Processing Client.Media.Stop message');
this.handleStop(data); this.handleStop(data);
} else {
console.log(`Unhandled Client.Media message type: ${module}`);
} }
} catch (error) { } catch (error) {
console.error('Error handling GMCP Media message:', error); console.error('Error handling GMCP Media message:', error);
@@ -169,17 +192,45 @@ export class ClientMediaPackage implements GmcpPackageHandler {
this.tagToIdsMap.get(tag)?.add(soundId); this.tagToIdsMap.get(tag)?.add(soundId);
} }
// Play the sound try {
sound.play(); // 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`);
});
// Set up cleanup when sound ends
sound.once('end', () => { sound.once('end', () => {
console.log(`Sound ${soundId} finished playing`); console.log(`Sound ${soundId} finished playing`);
this.cleanupSound(soundId); this.cleanupSound(soundId);
}); });
// Emit an event for other components sound.on('loaderror', (id, error) => {
this.emitter?.emit('playSound', fullUrl, soundVolume, !!data.loop); 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);
}
// 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) { } catch (error) {
console.error('Error in handlePlay:', error); console.error('Error in handlePlay:', error);
} }