Skip to content

Commit 7fca80a

Browse files
authored
Merge pull request #131 from neph1/copilot/remove-old-eventsource-api
Remove EventSource-based WSGI API from IF mode
2 parents 112d6e0 + 998ad09 commit 7fca80a

File tree

11 files changed

+366
-1581
lines changed

11 files changed

+366
-1581
lines changed

WEBSOCKET.md

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,41 @@
22

33
## Overview
44

5-
LlamaTale now supports WebSocket connections for the web browser interface, providing a modern bidirectional communication channel between the client and server. This is an alternative to the traditional Server-Sent Events (EventSource) approach.
5+
LlamaTale uses WebSocket connections for the web browser interface in both single-player (IF) mode and multi-player (MUD) mode, providing a modern bidirectional communication channel between the client and server.
66

77
## Features
88

99
- **Bidirectional Communication**: WebSocket enables real-time, two-way communication between the browser and server
10-
- **Reduced Latency**: Direct WebSocket communication can be faster than HTTP polling or EventSource
10+
- **Reduced Latency**: Direct WebSocket communication is faster than HTTP polling or EventSource
1111
- **Modern Stack**: Uses FastAPI and uvicorn for a modern, async Python web framework
12-
- **Backward Compatibility**: The JavaScript client automatically falls back to EventSource if WebSocket is not available
12+
- **Unified Approach**: Both IF and MUD modes now use the same WebSocket-based architecture
1313

1414
## Requirements
1515

16-
Install the additional dependencies:
16+
Install the required dependencies:
1717

1818
```bash
1919
pip install fastapi websockets uvicorn
2020
```
2121

22-
Or install all requirements including WebSocket support:
22+
Or install all requirements:
2323

2424
```bash
2525
pip install -r requirements.txt
2626
```
2727

2828
## Usage
2929

30-
### Starting a Game with WebSocket Support
31-
32-
To enable WebSocket mode, use the `--websocket` flag when starting a game with the web interface:
30+
### Starting a Single-Player Game
3331

34-
```bash
35-
python -m tale.main --game stories/dungeon --web --websocket
36-
```
37-
38-
### Command-Line Arguments
39-
40-
- `--web`: Enable web browser interface
41-
- `--websocket`: Use WebSocket instead of EventSource (requires `--web`)
42-
43-
### Example Commands
44-
45-
**Standard EventSource mode (default):**
4632
```bash
4733
python -m tale.main --game stories/dungeon --web
4834
```
4935

50-
**WebSocket mode:**
36+
### Starting a Multi-Player (MUD) Game
37+
5138
```bash
52-
python -m tale.main --game stories/dungeon --web --websocket
39+
python -m tale.main --game stories/dungeon --mode mud
5340
```
5441

5542
## Architecture
@@ -58,7 +45,8 @@ python -m tale.main --game stories/dungeon --web --websocket
5845

5946
The WebSocket implementation uses FastAPI and includes:
6047

61-
- **TaleFastAPIApp**: Main FastAPI application with WebSocket endpoint
48+
- **TaleFastAPIApp** (IF mode): FastAPI application for single-player with WebSocket endpoint
49+
- **TaleMudFastAPIApp** (MUD mode): FastAPI application for multi-player with session management
6250
- **WebSocket Endpoint** (`/tale/ws`): Handles bidirectional communication
6351
- **HTTP Routes**: Serves static files and HTML pages
6452
- **Message Protocol**: JSON-based messages for commands and responses
@@ -67,7 +55,7 @@ The WebSocket implementation uses FastAPI and includes:
6755

6856
The JavaScript client (`script.js`) includes:
6957

70-
- **Automatic Detection**: Tries WebSocket first, falls back to EventSource
58+
- **WebSocket Connection**: Connects to the WebSocket endpoint
7159
- **Message Handling**: Processes incoming text, data, and status messages
7260
- **Command Sending**: Sends commands and autocomplete requests via WebSocket
7361

@@ -109,48 +97,44 @@ The JavaScript client (`script.js`) includes:
10997
### Key Components
11098

