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
+91 -162
View File
@@ -1,5 +1,5 @@
import { EventEmitter } from '$lib/utils/EventEmitter';
import type { GmcpHandler } from '$lib/gmcp/GmcpHandler';
import { GmcpHandler } from '$lib/gmcp/GmcpHandler';
// IAC codes for telnet negotiation
enum TelnetCommand {
@@ -16,38 +16,77 @@ enum TelnetCommand {
interface MudConnectionOptions {
host: string;
port: number;
gmcpHandler?: GmcpHandler;
useSSL?: boolean;
id: string;
}
/**
* MudConnection - Handles a single connection to a MUD server
* Each instance has its own GMCP handler and maintains its own state
*/
export class MudConnection extends EventEmitter {
private host: string;
private port: number;
private useSSL: boolean;
private webSocket: WebSocket | null = null;
private gmcpHandler: GmcpHandler | null = null;
private gmcpHandler: GmcpHandler;
private buffer: number[] = [];
private connected: boolean = false;
private negotiationBuffer: number[] = [];
private isInIAC: boolean = false;
private inSubnegotiation: boolean = false;
private simulationMode: boolean = false; // Disable simulation mode to use real connections
public readonly id: string;
constructor(options: MudConnectionOptions) {
super();
this.host = options.host;
this.port = options.port;
this.useSSL = options.useSSL || false;
this.gmcpHandler = options.gmcpHandler || null;
this.id = options.id;
// Create GMCP handler
this.gmcpHandler = new GmcpHandler();
// Set up GMCP event forwarding
this.setupGmcpEvents();
console.log(`MudConnection created for ${this.host}:${this.port} with ID ${this.id}`);
}
/**
* Set up event forwarding from GMCP handler
*/
private setupGmcpEvents(): void {
// Forward all GMCP events to listeners of this connection
this.gmcpHandler.on('gmcp', (module, data) => {
this.emit('gmcp', module, data);
});
// Forward specific module events (like gmcp:Core.Ping)
this.gmcpHandler.on('*', (eventName, ...args) => {
if (eventName.startsWith('gmcp:')) {
this.emit(eventName, ...args);
}
});
// Handle GMCP events that need special processing
this.gmcpHandler.on('playSound', (url, volume, loop) => {
console.log(`MudConnection forwarding playSound event: ${url}`);
this.emit('playSound', { url, volume, loop });
});
// Listen for sendGmcp events from the GMCP handler
this.gmcpHandler.on('sendGmcp', (module, data) => {
this.sendGmcp(module, data);
});
}
/**
* Connect to the MUD server
*/
public connect(): void {
// For development/testing purposes, we'll use a simulated connection
if (this.simulationMode) {
this.connectSimulated();
if (this.connected) {
console.log(`Already connected to ${this.host}:${this.port}`);
return;
}
@@ -63,24 +102,24 @@ export class MudConnection extends EventEmitter {
wsUrl = `${wsProtocol}://${window.location.host}/mud-ws?host=${encodeURIComponent(this.host)}&port=${this.port}&useSSL=${this.useSSL}`;
}
console.log('Connecting to WebSocket server:', wsUrl);
console.log(`Connecting to WebSocket server: ${wsUrl}`);
this.webSocket = new WebSocket(wsUrl);
this.webSocket.binaryType = 'arraybuffer';
this.webSocket.onopen = () => {
this.connected = true;
console.log(`Connected to ${this.host}:${this.port}`);
this.emit('connected');
// Send GMCP negotiation upon connection
if (this.gmcpHandler) {
this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP);
}
console.log('Sending GMCP negotiation');
this.sendIAC(TelnetCommand.WILL, TelnetCommand.GMCP);
};
this.webSocket.onclose = () => {
this.connected = false;
console.log(`Disconnected from ${this.host}:${this.port}`);
this.emit('disconnected');
};
@@ -109,35 +148,6 @@ export class MudConnection extends EventEmitter {
};
}
/**
* Connect to a simulated MUD server for testing
*/
private connectSimulated(): void {
console.log('Using simulated MUD connection');
// Simulate connection delay
setTimeout(() => {
this.connected = true;
this.emit('connected');
// Send welcome message
const welcomeMessage = `
============================================================
| Welcome to ${this.host} |
============================================================
This is a simulated connection for testing purposes.
Commands you type will be echoed back to the terminal.
Type 'help' for a list of available commands.
Type 'look' to see the current room.
`;
setTimeout(() => {
this.emit('received', welcomeMessage);
}, 500);
}, 1000);
}
/**
* Send text to the MUD server
*/
@@ -146,12 +156,6 @@ Type 'look' to see the current room.
throw new Error('Not connected to MUD server');
}
// In simulation mode, generate appropriate responses
if (this.simulationMode) {
this.handleSimulatedCommand(text);
return;
}
if (!this.webSocket) {
throw new Error('WebSocket not initialized');
}
@@ -175,76 +179,10 @@ Type 'look' to see the current room.
}
}
/**
* Handle commands in simulation mode
*/
private handleSimulatedCommand(command: string): void {
// Emit that we sent the command
this.emit('sent', command);
// Process command with a small delay to simulate network latency
setTimeout(() => {
const cmd = command.trim().toLowerCase();
if (cmd === 'help') {
this.emit('received', `
Available commands:
- help: Show this help message
- look: Look at the current room
- say <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
*/
public disconnect(): void {
if (this.simulationMode) {
this.connected = false;
this.emit('disconnected');
return;
}
if (this.webSocket) {
this.webSocket.close();
this.webSocket = null;
@@ -255,8 +193,6 @@ You are carrying:
* Handle incoming data from the MUD server
*/
private handleIncomingData(data: Uint8Array): void {
console.log(`Received ${data.length} bytes from server`);
// Quickly check if we need to handle telnet negotiation
let containsIAC = false;
for (let i = 0; i < data.length; i++) {
@@ -266,6 +202,12 @@ You are carrying:
}
}
// Debug: Log raw data for debugging if it contains IAC
if (containsIAC) {
const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ');
console.log(`Raw data with IAC: ${hexData}`);
}
// Fast path if no IAC codes
if (!containsIAC && !this.isInIAC) {
const text = new TextDecoder().decode(data);
@@ -273,12 +215,6 @@ You are carrying:
return;
}
// Debug: Log the raw bytes if IAC is present
if (containsIAC) {
const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ');
console.log(`Data with IAC: ${hexData}`);
}
// Process each byte in the incoming data
for (let i = 0; i < data.length; i++) {
const byte = data[i];
@@ -294,7 +230,7 @@ You are carrying:
this.negotiationBuffer.length > 0 &&
this.negotiationBuffer[this.negotiationBuffer.length - 2] === TelnetCommand.IAC) {
console.log('FOUND IAC SE SEQUENCE - End of subnegotiation');
console.log('End of subnegotiation found');
// Process the complete subnegotiation
this.handleCompleteSubnegotiation();
@@ -309,10 +245,8 @@ You are carrying:
if (byte === TelnetCommand.SB) {
// Start of subnegotiation
this.inSubnegotiation = true;
console.log('Started subnegotiation - waiting for option code');
} else if (byte === TelnetCommand.WILL || byte === TelnetCommand.DO) {
// Need one more byte for option
console.log(`Telnet WILL/DO command, waiting for option`);
} else {
// Simple 3-byte command
this.processSimpleTelnetCommand();
@@ -329,7 +263,7 @@ You are carrying:
// Start of telnet command
this.isInIAC = true;
this.negotiationBuffer = [byte];
console.log('Found IAC - start of telnet command');
console.log('IAC command detected');
} else {
// Normal data byte, add to buffer
this.buffer.push(byte);
@@ -353,19 +287,15 @@ You are carrying:
try {
const [iac, command, option] = this.negotiationBuffer;
console.log(`Processing telnet command: IAC(${iac}) ${command} ${option}`);
// Handle specific commands
if ((command === TelnetCommand.WILL || command === TelnetCommand.DO) && option === TelnetCommand.GMCP) {
console.log('Server indicates WILL/DO GMCP, responding with DO GMCP');
console.log('Server supports GMCP, responding with DO GMCP');
// Server wants to use GMCP, we'll respond with IAC DO GMCP
this.sendIAC(TelnetCommand.DO, TelnetCommand.GMCP);
// And request Core.Hello
if (this.gmcpHandler) {
console.log('Requesting GMCP capabilities');
this.gmcpHandler.requestCapabilities();
}
// Request GMCP capabilities
console.log('Requesting GMCP capabilities');
this.gmcpHandler.requestCapabilities();
}
} catch (error) {
console.error('Error processing telnet command:', error);
@@ -377,39 +307,31 @@ You are carrying:
*/
private handleCompleteSubnegotiation(): void {
try {
console.log(`Handling subnegotiation, buffer length: ${this.negotiationBuffer.length}`);
// Debug buffer contents
const bufferHex = this.negotiationBuffer.map(b => b.toString(16).padStart(2, '0')).join(' ');
console.log(`Processing subnegotiation, buffer: ${bufferHex}`);
// Check if this is a GMCP subnegotiation
// IAC SB GMCP ... IAC SE
// Indexes: 0 1 2 ... -2 -1
if (this.negotiationBuffer.length >= 5 && this.negotiationBuffer[2] === TelnetCommand.GMCP) {
console.log('Processing complete GMCP subnegotiation');
console.log('Processing GMCP subnegotiation');
try {
// Extract the GMCP data (skip IAC SB GMCP, and the final IAC SE)
const gmcpData = this.negotiationBuffer.slice(3, -2);
const gmcpText = new TextDecoder().decode(new Uint8Array(gmcpData));
console.log('RECEIVED GMCP DATA:', gmcpText);
console.log(`GMCP message: ${gmcpText}`);
// Process the GMCP message
if (this.gmcpHandler) {
setTimeout(() => {
try {
console.log('Passing GMCP to handler:', gmcpText);
this.gmcpHandler?.handleGmcpMessage(gmcpText);
} catch (error) {
console.error('Error in GMCP handler:', error);
}
}, 0);
} else {
console.warn('No GMCP handler available for message:', gmcpText);
}
// Process the GMCP message immediately
console.log('Passing GMCP to handler:', gmcpText);
this.gmcpHandler.handleGmcpMessage(gmcpText);
} catch (error) {
console.error('Error processing GMCP data:', error, 'Buffer:', this.negotiationBuffer);
console.error('Error processing GMCP data:', error);
}
} else {
console.log(`Non-GMCP subnegotiation complete, option: ${this.negotiationBuffer[2]}`);
console.log(`Non-GMCP subnegotiation received: ${this.negotiationBuffer[2]}`);
}
} catch (error) {
console.error('Error handling subnegotiation:', error);
@@ -432,19 +354,12 @@ You are carrying:
* Send a GMCP message
*/
public sendGmcp(module: string, data: any): void {
if (!this.connected) {
return;
}
if (this.simulationMode) {
console.log(`GMCP send (simulated): ${module}`, data);
return;
}
if (!this.webSocket) {
if (!this.connected || !this.webSocket) {
console.log('Cannot send GMCP - not connected');
return;
}
console.log(`Sending GMCP: ${module}`, data);
const gmcpString = `${module} ${JSON.stringify(data)}`;
const gmcpData = new TextEncoder().encode(gmcpString);
@@ -460,4 +375,18 @@ You are carrying:
this.webSocket.send(telnetSequence);
}
/**
* Get the GMCP handler associated with this connection
*/
public getGmcpHandler(): GmcpHandler {
return this.gmcpHandler;
}
/**
* Check if the connection is active
*/
public isConnected(): boolean {
return this.connected;
}
}