Skip to content

Commit b1cf6da

Browse files
authored
Merge pull request #34 from iErcann/smoke
CI/CD Linting + Smoke
2 parents f34bba4 + d69030f commit b1cf6da

File tree

11 files changed

+306
-171
lines changed

11 files changed

+306
-171
lines changed

.github/workflows/ci.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ['**']
6+
pull_request:
7+
branches: ['**']
8+
9+
jobs:
10+
lint-typecheck:
11+
name: Lint & Type-check (backend)
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup pnpm
19+
uses: pnpm/action-setup@v4
20+
with:
21+
version: latest
22+
23+
- name: Setup Node 24
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: '24'
27+
cache: 'pnpm'
28+
29+
- name: Install dependencies
30+
run: pnpm install --frozen-lockfile
31+
32+
- name: Build shared
33+
run: pnpm run build:shared
34+
35+
- name: Lint backend
36+
run: pnpm --filter @notblox/back run lint
37+
38+
- name: Type-check backend
39+
run: pnpm --filter @notblox/back run build
40+
41+
docker-smoke:
42+
name: Docker build & smoke test
43+
runs-on: ubuntu-latest
44+
45+
steps:
46+
- name: Checkout
47+
uses: actions/checkout@v4
48+
49+
- name: Set up Docker Buildx
50+
uses: docker/setup-buildx-action@v3
51+
52+
- name: Build image
53+
uses: docker/build-push-action@v5
54+
with:
55+
context: .
56+
load: true
57+
tags: notblox-game-server:ci
58+
cache-from: type=gha
59+
cache-to: type=gha,mode=max
60+
61+
- name: Start container
62+
run: docker run -d --name smoke notblox-game-server:ci
63+
64+
- name: Wait for healthy
65+
run: |
66+
for i in $(seq 1 12); do
67+
STATUS=$(docker inspect --format='{{.State.Health.Status}}' smoke 2>/dev/null || echo 'missing')
68+
echo "Attempt $i: $STATUS"
69+
if [ "$STATUS" = "healthy" ]; then
70+
echo "Container is healthy"
71+
exit 0
72+
fi
73+
if [ "$STATUS" = "unhealthy" ]; then
74+
echo "Container became unhealthy"
75+
docker logs smoke
76+
exit 1
77+
fi
78+
sleep 5
79+
done
80+
echo "Timed out waiting for container to become healthy"
81+
docker logs smoke
82+
exit 1
83+
84+
- name: Print logs
85+
if: always()
86+
run: docker logs smoke
87+
88+
- name: Stop container
89+
if: always()
90+
run: docker rm -f smoke

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ COPY back/src ./back/src
4242

4343
# Run from back/ so Node resolves tsx from back/node_modules
4444
WORKDIR /app/back
45+
46+
# Probe TCP port 8001 – container is healthy once the WS server is accepting connections
47+
HEALTHCHECK --interval=5s --timeout=3s --start-period=15s --retries=3 \
48+
CMD node -e "const n=require('net').createConnection(8001,'localhost'); n.on('connect',()=>{n.destroy();process.exit(0);}); n.on('error',()=>process.exit(1));"
49+
4550
CMD ["node", "--import", "tsx/esm", "src/sandbox.ts"]

back/eslint.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
import tseslint from "typescript-eslint";
4+
import { defineConfig } from "eslint/config";
5+
6+
export default defineConfig([
7+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
8+
tseslint.configs.recommended,
9+
]);

