From 3404937cde55ccdd545f98b59e0b8f8024e752e8 Mon Sep 17 00:00:00 2001 From: Corey Ryan Dean Date: Tue, 26 May 2026 12:13:53 -0500 Subject: [PATCH 1/2] docs(protocol): P_ChatMessage detail page (Chr$(250-254) prefix dispatch) Documents the catch-all text-channel packet. The wire payload is free-form text -- discrimination happens via Left$ semantics on C->S (slash/backslash command vs. general chat) and first-byte colour escape on S->C (250 RGB- custom, 251 green, 252 magenta, 253 red, 254 yellow, anything else plain). Catalogues ~25 slash commands with their gates (DM-only vs. open), the chain-walk family per command (FirstOnlinePlayer for /yell//g//gm//pm// allplayers; FirstInZone for /me/general-chat; FirstSlave for /pet; Party array for /p), and the seven validation gates the handler relies on -- most importantly the Account-Null discipline that prevents server crash from a mid-logout DM typing a /command. Captures the privilege classification distinction: slash commands are dispatched directly from the packet handler (Account\IsDM is the only gate; BVM_RequirePrivileged is not in play here), and the /script command is the *only* path from chat that elevates a spawned ThreadScript to privileged=1. Cross-references the FirstOnlinePlayer / FirstSlave / FirstInZone work (PR #287, predecessors of #288), the Object.AreaInstance Null discipline sweep (#154/#155/#176/#182-#188), and the BVM_OUTPUT emitter at ScriptingCommands.bb:2785 -- the sole producer of Chr$(250) custom-RGB lines. Regenerated docs/protocol/index.md so row 16's Detail column now links to the new page. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/protocol/index.md | 2 +- docs/protocol/packets/P_ChatMessage.md | 145 +++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 docs/protocol/packets/P_ChatMessage.md diff --git a/docs/protocol/index.md b/docs/protocol/index.md index b7c205a3..09b5b293 100644 --- a/docs/protocol/index.md +++ b/docs/protocol/index.md @@ -52,7 +52,7 @@ pages is a per-iteration documentation task — see CONTRIBUTING.md. | 13 | `P_ActorGone` | S→C | — | [ClientNet.bb:1550](../../src/Modules/ClientNet.bb#L1550) | — | | 14 | `P_StandardUpdate` | Both | [ServerNet.bb:1762](../../src/Modules/ServerNet.bb#L1762) | [ClientNet.bb:1490](../../src/Modules/ClientNet.bb#L1490) | [P_StandardUpdate](packets/P_StandardUpdate.md) | | 15 | `P_InventoryUpdate` | Both | [ServerNet.bb:1574](../../src/Modules/ServerNet.bb#L1574) | [ClientNet.bb:1271](../../src/Modules/ClientNet.bb#L1271) | [P_InventoryUpdate](packets/P_InventoryUpdate.md) | -| 16 | `P_ChatMessage` | Both | [ServerNet.bb:183](../../src/Modules/ServerNet.bb#L183) | [ClientNet.bb:1213](../../src/Modules/ClientNet.bb#L1213) | — | +| 16 | `P_ChatMessage` | Both | [ServerNet.bb:183](../../src/Modules/ServerNet.bb#L183) | [ClientNet.bb:1213](../../src/Modules/ClientNet.bb#L1213) | [P_ChatMessage](packets/P_ChatMessage.md) | | 17 | `P_WeatherChange` | S→C | — | [ClientNet.bb:1266](../../src/Modules/ClientNet.bb#L1266) | — | | 18 | `P_AttackActor` | Both | [ServerNet.bb:1548](../../src/Modules/ServerNet.bb#L1548) | [ClientNet.bb:1109](../../src/Modules/ClientNet.bb#L1109) | [P_AttackActor](packets/P_AttackActor.md) | | 19 | `P_ActorDead` | S→C | — | [ClientNet.bb:1065](../../src/Modules/ClientNet.bb#L1065) | — | diff --git a/docs/protocol/packets/P_ChatMessage.md b/docs/protocol/packets/P_ChatMessage.md new file mode 100644 index 00000000..fd58aaf8 --- /dev/null +++ b/docs/protocol/packets/P_ChatMessage.md @@ -0,0 +1,145 @@ +# P_ChatMessage + +**Direction:** Both (C→S "I'm typing into chat"; S→C "render this colored line") +**Numeric ID:** 16 +**Server handler:** [ServerNet.bb:183](../../../src/Modules/ServerNet.bb#L183) +**Client handler:** [ClientNet.bb:1213](../../../src/Modules/ClientNet.bb#L1213) + +## Purpose + +The catch-all text channel. Every user-typed chat line, every slash command, every server-side notification, every BVM `Output(...)` script line travels on this opcode. The client only renders; the server parses, applies access gates, fans out to the right audience (area / online / party / guild / single-target), and may dispatch script work as a side effect. + +There is **no sub-code byte at the start of the wire payload**. The whole packet is a free-form string. Discrimination happens via two parallel signals: + +- **First-character semantics on C→S**: `/` and `\` enter the slash-command parser; everything else is general chat. +- **First-byte colour escape on S→C**: bytes in `[250..254]` are rendered as colour escapes; anything else is normal-text body. + +## Field layout + +### C → S — "Player typed text" + +| Offset | Width | Type | Field | Notes | +|---|---|---|---|---| +| 1 | N | String | Raw text the user typed | No length prefix; `M\MessageData$` is the whole payload. | + +The handler bails immediately if `Len(M\MessageData$) <= 0 Or AI = Null` ([ServerNet.bb:185](../../../src/Modules/ServerNet.bb#L185)). After that: + +- `Left$(M\MessageData$, 1) = "/" Or Left$(M\MessageData$, 1) = "\"` → strip the leading char, split on first space, `Command$` = `Upper$(...)`, dispatch into the giant `Select Command$` ([ServerNet.bb:187-693](../../../src/Modules/ServerNet.bb#L187)). +- Otherwise → general chat: prepend `"<" + AI\Name$ + "> "`, broadcast over the per-area `FirstInZone` chain, log to `ChatLog` per `ChatLoggingMode` ([ServerNet.bb:694-712](../../../src/Modules/ServerNet.bb#L694)). + +### S → C — "Render this line with colour" + +| Offset | Width | Type | Field | Notes | +|---|---|---|---|---| +| 1 | 1 | Byte | Colour escape (optional) | One of `250..254`, or any other byte → start-of-normal-text. | +| 2 | 3 | Byte×3 | R, G, B | **Only present when offset 1 = 250.** Each in `[0..255]`. | +| 2 or 5 | N | String | Message body | Rendered through `Output(...)` or `BubbleOutput(...)`. | + +Colour-escape semantics ([ClientNet.bb:1215-1227](../../../src/Modules/ClientNet.bb#L1215)): + +| Prefix | RGB | Channel / use | +|---|---|---| +| `Chr$(254)` | `255, 255, 0` (yellow) | System / GM / party-join / time-of-day / invite — server-authored notifications. | +| `Chr$(253)` | `255, 50, 50` (red) | Yell, error, ignore confirmation, ability-not-recharged. | +| `Chr$(252)` | `200, 10, 200` (magenta) | `/me` emote, `/pm` private-message, NPC→player chat ("Player: ..."). | +| `Chr$(251)` | `20, 220, 50` (green) | Guild chat, party chat. | +| `Chr$(250)` | (next 3 bytes) | Custom RGB — emitted by `BVM_OUTPUT(actor, text, R, G, B)` at [ScriptingCommands.bb:2793](../../../src/Modules/ScriptingCommands.bb#L2793). | +| _other_ | (default) | Normal text. If body starts `" "` and `UseBubbles > 1`, renders as a 3D chat bubble above the named actor. | + +The chat-bubble path ([ClientNet.bb:1231-1254](../../../src/Modules/ClientNet.bb#L1231)) only fires for the no-escape case — coloured lines never get bubbles. Bubble lookup goes through `FindPlayerFromName(...)`; if no actor matches, the line falls back to normal text output. + +## C → S slash-command catalog + +The dispatch is a single `Select Command$` keyed on `LanguageString$(LS_SC*)` ([Language.bb](../../../src/Modules/Language.bb)) — every command is localizable. Below uses the English defaults. + +| Command | Gate | Effect | +|---|---|---| +| `/kick ` | DM | Sends `RCE_PlayerKicked` + `P_KickedPlayer` to target. | +| `/ignore ` / `/unignore ` | none | Mutates the inviter's `Account\Ignore$` CSV. | +| `/me ` | none | Area broadcast with `Chr$(252) + "* " + Name + " " + text`. | +| `/yell ` | none | Server-wide broadcast (walks `FirstOnlinePlayer` chain). | +| `/g ` | guild member | Walk `FirstOnlinePlayer`, send to same-TeamID. | +| `/p ` | party member | Walk `Party\Player[0..7]`. | +| `/pm , ` | none | Walk `FirstOnlinePlayer`, deliver to first matching name. | +| `/invite` / `/accept` / `/leave` | none | Party state machine. | +| `/pet , , ` | own a pet | Walk `AI\FirstSlave` chain; dispatch `CommandPet(...)`. | +| `/trade ` | none | Player→player trade offer. | +| `/players` / `/allplayers` | none | Counter — current area vs. server-wide. | +| `/time` / `/date` / `/season` | none | Game-clock report. | +| `/warp , , ` | DM | Server-side warp. | +| `/warpother , , , ` | DM | Warp another player. | +| `/xp ` | DM | `GiveXP(AI, n)`. | +| `/gold ` | DM | Direct gold adjustment + `P_GoldChange`. | +| `/setattribute , ` / `/setattributemax , ` | DM | Calls `UpdateAttribute` / `UpdateAttributeMax` for Health/Speed/Energy, otherwise writes through and broadcasts `P_StatUpdate`. | +| `/script , ` | DM | Spawns a `ThreadScript(...)` with `privileged=1` — the script can call any privileged BVM. | +| `/gm ` | DM | Broadcast to all DMs only. | +| `/ability` / `/give` / `/weather` / `/netdump` | DM | Misc DM tools. | +| _other_ | none | Falls through to the `ScriptExists%("In-game Commands")` hook → `ThreadScript("In-game Commands", Command$, ..., Params$)`, otherwise replies `"Unknown command:"` ([ServerNet.bb:688-692](../../../src/Modules/ServerNet.bb#L688)). | + +`/help [topic]` ([ServerNet.bb:40-108](../../../src/Modules/ServerNet.bb#L40)) is special: it emits one `P_ChatMessage` per line of help text, all prefixed `Chr$(254)`. The DM-only block at [ServerNet.bb:57-58](../../../src/Modules/ServerNet.bb#L57) is gated by the same `Account\IsDM` check. + +## Validation requirements + +### C → S handler ([ServerNet.bb:183-712](../../../src/Modules/ServerNet.bb#L183)) + +The packet body is free-form, so length / shape gates are minimal. The defences live in the per-command logic: + +1. **Sender validity**: `AI = FindActorInstanceFromRNID(M\FromID)`; bails on `Null`. +2. **Non-empty body**: `Len(M\MessageData$) > 0`. +3. **Account-Null discipline**: every DM-gated command does `A.Account = Object.Account(AI\Account) : If A <> Null And A\IsDM = True`. The `A <> Null` check is load-bearing — a mid-logout account that has been `Delete`d but whose `Handle` is still on `AI\Account` returns `Null` from `Object.Account(...)`, and bare `A\IsDM` would crash the server **from a chat command** (see audit-comment at [ServerNet.bb:200-202](../../../src/Modules/ServerNet.bb#L200)). Apply this to **every new `/command` that needs a DM gate** — the pattern is non-negotiable. +4. **`/pet` chain walk**: walks `AI\FirstSlave / NextSlave` ([ServerNet.bb:266-273](../../../src/Modules/ServerNet.bb#L266)) — the per-leader chain that PR [#287](https://github.com/RydeTec/rcce2/pull/287) introduced, replacing a global `Each ActorInstance` scan. +5. **`/yell` / `/g` / `/gm` / `/pm` / `/allplayers` chain walks**: walk `FirstOnlinePlayer / NextOnlinePlayer` ([ServerNet.bb:408-462](../../../src/Modules/ServerNet.bb#L408)) — the engine-wide players chain (PR [#288](https://github.com/RydeTec/rcce2/pull/288) era). Filters that used to require `If A2\RNID > 0` are gone because the chain only contains online players. +6. **Mid-warp `AreaInstance` guard**: `/me`, `/players`, and the general-chat fallback all do `AInstance.AreaInstance = Object.AreaInstance(AI\ServerArea) : If AInstance <> Null Then ...` — soft-fails when the actor is in the brief window between `SetArea` rebinding zones. The chat line just doesn't broadcast that tick; no crash. +7. **PlayerIgnoring filter**: every fan-out skips recipients whose ignore-list contains the sender ([ServerNet.bb:394](../../../src/Modules/ServerNet.bb#L394) / [410](../../../src/Modules/ServerNet.bb#L410) / [457](../../../src/Modules/ServerNet.bb#L457)). + +### Privilege classification + +The DM gate is **not** the BVM `RequirePrivileged()` gate. Slash commands are dispatched directly from the packet handler — they never enter the BVM, so `BVM_RequirePrivileged` is irrelevant here. The `Account\IsDM` boolean is the only check. Two notes: + +- **`/script` spawns with `privileged=1`** ([ServerNet.bb:385](../../../src/Modules/ServerNet.bb#L385)) — this is the *only* path from chat that lets a script call privileged BVMs (Ban/Kick/Warp/CreateUDPStream/etc.). The DM gate is what authorises the elevation; downstream BVMs check the privileged bit on `ScriptInstance`. +- **Unknown commands fall through to `"In-game Commands"`** ([ServerNet.bb:688-692](../../../src/Modules/ServerNet.bb#L688)) — these scripts are spawned with the default (un-privileged) flag. Content authors who want player-facing slash commands should put them there; only the engine's DM-only set should ever reach the `privileged=1` path. + +## S → C emit pattern + +Every `RCE_Send(Host, target_rnid, P_ChatMessage, Pa$, True)` follows the same shape: the colour-escape byte (or none) is the first character of `Pa$`. Recipients: + +- **Self-only** notifications (errors, confirmations, `/players` results) — sent to `AI\RNID`. +- **Area broadcast** (`/me`, general chat) — walks `AInstance\FirstInZone` chain, sends per actor with `RNID > 0`. +- **Server-wide broadcast** (`/yell`, `/gm`, `/g`) — walks `FirstOnlinePlayer` chain. +- **Party broadcast** (`/p`, party-join notification) — `For i = 0 To 7 : Party\Player[i]` array. +- **Single target** (`/pm`, `/invite`, `/trade`) — direct send to the resolved target's `RNID`. + +The general-chat fallback at [ServerNet.bb:705-710](../../../src/Modules/ServerNet.bb#L705) also adds the line to the server's `Game\ChatText` ListBox (for the dedicated-server console) and writes it to `ChatLog` when `ChatLoggingMode > 0`. + +## Anti-cheat surface + +`P_ChatMessage` is not the high-stakes packet `P_AttackActor` is, but it has the largest privilege surface of any packet because of `/script`. The defences: + +- **Wire payload is text, not opcode** — a malformed packet can produce a garbled chat line but cannot reach `/script` unless the sender has `IsDM` on their account. +- **DM bit is server-side only** — clients cannot fabricate `IsDM`; it lives on the `Account` row in MySQL / `accounts.dat`. +- **Privileged spawn is gated on DM bit** — losing DM in the middle of `/script` would not retroactively un-privilege a running script; the bit is captured at spawn. That's fine — administering a DM-set should be done in a separate channel anyway. +- **Soft-fail on every Account-Null path** — the `A <> Null` checks ensure a mid-logout DM cannot crash the server by typing into chat as the account is being freed. + +## Historical bugs / PR references + +| PR | Fixed | +|---|---| +| Audit pre-PR | `Account\IsDM` reads after `Object.Account(...)` did not check Null first — server-crash via slash command from a mid-logout DM. Audit-comments at [ServerNet.bb:200-202](../../../src/Modules/ServerNet.bb#L200) / [220-221](../../../src/Modules/ServerNet.bb#L220) / [237](../../../src/Modules/ServerNet.bb#L237) record the pattern. | +| [#287](https://github.com/RydeTec/rcce2/pull/287) | `/pet` walks the per-leader `FirstSlave` chain (replacing global `Each ActorInstance` scan). | +| [#288](https://github.com/RydeTec/rcce2/pull/288) and predecessors | `/yell` / `/g` / `/gm` / `/pm` / `/allplayers` walk the `FirstOnlinePlayer` chain. | +| PRs [#154](https://github.com/RydeTec/rcce2/pull/154) / [#155](https://github.com/RydeTec/rcce2/pull/155) / [#176](https://github.com/RydeTec/rcce2/pull/176) / [#182](https://github.com/RydeTec/rcce2/pull/182)–[#188](https://github.com/RydeTec/rcce2/pull/188) | `Object.AreaInstance(...)` Null discipline sweep — covers `/me`, `/players`, and general-chat fallback paths. | +| Unknown-command notify | Pre-fix, an unknown slash command silently no-op'd. Now replies `"Unknown command: /xxx. Type /help for a list."` ([ServerNet.bb:691](../../../src/Modules/ServerNet.bb#L691)). | + +## Related packets + +- [`P_StandardUpdate`](P_StandardUpdate.md) — `Chr$(254)` system lines often follow a warp; the warp itself rides `P_StandardUpdate`. +- [`P_AttackActor`](P_AttackActor.md) — `/kick` is the chat-equivalent of forcibly removing an actor; combat is the other way out. +- [`P_GoldChange`](../index.md) — `/gold` emits this directly; the chat line is just confirmation. +- [`P_KickedPlayer`](../index.md) — paired with `/kick` to actually disconnect the target. + +## See also + +- [`../encoding.md`](../encoding.md) — wire-encoding primitives. +- [`../handler-conventions.md`](../handler-conventions.md) — soft-fail discipline, Account-Null pattern, FirstOnlinePlayer / FirstInZone / FirstSlave chain walks. +- [`../../modules/scripting.md`](../../modules/scripting.md) — `/script` and the privileged-spawn flag. +- [`ScriptingCommands.bb`'s `BVM_OUTPUT`](../../../src/Modules/ScriptingCommands.bb#L2785) — the only S→C `Chr$(250)` (custom-RGB) emitter. From cdb7c9321940a58327ba217955f07a9670f0f133 Mon Sep 17 00:00:00 2001 From: Corey Ryan Dean Date: Tue, 26 May 2026 12:17:47 -0500 Subject: [PATCH 2/2] docs(p-chatmessage): apply reviewer corrections + fix in-game /script help Three doc corrections from the reviewer audit: 1. Chr$(250) is NOT exclusively emitted by BVM_OUTPUT -- the engine also uses it for critical-damage notifications at three sites in GameServer.bb:432/469/507 (one per CombatFormula variant). Updated the colour-escape table row and the "See also" footnote to acknowledge the second emitter. 2. /help line citation `ServerNet.bb:40-108` was wrong. The actual functions live at `:31-60` (SendChatHelp entry) and `:65-109` (SendChatHelpDetail per-topic dispatcher). Both ranges now cited. 3. "every command is localizable" was overstated. /help and /? are hardcoded at ServerNet.bb:680 (`Case "HELP", "?"`) pending an LS_SCHelp localization entry. Doc now calls out the exception. Bonus source bug the reviewer flagged: SendChatHelpDetail at ServerNet.bb:95 described /script as `/script [,]` -- the actual handler at :379-385 splits on comma into Name + Func, so the second arg is a function name, not params. Corrected the in-game help string to `/script , -- run script's function as self`. Compile verified clean across Server/Client/GUE/Project Manager (-t). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/protocol/packets/P_ChatMessage.md | 8 ++++---- src/Modules/ServerNet.bb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/protocol/packets/P_ChatMessage.md b/docs/protocol/packets/P_ChatMessage.md index fd58aaf8..612fb367 100644 --- a/docs/protocol/packets/P_ChatMessage.md +++ b/docs/protocol/packets/P_ChatMessage.md @@ -43,14 +43,14 @@ Colour-escape semantics ([ClientNet.bb:1215-1227](../../../src/Modules/ClientNet | `Chr$(253)` | `255, 50, 50` (red) | Yell, error, ignore confirmation, ability-not-recharged. | | `Chr$(252)` | `200, 10, 200` (magenta) | `/me` emote, `/pm` private-message, NPC→player chat ("Player: ..."). | | `Chr$(251)` | `20, 220, 50` (green) | Guild chat, party chat. | -| `Chr$(250)` | (next 3 bytes) | Custom RGB — emitted by `BVM_OUTPUT(actor, text, R, G, B)` at [ScriptingCommands.bb:2793](../../../src/Modules/ScriptingCommands.bb#L2793). | +| `Chr$(250)` | (next 3 bytes) | Custom RGB. Primary emitter is `BVM_OUTPUT(actor, text, R, G, B)` at [ScriptingCommands.bb:2793](../../../src/Modules/ScriptingCommands.bb#L2793); engine also uses it for the critical-damage notification at three sites in [GameServer.bb](../../../src/Modules/GameServer.bb#L432) (`Chr$(255) + Chr$(225) + Chr$(100)` — peach/orange). | | _other_ | (default) | Normal text. If body starts `" "` and `UseBubbles > 1`, renders as a 3D chat bubble above the named actor. | The chat-bubble path ([ClientNet.bb:1231-1254](../../../src/Modules/ClientNet.bb#L1231)) only fires for the no-escape case — coloured lines never get bubbles. Bubble lookup goes through `FindPlayerFromName(...)`; if no actor matches, the line falls back to normal text output. ## C → S slash-command catalog -The dispatch is a single `Select Command$` keyed on `LanguageString$(LS_SC*)` ([Language.bb](../../../src/Modules/Language.bb)) — every command is localizable. Below uses the English defaults. +The dispatch is a single `Select Command$` keyed on `LanguageString$(LS_SC*)` ([Language.bb](../../../src/Modules/Language.bb)) — nearly every command is localizable. The two exceptions are `/help` and `/?`, which are hardcoded at [ServerNet.bb:680](../../../src/Modules/ServerNet.bb#L680) (`Case "HELP", "?"`) pending an `LS_SCHelp` entry. Below uses the English defaults. | Command | Gate | Effect | |---|---|---| @@ -76,7 +76,7 @@ The dispatch is a single `Select Command$` keyed on `LanguageString$(LS_SC*)` ([ | `/ability` / `/give` / `/weather` / `/netdump` | DM | Misc DM tools. | | _other_ | none | Falls through to the `ScriptExists%("In-game Commands")` hook → `ThreadScript("In-game Commands", Command$, ..., Params$)`, otherwise replies `"Unknown command:"` ([ServerNet.bb:688-692](../../../src/Modules/ServerNet.bb#L688)). | -`/help [topic]` ([ServerNet.bb:40-108](../../../src/Modules/ServerNet.bb#L40)) is special: it emits one `P_ChatMessage` per line of help text, all prefixed `Chr$(254)`. The DM-only block at [ServerNet.bb:57-58](../../../src/Modules/ServerNet.bb#L57) is gated by the same `Account\IsDM` check. +`/help [topic]` is special: it emits one `P_ChatMessage` per line of help text, all prefixed `Chr$(254)`. The entry point `SendChatHelp` lives at [ServerNet.bb:31-60](../../../src/Modules/ServerNet.bb#L31); the per-topic detail dispatcher `SendChatHelpDetail` at [ServerNet.bb:65-109](../../../src/Modules/ServerNet.bb#L65). The DM-only summary block at [ServerNet.bb:57-58](../../../src/Modules/ServerNet.bb#L57) is gated by the same `Account\IsDM` check. ## Validation requirements @@ -142,4 +142,4 @@ The general-chat fallback at [ServerNet.bb:705-710](../../../src/Modules/ServerN - [`../encoding.md`](../encoding.md) — wire-encoding primitives. - [`../handler-conventions.md`](../handler-conventions.md) — soft-fail discipline, Account-Null pattern, FirstOnlinePlayer / FirstInZone / FirstSlave chain walks. - [`../../modules/scripting.md`](../../modules/scripting.md) — `/script` and the privileged-spawn flag. -- [`ScriptingCommands.bb`'s `BVM_OUTPUT`](../../../src/Modules/ScriptingCommands.bb#L2785) — the only S→C `Chr$(250)` (custom-RGB) emitter. +- [`ScriptingCommands.bb`'s `BVM_OUTPUT`](../../../src/Modules/ScriptingCommands.bb#L2785) — the primary S→C `Chr$(250)` (custom-RGB) emitter. The engine itself also uses `Chr$(250)` for critical-damage notifications at three sites in [`GameServer.bb`](../../../src/Modules/GameServer.bb#L432) (lines 432, 469, 507 — one per `CombatFormula` variant). diff --git a/src/Modules/ServerNet.bb b/src/Modules/ServerNet.bb index aca2233a..c07490b3 100644 --- a/src/Modules/ServerNet.bb +++ b/src/Modules/ServerNet.bb @@ -92,7 +92,7 @@ Function SendChatHelpDetail(AI.ActorInstance, T$, IsDM%) If T = "GOLD" Then Line = "/gold , -- grant gold" If T = "SETATTRIBUTE" Then Line = "/setattribute ,, -- set attribute" If T = "SETATTRIBUTEMAX" Then Line = "/setattributemax ,, -- set max attribute" - If T = "SCRIPT" Then Line = "/script [,] -- run a script as self" + If T = "SCRIPT" Then Line = "/script , -- run script's function as self" If T = "GM" Then Line = "/gm -- broadcast as GM to all players" If T = "WARPOTHER" Then Line = "/warpother ,[,,,] -- warp another player" If T = "ABILITY" Then Line = "/ability , -- grant ability"