@ankorcs/protocol 0.0.0-stage → 0.6.1

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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Plain checks the schemas and the helpers share. Nothing here imports the rest of the contract.
3
+ */
4
+ /** The validation message for a slot id that is not 1 to 32 letters, digits, `_` or `-`. */
5
+ export declare const SLOT_ID_MESSAGE = "slot id must be 1-32 of [A-Za-z0-9_-] (it becomes part of a container name)";
6
+ /** The validation message for a `controller.host` that {@link isControllerHost} rejects. */
7
+ export declare const CONTROLLER_HOST_MESSAGE = "must be a host with an optional port and no scheme or path, e.g. cs.example.com, 192.168.0.10:3000 or [fd00::1]";
8
+ /** Whether a value is a dotted-quad IPv4 address. */
9
+ export declare function isIpv4(value: string): boolean;
10
+ /**
11
+ * The hostname inside a `host[:port]`, without the brackets of an IPv6 literal. Null for a value
12
+ * that is not a host with an optional port.
13
+ */
14
+ export declare function hostnameOf(host: string): string | null;
15
+ /**
16
+ * Whether a value is a valid `controller.host`, by the rules that `agent.yml`, its editors and the
17
+ * node-setup script agree on.
18
+ */
19
+ export declare function isControllerHost(value: string): boolean;
package/dist/checks.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Plain checks the schemas and the helpers share. Nothing here imports the rest of the contract.
3
+ */
4
+ /** The validation message for a slot id that is not 1 to 32 letters, digits, `_` or `-`. */
5
+ export const SLOT_ID_MESSAGE = 'slot id must be 1-32 of [A-Za-z0-9_-] (it becomes part of a container name)';
6
+ /** The validation message for a `controller.host` that {@link isControllerHost} rejects. */
7
+ export const CONTROLLER_HOST_MESSAGE = 'must be a host with an optional port and no scheme or path, e.g. cs.example.com, 192.168.0.10:3000 or [fd00::1]';
8
+ const IPV4 = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
9
+ /** Whether a value is a dotted-quad IPv4 address. */
10
+ export function isIpv4(value) {
11
+ return IPV4.test(value);
12
+ }
13
+ /**
14
+ * The hostname inside a `host[:port]`, without the brackets of an IPv6 literal. Null for a value
15
+ * that is not a host with an optional port.
16
+ */
17
+ export function hostnameOf(host) {
18
+ // URL parsing normalizes these away, but callers append paths to the original host.
19
+ if (/[\s\p{Cc}/\\?#]/u.test(host)) {
20
+ return null;
21
+ }
22
+ let url;
23
+ try {
24
+ url = new URL(`http://${host}`);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ if (url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '' || url.pathname !== '/') {
30
+ return null;
31
+ }
32
+ return url.hostname === '' ? null : url.hostname.replace(/^\[|\]$/g, '');
33
+ }
34
+ /**
35
+ * Whether a value is a valid `controller.host`, by the rules that `agent.yml`, its editors and the
36
+ * node-setup script agree on.
37
+ */
38
+ export function isControllerHost(value) {
39
+ return hostnameOf(value) !== null;
40
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The interface between the agent and the game containers it runs, outside the wire to the
3
+ * controller: the environment the CS2 image and the plugin read, the plugin's config file, where
4
+ * recordings land and how they are named, the cfg files both sides exec, the local plugin socket
5
+ * and the handoff to a replacement agent. `packages/plugin/core/Wire/Launch.cs` mirrors the
6
+ * plugin's half.
7
+ */
8
+ import { z } from 'zod';
9
+ export * from './plugin-socket.js';
10
+ export * from './agent-image.js';
11
+ /** The path of the agent's local plugin socket. */
12
+ export declare const PLUGIN_WS_PATH = "/ws/plugin";
13
+ /**
14
+ * The environment variables the image entrypoint reads. The plugin also reads `startMap` and
15
+ * `workshopMap` to know when its map is loaded.
16
+ */
17
+ export declare const CS2_ENV: {
18
+ /** The server's Game Server Login Token. */
19
+ readonly srcdsToken: "SRCDS_TOKEN";
20
+ readonly port: "CS2_PORT";
21
+ readonly tvPort: "CS2_TV_PORT";
22
+ /** `+tv_enable`, `1` or `0`. GOTV starts with the map, so it is set on the command line. */
23
+ readonly tvEnable: "CS2_TV_ENABLE";
24
+ readonly gamePassword: "CS2_PASSWORD";
25
+ readonly maxPlayers: "CS2_MAXPLAYERS";
26
+ /** `+game_type`, from {@link GAME_MODE_ARGS}. */
27
+ readonly gameType: "CS2_GAME_TYPE";
28
+ /** `+game_mode`, from {@link GAME_MODE_ARGS}. */
29
+ readonly gameMode: "CS2_GAME_MODE";
30
+ readonly startMap: "CS2_STARTMAP";
31
+ /**
32
+ * Passed separately so the entrypoint places `host_workshop_map` after the stock bootstrap map.
33
+ */
34
+ readonly workshopMap: "CS2_WORKSHOP_MAP";
35
+ /** The Steam Web API key that workshop downloads need, as `-authkey`. */
36
+ readonly authKey: "CS2_AUTHKEY";
37
+ readonly extraArgs: "CS2_EXTRA_ARGS";
38
+ readonly updateOnly: "CS2_UPDATE_ONLY";
39
+ readonly validate: "CS2_VALIDATE";
40
+ /** Empties the install volume before steamcmd runs. */
41
+ readonly reinstall: "CS2_REINSTALL";
42
+ /** Whole gigabytes the entrypoint requires free before it lets steamcmd stage anything. */
43
+ readonly minFreeGb: "CS2_MIN_FREE_GB";
44
+ };
45
+ /**
46
+ * The `+game_type` and `+game_mode` for each {@link GameMode}, as `gamemodes.txt` numbers them. The
47
+ * plugin's `CfgNames.Mode` maps the same pairs back to the modes' cfgs.
48
+ */
49
+ export declare const GAME_MODE_ARGS: {
50
+ readonly casual: {
51
+ readonly gameType: 0;
52
+ readonly gameMode: 0;
53
+ };
54
+ readonly competitive: {
55
+ readonly gameType: 0;
56
+ readonly gameMode: 1;
57
+ };
58
+ readonly wingman: {
59
+ readonly gameType: 0;
60
+ readonly gameMode: 2;
61
+ };
62
+ readonly rush: {
63
+ readonly gameType: 0;
64
+ readonly gameMode: 6;
65
+ };
66
+ readonly arms_race: {
67
+ readonly gameType: 1;
68
+ readonly gameMode: 0;
69
+ };
70
+ readonly demolition: {
71
+ readonly gameType: 1;
72
+ readonly gameMode: 1;
73
+ };
74
+ readonly deathmatch: {
75
+ readonly gameType: 1;
76
+ readonly gameMode: 2;
77
+ };
78
+ readonly custom: {
79
+ readonly gameType: 3;
80
+ readonly gameMode: 0;
81
+ };
82
+ };
83
+ /** The environment variables that only the plugin reads. */
84
+ export declare const ANKOR_ENV: {
85
+ /** The local relay the plugin dials for its slot. */
86
+ readonly wsUrl: "ANKOR_WS_URL";
87
+ /**
88
+ * The server's `allocation_id`, which identifies its events. A plugin without one sends nothing.
89
+ */
90
+ readonly allocationId: "ANKOR_ALLOCATION_ID";
91
+ readonly debugCaptureDir: "ANKOR_DEBUG_CAPTURE_DIR";
92
+ };
93
+ /** Where the cs2 image keeps its addon overlay. The agent copies it out once per image id. */
94
+ export declare const IMAGE_OVERLAY_DIR = "/opt/ankor/overlay";
95
+ /** The game installation inside the container. Per-slot writable paths shadow parts of it. */
96
+ export declare const GAME_INSTALL_DIR = "/home/steam/cs2-dedicated";
97
+ /**
98
+ * The slot's writable directory, relative to {@link GAME_INSTALL_DIR}. It holds everything per slot
99
+ * that the agent, the plugin and the engine share. The agent lays out its contents, so a new file
100
+ * or directory in it needs no mountpoint in the install.
101
+ */
102
+ export declare const ANKOR_DIR = "game/csgo/ankor";
103
+ /** GOTV recordings, relative to {@link GAME_INSTALL_DIR}, kept until uploaded. */
104
+ export declare const DEMO_DIR = "game/csgo/ankor/demos";
105
+ /** The engine's round backups, relative to {@link GAME_INSTALL_DIR}, emptied at every launch. */
106
+ export declare const BACKUP_DIR = "game/csgo/ankor/backups";
107
+ /**
108
+ * The file name of the engine's backup of a round, from the rounds played before it. The engine
109
+ * numbers a backup by those rounds in two digits or more, so `ankor_round13.txt` is round 14's.
110
+ */
111
+ export declare function backupFileName(roundsPlayed: number): string;
112
+ /**
113
+ * The launch's `restore.file` as the agent wrote it, relative to {@link GAME_INSTALL_DIR}. It is
114
+ * absent on a launch without a restore.
115
+ */
116
+ export declare const RESTORE_FILE = "game/csgo/ankor/restore.txt";
117
+ /**
118
+ * The match the server plays, relative to {@link GAME_INSTALL_DIR}. Every match server has it as
119
+ * {@link MatchConfig} in JSON, and a server without a match has none.
120
+ */
121
+ export declare const MATCH_CONFIG_FILE = "game/csgo/ankor/match.json";
122
+ /**
123
+ * The suffix the plugin adds to a recording's path to create a file once the recording is
124
+ * finished. The agent uploads a `.dem` only once that file exists.
125
+ */
126
+ export declare const DEMO_READY_SUFFIX = ".ready";
127
+ /**
128
+ * The plugin's SwiftlyS2 config file, rendered per launch and bind-mounted over the image's copy.
129
+ * Keys are PascalCase because the framework's configuration provider reads them that way.
130
+ */
131
+ export declare const PluginConfigFile: z.ZodObject<{
132
+ Chat: z.ZodObject<{
133
+ Tag: z.ZodString;
134
+ TagColor: z.ZodEnum<{
135
+ white: "white";
136
+ darkred: "darkred";
137
+ lightpurple: "lightpurple";
138
+ green: "green";
139
+ olive: "olive";
140
+ lime: "lime";
141
+ red: "red";
142
+ gray: "gray";
143
+ lightyellow: "lightyellow";
144
+ yellow: "yellow";
145
+ silver: "silver";
146
+ bluegrey: "bluegrey";
147
+ lightblue: "lightblue";
148
+ blue: "blue";
149
+ darkblue: "darkblue";
150
+ purple: "purple";
151
+ magenta: "magenta";
152
+ lightred: "lightred";
153
+ gold: "gold";
154
+ orange: "orange";
155
+ }>;
156
+ }, z.core.$strip>;
157
+ }, z.core.$strip>;
158
+ export type PluginConfigFile = z.infer<typeof PluginConfigFile>;
159
+ /**
160
+ * Bytes in the largest frame the local event socket carries, on both the plugin's and the relay's
161
+ * side. It is at most {@link EVENT_BATCH_MAX_BYTES}, so an event that fits a frame fits an upload
162
+ * batch.
163
+ */
164
+ export declare const MAX_FRAME_BYTES: number;
165
+ /**
166
+ * Events a plugin may have waiting to be stored on the node. Past this or
167
+ * {@link RELAY_QUEUE_MAX_BYTES}, its history pauses.
168
+ */
169
+ export declare const RELAY_QUEUE_MAX_FRAMES = 512;
170
+ /** Bytes of events a plugin may have waiting to be stored on the node. */
171
+ export declare const RELAY_QUEUE_MAX_BYTES: number;
172
+ /**
173
+ * The directory under `cfg/` that the image ships its cfgs in. The plugin execs them, and a launch
174
+ * may exec any of them.
175
+ */
176
+ export declare const CFG_DIR = "ankor";
177
+ export declare const CFG_NAMES: {
178
+ /**
179
+ * Ankor's settings, which the plugin runs over the game mode's cfg on every map load and when a
180
+ * match goes live.
181
+ */
182
+ readonly base: "ankor/base";
183
+ readonly warmup: "ankor/warmup";
184
+ readonly knife: "ankor/knife";
185
+ /**
186
+ * The cfg the agent renders per launch at `cfg/ankor_launch.cfg`. The plugin runs it after
187
+ * `base`.
188
+ */
189
+ readonly launch: "ankor_launch";
190
+ };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The interface between the agent and the game containers it runs, outside the wire to the
3
+ * controller: the environment the CS2 image and the plugin read, the plugin's config file, where
4
+ * recordings land and how they are named, the cfg files both sides exec, the local plugin socket
5
+ * and the handoff to a replacement agent. `packages/plugin/core/Wire/Launch.cs` mirrors the
6
+ * plugin's half.
7
+ */
8
+ import { z } from 'zod';
9
+ import { ChatColor } from './index.js';
10
+ export * from './plugin-socket.js';
11
+ export * from './agent-image.js';
12
+ /** The path of the agent's local plugin socket. */
13
+ export const PLUGIN_WS_PATH = '/ws/plugin';
14
+ /**
15
+ * The environment variables the image entrypoint reads. The plugin also reads `startMap` and
16
+ * `workshopMap` to know when its map is loaded.
17
+ */
18
+ export const CS2_ENV = {
19
+ /** The server's Game Server Login Token. */
20
+ srcdsToken: 'SRCDS_TOKEN',
21
+ port: 'CS2_PORT',
22
+ tvPort: 'CS2_TV_PORT',
23
+ /** `+tv_enable`, `1` or `0`. GOTV starts with the map, so it is set on the command line. */
24
+ tvEnable: 'CS2_TV_ENABLE',
25
+ gamePassword: 'CS2_PASSWORD',
26
+ maxPlayers: 'CS2_MAXPLAYERS',
27
+ /** `+game_type`, from {@link GAME_MODE_ARGS}. */
28
+ gameType: 'CS2_GAME_TYPE',
29
+ /** `+game_mode`, from {@link GAME_MODE_ARGS}. */
30
+ gameMode: 'CS2_GAME_MODE',
31
+ startMap: 'CS2_STARTMAP',
32
+ /**
33
+ * Passed separately so the entrypoint places `host_workshop_map` after the stock bootstrap map.
34
+ */
35
+ workshopMap: 'CS2_WORKSHOP_MAP',
36
+ /** The Steam Web API key that workshop downloads need, as `-authkey`. */
37
+ authKey: 'CS2_AUTHKEY',
38
+ extraArgs: 'CS2_EXTRA_ARGS',
39
+ updateOnly: 'CS2_UPDATE_ONLY',
40
+ validate: 'CS2_VALIDATE',
41
+ /** Empties the install volume before steamcmd runs. */
42
+ reinstall: 'CS2_REINSTALL',
43
+ /** Whole gigabytes the entrypoint requires free before it lets steamcmd stage anything. */
44
+ minFreeGb: 'CS2_MIN_FREE_GB',
45
+ };
46
+ /**
47
+ * The `+game_type` and `+game_mode` for each {@link GameMode}, as `gamemodes.txt` numbers them. The
48
+ * plugin's `CfgNames.Mode` maps the same pairs back to the modes' cfgs.
49
+ */
50
+ export const GAME_MODE_ARGS = {
51
+ casual: { gameType: 0, gameMode: 0 },
52
+ competitive: { gameType: 0, gameMode: 1 },
53
+ wingman: { gameType: 0, gameMode: 2 },
54
+ rush: { gameType: 0, gameMode: 6 },
55
+ arms_race: { gameType: 1, gameMode: 0 },
56
+ demolition: { gameType: 1, gameMode: 1 },
57
+ deathmatch: { gameType: 1, gameMode: 2 },
58
+ custom: { gameType: 3, gameMode: 0 },
59
+ };
60
+ /** The environment variables that only the plugin reads. */
61
+ export const ANKOR_ENV = {
62
+ /** The local relay the plugin dials for its slot. */
63
+ wsUrl: 'ANKOR_WS_URL',
64
+ /**
65
+ * The server's `allocation_id`, which identifies its events. A plugin without one sends nothing.
66
+ */
67
+ allocationId: 'ANKOR_ALLOCATION_ID',
68
+ debugCaptureDir: 'ANKOR_DEBUG_CAPTURE_DIR',
69
+ };
70
+ /** Where the cs2 image keeps its addon overlay. The agent copies it out once per image id. */
71
+ export const IMAGE_OVERLAY_DIR = '/opt/ankor/overlay';
72
+ /** The game installation inside the container. Per-slot writable paths shadow parts of it. */
73
+ export const GAME_INSTALL_DIR = '/home/steam/cs2-dedicated';
74
+ /**
75
+ * The slot's writable directory, relative to {@link GAME_INSTALL_DIR}. It holds everything per slot
76
+ * that the agent, the plugin and the engine share. The agent lays out its contents, so a new file
77
+ * or directory in it needs no mountpoint in the install.
78
+ */
79
+ export const ANKOR_DIR = 'game/csgo/ankor';
80
+ /** GOTV recordings, relative to {@link GAME_INSTALL_DIR}, kept until uploaded. */
81
+ export const DEMO_DIR = `${ANKOR_DIR}/demos`;
82
+ /** The engine's round backups, relative to {@link GAME_INSTALL_DIR}, emptied at every launch. */
83
+ export const BACKUP_DIR = `${ANKOR_DIR}/backups`;
84
+ /**
85
+ * The file name of the engine's backup of a round, from the rounds played before it. The engine
86
+ * numbers a backup by those rounds in two digits or more, so `ankor_round13.txt` is round 14's.
87
+ */
88
+ export function backupFileName(roundsPlayed) {
89
+ return `ankor_round${String(roundsPlayed).padStart(2, '0')}.txt`;
90
+ }
91
+ /**
92
+ * The launch's `restore.file` as the agent wrote it, relative to {@link GAME_INSTALL_DIR}. It is
93
+ * absent on a launch without a restore.
94
+ */
95
+ export const RESTORE_FILE = `${ANKOR_DIR}/restore.txt`;
96
+ /**
97
+ * The match the server plays, relative to {@link GAME_INSTALL_DIR}. Every match server has it as
98
+ * {@link MatchConfig} in JSON, and a server without a match has none.
99
+ */
100
+ export const MATCH_CONFIG_FILE = `${ANKOR_DIR}/match.json`;
101
+ /**
102
+ * The suffix the plugin adds to a recording's path to create a file once the recording is
103
+ * finished. The agent uploads a `.dem` only once that file exists.
104
+ */
105
+ export const DEMO_READY_SUFFIX = '.ready';
106
+ /**
107
+ * The plugin's SwiftlyS2 config file, rendered per launch and bind-mounted over the image's copy.
108
+ * Keys are PascalCase because the framework's configuration provider reads them that way.
109
+ */
110
+ export const PluginConfigFile = z.object({
111
+ Chat: z.object({ Tag: z.string(), TagColor: ChatColor }),
112
+ });
113
+ /**
114
+ * Bytes in the largest frame the local event socket carries, on both the plugin's and the relay's
115
+ * side. It is at most {@link EVENT_BATCH_MAX_BYTES}, so an event that fits a frame fits an upload
116
+ * batch.
117
+ */
118
+ export const MAX_FRAME_BYTES = 2 << 20;
119
+ /**
120
+ * Events a plugin may have waiting to be stored on the node. Past this or
121
+ * {@link RELAY_QUEUE_MAX_BYTES}, its history pauses.
122
+ */
123
+ export const RELAY_QUEUE_MAX_FRAMES = 512;
124
+ /** Bytes of events a plugin may have waiting to be stored on the node. */
125
+ export const RELAY_QUEUE_MAX_BYTES = 4 << 20;
126
+ /**
127
+ * The directory under `cfg/` that the image ships its cfgs in. The plugin execs them, and a launch
128
+ * may exec any of them.
129
+ */
130
+ export const CFG_DIR = 'ankor';
131
+ export const CFG_NAMES = {
132
+ /**
133
+ * Ankor's settings, which the plugin runs over the game mode's cfg on every map load and when a
134
+ * match goes live.
135
+ */
136
+ base: `${CFG_DIR}/base`,
137
+ warmup: `${CFG_DIR}/warmup`,
138
+ knife: `${CFG_DIR}/knife`,
139
+ /**
140
+ * The cfg the agent renders per launch at `cfg/ankor_launch.cfg`. The plugin runs it after
141
+ * `base`.
142
+ */
143
+ launch: 'ankor_launch',
144
+ };