back/package.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,32 @@
1010
"dev": "tsx watch src/sandbox.ts",
1111
"build": "tsc --noEmit",
1212
"start": "node --import tsx/esm src/sandbox.ts",
13-
"lint": "eslint src/**/*.ts",
14-
"format": "eslint src/**/*.ts --fix"
13+
"lint": "eslint src/",
14+
"format": "eslint src/ --fix"
1515
},
1616
"author": "iercann",
1717
"license": "MIT",
1818
"engines": {
1919
"node": ">=24"
2020
},
2121
"overrides": {
22-
"minimatch": "^10.2.1"
22+
"minimatch": "^10.2.1",
23+
"jiti": "latest"
2324
},
2425
"dependencies": {
25-
"@notblox/shared": "workspace:*",
26-
"tsx": "^4.0.0",
2726
"@dimforge/rapier3d-compat": "^0.14.0",
27+
"@notblox/shared": "workspace:*",
2828
"dotenv": "^16.3.1",
2929
"msgpackr": "^1.9.9",
3030
"node-three-gltf": "^1.8.3",
3131
"pako": "^2.1.0",
3232
"rate-limiter-flexible": "^5.0.3",
3333
"three": "^0.183.0",
34+
"tsx": "^4.0.0",
3435
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.57.0"
3536
},
3637
"devDependencies": {
38+
"@eslint/js": "^10.0.1",
3739
"@types/node": "^24.0.0",
3840
"@types/pako": "^2.0.3",
3941
"@types/three": "^0.183.0",
@@ -42,6 +44,7 @@
4244
"@typescript-eslint/parser": "latest",
4345
"eslint": "latest",
4446
"globals": "latest",
47+
"jiti": "^2.6.1",
4548
"typescript": "^5.7.3",
4649
"typescript-eslint": "latest"
4750
}

back/src/ecs/component/WebsocketComponent.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Component } from '@shared/component/Component.js'
2+
import type { WebSocket } from 'uWebSockets.js'
23

