Improve netcode and simplify #4

Merged
andras merged 15 commits from fix/audit-findings into main 2026-09-15 07:57:58 +01:00
9 changed files with 2 additions and 32 deletions
Showing only changes of commit 779985c322 - Show all commits

No dumb comments
Some checks failed
Build & deploy / Build & publish server image (pull_request) Has been skipped
Build & deploy / Build & deploy website (pull_request) Failing after 16s

Andras Schmelczer 2026-09-14 22:13:15 +01:00

View file

@ -1,4 +1,3 @@
# The server image only needs `shared/` and `backend/`.
**/node_modules **/node_modules
**/dist **/dist
**/lib **/lib

View file

@ -13,7 +13,6 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# ---- Static website -> /pages mount on the runner host --------------------
website: website:
name: Build & deploy website name: Build & deploy website
runs-on: docker runs-on: docker
@ -45,11 +44,9 @@ jobs:
source: frontend/dist source: frontend/dist
target: declared target: declared
# ---- Server Docker image -> Forgejo container registry --------------------
server-image: server-image:
name: Build & publish server image name: Build & publish server image
runs-on: docker runs-on: docker
# No registry push on PRs; build validation still happens in the website job.
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
steps: steps:
- name: Checkout - name: Checkout

3
.vscode/launch.json vendored
View file

@ -1,7 +1,4 @@
{ {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {

View file

@ -1,12 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# doppler game server (the `doppler-server` package). # The full image carries the build tools for socket.io's optional native dependencies.
# The frontend is a static site and is NOT built here — see .forgejo/workflows.
# ---- Stage 1: build the shared lib, then bundle the server -------------------
# Node 22 (current LTS) matches the project toolchain (webpack 5 / TypeScript 6,
# see .devcontainer). The full (non-slim) image carries the build tools that
# socket.io's optional native deps (bufferutil/utf-8-validate) compile against.
FROM node:22-bookworm AS build FROM node:22-bookworm AS build
WORKDIR /app WORKDIR /app
@ -17,7 +11,6 @@ RUN cd shared && npm install
COPY shared/ shared/ COPY shared/ shared/
RUN cd shared && npm run build RUN cd shared && npm run build
# Install the backend deps. `file:../shared` now resolves to the built /app/shared.
COPY backend/package.json backend/ COPY backend/package.json backend/
RUN cd backend && npm install RUN cd backend && npm install
COPY backend/ backend/ COPY backend/ backend/
@ -27,12 +20,10 @@ RUN cd backend && npm run build
# left external (express, socket.io, cors, gl-matrix, minimist, msgpack parser). # left external (express, socket.io, cors, gl-matrix, minimist, msgpack parser).
RUN cd backend && npm prune --production RUN cd backend && npm prune --production
# ---- Stage 2: minimal runtime ----------------------------------------------
FROM node:22-bookworm-slim FROM node:22-bookworm-slim
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
# Run as an unprivileged user.
RUN groupadd -r app && useradd -r -g app -d /app app RUN groupadd -r app && useradd -r -g app -d /app app
COPY --from=build /app/backend/dist ./dist COPY --from=build /app/backend/dist ./dist
@ -41,7 +32,6 @@ COPY --from=build /app/backend/node_modules ./node_modules
USER app USER app
EXPOSE 3000 EXPOSE 3000
# Hits the same /state endpoint the website polls (see serverInformationEndpoint).
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"

View file

@ -230,8 +230,6 @@ body {
} }
} }
// Top-down radar of the whole arena (see Minimap). The circular border is
// the world boundary; dots are painted onto the canvas.
.minimap { .minimap {
top: $small-padding; top: $small-padding;
left: $small-padding; left: $small-padding;
@ -252,7 +250,6 @@ body {
z-index: 100; z-index: 100;
} }
// Off-screen pointer to the keystone "Heart", tinted by its current owner.
.keystone-arrow { .keystone-arrow {
opacity: 0.9; opacity: 0.9;
@ -330,8 +327,6 @@ body {
content: '\25C9'; content: '\25C9';
} }
// Twin-stick aim indicator: drawn from the button centre toward the
// current drag direction while aiming a shot (see TouchListener).
.aim-line { .aim-line {
position: absolute; position: absolute;
left: 50%; left: 50%;
@ -735,8 +730,6 @@ body {
} }
} }
// Centred "Eliminated" overlay shown while the local player is dead; the
// respawn countdown is the server-driven "Reviving in N…" announcement.
.elimination { .elimination {
position: absolute; position: absolute;
top: 42%; top: 42%;

View file

@ -75,7 +75,6 @@ module.exports = (env, argv) => {
], ],
}, },
{ {
// svg-url-loader emits a CJS module that css-loader 7 writes out verbatim.
test: /\.svg$/, test: /\.svg$/,
type: 'asset/inline', type: 'asset/inline',
}, },

View file

@ -13,7 +13,6 @@ export const resolveCircleMovement = (
let remainingSeconds = deltaTimeInSeconds; let remainingSeconds = deltaTimeInSeconds;
let firstHit: Sdf | undefined; let firstHit: Sdf | undefined;
// Resolve at most two contacts per step, spending only the time left after each hit.
for (let pass = 0; pass < 2 && remainingSeconds > 0; pass++) { for (let pass = 0; pass < 2 && remainingSeconds > 0; pass++) {
const speed = vec2.length(body.velocity); const speed = vec2.length(body.velocity);
if (speed === 0) { if (speed === 0) {

View file

@ -64,7 +64,6 @@ interface Result {
maximumSpeed: number; maximumSpeed: number;
} }
// Exercise shared movement with independent server, network, and render clocks.
// Measure world travel: the camera tracks the local character exactly, so measuring // Measure world travel: the camera tracks the local character exactly, so measuring
// their relative position would always report zero even when the world visibly stutters. // their relative position would always report zero even when the world visibly stutters.
const measure = ({ const measure = ({
@ -96,7 +95,6 @@ const measure = ({
const predictor = new LocalCharacterPredictor(); const predictor = new LocalCharacterPredictor();
predictor.setStrength(settings.playerMaxStrength); predictor.setStrength(settings.playerMaxStrength);
// Deterministic frame-time noise: a real display never delivers a perfectly even cadence.
let seed = 12345; let seed = 12345;
const random = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff; const random = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;

View file

@ -61,7 +61,6 @@ function character(id = 1) {
step: vi.fn(), step: vi.fn(),
render: vi.fn(), render: vi.fn(),
beforeDestroy: vi.fn(), beforeDestroy: vi.fn(),
// A health setter also represents damage sound/flash feedback in CharacterView.
setHealth: vi.fn((health: number) => (object.health = health)), setHealth: vi.fn((health: number) => (object.health = health)),
onShoot: vi.fn(), onShoot: vi.fn(),
onDie: vi.fn(), onDie: vi.fn(),
@ -126,7 +125,6 @@ describe('suspended rendering', () => {
expect(survivor.onShoot).not.toHaveBeenCalled(); expect(survivor.onShoot).not.toHaveBeenCalled();
expect(retained(container).remoteCalls.length).toBeLessThanOrEqual(3); expect(retained(container).remoteCalls.length).toBeLessThanOrEqual(3);
// With no additional effects, subsequent snapshots retire the entire old tail.
snapshot(container, 46); snapshot(container, 46);
expect(container.planets).toHaveLength(0); expect(container.planets).toHaveLength(0);
expect(retained(container).objects.size).toBe(1); expect(retained(container).objects.size).toBe(1);