11199
1. **`tale/tio/if_browser_io.py`**:
112-
- `TaleFastAPIApp`: FastAPI application with WebSocket support
113-
- `HttpIo`: Updated to support both WSGI and FastAPI modes
114-
115-
2. **`tale/driver_if.py`**:
116-
- `IFDriver`: Updated constructor with `use_websocket` parameter
117-
- `connect_player()`: Creates FastAPI server when WebSocket mode is enabled
100+
- `TaleFastAPIApp`: FastAPI application for single-player mode
101+
- `HttpIo`: I/O adapter for the FastAPI web server
118102

119-
3. **`tale/web/script.js`**:
120-
- `tryWebSocket()`: Attempts WebSocket connection
121-
- `setupEventSource()`: Fallback to EventSource
122-
- `send_cmd()`: Sends commands via WebSocket or AJAX
103+
2. **`tale/tio/mud_browser_io.py`**:
104+
- `TaleMudFastAPIApp`: FastAPI application for multi-player mode with session management
105+
- `MudHttpIo`: I/O adapter for multi-player browser interface
123106

124-
4. **`tale/main.py`**:
125-
- Added `--websocket` command-line argument
107+
3. **`tale/driver_if.py`**:
108+
- `IFDriver`: Creates FastAPI server for IF web interface
126109

127-
### Limitations
110+
4. **`tale/driver_mud.py`**:
111+
- `MudDriver`: Creates FastAPI server for MUD web interface
128112

129-
- WebSocket mode is currently only supported in single-player (IF) mode
130-
- SSL/TLS configuration may require additional setup for WebSocket secure connections
131-
- The implementation maintains backward compatibility with the original WSGI-based approach
113+
5. **`tale/web/script.js`**:
114+
- `connectWebSocket()`: Establishes WebSocket connection
115+
- `send_cmd()`: Sends commands via WebSocket
132116

133117
## Troubleshooting
134118

135119
### WebSocket Connection Fails
136120

137-
If the WebSocket connection fails, the client will automatically fall back to EventSource. Check:
121+
If the WebSocket connection fails:
138122

139-
1. FastAPI and uvicorn are installed
140-
2. Port is not blocked by firewall
141-
3. Browser console for error messages
123+
1. Ensure FastAPI and uvicorn are installed
124+
2. Check that the port is not blocked by firewall
125+
3. Check browser console for error messages
142126

143127
### Module Not Found Errors
144128

145129
Ensure all dependencies are installed:
146130

147131
```bash
148-
pip install -r requirements.txt
132+
pip install fastapi websockets uvicorn
149133
```
150134

151135
### ImportError for FastAPI
152136

153-
If FastAPI is not available, the system will fall back to the traditional WSGI server. Install FastAPI to enable WebSocket support:
137+
If FastAPI is not available, an error will be raised when starting with web interface. Install FastAPI:
154138