34
export class WebSocketComponent extends Component {
45
/**
@@ -10,7 +11,7 @@ export class WebSocketComponent extends Component {
1011
*/
1112
constructor(
1213
entityId: number,
13-
public ws: any,
14+
public ws: WebSocket<unknown>,
1415
public isFirstSnapshotSent = false
1516
) {
1617
super(entityId)

back/src/ecs/entity/Player.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ import { ColorComponent } from '@shared/component/ColorComponent.js'
1717
import { ServerMeshComponent } from '@shared/component/ServerMeshComponent.js'
1818
import { TextComponent } from '@shared/component/TextComponent.js'
1919
import { PhysicsPropertiesComponent } from '../component/physics/PhysicsPropertiesComponent.js'
20+
import type { WebSocket } from 'uWebSockets.js'
2021

2122
export class Player {
2223
entity: Entity
2324

24-
constructor(ws: WebSocket, initialX: number, initialY: number, initialZ: number) {
25+
constructor(ws: WebSocket<unknown>, initialX: number, initialY: number, initialZ: number) {
2526
this.entity = EntityManager.createEntity(SerializedEntityType.PLAYER)
2627
// Tag
2728
const playerComponent = new PlayerComponent(this.entity.id)

back/src/ecs/system/network/NetworkSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class NetworkSystem {
7777
}
7878

7979
// Broadcasts a message to all connected clients.
80-
private broadcast(entities: Entity[], message: any): void {
80+
private broadcast(entities: Entity[], message: Uint8Array): void {
8181
for (const entity of entities) {
8282
const websocketComponent = entity.getComponent(WebSocketComponent)
8383
if (websocketComponent) {

back/src/ecs/system/network/WebsocketSystem.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
HttpRequest,
55
HttpResponse,
66
SSLApp,
7+
WebSocket,
78
us_listen_socket,
89
us_socket_context_t,
910
} from 'uWebSockets.js'
@@ -38,7 +39,9 @@ import { EntityManager } from '@shared/system/EntityManager.js'
3839
import { MessageListComponent } from '@shared/component/MessageComponent.js'
3940
import { ChatComponent } from '../../component/tag/TagChatComponent.js'
4041
import { WebSocketComponent } from '../../component/WebsocketComponent.js'
41-
type MessageHandler = (ws: any, message: any) => void
42+
43+
type PlayerData = { player?: Player }
44+
type MessageHandler = (ws: WebSocket<PlayerData>, message: ClientMessage) => void
4245

4346
export class WebsocketSystem {
4447
private port: number = 8001
@@ -132,7 +135,7 @@ export class WebsocketSystem {
132135
res.end(JSON.stringify(healthData))
133136
})
134137

135-
app.ws('/*', {
138+
app.ws<PlayerData>('/*', {
136139
idleTimeout: 32,
137140
maxBackpressure: 1024,
138141
maxPayloadLength: 512,
@@ -160,8 +163,8 @@ export class WebsocketSystem {
160163
return
161164
}
162165

163-
res.upgrade(
164-
{}, // WebSocket handler will go here
166+
res.upgrade<PlayerData>(
167+
{},
165168
req.getHeader('sec-websocket-key'),
166169
req.getHeader('sec-websocket-protocol'),
167170
req.getHeader('sec-websocket-extensions'),
@@ -178,15 +181,21 @@ export class WebsocketSystem {
178181
}
179182

180183
private initializeMessageHandlers() {
181-
this.addMessageHandler(ClientMessageType.INPUT, this.handleInputMessage.bind(this))
182-
this.addMessageHandler(ClientMessageType.CHAT_MESSAGE, this.handleChatMessage.bind(this))
184+
this.addMessageHandler(
185+
ClientMessageType.INPUT,
186+
this.handleInputMessage.bind(this) as MessageHandler
187+
)
188+
this.addMessageHandler(
189+
ClientMessageType.CHAT_MESSAGE,
190+
this.handleChatMessage.bind(this) as MessageHandler
191+
)
183192
this.addMessageHandler(
184193
ClientMessageType.PROXIMITY_PROMPT_INTERACT,
185-
this.handleProximityPromptInteractMessage.bind(this)
194+
this.handleProximityPromptInteractMessage.bind(this) as MessageHandler
186195
)
187196
this.addMessageHandler(
188197
ClientMessageType.SET_PLAYER_NAME,
189-
this.handleSetPlayerNameMessage.bind(this)
198+
this.handleSetPlayerNameMessage.bind(this) as MessageHandler
190199
)
191200
}
192201

@@ -198,8 +207,8 @@ export class WebsocketSystem {
198207
this.messageHandlers.delete(type)
199208
}
200209

201-
private onMessage(ws: any, message: any) {
202-
const clientMessage: ClientMessage = unpack(message)
210+
private onMessage(ws: WebSocket<PlayerData>, message: ArrayBuffer) {
211+
const clientMessage: ClientMessage = unpack(Buffer.from(message))
203212
const handler = this.messageHandlers.get(clientMessage.t)
204213
if (handler) {
205214
handler(ws, clientMessage)
@@ -209,12 +218,12 @@ export class WebsocketSystem {
209218
// TODO: Create EventOnPlayerConnect and EventOnPlayerDisconnect to respects ECS
210219
// Might be useful to query the chat and send a message to all players when a player connects or disconnects
211220
// Also could append scriptable events to be triggered on connect/disconnect depending on the game
212-
private async onConnect(ws: any) {
221+
private async onConnect(ws: WebSocket<PlayerData>) {
213222
const ipBuffer = ws.getRemoteAddressAsText() as ArrayBuffer
214223
const ip = Buffer.from(ipBuffer).toString()
215224
if (await this.isRateLimited(ip)) {
216225
// Respond to the client indicating that the connection is rate limited
217-
return ws.close(429, 'Rate limit exceeded')
226+
return ws.close()
218227
}
219228
const player = new Player(ws, Math.random() * 5, 5, Math.random() * 5)
220229
const connectionMessage: ConnectionMessage = {
@@ -223,7 +232,7 @@ export class WebsocketSystem {
223232
tickRate: config.SERVER_TICKRATE,
224233
}
225234
// player.entity.addComponent(new RandomizeComponent(player.entity.id))
226-
ws.player = player
235+
ws.getUserData().player = player
227236
ws.send(NetworkSystem.compress(connectionMessage), true)
228237

229238
EventSystem.addEvent(
@@ -236,12 +245,12 @@ export class WebsocketSystem {
236245
this.players.push(player)
237246
}
238247

239-
private onDrain(ws: any) {
248+
private onDrain(ws: WebSocket<PlayerData>) {
240249
console.log('WebSocket backpressure: ' + ws.getBufferedAmount())
241250
}
242251

243-
private onClose(ws: any) {
244-
const disconnectedPlayer: Player = ws.player
252+
private onClose(ws: WebSocket<PlayerData>) {
253+
const disconnectedPlayer = ws.getUserData().player
245254
if (!disconnectedPlayer) {
246255
console.error('Disconnect: Player not found?', ws)
247256
return
@@ -260,8 +269,8 @@ export class WebsocketSystem {
260269
entity.removeComponent(WebSocketComponent)
261270
}
262271

263-
private async handleInputMessage(ws: any, message: InputMessage) {
264-
const player: Player = ws.player
272+
private handleInputMessage(ws: WebSocket<PlayerData>, message: InputMessage) {
273+
const player = ws.getUserData().player
265274
if (!player) {
266275
console.error(`Player with WS ${ws} not found.`)
267276
return
@@ -283,9 +292,13 @@ export class WebsocketSystem {
283292
this.inputProcessingSystem.receiveInputPacket(player.entity, message)
284293
}
285294

286-
private handleChatMessage(ws: any, message: ChatMessage) {
295+
private handleChatMessage(ws: WebSocket<PlayerData>, message: ChatMessage) {
287296
console.log('Chat message received', message)
288-
const player: Player = ws.player
297+
const player = ws.getUserData().player
298+
if (!player) {
299+
console.error(`Player with WS ${ws} not found.`)
300+
return
301+
}
289302

290303
const { content } = message
291304
if (!content || typeof content !== 'string' || content.length === 0) {
@@ -301,8 +314,11 @@ export class WebsocketSystem {
301314

302315
EventSystem.addEvent(new MessageEvent(player.entity.id, playerName, content))
303316
}
304-
private handleProximityPromptInteractMessage(ws: any, message: ProximityPromptInteractMessage) {
305-
const player: Player = ws.player
317+
private handleProximityPromptInteractMessage(
318+
ws: WebSocket<PlayerData>,
319+
message: ProximityPromptInteractMessage
320+
) {
321+
const player = ws.getUserData().player
306322
if (!player) {
307323
console.error(`Player with WS ${ws} not found.`)
308324
return
@@ -311,8 +327,8 @@ export class WebsocketSystem {
311327
EventSystem.addEvent(new ProximityPromptInteractEvent(player.entity.id, eId))
312328
}
313329

314-
private handleSetPlayerNameMessage(ws: any, message: SetPlayerNameMessage) {
315-
const player: Player = ws.player
330+
private handleSetPlayerNameMessage(ws: WebSocket<PlayerData>, message: SetPlayerNameMessage) {
331+
const player = ws.getUserData().player
316332
if (!player) {
317333
console.error(`Player with WS ${ws} not found.`)
318334
return

back/src/scripts/defaultScript.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ const cube = new Cube({
137137
})
138138
const proximityPromptComponent = new ProximityPromptComponent(cube.entity.id, {
139139
text: 'Press E to change color',
140-
onInteract: (interactingEntity) => {
140+
onInteract: () => {
141141
cube.entity
142142
.getComponent(DynamicRigidBodyComponent)!
143143
.body!.applyImpulse(new Rapier.Vector3(0, 5, 0), true)
@@ -171,7 +171,7 @@ for (let i = 1; i < 10; i++) {
171171
const y = 5
172172
const z = 20 * i
173173

174-
let wheelConfig: Record<string, number> = {}
174+
let wheelConfig: Record<string, number>
175175
if (i < 5) {
176176
wheelConfig = {
177177
frontLeft: Math.max(1, i / 2.5),

0 commit comments

Comments
 (0)