@s2script/sdk 0.8.0 → 0.10.0

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/clients.d.ts CHANGED
@@ -36,6 +36,42 @@ export declare class Client {
36
36
  * reason in the server log), setting is an inert no-op and reads stay false.
37
37
  */
38
38
  voiceMuted: boolean;
39
+ /**
40
+ * Tell this client to run `cmd` in their own console, as if they had typed it
41
+ * (SourceMod `ClientCommand`).
42
+ *
43
+ * Requires a real client: a bot has no console, so this is a no-op on bots — use
44
+ * {@link Client.fakeCommand} for server-side execution. Returns `false` when the command was not
45
+ * dispatched (empty text, bad slot, or the engine interface is unavailable on this build), never
46
+ * a silent no-op.
47
+ *
48
+ * @example
49
+ * client.command("play sounds/ui/beep.vsnd");
50
+ */
51
+ command(cmd: string): boolean;
52
+ /**
53
+ * Have the SERVER process `cmd` as if this client had sent it (SourceMod `FakeClientCommand`).
54
+ *
55
+ * Unlike {@link Client.command} this works on bots, because nothing is sent to a client — the
56
+ * engine dispatches the command itself, attributed to this player's slot. Live-verified:
57
+ * `fakeCommand("say hi")` on slot 0 prints as that player, not as Console.
58
+ *
59
+ * Returns `false` — never a silent no-op — for a bad slot, empty text, an unavailable interface,
60
+ * or a name that is not a registered console **command** (a ConVar such as `mp_friendlyfire` is
61
+ * refused: use `Server.command` for those).
62
+ *
63
+ * KNOWN LIMITATION: engine commands (`say`, `kill`, …) execute. A command registered by an
64
+ * s2script plugin is dispatched by the engine, but its JS handler will NOT run — the core holds
65
+ * the isolate borrow across all JS, so re-entering the dispatcher hits the documented
66
+ * re-entrancy skip. (This is not a permissions issue: every s2script command is registered
67
+ * client-executable, which is why a player can type it in their own console.) To invoke another
68
+ * plugin's behaviour, use a cross-plugin interface rather than faking its command. See
69
+ * docs/superpowers/specs/2026-07-25-client-command-design.md §12.
70
+ *
71
+ * @example
72
+ * client.fakeCommand("say hello"); // as though the player typed it
73
+ */
74
+ fakeCommand(cmd: string): boolean;
39
75
  }