155139
```bash
156140
pip install fastapi websockets uvicorn
@@ -160,7 +144,6 @@ pip install fastapi websockets uvicorn
160144

161145
Possible improvements for the WebSocket implementation:
162146

163-
- Multi-player (MUD) mode support
164147
- Compression for large text outputs
165148
- Reconnection handling with session persistence
166149
- WebSocket authentication and security enhancements

tale/driver_if.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,13 @@ class IFDriver(driver.Driver):
3030
The Single user 'driver'.
3131
Used to control interactive fiction where there's only one 'player'.
3232
"""
33-
def __init__(self, *, screen_delay: int=DEFAULT_SCREEN_DELAY, gui: bool=False, web: bool=False, wizard_override: bool=False, character_to_load: str='', use_websocket: bool=False) -> None:
33+
def __init__(self, *, screen_delay: int=DEFAULT_SCREEN_DELAY, gui: bool=False, web: bool=False, wizard_override: bool=False, character_to_load: str='') -> None:
3434
super().__init__()
3535
self.game_mode = GameMode.IF
3636
if screen_delay < 0 or screen_delay > 100:
3737
raise ValueError("invalid delay, valid range is 0-100")
3838
self.screen_delay = screen_delay
3939
self.io_type = "console"
40-
self.use_websocket = use_websocket # Store WebSocket preference
4140
if gui:
4241
self.io_type = "gui"
4342
if web:
@@ -115,28 +114,16 @@ def connect_player(self, player_io_type: str, line_delay: int) -> PlayerConnecti
115114
from .tio.tkinter_io import TkinterIo
116115
io = TkinterIo(self.story.config, connection) # type: iobase.IoAdapterBase
117116
elif player_io_type == "web":
118-
if self.use_websocket:
119-
# Use FastAPI with WebSocket support
120-
from .tio.if_browser_io import FASTAPI_AVAILABLE
121-
if not FASTAPI_AVAILABLE:
122-
raise RuntimeError("FastAPI is not available. Install it with: pip install fastapi websockets uvicorn")
123-
from .tio.if_browser_io import HttpIo, TaleFastAPIApp
124-
fastapi_server = TaleFastAPIApp.create_app_server(self, connection, use_ssl=False, ssl_certs=None)
125-
# you can enable SSL by using the following:
126-
# fastapi_server = TaleFastAPIApp.create_app_server(self, connection, use_ssl=True,
127-
# ssl_certs=("certs/localhost_cert.pem", "certs/localhost_key.pem", ""))
128-
io = HttpIo(connection, fastapi_server)
129-
io.fastapi_mode = True # Mark as FastAPI mode
130-
io.fastapi_server = fastapi_server # Store reference
131-
else:
132-
# Use traditional WSGI server
133-
from .tio.if_browser_io import HttpIo, TaleWsgiApp
134-
wsgi_server = TaleWsgiApp.create_app_server(self, connection, use_ssl=False, ssl_certs=None)
135-
# you can enable SSL by using the following:
136-
# wsgi_server = TaleWsgiApp.create_app_server(self, connection, use_ssl=True,
137-
# ssl_certs=("certs/localhost_cert.pem", "certs/localhost_key.pem", ""))
138-
io = HttpIo(connection, wsgi_server)
139-
io.fastapi_mode = False
117+
# Use FastAPI with WebSocket support
118+
from .tio.if_browser_io import FASTAPI_AVAILABLE
119+
if not FASTAPI_AVAILABLE:
120+
raise RuntimeError("FastAPI is not available. Install it with: pip install fastapi websockets uvicorn")
121+
from .tio.if_browser_io import HttpIo, TaleFastAPIApp
122+
fastapi_server = TaleFastAPIApp.create_app_server(self, connection, use_ssl=False, ssl_certs=None)
123+
# you can enable SSL by using the following:
124+
# fastapi_server = TaleFastAPIApp.create_app_server(self, connection, use_ssl=True,
125+
# ssl_certs=("certs/localhost_cert.pem", "certs/localhost_key.pem", ""))
126+
io = HttpIo(connection, fastapi_server)
140127
elif player_io_type == "console":
141128
from .tio.console_io import ConsoleIo
142129
io = ConsoleIo(connection)

tale/driver_mud.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77

88
import time
9-
import socket
109
import threading
1110
from typing import Union, Generator, Dict, Tuple, Optional, Any
1211

@@ -20,7 +19,7 @@
2019
from . import pubsub
2120
from . import util
2221
from .player import PlayerConnection, Player
23-
from .tio.mud_browser_io import TaleMudWsgiApp
22+
from .tio.mud_browser_io import TaleMudFastAPIApp
2423

2524

2625
class MudDriver(driver.Driver):
@@ -35,28 +34,24 @@ def __init__(self, restricted=False) -> None:
3534
self.mud_accounts = None # type: accounts.MudAccounts
3635

3736
def start_main_loop(self):
38-
# Driver runs as main thread, wsgi webserver runs in background thread
37+
# Driver runs as main thread, FastAPI webserver runs in background thread
3938
accounts_db_file = self.user_resources.validate_path("useraccounts.sqlite")
4039
self.mud_accounts = accounts.MudAccounts(accounts_db_file)
4140
base._limbo.init_inventory([LimboReaper()]) # add the grim reaper to Limbo
42-
wsgi_server = TaleMudWsgiApp.create_app_server(self, use_ssl=False, ssl_certs=None) # you can enable SSL here
43-
wsgi_thread = threading.Thread(name="wsgi", target=wsgi_server.serve_forever)
44-
wsgi_thread.daemon = True
45-
wsgi_thread.start()
41+
fastapi_server = TaleMudFastAPIApp.create_app_server(self, use_ssl=False, ssl_certs=None) # you can enable SSL here
42+
fastapi_thread = threading.Thread(name="fastapi", target=fastapi_server.run,
43+
args=(self.story.config.mud_host, self.story.config.mud_port))
44+
fastapi_thread.daemon = True
45+
fastapi_thread.start()
4646
self.print_game_intro(None)
4747
if self.restricted:
4848
print("\n* Restricted mode: no new players allowed *\n")
49-
protocol = "https" if wsgi_server.use_ssl else "http"
50-
if wsgi_server.address_family == socket.AF_INET6:
51-
hostname, port, _, _ = wsgi_server.server_address
52-
if hostname[0] != '[':
53-
hostname = '[' + hostname + ']'
54-
print("Access the game on this web server url (ipv6): %s://%s:%d/tale/" % (protocol, hostname, port), end="\n\n")
55-
else:
56-
hostname, port = wsgi_server.server_address
57-
if hostname.startswith("127.0"):
58-
hostname = "localhost"
59-
print("Access the game on this web server url (ipv4): %s://%s:%d/tale/" % (protocol, hostname, port), end="\n\n")
49+
protocol = "https" if fastapi_server.use_ssl else "http"
50+
hostname = self.story.config.mud_host
51+
port = self.story.config.mud_port
52+
if hostname.startswith("127.0"):
53+
hostname = "localhost"
54+
print("Access the game on this web server url (WebSocket): %s://%s:%d/tale/" % (protocol, hostname, port), end="\n\n")
6055
self._main_loop_wrapper(None) # this doesn't return!
6156

6257
def show_motd(self, player: Player, notify_no_motd: bool=False) -> None:

tale/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def run_from_cmdline(cmdline: Sequence[str]) -> None:
2727
default=DEFAULT_SCREEN_DELAY)
2828
parser.add_argument('-m', '--mode', type=str, help='game mode, default=if', default="if", choices=["if", "mud"])
2929
parser.add_argument('-i', '--gui', help='gui interface', action='store_true')
30-
parser.add_argument('-w', '--web', help='web browser interface', action='store_true')
31-
parser.add_argument('--websocket', help='use WebSocket instead of EventSource for web interface (requires FastAPI)', action='store_true')
30+
parser.add_argument('-w', '--web', help='web browser interface (uses WebSocket)', action='store_true')
3231
parser.add_argument('-r', '--restricted', help='restricted mud mode; do not allow new players', action='store_true')
3332
parser.add_argument('-z', '--wizard', help='force wizard mode on if story character (for debug purposes)', action='store_true')
3433
parser.add_argument('-c', '--character', help='load a v2 character card as player (skips character builder)')
@@ -38,7 +37,7 @@ def run_from_cmdline(cmdline: Sequence[str]) -> None:
3837
game_mode = GameMode(args.mode)
3938
if game_mode == GameMode.IF:
4039
from .driver_if import IFDriver
41-
driver = IFDriver(screen_delay=args.delay, gui=args.gui, web=args.web, wizard_override=args.wizard, character_to_load=args.character, use_websocket=args.websocket) # type: Driver
40+
driver = IFDriver(screen_delay=args.delay, gui=args.gui, web=args.web, wizard_override=args.wizard, character_to_load=args.character) # type: Driver
4241
elif game_mode == GameMode.MUD:
4342
from .driver_mud import MudDriver
4443
driver = MudDriver(args.restricted)

0 commit comments

Comments
 (0)