create-esengine-server 1.1.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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +511 -0
  3. package/package.json +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ESEngine Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,511 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+
5
+ // src/index.ts
6
+ import { Command } from "commander";
7
+ import prompts from "prompts";
8
+ import chalk from "chalk";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import { execSync } from "child_process";
12
+ import { fileURLToPath } from "url";
13
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ var VERSION = "1.0.0";
15
+ function printLogo() {
16
+ console.log();
17
+ console.log(chalk.cyan(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
18
+ console.log(chalk.cyan(" \u2502 \u2502"));
19
+ console.log(chalk.cyan(" \u2502 ") + chalk.bold.white("Create ESEngine Server") + chalk.cyan(" \u2502"));
20
+ console.log(chalk.cyan(" \u2502 \u2502"));
21
+ console.log(chalk.cyan(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
22
+ console.log();
23
+ }
24
+ __name(printLogo, "printLogo");
25
+ function detectPackageManager() {
26
+ const userAgent = process.env.npm_config_user_agent || "";
27
+ if (userAgent.includes("pnpm")) return "pnpm";
28
+ if (userAgent.includes("yarn")) return "yarn";
29
+ return "npm";
30
+ }
31
+ __name(detectPackageManager, "detectPackageManager");
32
+ function getInstallCommand(pm) {
33
+ return pm === "yarn" ? "yarn" : `${pm} install`;
34
+ }
35
+ __name(getInstallCommand, "getInstallCommand");
36
+ function writeFile(projectPath, relativePath, content) {
37
+ const fullPath = path.join(projectPath, relativePath);
38
+ fs.mkdirSync(path.dirname(fullPath), {
39
+ recursive: true
40
+ });
41
+ fs.writeFileSync(fullPath, content);
42
+ }
43
+ __name(writeFile, "writeFile");
44
+ function generateProject(projectPath, projectName) {
45
+ const packageJson = {
46
+ name: projectName,
47
+ version: "1.0.0",
48
+ type: "module",
49
+ private: true,
50
+ scripts: {
51
+ dev: "tsx watch src/server/main.ts",
52
+ start: "tsx src/server/main.ts",
53
+ build: "tsc",
54
+ "build:start": "tsc && node dist/server/main.js"
55
+ },
56
+ dependencies: {
57
+ "@esengine/server": "latest",
58
+ "@esengine/rpc": "latest"
59
+ },
60
+ devDependencies: {
61
+ "@types/node": "^20.0.0",
62
+ tsx: "^4.0.0",
63
+ typescript: "^5.0.0"
64
+ }
65
+ };
66
+ writeFile(projectPath, "package.json", JSON.stringify(packageJson, null, 2));
67
+ const tsconfig = {
68
+ compilerOptions: {
69
+ target: "ES2022",
70
+ module: "NodeNext",
71
+ moduleResolution: "NodeNext",
72
+ lib: [
73
+ "ES2022"
74
+ ],
75
+ outDir: "./dist",
76
+ rootDir: "./src",
77
+ strict: true,
78
+ esModuleInterop: true,
79
+ skipLibCheck: true,
80
+ forceConsistentCasingInFileNames: true,
81
+ declaration: true,
82
+ sourceMap: true,
83
+ experimentalDecorators: true,
84
+ emitDecoratorMetadata: true
85
+ },
86
+ include: [
87
+ "src/**/*"
88
+ ],
89
+ exclude: [
90
+ "node_modules",
91
+ "dist"
92
+ ]
93
+ };
94
+ writeFile(projectPath, "tsconfig.json", JSON.stringify(tsconfig, null, 2));
95
+ const protocolTs = `/**
96
+ * \u6E38\u620F\u534F\u8BAE\u5B9A\u4E49
97
+ * Game Protocol Definition
98
+ *
99
+ * \u8FD9\u4E2A\u6587\u4EF6\u5B9A\u4E49\u4E86\u5BA2\u6237\u7AEF\u548C\u670D\u52A1\u7AEF\u5171\u4EAB\u7684\u534F\u8BAE\u7C7B\u578B
100
+ * This file defines protocol types shared between client and server
101
+ */
102
+
103
+ // ============================================================================
104
+ // \u623F\u95F4 API | Room API
105
+ // ============================================================================
106
+
107
+ /** \u52A0\u5165\u623F\u95F4\u8BF7\u6C42 | Join room request */
108
+ export interface JoinRoomReq {
109
+ roomType: string
110
+ playerName: string
111
+ options?: Record<string, unknown>
112
+ }
113
+
114
+ /** \u52A0\u5165\u623F\u95F4\u54CD\u5E94 | Join room response */
115
+ export interface JoinRoomRes {
116
+ roomId: string
117
+ playerId: string
118
+ }
119
+
120
+ // ============================================================================
121
+ // \u6E38\u620F\u6D88\u606F | Game Messages
122
+ // ============================================================================
123
+
124
+ /** \u79FB\u52A8\u6D88\u606F | Move message */
125
+ export interface MsgMove {
126
+ x: number
127
+ y: number
128
+ }
129
+
130
+ /** \u804A\u5929\u6D88\u606F | Chat message */
131
+ export interface MsgChat {
132
+ text: string
133
+ }
134
+
135
+ // ============================================================================
136
+ // \u670D\u52A1\u7AEF\u5E7F\u64AD | Server Broadcasts
137
+ // ============================================================================
138
+
139
+ /** \u73A9\u5BB6\u52A0\u5165\u5E7F\u64AD | Player joined broadcast */
140
+ export interface BroadcastJoined {
141
+ playerId: string
142
+ playerName: string
143
+ }
144
+
145
+ /** \u73A9\u5BB6\u79BB\u5F00\u5E7F\u64AD | Player left broadcast */
146
+ export interface BroadcastLeft {
147
+ playerId: string
148
+ }
149
+
150
+ /** \u72B6\u6001\u540C\u6B65\u5E7F\u64AD | State sync broadcast */
151
+ export interface BroadcastSync {
152
+ players: PlayerState[]
153
+ }
154
+
155
+ // ============================================================================
156
+ // \u5171\u4EAB\u7C7B\u578B | Shared Types
157
+ // ============================================================================
158
+
159
+ /** \u73A9\u5BB6\u72B6\u6001 | Player state */
160
+ export interface PlayerState {
161
+ id: string
162
+ name: string
163
+ x: number
164
+ y: number
165
+ }
166
+ `;
167
+ writeFile(projectPath, "src/shared/protocol.ts", protocolTs);
168
+ const sharedIndexTs = `export * from './protocol.js'
169
+ `;
170
+ writeFile(projectPath, "src/shared/index.ts", sharedIndexTs);
171
+ const serverMainTs = `import { createServer } from '@esengine/server'
172
+ import { GameRoom } from './rooms/GameRoom.js'
173
+
174
+ const PORT = Number(process.env.PORT) || 3000
175
+
176
+ async function main() {
177
+ const server = await createServer({
178
+ port: PORT,
179
+ onConnect(conn) {
180
+ console.log('[Server] Client connected:', conn.id)
181
+ },
182
+ onDisconnect(conn) {
183
+ console.log('[Server] Client disconnected:', conn.id)
184
+ },
185
+ })
186
+
187
+ // \u6CE8\u518C\u623F\u95F4\u7C7B\u578B
188
+ server.define('game', GameRoom)
189
+
190
+ await server.start()
191
+
192
+ console.log('========================================')
193
+ console.log(' ${projectName}')
194
+ console.log('========================================')
195
+ console.log(\` WebSocket: ws://localhost:\${PORT}\`)
196
+ console.log(' Room type: "game"')
197
+ console.log(' Press Ctrl+C to stop')
198
+ console.log('========================================')
199
+ }
200
+
201
+ process.on('SIGINT', () => {
202
+ console.log('\\nShutting down...')
203
+ process.exit(0)
204
+ })
205
+
206
+ main().catch(console.error)
207
+ `;
208
+ writeFile(projectPath, "src/server/main.ts", serverMainTs);
209
+ const gameRoomTs = `import { Room, Player, onMessage } from '@esengine/server'
210
+ import type {
211
+ MsgMove,
212
+ MsgChat,
213
+ PlayerState,
214
+ BroadcastSync,
215
+ BroadcastJoined,
216
+ BroadcastLeft,
217
+ } from '../../shared/index.js'
218
+
219
+ /** \u73A9\u5BB6\u6570\u636E | Player data */
220
+ interface PlayerData {
221
+ name: string
222
+ x: number
223
+ y: number
224
+ }
225
+
226
+ /**
227
+ * \u6E38\u620F\u623F\u95F4
228
+ * Game Room
229
+ */
230
+ export class GameRoom extends Room<{ players: PlayerState[] }, PlayerData> {
231
+ // \u914D\u7F6E
232
+ maxPlayers = 8
233
+ tickRate = 20
234
+
235
+ // \u72B6\u6001
236
+ state = {
237
+ players: [] as PlayerState[],
238
+ }
239
+
240
+ // ========================================================================
241
+ // \u751F\u547D\u5468\u671F | Lifecycle
242
+ // ========================================================================
243
+
244
+ onCreate() {
245
+ console.log(\`[GameRoom] Room \${this.id} created\`)
246
+ }
247
+
248
+ onJoin(player: Player<PlayerData>) {
249
+ // \u521D\u59CB\u5316\u73A9\u5BB6\u6570\u636E
250
+ player.data.name = 'Player_' + player.id.slice(-4)
251
+ player.data.x = Math.random() * 800
252
+ player.data.y = Math.random() * 600
253
+
254
+ // \u6DFB\u52A0\u5230\u72B6\u6001
255
+ this.state.players.push({
256
+ id: player.id,
257
+ name: player.data.name,
258
+ x: player.data.x,
259
+ y: player.data.y,
260
+ })
261
+
262
+ // \u5E7F\u64AD\u73A9\u5BB6\u52A0\u5165
263
+ this.broadcast<BroadcastJoined>('Joined', {
264
+ playerId: player.id,
265
+ playerName: player.data.name,
266
+ })
267
+
268
+ console.log(\`[GameRoom] \${player.data.name} joined room \${this.id}\`)
269
+ }
270
+
271
+ onLeave(player: Player<PlayerData>) {
272
+ // \u4ECE\u72B6\u6001\u79FB\u9664
273
+ this.state.players = this.state.players.filter(p => p.id !== player.id)
274
+
275
+ // \u5E7F\u64AD\u73A9\u5BB6\u79BB\u5F00
276
+ this.broadcast<BroadcastLeft>('Left', {
277
+ playerId: player.id,
278
+ })
279
+
280
+ console.log(\`[GameRoom] \${player.data.name} left room \${this.id}\`)
281
+ }
282
+
283
+ onTick(_dt: number) {
284
+ // \u5E7F\u64AD\u72B6\u6001\u540C\u6B65
285
+ this.broadcast<BroadcastSync>('Sync', {
286
+ players: this.state.players,
287
+ })
288
+ }
289
+
290
+ onDispose() {
291
+ console.log(\`[GameRoom] Room \${this.id} disposed\`)
292
+ }
293
+
294
+ // ========================================================================
295
+ // \u6D88\u606F\u5904\u7406 | Message Handlers
296
+ // ========================================================================
297
+
298
+ @onMessage('Move')
299
+ handleMove(data: MsgMove, player: Player<PlayerData>) {
300
+ player.data.x = data.x
301
+ player.data.y = data.y
302
+
303
+ // \u66F4\u65B0\u72B6\u6001
304
+ const p = this.state.players.find(p => p.id === player.id)
305
+ if (p) {
306
+ p.x = data.x
307
+ p.y = data.y
308
+ }
309
+ }
310
+
311
+ @onMessage('Chat')
312
+ handleChat(data: MsgChat, player: Player<PlayerData>) {
313
+ // \u5E7F\u64AD\u804A\u5929\u6D88\u606F
314
+ this.broadcast('Chat', {
315
+ from: player.data.name,
316
+ text: data.text,
317
+ })
318
+ }
319
+ }
320
+ `;
321
+ writeFile(projectPath, "src/server/rooms/GameRoom.ts", gameRoomTs);
322
+ const clientIndexTs = `/**
323
+ * \u5BA2\u6237\u7AEF\u793A\u4F8B\u4EE3\u7801
324
+ * Client Example Code
325
+ *
326
+ * \u8FD9\u662F\u4E00\u4E2A\u793A\u4F8B\uFF0C\u5C55\u793A\u5982\u4F55\u4ECE\u5BA2\u6237\u7AEF\u8FDE\u63A5\u670D\u52A1\u5668
327
+ * This is an example showing how to connect to the server from client
328
+ */
329
+
330
+ import { connect } from '@esengine/rpc/client'
331
+ import type {
332
+ JoinRoomReq,
333
+ JoinRoomRes,
334
+ MsgMove,
335
+ BroadcastSync,
336
+ BroadcastJoined,
337
+ } from '../shared/index.js'
338
+
339
+ async function main() {
340
+ // \u8FDE\u63A5\u670D\u52A1\u5668
341
+ const client = await connect('ws://localhost:3000')
342
+
343
+ // \u52A0\u5165\u623F\u95F4
344
+ const result = await client.call<JoinRoomReq, JoinRoomRes>('JoinRoom', {
345
+ roomType: 'game',
346
+ playerName: 'Alice',
347
+ })
348
+ console.log('Joined room:', result.roomId)
349
+
350
+ // \u76D1\u542C\u5E7F\u64AD
351
+ client.onMessage<BroadcastJoined>('Joined', (data) => {
352
+ console.log('Player joined:', data.playerName)
353
+ })
354
+
355
+ client.onMessage<BroadcastSync>('Sync', (data) => {
356
+ console.log('State update:', data.players.length, 'players')
357
+ })
358
+
359
+ // \u53D1\u9001\u79FB\u52A8\u6D88\u606F
360
+ client.send<MsgMove>('RoomMessage', {
361
+ type: 'Move',
362
+ payload: { x: 100, y: 200 },
363
+ })
364
+ }
365
+
366
+ main().catch(console.error)
367
+ `;
368
+ writeFile(projectPath, "src/client/index.ts", clientIndexTs);
369
+ const gitignore = `node_modules/
370
+ dist/
371
+ *.log
372
+ .DS_Store
373
+ `;
374
+ writeFile(projectPath, ".gitignore", gitignore);
375
+ const readme = `# ${projectName}
376
+
377
+ ESEngine \u6E38\u620F\u670D\u52A1\u5668\u9879\u76EE\u3002
378
+
379
+ ## \u9879\u76EE\u7ED3\u6784
380
+
381
+ \`\`\`
382
+ src/
383
+ \u251C\u2500\u2500 shared/ # \u5171\u4EAB\u534F\u8BAE\uFF08\u5BA2\u6237\u7AEF\u670D\u52A1\u7AEF\u90FD\u7528\uFF09
384
+ \u2502 \u251C\u2500\u2500 protocol.ts # \u7C7B\u578B\u5B9A\u4E49
385
+ \u2502 \u2514\u2500\u2500 index.ts
386
+ \u251C\u2500\u2500 server/ # \u670D\u52A1\u7AEF
387
+ \u2502 \u251C\u2500\u2500 main.ts # \u5165\u53E3
388
+ \u2502 \u2514\u2500\u2500 rooms/
389
+ \u2502 \u2514\u2500\u2500 GameRoom.ts # \u6E38\u620F\u623F\u95F4
390
+ \u2514\u2500\u2500 client/ # \u5BA2\u6237\u7AEF\u793A\u4F8B
391
+ \u2514\u2500\u2500 index.ts
392
+ \`\`\`
393
+
394
+ ## \u5FEB\u901F\u5F00\u59CB
395
+
396
+ \`\`\`bash
397
+ # \u542F\u52A8\u670D\u52A1\u5668
398
+ npm run dev
399
+
400
+ # \u670D\u52A1\u5668\u5C06\u5728 ws://localhost:3000 \u542F\u52A8
401
+ \`\`\`
402
+
403
+ ## \u5BA2\u6237\u7AEF\u8FDE\u63A5
404
+
405
+ \`\`\`typescript
406
+ import { connect } from '@esengine/rpc/client'
407
+
408
+ const client = await connect('ws://localhost:3000')
409
+
410
+ // \u52A0\u5165\u623F\u95F4
411
+ const { roomId } = await client.call('JoinRoom', {
412
+ roomType: 'game',
413
+ playerName: 'Alice',
414
+ })
415
+
416
+ // \u76D1\u542C\u540C\u6B65
417
+ client.onMessage('Sync', (state) => {
418
+ console.log(state.players)
419
+ })
420
+
421
+ // \u53D1\u9001\u6D88\u606F
422
+ client.send('RoomMessage', { type: 'Move', payload: { x: 100, y: 200 } })
423
+ \`\`\`
424
+ `;
425
+ writeFile(projectPath, "README.md", readme);
426
+ }
427
+ __name(generateProject, "generateProject");
428
+ async function main() {
429
+ printLogo();
430
+ const program = new Command();
431
+ program.name("create-esengine-server").description("Create a new ESEngine game server project").version(VERSION).argument("[project-name]", "Name of the project").action(async (projectName) => {
432
+ if (!projectName) {
433
+ const response = await prompts({
434
+ type: "text",
435
+ name: "name",
436
+ message: "Project name:",
437
+ initial: "my-game-server"
438
+ }, {
439
+ onCancel: /* @__PURE__ */ __name(() => {
440
+ console.log(chalk.yellow("\n Cancelled."));
441
+ process.exit(0);
442
+ }, "onCancel")
443
+ });
444
+ projectName = response.name;
445
+ }
446
+ if (!projectName) {
447
+ console.log(chalk.red(" Project name is required."));
448
+ process.exit(1);
449
+ }
450
+ const projectPath = path.resolve(process.cwd(), projectName);
451
+ if (fs.existsSync(projectPath)) {
452
+ const files = fs.readdirSync(projectPath);
453
+ if (files.length > 0) {
454
+ const response = await prompts({
455
+ type: "confirm",
456
+ name: "overwrite",
457
+ message: `Directory "${projectName}" is not empty. Continue?`,
458
+ initial: false
459
+ });
460
+ if (!response.overwrite) {
461
+ console.log(chalk.yellow("\n Cancelled."));
462
+ process.exit(0);
463
+ }
464
+ }
465
+ } else {
466
+ fs.mkdirSync(projectPath, {
467
+ recursive: true
468
+ });
469
+ }
470
+ console.log();
471
+ console.log(chalk.bold(` Creating project in ${chalk.cyan(projectPath)}...`));
472
+ console.log();
473
+ generateProject(projectPath, projectName);
474
+ console.log(chalk.green(" \u2713 Created project files"));
475
+ const pm = detectPackageManager();
476
+ const installCmd = getInstallCommand(pm);
477
+ console.log(chalk.gray(` Running ${installCmd}...`));
478
+ console.log();
479
+ try {
480
+ execSync(installCmd, {
481
+ cwd: projectPath,
482
+ stdio: "inherit"
483
+ });
484
+ console.log();
485
+ console.log(chalk.green(" \u2713 Dependencies installed"));
486
+ } catch {
487
+ console.log(chalk.yellow(`
488
+ \u26A0 Failed to install. Run "${installCmd}" manually.`));
489
+ }
490
+ console.log();
491
+ console.log(chalk.bold(" Done! Next steps:"));
492
+ console.log();
493
+ console.log(chalk.cyan(` cd ${projectName}`));
494
+ console.log(chalk.cyan(` ${pm} run dev`));
495
+ console.log();
496
+ console.log(chalk.gray(" Project structure:"));
497
+ console.log(chalk.gray(" src/"));
498
+ console.log(chalk.gray(" \u251C\u2500\u2500 shared/ # Shared protocol types"));
499
+ console.log(chalk.gray(" \u2502 \u2514\u2500\u2500 protocol.ts"));
500
+ console.log(chalk.gray(" \u251C\u2500\u2500 server/ # Server code"));
501
+ console.log(chalk.gray(" \u2502 \u251C\u2500\u2500 main.ts"));
502
+ console.log(chalk.gray(" \u2502 \u2514\u2500\u2500 rooms/"));
503
+ console.log(chalk.gray(" \u2502 \u2514\u2500\u2500 GameRoom.ts"));
504
+ console.log(chalk.gray(" \u2514\u2500\u2500 client/ # Client example"));
505
+ console.log(chalk.gray(" \u2514\u2500\u2500 index.ts"));
506
+ console.log();
507
+ });
508
+ program.parse();
509
+ }
510
+ __name(main, "main");
511
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "create-esengine-server",
3
+ "version": "1.1.0",
4
+ "description": "Create ESEngine game server projects",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-esengine-server": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "dependencies": {
14
+ "chalk": "^5.3.0",
15
+ "commander": "^11.0.0",
16
+ "prompts": "^2.4.2"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.0.0",
20
+ "@types/prompts": "^2.4.9",
21
+ "tsup": "^8.0.0",
22
+ "typescript": "^5.7.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "keywords": [
28
+ "esengine",
29
+ "create",
30
+ "game-server",
31
+ "scaffold",
32
+ "cli"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch"
37
+ }
38
+ }