csterm-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ import { RoomManager } from '../RoomManager.js';
2
+ import { ServerConfig } from '../types.js';
3
+ export interface GameServerConfig extends ServerConfig {
4
+ serverName: string;
5
+ publicEndpoint: string;
6
+ hubUrl?: string;
7
+ }
8
+ export declare class GameServer {
9
+ private config;
10
+ private wss;
11
+ private httpServer;
12
+ private roomManager;
13
+ private poolClient;
14
+ private socketToClientId;
15
+ constructor(config?: Partial<GameServerConfig>);
16
+ start(): void;
17
+ stop(): void;
18
+ private handleConnection;
19
+ private connectToHub;
20
+ private calculateLoad;
21
+ getRoomManager(): RoomManager;
22
+ getStats(): {
23
+ clients: number;
24
+ rooms: number;
25
+ hubConnected: boolean;
26
+ };
27
+ isHubConnected(): boolean;
28
+ private printBanner;
29
+ }
@@ -0,0 +1,185 @@
1
+ // Game Server - Handles actual game rooms and client connections
2
+ import { WebSocketServer } from 'ws';
3
+ import { createServer } from 'http';
4
+ import { RoomManager } from '../RoomManager.js';
5
+ import { PoolClient } from './PoolClient.js';
6
+ import { parseClientMessage } from '../protocol.js';
7
+ import { DEFAULT_SERVER_CONFIG } from '../types.js';
8
+ export class GameServer {
9
+ config;
10
+ wss = null;
11
+ httpServer = null;
12
+ roomManager;
13
+ poolClient = null;
14
+ socketToClientId = new WeakMap();
15
+ constructor(config = {}) {
16
+ this.config = {
17
+ ...DEFAULT_SERVER_CONFIG,
18
+ serverName: 'CS-CLI Server',
19
+ publicEndpoint: `ws://localhost:${config.port || DEFAULT_SERVER_CONFIG.port}`,
20
+ ...config,
21
+ };
22
+ this.roomManager = new RoomManager(this.config);
23
+ // Set up room event handlers for hub notifications
24
+ this.roomManager.onRoomCreated = (room) => {
25
+ this.poolClient?.notifyRoomCreated(room);
26
+ };
27
+ this.roomManager.onRoomClosed = (roomId) => {
28
+ this.poolClient?.notifyRoomClosed(roomId);
29
+ };
30
+ }
31
+ // Start the game server
32
+ start() {
33
+ this.httpServer = createServer();
34
+ this.wss = new WebSocketServer({ server: this.httpServer });
35
+ this.wss.on('connection', (socket, request) => {
36
+ this.handleConnection(socket, request);
37
+ });
38
+ this.wss.on('error', (error) => {
39
+ console.error('[GameServer] WebSocket server error:', error);
40
+ });
41
+ this.httpServer.listen(this.config.port, () => {
42
+ this.printBanner();
43
+ });
44
+ // Connect to hub if configured
45
+ if (this.config.hubUrl) {
46
+ this.connectToHub();
47
+ }
48
+ // Stats logging
49
+ setInterval(() => {
50
+ const stats = this.roomManager.getStats();
51
+ if (stats.clients > 0 || stats.rooms > 0) {
52
+ console.log(`[GameServer] Stats: ${stats.clients} clients, ${stats.rooms} rooms`);
53
+ }
54
+ }, 60000);
55
+ }
56
+ // Stop the game server
57
+ stop() {
58
+ console.log('[GameServer] Shutting down...');
59
+ // Disconnect from hub
60
+ this.poolClient?.disconnect();
61
+ // Close all client connections
62
+ if (this.wss) {
63
+ this.wss.clients.forEach((socket) => {
64
+ socket.close(1001, 'Server shutting down');
65
+ });
66
+ this.wss.close();
67
+ this.wss = null;
68
+ }
69
+ // Stop room manager
70
+ this.roomManager.shutdown();
71
+ // Close HTTP server
72
+ if (this.httpServer) {
73
+ this.httpServer.close();
74
+ this.httpServer = null;
75
+ }
76
+ }
77
+ // Handle new client connection
78
+ handleConnection(socket, request) {
79
+ const clientIp = request.socket.remoteAddress || 'unknown';
80
+ const clientId = this.roomManager.addClient(socket);
81
+ this.socketToClientId.set(socket, clientId);
82
+ console.log(`[GameServer] New connection from ${clientIp} (${clientId})`);
83
+ socket.on('message', (data, isBinary) => {
84
+ try {
85
+ // Check for binary voice frames (first byte 0x01)
86
+ if (isBinary || (data.length > 0 && data[0] === 0x01)) {
87
+ // Route to room's binary handler
88
+ this.roomManager.handleBinaryData(clientId, data);
89
+ return;
90
+ }
91
+ const message = parseClientMessage(data.toString());
92
+ if (message) {
93
+ this.roomManager.handleMessage(clientId, message);
94
+ }
95
+ else {
96
+ console.warn(`[GameServer] Invalid message from ${clientId}`);
97
+ }
98
+ }
99
+ catch (error) {
100
+ console.error(`[GameServer] Error processing message from ${clientId}:`, error);
101
+ }
102
+ });
103
+ socket.on('close', (code) => {
104
+ console.log(`[GameServer] Connection closed: ${clientId} (code: ${code})`);
105
+ this.roomManager.removeClient(clientId);
106
+ });
107
+ socket.on('error', (error) => {
108
+ console.error(`[GameServer] Socket error for ${clientId}:`, error.message);
109
+ });
110
+ // Send initial room list
111
+ socket.send(JSON.stringify({
112
+ type: 'room_list',
113
+ rooms: this.roomManager.listRooms(),
114
+ }));
115
+ }
116
+ // Connect to hub as a pool server
117
+ connectToHub() {
118
+ if (!this.config.hubUrl)
119
+ return;
120
+ console.log(`[GameServer] Connecting to hub at ${this.config.hubUrl}`);
121
+ this.poolClient = new PoolClient({
122
+ hubUrl: this.config.hubUrl,
123
+ serverName: this.config.serverName,
124
+ endpoint: this.config.publicEndpoint,
125
+ maxRooms: this.config.maxRooms,
126
+ }, {
127
+ onStatusChange: (status) => {
128
+ console.log(`[GameServer] Hub connection status: ${status}`);
129
+ },
130
+ onRegistered: (poolId) => {
131
+ console.log(`[GameServer] Registered with hub as ${poolId}`);
132
+ },
133
+ onRejected: (reason) => {
134
+ console.error(`[GameServer] Hub rejected registration: ${reason}`);
135
+ },
136
+ onError: (error) => {
137
+ console.error(`[GameServer] Hub connection error:`, error.message);
138
+ },
139
+ });
140
+ // Set up state callbacks
141
+ this.poolClient.setStateCallbacks(() => this.roomManager.listRooms(), () => this.roomManager.getStats().clients, () => this.calculateLoad());
142
+ this.poolClient.connect();
143
+ }
144
+ // Calculate server load (0-100)
145
+ calculateLoad() {
146
+ const stats = this.roomManager.getStats();
147
+ const roomLoad = (stats.rooms / this.config.maxRooms) * 100;
148
+ return Math.min(100, Math.round(roomLoad));
149
+ }
150
+ // Get room manager for external access
151
+ getRoomManager() {
152
+ return this.roomManager;
153
+ }
154
+ // Get stats
155
+ getStats() {
156
+ const stats = this.roomManager.getStats();
157
+ return {
158
+ ...stats,
159
+ hubConnected: this.poolClient?.isRegistered() ?? false,
160
+ };
161
+ }
162
+ // Check if connected to hub
163
+ isHubConnected() {
164
+ return this.poolClient?.isRegistered() ?? false;
165
+ }
166
+ // Print startup banner
167
+ printBanner() {
168
+ const hubStatus = this.config.hubUrl ? `Connecting to ${this.config.hubUrl}` : 'Standalone';
169
+ console.log(`
170
+ ╔═══════════════════════════════════════════════════╗
171
+ ║ ║
172
+ ║ CS-CLI Game Server ║
173
+ ║ ║
174
+ ║ Name: ${this.config.serverName.padEnd(40)}║
175
+ ║ Port: ${this.config.port.toString().padEnd(40)}║
176
+ ║ Endpoint: ${this.config.publicEndpoint.substring(0, 36).padEnd(36)}║
177
+ ║ Max Rooms: ${this.config.maxRooms.toString().padEnd(36)}║
178
+ ║ Hub: ${hubStatus.substring(0, 41).padEnd(41)}║
179
+ ║ ║
180
+ ║ Server is running... ║
181
+ ║ ║
182
+ ╚═══════════════════════════════════════════════════╝
183
+ `);
184
+ }
185
+ }
@@ -0,0 +1,48 @@
1
+ import { RoomInfo } from '../protocol.js';
2
+ export interface PoolClientConfig {
3
+ hubUrl: string;
4
+ serverName: string;
5
+ endpoint: string;
6
+ maxRooms: number;
7
+ reconnectInterval?: number;
8
+ heartbeatInterval?: number;
9
+ }
10
+ export type PoolClientStatus = 'disconnected' | 'connecting' | 'connected' | 'registered';
11
+ export interface PoolClientEvents {
12
+ onStatusChange?: (status: PoolClientStatus) => void;
13
+ onRegistered?: (poolId: string) => void;
14
+ onRejected?: (reason: string) => void;
15
+ onError?: (error: Error) => void;
16
+ }
17
+ export declare class PoolClient {
18
+ private config;
19
+ private events;
20
+ private ws;
21
+ private status;
22
+ private poolId;
23
+ private reconnectTimer;
24
+ private heartbeatTimer;
25
+ private getRoomsCallback;
26
+ private getPlayerCountCallback;
27
+ private getLoadCallback;
28
+ constructor(config: PoolClientConfig, events?: PoolClientEvents);
29
+ setStateCallbacks(getRooms: () => RoomInfo[], getPlayerCount: () => number, getLoad: () => number): void;
30
+ connect(): void;
31
+ disconnect(): void;
32
+ notifyRoomCreated(room: RoomInfo): void;
33
+ notifyRoomClosed(roomId: string): void;
34
+ getStatus(): PoolClientStatus;
35
+ getPoolId(): string | null;
36
+ isRegistered(): boolean;
37
+ private handleOpen;
38
+ private handleMessage;
39
+ private handleClose;
40
+ private handleError;
41
+ private send;
42
+ private startHeartbeat;
43
+ private stopHeartbeat;
44
+ private sendHeartbeat;
45
+ private scheduleReconnect;
46
+ private stopReconnect;
47
+ private setStatus;
48
+ }
@@ -0,0 +1,204 @@
1
+ // Pool Client - Connects a game server to the central hub
2
+ import WebSocket from 'ws';
3
+ import { serializePoolToHubMessage, } from '../protocol.js';
4
+ export class PoolClient {
5
+ config;
6
+ events;
7
+ ws = null;
8
+ status = 'disconnected';
9
+ poolId = null;
10
+ reconnectTimer = null;
11
+ heartbeatTimer = null;
12
+ getRoomsCallback = null;
13
+ getPlayerCountCallback = null;
14
+ getLoadCallback = null;
15
+ constructor(config, events = {}) {
16
+ this.config = {
17
+ reconnectInterval: 5000,
18
+ heartbeatInterval: 10000,
19
+ ...config,
20
+ };
21
+ this.events = events;
22
+ }
23
+ // Set callbacks for getting current state
24
+ setStateCallbacks(getRooms, getPlayerCount, getLoad) {
25
+ this.getRoomsCallback = getRooms;
26
+ this.getPlayerCountCallback = getPlayerCount;
27
+ this.getLoadCallback = getLoad;
28
+ }
29
+ // Connect to the hub
30
+ connect() {
31
+ if (this.ws) {
32
+ this.ws.close();
33
+ }
34
+ this.setStatus('connecting');
35
+ console.log(`[PoolClient] Connecting to hub at ${this.config.hubUrl}`);
36
+ this.ws = new WebSocket(this.config.hubUrl);
37
+ this.ws.on('open', () => {
38
+ this.handleOpen();
39
+ });
40
+ this.ws.on('message', (data) => {
41
+ this.handleMessage(data.toString());
42
+ });
43
+ this.ws.on('close', () => {
44
+ this.handleClose();
45
+ });
46
+ this.ws.on('error', (error) => {
47
+ this.handleError(error);
48
+ });
49
+ }
50
+ // Disconnect from the hub
51
+ disconnect() {
52
+ this.stopHeartbeat();
53
+ this.stopReconnect();
54
+ if (this.ws) {
55
+ // Send unregister message
56
+ if (this.status === 'registered') {
57
+ this.send({ type: 'pool_unregister' });
58
+ }
59
+ this.ws.close();
60
+ this.ws = null;
61
+ }
62
+ this.poolId = null;
63
+ this.setStatus('disconnected');
64
+ }
65
+ // Notify hub of a new room
66
+ notifyRoomCreated(room) {
67
+ if (this.status === 'registered') {
68
+ this.send({
69
+ type: 'pool_room_created',
70
+ room,
71
+ });
72
+ }
73
+ }
74
+ // Notify hub of a closed room
75
+ notifyRoomClosed(roomId) {
76
+ if (this.status === 'registered') {
77
+ this.send({
78
+ type: 'pool_room_closed',
79
+ roomId,
80
+ });
81
+ }
82
+ }
83
+ // Get current status
84
+ getStatus() {
85
+ return this.status;
86
+ }
87
+ // Get assigned pool ID
88
+ getPoolId() {
89
+ return this.poolId;
90
+ }
91
+ // Check if connected and registered
92
+ isRegistered() {
93
+ return this.status === 'registered';
94
+ }
95
+ // Handle WebSocket open
96
+ handleOpen() {
97
+ console.log(`[PoolClient] Connected to hub, registering...`);
98
+ this.setStatus('connected');
99
+ // Send registration
100
+ this.send({
101
+ type: 'pool_register',
102
+ serverName: this.config.serverName,
103
+ endpoint: this.config.endpoint,
104
+ maxRooms: this.config.maxRooms,
105
+ });
106
+ }
107
+ // Handle incoming message
108
+ handleMessage(data) {
109
+ try {
110
+ const msg = JSON.parse(data);
111
+ switch (msg.type) {
112
+ case 'pool_accepted':
113
+ this.poolId = msg.poolId;
114
+ this.setStatus('registered');
115
+ this.startHeartbeat();
116
+ console.log(`[PoolClient] Registered with hub as ${msg.poolId}`);
117
+ this.events.onRegistered?.(msg.poolId);
118
+ break;
119
+ case 'pool_rejected':
120
+ console.error(`[PoolClient] Registration rejected: ${msg.reason}`);
121
+ this.events.onRejected?.(msg.reason);
122
+ this.ws?.close();
123
+ break;
124
+ case 'pool_ping':
125
+ // Hub is checking if we're alive - respond with heartbeat
126
+ this.sendHeartbeat();
127
+ break;
128
+ }
129
+ }
130
+ catch (error) {
131
+ console.error(`[PoolClient] Error parsing message:`, error);
132
+ }
133
+ }
134
+ // Handle WebSocket close
135
+ handleClose() {
136
+ console.log(`[PoolClient] Disconnected from hub`);
137
+ this.stopHeartbeat();
138
+ this.ws = null;
139
+ this.poolId = null;
140
+ if (this.status !== 'disconnected') {
141
+ this.setStatus('disconnected');
142
+ this.scheduleReconnect();
143
+ }
144
+ }
145
+ // Handle WebSocket error
146
+ handleError(error) {
147
+ console.error(`[PoolClient] WebSocket error:`, error.message);
148
+ this.events.onError?.(error);
149
+ }
150
+ // Send a message to the hub
151
+ send(msg) {
152
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
153
+ this.ws.send(serializePoolToHubMessage(msg));
154
+ }
155
+ }
156
+ // Start heartbeat timer
157
+ startHeartbeat() {
158
+ this.stopHeartbeat();
159
+ this.heartbeatTimer = setInterval(() => {
160
+ this.sendHeartbeat();
161
+ }, this.config.heartbeatInterval);
162
+ }
163
+ // Stop heartbeat timer
164
+ stopHeartbeat() {
165
+ if (this.heartbeatTimer) {
166
+ clearInterval(this.heartbeatTimer);
167
+ this.heartbeatTimer = null;
168
+ }
169
+ }
170
+ // Send heartbeat with current state
171
+ sendHeartbeat() {
172
+ const rooms = this.getRoomsCallback?.() ?? [];
173
+ const playerCount = this.getPlayerCountCallback?.() ?? 0;
174
+ const load = this.getLoadCallback?.() ?? 0;
175
+ this.send({
176
+ type: 'pool_heartbeat',
177
+ rooms,
178
+ playerCount,
179
+ load,
180
+ });
181
+ }
182
+ // Schedule reconnection
183
+ scheduleReconnect() {
184
+ this.stopReconnect();
185
+ console.log(`[PoolClient] Reconnecting in ${this.config.reconnectInterval}ms...`);
186
+ this.reconnectTimer = setTimeout(() => {
187
+ this.connect();
188
+ }, this.config.reconnectInterval);
189
+ }
190
+ // Stop reconnection timer
191
+ stopReconnect() {
192
+ if (this.reconnectTimer) {
193
+ clearTimeout(this.reconnectTimer);
194
+ this.reconnectTimer = null;
195
+ }
196
+ }
197
+ // Update status and notify
198
+ setStatus(status) {
199
+ if (this.status !== status) {
200
+ this.status = status;
201
+ this.events.onStatusChange?.(status);
202
+ }
203
+ }
204
+ }