Fix GMCP
This commit is contained in:
@@ -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,195 +92,128 @@
|
|||||||
// 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) {
|
||||||
|
console.log(`Found existing connection for ${profileId}`);
|
||||||
|
connection = existingConnection;
|
||||||
|
setupConnectionListeners(connection);
|
||||||
|
}
|
||||||
|
|
||||||
// Get or create connection from ConnectionManager
|
// Subscribe to connections to get updates
|
||||||
updateConnectionFromStore();
|
const unsubscribe = connections.subscribe(conns => {
|
||||||
}
|
const conn = conns[profileId];
|
||||||
|
if (conn && conn !== connection) {
|
||||||
|
console.log(`Connection updated for ${profileId}`);
|
||||||
|
|
||||||
/**
|
// Clean up old connection listeners
|
||||||
* Update local connection reference from store
|
if (connection) {
|
||||||
*/
|
removeConnectionListeners(connection);
|
||||||
function updateConnectionFromStore() {
|
}
|
||||||
// Subscribe to activeConnections to get updates
|
|
||||||
const unsubscribe = activeConnections.subscribe(connections => {
|
|
||||||
connection = connections[profileId] || null;
|
|
||||||
|
|
||||||
if (connection) {
|
// Set new connection and listeners
|
||||||
// Re-attach event listeners when connection changes
|
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() {
|
||||||
|
// Don't connect if already connected
|
||||||
|
if (connection && connection.isConnected()) {
|
||||||
|
console.log(`Already connected for profile ${profileId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the profile
|
||||||
|
const allProfiles = get(profiles);
|
||||||
|
const profile = allProfiles.find(p => p.id === profileId);
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
addToOutputHistory(`Error: Profile ${profileId} not found.`);
|
||||||
|
console.error(`Profile ${profileId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connection status
|
||||||
|
connectionStatus.update(statuses => ({
|
||||||
|
...statuses,
|
||||||
|
[profileId]: 'connecting'
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (get(activeProfileId) === profileId) {
|
||||||
|
addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect using the connection manager
|
||||||
try {
|
try {
|
||||||
// First check if we are already connected
|
|
||||||
const currentStatus = get(connectionStatus)[profileId];
|
|
||||||
if (currentStatus === 'connected' || currentStatus === 'connecting') {
|
|
||||||
console.log(`Already ${currentStatus} for profile ${profileId}, not connecting again`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the profile by ID - don't rely on activeProfile
|
|
||||||
const allProfiles = get(profiles);
|
|
||||||
const profile = allProfiles.find(p => p.id === profileId);
|
|
||||||
|
|
||||||
console.log(`Connecting to profile ${profileId}:`, profile);
|
|
||||||
|
|
||||||
if (!profile) {
|
|
||||||
addToOutputHistory(`Error: Profile ${profileId} not found.`);
|
|
||||||
console.error(`Profile ${profileId} not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update connection status
|
|
||||||
connectionStatus.update(statuses => {
|
|
||||||
console.log(`Setting connection status for ${profileId} to 'connecting'`);
|
|
||||||
return {
|
|
||||||
...statuses,
|
|
||||||
[profileId]: 'connecting'
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only add to output history if this is the active profile
|
|
||||||
if (get(activeProfileId) === profileId) {
|
|
||||||
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
|
|
||||||
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 => ({
|
||||||
|
...statuses,
|
||||||
|
[profileId]: 'error'
|
||||||
|
}));
|
||||||
|
|
||||||
connectionStatus.update(statuses => {
|
|
||||||
return {
|
|
||||||
...statuses,
|
|
||||||
[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,53 +222,29 @@
|
|||||||
* 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
|
// Only add to output history if this is the active profile
|
||||||
connectionStatus.update(statuses => {
|
if (get(activeProfileId) === profileId) {
|
||||||
console.log(`Setting connection status for ${profileId} to 'connected'`);
|
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`);
|
||||||
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
|
|
||||||
if (get(activeProfileId) === profileId) {
|
|
||||||
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle auto-login if enabled
|
// Handle auto-login if enabled
|
||||||
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]}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 => ({
|
||||||
|
|||||||
@@ -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,19 +287,15 @@ 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) {
|
console.log('Passing GMCP to handler:', gmcpText);
|
||||||
setTimeout(() => {
|
this.gmcpHandler.handleGmcpMessage(gmcpText);
|
||||||
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);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing GMCP data:', error, 'Buffer:', this.negotiationBuffer);
|
console.error('Error processing GMCP data:', error);
|
||||||
}
|
}
|
||||||
} 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)) {
|
||||||
handler.handleMessage(module, data);
|
console.log(`Found handler for ${module}: ${packagePrefix}`);
|
||||||
handled = true;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|
||||||
// Set up cleanup when sound ends
|
// Register event handlers for debugging
|
||||||
sound.once('end', () => {
|
sound.once('load', () => {
|
||||||
console.log(`Sound ${soundId} finished playing`);
|
console.log(`Sound ${soundId} loaded successfully from ${fullUrl}`);
|
||||||
this.cleanupSound(soundId);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Emit an event for other components
|
sound.once('play', () => {
|
||||||
this.emitter?.emit('playSound', fullUrl, soundVolume, !!data.loop);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user