@inglorious/server 0.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.
- package/LICENSE +9 -0
- package/README.md +85 -0
- package/package.json +60 -0
- package/src/default-game.js +1 -0
- package/src/game-loader.js +49 -0
- package/src/game-loop.js +24 -0
- package/src/index.js +70 -0
- package/src/ws-handler.js +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2025 Inglorious Coderz Srl.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Inglorious Server
|
|
2
|
+
|
|
3
|
+
A real-time, lightweight server designed to enable multiplayer experiences for games built with the Inglorious Engine. This server is built on Node.js and uses WebSockets to handle real-time communication, ensuring low-latency updates for all connected clients.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Real-Time Communication**: Uses WebSockets to provide bidirectional, low-latency communication between the server and clients.
|
|
8
|
+
- **Server-Authoritative Game State**: Manages a central game state using the `@inglorious/store` and acts as the single source of truth for all clients.
|
|
9
|
+
- **Scalable Architecture**: The server's logic is modular and split into dedicated files for handling game loading, the main game loop, and WebSocket events.
|
|
10
|
+
- **Dynamic Game Loading**: Supports loading game data from a specified JavaScript module, allowing you to easily run different game worlds without changing the server code.
|
|
11
|
+
- **Production-Ready Logging**: Uses `pino` for fast, structured logging, making it easy to monitor and debug in a production environment.
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
### Prerequisites
|
|
16
|
+
|
|
17
|
+
- [Node.js](https://nodejs.org/) (version 22 or higher)
|
|
18
|
+
- [pnpm](https://pnpm.io/) (used for package management)
|
|
19
|
+
|
|
20
|
+
### Installation
|
|
21
|
+
|
|
22
|
+
1. Clone the Inglorious Engine repository:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
git clone https://github.com/IngloriousCoderz/inglorious-engine.git
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. Navigate to the `@inglorious/server` directory:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
cd packages/server
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. Install the dependencies:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pnpm install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
The server can be started using the `start` or `dev` scripts. You can optionally provide a path to a game module to load a specific game state.
|
|
43
|
+
|
|
44
|
+
### Running in Development Mode
|
|
45
|
+
|
|
46
|
+
To run the server with automatic restarts on file changes, use the `dev` script.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pnpm dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Running in Production Mode
|
|
53
|
+
|
|
54
|
+
To run the server in a production environment, use the `start` script.
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
pnpm start
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Loading a Specific Game
|
|
61
|
+
|
|
62
|
+
By default, the server will attempt to load a `./default-game.js` file. To load a different game, provide the file path as a command-line argument.
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
pnpm start ./path/to/your-game.js
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Project Structure
|
|
69
|
+
|
|
70
|
+
The server's code is split into several files to maintain a clear separation of concerns.
|
|
71
|
+
|
|
72
|
+
- `src/index.js`: The main entry point for the server. It orchestrates the setup of the HTTP and WebSocket servers and initializes the game store and its core handlers.
|
|
73
|
+
- `src/game-loader.js`: Handles the dynamic loading of the initial game state from a JavaScript module. It also includes a fallback state if the game file is not found.
|
|
74
|
+
- `src/game-loop.js`: Contains the core setInterval loop that drives the game logic by calling the store.update() function at a fixed tick rate (60 updates per second).
|
|
75
|
+
- `src/websocket-handler.js`: Manages all WebSocket events, including new connections, incoming messages, and disconnections. It dispatches client events to the store and broadcasts changes to all clients.
|
|
76
|
+
|
|
77
|
+
## Dependencies
|
|
78
|
+
|
|
79
|
+
- `@inglorious/store`: The state management solution for the game engine.
|
|
80
|
+
- `ws`: A simple and fast WebSocket library.
|
|
81
|
+
- `pino`: A highly performant logger for Node.js.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
This project is licensed under the MIT License.
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inglorious/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A real-time, lightweight server designed to enable multiplayer experiences for games built with the Inglorious Engine.",
|
|
5
|
+
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/IngloriousCoderz/inglorious-engine.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://inglorious-engine.vercel.app/",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/IngloriousCoderz/inglorious-engine/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"functional-programming",
|
|
17
|
+
"gamedev",
|
|
18
|
+
"game-engine",
|
|
19
|
+
"inglorious-engine",
|
|
20
|
+
"server",
|
|
21
|
+
"real-time",
|
|
22
|
+
"multiplayer",
|
|
23
|
+
"websocket"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./src/index.js",
|
|
27
|
+
"bin": {
|
|
28
|
+
"inglorious-server": "./src/index.js"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"pino": "^9.9.4",
|
|
43
|
+
"ws": "^8.18.3",
|
|
44
|
+
"@inglorious/store": "1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"globals": "^16.3.0",
|
|
48
|
+
"nodemon": "^3.1.10",
|
|
49
|
+
"prettier": "^3.6.2"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">= 22"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"format": "prettier --write '**/*.{js,jsx}'",
|
|
56
|
+
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
57
|
+
"start": "node src/index.js",
|
|
58
|
+
"dev": "nodemon src/index.js"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dynamically loads the initial game state from a module and provides a fallback.
|
|
6
|
+
* @param {string} gameFilePath The path to the game module.
|
|
7
|
+
* @param {object} logger A Pino logger instance.
|
|
8
|
+
* @returns {Promise<{initialTypes: object, initialEntities: object}>} The initial game state.
|
|
9
|
+
*/
|
|
10
|
+
export async function loadGame(gameFilePath, logger) {
|
|
11
|
+
let gameConfig = {}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Dynamically import the game module.
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const __dirname = path.dirname(__filename)
|
|
17
|
+
const modulePath = path.resolve(__dirname, gameFilePath)
|
|
18
|
+
const { default: config } = await import(`file://${modulePath}`)
|
|
19
|
+
gameConfig = config
|
|
20
|
+
gameConfig.types ??= {}
|
|
21
|
+
gameConfig.entities ??= {}
|
|
22
|
+
|
|
23
|
+
logger.info(`Loaded game data from ${gameFilePath}`)
|
|
24
|
+
} catch (error) {
|
|
25
|
+
logger.error(`Failed to load game module: ${gameFilePath}`)
|
|
26
|
+
logger.error(error)
|
|
27
|
+
// Fallback to a basic initial state if loading fails.
|
|
28
|
+
gameConfig = {
|
|
29
|
+
types: {
|
|
30
|
+
player: {
|
|
31
|
+
move: (entity, payload) => {
|
|
32
|
+
logger.info(
|
|
33
|
+
`Player ${entity.id} moved to ${payload.x}, ${payload.y}`,
|
|
34
|
+
)
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
box: {},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
entities: {
|
|
41
|
+
player1: { id: "player1", type: "player", x: 0, y: 0 },
|
|
42
|
+
box1: { id: "box1", type: "box", x: 10, y: 10 },
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
logger.warn("Using default game state as a fallback.")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return gameConfig
|
|
49
|
+
}
|
package/src/game-loop.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks"
|
|
2
|
+
|
|
3
|
+
const ONE_SECOND = 1000
|
|
4
|
+
const TICK_RATE = 60 // 60 updates per second
|
|
5
|
+
const TICK_INTERVAL = ONE_SECOND / TICK_RATE // ~16.67ms
|
|
6
|
+
let lastTime = performance.now()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Starts the main server-side game loop.
|
|
10
|
+
* @param {object} store The game store instance.
|
|
11
|
+
* @param {object} api The game API with broadcast functionality.
|
|
12
|
+
*/
|
|
13
|
+
export function start(store, api) {
|
|
14
|
+
// The main server-side game loop.
|
|
15
|
+
setInterval(() => {
|
|
16
|
+
const currentTime = performance.now()
|
|
17
|
+
const dt = (currentTime - lastTime) / ONE_SECOND // Convert to seconds
|
|
18
|
+
lastTime = currentTime
|
|
19
|
+
|
|
20
|
+
// The store's update function is the heart of the game loop.
|
|
21
|
+
// It processes events and advances the game state.
|
|
22
|
+
store.update(dt, api)
|
|
23
|
+
}, TICK_INTERVAL)
|
|
24
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import http from "node:http"
|
|
2
|
+
|
|
3
|
+
import { createStore } from "@inglorious/store"
|
|
4
|
+
import pino from "pino"
|
|
5
|
+
import { WebSocketServer } from "ws"
|
|
6
|
+
|
|
7
|
+
import { loadGame } from "./game-loader.js"
|
|
8
|
+
import { start as startGameLoop } from "./game-loop.js"
|
|
9
|
+
import { setup as setupWebSocketHandler } from "./ws-handler.js"
|
|
10
|
+
|
|
11
|
+
// The server's port. You can change this or use an environment variable.
|
|
12
|
+
const PORT = 3000
|
|
13
|
+
const HTTP_STATUS_OK = 200
|
|
14
|
+
|
|
15
|
+
const logger = pino({ name: "Inglorious Server", level: "info" })
|
|
16
|
+
|
|
17
|
+
// =========================================================================
|
|
18
|
+
// Initial Game State
|
|
19
|
+
// =========================================================================
|
|
20
|
+
|
|
21
|
+
// Get the game file path from command-line arguments.
|
|
22
|
+
let [, , gameFilePath] = process.argv
|
|
23
|
+
gameFilePath ??= "./default-game.js"
|
|
24
|
+
|
|
25
|
+
const gameConfig = await loadGame(gameFilePath, logger)
|
|
26
|
+
|
|
27
|
+
// =========================================================================
|
|
28
|
+
// Server Setup
|
|
29
|
+
// =========================================================================
|
|
30
|
+
|
|
31
|
+
// Create a minimal HTTP server. The WebSocket server will be "attached" to this.
|
|
32
|
+
const httpServer = http.createServer((req, res) => {
|
|
33
|
+
res.writeHead(HTTP_STATUS_OK, { "Content-Type": "text/plain" })
|
|
34
|
+
res.end("Inglorious Server is running.")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Create the WebSocket server.
|
|
38
|
+
const wss = new WebSocketServer({ server: httpServer })
|
|
39
|
+
const clients = new Set() // A Set to keep track of all connected clients.
|
|
40
|
+
|
|
41
|
+
// Initialize the game store. This is the central source of truth.
|
|
42
|
+
const store = createStore(gameConfig)
|
|
43
|
+
|
|
44
|
+
// A small-scale API for your systems and events to interact with the world.
|
|
45
|
+
// This can be expanded as needed.
|
|
46
|
+
const api = {
|
|
47
|
+
// A simple broadcast function.
|
|
48
|
+
broadcast: (event) => {
|
|
49
|
+
// Stringify the event for transmission.
|
|
50
|
+
const message = JSON.stringify(event)
|
|
51
|
+
for (const client of clients) {
|
|
52
|
+
client.send(message)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// =========================================================================
|
|
58
|
+
// Start Game Loop and WebSocket Handling
|
|
59
|
+
// =========================================================================
|
|
60
|
+
|
|
61
|
+
// Start the core game loop.
|
|
62
|
+
startGameLoop(store, api)
|
|
63
|
+
|
|
64
|
+
// Set up the WebSocket event handlers.
|
|
65
|
+
setupWebSocketHandler(wss, store, clients, logger)
|
|
66
|
+
|
|
67
|
+
// Start the server and listen on the specified port.
|
|
68
|
+
httpServer.listen(PORT, () => {
|
|
69
|
+
logger.info(`Listening on http://localhost:${PORT}`)
|
|
70
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets up WebSocket event handling for a given server and store.
|
|
3
|
+
* @param {object} wss The WebSocket server instance.
|
|
4
|
+
* @param {object} store The game store instance.
|
|
5
|
+
* @param {Set<object>} clients A Set to keep track of all connected clients.
|
|
6
|
+
* @param {object} logger A Pino logger instance.
|
|
7
|
+
*/
|
|
8
|
+
export function setup(wss, store, clients, logger) {
|
|
9
|
+
// Listen for new client connections.
|
|
10
|
+
wss.on("connection", (ws) => {
|
|
11
|
+
logger.info("A new client has connected.")
|
|
12
|
+
clients.add(ws)
|
|
13
|
+
|
|
14
|
+
// Send the initial state to the new client.
|
|
15
|
+
ws.send(JSON.stringify({ type: "initialState", payload: store.getState() }))
|
|
16
|
+
|
|
17
|
+
// Listen for messages from the client.
|
|
18
|
+
ws.on("message", (message) => {
|
|
19
|
+
try {
|
|
20
|
+
// Parse the incoming event.
|
|
21
|
+
const event = JSON.parse(message)
|
|
22
|
+
|
|
23
|
+
// Dispatch the event to the central store.
|
|
24
|
+
store.dispatch(event)
|
|
25
|
+
|
|
26
|
+
// Broadcast the event to all other clients.
|
|
27
|
+
const eventMessage = JSON.stringify(event)
|
|
28
|
+
for (const client of clients) {
|
|
29
|
+
if (client !== ws) {
|
|
30
|
+
// Exclude the sender from the broadcast.
|
|
31
|
+
client.send(eventMessage)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
logger.error("Failed to parse message:", error)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Listen for the client disconnecting.
|
|
40
|
+
ws.on("close", () => {
|
|
41
|
+
logger.info("A client has disconnected.")
|
|
42
|
+
clients.delete(ws)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
ws.on("error", (error) => {
|
|
46
|
+
logger.error("WebSocket error:", error)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
}
|