40
76
  /**
41
77
  * Look up connected clients by slot or enumerate them all.
@@ -0,0 +1,37 @@
1
+ # Standard interface contracts
2
+
3
+ These are **type-only contracts for inter-plugin interfaces** — not SDK capability modules.
4
+
5
+ The framework deliberately ships **no implementation** for anything in here. Each contract describes
6
+ a shape that a *community plugin* implements and publishes, so that consumers can depend on one
7
+ agreed interface instead of a different ad-hoc shape per plugin:
8
+
9
+ ```ts
10
+ // producer
11
+ import type { EconService } from "@s2script/cs2/econ";
12
+ const impl: EconService = { /* … */ };
13
+ ctx.publish("econ", impl); // manifest: s2script.publishes
14
+
15
+ // consumer
16
+ import type { EconService } from "@s2script/cs2/econ";
17
+ const econ = ctx.tryUse<EconService>("econ"); // null while unpublished
18
+ ```
19
+
20
+ **Why contracts and not features.** Skins and workshop integration are Valve-backend and
21
+ game-content concerns, not engine touchpoints. Implementing them in the framework would drag the
22
+ core toward a specific game's economy and Steam's services, against *the core is engine-generic* and
23
+ *dependencies point one way: game → core*. Publishing the contract gets the ecosystem interop
24
+ without the framework taking on that surface.
25
+
26
+ **Rules that apply to every contract here** (they cross the plugin boundary, see
27
+ `docs/ARCHITECTURE.md`):
28
+
29
+ - Arguments and return values cross by **structured copy as JSON**. A `BigInt` throws and silently
30
+ drops the whole payload, so every 64-bit value (Steam IDs, workshop IDs, item IDs) is typed as a
31
+ **decimal string**, never `number` or `bigint`.
32
+ - `EntityRef` is the one live-ish exception: it crosses tagged and is revived serial-gated on the
33
+ other side, so it stays `T | null`-safe.
34
+ - A hard dependency (`ctx.use`) throws `InterfaceUnavailable` if the producer is absent; an optional
35
+ one (`ctx.tryUse`) returns `null`. For anything in here, prefer `tryUse` — these are third-party.
36
+ - Contracts are semver-governed. Adding an optional member is a minor; changing or removing one is a
37
+ major.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `@s2script/sdk/contracts/workshop` — the standard contract for a **workshop / UGC** plugin.
3
+ *
4
+ * TYPE-ONLY. The framework ships no implementation: subscribing to, downloading and mounting UGC is
5
+ * a Steam-services concern, not a Source 2 engine touchpoint. A community plugin implements this and
6
+ * publishes it; consumers depend on the one agreed shape. See `contracts/README.md`.
7
+ *
8
+ * Engine-generic on purpose — every Source 2 title has workshop content, so nothing here is
9
+ * CS2-specific. Game-specific economy lives in `@s2script/cs2/econ`.
10
+ *
11
+ * @example
12
+ * import type { WorkshopService } from "@s2script/sdk/contracts/workshop";
13
+ * const ws = ctx.tryUse<WorkshopService>("workshop");
14
+ * if (ws) {
15
+ * const map = await ws.currentMap();
16
+ * console.log(map ? `${map.title} (${map.id})` : "not a workshop map");
17
+ * }
18
+ */
19
+
20
+ /**
21
+ * A workshop item. `id` is a **decimal string**, never a number: published-file IDs are 64-bit and a
22
+ * `BigInt` cannot cross the plugin boundary (it throws and drops the whole payload).
23
+ */
24
+ export interface WorkshopItem {
25
+ /** Published-file ID as a decimal string, e.g. `"3070288894"`. */
26
+ readonly id: string;
27
+ /** Item title, or `""` when metadata has not been fetched yet. */
28
+ readonly title: string;
29
+ /** Owning author's SteamID64 as a decimal string, or `""` if unknown. */
30
+ readonly authorSteamId: string;
31
+ /** Bytes on disk once installed; `0` when not installed or unknown. */
32
+ readonly sizeBytes: number;
33
+ /** Last-updated Unix seconds; `0` if unknown. */
34
+ readonly updatedAt: number;
35
+ }
36
+
37
+ /** An installed workshop map, as reported by {@link WorkshopService.currentMap}. */
38
+ export interface WorkshopMap extends WorkshopItem {
39
+ /** The BSP name the engine loads (what `Server.mapName` reports once live). */
40
+ readonly mapName: string;
41
+ }
42
+
43
+ /** Progress for an in-flight download. */
44
+ export interface WorkshopDownloadProgress {
45
+ readonly id: string;
46
+ readonly bytesDownloaded: number;
47
+ readonly bytesTotal: number;
48
+ }
49
+
50
+ /**
51
+ * The contract a workshop plugin publishes.
52
+ *
53
+ * Everything that touches Steam is asynchronous — an implementation must not block the game frame.
54
+ * Methods resolve to `null`/`false` rather than throwing when an item is unknown or Steam is
55
+ * unreachable, so a consumer degrades instead of crashing (the framework's standing posture).
56
+ */
57
+ export interface WorkshopService {
58
+ /** The currently-running workshop map, or `null` when the server is on a stock map. */
59
+ currentMap(): Promise<WorkshopMap | null>;
60
+
61
+ /** Metadata for `id`, or `null` if unknown/unreachable. */
62
+ info(id: string): Promise<WorkshopItem | null>;
63
+
64
+ /**
65
+ * Ensure `id` is installed and up to date, resolving `true` once usable.
66
+ * Implementations should be idempotent — calling it for an already-current item is a cheap `true`.
67
+ */
68
+ ensureInstalled(id: string): Promise<boolean>;
69
+
70
+ /** In-flight download progress, or `null` when nothing is downloading for `id`. */
71
+ progress(id: string): Promise<WorkshopDownloadProgress | null>;
72
+
73
+ /**
74
+ * Change level to a workshop map. Resolves `false` when the item is not a map, is not installed
75
+ * and could not be fetched, or the change was refused. On success the server changes level, so a
76
+ * caller should not assume any code after this runs on the same map.
77
+ */
78
+ changeMap(id: string): Promise<boolean>;
79
+
80
+ /** Every installed workshop item known to the implementation. */
81
+ installed(): Promise<readonly WorkshopItem[]>;
82
+ }