Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,7 @@ list(APPEND CLIENT_SRCS
src/game/g_horde.c
src/game/g_goap.c
src/game/g_aiml.c
src/game/g_eda.c
src/game/g_engine_systems.c
src/game/g_bt.c
src/game/g_entity_bridge.c
Expand Down
70 changes: 70 additions & 0 deletions examples/websocket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# WebSocket example pack (templated)

Self-contained **templates** you can copy: a small **Node** echo/JSON server and a **browser** client. The idTech3 **client** implements `ws://` in `src/client/cl_websocket.c` (RFC 6455 over raw TCP, no TLS). Use a reverse proxy (Caddy, nginx) for `wss://` in production.

## Layout

| Path | Role |
|------|------|
| `server/node/` | Node.js server using the `ws` package |
| `client/browser/echo_client.html` | Single-file browser demo; default URL in the input field |
| `engine_snippet.c` | Copy into `src/client/`: C sketch (not built by CMake) for `WS_*` API |

## Server (Node)

```bash
cd server/node
npm install
node echo_server.mjs
```

Default listen: `0.0.0.0:8765` (override with `PORT=9000`).

## Browser client

1. After the server is running, open `client/browser/echo_client.html` in a browser **or** serve the directory over HTTP (some browsers block `file://` WebSockets depending on security settings).
2. Set the **Server URL** field in the page (default `ws://127.0.0.1:8765/`) to match your host and port.

## Engine client (`idtech3`)

At runtime, `cl_websocket` must be `1` (default). The API is declared in `cl_websocket.h`: `WS_Init` / `WS_Frame` / `WS_Connect` / `WS_SendText` / `WS_Disconnect`. Connections are **non-blocking**; drive I/O with `WS_Frame` each client frame (already called from `cl_main.c`).

**Sketch** (not built by CMake—copy into a module that registers a `Cmd_AddCommand` and stores `wsHandle_t`):

```c
#include "cl_websocket.h"

static wsHandle_t g_ws = WS_INVALID_HANDLE;

static void Demo_OnMessage( wsHandle_t h, wsOpcode_t op, const byte *data, int len ) {
(void)h; (void)op;
Com_Printf( "WebSocket: got %d bytes\n", len );
/* data is not NUL-terminated; use len */
}

static void Demo_OnOpen( wsHandle_t h ) { Com_Printf( "WebSocket: open slot %d\n", h ); }
static void Demo_OnClose( wsHandle_t h, int code, const char *reason ) {
(void)h; Com_Printf( "WebSocket: close %d %s\n", code, reason ? reason : "" );
g_ws = WS_INVALID_HANDLE;
}
static void Demo_OnError( wsHandle_t h, const char *err ) {
(void)h; Com_Printf( S_COLOR_YELLOW "WebSocket: %s\n", err ? err : "error" );
}

/* After WS_Init and while the client is running, each frame: WS_Frame() already runs globally. */
static void CL_WsConnectDemo_f( void ) {
if ( g_ws != WS_INVALID_HANDLE ) WS_Disconnect( g_ws );
g_ws = WS_Connect( "ws://127.0.0.1:8765/", Demo_OnMessage, Demo_OnOpen, Demo_OnClose, Demo_OnError );
}
```

`wss://` URLs are parsed, but the stack does not perform TLS—terminate TLS at a proxy and use `ws://` to localhost, or extend the client for `SSL_connect` (out of scope for this template).

## Protocol in this example

- **Text frames** are echoed back to the sender.
- If a text message parses as JSON with `"type":"ping"`, the server replies with `{"type":"pong","t":<server_ms>}`.

## Security note

Do not expose raw `ws://` to untrusted networks. Prefer VPN or TLS at the edge.
86 changes: 86 additions & 0 deletions examples/websocket/client/browser/echo_client.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>WebSocket demo (template) — idTech3 echo server</title>
<style>
:root { font-family: system-ui, sans-serif; }
body { max-width: 40rem; margin: 2rem auto; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; min-height: 6rem; overflow: auto; }
button { margin-right: 0.5rem; }
label { display: block; margin: 0.5rem 0; }
</style>
</head>
<body>
<h1>WebSocket client (browser template)</h1>
<p>
Set the <strong>Server URL</strong> field to your echo server
(Node template default: <code>ws://127.0.0.1:8765/</code>).
</p>

<label>Server URL: <input id="url" size="40" value="ws://127.0.0.1:8765/" /></label>
<p>
<button type="button" id="connect">Connect</button>
<button type="button" id="disconnect" disabled>Disconnect</button>
<button type="button" id="ping">Send JSON ping</button>
</p>
<p>
<label>Outgoing text: <input id="out" size="50" value="Hello from browser" /></label>
<button type="button" id="send" disabled>Send</button>
</p>
<h2>Log</h2>
<pre id="log"></pre>

<script>
/** @type {WebSocket | null} */
let sock = null;
const logEl = document.getElementById("log");
function log(line) {
logEl.textContent += line + "\n";
}

document.getElementById("connect").onclick = () => {
const u = document.getElementById("url").value.trim() || "ws://127.0.0.1:8765/";
if (sock) sock.close();
sock = new WebSocket(u);
sock.addEventListener("open", () => {
log("[open] " + u);
document.getElementById("send").disabled = false;
document.getElementById("disconnect").disabled = false;
});
sock.addEventListener("message", (ev) => {
log("[in] " + (typeof ev.data === "string" ? ev.data : "<binary>"));
});
sock.addEventListener("close", (ev) => {
log("[close] code=" + ev.code);
document.getElementById("send").disabled = true;
document.getElementById("disconnect").disabled = true;
sock = null;
});
sock.addEventListener("error", () => {
log("[error]");
});
};

document.getElementById("disconnect").onclick = () => {
if (sock) sock.close(1000, "user");
};
document.getElementById("send").onclick = () => {
if (sock && sock.readyState === WebSocket.OPEN) {
const t = document.getElementById("out").value;
sock.send(t);
log("[out] " + t);
}
};
document.getElementById("ping").onclick = () => {
if (sock && sock.readyState === WebSocket.OPEN) {
const p = JSON.stringify({ type: "ping" });
sock.send(p);
log("[out] " + p);
} else {
log("(not connected; connect first)");
}
};
</script>
</body>
</html>
50 changes: 50 additions & 0 deletions examples/websocket/engine_snippet.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* TEMPLATE — not part of the CMake build.
* Copy into a client module, register a command in CL_Init (or similar), and call
* after WS_Init() (see cl_websocket.h / cl_websocket.c).
*/
#include "client.h"
#include "cl_websocket.h"

static wsHandle_t g_exampleWs = WS_INVALID_HANDLE;

static void ExampleWS_OnMessage( wsHandle_t h, wsOpcode_t op, const byte *data, int len ) {
(void)h;
(void)op;
Com_Printf( "WebSocket example: received %d byte(s)\n", len );
}

static void ExampleWS_OnOpen( wsHandle_t h ) {
Com_Printf( "WebSocket example: connected (handle %d)\n", h );
(void)WS_SendText( h, "hello from idtech3" );
}

static void ExampleWS_OnClose( wsHandle_t h, int code, const char *reason ) {
Com_Printf( "WebSocket example: closed code=%d %s\n", code, reason ? reason : "" );
if ( h == g_exampleWs ) {
g_exampleWs = WS_INVALID_HANDLE;
}
}

static void ExampleWS_OnError( wsHandle_t h, const char *err ) {
Com_Printf( S_COLOR_YELLOW "WebSocket example: %s (handle %d)\n", err ? err : "error", h );
}

static void ExampleWS_Connect_f( void ) {
if ( g_exampleWs != WS_INVALID_HANDLE ) {
WS_Disconnect( g_exampleWs );
}
/* Point at your local echo server (see examples/websocket/server/node) */
g_exampleWs = WS_Connect( "ws://127.0.0.1:8765/", ExampleWS_OnMessage, ExampleWS_OnOpen, ExampleWS_OnClose, ExampleWS_OnError );
}

static void ExampleWS_Disconnect_f( void ) {
if ( g_exampleWs != WS_INVALID_HANDLE ) {
WS_Disconnect( g_exampleWs );
g_exampleWs = WS_INVALID_HANDLE;
}
}

/* In your init: Cmd_AddCommand( "ws_example", ExampleWS_Connect_f ); */
/* Cmd_AddCommand( "ws_example_close", ExampleWS_Disconnect_f ); */
/* I/O: WS_Frame() is already called each client frame from cl_main.c. */
1 change: 1 addition & 0 deletions examples/websocket/server/node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
51 changes: 51 additions & 0 deletions examples/websocket/server/node/echo_server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Templated WebSocket server for idTech3 / browser demos.
* Text messages are echoed. JSON { "type": "ping" } is answered with pong + server time.
*
* Usage: node echo_server.mjs
* Env: PORT (default 8765), HOST (default 0.0.0.0)
*/
import { WebSocketServer } from "ws";

const port = Number(process.env.PORT) || 8765;
const host = process.env.HOST || "0.0.0.0";

const wss = new WebSocketServer({ port, host });

wss.on("listening", () => {
// eslint-disable-next-line no-console
console.log(`[websocket-example] listening ws://${host}:${port}/`);
});

wss.on("connection", (ws, req) => {
const from = req.socket?.remoteAddress ?? "unknown";
// eslint-disable-next-line no-console
console.log(`[websocket-example] client connected from ${from}`);

ws.on("message", (data, isBinary) => {
if (isBinary) {
ws.send(data);
return;
}
const text = data.toString("utf8");
let reply = null;
try {
const o = JSON.parse(text);
if (o && o.type === "ping") {
reply = JSON.stringify({ type: "pong", t: Date.now() });
}
} catch {
/* not JSON — echo as plain text */
}
if (reply !== null) {
ws.send(reply);
} else {
ws.send(text);
}
});

ws.on("close", (code) => {
// eslint-disable-next-line no-console
console.log(`[websocket-example] client closed code=${code}`);
});
});
40 changes: 40 additions & 0 deletions examples/websocket/server/node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions examples/websocket/server/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "idtech3-websocket-example",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Echo and JSON ping/pong templates for the idTech3 WebSocket client",
"license": "GPL-2.0",
"engines": {
"node": ">=18"
},
"dependencies": {
"ws": "^8.18.0"
}
}
17 changes: 17 additions & 0 deletions src/client/cl_gameframe.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Ticks all gameplay subsystems each client frame:
#include "../game/g_dismember.h"
#include "../game/g_goap.h"
#include "../game/g_aiml.h"
#include "../game/g_eda.h"
#include "../game/g_bt.h"
#include "../game/g_engine_systems.h"
#include "../game/g_lua_bindings.h"
Expand All @@ -51,6 +52,9 @@ static cvar_t *cl_physicsEnabled;
static cvar_t *cl_navEnabled;
static cvar_t *cl_particlesEnabled;
static cvar_t *cl_btEnabled;
#ifdef USE_ECS
static cvar_t *g_ecsMotion;
#endif

extern void Nav_BSP_ClearGeometry(void);
extern int Nav_BSP_AddVertex(float x, float y, float z);
Expand Down Expand Up @@ -157,6 +161,7 @@ void CL_InitGameSystems(void) {
Dismember_Init();
GOAP_Init();
AIML_Init();
EDA_Init();
EngineTelemetry_Init();
EngineReplay_Init();
EngineSave_Init();
Expand All @@ -165,6 +170,8 @@ void CL_InitGameSystems(void) {
BT_Init();
#ifdef USE_ECS
ECS_Init();
g_ecsMotion = Cvar_Get( "g_ecsMotion", "1", CVAR_ARCHIVE_ND );
Cvar_SetDescription( g_ecsMotion, "When 1, integrate ECS velocity into position each client frame (USE_ECS builds)." );
#endif
MobileFog_Init();
BgMap_Init();
Expand Down Expand Up @@ -195,6 +202,8 @@ void CL_ShutdownGameSystems(void) {
EngineSave_Shutdown();
EngineReplay_Shutdown();
EngineTelemetry_Shutdown();
AIML_Shutdown();
EDA_Shutdown();
BT_Shutdown();
#ifdef USE_ECS
ECS_Shutdown();
Expand All @@ -218,6 +227,12 @@ void CL_GameFrame(float frametime) {
Phys_StepSimulation(frametime);
}

#ifdef USE_ECS
if ( g_ecsMotion && g_ecsMotion->integer ) {
ECS_StepMotion( frametime );
}
#endif

if (cl_navEnabled && cl_navEnabled->integer && activeNavMesh >= 0) {
Nav_UpdateCrowd(activeNavMesh, frametime);
}
Expand Down Expand Up @@ -277,6 +292,8 @@ void CL_GameFrame(float frametime) {
BgMap_Frame(frametime);
WinTitle_Update(frametime);

EDA_Frame();

{
vec3_t fwd = {1,0,0}, right = {0,1,0}, up = {0,0,1}, origin = {0,0,0};
MobileFog_Frame(origin, fwd, right, up, frametime);
Expand Down
Loading
Loading