@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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +202 -0
- package/README.md +72 -2
- package/dist/agent-config.d.ts +101 -0
- package/dist/agent-config.js +144 -0
- package/dist/agent-image.d.ts +12 -0
- package/dist/agent-image.js +12 -0
- package/dist/agent-socket.d.ts +3927 -0
- package/dist/agent-socket.js +1764 -0
- package/dist/agent-yml.d.ts +21 -0
- package/dist/agent-yml.js +71 -0
- package/dist/agent-yml.schema.json +160 -0
- package/dist/asyncapi.yaml +10569 -0
- package/dist/checks.d.ts +19 -0
- package/dist/checks.js +40 -0
- package/dist/container.d.ts +190 -0
- package/dist/container.js +144 -0
- package/dist/event-batch.d.ts +857 -0
- package/dist/event-batch.js +90 -0
- package/dist/game-events.d.ts +2137 -0
- package/dist/game-events.js +1082 -0
- package/dist/helpers.d.ts +176 -0
- package/dist/helpers.js +362 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +26 -0
- package/dist/launch.d.ts +316 -0
- package/dist/launch.js +287 -0
- package/dist/limits.d.ts +381 -0
- package/dist/limits.js +277 -0
- package/dist/paths.d.ts +73 -0
- package/dist/paths.js +73 -0
- package/dist/plugin-socket.d.ts +1946 -0
- package/dist/plugin-socket.js +160 -0
- package/dist/session.d.ts +127 -0
- package/dist/session.js +114 -0
- package/dist/values.d.ts +186 -0
- package/dist/values.js +230 -0
- package/node-setup.sh +1061 -0
- package/package.json +57 -4
package/dist/launch.d.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a game server is launched with: the configuration that the controller sends the agent in
|
|
3
|
+
* {@link ControllerStartServer | start_server}. The agent's `container.ts` defines what the agent hands
|
|
4
|
+
* the container and the plugin inside it.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
/**
|
|
8
|
+
* How starting sides are decided.
|
|
9
|
+
*
|
|
10
|
+
* - `knife`: a knife round decides them.
|
|
11
|
+
* - `team1_ct`: team 1 starts as CT.
|
|
12
|
+
* - `team1_t`: team 1 starts as T.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SidesMode: z.ZodEnum<{
|
|
15
|
+
knife: "knife";
|
|
16
|
+
team1_ct: "team1_ct";
|
|
17
|
+
team1_t: "team1_t";
|
|
18
|
+
}>;
|
|
19
|
+
export type SidesMode = z.infer<typeof SidesMode>;
|
|
20
|
+
/**
|
|
21
|
+
* How warmup ends.
|
|
22
|
+
*
|
|
23
|
+
* - `auto`: counts down once every rostered player is connected. Players have no ready commands.
|
|
24
|
+
* - `manual`: also waits for each rostered player and each rostered spectator to type `!ready`,
|
|
25
|
+
* which `!unready` withdraws. A captain's `!forceready` readies the captain's team with
|
|
26
|
+
* whichever of its players are connected, and any rostered spectator's `!forceready` readies the
|
|
27
|
+
* spectators.
|
|
28
|
+
* - `external`: only {@link ControllerStartMatch | start_match} from the controller starts the match, and
|
|
29
|
+
* players have no commands for it.
|
|
30
|
+
*
|
|
31
|
+
* In every mode, {@link ControllerStartMatch | start_match} from the controller starts the match.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ReadyMode: z.ZodEnum<{
|
|
34
|
+
auto: "auto";
|
|
35
|
+
manual: "manual";
|
|
36
|
+
external: "external";
|
|
37
|
+
}>;
|
|
38
|
+
export type ReadyMode = z.infer<typeof ReadyMode>;
|
|
39
|
+
/**
|
|
40
|
+
* {@link MatchRules} as a launch asks for them, where each rule left out takes the value of a
|
|
41
|
+
* typical competitive match. The reports of {@link GoingLive | going_live} and
|
|
42
|
+
* {@link RoundBackup | round_backup} always carry every rule.
|
|
43
|
+
*/
|
|
44
|
+
export declare const LaunchRules: z.ZodObject<{
|
|
45
|
+
max_rounds: z.ZodDefault<z.ZodNumber>;
|
|
46
|
+
overtime: z.ZodDefault<z.ZodBoolean>;
|
|
47
|
+
overtime_max_rounds: z.ZodDefault<z.ZodNumber>;
|
|
48
|
+
friendly_fire: z.ZodDefault<z.ZodBoolean>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export type LaunchRules = z.infer<typeof LaunchRules>;
|
|
51
|
+
/**
|
|
52
|
+
* The match a server is launched with, which covers one map. Each server plays one match. A restore
|
|
53
|
+
* launches the same `match_id` again under a new `allocation_id`, from a
|
|
54
|
+
* {@link RoundBackup | round_backup} that the first server sent.
|
|
55
|
+
*/
|
|
56
|
+
export declare const MatchConfig: z.ZodObject<{
|
|
57
|
+
match_id: z.ZodDefault<z.ZodNullable<z.ZodUUID>>;
|
|
58
|
+
send_statistics: z.ZodDefault<z.ZodBoolean>;
|
|
59
|
+
roster: z.ZodObject<{
|
|
60
|
+
team1: z.ZodObject<{
|
|
61
|
+
name: z.ZodString;
|
|
62
|
+
players: z.ZodArray<z.ZodString>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
team2: z.ZodObject<{
|
|
65
|
+
name: z.ZodString;
|
|
66
|
+
players: z.ZodArray<z.ZodString>;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
spectators: z.ZodArray<z.ZodString>;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
sides: z.ZodEnum<{
|
|
71
|
+
knife: "knife";
|
|
72
|
+
team1_ct: "team1_ct";
|
|
73
|
+
team1_t: "team1_t";
|
|
74
|
+
}>;
|
|
75
|
+
knife_decision_seconds: z.ZodDefault<z.ZodNumber>;
|
|
76
|
+
ready_mode: z.ZodDefault<z.ZodEnum<{
|
|
77
|
+
auto: "auto";
|
|
78
|
+
manual: "manual";
|
|
79
|
+
external: "external";
|
|
80
|
+
}>>;
|
|
81
|
+
spectators_ready_up: z.ZodDefault<z.ZodBoolean>;
|
|
82
|
+
ready_timeout_seconds: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
83
|
+
record_demo: z.ZodBoolean;
|
|
84
|
+
debug_capture: z.ZodDefault<z.ZodBoolean>;
|
|
85
|
+
rules: z.ZodPrefault<z.ZodObject<{
|
|
86
|
+
max_rounds: z.ZodDefault<z.ZodNumber>;
|
|
87
|
+
overtime: z.ZodDefault<z.ZodBoolean>;
|
|
88
|
+
overtime_max_rounds: z.ZodDefault<z.ZodNumber>;
|
|
89
|
+
friendly_fire: z.ZodDefault<z.ZodBoolean>;
|
|
90
|
+
}, z.core.$strip>>;
|
|
91
|
+
restore: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
92
|
+
team1_side: z.ZodEnum<{
|
|
93
|
+
t: "t";
|
|
94
|
+
ct: "ct";
|
|
95
|
+
}>;
|
|
96
|
+
rules: z.ZodObject<{
|
|
97
|
+
max_rounds: z.ZodNumber;
|
|
98
|
+
overtime: z.ZodBoolean;
|
|
99
|
+
overtime_max_rounds: z.ZodNumber;
|
|
100
|
+
friendly_fire: z.ZodBoolean;
|
|
101
|
+
}, z.core.$strip>;
|
|
102
|
+
file: z.ZodString;
|
|
103
|
+
}, z.core.$strip>>>;
|
|
104
|
+
}, z.core.$strip>;
|
|
105
|
+
export type MatchConfig = z.infer<typeof MatchConfig>;
|
|
106
|
+
/** A match as a controller builds it, which may leave out the fields that have a default. */
|
|
107
|
+
export type MatchConfigInput = z.input<typeof MatchConfig>;
|
|
108
|
+
/** Control characters, which a launch value must not carry into generated cfg commands. */
|
|
109
|
+
export declare const CFG_CONTROL_CHARS: RegExp;
|
|
110
|
+
/**
|
|
111
|
+
* The password players type. It is printable ASCII without spaces, `"` or `;`, because the server
|
|
112
|
+
* would read those as separators.
|
|
113
|
+
*/
|
|
114
|
+
export declare const GamePassword: z.ZodString;
|
|
115
|
+
/** A Steam Web API key, which the server needs to download workshop maps. */
|
|
116
|
+
export declare const SteamWebApiKey: z.ZodString;
|
|
117
|
+
/** The name of a stock map, such as `de_mirage`. A name of all digits would be a workshop id. */
|
|
118
|
+
export declare const StockMapName: z.ZodString;
|
|
119
|
+
/** A Steam Workshop map's id. */
|
|
120
|
+
export declare const WorkshopId: z.ZodString;
|
|
121
|
+
/** The colour names SwiftlyS2 accepts in chat markup. */
|
|
122
|
+
export declare const CHAT_COLORS: readonly ["white", "darkred", "lightpurple", "green", "olive", "lime", "red", "gray", "lightyellow", "yellow", "silver", "bluegrey", "lightblue", "blue", "darkblue", "purple", "magenta", "lightred", "gold", "orange"];
|
|
123
|
+
/**
|
|
124
|
+
* A chat colour name from
|
|
125
|
+
* [SwiftlyS2's chat styling](https://swiftlys2.net/docs/guides/chat-and-html-styling).
|
|
126
|
+
*/
|
|
127
|
+
export declare const ChatColor: z.ZodEnum<{
|
|
128
|
+
white: "white";
|
|
129
|
+
darkred: "darkred";
|
|
130
|
+
lightpurple: "lightpurple";
|
|
131
|
+
green: "green";
|
|
132
|
+
olive: "olive";
|
|
133
|
+
lime: "lime";
|
|
134
|
+
red: "red";
|
|
135
|
+
gray: "gray";
|
|
136
|
+
lightyellow: "lightyellow";
|
|
137
|
+
yellow: "yellow";
|
|
138
|
+
silver: "silver";
|
|
139
|
+
bluegrey: "bluegrey";
|
|
140
|
+
lightblue: "lightblue";
|
|
141
|
+
blue: "blue";
|
|
142
|
+
darkblue: "darkblue";
|
|
143
|
+
purple: "purple";
|
|
144
|
+
magenta: "magenta";
|
|
145
|
+
lightred: "lightred";
|
|
146
|
+
gold: "gold";
|
|
147
|
+
orange: "orange";
|
|
148
|
+
}>;
|
|
149
|
+
export type ChatColor = z.infer<typeof ChatColor>;
|
|
150
|
+
/** The bracketed tag in front of every plugin chat line. */
|
|
151
|
+
export declare const ChatTag: z.ZodObject<{
|
|
152
|
+
text: z.ZodDefault<z.ZodString>;
|
|
153
|
+
color: z.ZodDefault<z.ZodEnum<{
|
|
154
|
+
white: "white";
|
|
155
|
+
darkred: "darkred";
|
|
156
|
+
lightpurple: "lightpurple";
|
|
157
|
+
green: "green";
|
|
158
|
+
olive: "olive";
|
|
159
|
+
lime: "lime";
|
|
160
|
+
red: "red";
|
|
161
|
+
gray: "gray";
|
|
162
|
+
lightyellow: "lightyellow";
|
|
163
|
+
yellow: "yellow";
|
|
164
|
+
silver: "silver";
|
|
165
|
+
bluegrey: "bluegrey";
|
|
166
|
+
lightblue: "lightblue";
|
|
167
|
+
blue: "blue";
|
|
168
|
+
darkblue: "darkblue";
|
|
169
|
+
purple: "purple";
|
|
170
|
+
magenta: "magenta";
|
|
171
|
+
lightred: "lightred";
|
|
172
|
+
gold: "gold";
|
|
173
|
+
orange: "orange";
|
|
174
|
+
}>>;
|
|
175
|
+
}, z.core.$strip>;
|
|
176
|
+
export type ChatTag = z.infer<typeof ChatTag>;
|
|
177
|
+
/**
|
|
178
|
+
* A cfg the server runs, relative to `game/csgo/cfg/` and without `.cfg`, such as `node/<name>`
|
|
179
|
+
* from the node's cfgs.
|
|
180
|
+
*/
|
|
181
|
+
export declare const CfgName: z.ZodString;
|
|
182
|
+
/**
|
|
183
|
+
* The game mode a server runs, which sets the game's rules. The engine runs the mode's cfg, such
|
|
184
|
+
* as `gamemode_competitive` or `gamemode_competitive2v2` for `wingman`, on every map load, and the
|
|
185
|
+
* plugin runs it again when a match goes live.
|
|
186
|
+
*
|
|
187
|
+
* `custom` is Valve's mode for community content. Its cfg sets little, so the launch's `exec` and
|
|
188
|
+
* `commands` set the game.
|
|
189
|
+
*
|
|
190
|
+
* A match plays only in `casual`, `competitive` or `wingman`. A server without a match runs in any
|
|
191
|
+
* mode.
|
|
192
|
+
*/
|
|
193
|
+
export declare const GameMode: z.ZodEnum<{
|
|
194
|
+
custom: "custom";
|
|
195
|
+
casual: "casual";
|
|
196
|
+
competitive: "competitive";
|
|
197
|
+
wingman: "wingman";
|
|
198
|
+
rush: "rush";
|
|
199
|
+
arms_race: "arms_race";
|
|
200
|
+
demolition: "demolition";
|
|
201
|
+
deathmatch: "deathmatch";
|
|
202
|
+
}>;
|
|
203
|
+
export type GameMode = z.infer<typeof GameMode>;
|
|
204
|
+
/**
|
|
205
|
+
* What a game server is launched with: any CS2 server, and the match Ankor's plugin plays on it, if
|
|
206
|
+
* any.
|
|
207
|
+
*
|
|
208
|
+
* Rejected when both `workshop_id` and `map_name` are null. With a `match`, it is also rejected:
|
|
209
|
+
*
|
|
210
|
+
* - when `workshop_id` is set, because a match plays a stock map.
|
|
211
|
+
* - when the roster's players and spectators together outnumber `max_players`.
|
|
212
|
+
* - when `game_mode` is not `casual`, `competitive` or `wingman`.
|
|
213
|
+
* - when `ready_mode` is `external` and `ready_timeout_seconds` is set.
|
|
214
|
+
*/
|
|
215
|
+
export declare const ServerLaunchConfig: z.ZodObject<{
|
|
216
|
+
hostname: z.ZodDefault<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodString>>>;
|
|
217
|
+
steam_web_api_key: z.ZodDefault<z.ZodNullable<z.ZodUnion<[z.ZodString, z.ZodLiteral<"">]>>>;
|
|
218
|
+
game_password: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
219
|
+
chat_tag: z.ZodPrefault<z.ZodObject<{
|
|
220
|
+
text: z.ZodDefault<z.ZodString>;
|
|
221
|
+
color: z.ZodDefault<z.ZodEnum<{
|
|
222
|
+
white: "white";
|
|
223
|
+
darkred: "darkred";
|
|
224
|
+
lightpurple: "lightpurple";
|
|
225
|
+
green: "green";
|
|
226
|
+
olive: "olive";
|
|
227
|
+
lime: "lime";
|
|
228
|
+
red: "red";
|
|
229
|
+
gray: "gray";
|
|
230
|
+
lightyellow: "lightyellow";
|
|
231
|
+
yellow: "yellow";
|
|
232
|
+
silver: "silver";
|
|
233
|
+
bluegrey: "bluegrey";
|
|
234
|
+
lightblue: "lightblue";
|
|
235
|
+
blue: "blue";
|
|
236
|
+
darkblue: "darkblue";
|
|
237
|
+
purple: "purple";
|
|
238
|
+
magenta: "magenta";
|
|
239
|
+
lightred: "lightred";
|
|
240
|
+
gold: "gold";
|
|
241
|
+
orange: "orange";
|
|
242
|
+
}>>;
|
|
243
|
+
}, z.core.$strip>>;
|
|
244
|
+
game_mode: z.ZodDefault<z.ZodEnum<{
|
|
245
|
+
custom: "custom";
|
|
246
|
+
casual: "casual";
|
|
247
|
+
competitive: "competitive";
|
|
248
|
+
wingman: "wingman";
|
|
249
|
+
rush: "rush";
|
|
250
|
+
arms_race: "arms_race";
|
|
251
|
+
demolition: "demolition";
|
|
252
|
+
deathmatch: "deathmatch";
|
|
253
|
+
}>>;
|
|
254
|
+
max_players: z.ZodNumber;
|
|
255
|
+
workshop_id: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
256
|
+
map_name: z.ZodNullable<z.ZodString>;
|
|
257
|
+
gotv: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
258
|
+
password: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
259
|
+
delay_seconds: z.ZodNumber;
|
|
260
|
+
}, z.core.$strip>>>;
|
|
261
|
+
idle_shutdown_seconds: z.ZodNullable<z.ZodNumber>;
|
|
262
|
+
exec: z.ZodArray<z.ZodString>;
|
|
263
|
+
commands: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodString>>;
|
|
264
|
+
match: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
265
|
+
match_id: z.ZodDefault<z.ZodNullable<z.ZodUUID>>;
|
|
266
|
+
send_statistics: z.ZodDefault<z.ZodBoolean>;
|
|
267
|
+
roster: z.ZodObject<{
|
|
268
|
+
team1: z.ZodObject<{
|
|
269
|
+
name: z.ZodString;
|
|
270
|
+
players: z.ZodArray<z.ZodString>;
|
|
271
|
+
}, z.core.$strip>;
|
|
272
|
+
team2: z.ZodObject<{
|
|
273
|
+
name: z.ZodString;
|
|
274
|
+
players: z.ZodArray<z.ZodString>;
|
|
275
|
+
}, z.core.$strip>;
|
|
276
|
+
spectators: z.ZodArray<z.ZodString>;
|
|
277
|
+
}, z.core.$strip>;
|
|
278
|
+
sides: z.ZodEnum<{
|
|
279
|
+
knife: "knife";
|
|
280
|
+
team1_ct: "team1_ct";
|
|
281
|
+
team1_t: "team1_t";
|
|
282
|
+
}>;
|
|
283
|
+
knife_decision_seconds: z.ZodDefault<z.ZodNumber>;
|
|
284
|
+
ready_mode: z.ZodDefault<z.ZodEnum<{
|
|
285
|
+
auto: "auto";
|
|
286
|
+
manual: "manual";
|
|
287
|
+
external: "external";
|
|
288
|
+
}>>;
|
|
289
|
+
spectators_ready_up: z.ZodDefault<z.ZodBoolean>;
|
|
290
|
+
ready_timeout_seconds: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
291
|
+
record_demo: z.ZodBoolean;
|
|
292
|
+
debug_capture: z.ZodDefault<z.ZodBoolean>;
|
|
293
|
+
rules: z.ZodPrefault<z.ZodObject<{
|
|
294
|
+
max_rounds: z.ZodDefault<z.ZodNumber>;
|
|
295
|
+
overtime: z.ZodDefault<z.ZodBoolean>;
|
|
296
|
+
overtime_max_rounds: z.ZodDefault<z.ZodNumber>;
|
|
297
|
+
friendly_fire: z.ZodDefault<z.ZodBoolean>;
|
|
298
|
+
}, z.core.$strip>>;
|
|
299
|
+
restore: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
300
|
+
team1_side: z.ZodEnum<{
|
|
301
|
+
t: "t";
|
|
302
|
+
ct: "ct";
|
|
303
|
+
}>;
|
|
304
|
+
rules: z.ZodObject<{
|
|
305
|
+
max_rounds: z.ZodNumber;
|
|
306
|
+
overtime: z.ZodBoolean;
|
|
307
|
+
overtime_max_rounds: z.ZodNumber;
|
|
308
|
+
friendly_fire: z.ZodBoolean;
|
|
309
|
+
}, z.core.$strip>;
|
|
310
|
+
file: z.ZodString;
|
|
311
|
+
}, z.core.$strip>>>;
|
|
312
|
+
}, z.core.$strip>>>;
|
|
313
|
+
}, z.core.$strip>;
|
|
314
|
+
export type ServerLaunchConfig = z.infer<typeof ServerLaunchConfig>;
|
|
315
|
+
/** A launch as a controller builds it, which may leave out the fields that have a default. */
|
|
316
|
+
export type ServerLaunchConfigInput = z.input<typeof ServerLaunchConfig>;
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a game server is launched with: the configuration that the controller sends the agent in
|
|
3
|
+
* {@link ControllerStartServer | start_server}. The agent's `container.ts` defines what the agent hands
|
|
4
|
+
* the container and the plugin inside it.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { MatchId, MatchRules, MAX_SERVER_PLAYERS, RestoreFile, Roster, } from './game-events.js';
|
|
8
|
+
/**
|
|
9
|
+
* How starting sides are decided.
|
|
10
|
+
*
|
|
11
|
+
* - `knife`: a knife round decides them.
|
|
12
|
+
* - `team1_ct`: team 1 starts as CT.
|
|
13
|
+
* - `team1_t`: team 1 starts as T.
|
|
14
|
+
*/
|
|
15
|
+
export const SidesMode = z.enum(['knife', 'team1_ct', 'team1_t']);
|
|
16
|
+
/**
|
|
17
|
+
* How warmup ends.
|
|
18
|
+
*
|
|
19
|
+
* - `auto`: counts down once every rostered player is connected. Players have no ready commands.
|
|
20
|
+
* - `manual`: also waits for each rostered player and each rostered spectator to type `!ready`,
|
|
21
|
+
* which `!unready` withdraws. A captain's `!forceready` readies the captain's team with
|
|
22
|
+
* whichever of its players are connected, and any rostered spectator's `!forceready` readies the
|
|
23
|
+
* spectators.
|
|
24
|
+
* - `external`: only {@link ControllerStartMatch | start_match} from the controller starts the match, and
|
|
25
|
+
* players have no commands for it.
|
|
26
|
+
*
|
|
27
|
+
* In every mode, {@link ControllerStartMatch | start_match} from the controller starts the match.
|
|
28
|
+
*/
|
|
29
|
+
export const ReadyMode = z.enum(['auto', 'manual', 'external']);
|
|
30
|
+
/**
|
|
31
|
+
* {@link MatchRules} as a launch asks for them, where each rule left out takes the value of a
|
|
32
|
+
* typical competitive match. The reports of {@link GoingLive | going_live} and
|
|
33
|
+
* {@link RoundBackup | round_backup} always carry every rule.
|
|
34
|
+
*/
|
|
35
|
+
export const LaunchRules = MatchRules.extend({
|
|
36
|
+
/** Rounds in regulation, as `mp_maxrounds`. When absent, 24. */
|
|
37
|
+
max_rounds: MatchRules.shape.max_rounds.default(24),
|
|
38
|
+
/**
|
|
39
|
+
* Whether a tie after regulation goes to overtime, as `mp_overtime_enable`. When absent, `true`.
|
|
40
|
+
*/
|
|
41
|
+
overtime: MatchRules.shape.overtime.default(true),
|
|
42
|
+
/** Rounds in one overtime, both halves together, as `mp_overtime_maxrounds`. When absent, 6. */
|
|
43
|
+
overtime_max_rounds: MatchRules.shape.overtime_max_rounds.default(6),
|
|
44
|
+
/** Whether bullets and melee hurt teammates. Grenades always do. When absent, `true`. */
|
|
45
|
+
friendly_fire: MatchRules.shape.friendly_fire.default(true),
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* The match a server is launched with, which covers one map. Each server plays one match. A restore
|
|
49
|
+
* launches the same `match_id` again under a new `allocation_id`, from a
|
|
50
|
+
* {@link RoundBackup | round_backup} that the first server sent.
|
|
51
|
+
*/
|
|
52
|
+
export const MatchConfig = z.object({
|
|
53
|
+
/**
|
|
54
|
+
* The match's id, which names its demos and which {@link AgentSlotStatus | slot_status} and
|
|
55
|
+
* {@link ControllerGetServerStatus | get_server_status} report. Null for none, and when absent.
|
|
56
|
+
*/
|
|
57
|
+
match_id: MatchId.nullable().default(null),
|
|
58
|
+
/**
|
|
59
|
+
* Whether the plugin sends the match's gameplay events, its {@link RoundBackup | round_backup}
|
|
60
|
+
* among them, which the agent uploads in an {@link EventBatch | event_batch}. When absent,
|
|
61
|
+
* `false`.
|
|
62
|
+
*
|
|
63
|
+
* Every match reports its {@link AgentPhaseChange | phase_change} and {@link AgentScore | score}.
|
|
64
|
+
* A match without statistics sends each {@link RoundBackup | round_backup} in
|
|
65
|
+
* {@link AgentBackup | backup} instead, and a match with statistics only in its events.
|
|
66
|
+
*/
|
|
67
|
+
send_statistics: z.boolean().default(false),
|
|
68
|
+
/** Who may join, and on which team. */
|
|
69
|
+
roster: Roster,
|
|
70
|
+
/** How starting sides are decided. */
|
|
71
|
+
sides: SidesMode,
|
|
72
|
+
/**
|
|
73
|
+
* Seconds the knife winners' captain has to type `!stay`, `!swap`, `!ct` or `!t`, from 5 to 300.
|
|
74
|
+
* The server stays in warmup meanwhile, with its clock counting these seconds down. An answer
|
|
75
|
+
* starts the match immediately. Without one, the team stays on its side when the clock runs out.
|
|
76
|
+
* When absent, 60.
|
|
77
|
+
*/
|
|
78
|
+
knife_decision_seconds: z.number().int().min(5).max(300).default(60),
|
|
79
|
+
/** How warmup ends. When absent, `auto`. */
|
|
80
|
+
ready_mode: ReadyMode.default('auto'),
|
|
81
|
+
/**
|
|
82
|
+
* Whether, under `manual`, the match also waits for every rostered spectator to connect and type
|
|
83
|
+
* `!ready`, and any of them can force-ready the spectators. When absent, `false`, so the match
|
|
84
|
+
* waits for the teams alone.
|
|
85
|
+
*/
|
|
86
|
+
spectators_ready_up: z.boolean().default(false),
|
|
87
|
+
/**
|
|
88
|
+
* Seconds from warmup until both teams must be ready, up to 1800. Null for no time limit, and
|
|
89
|
+
* when absent.
|
|
90
|
+
*
|
|
91
|
+
* When the time passes, the server kicks every player and the map ends with
|
|
92
|
+
* {@link MapEnd | map_end}, which says who was connected and ready. The controller decides what
|
|
93
|
+
* that means for the match. Must be null with `external`, where only the controller starts the
|
|
94
|
+
* match.
|
|
95
|
+
*/
|
|
96
|
+
ready_timeout_seconds: z.number().int().positive().max(1800).nullable().default(null),
|
|
97
|
+
/**
|
|
98
|
+
* Whether GOTV records the live match as a demo.
|
|
99
|
+
*/
|
|
100
|
+
record_demo: z.boolean(),
|
|
101
|
+
/**
|
|
102
|
+
* Whether the plugin records a development capture of its callbacks. Only a match that sends
|
|
103
|
+
* statistics records one. When absent, `false`.
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
debug_capture: z.boolean().default(false),
|
|
108
|
+
/**
|
|
109
|
+
* The rules the match is played under, applied over those of the game mode. When absent, every
|
|
110
|
+
* rule takes its default.
|
|
111
|
+
*/
|
|
112
|
+
rules: LaunchRules.prefault({}),
|
|
113
|
+
/**
|
|
114
|
+
* A live round to play on from, taken from the {@link RoundBackup | round_backup} that reported
|
|
115
|
+
* it. Null for a match that plays the map from its start, and when absent.
|
|
116
|
+
*
|
|
117
|
+
* With a restore, the server skips the knife round and the side choice whatever `sides` says, and
|
|
118
|
+
* after warmup it loads the backup instead of going live. Team 1 takes the restore's
|
|
119
|
+
* `team1_side`, the round and the scores come from the file, and the restore's `rules` replace
|
|
120
|
+
* the match's `rules`. The agent writes `file` to the slot for `mp_backup_restore_load_file`.
|
|
121
|
+
*/
|
|
122
|
+
restore: RestoreFile.nullable().default(null),
|
|
123
|
+
});
|
|
124
|
+
/** Control characters, which a launch value must not carry into generated cfg commands. */
|
|
125
|
+
export const CFG_CONTROL_CHARS = /[\r\n\u0000-\u001f\u007f]/;
|
|
126
|
+
const CFG_TEXT_MESSAGE = 'must not contain control characters: a newline would end the console line and the rest would ' +
|
|
127
|
+
'be parsed as another command';
|
|
128
|
+
const cfgText = z.string().refine((value) => !CFG_CONTROL_CHARS.test(value), CFG_TEXT_MESSAGE);
|
|
129
|
+
/**
|
|
130
|
+
* The password players type. It is printable ASCII without spaces, `"` or `;`, because the server
|
|
131
|
+
* would read those as separators.
|
|
132
|
+
*/
|
|
133
|
+
export const GamePassword = z.string().max(128).regex(/^[!#-:<-~]*$/, 'must be printable ASCII without spaces, quotes or semicolons');
|
|
134
|
+
/** A Steam Web API key, which the server needs to download workshop maps. */
|
|
135
|
+
export const SteamWebApiKey = z.string().regex(/^[A-Za-z0-9]{1,64}$/, 'a Steam Web API key is letters and digits');
|
|
136
|
+
/** The name of a stock map, such as `de_mirage`. A name of all digits would be a workshop id. */
|
|
137
|
+
export const StockMapName = z.string().regex(/^(?!\d+$)[A-Za-z0-9_-]{1,64}$/, 'must be a stock map name');
|
|
138
|
+
/** A Steam Workshop map's id. */
|
|
139
|
+
export const WorkshopId = z.string().regex(/^\d{1,20}$/, 'must be a numeric workshop id');
|
|
140
|
+
// The plugin's ChatTagTests mirrors this list by hand; change both together.
|
|
141
|
+
/** The colour names SwiftlyS2 accepts in chat markup. */
|
|
142
|
+
export const CHAT_COLORS = [
|
|
143
|
+
'white', 'darkred', 'lightpurple', 'green', 'olive', 'lime', 'red', 'gray', 'lightyellow',
|
|
144
|
+
'yellow', 'silver', 'bluegrey', 'lightblue', 'blue', 'darkblue', 'purple', 'magenta', 'lightred',
|
|
145
|
+
'gold', 'orange',
|
|
146
|
+
];
|
|
147
|
+
/**
|
|
148
|
+
* A chat colour name from
|
|
149
|
+
* [SwiftlyS2's chat styling](https://swiftlys2.net/docs/guides/chat-and-html-styling).
|
|
150
|
+
*/
|
|
151
|
+
export const ChatColor = z.enum(CHAT_COLORS);
|
|
152
|
+
/** The bracketed tag in front of every plugin chat line. */
|
|
153
|
+
export const ChatTag = z.object({
|
|
154
|
+
/**
|
|
155
|
+
* The tag's text, without brackets or control characters. Brackets would end the tag early in
|
|
156
|
+
* SwiftlyS2's colour markup. When absent, `Ankor`.
|
|
157
|
+
*/
|
|
158
|
+
text: z.string().min(1).max(24).regex(/^[^[\]\p{Cc}]+$/u, 'must not contain brackets or control characters').default('Ankor'),
|
|
159
|
+
/** The tag's colour. When absent, `blue`. */
|
|
160
|
+
color: ChatColor.default('blue'),
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* A cfg the server runs, relative to `game/csgo/cfg/` and without `.cfg`, such as `node/<name>`
|
|
164
|
+
* from the node's cfgs.
|
|
165
|
+
*/
|
|
166
|
+
export const CfgName = z.string().max(128).regex(/^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$/, 'must be cfg path segments of letters, digits, `_` and `-`, without `.cfg`');
|
|
167
|
+
/**
|
|
168
|
+
* The game mode a server runs, which sets the game's rules. The engine runs the mode's cfg, such
|
|
169
|
+
* as `gamemode_competitive` or `gamemode_competitive2v2` for `wingman`, on every map load, and the
|
|
170
|
+
* plugin runs it again when a match goes live.
|
|
171
|
+
*
|
|
172
|
+
* `custom` is Valve's mode for community content. Its cfg sets little, so the launch's `exec` and
|
|
173
|
+
* `commands` set the game.
|
|
174
|
+
*
|
|
175
|
+
* A match plays only in `casual`, `competitive` or `wingman`. A server without a match runs in any
|
|
176
|
+
* mode.
|
|
177
|
+
*/
|
|
178
|
+
export const GameMode = z.enum(['casual', 'competitive', 'wingman', 'rush', 'arms_race', 'demolition', 'deathmatch', 'custom']);
|
|
179
|
+
/** The player capacity of a server. */
|
|
180
|
+
const MaxPlayers = z.number().int().min(1).max(MAX_SERVER_PLAYERS);
|
|
181
|
+
/** The game modes that a match plays in: Counter-Strike's own, whose rounds end. */
|
|
182
|
+
const MATCH_GAME_MODES = ['casual', 'competitive', 'wingman'];
|
|
183
|
+
/**
|
|
184
|
+
* What a game server is launched with: any CS2 server, and the match Ankor's plugin plays on it, if
|
|
185
|
+
* any.
|
|
186
|
+
*
|
|
187
|
+
* Rejected when both `workshop_id` and `map_name` are null. With a `match`, it is also rejected:
|
|
188
|
+
*
|
|
189
|
+
* - when `workshop_id` is set, because a match plays a stock map.
|
|
190
|
+
* - when the roster's players and spectators together outnumber `max_players`.
|
|
191
|
+
* - when `game_mode` is not `casual`, `competitive` or `wingman`.
|
|
192
|
+
* - when `ready_mode` is `external` and `ready_timeout_seconds` is set.
|
|
193
|
+
*/
|
|
194
|
+
export const ServerLaunchConfig = z.object({
|
|
195
|
+
/**
|
|
196
|
+
* The server's name, which players see in game, such as on the scoreboard. It is up to 128
|
|
197
|
+
* characters. Null or empty leaves the game's own name, and null is the value when absent.
|
|
198
|
+
* Rejected when it holds an ASCII control character, such as a line break.
|
|
199
|
+
*/
|
|
200
|
+
hostname: cfgText.pipe(z.string().max(128)).nullable().default(null),
|
|
201
|
+
/**
|
|
202
|
+
* The Steam Web API key that workshop downloads need. Null or empty skips them, and null is the
|
|
203
|
+
* value when absent. **Whoever runs the node can read the key.**
|
|
204
|
+
*/
|
|
205
|
+
steam_web_api_key: SteamWebApiKey.or(z.literal('')).nullable().default(null),
|
|
206
|
+
/** The password players type to join. Null or empty for none, and null when absent. */
|
|
207
|
+
game_password: GamePassword.nullable().default(null),
|
|
208
|
+
/**
|
|
209
|
+
* The tag in front of the plugin's chat lines. When absent, each of its fields takes its default.
|
|
210
|
+
*/
|
|
211
|
+
chat_tag: ChatTag.prefault({}),
|
|
212
|
+
/** The game mode, which sets the game's rules. When absent, `competitive`. */
|
|
213
|
+
game_mode: GameMode.default('competitive'),
|
|
214
|
+
/**
|
|
215
|
+
* The player capacity of the server, as `-maxplayers`. A match's roster, players and spectators
|
|
216
|
+
* together, must fit.
|
|
217
|
+
*/
|
|
218
|
+
max_players: MaxPlayers,
|
|
219
|
+
/** A workshop map to play. Null for a stock map, and when absent. A match plays a stock map. */
|
|
220
|
+
workshop_id: WorkshopId.nullable().default(null),
|
|
221
|
+
/**
|
|
222
|
+
* The stock map to play, required without `workshop_id`. With a `workshop_id`, the server boots
|
|
223
|
+
* this map before it switches to the workshop map. Null for `de_dust2` in that case.
|
|
224
|
+
*/
|
|
225
|
+
map_name: StockMapName.nullable(),
|
|
226
|
+
/**
|
|
227
|
+
* GOTV that spectators can watch. Null for none, and null when absent. A match that
|
|
228
|
+
* records a demo still runs GOTV then, because demos are recorded through it. That GOTV runs with
|
|
229
|
+
* `tv_delay 0` and a random `tv_password` that the agent sends nowhere, so no spectator can join.
|
|
230
|
+
*
|
|
231
|
+
* Whether GOTV runs is set on the command line, because GOTV starts with the map. The other
|
|
232
|
+
* settings apply on every map load and when a match goes live, never while a demo records.
|
|
233
|
+
*/
|
|
234
|
+
gotv: z.object({
|
|
235
|
+
/**
|
|
236
|
+
* The GOTV password, as `tv_password`, apart from `game_password`. Null or empty for none, and
|
|
237
|
+
* null when absent.
|
|
238
|
+
*/
|
|
239
|
+
password: GamePassword.nullable().default(null),
|
|
240
|
+
/** Seconds GOTV runs behind the game. */
|
|
241
|
+
delay_seconds: z.number().int().nonnegative(),
|
|
242
|
+
}).nullable().default(null),
|
|
243
|
+
/**
|
|
244
|
+
* Seconds the server may stay empty once its slot is `running` before the agent stops it,
|
|
245
|
+
* reporting `idle_empty` as its {@link ServerExitReason}. Null for a server that runs until
|
|
246
|
+
* stopped.
|
|
247
|
+
* Hibernation must stay off. See [When the cfgs run](https://docs.ankor.sh/protocol/#when-the-cfgs-run).
|
|
248
|
+
*/
|
|
249
|
+
idle_shutdown_seconds: z.number().int().positive().nullable(),
|
|
250
|
+
/**
|
|
251
|
+
* Cfgs the server runs in order, after the game mode's cfg and the match's `rules`. A server
|
|
252
|
+
* without a match runs them 1 second after every map load, and a match server runs them at warmup
|
|
253
|
+
* and when the match goes live. Warmup and the knife round run Ankor's cfgs over them.
|
|
254
|
+
*/
|
|
255
|
+
exec: z.array(CfgName).max(16),
|
|
256
|
+
/**
|
|
257
|
+
* Console lines that the server runs after `exec`, in order and at the same moments, so they
|
|
258
|
+
* override `exec` and `rules`. Rejected when a line holds an ASCII control character, such as a
|
|
259
|
+
* line break, which would end the console line.
|
|
260
|
+
*/
|
|
261
|
+
commands: z.array(cfgText.pipe(z.string().min(1).max(256))).max(128),
|
|
262
|
+
/**
|
|
263
|
+
* The match Ankor's plugin plays. Null runs a plain CS2 server, and is the value when absent. The
|
|
264
|
+
* plugin then runs no match, and the map, `exec`, `commands` and the node's plugins set the game.
|
|
265
|
+
*/
|
|
266
|
+
match: MatchConfig.nullable().default(null),
|
|
267
|
+
}).superRefine((launch, ctx) => {
|
|
268
|
+
if (launch.workshop_id === null && launch.map_name === null) {
|
|
269
|
+
ctx.addIssue({ code: 'custom', message: 'a launch without a workshop id names its stock map', path: ['map_name'] });
|
|
270
|
+
}
|
|
271
|
+
if (launch.match === null) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (launch.workshop_id !== null) {
|
|
275
|
+
ctx.addIssue({ code: 'custom', message: 'a match plays a stock map', path: ['workshop_id'] });
|
|
276
|
+
}
|
|
277
|
+
const { team1, team2, spectators } = launch.match.roster;
|
|
278
|
+
if (team1.players.length + team2.players.length + spectators.length > launch.max_players) {
|
|
279
|
+
ctx.addIssue({ code: 'custom', message: 'the roster, players and spectators, must fit in `max_players`', path: ['max_players'] });
|
|
280
|
+
}
|
|
281
|
+
if (!MATCH_GAME_MODES.includes(launch.game_mode)) {
|
|
282
|
+
ctx.addIssue({ code: 'custom', message: 'a match needs `casual`, `competitive` or `wingman`', path: ['game_mode'] });
|
|
283
|
+
}
|
|
284
|
+
if (launch.match.ready_mode === 'external' && launch.match.ready_timeout_seconds !== null) {
|
|
285
|
+
ctx.addIssue({ code: 'custom', message: '`ready_timeout_seconds` must be null with an `external` ready-up, because the controller starts the match', path: ['match', 'ready_timeout_seconds'] });
|
|
286
|
+
}
|
|
287
|
+
});
|