diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index ece94c6b..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.claude/.no-autoformat b/.claude/.no-autoformat new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..fdbfb990 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + build-test-smoke: + name: Build, Unit Tests, and Smoke Tests + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + clang-format \ + cmake \ + cargo \ + gcc-multilib \ + g++-multilib \ + libc6-dev-i386 \ + libcrypt-dev \ + libgtest-dev \ + make \ + pkg-config \ + python3 \ + rustc + + - name: Configure build tree + run: make configure + + - name: Run C++ unit tests + run: make test + + - name: Run account smoke flow + run: make smoke-account diff --git a/.gitignore b/.gitignore index a38fb6d7..3396724b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,14 +47,18 @@ CVS/ old/ backup/ lib/misc/plrmail +lib/accounts/ lib/misc/maze.dat lib/misc/mdl lib/misc/mudlle.keys lib/misc/oldcrimelist lib/misc/pk* lib/misc/repair* +lib/misc/xnames* +tools/__pycache__/ core/ *~ +autorun autorun4000 switchserver.sh crashes @@ -77,3 +81,8 @@ target/ # cmake .cache +cmake_test_discovery*.json + +# macOS +.DS_Store +**/.DS_Store diff --git a/AGENTS.md b/AGENTS.md index 92f935b3..27b7105b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,25 +8,52 @@ - proxy/: Rust workspace member (`cargo` crate) for proxy/CLI utilities. - release-notes/, game design docs/, code documentation/: Docs and release history. +## Planning Workflow +- Before starting feature work, always read `FEATURES.md` and `WIP.md` if they exist. +- Treat `FEATURES.md` as the current feature scope, breakdown, and implementation checklist. +- Treat `WIP.md` as the current execution log and update it during feature work with the current task, recent progress, and next step. +- If feature scope changes during discussion, update `FEATURES.md` before implementing. +- If active work changes during implementation, update `WIP.md` before continuing. + ## Build, Test, and Development Commands -- Bootstrap data: `cd src && make setup` — creates required runtime directories/files under `lib/`, `log/`, and `bin/`. -- Build (Make): `cd src && make all` — compiles C/C++ sources to `bin/ageland`. -- Run: `cd src && make run` or `./bin/ageland -p &` — starts server in background. -- Clean: `cd src && make clean` — removes `*.o` objects. -- CMake alt build: `cmake -S src -B build && cmake --build build` (C++17). +- Configure: `make configure` — generates the CMake build tree in `build/`. +- Bootstrap data: `make setup` — creates required runtime directories/files under `lib/`, `log/`, and `bin/`. +- Build: `make build` — compiles C/C++ sources to `bin/ageland`. +- Test: `make test` — builds and runs the GoogleTest-based C++ unit tests. +- Manual smoke: `make smoke-account` — builds the game/proxy and runs the proxy-backed account smoke flow. + Use this as a required separate validation step for account/login/authentication changes because `make test` is intentionally unit-test-only. +- Run: `make run` — builds and starts the server in the foreground on port `3791`. +- Clean: `make clean` — removes build outputs from the configured tree. +- Raw CMake fallback: `cmake -S src -B build -DCMAKE_CXX_COMPILER=g++ && cmake --build build --target ageland` - Rust proxy: `cargo build -p proxy` | `cargo test -p proxy` | `cargo run -p proxy -- --help`. ## Coding Style & Naming Conventions -- Formatter: run `cd src && make format` (WebKit style). Prefer this over local defaults; CI expects formatted diffs. +- Formatter: run `cmake --build build --target format` (or `cd src && make format`) using WebKit style. Prefer the repo-provided target over local defaults; CI expects formatted diffs. - .clang-format: present for IDEs; indentation 4 spaces; column limit ~100. - Filenames: lower_snake_case for `.cpp`/`.h` (e.g., `act_comm.cpp`, `protocol.h`). - C/C++: functions/variables lower_snake_case; constants UPPER_SNAKE_CASE; types TitleCase where applicable. - Rust (proxy): follow `rustfmt` defaults; module/file lowercase with underscores. ## Testing Guidelines -- C/C++: no formal unit tests; perform smoke tests by building and running locally. Verify server boots, accepts connections, and changed features behave as expected. +- C/C++: add or update unit tests in `src/tests/` when working in covered areas, run them via `make test`, and also perform smoke tests manually by building and running locally, typically via `make smoke-account` for the account flow. For account/login/authentication changes, treat that smoke run as a required separate validation step before finalizing. Verify server boots, accepts connections, and changed features behave as expected. +- When the user reports a bug or finding, default to adding a focused unit/regression test for it first whenever the affected area is unit-testable. If a meaningful unit test is not practical, say so explicitly and add the next-best automated coverage you can. +- C/C++ test style: prefer behavior-oriented GoogleTest names that read clearly in CTest output, such as `ReturnsConfiguredWeaponType` instead of terse names like `WeaponType`. Use readable assertions like `EXPECT_TRUE` when appropriate, and add concise failure messages that explain the expectation and include important domain values when a failure would otherwise be cryptic. +- Do not modify production code solely to accommodate tests. Prefer test fixtures, helper builders, dependency-free coverage, and existing public behavior. Only introduce a production seam for testability when it is also a legitimate design improvement, and call that out explicitly. +- New code: add unit tests for newly written code when the surrounding module supports them, and document any gaps when tests are not practical. +- When writing or expanding unit tests for non-trivial code paths, maintain a constructively adversarial test-design partner named `Bazarat`. Use `Bazarat` to challenge assumptions, look for missing edge cases, identify weak assertions, and pressure-test whether the tests would catch realistic regressions instead of only happy paths. - Rust: write unit/integration tests in `proxy/`; run with `cargo test -p proxy` and keep coverage reasonable. +## Review Workflow +- Before finalizing any non-trivial change set, maintain two review subagents in parallel: `Magus` as the quality engineer reviewer and `Vincent` as the security engineer reviewer. +- When the change set includes meaningful unit-test work, keep `Bazarat` engaged during test design and test review as a constructively adversarial pairing partner in addition to the normal reviewer pair. `Bazarat` is not a replacement for `Magus` or `Vincent`; the role is to pressure-test test intent, edge coverage, and failure realism while the code is still being written. +- Reuse the same reviewer pair across successive changes by sending them updated diff context, instead of spawning a fresh pair for every round. Only replace a reviewer when it has been closed, becomes unavailable, or its context is no longer reliable. If either reviewer must be replaced, assign the replacement the same role name so the workflow stays consistent. +- In user-facing updates, always refer to the reviewer roles as `Magus` and `Vincent`, and the test-design partner role as `Bazarat`, even if the underlying subagent identity has changed, so the review trail stays clear and consistent. +- The quality engineer should focus on regressions, correctness, maintainability, test quality, developer ergonomics, and documentation gaps. +- The security engineer should focus on trust boundaries, unsafe execution paths, secrets or data exposure, command safety, and build/test workflow risks. +- `Bazarat` should focus on adversarial test questions such as: what assumption is unproven, what malformed input is untested, what rollback path is uncovered, what assertion is too weak, and what realistic regression would still slip through the current tests. +- Give both reviewers the relevant changed files or diff context, ask for findings first with severity, file references, and concrete recommendations, and do not treat the work as complete until their feedback has been reviewed. +- Address their findings in code or docs when appropriate, or explicitly document why a recommendation is being deferred. + ## Commit & Pull Request Guidelines - Commits: concise, imperative subject (<=72 chars). Reference issues/PRs, e.g., "ranger: fix stun timing (#255)". - Scope small, logically grouped changes; include short body for context when needed. @@ -36,4 +63,3 @@ ## Security & Configuration - World files live in a separate repo; keep `lib/world/` and player data out of commits. - Never check in PII or live server logs (`log/`). Use local testing accounts and sanitized samples. - diff --git a/Dockerfile b/Dockerfile index 1f65c8ac..b9bfb882 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,11 @@ # Build/run with docker compose (see docker-compose.yml) or scripts/rots-docker.sh. FROM --platform=linux/386 i386/debian:bullseye -# g++ 10 (supports the Makefile's -std=c++1z) + make. The Makefile links no extra -# libraries, so no other build deps are needed. telnet/procps are dev conveniences. +# g++ 10 (supports -std=c++1z/c++17) + make. The CMake build (src/CMakeLists.txt) also +# needs cmake, GoogleTest (libgtest-dev) for the ageland_tests suite, and libcrypt-dev for +# the crypt() link; pkg-config is a CMake convenience. telnet/procps are dev conveniences. RUN apt-get update && apt-get install -y --no-install-recommends \ - g++ make telnet procps ca-certificates \ + g++ make cmake libgtest-dev libcrypt-dev pkg-config telnet procps ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /rots diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 00000000..bcf4e2e2 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,415 @@ +# Features to Add + +## MSDP Unit Test Coverage Requirements + +Add broad unit-test coverage for the game's MSDP implementation. The goal is to test as much of the current MSDP behavior as practical without depending on a live server socket, live telnet client, or full interactive smoke flow. + +Requirements: +- Cover the protocol core in `src/protocol.cpp`, including MSDP negotiation, subnegotiation parsing, command handling, configurable variables, reporting state, dirty-state flushing, and output formatting. +- Cover the game-facing MSDP update paths in `src/comm.cpp` and `src/act_move.cpp`, including periodic character updates and room updates. +- Include structured group status reporting with every group member's name plus health, mana, and movement percentages. +- Prefer focused C++ unit tests in `src/tests/` that can run through `make test`. +- Build reusable test helpers for descriptors, protocol state, output capture, fake player characters, rooms, and MSDP packet parsing so individual tests are readable and do not require a real network connection. +- Include negative and boundary coverage for malformed or truncated MSDP packets, unknown commands/variables, oversized values, invalid configurable values, write-once client identity variables, and disabled or missing protocol state. +- Verify the actual bytes or parsed structure emitted for MSDP arrays, tables, scalar variables, `REPORT` / `UNREPORT` / `RESET` behavior, `LIST` responses, and ATCP fallback where applicable. +- Include regression coverage for known suspicious or fragile paths as tests expose them, especially room updates and string escaping/sanitization. +- Keep smoke tests as a complement only; this feature should primarily be unit-test driven. + +I'd currently like to add an account management system to the game. Accounts should now be email-first: at login the player is prompted for an account name, and that account name is the player's email address. If the account does not exist yet, the login flow should offer to create it and set a secure password with a minimum of 8 characters, upper and lower case, and a number. After logging into the account, the player should land in an account menu where they can list their characters, add an existing legacy character, create a new character, reset the account password, or play a linked character. When they add a pre-existing character it should verify the legacy character password, transform the character file, character object, and character exploit file to json, and store it in a new directory linked back to the account. The account files should also be in json and stored in the similar fashion of how player files are stored now with the alphabet being split up. New characters should not be born into the legacy file layout at all: their character data, object data, and exploit history should be written directly into the new JSON-backed account storage from the start. Character state should live in `character.json`, while object state and exploit history should each live in their own JSON files so they can be maintained independently, and the account file should reference those separate per-character files so it still shows what is linked. The old single-file character snapshot idea is transitional only and should not be the final design. + +## Desired Login Workflow + +1. Prompt for account name, which is the player's email address. +2. If the account exists, prompt for the account password and authenticate it. +3. If the account does not exist, offer to create it and collect/confirm the new password. +4. After account creation, send an email verification code to the account address and require the player to enter it before the account is trusted. +5. If the account exists but is still unverified, authenticate the password, send or resend a verification code, and prompt for that code instead of entering the account menu yet. +6. Verification must be by an emailed code that expires 15 minutes after it is issued. +7. After successful authentication and email verification, enter an account menu with these options: + - list linked characters + - add an existing character + - create a new character + - reset the account password + - play a linked character + - quit/back out +8. List linked characters as a simple list of the character names tied to the account. +9. Add existing character flow: + - prompt for character name + - prompt for that character's existing legacy password + - if the password is correct, migrate the legacy data into account-linked JSON storage and attach it to the account + - once migration succeeds and the account-owned JSON character storage is safely written, delete the old legacy player, object, and exploit files for that character +10. Reset password flow: + - prompt for existing account password + - prompt for new password + - prompt to confirm the new password +11. Play character flow: + - select a linked character + - enter the world using the current character login behavior after selection +12. New-character storage rule: + - a newly created character should be written directly to account-native JSON storage for character data, object data, and exploit data + - each character should have its own dedicated `character.json` file containing the character state; that data must not be combined into a shared or multi-character JSON document + - each character should likewise have its own dedicated `objects.json` file and `exploits.json` file + - the account file should reference that character's separate files so an operator can inspect the account file and see exactly which assets are linked + - legacy `lib/players`, `lib/plrobjs`, and `lib/exploits` files should be treated as migration/backward-compatibility inputs, not the authoritative home for newly created account characters +13. Active account-session/reconnect rule: + - if an authenticated account already has a character still connected to the game, the account menu should show which linked character is currently active + - this must cover both linkless characters left in-game after a socket disconnect and a second connection to the same account while another descriptor is actively playing + - while the active character is not over level 91, the account must not be able to enter the game as a different character + - treat "over level 91" as `GET_LEVEL(active_character) > 91`; level 91 and below remain restricted + - if any currently active linked character on the account is over level 91, the account may select any linked character even if another active linked character is level 91 or below + - the account should still be able to resume or reconnect to that same active character through the existing character reconnect behavior + - if the active character is over level 91, selecting another linked character remains allowed +14. Administrator active-session unlock rule: + - high-level account administrators need an `account unlockselect ` style command for cases where a low-level linked character is stuck active and blocks the account from selecting another character + - the unlock should be account-scoped, runtime-only, and one-shot so it fixes the stuck-session case without permanently weakening the active-session guard + - the unlock should allow linked-character selection only; it must not unlock account-menu new-character creation + - the command should only grant an unlock when the account currently has a restricting active linked character session, and it should be logged like the other immortal account-management commands + +## Execution Breakdown + +Current repo/storage notes: +- Character files are currently stored in `lib/players//`. +- Character exploit files are currently stored in `lib/exploits//.exploits`. +- Character object save data is currently stored in `lib/plrobjs//`. +- Existing player data is split into alphabet buckets (`A-E`, `F-J`, `K-O`, `P-T`, `U-Z`, `ZZZ`), so account storage should likely follow the same pattern for consistency. +- Updated target rule: newly created account characters should persist directly into account-owned JSON storage and should not require a legacy-format birth write before they can be played. +- Updated storage rule: each character should have its own separate `character.json`, `objects.json`, and `exploits.json` files, with references from the account file, instead of being bundled into one monolithic document or any shared multi-character JSON file. +- Updated cutover rule: replace the transitional single-file character snapshot layout with separate per-character JSON assets plus account-owned references to them. +- Updated path rule: keep all account-owned files directly under `lib/accounts///`, and prefix character-owned asset filenames with the character slug instead of nesting them under a per-character directory. +- Updated schema rule: define `character.json` from the post-load runtime character/player structs rather than from the raw legacy save-file text, so migrated and newly created characters share the same canonical shape. +- Updated schema rule: preserve profession allocation points, profession coeffs, and other important persisted point/coefficient data in `character.json`; these are gameplay-relevant and must survive the cutover. +- Updated terminology rule: use `mystic` in the new JSON schema/docs where the legacy codebase still uses `cleric` identifiers internally. +- Updated schema rule: omit `pretitle` and `prompt` from the new `character.json` schema; they are not needed in the account-native character persistence format. + +Proposed implementation slices: +1. Define the account data model and file layout. +2. Add JSON read/write support for accounts and migrated character assets with unit tests. +3. Add account creation and password validation flow with unit tests. +4. Add account login/authentication flow with unit tests. +5. Add administrator account-management tools with unit tests. +6. Add character linking/migration flow for pre-existing characters with unit tests. +7. Update game login/menu flow so players choose an account, then a linked character, with unit tests where practical. +8. Add final migration/smoke-test coverage and fill any remaining test gaps. + +## Completed So Far + +- Added a standalone account-management module with JSON read/write support for accounts and transitional migrated character storage. +- Added secure account password hashing and verification using `libcrypt`. +- Added bucketed account storage under `lib/accounts/...` and account-linked character storage under `lib/account_characters/...`. +- Added file-backed account creation, authentication, password reset, block/unblock, and character-link helpers with focused unit coverage. +- Added admin account-management commands for showing accounts, blocking/unblocking, resetting passwords, linking characters, and forcing migration. +- Added player-side `linkaccount` support with a masked password prompt instead of raw command-line password entry. +- Added transitional live login support for account authentication and linked-character selection. +- Added email validation, email-based account lookup, email-based authentication, and account creation from an email address as groundwork for the new email-first login flow. +- Added duplicate-link protection across accounts, migration-first linking to avoid stale partial links, duplicate-email detection that fails closed, and resilient email lookup when an unrelated account file is corrupt. +- Added account verification metadata, emailed verification-code generation, 15-minute expiry tracking, and confirmation helpers. +- Added outbound verification email delivery through the local `sendmail` interface. +- Added a configurable `ROTS_SENDMAIL_COMMAND` override plus a more robust live mail-delivery subprocess path so local smoke testing can capture verification emails without changing the feature behavior. +- Added fallback resolution for versioned legacy player-save filenames, so freshly created characters can be migrated into account storage immediately instead of only legacy characters that happen to live at the old unsuffixed path. +- Updated the live login flow so new and pending accounts email a verification code, accept `RESEND` / `CANCEL`, and require code entry before entering the account menu. +- Hid verification-code entry from snoops the same way other secret prompts are masked. +- Added persistent verification-attempt tracking, verification-code invalidation after too many bad tries, and resend cooldown protection for emailed codes. +- Hardened account creation to fail closed if stored account records are unreadable, so email uniqueness cannot be bypassed through corrupted account data. +- Added account-storage refresh helpers so linked characters can self-heal missing migrated character storage and refresh account storage from current legacy files. +- Added migration-restore helpers so account-selected play can reconstruct legacy player/object/exploit files from account storage before loading the character. +- Added asset-decoding helpers and a direct player-text load path so account-backed character selection can parse player data from account storage without first recreating the legacy player file. +- Updated runtime flows so account-created characters are immediately linked and migrated, account-selected play requires account-owned character storage readiness and restores from account storage before loading, and normal character saves refresh linked account storage. +- Updated migration/backfill flows so legacy-character migration now writes authoritative account-owned `character.json` immediately from decoded legacy player data, and account-backed selection now re-reads `character.json` after migration/backfill instead of decoding `migration.player_file` directly at runtime. +- Updated account-backed selection so the direct authoritative `character.json` fast path also loads account-owned object-save bytes before staging `Crash_load()`, which keeps already-account-native characters from dropping equipment when they enter the world without needing migration fallback first. +- Hardened account-backed cutover behavior so corrupt existing migrated character storage self-heals from legacy files, duplicate linked-character ownership fails closed, runtime support-file restore validates character-storage identity before writing, and malformed stored player text is rejected safely during direct account-backed load. +- Cut exploit history one step farther away from legacy login-time restore by removing exploit-file restoration from account-backed play, teaching exploit reads to fall back to account-owned character storage when the runtime file is absent, and seeding new runtime exploit files from stored account data when gameplay appends fresh records. +- Expanded and kept passing focused `AccountManagement` unit coverage for the foundation work, including verification-code success, invalid-code, expired-code, resend-cooldown, and pending-auth cases. +- Added regression coverage for corrupt migrated-character rebuilds, duplicate linked-character ownership, character-storage identity restore mismatches, malformed player-text decoding, exploit-history fallback/append behavior when the legacy exploit file is missing, corrupt runtime exploit-file self-healing, temp-file conflict failure, and preserving exploit history across account-storage refreshes. +- Ran `make test` and confirmed the full C++ unit test suite passes locally at 240/240. +- Expanded the proxy-backed Python smoke harness to cover account-menu new-character creation, reconnect, and account-backed play-character selection. +- Ran the required `Magus` quality review and `Vincent` security review for this exploit-history cutover slice, then addressed their findings before finalizing the pass. +- Added a shared `character_json` foundation module and focused unit coverage for profession points/coeffs, symbolic player/preference/affected flag arrays, structured affect state, `mystic` profession naming, and `char_file_u` conversion helpers as groundwork for the account-native `character.json` cutover. +- Expanded the shared `character_json` groundwork so it now round-trips a broader slice of normalized `char_file_u` state, including identity/physical fields, temporary and rolled abilities, point data, conditions, timers, talks, skills, hide flags, and array-capacity validation for applying JSON back into the stored character form. +- Hardened the shared `character_json` reader/apply path so malformed JSON now fails closed on out-of-range narrowed values, truncated fixed-width arrays, and overlong fixed-buffer strings instead of silently truncating or wrapping stored character state. +- Tightened the shared JSON/parser boundary further so parsed integers fail before out-of-range narrowing, fixed-width arrays are capped while parsing, embedded NUL bytes are rejected for fixed-buffer character strings, and oversized `affects` arrays are rejected before they can accumulate unboundedly. +- Updated the planned `character.json` shape so `skills` and `talks` are now represented as named key/value JSON objects rather than positional arrays, while the serializer still translates those objects back into the legacy fixed arrays for runtime compatibility. +- Added a shared `exploits_json` module plus focused unit coverage for exploit-history binary/JSON round-trip behavior, malformed binary-length rejection, and fixed-width string validation. +- Added account-layer helpers to write/read/check/remove per-character `exploits.json` files in the flat account directory layout. +- Updated new-character introduction so account-created characters now create an account-owned `exploits.json` during their initial account-link flow, with rollback cleanup if linking fails. +- Updated legacy migration so successful migrations now seed canonical account-owned `exploits.json` immediately when legacy exploit data is valid, write an empty default `exploits.json` when the legacy exploit file is absent, and fail closed when legacy exploit bytes are malformed. +- Updated exploit-history runtime flows so linked characters now prefer account-owned `exploits.json`, refresh it directly when new exploit records are written, and retire stale legacy runtime exploit files after successful account-native reads/writes. +- Added focused regression coverage proving corrupt authoritative account-owned `exploits.json` fails closed even when a stale legacy runtime exploit file is still present. +- Sanitized the transitional `.migration.json` artifact so it no longer persists raw legacy player-file bytes at rest; object/exploit transitional data remains available where still needed, while legacy player password/host content is no longer carried forward in the on-disk migration metadata. +- Stopped treating `.migration.json` as a routine persisted artifact during successful migration/refresh flows; ordinary migration now retires any leftover snapshot file, `ensure_character_migration(...)` no longer depends on it in the normal account-native path, and exploit-history refresh now falls back to authoritative account-native `exploits.json` data instead of the old snapshot file. + +## Todo List + +- [ ] Confirm product decisions before coding: + - Account identifier rules: login should be email-first, so confirm whether email is the only account identifier or whether a separate display name still exists. + - Email rules: normalization, uniqueness, and whether verification is required. + - Email ownership proof: email-first accounts should not become the authoritative identity without verification or operator approval, otherwise unused email addresses can be squatted by whoever creates the account first. + - Password storage approach: hashed/salted format for accounts; do not reuse current reversible character password handling. + - Migration policy: whether a linked character remains playable through the old login path or becomes account-only. + - Recovery/admin flows: how password resets, duplicate-email cases, and account unlinking should work. + - Admin permissions: which immortals/admin levels can view, block, reset, or modify accounts. + - Blocking semantics: whether blocked accounts are prevented from login entirely, character selection only, or specific actions. + +- [x] Design the new on-disk account structure: + - Add `lib/accounts//.json` or equivalent. + - Define JSON schema for account data: email-based account identifier, normalized email, password hash, linked characters, created/updated timestamps, and status flags. + - Include administrative metadata such as block status, block reason, blocked-by, blocked-at, last password reset info, and audit history if needed. + - Define JSON schema for account-owned character metadata and how it references separate `character.json` / `objects.json` / `exploits.json` assets under account-owned storage. + - Decide whether migrated data lives under the account directory or under separate bucketed directories with back-references. + Update: use `lib/accounts///account.json` for the account record, and keep character-owned files in that same directory with names prefixed by the character slug, such as `.character.json`, `.objects.json`, and `.exploits.json`. + +- [x] Build serialization/deserialization support in the server: + - Add helpers for reading/writing account JSON safely. + - Add helpers for exporting existing character file, object save file, and exploit file into JSON. + - Store character data, object data, and exploit data in separate JSON files so each asset can be maintained independently. + - Add validation and error handling for missing/malformed JSON files. + - Ensure writes are atomic enough to avoid partial migrations. + - Add unit tests for valid reads/writes, malformed input, missing files, and partial-write safeguards where testable. + +- [~] Implement account creation: + - Add the creation flow directly to the login prompt when an email account is missing. + - Enforce unique email-based account identifiers. + - Enforce password complexity: minimum 8 chars, at least one uppercase letter, one lowercase letter, and one number. + - Store passwords securely using one-way hashing plus salt. + - Add unit tests for email normalization/uniqueness checks, password complexity, and account creation success/failure paths. + Status: backend helpers and the login-prompt create-on-miss flow are wired into `nanny()`, new accounts are created as unverified, account creation immediately sends a verification code by email, mail delivery now supports a configurable local capture command for smoke testing, and creation fails closed if account storage is unreadable; remaining work is broader interactive smoke coverage around edge cases. + +- [~] Implement account authentication: + - Add login prompts/state transitions for email-first account lookup and password entry. + - Validate credentials against stored account JSON. + - Add failure handling, lockout/throttling considerations, and clear player messaging. + - Prevent blocked accounts from authenticating or entering the game according to the chosen policy. + - Add unit tests for successful login, failed login, blocked-account handling, and password verification behavior. + Status: the live login prompt is now email-first, authenticates against account JSON, sends emailed verification codes for pending accounts, requires a valid unexpired code before entering the account menu, rate-limits resend attempts, invalidates codes after repeated failures, and now has local smoke coverage for create-account, verify, login, password reset, and re-login flows; remaining work is deeper interactive smoke coverage for linking and play-character paths. + +- [~] Build the account menu workflow: + - Add an account menu shown immediately after successful login or account creation. + - Add a simple "list characters" option that prints the linked character names. + - Add a "play character" option that selects one linked character and enters the world. + - Add an "add existing character" option that prompts for legacy character name and legacy character password. + - Add a "create new character" option that bridges into the existing character-creation flow under the authenticated account. + - Change post-creation persistence so newly created characters are written directly into account-native JSON `character.json` / `objects.json` / `exploits.json` storage instead of being born in the legacy file layout and migrated afterward. + - Add a "reset password" option that prompts for old password, new password, and confirmation. + - Add unit tests for account-menu helper logic where practical and smoke test the full menu flow locally. + Status: the live menu flow is now wired into `nanny()` with all requested options and sits behind verified-email gating; a local proxy-backed smoke test now covers account creation, emailed verification, verified-account login, character listing, password reset, logout, and re-login with the new password, and remaining work is broader automated coverage plus deeper socket-level smoke coverage for link/play paths. That smoke run now lives outside `make test` and should be run manually via `make smoke-account` when validating account/login/authentication changes. + Update: socket-level smoke coverage now also covers creating a new character from the account menu, reconnecting, and entering the world through account-backed character selection. + Update: account-created characters now write an account-native `character.json` as part of their initial account-link path, but object/exploit birth storage still needs the same direct-account-native cutover. + +- [~] Implement administrator account management: + - Add admin-visible commands or menu tools for account lookup. + - Add a way to view all characters linked to an account. + - Add a way to link/add a character to an account as an administrator. + - Add a way to block or unblock an account. + - Add a way to reset an account password securely. + - Record audit information for sensitive admin actions where practical. + - Add permission checks and clear logging for account-management actions. + - Add unit tests for permission checks, block/unblock behavior, password reset behavior, character listing, and admin-driven character linking. + Status: admin commands and helper tests are in place, including account email verify/unverify support; menu/help/doc polish is still pending. + +- [~] Implement character linking for existing characters: + - Verify ownership/authentication rules for linking an existing character. + - Read the current character file from `lib/players`. + - Read the current object save data from `lib/plrobjs`. + - Read the current exploit history from `lib/exploits`. + - Convert all three into JSON and store them as separate account-owned `character.json` / `objects.json` / `exploits.json` files. + - After successful account-owned character storage is written and linked, delete the old legacy player/object/exploit files for that migrated character. + - Record linkage metadata in the account-owned character record so the account can list/select the character later and so the account file clearly shows which `character.json` / `objects.json` / `exploits.json` files are linked. + - Verify the legacy character's existing password before migrating/linking it through the account menu flow. + - Add unit tests for successful migration, duplicate-link prevention, missing legacy file handling, password verification, and rollback/error behavior where practical. + Status: migration/link helpers, duplicate-link protection, rollback protection, legacy-character password verification, storage refresh helpers, immediate migration for account-created characters, immediate account-owned `objects.json` / `exploits.json` creation, and sanitization of the persisted transitional `.migration.json` player payload are now in place; remaining work is the last migration-policy cleanup plus removing the temporary dependency on legacy-format birth writes for newly created account characters. + +- [~] Update runtime character selection flow: + - After account login, enter the account menu instead of dropping directly to character selection. + - Allow selecting a linked character from the account menu to enter the world. + - Define behavior for accounts with zero linked characters from the menu. + - Preserve compatibility with current descriptor/login state machinery. + - Add unit tests for account-with-no-characters, valid character selection, and invalid/unlinked character selection paths where practical. + Status: account-authenticated login now lands in the account menu, handles zero-character accounts, can play linked characters from there, requires account-owned character storage readiness for linked-character play, loads player data directly from account storage, clears stale runtime object/exploit files before account-backed play, loads object/alias/follower save bytes from account storage when the runtime object file is absent, serves exploit history from account storage when no runtime exploit file exists, preserves account-backed exploit history across ordinary saves, self-heals corrupt account-owned character storage and corrupt runtime exploit files, and now fails closed on duplicate ownership or storage-identity mismatches; remaining work is deeper interactive smoke coverage and the remaining migration-policy cleanup. + Update: account-menu new-character creation now smoke-tests cleanly against the account-backed play path because migration resolves the real versioned player-save filename written by fresh character creation. + Update: account-backed selection now prefers direct `character.json` load and only falls back to migration when the authoritative account-native character file is absent. + +- [x] Implement active account-session/reconnect guard: + - Add an account-scoped active-session lookup for the live descriptor list: + - match the authenticated normalized account on descriptors with the same account identity + - ignore the current descriptor and unauthenticated/login-in-progress descriptors + - include descriptors in `CON_PLYNG` so a second account connection sees an already-playing character + - include descriptors in `CON_LINKLS` so a reconnect after a dropped socket sees the linkless character still in-game + - require a live non-NPC character and verify that character is still linked to the authenticated account before treating it as the account's active character + - return the active character name, level, connection state (`playing` vs `linkless`), and whether selecting a different character is allowed + - Update the account menu display: + - show the currently active linked character when one exists, including enough state for the player to understand whether it is still playing or linkless + - keep the normal linked-character count and menu options visible + - do not show the level-91 lock hint in the account menu when no over-level-91 choice is relevant; keep the menu focused on which character is active + - Gate account-menu actions while an active character is level 91 or below: + - allow listing linked characters, password reset, and logout + - allow selecting/resuming the same active character so the existing reconnect/usurp path can take over that body + - reject selecting any different linked character with a clear message and return to the account menu or account character prompt + - reject account-menu new-character creation because it would enter the game as a different character + - allow adding/linking an existing character while restricted because it changes the account roster but does not enter the game as another character + - Preserve high-level exception behavior: + - when any active linked character on the account is over level 91, keep the existing ability to select any linked character + - still show the active character in the account menu so the player understands that another body is in-game + - Keep existing same-character reconnect semantics intact: + - do not duplicate live `char_data` records for the same active character + - continue to close or usurp the old descriptor using the current `complete_existing_character_login(...)` / `CON_SLCT` reconnect logic + - preserve account fields on the new descriptor so returning to the account menu still works after reconnect + - Add focused unit coverage, with `Bazarat` pressure-testing the cases: + - account menu shows an active linked character for a `CON_PLYNG` descriptor on the same account + - account menu shows an active linked character for a `CON_LINKLS` descriptor on the same account + - active sessions from another account, unauthenticated descriptors, NPCs, and unlinked characters are ignored + - a level-91-or-below active character blocks selection of a different linked character + - the same active character can still be selected to reconnect + - an over-level-91 active character does not block selecting a different linked character + - a mixed active-session account with one over-level-91 character and one lower-level character remains unrestricted + - restricted accounts cannot create a new character from the account menu + - selected-character failures leave descriptor state clean and do not strand staged account/object data + - Add smoke/e2e coverage after the unit path is stable: + - extend `make smoke-account` or add a targeted proxy-backed flow with two simultaneous connections to the same account + - prove a second login sees the active character and cannot enter a different low-level character + - prove reconnecting the same linkless account-backed character succeeds without corrupting account-native character/object/exploit storage + Status: account-menu display and linked-character selection now scan live descriptors for same-account linked characters in `CON_PLYNG` and `CON_LINKLS`, show the active character in the account menu without a level-91 lock hint, block different-character selection and new-character creation while all active linked characters are level 91 or below, recheck stale character-menu and creation-wizard states before entering/birthing a character, preserve same-character reconnect/usurp behavior, and allow different-character selection when any active linked character on the account is over level 91. Focused unit coverage pins playing vs linkless display, absence of the menu lock hint, false-positive descriptor filtering, the level 91/92 boundary, mixed active-session high-level override, same-character usurp and linkless reconnect, side-effect-free blocking, stale-state races, and allowed list/reset/link/logout actions. The proxy-backed account smoke now includes a two-connection guard flow that proves a second login sees the active character, blocks selecting a different low-level character, and can reconnect the same active character. + +- [x] Add administrator account-selection unlock: + - Add `account unlockselect ` to the existing high-level account-management command surface. + - Reuse the current account identifier lookup so either email or internal account name works. + - Grant only a runtime, account-scoped, one-shot linked-character selection unlock. + - Refuse to grant an unlock if the account does not currently have a restricting active linked character session. + - Make linked-character selection and stale account-backed character-menu entry honor the unlock. + - Keep account-menu new-character creation and stale character birth blocked even when an unlock is pending. + - Consume the unlock when the account uses it to pass the final account-backed character-menu entry guard. + - Log the administrative grant and add focused unit coverage for command behavior, unlock consumption, non-use when no restriction exists, and the new-character non-bypass. + Status: `account unlockselect ` now resolves accounts by email or internal account name, grants a runtime-only account-scoped one-shot linked-character selection unlock only when the account currently has a restricting active linked character session, lets the early linked-character selection prompt and final account-backed character-menu entry guard honor that pending unlock, consumes it at final entry, and leaves account-menu new-character creation plus stale account-backed birth blocked. Immortal help documents the command and its one-use selection-only scope. + +- [ ] Handle migration and backward compatibility: + - New characters created through the account flow must be created directly under account-owned JSON storage, not legacy player/object/exploit files. + - Decide how renamed/deleted characters affect linked account data. + - Add guardrails to prevent duplicate links or partial conversions. + - On successful migration of a legacy character into account-owned JSON storage, delete the old legacy player/object/exploit files instead of retaining or archiving them in place. + - Define how admin-added character links interact with legacy standalone character login rules. + - Keep `player_table` as a unified boot-time index for both legacy characters and account-native characters; account-native-only characters should be indexed at startup, not only when selected through the account flow. + - Fail closed if the same normalized character name appears in both legacy storage and account-native storage, or in multiple account-native records, because character identities should never be duplicated across stores. + Status: boot-time `player_table` indexing now scans both legacy player files and account-owned `character.json` files, account-native name-based loads now resolve through the shared `player_table`, and duplicate names fail closed during startup; remaining work is deleting legacy files after successful migration, finishing the direct-authority `objects.json` / `exploits.json` cutover, and closing the remaining rename/delete policy decisions. + +- [~] Replace legacy runtime persistence with account-native JSON persistence for new characters: + - Define the authoritative JSON schema for character state, object state, and exploit history for newly created account characters. + - Base `character.json` on the normalized post-load runtime character/player structs, not on the raw legacy save-file text layout. + - Include profession/class points, coeffs, and other gameplay-relevant point/coefficient fields in `character.json`. + - Use `mystic` terminology in the schema/docs for the profession represented internally by legacy `PROF_CLERIC` fields. + - Store those three assets in separate per-character `character.json`, `objects.json`, and `exploits.json` files under account-owned storage instead of a single bundled file or any shared multi-character JSON file. + - Record references to those separate JSON files in the account-owned character metadata so the account file can show what is linked. + - Remove the remaining transitional single-file character snapshot layout once the separate per-character JSON assets are in place. + - Write new-character saves directly into account-owned JSON storage instead of legacy `players`, `plrobjs`, and `exploits` paths. + - Update load/save/runtime flows so account-selected play reads and writes the JSON-backed form directly for new characters. + - Update boot-time player indexing and name-based character loading so both legacy characters and account-native characters populate and resolve through the same `player_table`. + - Keep legacy file ingestion only for migrated pre-existing characters until the full cutover is complete. + - When a pre-existing legacy character is migrated successfully, remove its legacy on-disk player/object/exploit files so account-owned JSON storage becomes the only authoritative copy. + - Add unit tests and smoke coverage proving a newly created character can be created, saved, reloaded, and played without depending on the legacy on-disk format. + Status: the shared `character_json` module is now in place, account-layer helpers can read/write/remove per-character `character.json` files in the flat account directory layout, new account-created characters now write `character.json` during initial linking, legacy-character migration now also writes/backfills authoritative `character.json` from decoded legacy player data, migration now prefers a valid versioned player save over a stale flat file when both exist, retires that stale flat artifact during successful migration, cleans up newly written account-native outputs again if stale-flat retirement fails, and no longer persists raw legacy player-file bytes into the transitional `.migration.json` artifact at rest. Account-backed selection now prefers direct `character.json` load, no longer re-reads the migration snapshot just to clear runtime support files after fallback migration, now succeeds when a valid authoritative `character.json` exists even if the old migration snapshot is corrupt, and now fails closed if only the transitional snapshot remains while the authoritative `character.json` is missing. Ordinary saves now refresh account-native character files when they already exist, linked characters now repair a missing `character.json` directly from current store state instead of reviving the old snapshot-refresh path once migration has retired their legacy files, and unreadable account records now fail closed instead of letting account-native saves fall back to legacy player files. Boot-time player indexing/name-based loading now also include account-native characters, and the legacy boot scan now ignores flat player artifacts when a valid versioned sibling exists so pre-migration stale-flat data no longer causes duplicate-name startup failures. The `character_json` boundary is also tighter now, with required top-level section enforcement, explicit rejection of legacy `cleric` in favor of `mystic`, complete `flags` / `professions` object enforcement, and fail-closed parsing for incomplete structured affects. The `objects.json` cutover is also now further along with a shared `objects_json` module, account-owned object-file read/write helpers, loader preference for account-native object files, object-save refresh after crash/rent/idle writes, default empty `objects.json` creation for new account-born characters, immediate account-owned `objects.json` creation during migration when legacy object data is valid or absent, focused loader coverage proving account-backed login can equip staged objects, and migration-parity coverage proving legacy object payloads and the resulting account-owned `objects.json` decode to the same structure; the `exploits.json` cutover now has a shared `exploits_json` module, account-owned exploit-file read/write helpers, default empty `exploits.json` creation for new account-born characters, immediate account-owned `exploits.json` creation during migration when legacy exploit data is valid or absent, direct account-native exploit read/write preference for linked characters, and fail-closed coverage for corrupt authoritative exploit JSON. Remaining work is closing the remaining migration-policy/test gaps and continuing to run the smoke harness manually via `make smoke-account`, which still has a known telnet prompt-detection flake during some runs. + +- [ ] Add validation and tests: + - Treat unit tests as part of each implementation slice, not a final pass-only task. + - Unit tests for password validation and account schema parsing. + - Unit tests for bucket/path resolution for accounts. + - Unit tests for legacy-to-JSON migration helpers. + - Unit tests for blocked-account behavior. + - Unit tests for admin permission checks and admin account actions. + - Smoke test the login/account/character-selection flow locally. + - Smoke test administrator workflows for block, password reset, character listing, and character linking. + Status: focused unit coverage now includes unified legacy/account-native boot indexing, the shared `objects_json` and `exploits_json` round-trip modules, account-owned `objects.json` / `exploits.json` read/write helpers, account-backed object-save fallback, direct account-native exploit read/write preference, corrupt-authoritative-exploit fail-closed behavior, runtime-support-file clearing behavior, configurable verification-email delivery, versioned-player migration for freshly created characters, explicit precedence coverage proving a valid versioned legacy player save beats a stale flat file during migration, coverage proving that same migration still succeeds when the stale flat file is unreadable, boot-index coverage proving startup indexing prefers the versioned legacy save over a flat artifact even before migration runs, boot-index coverage proving successful migration also retires the stale flat artifact before startup indexing runs, direct account-native `character.json` file read/write/remove behavior, migration-time/backfill-time `character.json` hydration from real legacy player saves, cleanup-on-failure coverage proving stale-flat retirement failure removes partially written account-native outputs instead of leaving a duplicate boot hazard behind, direct account-native `character.json` plus `objects.json` staged-login coverage for equipped items, required-top-level-section enforcement for `character.json`, fail-closed nested `identity` / `progression` / `abilities` / `points` / `conditions` / `timers` / `perception` / `state` parsing in `character.json`, explicit missing-field regressions for each of those nested `character.json` sections, restore-path coverage proving mismatched migration identity does not overwrite stale runtime legacy files, coverage proving snapshot-only state no longer repairs a missing authoritative `character.json`, coverage proving corrupt snapshots do not block an already-authoritative `character.json`, legacy-file retirement immediately after successful migration into account-owned storage, rollback restoration when a later legacy retirement step fails after earlier files have already been removed, linked-character object/exploit loaders now using account-native JSON first and only still-present runtime legacy files second instead of decoding migration snapshot payloads, structured account-owned file inspection so unreadable authoritative character/object/exploit JSON fails closed instead of being misclassified as missing, runtime-legacy fallback coverage when account-native object/exploit JSON is absent, authority-order coverage proving account-native object/exploit JSON wins over conflicting runtime legacy data, fail-closed malformed-authoritative-object-JSON coverage that preserves the stale runtime file, fail-closed unreadable-authoritative-object/exploit coverage that preserves stale runtime files, save-path coverage proving already account-native linked characters do not attempt legacy snapshot refresh after migration retirement, that linked saves can repair a missing `character.json` directly from current store state, and that unreadable account records do not revive legacy player-file saves for account-native characters, legacy `cleric` rejection in favor of `mystic`, duplicate named `talks` rejection, unknown affected/hide flag rejection, missing structured-affect-field rejection, stored-object-path validation, narrowed `objects.json` field-range validation, empty/default `objects.json` round-trip coverage, required-top-level-section enforcement for `objects.json`, alias/follower truncation coverage, missing nested object/alias/follower field rejection in `objects.json`, missing nested object-affect field rejection in `objects.json`, stale verification-code rejection after resend, verified-account re-verification safety, and conflicting old/new-layout duplicate email record rejection. Focused `AccountManagement` is green at `100` tests, focused `DbLoader` is green at `28` tests, `make test` is now back to C++ unit-test coverage only and passes at `354/354`, and the proxy-backed Python smoke flow should be run manually via `make smoke-account` as a required separate validation step for account/login/authentication changes. Remaining work is broader interactive smoke coverage for legacy-character linking, the last migration-policy cleanup, and eventually tightening the flaky prompt-detection in the manual smoke harness. + +- [ ] Upgrade player colors to support true color selection: + - Keep the current per-category color slots, including `magic` and `weather`, but replace the legacy “small integer only” assumption with a richer internal color model. + - Define each stored color selection as a mode-aware value: + - `default` + - `ansi16` + - `truecolor` + - Preserve backward compatibility for older saved characters: + - existing integer color values should load as `ansi16` + - missing color data should still default safely + - old clients should still receive usable downgraded output + - Centralize color rendering so every colorized output path goes through one renderer that knows: + - the selected color slot + - the player’s configured foreground/background values + - the client’s supported color capability + - the required fallback behavior + - Support true color escape generation using standard terminal sequences: + - foreground: `ESC[38;2;R;G;Bm` + - background: `ESC[48;2;R;G;Bm` + - full reset at the end of colored segments: `ESC[0m` + - Introduce terminal-capability-aware fallback rules: + - no-color clients receive plain text + - ANSI-only clients receive nearest supported ANSI colors + - if an intermediate 256-color tier is added later, true color may downgrade to nearest 256-color before ANSI + - Extend account-native `character.json` color persistence from named integers to structured named objects. + - Proposed schema shape for future account-native color data: + - `foreground` and `background` should be stored independently per slot + - `background` should be optional and default to `default` + - example: + ```json + "colors": { + "magic": { + "foreground": { "mode": "truecolor", "r": 180, "g": 80, "b": 255 }, + "background": { "mode": "default" } + }, + "weather": { + "foreground": { "mode": "truecolor", "r": 90, "g": 170, "b": 255 }, + "background": { "mode": "truecolor", "r": 10, "g": 20, "b": 35 } + } + } + ``` + - Keep JSON deserialization backward compatible with the current integer form during the transition. + - For legacy text player-file compatibility: + - account-native JSON should remain authoritative + - legacy save compatibility should keep only the nearest ANSI fallback if needed + - true color should not require the legacy file format to become authoritative again + - Expand the `color` command UX without breaking existing syntax: + - keep `color ` working + - add forms like: + - `color magic fg hex #B450FF` + - `color magic bg hex #0A1423` + - `color weather fg rgb 90 170 255` + - `color magic bg default` + - validate RGB ranges and hex format strictly + - Update no-argument `color` output so it shows readable current values, for example: + - `magic: truecolor fg #B450FF bg default` + - `weather: truecolor fg #5AAAFF bg #0A1423` + - `chat: ansi bright magenta` + - Recommended rollout order: + 1. introduce the internal color model and centralized renderer + 2. add backward-compatible JSON read/write for the new schema + 3. expose true color selection in the `color` command for foreground values + 4. wire more message families through the centralized renderer + 5. add optional background-color support after foreground behavior is proven stable + - Recommended implementation boundary for v1: + - design the model for both foreground and background now + - implement foreground first + - treat background as advanced/optional follow-up work even though the schema should already support it + - Unit tests for: + - ANSI legacy color migration into the new model + - true color JSON read/write + - invalid RGB and hex rejection + - exact escape-sequence rendering + - capability downgrade fallback + - no-color plain-text fallback + - Regression coverage for: + - older characters still loading correctly + - existing color categories like `magic` and `weather` continuing to work + - spellcasting and other migrated message families still rendering correctly after the renderer centralization + +- [ ] Document the feature: + - Update help text/admin notes for account creation and linking. + - Document administrator account-management commands/workflows. + - Document new file locations, the separate `character.json` / `objects.json` / `exploits.json` asset layout, and migration behavior for operators. + +## Suggested Delivery Order + +1. Finalize account schema and migration rules. +2. Implement account JSON storage helpers plus their unit tests. +3. Implement password validation and secure hashing plus their unit tests. +4. Implement account creation/login flow plus their unit tests. +5. Build the account menu, password reset flow, and simple character listing. +6. Implement character link + migration flow plus their unit tests. +7. Wire play-character and new-character creation into the account menu flow plus unit tests where practical. +8. Implement the active account-session/reconnect guard so second logins and linkless reconnects cannot branch into another low-level character. +9. Replace new-character legacy birth writes with direct account-native JSON persistence for `character.json` / `objects.json` / `exploits.json`. +10. Implement administrator account-management tools plus their unit tests. +11. Add docs, smoke tests, and close any remaining test gaps. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..720b1e42 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +BUILD_DIR := build +SRC_DIR := src +CMAKE := cmake +CMAKE_CONFIGURE_ARGS ?= -DCMAKE_CXX_COMPILER=g++ +CMAKE_CACHE := $(BUILD_DIR)/CMakeCache.txt + +.PHONY: help configure setup build test run smoke-account format clean + +help: + @printf "Available targets:\n" + @printf " make configure Configure the CMake build in %s\n" "$(BUILD_DIR)" + @printf " make setup Create runtime directories and bootstrap files\n" + @printf " make build Build the ageland server binary\n" + @printf " make test Run the C++ unit tests\n" + @printf " make smoke-account Build the game/proxy and run the account smoke flow\n" + @printf " make format clang-format the WHOLE tree -- see the warning above the target;\n" + @printf " format only your changed files instead\n" + @printf " make run Build and start the server in the foreground\n" + @printf " make clean Clean the configured CMake build tree\n" + +$(CMAKE_CACHE): + $(CMAKE) -S $(SRC_DIR) -B $(BUILD_DIR) $(CMAKE_CONFIGURE_ARGS) + +configure: $(CMAKE_CACHE) + +setup: $(CMAKE_CACHE) + +$(CMAKE) --build $(BUILD_DIR) --target setup + +build: $(CMAKE_CACHE) + +$(CMAKE) --build $(BUILD_DIR) --target ageland -j16 + +test: $(CMAKE_CACHE) + +$(CMAKE) --build $(BUILD_DIR) --target ageland ageland_tests -j16 + ctest --test-dir $(BUILD_DIR) --output-on-failure + +run: build + ./bin/ageland -p 3791 + +smoke-account: setup build + cargo build -p proxy + python3 tools/account_smoke.py + +# DO NOT RUN THIS on a change you are about to commit. It is clang-format over the WHOLE +# tree: ~1200 lines nobody touched get rewritten (db.cpp and act_wiz.cpp worst), and the +# fragment #includes in account_management.cpp get reordered into something that does not +# compile. Format only what you changed: +# cd src && clang-format -i -style=WebKit +# (-style=WebKit is passed on the command line here too, which overrides the repo-root +# .clang-format, so that file is effectively unused -- match WebKit.) +format: $(CMAKE_CACHE) + +$(CMAKE) --build $(BUILD_DIR) --target format + +clean: + @if [ ! -f "$(CMAKE_CACHE)" ]; then \ + printf "No configured CMake build tree found in %s\n" "$(BUILD_DIR)"; \ + else \ + $(CMAKE) --build $(BUILD_DIR) --target clean; \ + fi diff --git a/README.md b/README.md index 958b3761..edc7338f 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,370 @@ # Return of the Shadow -This is the current live code for the MUD Return of the Shadow. The majority of the code base is C++, but there is still some C that is used to generate the random maze files and the PK Fame. + +This is the current live code for the MUD Return of the Shadow. The majority of +the code base is C++, but there is still some C that is used to generate the +random maze files and the PK Fame. + ## Getting Started -These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on the live system. + +These instructions will get you a copy of the project up and running on your +local machine for development and testing purposes. See deployment for notes on +how to deploy the project on the live system. + ### Prerequisites + On your Unix based system you'll need to install the following packages. 1. gcc (This is needed to create the C files) 2. g++ (This is needed for the main game compiler) 3. clang-format (We use this to format all the code base) 4. make (This is just something you should have in general) +5. cmake (Used by the root Makefile and the direct CMake workflow) +6. GoogleTest development files (Needed to configure and build `ageland_tests`) +7. 32-bit C/C++ development support (The game build uses `-m32`) +8. 32-bit libcrypt development files (Needed when linking the game) +9. Rust and Cargo (Needed for the proxy and `make smoke-account`) +10. python3 (Needed for the account smoke harness) + +On Debian or Ubuntu, the missing build prerequisites usually look like this: + +```bash +sudo dpkg --add-architecture i386 +sudo apt update +sudo apt install \ + build-essential \ + clang-format \ + cmake \ + g++-multilib \ + libc6-dev-i386 \ + libgtest-dev \ + libcrypt-dev:i386 \ + python3 +``` + +Some distributions package the 32-bit crypt development files as +`libxcrypt-dev:i386` instead of `libcrypt-dev:i386`. +Install Rust and Cargo with rustup or your system package manager. ### Installing + Below is a step by step series that help you setup your development environment #### Step 1: Fork the Project -First you'll need to fork this repository, and create a local clone of that fork. [please follow these instructions on how to fork a project](https://help.github.com/articles/fork-a-repo/) + +First you'll need to fork this repository, and create a local clone of that +fork. [please follow these instructions on how to fork a +project](https://help.github.com/articles/fork-a-repo/) #### Step 2: Setup the Player Files -After you have successfully forked this repository, you'll need to setup the games files. In your terminal navigate to the local repository and run the following commands. + +After you have successfully forked this repository, you'll need to setup the +games files. In your terminal navigate to the local repository and run the +following commands. + ```bash -cd src make setup ``` -This will create all user folder structure that the game needs to run. This will not important any characters to the game, so the first character created will be promoted to a level 100 Implementor. + +Or directly with CMake from the repository root: + +```bash +cmake -S src -B build -DCMAKE_CXX_COMPILER=g++ +cmake --build build --target setup +``` + +This will create all user folder structure that the game needs to run. This +will not important any characters to the game, so the first character created +will be promoted to a level 100 Implementor. #### Step 3: Setting up the World Files -We keep the world files in a separate git repository to keep from having merge conflicts with the main game code. -You'll need to fork the following repository [https://github.com/Noobinabox/RotS-WorldFiles](https://github.com/Noobinabox/RotS-WorldFiles) +We keep the world files in a separate git repository to keep from having merge +conflicts with the main game code. + +You'll need to fork the following repository +[https://github.com/Noobinabox/RotS-WorldFiles](https://github.com/Noobinabox/RotS-WorldFiles) -Once you have successfully forked the project, copy the files into the main code root directory. +Once you have successfully forked the project, copy the files into the main +code root directory. #### Step 4: Compiling the Game -Once all the game files are setup from Step 2 you'll need to compile the game. In your terminal navigate to the local repository and run the following commands. + +Once all the game files are setup from Step 2 you'll need to compile the game. +In your terminal navigate to the local repository and run the following +commands. + +```bash +make build +``` + +Or directly with CMake from the repository root: + +```bash +cmake -S src -B build -DCMAKE_CXX_COMPILER=g++ +cmake --build build --target ageland +``` + +> You'll see tons of notifications of deprecated functions, but the game should +> compile none the less. + +This will compile all the code and create an executable called ageland in the +./bin folder. + +#### Step 4a: Running the Unit Tests + +For the C++ unit tests you can use either workflow. + +```bash +make test +``` + +Or directly with CMake from the repository root: + +```bash +cmake -S src -B build -DCMAKE_CXX_COMPILER=g++ +cmake --build build --target ageland_tests +ctest --test-dir build --output-on-failure +``` + +#### Step 4b: Running the Account Smoke Test + +The account/login smoke flow is kept separate from `make test` so unit tests stay +fast and stable. Run it manually when validating account, login, authentication, +or character-selection changes. + +```bash +make smoke-account +``` + +The smoke harness creates and removes temporary ignored runtime data under +`lib/accounts`, `lib/players`, `lib/plrobjs`, and `lib/exploits`. Failed runs +preserve their `/tmp/rots-account-smoke-*` logs for debugging, and +`--keep-artifacts` also preserves the temporary account files. + +### Production Account Verification Email + +The account system sends verification codes through a local sendmail-compatible +command. By default the game executes: + +```bash +/usr/sbin/sendmail -t -oi +``` + +You can override that command with `ROTS_SENDMAIL_COMMAND`, but the usual Ubuntu +VPS setup is to install `msmtp` as the local sendmail bridge and have it relay +through the no-reply Gmail or Google Workspace account. + +#### Gmail Account Setup + +1. Enable 2-Step Verification on the no-reply Google account. +2. Create an app password for the VPS mail sender. +3. Use the full no-reply email address as the SMTP username. +4. Store only the app password on the VPS; do not commit it to this repository. + +Google currently requires an app password for this type of username/password SMTP +setup when 2-Step Verification is enabled. App passwords can be unavailable for +some accounts, including organization accounts with policy restrictions, +Advanced Protection, or security-key-only 2-Step Verification. Google Workspace +documents `smtp.gmail.com` with TLS port `587`, SSL port `465`, and app-password +authentication for app/device SMTP sending: + +* https://support.google.com/accounts/answer/185833 +* https://knowledge.workspace.google.com/admin/gmail/send-email-from-a-printer-scanner-or-app + +#### Ubuntu VPS Setup With msmtp + +Install the sendmail-compatible bridge: + +```bash +sudo apt update +sudo apt install msmtp msmtp-mta ca-certificates +``` + +Create the config where the user that runs `/usr/sbin/sendmail` can read it. +For a system-wide config, create `/etc/msmtprc`: + +```bash +sudo install -m 600 -o root -g root /dev/null /etc/msmtprc +sudo nano /etc/msmtprc +``` + +Example config: + +```ini +defaults +auth on +tls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt +logfile /var/log/msmtp.log + +account gmail +host smtp.gmail.com +port 587 +from no-reply@example.com +user no-reply@example.com +password YOUR_16_CHARACTER_APP_PASSWORD + +account default : gmail +``` + +Keep the config and log file locked down, but readable by the runtime user. If +the game runs as root, mode `600` is enough: + +```bash +sudo chmod 600 /etc/msmtprc +sudo touch /var/log/msmtp.log +sudo chmod 660 /var/log/msmtp.log +sudo chown root:adm /var/log/msmtp.log +``` + +If the game runs as a dedicated non-root user, the safest setup is usually a +per-user config owned by that game user: + +```bash +sudo -u rots install -m 600 /dev/null /home/rots/.msmtprc +sudo -u rots nano /home/rots/.msmtprc +``` + +Use the same config body shown above. The file must include +`account default : gmail`. If the user running `/usr/sbin/sendmail` cannot read +any config file with that default account, `msmtp` reports: + +```text +sendmail: account default not found: no configuration file available +``` + +Alternatively, keep `/etc/msmtprc` and grant a tightly scoped group read path to +that file for the game user. + +For per-user configs, also make the `logfile` path writable by that same user. +For example: + +```ini +logfile /home/rots/.logs/msmtp.log +``` + +```bash +sudo -u rots mkdir -p /home/rots/.logs +sudo -u rots touch /home/rots/.logs/msmtp.log +sudo -u rots chmod 700 /home/rots/.logs +sudo -u rots chmod 600 /home/rots/.logs/msmtp.log +``` + +#### Validation + +Send a direct test message from the VPS: + +```bash +printf 'To: your-test-address@example.com\nFrom: no-reply@example.com\nSubject: RotS mail test\n\nTest from RotS VPS.\n' | /usr/sbin/sendmail -t -oi +``` + +Also run the same test as the actual game service user: + +```bash +sudo -u rots sh -c "printf 'To: your-test-address@example.com\nFrom: no-reply@example.com\nSubject: RotS mail test\n\nTest from RotS game user.\n' | /usr/sbin/sendmail -t -oi" +``` + +Check delivery and the local msmtp log: + +```bash +tail -n 50 /var/log/msmtp.log +``` + +Then run the normal account smoke flow: + ```bash -cd src -make all +make smoke-account +``` + +For a systemd service, the default command usually needs no environment +override. If you want the service file to be explicit, add: + +```ini +Environment="ROTS_SENDMAIL_COMMAND=/usr/sbin/sendmail -t -oi" ``` -> You'll see tons of notifications of deprecated functions, but the game should compile none the less. -This will compile all the code and create an executable called ageland in the ./bin folder. + +#### Troubleshooting + +If no verification email arrives: + +1. Run the direct `/usr/sbin/sendmail -t -oi` test above from the same user that + runs the game. +2. If you see `account default not found: no configuration file available`, + create `~/.msmtprc` for the game user or make `/etc/msmtprc` readable by that + user, and confirm the config includes `account default : gmail`. +3. If you see `cannot log to ... Permission denied`, change the `logfile` path + to a file writable by the same user running `/usr/sbin/sendmail`, create its + parent directory, or temporarily remove the `logfile` line while testing. +4. Check `/var/log/msmtp.log` or the configured per-user log file for + authentication, TLS, or quota errors. +5. Confirm the Google account still has 2-Step Verification enabled and that the + app password has not been revoked. Google revokes app passwords after the + account password changes. +6. Confirm the VPS can make outbound TCP connections to `smtp.gmail.com:587`. +7. Check spam filtering on the receiving mailbox. + +Gmail and Google Workspace apply sending limits and may reject suspicious +messages. Google Workspace currently documents a rolling 24-hour sending limit +for Gmail SMTP users and recommends SMTP relay for organization app/device +sending at higher scale: + +* https://support.google.com/a/answer/166852 +* https://knowledge.workspace.google.com/admin/gmail/send-email-from-a-printer-scanner-or-app + +## GitHub Actions + +This repository includes a GitHub Actions workflow that runs on pushes to +`master` and pull requests targeting `master`. It builds the game, runs the C++ +unit tests, and then runs the proxy-backed account smoke flow. + +If you want GitHub to block merges until those checks pass, enable branch +protection for `master` in the repository settings and mark the `Build, Unit +Tests, and Smoke Tests` job from the CI workflow as a required status check. +If you also want to block direct pushes to `master`, make sure your branch +protection or ruleset disables direct-push bypass as well. #### Step 5: Running the Game -In the src directory you can run the following command. + +From the repository root you can run the following command. + ```bash make run ``` -Or if you want you do the following from the root directory in your local repository + +Or if you want you can still run the binary directly from the root directory + +```bash +./bin/ageland -p 3791 +``` + +Either command will start the game in the foreground and keep it attached to your terminal until you stop it. + +If you want the game to expect the Rust proxy header, use the explicit proxy flag instead: + ```bash -./ageland & +./bin/ageland -x 3791 ``` -Either command will start the game up and put it in a background process. + ## Contributing -Please read [CONTRIBUTING.md](CONTRIBUTING.MD) for details on our code of conduct, and the process for submitting pull request to us. + +Please read [CONTRIBUTING.md](CONTRIBUTING.MD) for details on our code of +conduct, and the process for submitting pull request to us. ## Releases and Design Documentation -All releases should be documented on what was changed and added. Please don't documentation line for line what you changed but a summarization so that we can present it to the end-users. You can find all the release notes here. + +All releases should be documented on what was changed and added. Please don't +documentation line for line what you changed but a summarization so that we can +present it to the end-users. You can find all the release notes here. + * [RotS Code Release Builds](release-notes/README.md) Design documentation should be added to the following location. + * [RotS Design Documentation](game%20design%20docs/README.md) ## Authors - * Seth Lyon [Noobinabox](https://github.com/Noobinabox) - * David Gurley [drelidan7](https://github.com/drelidan7) - * KJ Valencik [kjvalencik](https://github.com/kjvalencik) - * Lee Gurley [LeeIsMe77](https://github.com/LeeIsMe77) + +* **Seth Lyon** [Noobinabox](https://github.com/Noobinabox) +* **David Gurley** [drelidan7](https://github.com/drelidan7) +* **Andrew Humbert** [ahumbert](https://github.com/ahumbert) diff --git a/WIP.md b/WIP.md new file mode 100644 index 00000000..c431c005 --- /dev/null +++ b/WIP.md @@ -0,0 +1,1066 @@ +# Work In Progress + +## Current Feature Planning Task - MSDP Unit Test Coverage +- Active implementation slice complete: `GROUP` MSDP reporting now emits all group members with health/mana/movement percentages. +- User requirement: + - add as much unit-test coverage for the game's MSDP features as practical + - keep the work unit-test focused instead of relying on live telnet/proxy smoke tests +- Current implementation surface to cover: + - `src/protocol.cpp` / `src/protocol.h`: protocol creation/destruction, telnet MSDP negotiation, MSDP subnegotiation parsing, `SEND`, `REPORT`, `UNREPORT`, `RESET`, `LIST`, configurable variables, dirty/report state, send/update/flush functions, arrays, tables, sanitization, and ATCP fallback + - `src/comm.cpp`: `msdp_update()` periodic character/opponent/weather/stat updates + - `src/act_move.cpp`: `msdp_room_update()` room name/vnum/exits/terrain table and room-exit array updates + - existing test home: `src/tests/protocol_tests.cpp`, which currently covers protocol input fragmentation but not MSDP behavior +- Work items: + - [x] Add test helpers for protocol descriptors: + - create/destroy a descriptor with `ProtocolCreate()` + - attach a minimal player character where `MSDPSend()` needs `PRF_MSDP` + - capture `write_to_descriptor(...)` output without a live network server/client + - provide helpers to build MSDP subnegotiation bytes and parse emitted MSDP variable/value packets + - [x] Add protocol table/default-state tests: + - every enum-backed MSDP variable initializes with the expected type/default/report/dirty state + - GUI variables initialize to the expected button/gauge payloads + - configurable variables start from documented defaults or `Unknown` + - [x] Add MSDP negotiation tests: + - `IAC DO MSDP` negotiates MSDP support and emits the server id when re-enabling a previously disabled MSDP session + - rejection/disable paths clear negotiated state + - fragmented MSDP negotiation and subnegotiation still parse correctly through `ProtocolInput` + - [x] Add MSDP output-format tests: + - `MSDPSend()` emits correct MSDP bytes for string and numeric variables when MSDP is enabled + - `MSDPSendPair()` emits ad hoc variable/value pairs + - `MSDPSendList()` emits an MSDP array and converts spaces to `MSDP_VAL` + - `MSDPSetTable()` / `MSDPSendTable()` wrap payloads in table markers + - `MSDPSetArray()` wraps payloads in array markers + - ATCP fallback emits `MSDP. ` when MSDP is unavailable but ATCP is active + - [x] Add dirty/report-state tests: + - `MSDPSetNumber()` and `MSDPSetString()` mark variables dirty only when values change + - `MSDPUpdate()` sends only dirty reported variables and clears dirty flags + - `MSDPFlush()` sends a single dirty reported variable and leaves unrelated dirty state alone + - unreported variables remain dirty but unsent until reported again + - [x] Add command parser tests through MSDP subnegotiation: + - `SEND ` sends a single known variable + - `REPORT ` enables reporting and marks that variable dirty + - `UNREPORT ` disables reporting and clears dirty state + - `RESET REPORTABLE_VARIABLES`, `RESET REPORTED_VARIABLES`, and `RESET VARIABLES` clear all reporting state + - `LIST COMMANDS`, `LIST LISTS`, `LIST SENDABLE_VARIABLES`, `LIST REPORTABLE_VARIABLES`, `LIST REPORTED_VARIABLES`, `LIST CONFIGURABLE_VARIABLES`, and `LIST GUI_VARIABLES` return the expected arrays + - unknown commands and unknown variables are ignored without corrupting protocol state + - [x] Add configurable-variable tests: + - boolean variables accept only values in range + - string variables reject too-short values, trim non-printable characters, and clamp to max length + - write-once variables such as `CLIENT_ID` and `CLIENT_VERSION` can be set while `Unknown` and cannot be overwritten afterward + - invalid numeric strings and out-of-range numbers are ignored + - [x] Add escaping and malformed-input tests: + - `MSDPSanitizeValue()` escapes quotes, backslashes, newlines, carriage returns, tabs, and low control characters + - string setters store sanitized values + - malformed/truncated MSDP payloads do not crash, overrun buffers, or leak control bytes into normal player input + - oversized variable names/values are rejected or logged without writing partial protocol data + - [ ] Add `msdp_update()` game-state tests: + - [x] skips descriptors without characters, NPCs, missing protocol state, and characters in `NOWHERE` + - [x] skips corrupted out-of-range negative and high room indexes without emitting stale updates or stopping later descriptors + - [x] emits character name, level, race, alignment, experience-to-next-level, health, mana, movement, money, abilities, permanent abilities, wimpy, spirit, tactic, spell save/pen/power, armor absorption, offense/parry/dodge, attack speed, perception/willpower, encumbrance, regeneration, room name/vnum, and weather + - [x] emits NPC opponent name/level/health, PC opponent star-name/hidden level, and blank opponent fields when not fighting + - [x] validates division-by-zero or invalid max-health guard behavior for health percentage if tests expose a gap + - [x] emits group member names plus health, mana, and movement percentages as a structured `GROUP` table + - [ ] Add `msdp_room_update()` tests: + - skips NPCs and descriptors without protocol state + - emits room name/vnum, `ROOM_EXITS`, and `ROOM` table with `VNUM`, sanitized `NAME`, `EXITS`, and `TERRAIN` + - includes only valid exits and excludes null, hidden, or `NOWHERE` exits + - catches the current suspicious early-return path where normal non-negative rooms appear to skip room updates + - [ ] Refactor seams only if needed: + - expose a small protocol output sink or test hook instead of using real sockets + - keep production behavior unchanged unless tests reveal a clear bug + - prefer local helpers over broad rewrites of the KaVir protocol snippet + - [ ] Validation targets: + - focused `Protocol*` / MSDP test filters + - `make test` + - `make smoke-account` only if production MSDP behavior or telnet negotiation is changed +- Notes: + - `Bazarat` should be used during test design because this will be a broad non-trivial unit-test feature. + - `Magus` and `Vincent` review is required before finalizing the eventual implementation change set. +- Completed in current implementation slice: + - added socketpair-backed MSDP output capture in `src/tests/protocol_tests.cpp` + - added core MSDP tests for string sanitization, dirty state, exact MSDP packet output, ATCP fallback, list/table/array markers, update/flush behavior, `SEND` / `REPORT` / `UNREPORT` / `RESET` / `LIST` command parsing, configurable variables, write-once client identity, and unknown command safety + - fixed MSDP variable-table metadata macros so configurable string min/max values and GUI-variable flags match the `variable_name_t` field order + - fixed configurable string minimum-length validation so a payload containing only filtered control bytes cannot become an accepted empty string + - hardened public MSDP send helpers so pre-login or partially initialized descriptors without protocol/character state do not crash when clients send `SEND` or helper calls occur early + - hardened client-controlled configurable string filtering to use explicit printable ASCII checks instead of signed-`char` `isprint()` behavior + - added protocol default-state coverage for negotiated flags, feature booleans, configurable defaults, and GUI payload defaults + - added telnet MSDP/TTYPE negotiation coverage for exact negotiation bytes, disable/re-enable behavior, and server-id emission on MSDP re-enable + - added malformed/truncated/oversized MSDP coverage for empty values, oversized values, malformed marker order, oversized outgoing ad hoc payloads, and split subnegotiation across `ProtocolInput()` calls + - fixed split MSDP subnegotiation handling by moving the in-progress IAC/subnegotiation buffer onto `protocol_t`, so bytes received before `IAC SE` survive across network reads + - fixed the exact split between a subnegotiation terminator's `IAC` and `SE`, so the terminal `IAC` is buffered as control state instead of becoming part of the MSDP value + - hardened oversized unterminated subnegotiation recovery so a descriptor leaves IAC mode and accepts later normal input instead of repeatedly logging and returning + - hardened public MSDP string/table/array helpers with shared enum bounds checks + - added `msdp_update()` tests for descriptor skip behavior, minimal player state, blank opponent state, NPC opponent detail emission, PC opponent masking, and invalid opponent max-health handling + - strengthened `msdp_update()` coverage to assert exact emitted MSDP packets, dirty-flag clearing, and empty output from skipped NPC/`NOWHERE` descriptors + - tightened `msdp_update()` test fixture isolation by restoring the previous `top_of_world` value after each scoped room test + - fixed `msdp_update()` so a character in `NOWHERE` skips only that descriptor instead of returning from the full descriptor update loop + - hardened `msdp_update()` so corrupted negative or above-`top_of_world` room indexes skip only that descriptor before `world[...]` is indexed + - hardened `get_health_percent()` so invalid or zero max health returns 0 instead of producing an invalid percentage + - added broad `msdp_update()` stat coverage for alignment, experience math, mana, movement, money, current/permanent abilities, wimpy, spell values, armor/combat values, attack speed, perception/willpower, encumbrance, and regeneration + - added indoor and outdoor weather coverage, including newline-stripped outdoor weather text + - tightened weather global test isolation with a scoped weather guard + - added assertions that skipped invalid-room descriptors do not mutate stale weather or character-name state before being skipped + - added structured `GROUP` MSDP reporting with all group members, sanitized names, health/mana/movement percentages, empty-group clearing, and dirty-state coverage for changed group stats +- Validation so far: + - `clang-format -i -style=WebKit src/comm.cpp src/protocol.cpp src/protocol.h src/tests/protocol_tests.cpp` passed + - focused `./bin/tests '--gtest_filter=MSDPProtocol.MsdpUpdate*'` passed at `9/9` tests after bounds, broad-stat, and weather coverage were added + - focused `./bin/tests '--gtest_filter=MSDPProtocol.*:ProtocolInput.*'` passed at `39/39` tests + - `make test` passed at `588/588` tests + - `make smoke-account` passed the full proxy-backed account flow after the protocol metadata, pre-login hardening, split-subnegotiation, and `msdp_update()` fixes + - focused `./bin/tests '--gtest_filter=MSDPProtocol.*:ProtocolInput.*'` passed at `45/45` tests after group reporting was added + - `make test` passed at `647/647` tests after group reporting was added + - `make smoke-account` initially exposed stale prompt waits in the e2e harness after reset-password logout and at character selection; `tools/account_smoke.py` now reconnects after logout and waits for the actual `Character number or name:` prompt + - `make smoke-account` passed after the e2e harness fixes, including shell-quoted verification-email capture path generation +- Reviewer status: + - `Bazarat`: requested actual packet-output assertions for `msdp_update()` plus skip-output checks; addressed with exact `expected_msdp_pair(...)` assertions and stale opponent state setup for clear-to-empty branches + - `Bazarat`: requested room-bounds tests before broad stat expansion, broad character stat coverage, and indoor/outdoor weather coverage; addressed in the follow-up `msdp_update()` slice + - `Magus`: requested `top_of_world` fixture restoration; addressed in `ScopedMSDPTestRoom` + - `Magus`: requested scoped restoration for weather mutations; addressed with `ScopedSectorWeather` + - `Bazarat`: requested stale weather and skipped character-name assertions for invalid-room descriptors; addressed in the bounds regression test + - `Vincent`: clear; earlier out-of-range non-`NOWHERE` room-index hardening note has been addressed; longer-term `sprintf` replacement in MSDP packet builders remains a follow-up + - `Vincent`: noted future hardening for invalid room `sector_type` / weather index values before indexing `weather_info.sky` and `weather_messages` + - `Magus`: requested `GROUP` manual documentation, checked WIP status, and changed-payload test coverage; addressed after review + - `Vincent`: clear on `GROUP` reporting; noted inherited large MSDP table drop behavior via `MAX_VARIABLE_LENGTH` + - `Magus`: clear on the account smoke harness reconnect and prompt-marker fixes + - `Vincent`: requested shell-quoting for the generated sendmail capture path; addressed with `shlex.quote(...)` + +## Current Bug Task - Legacy Specialization Smoke Coverage +- Active slice complete: the proxy-backed legacy-link smoke flow now catches lost specializations with a non-zero legacy fixture and live `info` output assertion. +- Scope: + - add smoke-fixture coverage for a non-zero legacy specialization + - assert account-native `character.json` preserves that specialization after live account-menu legacy linking + - run the smoke harness with the new failing-first coverage and fix any production gap it exposes +- Validation: + - `python3 tools/account_smoke_tests.py` passed at `54/54` tests + - focused `AccountManagement`/`CharacterJson` specialization filters passed + - `make test` passed at `554/554` tests + - `make smoke-account` first failed on the new live specialization assertion, then passed after the harness waited for the specialization line itself + - final `make smoke-account` passed with post-save account JSON and post-save live reload assertions for the migrated legacy specialization +- Reviewer status: + - `Bazarat`: requested post-save and reload coverage; addressed with account-native identity checks after quit and a second account-backed live reload of the migrated legacy character + - `Magus`: requested write-side validation and fixture drift clarification; addressed with account-native write validation, malformed-specialization regression coverage, and fixture comments/tests + - `Vincent`: requested fail-closed handling for malformed legacy/runtime specializations; addressed before account-native serialization so invalid data cannot create or replace character JSON + +## Current Bug Task - Specialization Persistence +- Active slice complete: specializations now persist in account-native `character.json` and are preserved during legacy account conversion. +- Scope: + - reproduce the missing `char_file_u.profs.specialization` round trip in `character_json` + - add focused regression coverage with `Bazarat` pressure on malformed and unknown specialization cases + - persist the active specialization through account-native character JSON so legacy migration and account saves keep it +- Validation: + - focused `CharacterJson` specialization and account-native/migration filters passed + - `make test` passed at `553/553` tests + - `make smoke-account` passed the full proxy-backed account flow, including account-backed legacy play + - `git diff --check -- src/character_json.cpp src/character_json.h src/tests/character_json_tests.cpp src/tests/account_management_tests.cpp WIP.md` passed +- Reviewer status: + - `Bazarat`: requested non-zero round-trip, account-native wrapper, legacy migration, missing-field, and malformed-value coverage + - `Magus`: clear after review; requested explicit deserialize-time invalid specialization coverage + - `Vincent`: clear after review; requested enum-derived JSON fragments and explicit hostile account-file coverage + +## Current Bug Task +- Active slice complete: fixed linked-character selection and new-character creation still being blocked when the account has a level-92+ linked character in its roster. +- Scope: + - reproduce the mismatch between active-session-only level checks and roster-level exception expectations + - let a high-level linked character in account storage unlock linked-character selection and new-character creation even when the currently active linked character is lower level + - keep the active-session display and same-character reconnect behavior intact +- Validation: + - `clang-format -i -style=WebKit src/interpre.cpp src/tests/interpre_account_menu_tests.cpp` passed + - `cmake --build build --target ageland_tests -j16` passed + - focused active-session/linked-roster `InterpreAccountMenu` filter passed at `28` tests after duplicate-owner, ambiguous-active-session, and corrupt-file hardening + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `88` tests + - `make test` passed at `550/550` tests + - `make smoke-account` passed the full proxy-backed account flow +- Reviewer status: + - `Magus`: clear after ambiguous active-session precedence and pending-unlock cleanup + - `Vincent`: clear after ambiguous ownership and unlockselect trust-boundary review + - `Bazarat`: clear after duplicate-active-low, new-character name-confirmation, and unlockselect stale-state coverage + +## Current Build Task +- Active slice complete: fixed the raw `src/Makefile` link failure from `make all`. +- Scope: + - reproduce the raw Makefile build failure + - align the raw `src/Makefile` object list with source files already included by CMake when they provide linked server symbols +- Validation: + - `cd src && make all` passed + - follow-up `cd src && make all` passed after tightening new Makefile header dependencies + - manual CMake-vs-Makefile source/object comparison returned no differences after whitespace normalization + - `git diff --check -- src/Makefile WIP.md` passed +- Reviewer status: + - `Magus`: clear; noted pre-existing duplication risk between the raw Makefile and CMake source lists + - `Vincent`: clear after direct local headers were listed for the new save benchmark Makefile rules + - `Bazarat`: clear after the stale-object fix and the recorded source-list alignment validation + +## Current Documentation Task +- Active slice complete: documented production verification-email setup for operators. +- Scope: + - capture Gmail/Google Workspace app-password setup requirements + - document an Ubuntu `msmtp` sendmail-compatible bridge for the game's existing `/usr/sbin/sendmail -t -oi` integration + - include validation and troubleshooting steps without storing secrets in the repository +- Follow-up: + - clarify that `msmtp` must be configured for the same user that runs `/usr/sbin/sendmail`, otherwise it reports `account default not found: no configuration file available` +- Validation: + - `git diff --check` passed + +## Current Behavior Task +- Active slice complete: lowered the active account-session alternate-character exception threshold. +- Scope: + - accounts with an active linked character above level 91 can select a different linked character + - level 91 and below remain restricted to reconnecting/resuming the active character + - update active-session guard tests and planning docs from the old level 95/96 boundary to the new level 91/92 boundary +- Follow-up: + - if any active linked character on the account is above level 91, do not block linked-character selection for that account, even when another active linked character is level 91 or below + - this keeps builders and implementors able to test builds and scripting from alternate linked characters + - `first_restricting_active_account_session(...)` now treats any active level-92+ linked character as an account-wide selection override + - `InterpreAccountMenu.MultipleActiveSessionsAllowSelectionWhenAnyActiveCharacterIsOverLevelNinetyOne` proves a mixed active-session account can select a third linked character +- Validation: + - `clang-format -i -style=WebKit src/interpre.cpp src/tests/interpre_account_menu_tests.cpp` passed + - `git diff --check` passed + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*Active*:*SelectingSameActive*:*SelectingSameLinkless*'` passed at `14` tests + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `69` tests + - sandboxed `make test` passed `493/497` and failed only the local-socket `AcceptPathTest.*` cases with `Operation not permitted` + - unsandboxed `make test` passed at `497/497` + - sandboxed `make smoke-account` failed with local-socket `Operation not permitted` + - unsandboxed `make smoke-account` passed the full proxy-backed account flow, including the second-login active-character guard + - follow-up `clang-format -i -style=WebKit src/interpre.cpp src/tests/interpre_account_menu_tests.cpp` passed + - follow-up `git diff --check` passed + - follow-up `cmake --build build --target ageland_tests -j16` passed + - follow-up `./bin/tests '--gtest_filter=InterpreAccountMenu.*Active*:*SelectingSameActive*:*SelectingSameLinkless*'` passed at `14` tests + - follow-up `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `69` tests + - follow-up sandboxed `make test` passed `493/497` and failed only the local-socket `AcceptPathTest.*` cases with `Operation not permitted` + - follow-up unsandboxed `make test` passed at `497/497` + - follow-up unsandboxed `make smoke-account` passed the full proxy-backed account flow, including the second-login active-character guard + - reviewer follow-up added all-low plural restriction coverage, linkless level-92 override coverage, stale `CON_SLCT` final guard coverage, and quoted the README systemd environment example + - post-review `git diff --check` passed + - post-review focused active/stale `InterpreAccountMenu` filter passed at `17` tests + - post-review full `InterpreAccountMenu.*` passed at `72` tests + - post-review `make test` passed at `500/500` + - post-review `make smoke-account` passed the full proxy-backed account flow + - `Magus`: clear after all-low plural restriction regression and final re-check + - `Vincent`: clear after README systemd quoting fix; audit-log suggestion is non-blocking + - `Bazarat`: clear after all-low, linkless high-level, and stale final-guard regressions + +## Current Help Documentation Task +- Active slice complete: updated immortal help for account-management arguments. +- Scope: + - inspect the live `do_account` command surface + - update `lib/text/wizh_tbl` so `ACCOUNT` documents all implemented account-management subcommands and argument forms +- Validation: + - `git diff --check` passed + - reviewer pass requested from `Magus`, `Vincent`, and `Bazarat` + - addressed `Magus` feedback by documenting block reasons as `` + - addressed `Vincent` feedback by using a neutral block example, warning against PII/payment/private notes in persisted block reasons, and documenting inline passwords as temporary + - `Magus`: clear after re-check + - `Vincent`: clear after re-check + - `Bazarat`: clear after re-check + +## Current Release Notes Task +- Active slice complete: generated end-user patch notes for `release-notes/1.5.9/README.md`. +- Scope: + - compare `origin/release-frodo` to the current working tree + - match the sectioned release-note format of `release-notes/1.5.7/README.md` + - focus on end-user visible changes, not internal test/build churn +- Validation: + - `git diff --check -- release-notes/1.5.9/README.md WIP.md` passed + - reformatted `release-notes/1.5.9/README.md` from the 1.5.8 flat bullet style to the 1.5.7 sectioned style + - reviewer pass requested from `Magus`, `Vincent`, and `Bazarat` + - addressed `Magus` feedback by documenting still-unverified account verification, new-character blocking under active-session restrictions, and the staff account command as a real management feature + - addressed `Vincent` feedback by replacing broad staff wording with eligible account administrators and removing builder/implementor rationale from the active-session bullet + - addressed `Bazarat` feedback by documenting legacy character migration explicitly and narrowing account-backed persistence wording to migration/storage preservation + - `Magus`: clear after re-check + - `Vincent`: no blocking security/trust-boundary findings remain; exact 91/92 active-session boundary retained for behavior clarity + - `Bazarat`: clear after re-check + - sectioned-format re-check addressed reviewer wording feedback by softening mail-delivery phrasing, tightening account-backed preservation wording, and making account-administration text public-note appropriate + +## Current Status +- Active slice complete: added an immortal account command to unlock linked-character selection for a stuck active account session. +- Planned command behavior: + - add `account unlockselect ` to the existing `LEVEL_GRGOD` account-management command surface + - grant a runtime-only, account-scoped, one-shot linked-character selection unlock + - require the account to currently have a restricting active linked character session before granting the unlock + - let linked-character selection and the final account-backed character-menu entry guard honor the unlock + - keep new-character creation and stale account-backed birth blocked even when an unlock is pending + - consume the unlock when it is used to pass the final character-entry guard +- Completed in this slice: + - added `account unlockselect ` to `do_account` + - reused the active-session helper boundary for grant eligibility instead of duplicating descriptor checks in the command handler + - stored unlocks in runtime-only normalized account-name state + - let pending unlocks pass the linked-character selection prompt without consuming early + - consumed unlocks at the final account-backed character-menu entry guard and logged consumption + - discarded stale pending unlocks if the restricting active session is gone before use + - tied pending unlocks to the restricting character names present at grant time so they cannot apply to a later unrelated stuck session + - allowed a fresh admin grant to replace a stale pending unlock once the original restricting session is gone + - kept account-menu new-character creation and stale account-backed birth on the strict restriction path + - updated `lib/text/wizh_tbl` with usage, subcommand description, example, and runtime-only/one-use selection-only warning +- Validation so far: + - `clang-format -i -style=WebKit src/interpre.cpp src/interpre.h src/act_wiz.cpp src/tests/act_wiz_tests.cpp src/tests/interpre_account_menu_tests.cpp` passed + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=ActWiz.AccountUnlockSelect*:InterpreAccountMenu.UnlockSelect*'` passed at `8` tests + - `./bin/tests '--gtest_filter=ActWiz.*:InterpreAccountMenu.*'` passed at `89` tests + - `git diff --check` passed + - `make test` passed at `508/508` tests + - `make smoke-account` passed the full proxy-backed account flow after the stale-unlock lifecycle fixes +- Reviewer status: + - `Magus`: clear after stale-pending unlock replacement fix + - `Vincent`: clear; richer grant audit context is non-blocking and deferred + - `Bazarat`: clear after stale-use and stale-grant lifecycle regressions +- Follow-up complete: suppress the account-menu level-91 lock hint while keeping active-character display and blocked-action enforcement intact. +- Current follow-up changes: + - removed the "Different character selection is locked until ... is over level 91" line from the account menu active-session status + - kept the blocked-selection message shown when a player actually tries to enter as a different restricted character + - updated focused unit and smoke expectations to assert the menu only shows the active character, not the lock hint +- Current follow-up validation: + - `clang-format -i -style=WebKit src/interpre.cpp src/tests/interpre_account_menu_tests.cpp` passed + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `69` tests + - `python3 -m py_compile tools/account_smoke.py tools/account_smoke_tests.py` passed + - `python3 tools/account_smoke_tests.py` passed at `51` tests + - `git diff --check` passed + - `make test` passed unsandboxed at `497/497` tests + - `make smoke-account` passed with the menu hint removed and blocked-selection enforcement intact +- Current follow-up reviewer status: + - `Magus`: clear; residual risk is only that smoke does not explicitly assert absence of the removed hint, while unit tests do + - `Vincent`: clear; the removal is UI-only and blocked paths still send the enforcement message + - `Bazarat`: clear; menu omission and active-character display are unit-covered, and smoke still covers second-login enforcement +- Active slice complete: active account-session/reconnect guard is implemented for account-backed character selection. +- Completed in this slice: + - reviewed the current account menu, linked-character selection, reconnect, linkless descriptor, and `whoacct` account-session code paths + - added the requested active account-session behavior and implementation checklist to `FEATURES.md` + - added an account-scoped active-session helper that scans `descriptor_list` for same-account linked characters in `CON_PLYNG` and `CON_LINKLS` + - updated the account menu to show the active linked character and whether it is playing or linkless + - blocked selecting a different linked character and blocked new-character creation while the active account character is level 91 or below + - kept same-character reconnect/usurp behavior intact, including the second-session takeover path + - addressed review findings by adding guard rechecks for descriptors already sitting at the account-backed character menu or the final account-backed character birth path + - tightened active-session discovery so a character must point back to the descriptor that claims it + - preserved the over-level-91 exception so a different linked character can still be selected when the active character is level 92 or higher + - made the restricted-linking decision explicit: account-menu legacy linking remains allowed because it changes the roster but does not enter the game as another character + - expanded `InterpreAccountMenu` coverage for active playing/linkless display, false-positive descriptor filtering, descriptor/character back-pointer mismatches, level 91 vs 92 behavior, plural active-session display/restriction, side-effect-free blocking, same-character usurp, linkless reconnect/descriptor cleanup, stale character-menu and stale creation-wizard races, new-character blocking, and list/reset/link/logout allowance + - updated the duplicate-owner account-backed birth regression to assert the new fail-early behavior, so an already-linked name is rejected before player-index or account-native asset writes + - expanded `make smoke-account` so the proxy-backed flow opens a second account connection while the low-level account-born character is active, proves the different linked character is blocked, and then reconnects the same active character +- Validation for this slice: + - `clang-format -i -style=WebKit src/interpre.cpp src/tests/interpre_account_menu_tests.cpp` + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `69` tests + - `python3 -m py_compile tools/account_smoke.py tools/account_smoke_tests.py` passed + - `python3 tools/account_smoke_tests.py` passed at `51` tests + - `git diff --check` passed + - `make test` passed unsandboxed at `497/497` tests + - `make smoke-account` passed with the new second-login active-character guard flow +- Reviewer status: + - `Magus`: clear; noted that live smoke covers second-connection usurp while linkless reconnect is unit-covered + - `Vincent`: clear; duplicate-owner stale birth, descriptor filtering, staged cleanup, and same-character reconnect boundaries are acceptable + - `Bazarat`: clear; unit coverage covers the linkless path, with residual risk limited to no full socket-drop e2e for `CON_LINKLS` +- Active slice complete: legacy account-link object conversion accepts older save files without follower sections. +- Completed in this slice: + - `maga` legacy linking reaches object migration and fails with `Truncated objects data while reading follower record.` + - local inspection shows `lib/plrobjs/K-O/maga.obj` has a valid rent header, object section, board points, and aliases, then ends immediately after the alias terminator with no follower section + - kept `objects_json::object_save_data_from_binary(...)` strict by default so current object-save writes still reject missing follower sentinels + - added an explicit legacy object parser entry point for migration that accepts EOF immediately after the alias terminator as an older no-follower save shape + - logged when migration takes that compatibility path, including the account and character being migrated + - added parser coverage proving strict parsing rejects the old shape, legacy parsing accepts it, and partial follower sentinels still fail + - added migration coverage proving old no-follower object saves become account-owned `objects.json`, while partial follower data fails closed, preserves legacy files, and cleans up account-native outputs + - reviewed with `Magus`, `Vincent`, and `Bazarat`; addressed Vincent's scoped-parser and migration-negative-coverage findings plus Bazarat's follow-up tests for missing final follower sentinels, account-native write strictness, and legacy exploit preservation +- Validation for this slice: + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=ObjectsJson.*'` passed at `14` tests + - `./bin/tests '--gtest_filter=AccountManagement.*Object*:AccountManagement.Migration*'` passed at `19` tests + - `git diff --check` passed + - `make test` passed unsandboxed at `483/483` tests + - `make smoke-account` passed the full proxy-backed account lifecycle, including legacy link and account-backed legacy play +- Residual note: + - accepting EOF at the exact pre-follower boundary can still be indistinguishable from a modern save truncated at that same boundary, so the compatibility behavior is migration-only and logged instead of being enabled for ordinary object-save parsing +- Active slice complete: removed the remaining legacy-shaped live-index dependency for new account-created characters. +- Completed in this slice: + - updated account-backed character birth so the live `player_table` entry points at the authoritative account-owned `character.json` path immediately after link success + - updated account-backed selection and linked-character saves to refresh the live player index with the account-native character path + - tightened account-born character introduction coverage so it runs without `players/`, `plrobjs/`, or `exploits/` directories, asserts same-process `load_char()` reads account-native JSON, checks account link metadata, and decodes default account-owned object/exploit assets + - added rollback coverage for the later failure case where account-native assets are written but account linking fails because another account already owns the character + - addressed review feedback by making the account-native player-index path update a declared `db.h` API, rejecting overlong account-native paths instead of truncating `player_table[].ch_file`, and covering birth/selection fail-closed behavior for long account email storage paths + - closed the remaining boot-time truncation path by routing account-native index population through the checked API, added startup fail-closed coverage, covered linked `save_char()` refreshing a stale live index to the account-native path, and strengthened rollback/selection tests to prove existing account-owned assets survive +- Validation for this slice: + - `clang-format -i -style=WebKit src/comm.cpp src/db.cpp src/db.h src/interpre.cpp src/tests/interpre_account_menu_tests.cpp` passed + - `git diff --check` passed + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `55` tests + - `./bin/tests '--gtest_filter=DbLoader.*:InterpreAccountMenu.*'` passed at `91` tests + - `make test` passed unsandboxed at `478/478` tests + - `make smoke-account` passed the full proxy-backed account lifecycle smoke flow +- Active slice complete: extended descriptor-state unit coverage for stale staged password rejection paths and legacy-link migration failure cleanup. +- Completed in this slice: + - added blank and weak-password account creation tests that clear stale staged `account_password` while staying in `CON_ACCTNEWPWD` + - added blank and weak-password reset tests that clear stale staged `account_password` while staying in `CON_ACCTRESETNEW` and preserve the stored password hash + - added a correct-password malformed-object legacy-link migration failure test that clears `account_character_name` without creating a link or account-native character assets + - ran the repo formatter after refreshing CMake so the `format` target detected installed `clang-format`, then scoped formatter churn back to the touched files +- Validation for this slice: + - `make format` passed after regenerating the CMake build tree + - `clang-format -i -style=WebKit src/comm.cpp src/interpre.cpp src/tests/interpre_account_menu_tests.cpp` passed + - `git diff --check` passed + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `52` tests + - `make test` passed unsandboxed at `473/473` tests + - `make smoke-account` passed the full proxy-backed account lifecycle smoke flow +- Active slice complete: added focused descriptor-state unit coverage for account login, verification, account creation, reset, legacy-link cancellation, and delete secret-input behavior. +- Completed in this slice: + - pinned pending-verification login, verification cancel, invalid-code threshold, account creation mismatch/success, reset cancel/success, and legacy-link cancel/wrong-password descriptor transitions + - fixed stale transient descriptor state discovered by those tests (`account_password`, `account_character_name`, and verification `bad_pws`) + - added regressions around account-backed and legacy delete password input being treated as secret descriptor input, hidden from snoopers, and excluded from `last_input` replay +- Validation for this slice: + - `cmake --build build --target ageland_tests -j16` passed + - `./bin/tests '--gtest_filter=InterpreAccountMenu.*'` passed at `47` tests + - `git diff --check` passed + - sandboxed `make test` hit local socket `Operation not permitted` failures in `AcceptPathTest`; rerun unsandboxed passed at `468/468` tests + - `make smoke-account` passed the full proxy-backed account lifecycle smoke flow +- Active slice complete; expanded smoke e2e coverage and repeated validation are green. +- Active slice: expanding proxy-backed e2e coverage beyond the current account lifecycle smoke. +- Planned e2e additions: + - account-backed persistence flow: create a character, enter the game, change a persisted player-facing setting, quit through the normal save path, reconnect through the account menu, and prove the setting survived in account-native `character.json` plus the live command surface + - legacy link/migration flow: seed a real legacy player/object/exploit fixture, link it through account option `3`, verify account-native `character.json` / `objects.json` / `exploits.json` assets, verify legacy runtime files are retired, and prove the linked character can enter the game through the account menu + - keep both flows in the existing proxy-backed smoke harness unless runtime or readability forces a split +- Completed in this slice: + - added account-native `character.json` true-color foreground assertions to the smoke harness + - expanded `make smoke-account` so account-backed play now turns colours on, sets `magic` foreground to `#0C2238`, quits through the normal mortal save path, verifies the account-native character JSON, reconnects through the account menu, and verifies the live `color` listing still reports the persisted setting + - avoided the immortal-only `save` command after the first live attempt proved it is not available to mortal smoke characters + - added a live legacy-character fixture writer that seeds a versioned legacy player file before server boot using the old fixed-width password encoding and sentinel level/race/title/load-room/id values + - added legacy object and exploit fixture writers so migration verifies real object/exploit conversion immediately after successful link + - expanded `make smoke-account` so it first proves a wrong legacy password does not partially link or migrate assets, then links the legacy character through account option `3`, verifies account-native assets, verifies legacy player/object/exploit files are retired after migration, and enters the game with the migrated character through account-backed selection + - limited exact migrated object/exploit fixture assertions to the immediate post-link point because normal account-backed gameplay can rewrite object rent data on quit + - added expired verification-code resend handling to the smoke harness after repeated smoke validation exposed a time-window failure mode + - hardened smoke cleanup/name selection so generated names skip existing legacy and account-native artifacts, account lookup only scans `account.json`, retry is limited to the initial account-email prompt, child processes get an allowlisted environment, and cleanup removes only exact smoke-created fixture/account paths + - added live migrated-character `info` assertions so the e2e proves the account-backed loader uses the migrated sentinel title and level, not just that migration wrote the right JSON file + - updated `README.md` to document that `make smoke-account` creates and removes ignored runtime data under `lib/` +- Validation for this slice: + - `python3 -m py_compile tools/account_smoke.py tools/account_smoke_tests.py` passed + - `python3 tools/account_smoke_tests.py` passed at `51` tests + - `git diff --check` passed + - `make smoke-account` passed once with the expanded color-persistence and legacy link/migration/play flow before the expired-code resend patch + - a repeated `make smoke-account` run then exposed the expired verification-code path; the resend fix and focused Python regression are in place + - post-fix repeated `make smoke-account` passed `3/3` runs with the expanded color-persistence, legacy link/migration/play, and delete lifecycle flow + - Cargo still emits the existing workspace resolver warning during proxy builds; it did not affect the smoke result +- Next in this slice: + - no remaining implementation step for this slice + - optional cleanup remains for preserved debug artifacts under `/tmp/rots-account-smoke-*` and the kept debug account under `lib/accounts/P-T/smkb393ca001a76@example.com/` +- Active slice: repeated proxy-backed account smoke validation after the local world files were restored. +- Completed in this slice: + - ran `make smoke-account` three times after world data was present locally + - all three runs completed the full create -> verify -> login -> list -> reset password -> re-login -> create play character -> account-backed play -> create delete character -> delete back to account menu -> relogin-roster-check flow successfully +- Validation for this slice: + - `make smoke-account` passed `3/3` repeated runs + - Cargo still emits the existing workspace resolver warning during the proxy build; it did not affect the smoke result +- Active slice: documenting local development prerequisites that blocked setup in this environment. +- Completed in this slice: + - updated `README.md` prerequisites with the concrete applications/packages needed for CMake builds, C++ tests, 32-bit linking, the Rust proxy smoke flow, and the Python smoke harness + - documented Debian/Ubuntu package names for CMake, GTest, 32-bit multilib headers, 32-bit libcrypt support, and Python + - documented that Rust/Cargo must be installed +- Validation for this slice: + - reviewed the updated `README.md` prerequisite section +- Active slice: stabilizing the proxy-backed account smoke/e2e harness so prompt waits and startup failures produce deterministic results instead of intermittent `Account email:` / marker timeouts. +- Completed in this slice: + - switched the smoke harness to allocate distinct loopback ports by default instead of sharing fixed ports across runs + - kept dynamic game ports in the legacy-safe `20000..32767` range so the 32-bit game does not log/use wrapped negative port values + - added process-aware startup waits that fail immediately with game/proxy log tails when a child exits before readiness + - changed proxy readiness to wait for the proxy listening log marker instead of opening a throwaway proxied game session + - limited automatic retries to prompt-marker timeouts; startup/process exits now fail once with preserved artifacts + - cleaned generated repo account/character files, including current flat account-native character assets beside `account.json`, after failed runs by default while preserving `/tmp` logs for debugging + - added focused Python regressions for process-death diagnostics, refused-connect diagnostics, proxy log-marker readiness, dynamic port selection, mixed explicit/dynamic ports, flat account-native cleanup, non-retryable startup failures, and per-attempt dynamic port resolution +- Validation for this slice: + - `python3 tools/account_smoke_tests.py` passed at `33` tests + - `make test` passed at `459/459` + - `python3 tools/account_smoke.py --attempts 2 --startup-timeout 20` now fails fast locally with the real boot blocker, `Error opening index file 'world/scr/index': No such file or directory`, and preserves artifacts under `/tmp/rots-account-smoke-*` +- Remaining local validation gap resolved: + - after the separate world data was restored under `lib/world/`, repeated full `make smoke-account` runs pass locally +- Active slice completed: the first true-color groundwork pass is now green, with the richer foreground/background color model compiling cleanly and round-tripping through runtime rendering, account-native `character.json`, and legacy text player saves. +- Completed in this slice: + - added focused `Color` renderer regressions covering legacy ANSI foreground rendering, true-color foreground rendering, true-color background rendering, background clearing, and nearest-ANSI fallback mapping + - expanded `CharacterJson` coverage so structured color settings, legacy integer color compatibility, and out-of-range structured color validation are pinned directly + - expanded `AccountManagement` character-file coverage so account-native character JSON preserves structured color settings and rejects malformed color data + - expanded `DbLoader` legacy text-save coverage so `colorfg` / `colorbg` round-trip into live character rendering correctly + - preserved explicit ANSI fallback values for true-color entries in account-native JSON instead of always recomputing them from RGB +- Validation for this slice: + - `cmake --build build --target ageland_tests -j16` + - `./bin/tests '--gtest_filter=Color.*'` + - `./bin/tests '--gtest_filter=CharacterJson.*'` + - `./bin/tests '--gtest_filter=AccountManagement.*CharacterFile*'` + - `./bin/tests '--gtest_filter=DbLoader.LegacyPlayerTextRoundTripPreservesStructuredColorSettings'` + - `make test` passed at `452/452` +- Next recommended true-color step: + - add client-capability / downgrade behavior before exposing player-facing `color ... fg/bg rgb|hex` command syntax, so true-color selections do not become a user-facing footgun for non-truecolor terminals +- Active slice completed: the player-facing `color` command now supports explicit foreground/background true-color selection, and the player help entry documents the new syntax. +- Completed in this slice: + - kept the legacy shorthand `color ` syntax working + - added explicit command forms for: + - `color fg ansi ` + - `color fg rgb ` + - `color fg hex #RRGGBB` + - `color fg default` + - `color bg ansi ` + - `color bg rgb ` + - `color bg hex #RRGGBB` + - `color bg default` + - updated bare `color` output so it shows each slot as structured `fg ... bg ...` state instead of only the old single ANSI value + - added a new player help entry in `lib/text/help_tbl` covering the old shorthand, the new fg/bg forms, RGB/hex rules, and example commands +- Added focused `Color` regressions proving: + - legacy ANSI syntax still works + - RGB foreground selection works + - hex background selection works + - background reset works + - the bare `color` listing shows structured foreground/background state + - invalid RGB input is rejected without changing the slot +- Validation for this slice: + - `./bin/tests '--gtest_filter=Color.*'` + - `make test` passed at `458/458` +- Follow-up fix completed: the interpreter command table now accepts `color` as a real alias for `colour`, so the documented American spelling and the implemented command surface match again. +- Added a focused `Color.InterpreterAcceptsColorAsAliasForColour` regression to pin the command-table alias directly. +- Active slice: beginning the true-color upgrade by replacing the legacy “single ANSI index per slot” assumption with a richer runtime/account-native color model that can carry foreground/background true-color values while still keeping ANSI fallback data. +- Plan for this slice: + - expand the stored/runtime color representation so each slot can hold mode plus foreground/background values instead of only a single ANSI index + - keep legacy save/load and older account-native `character.json` color data backward compatible by treating existing numeric values as ANSI foreground selections + - add focused regressions for the new color model and compatibility paths before wiring it into `do_color` +- Active slice: wiring the new `magic` color slot onto live spellcasting messages so incantations and pre-cast room announcements actually use the configurable magic color. +- Plan for this slice: + - add focused `spell_pa` regressions for room-visible spellcasting text with and without `PRF_COLOR` + - route `say_spell(...)` and the delayed-cast muttering line through a shared magic-room-message helper + - keep the scope limited to spellcasting announcements, not every spell effect line +- Spellcasting announcements now use the `magic` color slot in the two main room-visible paths: + - `say_spell(...)` incantation lines + - the delayed-cast `begins quietly muttering...` room announcement before the spell resolves +- Added focused `SpellParser` regressions that prove: + - color-enabled observers receive magic-colored spell incantation output + - observers without `PRF_COLOR` still receive the same spellcasting text without ANSI color codes +- Validation for this slice: + - `./bin/tests '--gtest_filter=SpellParser.*'` + - `make test` passed at `445/445` +- Active slice: adding `magic` and `weather` as the next two configurable player color categories while leaving the final spare persisted color slot unused. +- Implementation plan for this slice: + - add dedicated `COLOR_MAGIC` and `COLOR_WEATHER` indices in the persisted color-slot layout + - update `do_color` so player-visible color categories only list real configurable fields instead of conflating the spare persisted slots with `off` / `on` / `default` + - persist the new categories cleanly through account-native `character.json` and account character file read/write coverage +- `magic` and `weather` are now added as real configurable color categories in the runtime color table, with the final sixteenth persisted color slot still left unused/reserved. +- `do_color` now treats the configurable color list as the real player-facing categories only, so `magic` and `weather` appear alongside the existing categories while `off` / `on` / `default` remain command words instead of being conflated with persisted color slots. +- Account-native `character.json` color persistence now uses named `magic` and `weather` keys, and the focused account-character read/write regressions now pin those two new categories in addition to the older custom color coverage. +- Validation for this slice: + - `cmake --build build --target ageland_tests -j16` + - focused `CharacterJson` and `AccountManagement` color regressions passed + - `make test` passed at `443/443` +- Active slice: fixing the account-backed persistence bug where combat tactics and two-handed weapon stance were lost after renting, returning to the account menu, and selecting the character again. +- Root cause: those states were runtime-only. `tactics`, `shooting`, `casting`, and two-handed stance were not persisted through the legacy text player save or the account-native `character.json` path, so rent/menu/replay reconstructed the character with default combat state. +- `src/db.cpp` now persists and restores: + - `tactics` + - `shooting` + - `casting` + - `twohanded` + through both the legacy text player-file parser/writer and `store_to_char(...)` / `char_to_store(...)`. +- `src/character_json.cpp` and `src/character_json.h` now carry the same combat-state fields in `character.json`, and older JSON files that omit them still load safely with normal/default values. +- Legacy/account-native hardening in this slice: + - malformed positive combat-state values from legacy saves are normalized back to safe defaults instead of being allowed to poison later account-native reads + - account-native export now normalizes unset or out-of-range combat-state values before writing `character.json` + - malformed account-native `character.json` combat-state values still fail closed on read by design; normalization is applied to legacy/runtime inputs, not to already-authored JSON files + - `Crash_load(...)` now clears persisted two-handed stance if the restored equipment does not actually support it (no wielded weapon or a shield is present) +- Added focused regressions proving: + - legacy text player-file round-trip preserves tactics, shooting, casting, and two-handed state + - account-native `character.json` write/read preserves those same values + - older `character.json` files that lack the new fields default back to normal tactics/shooting/casting and no two-handed stance + - out-of-range combat-state values in legacy text saves and stored characters are normalized back to safe defaults +- Validation for this slice so far: + - focused `CharacterJson`, `AccountManagement`, `DbLoader`, and affected `InterpreAccountMenu` regressions pass after a fresh rebuild + - `make test` is not a clean pass in this sandbox because the pre-existing `AcceptPathTest.*` listener tests cannot bind sockets here (`Operation not permitted`), so the full CTest wrapper still fails on those environment-restricted startup tests +- Broader test note: + - a manual run of `./bin/tests --gtest_filter=-AcceptPathTest.*` also exposed unrelated pre-existing `OlogHaiHelpers.*` failures in the current worktree; those are outside this tactics/two-handed persistence slice +- Follow-up discovered and fixed during this slice: + - older fixtures/account-native character records with unset combat-state fields were being serialized as invalid zeroes; `character_data_from_store(...)` now normalizes unset values to the normal tactics/shooting/casting defaults before writing JSON +- The immediate plan is: + - finish the reviewer pass for the tactics/two-handed persistence fix + - decide whether to extend manual smoke coverage specifically for this rent/menu/combat-state scenario +- `whoacct` is now wired into the command table as an immortal command. It lists authenticated account sessions by email, shows the currently played character when the session is in game, and shows account-side session labels like `Account Menu` when the account is connected but not yet playing a character. +- `whoacct` now renders with `Num / Account / Character / State / Site` columns similar to `users`, and account-side states are displayed as plain labels like `Account Menu` and `Character Select`. +- `whoacct` is now gated to the same higher-trust staff tier as `account`, and it only reports live post-auth account states instead of exposing pending-verification sessions. +- Added focused `ActWiz` regressions proving `whoacct`: + - lists only authenticated account sessions + - shows the live character name for account-backed in-game sessions + - shows `Account Menu` for authenticated account-menu sessions + - shows `Character Menu` for authenticated character-menu sessions without mislabeling them as `Playing` + - lists duplicate live sessions for the same account separately + - skips stale `CON_CLOSE` descriptors even if they still have account state set + - shows `Character Select` for authenticated character-selection sessions + - skips pending-verification sessions that have not reached the authenticated account menu/character flow yet + - sanitizes hostile email/host values before rendering them in the immortal-visible table + - keeps the rendered table stable for long email/host values and exact mixed-state output + - reports the no-sessions case cleanly +- Updated `lib/text/wizh_tbl` with a new `WHOACCT` immortal help entry and refreshed the `ACCOUNT` entry so both account-management commands are documented in the same table. +- `account show` now renders human-readable UTC timestamps instead of raw epoch values, including verification, verification-window, blocked, created, updated, and password-reset metadata where applicable. +- Added focused `AccountManagement` regressions proving the account summary: + - renders fixed UTC timestamps for the normal verified/blocked/reset happy path + - renders human-readable verification-window timestamps for pending-verification accounts + - omits unset blocked/password-reset metadata cleanly + - reports out-of-range persisted timestamps as `Invalid` +- Updated `lib/text/wizh_tbl` so the immortal `account show` help notes that timestamps are shown in UTC. +- Validation for this slice: focused `ActWiz` and `AccountManagement` regressions pass and `make test` passes at `436/436`. +- Recent completed slice: + - account-authentication observability in `nanny()` now mudlogs successful login, logout, invalid password attempts, and self-service password resets with focused regression coverage +- Added mudlogs for: + - successful account login + - account logout from the authenticated account menu + - invalid account password attempts + - self-service account password reset +- The immediate follow-up is account logout observability from the authenticated account menu. +- Added focused `InterpreAccountMenu` regressions that capture stderr and prove: + - login and reset logs include the account email plus host + - logout logs include the account email plus host exactly once + - invalid-password logs do not leak the attempted password + - pending-verification accounts are not mislabeled as bad-password attempts + - bad current-password attempts in the self-service reset flow log exactly once, leak no secret, and do not emit a reset-success event +- Validation for this slice: + - focused `InterpreAccountMenu` logging regressions pass + - `make test` passes at `427/427` + - `make smoke-account` still hit the known early prompt-detection flake before reaching the account menu, so there is no clean smoke confirmation for this observability-only change yet +- Durable repo state: + - successful migration no longer writes a routine `.migration.json` file, and normal account-native play/save paths no longer depend on that artifact + - account-native `character.json`, `objects.json`, and `exploits.json` are the intended authorities for linked characters + - the recent `Crash_load()` bugfix keeps missing legacy `plrobjs/...` files quiet on account-backed fallback while still logging real open failures +- `make test` now suppresses compiler warnings by default through the CMake test-target flags, so the build output stays focused on actual test failures instead of warning noise. +- The oversized `account_management` module is being split into smaller responsibility-based headers and implementation fragments so future account/auth/storage work is easier to navigate without changing the compiled entry point layout. +- `src/account_management.cpp` now keeps the shared helper/private logic while the public surface is broken out into focused identity, storage, assets, migration, and presentation fragments. +- Added compile-only test translation units so each new public `account_management_*` header has to compile on its own instead of only through the umbrella header. +- The transitional on-disk `.migration.json` artifact is now sanitized: it no longer persists raw legacy player-file bytes, so legacy password/host data is not carried forward at rest in migration metadata. In-memory migration data still keeps the player bytes long enough for the current rollback/restore helpers. +- Added focused regressions in `src/tests/account_management_tests.cpp` proving the persisted migration file omits legacy player password/host content while still preserving in-memory rollback data and the object/exploit migration payloads that remain in the transitional artifact. +- The account-backed play selector no longer exposes the generated internal account name in player-facing copy; it now says `Linked characters for your account:` instead. +- The immortal `account` command now accepts either an email address or the internal account name for lookup, so admins no longer need to know the generated internal name just to inspect or manage an account. +- Added a shared `read_account_file_by_identifier(...)` helper in `account_management` and wired `show`, `verify`, `unverify`, `block`, `unblock`, `passwd`, `addchar`, and `migratechar` in `src/act_wiz.cpp` through it. +- Account summaries now lead with the player-facing email and label the generated value as `Internal name:` instead of presenting it as the primary account identity. +- Added focused regressions in `src/tests/account_management_tests.cpp` for identifier-based lookup by email or internal name, mixed-case email normalization, and exact account-summary formatting. +- Added real command-level regressions in `src/tests/act_wiz_tests.cpp` that drive `do_account` through `show`, `verify`, `unverify`, `block`, `unblock`, `passwd`, and `addchar` using both email and internal-name identifiers, proving the immortal command path resolves through the new identifier lookup instead of only the helper layer. +- Added the missing command-level `migratechar` regression in `src/tests/act_wiz_tests.cpp`, including a boot-like fixture with a real player-table entry plus legacy object/exploit files so the admin email-lookup path is exercised through the full migration command. +- Validation is green for this slice: focused `AccountManagement` and `ActWiz` regressions pass, and `make test` passes at `417/417`. +- Fixed the account-backed crash reported after deleting one linked character and then selecting another to play on the same descriptor. +- Added a focused regression in `src/tests/interpre_account_menu_tests.cpp` that exercises the real flow: delete one linked character, return to the account menu, reopen the numbered selector, and successfully load the surviving linked character on the same connection. +- Tightened the follow-up lifecycle handling in `src/interpre.cpp` so the replacement `char_data` shell is only created when needed for account-backed selection, selection rejection before login leaves the descriptor shell null, and post-delete account-menu new-character creation now tolerates a null `d->character` instead of crashing on `free_char(...)`. +- Added two more focused regressions in `src/tests/interpre_account_menu_tests.cpp`: one proves a failed post-delete selection does not leave behind a replacement descriptor shell, and another proves delete -> account menu -> create new character recreates the shell cleanly on the same descriptor. +- Validation is green for this bugfix slice: focused `InterpreAccountMenu` regressions pass, `make test` passes at `413/413`, and `make smoke-account` passes on the live proxy-backed account flow after one bounded retry on the early telnet prompt wait. +- Account-menu character play selection is now numbered instead of name-based. +- The play prompt now lists linked characters as `1)`, `2)`, and so on, with `0) Back to Account Menu.` and a `Character number:` prompt. +- The manual smoke harness has been updated to drive the numbered selector and now passes again after fixing one remaining buffered-reader edge case where a prompt marker arriving in the final recv chunk could still time out. +- The selector is now truly number-only and bounded to the displayed roster range, so hidden entries and raw character-name input are both rejected in the account-backed play selector. +- The smoke harness now requires full account-menu and character-menu marker sets instead of accepting any one matching prompt fragment, and it now also checks `character_links` plus deleted account-native file cleanup on disk. +- The manual smoke harness is now materially more robust: `tools/account_smoke.py` uses a buffered prompt reader that preserves unread sanitized output across sequential prompt waits instead of dropping coalesced server output after the first matched marker. That fixes the intermittent account-menu / password-prompt flakes caused by multiple prompts arriving in a single recv. +- The smoke flow is also now split more cleanly by concern: one generated character is used to prove account-backed play, while a second generated character is created and then deleted directly from the character menu to prove the post-delete return-to-account-menu behavior without depending on the unrelated in-world `quit` / linkdead path. +- New focused Python regressions in `tools/account_smoke_tests.py` now cover buffered prompt consumption across coalesced prompts and the case where a delete success line and the returned account-menu text arrive in the same recv. +- Validation is green for this smoke-harness slice: `python3 tools/account_smoke_tests.py` now passes at `15` tests, and `make smoke-account` passes with the expanded create -> verify -> reset -> create play character -> play -> create delete character -> delete -> relogin roster flow. +- Account-backed character deletion now returns the player to the account menu instead of disconnecting the socket after a successful confirmed delete. +- The focused regression in `src/tests/interpre_account_menu_tests.cpp` now pins that successful account-backed delete both removes the account-native `character` / `objects` / `exploits` files and immediately renders the zero-character account menu again, instead of leaving the descriptor in `CON_CLOSE`. +- Validation is green on the unit side for this slice: focused account-delete regressions pass, and `make test` still passes at `407/407`. +- Manual smoke is now green on the broader account flow after the buffered prompt-reader fix; the earlier intermittent prompt-detection flake was a harness read-boundary bug rather than an account-menu behavior regression. +- The proxy-backed manual smoke flow is now broader and green locally: `tools/account_smoke.py` covers account creation, email verification, character listing, account-password reset, re-login with old-password rejection and new-password acceptance, new-character creation, account-backed play, returning from the character menu to the account menu with option `0`, account-password-backed character deletion, and a final re-login proving the roster is empty afterward. +- The smoke harness now uses account-specific and character-specific prompt waits (`0) Log out`, `5) Reset account password`, `0) Back to Account Menu.`, `5) Delete this character.`) instead of relying only on generic `Choice:` / `Make your choice:` markers, which makes the lifecycle assertions much less prone to stale-menu false positives. +- New focused Python regressions in `tools/account_smoke_tests.py` now cover split CR-NUL handling across recv boundaries in addition to the earlier split-IAC/subnegotiation cases, and helper-level assertions now verify the on-disk account character list matches expected linked-character state. +- Validation is green for this slice: `python3 tools/account_smoke_tests.py` now passes at `11` tests, `make test` passes at `407/407`, and `make smoke-account` passes with the expanded create -> verify -> reset -> create character -> play -> back to account -> delete -> relogin-empty-roster lifecycle. +- The proxy-backed smoke harness flake is now fixed locally: `tools/account_smoke.py` uses a stateful telnet stream sanitizer before marker matching, keeps both sanitized and raw timeout diagnostics, and now survives telnet negotiation noise and split `IAC` / `CR NUL` sequences that previously hid prompts like `Account email:` and `Verification code`. +- New focused Python regressions in `tools/account_smoke_tests.py` cover CR-NUL cleanup, split telnet negotiation across chunks, escaped `IAC IAC`, split subnegotiation, the negative case that incomplete negotiation noise does not fabricate prompt markers, and `recv_until(...)` timeout diagnostics that preserve both sanitized and raw output tails. +- Validation is green for this slice: `python3 tools/account_smoke_tests.py` passes at `8` tests, `make test` stays green at `407/407`, and `make smoke-account` now passes on the live proxy-backed flow. +- Account-backed character deletion now requires the account password instead of the legacy character-password path, and the confirmed delete flow is now account-aware all the way through: linked character JSON/object/exploit assets are staged, the account file is unlinked, and the staged assets are only removed after the unlink commits successfully. +- New focused regressions in `src/tests/interpre_account_menu_tests.cpp` cover the account-backed delete menu prompt, incorrect account-password rejection, correct account-password advancement to the permanent delete confirmation, and the full confirmed-delete cleanup of account-native files plus account linkage. +- Validation is green for this slice: focused account-backed delete regressions pass, and `make test` now passes at `407/407`. +- The live exploit-history regression reported from `print_exploits()` is now fixed locally: account-native `exploits.json` loads no longer hard-fail just because an older stored `victim_name` is longer than the legacy fixed-width field, and the compatibility truncation now emits a `SYSERR` warning so repaired audit data is visible in logs. +- New focused regressions in `src/tests/exploits_json_tests.cpp` cover the reported overlong-name compatibility case plus the exact fixed-buffer fenceposts: an exact-fit `victim_name` remains unchanged, and a one-byte-too-long `victim_name` truncates cleanly to a NUL-terminated legacy-width value. +- Validation is green for this slice: focused `ExploitsJson` now passes at `7` tests, and `make test` now passes at `401/401`. +- Legacy-character linking now uses player-facing success copy in both link flows instead of exposing account-storage internals: the account-menu path and the in-game `linkaccount` password-confirmation path both now say `Successfully added to your account.`. +- New focused regressions cover both success paths in `src/tests/interpre_account_menu_tests.cpp`, verify the account file really gains the linked character, and pin that the account-menu variant still immediately renders the follow-up menu after the success line. +- The display-name capitalization logic for those success lines now reuses a shared helper in `src/account_management.cpp`, and a new helper-level regression in `src/tests/account_management_tests.cpp` pins empty, lowercase, and mixed-case behavior. +- Validation is green for this slice: focused legacy-link message regressions pass, and `make test` now passes at `400/400`. +- Manual smoke is still flaky in the known telnet/proxy prompt-detection path: `make smoke-account` retried and then timed out in the verification/menu markers again, so this slice is unit-validated but the separate smoke harness issue is still unresolved. +- The account-backed character-menu label bug is now fixed in the live return-to-menu path: `extract_char(...)` in `src/handler.cpp` now routes back through the shared account-aware character-menu helper instead of sending the legacy raw `MENU` string directly, so account-backed sessions no longer fall back to `0) Exit from the MUD.` in that branch. +- New focused regressions now cover the previously missed branches in `src/tests/interpre_account_menu_tests.cpp`: the `extract_char(...)` return-to-menu path for account-backed characters, the real `CON_ACCTSLCT -> CON_SLCT` rerender path after an invalid menu choice, and an explicit legacy-session control proving non-account characters still see `0) Exit from the MUD.`. +- Validation is green for this label-fix slice: focused `InterpreAccountMenu` now passes at `19` tests, and full `make test` now passes at `395/395`. +- Review status for this slice: `Bazarat`, `Magus`, and `Vincent` all agreed the menu-label fix itself is covered, but `Magus` and `Vincent` also surfaced a separate high-risk follow-up that is not fixed yet: account-backed character self-delete still goes through the legacy character-password / legacy-file cleanup path, which can authenticate against the `*ACCOUNT*` sentinel and leave account-native artifacts behind. That needs its own bugfix slice with tests before the account-backed delete option is safe. +- Follow-up on the latest `Magus` / `Vincent` / `Bazarat` findings is now in locally: the real `CON_ACCTSLCT -> CON_SLCT` path keeps the authenticated account session through the character menu, account-backed rollback no longer recreates legacy files through `save_char(...)`, and the account-birth persistence deferral is now scoped to the actual character-creation states instead of broader account-session behavior. +- Focused regressions now pin those reviewer findings directly in `src/tests/interpre_account_menu_tests.cpp`: real account-backed character selection keeps the account session for menu options, account-backed rollback leaves no legacy or partial account-native files behind, legacy sessions still render the old disconnect wording, and `advance_level(...)` still persists outside the account-backed creation flow. +- Validation is green on the unit side after the follow-up: focused `InterpreAccountMenu` now passes at `16` tests, and the current full `make test` pass is green at `394/394`. +- Current smoke status is still red on the known harness flake: `make smoke-account` was rerun twice in this slice and both attempts timed out waiting for the initial `Account email:` marker even though the unit/regression coverage is green. +- The newly created account-character birth path is now fixed locally: account-backed new characters no longer create or clean up legacy player/object/exploit files during creation, and their first account-native save now preserves the real start room instead of overwriting it with `NOWHERE`. +- The new regression `InterpreAccountMenu.IntroduceCharForAccountBackedCharactersAvoidsLegacyFilesAndKeepsFirstLoginState` now exercises the real `introduce_char(...)` path and pins the behavior you reported: no legacy birth files, no shell `rm ...*` cleanup noise, valid account-backed object bytes on first login, correct start room, and correct naked perception/vision state after the first `Crash_load()`. +- Empty account-native object births now use a canonical crash-style rent code instead of `0`, so first login no longer reports the old “undefined rent code” warning just because the new character has no equipment yet. +- Validation is green on the unit side for this slice: the focused birth-path regressions pass, and `make test` now passes at `390/390`. +- The account-backed main character menu now labels option `0` as `Back to Account Menu.` instead of `Exit from the MUD.`, while legacy/non-account sessions still keep the old disconnect wording. +- Validation is green for that menu-copy fix too: focused `InterpreAccountMenu` coverage passes, and `make test` now passes at `391/391`. +- Manual smoke is still flaky in the known banner/prompt-detection path: `make smoke-account` timed out twice waiting for `Account email:` even though the focused direct-account birth regression is green, so the smoke harness still needs separate attention. +- The reported new-character start-state bug is now fixed locally: account-backed new characters now finalize their level-1 start state before the first account-native save, so they get the correct starting room and naked perception/vision state instead of being born with pre-start values in `character.json`. +- The regression for that bug is now covered in `InterpreAccountMenu.AccountBackedNewCharactersAreBornWithStartRoomAndNakedPerception`, which directly exercises the new start-state finalization helper and pins the expected load-room and perception values. +- Validation is green for this slice: focused `InterpreAccountMenu` coverage passes, `make test` passes at `387/387`, and `make smoke-account` passes through account creation, verification, login, new-character creation, and account-backed play. +- The account-menu linked-character list and play-selection prompt now both render in compact `who -s` style, so the two views stay visually aligned and use the same level/race/name layout. +- Validation is green for the who-style list slice too: focused `InterpreAccountMenu` and `AccountManagement` formatting regressions pass, `make test` passes at `388/388`, and `make smoke-account` passes after one bounded retry against the known telnet/proxy handshake flake. +- The account-backed rent/main-character menu now treats option `0` as “back to account menu” instead of disconnecting, so players can rent out one character and immediately switch to another linked character without reconnecting. +- Validation is green for that menu-flow fix: focused `InterpreAccountMenu` passes with `11` tests, `make test` passes at `389/389`, and `make smoke-account` passes. +- The manual account smoke harness is green again after updating it to match the current account-menu create-character flow that no longer asks for a separate legacy character password. +- The account character-list display now includes `who`-style level/race tags from account-native character storage, so linked characters render like `[ 50 WdE ] Aragorn` in the account menu instead of showing only the bare name. +- The account character-list display bug is now being fixed with a focused unit regression so linked character names from account storage display with a capitalized first letter in the account menu list instead of echoing raw lowercase storage values. +- The reported color-persistence bug is now fixed locally: account-native `character.json` files now preserve the actual custom color palette and legacy `color_mask` instead of silently dropping them during JSON serialization. +- The migrated-character login crash reported against `act_info.cpp` is now fixed locally: account-native `character.json` loads no longer apply onto uninitialized `char_file_u` memory, so omitted legacy-only fields like `profs.colors` cannot leak stack garbage into `store_to_char(...)` and blow up the first `do_look()` room render. +- The direct-launch login-display regression is now fixed locally: running `./bin/ageland -p 3791` no longer accidentally enables proxy-header mode, so direct telnet clients see the greeting and `Account email:` prompt immediately on connect instead of waiting for ENTER. +- The telnet login-display regression after the account rewrite is now fixed locally: telnet clients see the greeting and `Account email:` prompt immediately on connect instead of only after sending input. +- The emailed verification-code slice is implemented and hardened, and the account-authoritative cutover now reaches object saves as well as exploit history. +- The latest object-file cutover slice is implemented and validated, including the reviewer-reported empty-object-envelope ordering fix and the account-backed save-order fix that prevents linked object snapshots from being wiped on first save after login. +- Local proxy-backed smoke coverage now reaches account creation, emailed verification, verified-account login, character listing, password reset, logout, and re-login with the new password. +- The create-character smoke gap is now fixed: account migration resolves versioned legacy player-save filenames, and the proxy-backed smoke flow now reaches account-menu new-character creation, reconnect, and account-backed play successfully. +- Scope update: new account-created characters should ultimately live directly in account-owned JSON storage for character data, objects, and exploits, instead of being born in legacy files and migrated immediately afterward. +- Scope update: once a legacy character has been migrated successfully into account-owned JSON storage, its old legacy player/object/exploit files should be deleted. +- Scope update: character data should live in `character.json`, and object data plus exploit history should each live in their own JSON files, with account-owned references pointing at those files so the account file remains easy to inspect. +- Scope update: those JSON assets should be per character, not combined into any shared multi-character player/object/exploit JSON files. +- Scope update: the older single-file character snapshot layout is no longer the target design and should be replaced by separate per-character JSON assets plus account-owned references to them. +- Scope update: keep account-owned files directly under `lib/accounts///`, and prefix each character-owned asset filename with the character slug instead of using a per-character subdirectory. +- Scope update: `character.json` should be modeled from the post-load runtime character/player structs instead of the raw legacy save text so new and migrated characters converge on one canonical schema. +- Scope update: profession/class points and coeffs are important persisted gameplay data and must be included in `character.json`. +- Scope update: use `mystic` terminology in the new JSON schema/docs even where the legacy code still uses `cleric` identifiers internally. +- Scope update: `pretitle` and `prompt` are not needed in the new `character.json` schema and should be left out of the account-native character persistence format. +- Progress update: the first storage-helper pass for that flat account-directory layout is now in place, including slug-prefixed per-character asset filenames in account link metadata. +- Progress update: the reusable JSON reader/string-escaping code has now been split out of `account_management.cpp` into a shared `json_utils` module with its own focused unit-test coverage. +- Progress update: the shared JSON reader now rejects raw control characters inside JSON strings, and that malformed-input rule is covered directly in the new `JsonUtils` tests. +- Progress update: the shared JSON serializer now escapes any remaining low control bytes safely, and the reader understands the ASCII `\\u00XX` escapes it emits so the shared module round-trips its own output. +- Progress update: the first shared `character_json` module pass is now in place, covering profession points/coeffs, symbolic player/preference/affected flag arrays, structured affect state, and conversion helpers to/from `char_file_u`. +- Progress update: the shared `character_json` module now also covers persisted identity/physical fields, temporary and rolled abilities, point-state fields, conditions, timers, talks, skills, hide flags, and the related round-trip/apply validation against `char_file_u`. +- Progress update: account-native `character.json` helpers now read and write per-character JSON files under the flat account directory layout, and account-backed selection now prefers those files directly with migration fallback only when the authoritative character file is missing. +- Progress update: newly account-created characters now write their first `character.json` before linking completes, stale legacy object/exploit runtime files are cleared for account-backed play even for account-born characters, and focused regression coverage now exercises account-native character-file read/write/remove behavior. +- Scope update: boot-time `player_table` indexing must include both legacy characters and account-native characters, and duplicate names across those stores should fail closed because character identities should stay globally unique. +- Progress update: boot-time `player_table` indexing now scans both legacy player files and account-owned `character.json` files, name-based account-native loads resolve through that shared index, and duplicate names fail closed during startup instead of being silently preferred. +- Progress update: the first shared `objects_json` module is now in place, account-owned `.objects.json` files can be written/read from the account layer, object-load lookup now prefers those files for linked characters, and crash/rent/idle object saves now refresh the account-native object file after writing the legacy crashsave stream. +- Progress update: account-owned object-path handling now accepts safe legacy relative paths only when they still resolve to the expected `.objects.json` basename, rewrites those safe legacy paths back to the canonical basename on save, and still rejects traversal or mismatched paths. +- Progress update: focused runtime coverage now includes a staged account-backed `Crash_load()` path that exercises alias-tail loading from account-owned object bytes instead of only read/write helpers. +- Progress update: new account-created characters now lay down a canonical empty account-owned `objects.json` during their initial account-link flow instead of waiting for a later crashsave refresh. +- Progress update: legacy migration now writes account-owned `objects.json` immediately when the legacy object payload is valid, and falls back to an empty account-owned object file when the legacy character has no object payload yet. +- Progress update: focused loader coverage now proves an account-backed staged `Crash_load()` can equip a real wearable item and preserve carried inventory from account-owned object bytes. +- Progress update: focused migration-parity coverage now compares decoded legacy object payloads with the decoded account-owned `objects.json` result after migration so object-save structure matches through the cutover. +- Progress update: the first shared `exploits_json` module is now in place, account-owned `.exploits.json` files can be written/read from the account layer, exploit-load lookup now prefers those files for linked characters, and linked-character exploit writes now refresh the account-native exploit file directly instead of appending to legacy runtime files first. +- Progress update: new account-created characters now lay down a canonical empty account-owned `exploits.json` during their initial account-link flow, and legacy migration now writes account-owned `exploits.json` immediately when the legacy exploit payload is valid or absent. +- Progress update: focused loader coverage now proves corrupt authoritative account-owned `exploits.json` fails closed even when a stale legacy runtime exploit file exists, and linked-character exploit writes/readback now stay on the account-owned JSON path. +- Progress update: legacy-character migration now hydrates and backfills authoritative account-owned `character.json` from legacy player data during migration instead of relying on direct runtime decode of `migration.player_file`. +- Progress update: account-backed character selection now re-reads authoritative `character.json` after migration/backfill instead of decoding `migration.player_file` directly in `interpre.cpp`. +- Progress update: migration-focused test fixtures now generate real legacy player save text instead of placeholder blobs, so the new `character.json` backfill path is exercised through the actual legacy parser. +- Progress update: account-backed selection now also loads authoritative account-owned object-save bytes on the direct `character.json` fast path, so already-account-native characters no longer risk entering the world with an empty staged object envelope. +- Progress update: legacy-player migration now prefers a valid versioned player-save file over a stale flat player file when both exist, retires that stale flat artifact during successful migration, cleans up account-native outputs if stale-flat retirement fails, and keeps the boot-time player index from tripping duplicate-name failures by ignoring flat artifacts when a valid versioned sibling exists. + +## Current Task +- Close out the indexed account-character selection slice cleanly with reviewer follow-up. +- Keep the smoke harness stable now that the numbered selection flow is wired into the live proxy-backed account path; one first-attempt retry still showed up on the reset-password branch even though the second attempt passed. + +## Recent Progress +- Followed up on `Bazarat`, `Magus`, and `Vincent` by making `select_linked_character(...)` reject raw name input entirely and reject numeric selections beyond the displayed roster range, so the backend now matches the numbered UI exactly. +- Added focused regressions in `src/tests/account_management_tests.cpp` for name rejection and for rejecting selection `101` when only the first `100` linked characters are displayed. +- Added a focused interpreter regression in `src/tests/interpre_account_menu_tests.cpp` that drives `CON_ACCTSLCT` with `2` and proves the second linked character is the one loaded into the character-menu path. +- Fixed the stale `Character: ` rerender fallback in `CON_ACCTSLCT`; if the account cannot be reloaded on an empty submission, the flow now returns to the account email prompt instead of showing the old name-based selector copy. +- Tightened `tools/account_smoke.py` so `wait_for_account_menu(...)` and `wait_for_character_menu(...)` require the full intended menu markers instead of any single matching fragment, and added focused Python regressions for those stricter helpers plus account `character_links` expectations. +- Extended the live smoke flow to assert the account file’s `character_links` metadata matches the visible roster and to verify the deleted character’s account-native `character` / `objects` / `exploits` files are actually removed after account-backed delete. +- Tightened the final relogin roster assertion so it now requires the surviving numbered entry, the singular `1 character displayed.` footer, and the absence of the deleted character name from the visible roster. +- Fixed the smoke harness failure message so it no longer incorrectly claims artifacts were preserved when the default cleanup path was used. +- Re-ran `python3 tools/account_smoke_tests.py` successfully at `21` passing tests, re-ran `make test` successfully at `410/410`, and re-ran `make smoke-account` successfully after one bounded retry. +- Changed account-menu play selection to use numbered entries instead of typed character names, and updated the shared roster/prompt formatter so the same numbered `who -s`-style list is used for both “List linked characters” and “Play a linked character.” +- Updated `CON_ACCTSLCT` so input `0` returns directly to the account menu, while positive numeric input selects the corresponding linked character by roster order. +- Added focused regressions in `src/tests/interpre_account_menu_tests.cpp` and `src/tests/account_management_tests.cpp` for numbered roster rendering, `0` returning to the account menu, and numeric linked-character selection. +- Updated `tools/account_smoke.py` to use numbered character selection for both the play path and the delete-character path. +- Fixed the remaining smoke-reader bug where a marker could arrive in the final recv chunk just before the timeout boundary and still be reported as missing; added a focused Python regression for that case in `tools/account_smoke_tests.py`. +- Re-ran `python3 tools/account_smoke_tests.py` successfully at `16` passing tests and re-ran `make smoke-account` successfully with the numbered character-selection flow. +- Added the focused regression `InterpreAccountMenu.IntroduceCharForAccountBackedCharactersAvoidsLegacyFilesAndKeepsFirstLoginState`, which exercises the real account-backed new-character birth path end to end and proves first login uses account-native object bytes without creating legacy player/object/exploit files. +- Added the focused regression `InterpreAccountMenu.AccountBackedCharacterMenuUsesBackToAccountMenuLabel`, which proves the account-backed character menu renders the new `0) Back to Account Menu.` label instead of the old disconnect wording. +- Added an account-aware `show_character_menu(...)` helper in `interpre.cpp` so account sessions and legacy sessions can render the same main character menu with the correct `0` label for each flow. +- Updated `introduce_char(...)` so account-backed new characters skip the legacy `Crash_get_file_by_name(..., "wb")` birth write entirely, preserve the real start-room vnum in the first account-native `character.json`, and write their first `save_char(...)` using that real load room instead of `NOWHERE`. +- Updated `advance_level(...)` to defer level-1 exploit persistence and autosave for pre-linked account-backed births, which removes the last accidental legacy exploit/player writes during account-based character creation. +- Moved the birth exploit write to after the account-native link completes, so the first exploit record now lands in account-owned `exploits.json` instead of trying to go through the legacy runtime path. +- Normalized default empty account-owned object data so new characters get a valid crash-style empty object payload rather than a zero-rent placeholder. +- Updated `AccountManagement.WritesDefaultAccountNativeObjectFile` to pin that canonical empty-object default, and re-ran the full unit suite successfully at `390/390`. +- Re-ran `make smoke-account` twice for the follow-up and hit the existing `Account email:` prompt-detection timeout both times, so the new birth-path fix is unit-covered and green but the separate smoke harness is still flaky. +- Updated the shared `CON_SLCT` menu branch so account-backed characters return to `CON_ACCTMENU` on choice `0`, while non-account characters still follow the old disconnect path. +- Added the focused regression `InterpreAccountMenu.AccountBackedCharacterMenuChoiceZeroReturnsToAccountMenu`, which proves the post-login character menu now bounces back to the account menu for authenticated account sessions instead of closing the socket. +- Re-ran focused `InterpreAccountMenu`, re-ran the full unit-test path successfully at `389/389`, and re-ran `make smoke-account` successfully for the menu-flow follow-up. +- Updated the account-menu linked-character list and play-selection prompt to share a single compact `who -s`-style formatter, so both paths now display `[lvl race] name` entries with the same spacing, row wrapping, and displayed-count footer. +- Added focused regressions in `interpre_account_menu_tests.cpp` for exact who-style list rendering, unknown-file fallback rendering, truncation/count output, and the real `CON_ACCTMENU -> 2` play-selection prompt output. +- Tightened `account_management_tests.cpp` so the account-side prompt formatter is now pinned to the exact who-style roster shape instead of only checking for substrings. +- Re-ran focused `InterpreAccountMenu` and `AccountManagement` coverage, re-ran the full unit test path successfully at `388/388`, and re-ran `make smoke-account` successfully after one bounded retry. +- Moved new-character start-state finalization into `introduce_char(...)` after `init_char(...)`, so freshly created account-backed characters now persist their real post-start state instead of being saved before level/start-room/perception initialization is complete. +- Added `finalize_new_character_start_state(...)` in `limits.cpp` / `limits.h` so the start-state logic is centralized and testable instead of being an unpinned side effect in the create-character flow. +- Added the focused regression `InterpreAccountMenu.AccountBackedNewCharactersAreBornWithStartRoomAndNakedPerception`, which proves new account-backed characters get a valid level, the expected start-room vnum, and naked perception state before the first account-native save. +- Updated `tools/account_smoke.py` to match the current account-menu create-character flow that no longer prompts for a separate legacy character password, then re-ran the smoke path successfully. +- Cleaned up the `InterpreAccountMenu` fixture warning in `ScopedWorkingDirectory` and re-ran the focused test binary plus the full unit suite successfully. +- Used the reported symptom to trace the bug to a schema omission in `character_json`: `PRF_COLOR` survived in preference flags, but the actual persisted palette (`profs.colors[]`) and `profs.color_mask` were not represented in `CharacterData` at all. +- Added focused regressions in `character_json_tests.cpp` that prove custom color settings round-trip through `serialize_character_to_json(...)`, apply back into `char_file_u`, and remain backward-compatible with older `character.json` files that do not yet contain a color section. +- Added focused account-native regressions in `account_management_tests.cpp` that prove `write_account_character_file(...)` emits the new color section into `.character.json` and `read_account_character_file(...)` restores the exact custom color slots and legacy `color_mask`. +- Extended `character_json` so account-native character persistence now carries a top-level `color_mask` plus a named `colors` object keyed by color-setting names like `narrate`, `chat`, `roomname`, and `object`. +- Followed up on `Magus`' review by tightening color-value validation to the real supported runtime range (`CNRM..CBWHT`) and adding regressions that reject out-of-range named colors during direct deserialization and account-file reads. +- Rebuilt the test binary and passed the full unit-test path successfully at `378/378` after the color-persistence and color-range regressions. +- Used the provided gdb backtrace to narrow the crash to `do_look()` in `act_info.cpp` immediately after migrated-character selection, then traced the bad state back to account-native `character.json` loads writing onto an uninitialized `char_file_u`. +- Fixed `character_json::apply_character_data_to_store(...)` so it resets the destination store before populating JSON-backed fields, which prevents omitted legacy-only state like `profs.color_mask` / `profs.colors[]` from retaining garbage values. +- Added focused regressions in `character_json_tests.cpp`, `account_management_tests.cpp`, and `db_loader_tests.cpp` that poison legacy color fields first, then prove account-native loads clear them and no longer propagate bad color state into a live `char_data`. +- Rebuilt the test binary, passed the focused poisoned-store regressions, and re-ran `make test` successfully at `374/374`. +- Added a shared `parse_startup_options(...)` seam in `src/comm.cpp` / `src/comm.h` so startup-argument behavior can be unit tested directly instead of only through live process launches. +- Changed startup parsing so `-p ` now means the game port, plain positional ports still work, and explicit proxy mode now uses `-x`. +- Updated `main()` to consume `StartupOptions`, log the parsed launch mode cleanly, and keep `./bin/ageland -p 3791` on the direct telnet path instead of the proxy-header path. +- Added focused `StartupOptions` coverage for default launch mode, `-p `, explicit `-x` proxy mode with positional and `-p` ports, and rejection of stray extra arguments after a parsed port. +- Expanded `StartupOptions` coverage to reject stray arguments after positional ports, to pin the explicit mixed `-p ... -x` proxy contract, to accept the compact `-p3791` form explicitly, and to exercise the accept path itself with real loopback sockets so direct connections prove they receive the banner plus `Account email:` prompt immediately without sending ENTER. +- Hardened the proxy-header accept path so partial proxy headers are now buffered through the normal nonblocking descriptor input flow instead of stalling the accept loop, `pnew_descriptor()` still sends the direct banner immediately, and focused loopback coverage now proves no banner is sent before a partial proxy header completes. +- Added focused accept-path coverage proving a banned proxied IP is rejected before any greeting is sent once the proxy header resolves the real peer host. +- Updated the proxy-backed smoke harness to launch the game with `-x` so smoke runs still exercise the explicit proxy-header path after the CLI fix. +- Re-ran focused `StartupOptions` and accept-path coverage successfully, and re-ran the full unit test path successfully at `371/371`. +- Manually re-verified the reported reproduction path: direct `./bin/ageland -p 3791` plus telnet now shows the greeting and `Account email:` prompt immediately before any input, while `make smoke-account` still passes through the explicit proxy path. +- Enabled telnet protocol setup at descriptor creation in `src/comm.cpp`, started protocol negotiation before queuing the initial login output, and added protocol cleanup in `close_socket(...)`. +- Updated the two play-entry paths in `src/interpre.cpp` to reuse an existing descriptor protocol object instead of recreating it later. +- Followed up on Rawls' parser-boundary finding by hardening `ProtocolInput(...)` against short telnet/MXP fragments before any multi-byte lookahead, then followed up on Maxwell's packet-boundary findings by buffering incomplete telnet/MXP prefixes across calls so split negotiations still complete correctly, including the lone-`ESC` boundary. +- Added focused `ProtocolInput` regressions in `src/tests/protocol_tests.cpp` for lone IAC input, truncated handshake verbs, split handshake completion across two calls, truncated MXP-prefix buffering, and split MXP-prefix continuation after a lone `ESC`. +- Rebuilt the server, re-ran focused `ProtocolInput` coverage plus the full `make test` path at `359/359`, and manually validated with `telnet 127.0.0.1 4101` against the proxy-backed path that the welcome banner and `Account email:` prompt now display immediately before any input. +- Read `FEATURES.md`. +- Broke the feature request into smaller implementation slices. +- Added administrator account-management requirements to the feature plan. +- Mapped the current login flow in `src/interpre.cpp`. +- Confirmed existing player/exploit/object save directory layout under `lib/`. +- Confirmed there is no existing JSON library already wired into the repo. +- Added a standalone `account_management` module. +- Added bucketed account-path helpers, JSON serialization/deserialization, and file persistence helpers. +- Added secure account password hashing/verification using `libcrypt`. +- Built the test binary and passed the focused `AccountManagement` unit-test suite. +- Added admin-friendly helpers for blocking/unblocking accounts, password resets, and character linking. +- Expanded the focused unit-test suite to cover admin helper behavior. +- Added file-backed account creation, authentication, and admin workflows. +- Added account-linked character snapshot storage and default legacy path helpers for `players`, `plrobjs`, and `exploits`. +- Expanded the focused unit-test suite to cover migration snapshot behavior and default legacy path migration. +- Addressed review feedback around safer account creation, least-privilege file permissions, generic auth failures, and optional legacy object/exploit files. +- Added the immortal `account` command for account lookup, block/unblock, password reset, character linking, and character migration. +- Added the player `linkaccount ` command with a masked password prompt so account credentials do not travel through normal command logging. +- Added login-state support for `account ` at the name prompt, account password authentication, and linked-character selection before entering the normal game menu. +- Added account-selection helper coverage so linked-character selection rules and prompt formatting are unit tested. +- Added identifier validation before account-file access, cross-account character ownership checks, and migration-first linking so failed migrations do not leave stale account links behind. +- Rebuilt the test binary and passed the focused `AccountManagement` unit-test suite with 33 tests. +- Captured the new target workflow in `FEATURES.md`: email-first login, create-account-on-miss, authenticated account menu, legacy-character password verification during linking, menu-driven password reset, and play/create-character entry points. +- Added account-layer email validation, email-based account lookup, email-based authentication, and account creation from an email address so the login flow can move off the transitional account-name prompt. +- Tightened email lookup to fail closed if duplicate account files claim the same normalized email address. +- Rebuilt the test binary and passed the focused `AccountManagement` unit-test suite with 39 tests. +- Switched the live login prompt from character-name-first to account-email-first. +- Added login states for create-account confirmation, account password creation/confirmation, account menu, legacy-character linking, account password reset, and account-backed new-character entry. +- Wired `nanny()` so an email can authenticate an existing account or create a new one directly from the login prompt. +- Added the authenticated account menu flow for listing linked characters, playing a linked character, linking an existing legacy character via legacy password verification, creating a new character, resetting the account password, and logging out. +- Kept automatic post-creation linking in place so a character created from the account menu is associated back to the authenticated account. +- Split account-password entry off from the legacy 10-character player-password buffer so account creation and resets can accept longer passwords without inheriting the old character limit. +- Added linked-character owner lookup plus account-menu preflight checks so account-created characters cannot reuse names already claimed in account storage. +- Added rollback behavior so if post-creation account linking fails, the newly created character is immediately marked deleted instead of being left behind as an orphaned legacy character. +- Removed the hidden `legacy ` login bypass so the public login surface now follows the email-first account workflow instead of offering a parallel legacy path. +- Added account email-verification metadata to the JSON account schema plus admin verify/unverify helpers and commands. +- Changed newly created email-first accounts to remain pending until an administrator verifies the email address. +- Gated account authentication so unverified accounts cannot enter the account menu, and added player-facing messaging that the account is awaiting verification. +- Expanded the focused account suite to cover verification metadata, pending-verification auth failures, admin verify/unverify flows, and the verified-account migration path. +- Replaced the admin-only verification stopgap with emailed verification codes delivered through the local `sendmail` interface. +- Added verification-code hashing, persistence, resend support, and a 15-minute expiry window. +- Updated `nanny()` so new and pending accounts transition into a verification-code prompt, support `RESEND` and `CANCEL`, and only enter the account menu after successful code confirmation. +- Marked verification-code entry as secret input so snoops do not see emailed codes. +- Added persistent verification-attempt tracking, invalidated emailed codes after too many bad tries, and added resend cooldown protection. +- Fixed the account-creation handoff so a newly created account that becomes verified immediately routes into the account menu instead of getting stuck at the code prompt. +- Hardened create-on-miss to fail closed if existing account records are unreadable, preventing email uniqueness bypass through corrupt storage. +- Expanded the focused account suite to 49 passing tests, then 50 with the unreadable-record regression coverage folded into the full test suite. +- Re-ran `make test` and confirmed the full C++ unit test suite passes locally at 224/224. +- Added helpers to ensure a linked character has an account snapshot and to refresh linked snapshots from current legacy files. +- Updated account-character selection so linked characters must be account-snapshot-ready before play and will self-heal a missing snapshot by migrating current legacy files when possible. +- Updated new account-created characters to link and migrate immediately instead of only linking. +- Hooked normal player saves to refresh linked account snapshots so account storage tracks ongoing character changes. +- Added helpers to restore legacy player/object/exploit files back out of account snapshots. +- Updated account-backed play so the selected linked character is restored from account storage before the legacy runtime loader runs. +- Expanded the focused account suite to 55 passing tests. +- Added direct player-text loading from account snapshots, so account-backed play no longer has to recreate the legacy player file before character load. +- Narrowed runtime restoration during account-backed play to the still-legacy support files. +- Expanded the focused account suite to 57 passing tests. +- Hardened linked-character owner lookup to fail closed when multiple accounts claim the same character. +- Updated snapshot readiness so a corrupt existing migration snapshot is rebuilt from current legacy files instead of blocking account-backed play. +- Added snapshot-identity validation before restoring runtime support files out of account storage. +- Hardened direct player-text loading so malformed snapshot data is rejected safely instead of walking past buffer boundaries. +- Added regression coverage for duplicate ownership, corrupt-snapshot rebuilds, snapshot-identity mismatches, and malformed player-text decoding. +- Changed account-backed play so it no longer restores the legacy exploit file during login; stale runtime exploit files are removed instead. +- Added exploit-history helpers that read from account snapshots when the legacy exploit file is missing and seed new runtime exploit files from snapshot history when gameplay appends new records. +- Preserved existing exploit history during linked-character snapshot refreshes when the runtime exploit file is intentionally absent, so ordinary saves no longer erase account-backed history. +- Hardened exploit-history loading so malformed runtime exploit files are removed and rebuilt from account snapshots instead of poisoning later reads/appends. +- Hardened exploit writes to fail closed on pre-existing temp paths using secure temp-file creation. +- Updated `print_exploits()` and runtime exploit writes to use the new fallback path, and fixed runtime account-snapshot refresh calls to use the live data-directory root. +- Added focused regression coverage for exploit-history snapshot fallback, snapshot-seeded append behavior, corrupt runtime exploit-file fallback, temp-file conflict failure, and preserving snapshot exploit history across refreshes. +- Rebuilt the server and passed the focused account suite at 61 tests, focused db-loader coverage at 5 tests, and the full unit suite at 240/240. +- Sent the exploit-history cutover slice through `Magus` and `Vincent`, addressed their findings, and got a clean final reviewer pass from both. +- Replaced login-time runtime support-file restoration with account-play preparation that clears stale legacy object and exploit files after validating snapshot identity. +- Added `load_object_save_bytes_for_character(...)` so linked characters can read object-save bytes from the runtime file when present or fall back to the linked account snapshot when it is absent. +- Updated `Crash_load()` so account-backed play can stage snapshot-backed object bytes into an anonymous temporary stream and continue using the legacy object/alias/follower parser unchanged. +- Followed up on reviewer findings by staging account-backed object bytes from the authenticated selection context instead of re-resolving ownership by character name at load time. +- Hardened the legacy object/alias/follower parser so truncated crashsave streams fail closed instead of reading partial data, and added cleanup for staged object bytes on denied/reconnect login paths. +- Fixed the synthetic empty staged object payload so its alias/follower layout now matches the real crash-save serialization order for no-object account-backed logins. +- Fixed the account-backed linked-character login order so `save_char()` refreshes the account snapshot before stale legacy object/exploit files are cleared, preventing the first post-login save from rewriting the object snapshot as empty. +- Added focused regression coverage for clearing runtime support files during account-backed play and for loading object-save bytes from account snapshots when the legacy `plrobjs/...` file is missing. +- Added a configurable `ROTS_SENDMAIL_COMMAND` override and replaced the live verification mail path with an explicit pipe/fork/exec wait flow after the proxy-backed smoke uncovered live delivery failures in the old `popen`/`pclose` approach. +- Added unit coverage for the configurable verification-mail command and a reusable `tools/account_smoke.py` harness plus `make smoke-account` / `smoke_account` targets. +- Tightened the configurable mailer override so it is parsed into argv and executed with `execvp(...)` instead of shell parsing, then updated the smoke harness to use a fixed helper script path and UUID-based account ids with cleanup by `normalized_email`. +- Folded the Python proxy-backed account smoke flow into the default `make test` command so the standard test entry point now covers both the C++ suite and the create/verify/login/reset smoke path. +- Rebuilt the test binary and passed the focused account suite, the full unit suite at 243/243, the full server build, and the broader proxy-backed smoke flow. +- Smoke tested the live account flow through the Rust proxy with temporary accounts: created an account, captured and entered the emailed verification code locally, logged in, listed linked characters, reset the password, logged out, verified the old password was rejected, and re-logged in with the new password. +- Investigated the deeper smoke failure where account-menu new-character creation produced an unlinked character on reconnect. +- Confirmed from preserved live-server logs that fresh character creation writes versioned player-save filenames like `name.level.race.idnum.logtime.flags`, while account migration was still looking only for the old unsuffixed `players//` path. +- Updated account migration to resolve the real versioned player-save filename when the old canonical path is absent, and to fail closed if multiple versioned player files match the same character name. +- Added regression coverage for migrating a fresh versioned player save and for rejecting ambiguous multiple versioned player-save matches. +- Updated the smoke harness cleanup to remove versioned player-save artifacts and added persistent game/proxy log capture to the smoke temp directory for easier live-flow debugging. +- Expanded the proxy-backed smoke harness to cover creating a new character from the account menu, reconnecting, selecting the linked character, and entering the world through the account-backed play path. +- Re-ran the full validation path and passed `make test` with `245/245` C++ tests plus the expanded Python smoke flow. +- Updated account discovery and persistence helpers so account records now live under `lib/accounts///account.json` while account lookup by internal account name still works through directory scans. +- Flattened linked-character asset references so account metadata now defaults to `.character.json`, `.objects.json`, and `.exploits.json` in the same account directory. +- Moved the transitional migration-file path into that same account directory as `.migration.json` while the broader account-native JSON cutover is still in progress. +- Added focused regression coverage for the new account path layout and default character-link filenames, then re-ran the focused `AccountManagement` and `DbLoader` suites successfully. +- Hardened account-file rewrites so a different account cannot overwrite an occupied email-rooted path, legacy flat account files are retired after successful rewrites, and duplicate old/new account records now prefer the rooted `account.json` record instead of failing closed when both describe the same account. +- Tightened account-directory scans so empty or non-account bucket subdirectories are ignored unless they actually contain a broken `account.json`, which fixed the live smoke regression caused by leftover empty account directories. +- Added focused regression coverage for malicious stored character names, legacy flat-account rewrite migration, and overwrite-prevention behavior, then re-ran `make test` successfully with the updated suite count and live smoke flow. +- Added a first shared `character_json` module plus focused tests for profession points/coeffs, symbolic player/preference/affected flag arrays, structured affect serialization, `mystic` profession naming, and `char_file_u` conversion helpers. +- Expanded the shared `character_json` module to cover persisted identity/physical fields, temporary and rolled abilities, point data, conditions, timers, talks, skills, hide flags, and stricter array-capacity validation when applying JSON back to `char_file_u`. +- Rebuilt the test binary and passed the focused `CharacterJson` and `JsonUtils` suites plus the full `make test` path with the expanded `character_json` coverage in place. +- Addressed reviewer follow-up on malformed `character.json` input by rejecting out-of-range narrowed numeric fields, truncated fixed-width arrays, and overlong fixed-buffer strings before applying JSON back into `char_file_u`. +- Added focused regression tests for narrowed numeric overflow, truncated fixed-width arrays, and overlong `character_name` rejection in the shared `character_json` suite. +- Tightened the shared parser boundary further so `parse_integer()` now rejects out-of-range integers before narrowing, fixed-width arrays are capped while parsing instead of only after materialization, embedded NUL bytes are rejected for fixed-buffer strings, and oversized `affects` arrays fail immediately. +- Added focused regression tests for out-of-range parsed integers, embedded-NUL fixed-buffer strings, and oversized `affects` arrays in the shared JSON/character suites. +- Updated the `character_json` schema surface so `skills` and `talks` now serialize as named key/value objects in JSON instead of positional arrays, while still mapping back into the fixed runtime arrays internally. +- Added account-layer helpers to write, read, check, and remove account-native `character.json` files using the shared `character_json` module. +- Added `ensure_player_index_entry(...)` and `update_player_index_entry_from_store(...)` so account-backed selection can hydrate runtime player-index state after loading a character directly from JSON. +- Updated account-backed character selection to prefer direct `character.json` load and only fall back to migration when that authoritative file is genuinely absent. +- Updated account-backed login prep to clear stale runtime object/exploit files even for account-born characters that never had a migration snapshot. +- Updated new-character introduction so account-created characters write an account-native `character.json` as part of initial account linking instead of relying solely on the legacy player file birth path. +- Added focused regression coverage for account-native character-file write/read/remove behavior and re-ran the full test path successfully. +- Updated boot-time `build_player_index()` to scan account-owned `account.json` records directly, add account-native characters to `player_table`, and fail closed on duplicate names across legacy/account-native storage. +- Updated `load_player(...)` so name-based loads can resolve an account-native `character.json` path from `player_table` without falling back to legacy player text. +- Added `DbLoader.BuildPlayerIndexIncludesLegacyAndAccountNativeCharacters` to prove unified indexing and name-based account-native loads work together. +- Investigated the smoke regression after the indexing change, confirmed the first failure was a stale `bin/ageland` build rather than a loader bug, rebuilt the server binary, and re-ran the smoke flow successfully. +- Added a shared `objects_json` module that round-trips the current crashsave payload into JSON with explicit sections for rent data, top-level objects, board points, aliases, and followers. +- Added focused `ObjectsJson` unit coverage for binary round-trip, JSON round-trip, truncated-binary rejection, and alias-keyword validation. +- Added account-layer helpers to write/read/check per-character `objects.json` files using that shared module. +- Updated account-backed object-save lookup to prefer account-owned `objects.json` for linked characters before falling back to the runtime legacy file or migration snapshot. +- Updated crash/rent/idle object saves to refresh account-owned `objects.json` after writing the legacy crashsave stream. +- Added regression coverage proving the loader can read object-save bytes back from account-owned `objects.json`. +- Addressed security review feedback by rejecting stored `object_path` values that do not match the expected per-character basename and by validating narrowed `objects.json` fields before converting them back into crashsave storage types. +- Followed up on Maxwell's compatibility note by allowing safe legacy relative `object_path` values that still resolve to the expected object basename, then normalizing them back to the canonical basename on account-file/object-file rewrites. +- Added `DbLoader.CrashLoadConsumesStagedAccountBackedObjectBytesAndLoadsAliasTail` so the real staged `Crash_load()` path now has focused regression coverage instead of only helper-level object-load tests. +- Re-ran the full validation path successfully after the object-path compatibility follow-up and the new staged `Crash_load()` coverage. +- Added shared account-layer helpers to write a default empty account-owned object file and to remove an account-owned object file during rollback cleanup. +- Updated new-character introduction so account-created characters now create `objects.json` during the initial account-link transaction and clean it up if account linking rolls back. +- Updated legacy migration so successful migrations now seed canonical account-owned `objects.json` immediately when legacy object data is valid, or write an empty default `objects.json` when no legacy object file exists yet. +- Added focused regression coverage for default object-file writes plus migration-time account-owned object-file creation from valid and missing legacy object payloads. +- Re-ran the full validation path successfully after the object-birth and migration-authority follow-up. +- Added a scoped object-prototype test helper in `db_loader_tests.cpp` so the staged account-backed `Crash_load()` path can instantiate and equip real wearable objects during unit coverage. +- Added focused `DbLoader` coverage proving staged account-backed object bytes can enter the game with worn equipment and carried inventory intact. +- Added focused `DbLoader` migration-parity coverage proving decoded legacy object bytes and the resulting decoded account-owned `objects.json` payload stay structurally equivalent after migration. +- Followed up on reviewer findings by failing migration closed when legacy object bytes are malformed instead of silently downgrading them to an empty `objects.json`. +- Tightened account-owned character-file resolution so persisted `character_path` metadata must match the canonical per-character filename and gets normalized back to that basename on save. +- Updated the successful migration fixtures to use valid crashsave object bytes so the new malformed-payload guard is exercised only where intended. +- Hardened the staged account-backed object cache so it keys by normalized character identity instead of raw `char_data*`, and stale staged bytes are now cleared on socket close and `free_char()`. +- Reordered account-owned object-path normalization so `account.json` only rewrites a safe legacy `object_path` to the canonical basename after the new canonical object file write succeeds. +- Added regression coverage for the stale staged-object isolation path and for preserving a legacy-safe `object_path` when the canonical object-file write fails. +- Hardened `tools/account_smoke.py` with a bounded retry loop so the proxy-backed smoke path stays reliable under `make test` even when the first connect/prompt handshake flakes. +- Added a shared `exploits_json` module that round-trips legacy exploit-history records into JSON and back, with focused coverage for malformed binary length and fixed-width string validation. +- Added account-layer helpers to write/read/check/remove per-character `exploits.json` files using that shared module. +- Updated new-character introduction so account-created characters now create `exploits.json` during the initial account-link transaction and clean it up if account linking rolls back. +- Updated legacy migration so successful migrations now seed canonical account-owned `exploits.json` immediately when legacy exploit data is valid, write an empty default `exploits.json` when the legacy exploit file is missing, and fail closed when legacy exploit bytes are malformed. +- Updated exploit-history runtime flows so linked characters now prefer account-owned `exploits.json`, remove stale legacy runtime exploit files after successful account-native reads/writes, and fail closed when the authoritative account-owned exploit file is corrupt instead of silently falling back to stale runtime data. +- Added focused regression coverage for account-owned exploit-file read/write/default-empty behavior, migration-time exploit-file creation from valid and missing legacy payloads, malformed legacy exploit rejection, direct account-native exploit read/write preference, and corrupt-authoritative-JSON fail-closed behavior. +- Re-ran the full validation path successfully after the `exploits.json` authority follow-up. +- Had `Bazarat` adversarially review the `AccountManagement` suite and added targeted edge-case coverage for stale verification-code rejection after resend, verified-account re-verification safety, conflicting legacy-flat plus rooted duplicate email records, duplicate-email account creation rejection when a legacy-flat record already exists, and absolute-path rejection for stored object-path metadata. +- Re-ran focused `AccountManagement` coverage at 91 passing tests and the full `make test` path successfully after the new edge-case regressions. +- Had `Bazarat` adversarially review the `objects_json_tests` suite and added targeted edge-case coverage for empty object-save round-trip, alias/follower/object ordering fidelity, truncation inside alias and follower sections, wrong-typed nested JSON sections, and missing required top-level JSON sections. +- Tightened `deserialize_objects_from_json(...)` so account-owned `objects.json` now fails closed when required top-level sections like `rent`, `objects`, `board_points`, `aliases`, or `followers` are missing instead of silently defaulting them. +- Followed up on Maxwell's review by tightening nested object/alias/follower record parsing so missing required fields now fail closed instead of silently defaulting missing scalars or strings. +- Followed up on Rawls' review by tightening nested object-affect parsing so missing `location` or `modifier` fields now fail closed instead of defaulting silently to zero. +- Added focused `ObjectsJson` regressions for missing alias `command`, missing object `item_number`, and followers missing required numeric/object fields. +- Added focused `ObjectsJson` regressions for affect entries missing `location` or `modifier`. +- Re-ran focused `ObjectsJson` coverage at 12 passing tests, re-ran the full C++ test path at `320/320`, and re-ran the Python smoke harness successfully after the nested-record hardening follow-up. +- Had `Bazarat` adversarially review the `character_json_tests` suite and added targeted edge-case coverage for missing required top-level sections, legacy `cleric` schema drift, unknown affected/hide flags, duplicate named `talks`, and missing required structured-affect fields. +- Tightened `character_json` deserialization so required top-level sections must be present, `cleric` is rejected in favor of `mystic`, `flags`/`professions` objects must be complete, and structured affects now fail closed when required fields are missing. +- Re-ran focused `CharacterJson` coverage at 19 passing tests, re-ran the full C++ test path at `325/325`, and re-ran the Python smoke harness successfully after the `character_json` hardening follow-up. +- Tightened the nested `character_json` parsers so `identity`, `progression`, `abilities`, `points`, `conditions`, `timers`, `perception`, and `state` now fail closed when required scalar fields are missing instead of silently defaulting them to zero. +- Added focused `CharacterJson` regressions for missing required `identity`, `points`, and `state` fields during deserialization. +- Followed up on Maxwell's low test-coverage finding by adding the missing required-field regressions for `progression`, `abilities`, `conditions`, `timers`, and `perception`. +- Re-ran focused `CharacterJson` coverage at 27 passing tests, re-ran the full C++ test path at `333/333`, and re-ran the Python smoke harness successfully after the nested-field hardening follow-up. +- Updated legacy-character migration so successful migration now retires the old legacy player/object/exploit files immediately after the account-owned object/exploit files and migration record are written. +- Added focused `AccountManagement` regressions proving successful migration removes the legacy files, that object-retirement failures restore earlier retired files before returning failure, and that exploit-retirement failures restore both earlier retired legacy files before returning failure. +- Updated the affected `DbLoader` regressions so their fixtures now expect migration to have already retired the legacy object/exploit files before fallback reads occur. +- Re-ran focused `AccountManagement` coverage at 94 passing tests, re-ran the targeted `DbLoader` regressions for account-backed object/exploit fallback, and re-ran the full `make test` path successfully at `336/336` C++ tests plus the Python smoke flow. +- Removed the remaining `migration.object_file` / `migration.exploits_file` runtime fallback in `db.cpp`, so linked-character object/exploit loads now use account-native JSON first, then any still-present runtime legacy files, and otherwise return empty results instead of decoding the transitional migration snapshot. +- Removed the redundant object-snapshot decode in `interpre.cpp`, since account-backed selection already reloads object-save bytes through the authoritative loader path. +- Renamed the affected `DbLoader` cases to reflect account-native JSON authority and added focused regressions proving linked characters with neither account-native nor runtime object/exploit files now load as empty instead of reaching back into the migration snapshot. +- Had `Bazarat` adversarially review the loader cleanup and added the extra authority-order regressions he called out: linked-character runtime legacy fallback when account JSON is absent, preference for account-native object/exploit JSON over conflicting runtime legacy data, and fail-closed malformed-object-JSON behavior that preserves the stale runtime file. +- Replaced the remaining string-based `"Failed to open file"` fallback checks with structured account-owned file inspection in `account_management`, so unreadable authoritative account-native character/object/exploit files fail closed instead of being treated like missing files. +- Added a focused `DbLoader` regression proving unreadable authoritative account-native object/exploit JSON does not fall back to stale runtime legacy data. +- Re-ran focused `DbLoader` coverage at 22 passing tests and re-ran the full `make test` path successfully at `342/342` C++ tests plus the Python smoke flow. +- Updated migration/backfill helpers so legacy-character migration now writes authoritative account-owned `character.json` immediately from decoded legacy player data, and existing migration snapshots backfill missing `character.json` files the next time migration is ensured. +- Updated account-backed login in `interpre.cpp` so it re-reads authoritative `character.json` after migration/backfill instead of decoding `migration.player_file` directly at runtime. +- Reworked migration-heavy test fixtures in `AccountManagement` and `DbLoader` to generate real legacy player saves via `save_player(...)` instead of placeholder text, which exercises the actual legacy loader path during `character.json` backfill. +- Fixed the new fixture helper so it does not accidentally mark generated characters as NPCs, does not hand stack memory to `free_char()`, and leaves `player_table` in a clean state for `ensure_player_index_entry(...)`. +- Added regression expectations proving migration-created/backfilled `character.json` is readable after ensure/rebuild paths, and updated migration-retirement expectations so tests no longer expect retired legacy player files to remain on disk after successful migration. +- Re-ran focused migration regressions and the full C++ suite successfully at `342/342`. +- Re-ran the direct proxy-backed smoke harness successfully after the migration-backfill changes. +- Fixed Maxwell's review finding in `interpre.cpp` by loading account-owned object-save bytes on the direct `character.json` fast path instead of only after migration fallback. +- Added a new `DbLoader` regression proving an existing account-native `character.json` plus `objects.json` path can still equip staged objects through `Crash_load()` without forcing migration first. +- Re-ran focused `DbLoader` coverage successfully after the fast-path object fix. +- Re-ran the direct proxy-backed smoke harness successfully after the fast-path object fix. +- Fixed Maxwell's follow-up save-path finding in `save_char(...)` so already account-native linked characters no longer attempt `refresh_linked_character_snapshot(...)` after ordinary saves once their legacy player/object/exploit files have been retired. +- Added a focused `DbLoader` regression proving ordinary saves for an already account-native linked character do not emit the old `failed to refresh account snapshot` log noise after migration retirement. +- Re-ran focused `DbLoader` coverage successfully after the save-path fix at `24` passing tests. +- Re-ran `make test`; the C++ suite passed cleanly at `344/344`, and the Python smoke step still showed the known intermittent prompt-marker timeout in the combined target. +- Followed up on Bazarat's migration-test review by changing legacy player-path resolution so a valid versioned player save now wins over a stale flat file. +- Added focused `AccountManagement` regressions proving migration prefers the versioned player save, hydrates account-native `character.json` from that winning save, and rejects mismatched restore requests without overwriting stale runtime legacy files. +- Strengthened the corrupt-snapshot rebuild test so it now explicitly re-reads the backfilled account-native `character.json` and checks representative stored fields after rebuild. +- Followed up on Rawls' migration-integrity review by retiring the ignored stale flat player file during successful versioned-player migration and by adding a `DbLoader` regression proving boot-time `player_table` indexing stays consistent after that migration path. +- Followed up on Rawls' failure-path review by making stale-flat cleanup retirement-only instead of a second migration input, rolling back account-native migration outputs if stale-flat retirement fails, and teaching boot-time `build_directory()` to skip flat artifacts when a valid versioned sibling exists before any login-driven migration has run. +- Added focused `AccountManagement` coverage proving a valid versioned migration succeeds even when the stale flat file is unreadable and proving stale-flat retirement failure cleans up account-native outputs instead of leaving half-migrated duplicates behind. +- Added focused `DbLoader` coverage proving boot-time `player_table` indexing prefers the versioned legacy save over a flat artifact even before migration has run. +- Re-ran focused `AccountManagement` coverage successfully at `98` tests and focused `DbLoader` coverage successfully at `26` tests after fixing the stale-flat and pre-migration boot-index regressions. +- Re-ran the full `make test` path successfully at `350/350` C++ tests, and re-ran the Python smoke flow separately via `make smoke-account`. +- Workflow update: `make test` is back to C++ unit tests only; run the proxy-backed smoke harness manually from here on out via `make smoke-account`. +- Validation rule update: for account/login/authentication work, `make smoke-account` is now a required separate validation step rather than something implied by `make test`. +- Added a new GitHub Actions workflow at `.github/workflows/ci.yml` that runs on pushes to `master` and pull requests targeting `master`, installs the Ubuntu build dependencies, builds the game, runs `make test`, and then runs `make smoke-account`. +- Updated `README.md` to document the manual smoke-test workflow plus the GitHub Actions / branch-protection requirement for making CI a mandatory merge gate. +- Removed the remaining runtime player-data dependency on the transitional migration snapshot in the account-backed play path: once `character.json` has been read, runtime support-file cleanup now clears stale legacy object/exploit files directly by character name instead of re-reading the snapshot just to validate and clear them. +- Tightened `ensure_character_migration(...)` so an existing migration snapshot no longer backfills a missing authoritative `character.json`; it now only succeeds from the snapshot if `character.json` already exists, otherwise it falls back to real legacy migration and fails closed if only the snapshot remains. +- Tightened `ensure_character_migration(...)` again so a corrupt transitional snapshot no longer blocks a valid authoritative `character.json`; if the account-owned character file already exists, migration now succeeds without consulting the corrupt snapshot at all. +- Added an explicit fail-closed error message for the snapshot-only path so live account-play/login failures now explain that the authoritative `character.json` is missing and the transitional snapshot alone is insufficient. +- Updated `save_char(...)` so linked characters repair a missing account-native `character.json` directly from the current in-memory store state instead of writing a legacy player file and refreshing the migration snapshot. +- Added focused regressions proving snapshot-only state no longer repairs missing `character.json`, that a corrupt transitional snapshot does not block an existing authoritative `character.json`, that linked saves recreate `character.json` directly without reviving the snapshot-refresh path or legacy player-file writes, and that unreadable account records do not let account-native saves fall back to legacy player files. +- Re-ran focused `AccountManagement` coverage at `100` passing tests, focused `DbLoader` coverage at `28` passing tests, and the full `make test` path at `354/354` after the player-data authority cleanup. +- Manual `make smoke-account` is currently still blocked by the known telnet prompt-detection flake in `tools/account_smoke.py`; two reruns in this slice timed out during the handshake and kept artifacts under `/tmp/rots-account-smoke-*`. +- Investigated the new report that the first login menu/prompt only appears after typing input and traced the most suspicious change point to `pnew_descriptor(...)` in `src/comm.cpp`, where the account greeting/email prompt was queued but not flushed immediately on a newly accepted descriptor. +- Updated `pnew_descriptor(...)` to flush queued greeting/login output immediately after `SEND_TO_Q(GREETINGS, ...)` and the initial `Account email:` prompt are queued, so the first account/login screen is written as part of the accept path instead of waiting for a later loop pass. +- Re-ran `make test` successfully at `354/354` after the connection-path flush change. +- Re-ran `make smoke-account`, but this pass still did not produce a trustworthy login-flow verdict because the harness timed out waiting for `127.0.0.1:4001` to accept connections and kept artifacts under `/tmp/rots-account-smoke-*`. + +## Next Step +- No remaining implementation step for this slice. +- Optional cleanup remains for preserved debug artifacts under `/tmp/rots-account-smoke-*` and the kept debug account under `lib/accounts/P-T/smkb393ca001a76@example.com/`. + +## Last Validation +- `python3 -m py_compile tools/account_smoke.py tools/account_smoke_tests.py` +- `python3 tools/account_smoke_tests.py` +- `git diff --check` +- `make smoke-account` repeated three times +- Result: Python smoke-harness coverage passes at `51/51`, syntax compilation is clean, the diff has no whitespace errors, and the post-fix proxy-backed account smoke passed `3/3` full runs. Cargo still emits the existing workspace resolver warning during proxy builds. + +## Reviewer Status +- `Magus`: final blocker-only review is clear after retry narrowing, WIP refresh, Python checks, and `3/3` full smoke validation. +- `Vincent`: final blocker-only review is clear after exact cleanup scoping, account lookup hardening, child-process environment allowlisting, retry scoping, and `3/3` full smoke validation. +- `Bazarat`: final test-design review is clear after the live migrated-character assertion, pre-delete asset existence check, exploit field assertions, artifact-collision coverage, and `51/51` Python smoke-harness tests. +- `Magus`: found broad retry behavior could hide late e2e regressions and stale WIP footer content. Follow-up narrowed retries to an explicit initial-account-prompt `RetryableSmokeError` and refreshed the WIP status sections. +- `Vincent`: found cleanup still used name/glob deletion and child processes inherited the ambient environment. Follow-up changed smoke cleanup to exact fixture/account paths and added an allowlisted child-process environment helper. +- `Bazarat`: found the migrated play path did not prove the live loader used migrated sentinel data, delete checks did not prove assets existed before delete, and collision coverage missed several artifact classes. Follow-up added live `info` assertions, pre-delete asset existence checks, direct exploit-field assertions, and additional collision tests. +- `Bazarat`: clear on the current migration-sanitization direction and regression coverage; no test-design blockers on the new “do not persist raw legacy player payloads” contract. +- `Magus`: clear on the current migration-sanitization slice; no findings. +- `Vincent`: clear on the current migration-sanitization slice after the backward-compatible read-time scrub for older persisted player payloads. +- `Bazarat`: clear on the follow-up migration-policy direction; no test-design blockers on removing routine `.migration.json` writes while keeping in-memory migration data for active rollback helpers. +- `Magus`: clear on the latest migration-policy cleanup; no findings. +- `Vincent`: clear on the latest migration-policy cleanup; no findings. +- `Maxwell`: clear on the shared `json_utils` extraction after follow-up hardening for raw control characters and `\u00XX` serializer/parser round-trip coverage. +- `Rawls`: clear on the shared `json_utils` extraction; no new trust-boundary or parser-safety findings after the control-character follow-up. +- `Maxwell`: clear on the current `character_json` expansion after follow-up fixes for numeric range validation, exact-size array validation, and fixed-width string-length guards. +- `Rawls`: clear on the current `character_json` expansion after the final parser-boundary fixes for out-of-range integers, fixed-width array caps, embedded-NUL rejection, and `MAX_AFFECT` enforcement during parse. +- `Rawls`: clear on the latest `objects_json` follow-up; no new trust-boundary or data-exposure findings after the staged-object cache lifetime fix, the post-write object-path normalization change, and the new regressions. +- `Maxwell`: no blocking findings remain on the latest `objects_json` follow-up. He left three low notes to keep in mind for the next testing pass: the scoped object-prototype fixture in `db_loader_tests.cpp` is still brittle, the migration-parity test is still structural parity rather than true live-crashsave compatibility, and the smoke retry in `tools/account_smoke.py` is broad enough that we should make first-attempt failures more visible if it stays. +- `Maxwell` and `Rawls`: the newest follow-up covered equipped-login object coverage, migration parity, malformed-payload fail-closed behavior, canonical `character_path` enforcement, staged-object lifetime hardening, post-write `object_path` normalization, and the stabilized smoke harness. Both review gates are now satisfied for this slice. +- `Bazarat`: reviewed the first `exploits.json` cutover coverage and pushed on fail-closed authority boundaries plus stale-runtime precedence. The new corrupt-authoritative-JSON regression was added in response. +- `Bazarat`: also reviewed the `AccountManagement` suite and directly drove the new resend-invalidation, verified-account lifecycle, duplicate-record authority, legacy-flat duplicate-email, and stricter absolute-path guard regressions. +- `Bazarat`: also reviewed the `objects_json_tests` suite and directly drove the new empty-payload, required-sections, mid-stream truncation, nested-shape, and ordering-fidelity regressions. +- `Bazarat`: also reviewed the `character_json_tests` suite and directly drove the new required-sections, legacy-`cleric` rejection, unknown affected/hide flag, duplicate named-value, and missing structured-affect-field regressions. +- `Maxwell`: the first `character_json` nested-field hardening pass was internally consistent and had one low finding to add explicit regressions for the remaining required-field parser branches. That follow-up is now implemented and awaiting re-check. +- `Rawls`: clear on the latest `character_json` nested-field hardening follow-up; no new trust-boundary, parser-safety, or data-integrity findings. +- `Maxwell`: clear on the latest legacy-file-retirement migration follow-up after the exploit-rollback regression and rollback-helper readability cleanup. +- `Rawls`: clear on the latest legacy-file-retirement migration follow-up after the partial-migration rollback restoration fix. +- `Maxwell`: latest stale-flat-player retirement and boot-index follow-up review pending. +- `Rawls`: latest stale-flat-player retirement and boot-index follow-up review pending. +- `Bazarat`: reviewed the loader-cleanup tests and directly pushed the new runtime-legacy fallback, authoritative-preference, and malformed-object-JSON fail-closed regressions. +- `Bazarat`: also pushed the new unreadable-authoritative-object/exploit regression while Maxwell's follow-up was being addressed. +- `Maxwell`: exploit-cutover review pending. +- `Rawls`: exploit-cutover review pending. diff --git a/docker-compose.yml b/docker-compose.yml index 54597389..37539480 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,11 @@ services: platform: linux/386 image: rots-dev:bullseye-i386 container_name: rots + # Run as the host user (set by scripts/rots-docker.sh) instead of the container + # default (root), so build artifacts and any runtime-created directories + # (bin/ageland, *.o, lib/accounts/*, lib/account_characters/*, etc.) land on the + # bind-mounted host filesystem owned by you, not root. + user: "${ROTS_UID:-1000}:${ROTS_GID:-1000}" working_dir: /rots volumes: - .:/rots diff --git a/docs/README.md b/docs/README.md index 6aa3b6fd..58198a0a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,6 +38,7 @@ Legend: ✅ done · 🟡 partial · ⬜ not started | [Ranger skills](systems/ranger-skills.md) | ✅ skill catalog + DEX-vs-ranger-level for dodge/skills | `ranger.cpp`, `utility.cpp`, `consts.cpp` | | [Magic system — mage spells](systems/magic-system.md) | ✅ damage, saves, resistance, penetration, scaling, mana regen | `mage.cpp`, `spell_pa.cpp`, `consts.cpp` | | [Cleric / Mystic system](systems/cleric-mystic-system.md) | ✅ powers, saves, mental combat, spirit, scaling | `mystic.cpp`, `clerics.cpp`, `spell_pa.cpp` | +| [Idle, the void & followers](systems/idle-void-and-followers.md) | ✅ observed live; historic behaviour, not to be changed | `limits.cpp::check_idling`, `handler.cpp`, `objsave.cpp` | ⬜ Races · XP/leveling · Movement/zones · Objects/equipment · Mob AI/specprocs · Shops/economy · diff --git a/docs/Running the Game.md b/docs/Running the Game.md new file mode 100644 index 00000000..edc3d009 --- /dev/null +++ b/docs/Running the Game.md @@ -0,0 +1,82 @@ +# Running the Game + +The live, building, and coding ports on `` run as `systemd` +services. Use `systemctl` to start, stop, or restart each port: + +- `rotslive.service` +- `rotsbuilding.service` +- `rotscoding.service` + +## Port Overview + +| Purpose | Port | Working Directory | Autorun Script | Restart Command | +|----------|------|-----------------------------|----------------|-----------------------------------------| +| Live | 3791 | `/rots/live-default3791` | `autorun3791` | `systemctl restart rotslive` | +| Building | 4802 | `/rots/dev-building4802` | `autorun4802` | `systemctl restart rotsbuilding` | +| Coding | 4810 | `/rots/dev-coding4810` | `autorun4810` | `systemctl restart rotscoding` | + +Each directory follows the structure created by `make setup`: + +```text +-- /rots/ + - backups + - bin + - core + - levgen + - lib + - log + - src + - www +``` + +### Deploying Code to Any Port + +1. Upload the source via SFTP to the appropriate `src` directory. +2. In `src`, move the previous code base into the `backup/` folder after running `make clean` to avoid stale files. +3. Copy the new code into `src` and compile with `make all -j2`. +4. Restart the port so the changes take effect: + - In-game: `shutdown reboot` + - Or via shell: `systemctl restart ` + +> [!NOTE] The coding port does not maintain backups; treat it as a scratch space for compilation and testing. + +## Restoring Characters (Live Port) + +1. Change to the live working directory: `cd /rots/live-default3791`. +2. Enter the daily backups: `cd backups/daily`. Within this directory you will find: + +```text +-- backups/daily + - exploits/ + - players/ + - plrobjs/ +``` + +> [!TIP] Daily backups are retained for 30 days. + +1. Choose the correct archive: + - `exploits/` for exploit logs + - `players/` for player files + - `plrobjs/` for player objects +2. Copy the desired archive to a temporary working directory in your home folder: + +```bash +cp -v /rots/live-default3791/backups/daily/players/players-2026-02-01.tar.gz ~/temp/ +``` + +1. Extract the archive inside the temp directory: + +```bash +cd ~/temp +tar -xvzf players-2026-02-01.tar.gz ./ +``` + +> [!INFO] Files extract relative to the original tree (e.g., `./rots/live-default3791/lib/players`). + +1. Review or modify the restored files as needed, then copy them back into place: + +```bash +sudo cp -v rots/live-default3791/lib/players/A-E/aahz.32 /rots/live-default3791/lib/players/A-E/ +``` + +> [!IMPORTANT] Adjust the destination path to match the file type you are restoring. diff --git a/docs/color-command-audit.md b/docs/color-command-audit.md new file mode 100644 index 00000000..9ebc8a23 --- /dev/null +++ b/docs/color-command-audit.md @@ -0,0 +1,117 @@ +# Colour command & help audit + +**Date:** 2026-09-03 +**Branch:** `docs/color-help-mob-slot` +**Trigger:** documenting the `mob` colour slot added by PR #290 (`e6bc655`). + +Two defects surfaced while verifying the help text against a booted server. One is fixed +here; one is open and needs a design call. + +--- + +## 1. FIXED — `help color` answered the wrong page, and no page listed `mob` + +**Symptom.** PR #290 added a `mob` colour slot, but the help text still listed only the +original fifteen, so the new slot was undiscoverable in game. Worse, `help color` did not +even reach the page with the slot list on it. + +**Cause.** Two entries in `lib/text/help_tbl` both claimed the `COLOR` keyword: + +| entry | keywords | content | +|---|---|---| +| current | `COLOR COLOUR COLORS COLOURS` | full reference: slots, ANSI, RGB/hex, fg/bg | +| legacy | `ANSI COLOR COLOUR` | predates RGB/hex; lists **no** slots | + +`build_help_index()` (`src/modify.cpp:723`) creates one index entry **per keyword** on an +entry's first line, bubble-sorts them, and `do_help` (`src/act_info.cpp:2090`) binary +searches that list with a prefix compare. With a keyword duplicated across two entries, +which one answers is an artifact of where the search happens to land — it is not +first-match or last-match, and it varies per spelling: + +``` +help color -> legacy ANSI page (no slot list at all) +help colour -> current page +help colors -> current page +help colours -> current page +``` + +**Fix.** Added `mob` to the slot list plus a line explaining how `character` / `enemy` / +`mob` now divide up creature names, and narrowed the legacy entry's keyword line to `ANSI` +so all four colour spellings reach the current page. The legacy page keeps its content and +is still reachable as `help ansi`, now with a cross-reference to `HELP COLOUR`. + +**Verified** against a booted server: all four spellings show the slot list including +`mob`, `help ansi` still resolves, and the documented slots match the live `color` output +exactly (16 names). + +**Note for future slot additions.** `lib/text/help_tbl` **is tracked in git**, unlike most +of `lib/` — help edits are committable in this repo. It uses plain `\n`, not the `\n\r` of +the world files. The slot list there is hand-maintained and will drift from +`color_fields[]` (`src/color.cpp:227`) unless updated deliberately. + +--- + +## 2. OPEN — `color` is swallowed by the mail board in 45 rooms + +**Symptom.** In any room containing a board, typing `color` prints + +``` +Send the letter to whom? +``` + +and never reaches `do_color`. `colour` works normally in the same room, so the two +spellings of the same command behave differently depending on where the player stands. + +**Cause.** `src/interpre.h:328` defines `CMD_SEND 171`, but slot 171 in `command[]` +(`src/interpre.cpp:311`) is **`color`**. There is no `send` command in the table at all. +`gen_board` lets `CMD_SEND` through its command filter (`src/boards.cpp:221`) and then +rewrites it to `CMD_WRITE` against `mail_board` (`src/boards.cpp:284`), so the colour +command is dispatched into `mail_info_type::write_message` with an empty argument. + +Command numbers are 1-based: `old_search_block` (`src/interpre.cpp:601`) returns `guess` +*after* incrementing, so a word at 0-based array position `i` yields command number `i+1`. + +**Confirmed live** (2026-09-03, release-frodo): Creation Hall 1101 (has a board) swallows +`color` but honours `colour`; the Arena 1120 (no board) honours both. + +**Scope.** 24 object vnums get `gen_board` in `src/spec_ass.cpp`; zone `O` commands place +them in **45 distinct rooms**: + +``` +1101 1102 1103 1105 1106 1107 1109 1111 1112 1113 1114 1115 1116 1117 1119 +1125 1130 1131 1132 1133 1134 1197 1506 1551 3022 3039 6012 6015 6075 +10105 10129 10299 13530 13738 13759 13798 14485 14488 15898 16810 16856 +27533 27564 27576 32584 +``` + +**Not fixed here** — a fix has to decide what `CMD_SEND` should mean now that no `send` +command exists. Either retire the `CMD_SEND` branch in `gen_board` (and drop the constant), +or point it at a real command. Do not simply renumber it without re-checking the constant +against `command[]`. + +--- + +## 3. Checked and clear — the rest of the `CMD_*` block + +Because #2 looked like command-table drift, all 74 `CMD_*` constants in `src/interpre.h` +were audited against `command[]` and against the `COMMANDO()` registrations. + +**Result: `CMD_SEND` is the only genuine mismatch.** Every other constant lands on its own +word, and every `COMMANDO()` registration lands on the word it implements. The 59 +word/handler name differences are all legitimate aliases (`north`→`do_move`, +`kill`→`do_hit`, `cls`→`do_gen_ps`, `reply`→`do_tell`, and so on). + +> **Trap for anyone repeating this audit by script.** `src/interpre.cpp:531` reads +> `"trap", /* "trap", */`. A naive regex over string literals counts the commented-out +> copy as a real array entry and reports everything past index 220 as shifted by one — a +> false alarm that looks exactly like a systematic off-by-one. Strip comments first, and +> anchor on the `/* 221 */` marker next to `"account"`. + +Three constants point at deliberately empty word slots, so no player input can ever produce +them: + +| constant | slot | assessment | +|---|---|---| +| `CMD_SOCIAL` 22 | `""` | **By design.** `interpre.cpp:1358` assigns it internally when a social matches; an unreachable word slot is what makes it safe as a sentinel. | +| `CMD_RECITE` 112 | `""` | **Harmless.** Sole use is a commented-out line, `shop.cpp:578`. | +| `CMD_BLOCK` 133 | `""` | **Suspect, unverified.** `spec_pro.cpp` compares `cmd == CMD_BLOCK` in 7 places (2385, 2432, 2478, 2523, 2568, 2613, 2658). With no word mapped to 133, none of those can fire from player input — likely dead code from a retired `block` command, but not traced further. | diff --git a/docs/shape_mob.md b/docs/shape_mob.md new file mode 100644 index 00000000..8ffc1878 --- /dev/null +++ b/docs/shape_mob.md @@ -0,0 +1,402 @@ +# Shape Mob Command + +`shape mob` is the in-game tool for creating or modifying mobile prototypes. +Mob definitions live in `world/mob/.mob` (or the alternate `world/prx` +paths for special zones). This guide describes the shaping interface, every +editable field, and the conventions expected by Return of the Shadow so you can +retire the old `shap_tbl` excerpts. + +## Prerequisites + +- **Permissions**: you must have builder rights for the zone (`get_permission`). +- **Vnum assignment**: reserve a mob number via `register` before creating a new + entry (`shape mob new ` automatically targets the right file). +- **Reference mob**: for quick sanity checks, keep another mob with similar + behaviour handy and compare stats with `/50`. + +## Workflow overview + +| Action | Command | +|--------|---------| +| Load/create | `shape mob ` to edit an existing mob, or `shape mob new ` to start from a blank template. | +| Mode toggle | `/simple` switches between simple (fields 1–12 + spirit) and extended editing; `/extended` switches back. | +| Show menu | `/0` (or any non-numeric input) prints the numeric command list for the current mode. | +| Edit field | `/` runs the field editor described below. | +| List current values | `/50` dumps the whole mob definition (or `/49` to run the guided creation sequence). | +| Save & implement | `/save` writes to disk (after a backup); `/implement` pushes the temp mob into memory for live testing; `/done` performs save → implement → free. | +| Exit without saving | `/free`. | + +Editing uses the standard editor syntax: + +- Multiline text: enter text, `%f` to format, `%e` to finish (`%q` aborts). +- Numeric fields: enter the full value (e.g., `42`), apply offsets (`+5`, `-2`), + or toggle bit numbers (`p7` sets bit 7, `m7` clears). Blank input keeps the + previous value. + +## Simple-mode fields + +Simple mode exposes the minimum set needed for quick tuning: + +| `/n` | Field | Notes | +|------|-------|-------| +| `/1` | Aliases | Lowercase keywords players type (`orc guard orc guard`). | +| `/2` | Reference description | Short desc (`a surly orc guard`). Used in lists. | +| `/3` | Full room description | The long description players see when entering the room. Must end in a newline and period. | +| `/4` | Detailed description | The text shown when someone `look `. | +| `/5` | Mob flags | Bitvector of behaviour flags (see “Mob flags” below). Input accepts sums or `p` toggles. | +| `/6` | Affects | Bitvector of permanent affects applied to the mob (see “Affect flags”). | +| `/7` | Level | Combat level. Keep within expected zone ranges (check `/50` on similar mobs). | +| `/8` | Sex | `0` neutral, `1` male, `2` female. | +| `/9` | Race | Use the `RACE_*` constants from `src/structs.h` (see table in `docs/shape_script.md`). | +| `/10` | Body type | Determines hit locations (0 tiny, 1 humanoid, 2 quadruped, 3 tentacled, 4 bird). | +| `/11` | Race aggression | Bitvector built from race IDs (`1 << RACE_HUMAN`, etc.). Aggressive mobs attack those races on sight even without the `AGGRESSIVE` flag. | +| `/12` | Butcher item | Vnum of the object dropped when butchering the corpse (`0` for none). | +| `/40` | Mob spirit | Role-play marker shown in prompts (`docs/shape_script.md` lists the titles). Use the constants from `prompt_spirit` in `src/consts.cpp`. | + +## Extended-mode field reference + +| `/n` | Field | Description / Notes | +|------|-------|---------------------| +| `/1` – `/6` | Same as simple mode. | +| `/7` | Alignment | Stored in `specials2.alignment`, range roughly `-1000..1000`. Positive = good. | +| `/8` | Level | Combat level. | +| `/9` | Combat stats | Prompts for OB, parry, and dodge (three integers). Check balance against existing mobs. | +| `/10` | Hit points | Prompts for min and max hit (`min_hit`, `max_hit`). | +| `/11` | Damage | Base damage per swing (affects `points.damage`). | +| `/12` | Energy regen | Controls `points.ENE_regen`, which is used by `char_utils::get_energy_regen` and combat systems to determine both how quickly the mob regains energy and how fast it cycles attacks. Higher values shorten weapon recovery times (see ranger special shots, wild fighting handler, and `profs.cpp` multipliers), while lower values slow its swing rate. Defaults to roughly `70 + level * 2` in `new_mob`. | +| `/13` | Gold | Coins carried. Keep small unless the mob is meant to be a bank. | +| `/14` | Experience | Raw XP value. Use `/recalc` as a starting point and adjust sparingly. | +| `/16` | Current position | See `structs.h` `POSITION_*` defines (`POSITION_STANDING`, etc.). Determines the mob’s immediate pose when spawned. | +| `/17` | Default position | Fallback pose when not engaged (often `POSITION_STANDING` or `POSITION_RESTING`). | +| `/18` | Sex | 0 neutral, 1 male, 2 female. | +| `/19` | Race | `RACE_*` constant. Drives stat caps, languages, sunlight penalties. | +| `/20` | Race aggression | Bitvector of target races (see simple mode). | +| `/21` | Weight | Stored in hundredths of kg (`5000` = 50 kg). | +| `/22` | Height | Centimetres. | +| `/23` | Profession points | `GET_PROF_POINTS` per class (wizard/cleric/ranger/warrior). Enter four integers separated by spaces. | +| `/24` | Stamina (mana) | `constabilities.mana`. Use `/recalc` as reference when raising above default. | +| `/25` | Move points | `constabilities.move`. | +| `/26` | Body type | Same as `/10` in simple mode (0–4). | +| `/27` | Saving throw | Base save modifier (`GET_SAVE`). Negative is better. | +| `/28` | Stats | Enter `STR INT WILL DEX CON LEA` in that order. Range 0–40. | +| `/29` | Program number | ASIMA program ID (see `docs/shape_mudlle.md`). Clears `MOB_SPEC` when used. | +| `/30` | Language | The skill ID from `language_skills[]`/`LANG_*`. `LANG_BASIC` = common. | +| `/31` | Butcher item | Same as simple `/12`. | +| `/32` | Perception | `specials2.perception`; affects search/hide detection. | +| `/33` | Room death cry | Text shown in the room when the mob dies. | +| `/34` | Other-room death cry | Text broadcast to adjacent rooms. | +| `/35` | Corpse number | Vnum of the corpse object to spawn (`0` uses default). | +| `/36` | Resistances | Bitvector; see `MAN SHAPE MOB 36` (values align with spell schools such as fire, cold). | +| `/37` | Vulnerabilities | Bitvector, same mapping as resistances. | +| `/38` | Script number | Script vnum (see `docs/shape_script.md`). Works alongside ASIMA programs. | +| `/39` | RP flag | Bitmask of races allowed to roleplay with this mob (`specials2.rp_flag`). | +| `/40` | Mob spirit | Same as simple `/40`. | +| `/41` | Will teach | Toggles teaching capabilities (which skills/specs this trainer handles). Use bitmask defined in the training subsystem (`TRAIN_*`). | +| `/49` | Guided creation | Runs the field sequence recommended for new mobs. | +| `/50` | List | Prints every field (`/imp` plus `/50` is a good sanity check). | + +## Mob flags (`/5`) + +Bit numbers live in `src/structs.h` (`MOB_*`). Key ones: + +| Bit # | Flag | Description | +|-------|------|-------------| +| 0 | `MOB_SPEC` | Hard-coded special procedure. Clear when using ASIMA. | +| 1 | `MOB_SENTINEL` | Never roams. | +| 2 | `MOB_SCAVENGER` | Picks up/wears gear. | +| 3 | `MOB_ISNPC` | Always set. | +| 4 | `MOB_NOBASH` | Immune to bash. | +| 5 | `MOB_AGGRESSIVE` | Attacks any PC entering the room. | +| 6 | `MOB_STAY_ZONE` | Won’t leave its zone. | +| 7 | `MOB_WIMPY` | Flees when low on HP. | +| 8 | `MOB_STAY_SECT` (`STAY_TYPE`) | Only wanders within its sector type (e.g., water). | +| 9 | `MOB_IS_MOUNT` | Can be ridden. | +| 10 | `MOB_CAN_SWIM` | Doesn’t need a boat. | +| 11 | `MOB_MEMORY` | Remembers attackers and re-aggros them. | +| 12 | `MOB_HELPER` | Assists friends during fights. | +| 16 | `MOB_BODYGUARD` | Rescues its master. | +| 17 | `MOB_WRAITH` | Ghost-like (no corpse). | +| 18 | `MOB_SWITCHING` | Switches targets mid-fight. | +| 19 | `MOB_NORECALC` | Prevents `/recalc` from overwriting handcrafted stats. | +| 20 | `MOB_ACTIVE` | Acts immediately on room entry (50% chance). | +| 21 | `MOB_PET` | Tamed pet (usually set automatically). | +| 22 | `MOB_HUNTER` | Hunts down remembered attackers. | +| 23 | `MOB_ORC_FRIEND` | Recruitable by common orcs. | + +Use `p` / `m` to toggle bits or enter the summed integer. + +## Affect flags (`/6`) + +These map to the `AFF_*` bitvector (`src/structs.h`). Frequently used bits from +the old MAN SHAPE MOB 6 reference: + +| Bit # | Flag | Effect | +|-------|------|--------| +| 0 | `AFF_SENSE_LIFE` | Detect hidden/invisible players. | +| 1 | `AFF_INFRARED` | See in the dark. | +| 2 | `AFF_SNEAK` | Suppresses “leaves” messages when moving. | +| 3 | `AFF_HIDE` | Mob starts hidden (requires sense life to spot). | +| 4 | `AFF_DETECT_MAGIC` | Reserved for players; avoid setting. | +| 5 | `AFF_CHARM` | Acts as charmed (follows whoever issued `follow`). | +| 6 | `AFF_CURSE` | Broken—do not use. | +| 7 | `AFF_SANCTUARY` | Permanent sanctuary—heavily reduces damage. Use sparingly. | +| 8 | `AFF_TWOHANDED` | Forces two-handed wielding. | +| 13 | `AFF_BREATHE` | Breathe underwater. | +| 18 | `AFF_FLYING` | Leaves no tracks, immune to ground effects. | + +Setting an affect grants the mob the corresponding spell permanently—use with +caution, especially for sanctuary, flying, or invisibility. + +## Alignment guidelines (MAN SHAPE MOB 7) + +Alignment (`/7` in extended mode) is a flavour stat used by scripts and some +combat checks. Use these rough ranges when tuning mobs: + +| Race type | Recommended range | +|-----------|-------------------| +| Elves | `+200` to `+350` | +| Dwarves / Hobbits | `+150` to `+300` | +| Humans | `+100` to `+200` | +| Neutral creatures | `-100` to `+100` | +| Wargs | `-100` to `-200` | +| Orcs / Uruks | `-200` to `-350` | + +Avoid values beyond ±500 unless the mob is a unique lore figure. + +## Race aggression table (MAN SHAPE MOB 11) + +Race-aggression bits (field `/20`) let you target specific races even if the +mob lacks the global `AGGRESSIVE` flag. Add the bit value to the bitvector (or +`p`): + +| Target | Bit # | Value | +|--------|-------|-------| +| God | 0 | 1 | +| Human | 1 | 2 | +| Dwarf | 2 | 4 | +| Wood elf | 3 | 8 | +| Hobbit | 4 | 16 | +| High elf | 5 | 32 | +| Uruk | 11 | 2048 | +| Haradrim | 12 | 4096 | +| Orc | 13 | 8192 | +| Easterling | 14 | 16384 | +| Magus | 15 | 32768 | + +`62` makes a mob hostile to the “whitie” races, and `63488` to the “darkie” +factions. These bits stack with the global `AGGRESSIVE` flag. + +## Body types (MAN SHAPE MOB 10) + +Field `/26` chooses the hit-location template: + +| Value | Description | +|-------|-------------| +| 0 | Tiny/limbless creatures (snakes, slimes). | +| 1 | Humanoids (head, two arms, two legs). | +| 2 | Quadrupeds (four legs + head). Only these can be tamed as mounts. | +| 3 | Tentacled (octopi, aberrations). | +| 4 | Birds (wings + talons). | + +Pick the number that best matches the mob’s anatomy; scripts and combat tables +use it for butcher parts and hit messages. + +## Butcher items (MAN SHAPE MOB 12) + +Field `/31` sets the vnum dropped from butchering the corpse. Humanoids with no +special drop should use vnum `17` (the “body parts” placeholder). Non-humanoids +can point to custom meat/pelt objects. Use `0` to disable butchering entirely. + +## Languages (MAN SHAPE MOB 30) + +Field `/30` controls the language the mob speaks/listens for. The code currently +supports the three entries in `language_skills[]`: + +| Value | Constant | Notes | +|-------|----------|-------| +| 0 | `LANG_BASIC` | Westron/common. | +| 121 | `LANG_ANIMAL` | Used by beasts. | +| 122 | `LANG_HUMAN` | Human dialect (legacy). | +| 123 | `LANG_ORC` | Black-speech. | + +Mobs default to `LANG_BASIC`. Only change this if you have scripts that check +for specific dialects. + +## Perception defaults (MAN SHAPE MOB 32) + +Leaving `/32` at `-1` lets the engine assign a racial default: + +| Race type | Default perception | +|-----------|--------------------| +| Elves | 50 | +| Other humanoids | 30 | +| Undead (non-wraith) | 60 | +| Wraiths | 100 | + +Set an explicit number if you need sharper senses (higher) or dulled senses +(lower). Values feed the hide/search routines. + +## Death cries & corpses (MAN SHAPE MOB 33–35) + +- `/33` – in-room death cry. Defaults to “Your blood freezes as you hear its + death cry.” Enter a custom string to override. +- `/34` – other-room death cry echoed to adjacent rooms. +- `/35` – corpse vnum. `0` uses the generic corpse, which is a zero-capacity + container that inherits the mob’s weight. Custom corpses must still be + containers if you want loot to remain accessible. + +## Resistances and vulnerabilities (MAN SHAPE MOB 36/37) + +Fields `/36` (resistance) and `/37` (vulnerability) are bitvectors that map to +the specialization attack groups. Bits are shared with object resist/vuln flags: + +| Bit # | Group | Value | +|-------|-------|-------| +| 0 | None / general | 1 | +| 1 | Fire | 2 | +| 2 | Cold | 4 | +| 3 | Regeneration | 8 | +| 4 | Protection | 16 | +| 5 | Animals | 32 | +| 6 | Stealth | 64 | +| 7 | Wild fighting | 128 | +| 8 | Teleport | 256 | +| 9 | Illusion | 512 | +| 10 | Lightning | 1024 | +| 11 | Mind | 2048 | + +Most of these hooks are only consulted by a handful of spells/skills; when in +doubt, leave both vectors at 0. + +## Special procedures (MAN SHAPE MOB2 29) + +Field `/29` can reference built-in hard-coded behaviours instead of ASIMA +programs. Common IDs: + +| ID | Behaviour | +|----|-----------| +| 1 | Snake (poisons on hit). | +| 2 | Friendly gatekeeper (opens doors during day / on knock). | +| 3 | Caster-mystic (buffs/heals). | +| 4 | Caster/mage (offensive spells). | +| 5 | Warrior (bashes frequently). | +| 6 | Paranoid gatekeeper (keeps doors shut). | +| 7 | Jig (performs the jig command). | +| 8–13 | Exit blockers (north/east/south/west/up/down). | +| 14 | Resetter (practice resetter). | +| 15 | Ranger ambusher. | +| 26 | Summoner (calls adds during fights). | +| 27 | Reciter (reads textscrolls). | +| 28 | Herald (announces arrivals). | + +If you use a spec proc, remember to set the `SPECIAL` flag (`/5` bit 0). For +anything beyond these stock options, use ASIMA (`/29` with a program number) or +scripts (`/38`). + +## Best practices + +- **Use `/recalc` cautiously**: the command recalculates combat stats from the + level. It’s useful when starting but will wipe custom OB/hp/damage unless the + mob carries `MOB_NORECALC`. +- **Match zone expectations**: compare your mob’s `/50` output with similar + creatures already in the zone. Keep OB/HP/damage roughly aligned. +- **Body type matters**: choose the right `/26` for hit locations (humanoid vs + quadruped). Only body type 2 (quadruped) mobs can be tamed as mounts. +- **Race + sunlight**: orcs, uruks, and olog-hai suffer daylight penalties. If + your mob roams outside, consider equipping them with cloaks or scheduling + behaviour via scripts. +- **Programs vs scripts**: `/29` ASIMA programs run before `/38` script + triggers. Avoid using both unless you know which behaviour fires first. +- **Training mobs**: when using `/41`, ensure the trainer actually offers + corresponding lessons via the spec code—setting random bits does nothing if + the spec isn’t implemented. + +## Example: Updating a city guard + +Goal: create a level-40 human guard who challenges orcs at the gate, carries a +halberd, and speaks Westron. + +```text +shape mob 4005 +/1 gate guard guard human guard +/2 a vigilant gate guard +/3 A vigilant gate guard watches the traffic. +/4 The guard scans every traveler before waving them through. +/5 p1 p6 p12 # SENTINEL + STAY_ZONE + HELPER +/6 p1 # AFF_SENSE_LIFE +/7 200 # alignment +/8 40 # level +/9 95 60 20 # OB, parry, dodge +/10 1200 1500 # min/max hit +/11 28 +/12 30 +/13 15 # gold +/14 250000 # experience +/16 8 # current position (standing) +/17 8 # default position +/18 1 # male +/19 1 # RACE_HUMAN +/20 p13 # aggressive to orcs +/21 8500 +/22 185 +/23 10 10 10 10 # prof pools +/24 200 +/25 300 +/26 1 # humanoid +/27 -10 +/28 35 20 25 30 32 25 +/29 0 # no ASIMA program +/30 0 # LANG_BASIC +/31 0 # no butcher drop +/32 15 +/33 The guard crumples with a surprised gasp. +/34 You hear a guard fall nearby! +/35 0 +/36 0 +/37 0 +/38 4206 # gatekeeper script +/40 Master +/41 0 +/50 +/save +/implement +``` + +After shaping, add the mob to the zone file with an `M` command and kit it with +a halberd via `K`/`E`. + +## Example: Mountable warg for an orc patrol + +```text +shape mob 4802 +/1 warg mount patrol mount +/2 a hulking warg mount +/3 A hulking warg patiently waits for an orc rider. +/4 Slaver dripping from its jaws, the beast paws at the ground. +/5 p1 p6 p9 p10 p22 # ISNPC + STAY_ZONE + IS_MOUNT + CAN_SWIM + HUNTER +/8 32 # level +/9 85 20 10 +/10 900 1100 +/11 22 +/12 40 +/18 0 # neutral sex +/19 13 # RACE_ORC (shares penalties) +/21 18000 +/22 150 +/24 150 +/25 250 +/26 2 # quadruped +/32 5 # low perception +/29 0 +/38 0 # optional script for bucking non-orcs +/50 +/save +/implement +``` + +Use the `/49` guided sequence whenever you start a new mob; it steps through +aliases → descriptions → combat stats → loot → extras, mirroring the logical +order outlined above. + +Refer back to this document whenever you need the exact field semantics—the goal +is to keep shaping knowledge in one place so we can finally retire `shap_tbl`. diff --git a/docs/shape_mudlle.md b/docs/shape_mudlle.md new file mode 100644 index 00000000..1caaef60 --- /dev/null +++ b/docs/shape_mudlle.md @@ -0,0 +1,187 @@ +# Shape Mudlle Command (ASIMA) + +ASIMA (Assembler-Style Interpreter for Mobile Activity) is the in-game language +used to script “special” mobile behaviours without recompiling the server. This +file replaces the legacy `mudl_tbl` and describes how to load, edit, and assign +programs as well as the language primitives (stack, list, flow control) builders +need to write or maintain scripts. + +## Prerequisites & Program Numbers + +- Immortal level: only immortals may shape programs. You must also have + permission for the zone that owns the program (`get_permission(zone, ch)`). +- Program numbering: Programs live in `world/mdl/.mdl` files. Coordinate + with an implementor or use the `register` workflow to reserve a vnum before + editing (program #4205 lives in `world/mdl/42.mdl`). +- Assignment target: ASIMA only runs on mobiles. Removing a mobile’s `SPECIAL` + flag and setting its “program number” to an ASIMA vnum turns the script on. + Details appear in “Hooking a program to a mobile” below. + +## Session commands + +Start or resume via `shape program `. The shaper loads the program +from `world/mdl/.mdl`, creating a blank entry if none is found. + +| Command | Purpose | +|---------|---------| +| `/load ` | Re-read program text from disk. Usually invoked automatically by `shape program`. | +| `/show` | Display the current program number, its real index, and the raw ASIMA text. | +| `/edit` | Enter the line editor (type your ASIMA source, finish with a lone `@`). | +| `/save` | Write the updated program back to disk (backs up to `world/mdl/oldmdls/`). | +| `/implement` | Replaces the live program in memory (only works for existing programs; new ones require a reboot). | +| `/free` | Discard the in-memory buffer and exit shaping mode. | +| `/done` | Equivalent to `/implement`, `/save`, then `/free`. | + +> **Important:** `/implement` refuses to load a brand-new program because the +> runtime allocates space only at boot. After saving a new entry, coordinate a +> reboot so `boot_mudlle()` can add it to `mobile_program[]`. + +## Hooking a program to a mobile + +There are three ways to attach behaviour: + +1. **Hard-coded special** – reserved for stock guildmasters/quest NPCs. Avoid. +2. **Hard-coded proc selected at runtime** – use `shape mob /29` or zone + command `A 7 ` to pick from the limited list in `spec_pro.cpp`. +3. **ASIMA program** – clear the `SPECIAL` flag, set the mobile’s program number + to your ASIMA vnum (via `shape mob /29`), and ensure the mob’s regen command + (zone `M` or an `A 7`) sets `store_prog_number` appropriately. This is the + preferred route for custom logic. + +When a mobile with an ASIMA program resets, the runtime converts the text into +bytecode via `mudlle_converter` and stores it in `mobile_program[real_num]`. Use +`implement` to refresh the program of an existing mob without rebooting. + +## ASIMA language overview + +- **Instruction order**: arguments precede the command. For example, to add + 2 + 3, you write `2 3 +` (the stack stores both numbers and `+` consumes them). +- **Data structures**: + - **Stack**: holds integers for arithmetic and flow control. Commands like `t` + (duplicate), `T` (pop), and `x` (swap) manage it. Arithmetic operations `+`, + `-`, `*`, `/`, bitwise `&`, `|`, logic `>`, `<`, `=`, `!`, `~` operate on the + two lowest values. + - **List**: circular buffer storing references to strings, rooms, mobiles, + players, and objects. Commands such as `f` (fetch item into list), `l`/`L` + (walk forward/backward), `p` (duplicate), `P` (remove), `X` (swap) manage it. + Many commands act on the lowest list item (e.g., `s` says the string stored + there). +- **Flow control**: + - Use `@NNN` to mark a label and `MNNN` to push its address to the stack. + - `g` performs an unconditional goto to the address stored on the stack. + - `i` performs a goto if the previous stack value is non-zero. + - `Q/q` return FALSE (with/without reset), `R/r` return TRUE. +- **Call masks**: `I` pulls a bitmask from the stack to set triggers. Bits: + `1` = command handler, `2` = self (heartbeat), `4` = enter-room. Example: + `7I` enables all three. +- **Strings**: start with a backtick, end with `S` to push as a string literal. + Example: `` `Greetings, traveller.`S `` adds the text to the list, `s` says it. +- **Delays and randomness**: + - `d` consumes a stack value and waits that many pulses. + - `N` consumes a stack value and pushes a random number between 0 and value. + +### Interaction commands + +| Command | Description | +|---------|-------------| +| Movement (`mn`, `ms`, `me`, `mw`, `mu`, `md`) | Move the host mob north/south/etc. | +| `f` + letter | Fetch references into the list: `fs` self, `fa` argument text, `fi` number-from-stack as string, `fr` room, `fh` last command issuer, `fc` first char in room, `fp` first PC, `fm` first mob, `fN` next in room. | +| `v` + letter | Push stats of the lowest list item (or host) onto the stack: `vh/VH` hit/max hit, `vm/VM` mana, `vv/VV` move, `vl` level, `vc` command verb. Returns `1` on success, `0` otherwise. | +| `V` + letter | Set stats from the stack (hit/mana/move). Use with caution. | +| `s` | Say the lowest string in the list to the room. | +| `U` | Execute the command string stored in the list (acts like “force host to run command”). | +| `W` | Cast a spell: lowest list item is the spell command, the next item (if non-zero) is the target. | +| `g` / `i` | Goto unconditionally / conditionally (jump address must already be on the stack, typically via `MNNN`). | +| `_` | Interrupt (exit without resetting state). | +| `d` | Delay for N pulses (N is taken from the stack). | + +### Stack helpers + +- `t` – duplicate the last stack value (push a copy). +- `T` – pop the last stack value. +- `x` – swap the two lowest stack values. +- `.` – no-op; useful to separate numeric literals (`12.3` pushes 12 then 3). + +### List helpers + +- `l` / `L` – move forward / backward in the list. +- `p` – duplicate current item. +- `P` – remove current item. +- `c` / `C` – detect and optionally remove duplicate references. +- `=` / `!` – compare the two lowest items in the list and push 1/0 to the stack. + +### Return semantics + +- `R` / `r` return TRUE (reset / do not reset memory). +- `Q` / `q` return FALSE (reset / do not reset memory). Use `r`/`q` to keep the + stack/list contents between calls when you want stateful behaviour. + +## Example programs + +### Simple greeter (program #4205) + +``` +#4205 +7I ; handle command/self/enter_room triggers +`Greetings, traveler.`S +s ; say the string +r ; return TRUE without resetting lists/stack +``` + +Assign it to a mobile via `shape mob /29` (set to 4205) and clear the SPECIAL +flag. The mob will greet on heartbeat and when someone enters the room. + +### Conditional healer (program #4206) + +``` +#4206 +7I +fp ; put first player in room onto the list +vH.vh.= ; compare max HP to current HP +097i ; push label 97, conditional goto if HP equals max +`I see you're not well.`S s +Vh ; set HP from stack (must push desired value first) +R ; return TRUE and reset +@97 +`You look healthy.`S s +R +``` + +This script looks for the first player in the room, compares their current HP +to max HP, and either heals or compliments them. The label `@97` plus `097i` +demonstrates conditional flow: `i` jumps to label 97 if the preceding comparison +was TRUE. + +### Command relay (program #4207) + +``` +#4207 +1I ; command-only trigger +fa ; argument line (player input) to list +`say `S ; literal "say " +X+S ; concatenate "say " with the argument line +U ; execute the combined command (host repeats the player's request) +r +``` + +This turns the mobile into a parrot that repeats whatever players tell it +(`tell mobdude sing`). Because the call mask is `1`, the program only runs when +players issue commands at the mob (not on heartbeat). + +## Best practices + +- Keep ASIMA programs small. The language was designed for lightweight behaviours + (greeters, basic quest logic). Complex systems are still better handled via + hard-coded specials. +- Comment externally: there is no in-language comment syntax. Maintain a note in + the zone docs describing what each program number does. +- Back up the `.mdl` file before editing heavily. `/save` already writes a copy + to `world/mdl/oldmdls/`, but snapshotting your source is still wise. +- Use `/implement` after minor edits to existing programs so you can test them + immediately. +- New programs need a reboot before they can be referenced. Plan accordingly and + avoid hooking a brand-new vnum into mobs until after the restart. + +With this reference, you can retire `mudl_tbl`, edit programs entirely from the +CLI, and understand how ASIMA scripts interact with mobiles and zone regen. + diff --git a/docs/shape_object.md b/docs/shape_object.md new file mode 100644 index 00000000..37b8c23d --- /dev/null +++ b/docs/shape_object.md @@ -0,0 +1,411 @@ +# Shape Object Command + +Object shaping is the in-game workflow for creating or editing prototypes stored +under `world/obj/*.obj`. The entry point is `shape object …` inside +`src/shapemob.cpp`, while the interactive editor lives in `src/shapeobj.cpp`. +This guide explains how to start a session, what the slash commands do, and +what each numeric menu option edits. + +## Prerequisites + +- **Builder access** – `get_permission(zone, ch)` must grant write privileges + for the target zone. Implementors (`object_master_idnum`, etc.) bypass this, + but regular builders need explicit access. +- **Vnum assignment** – use the in-game `register` command (see `MAN WIZ REGISTER`) + to reserve a new + object vnum before running `shape object new `. Editing existing objects + requires knowing their vnums (`stat obj` or `show zone` helps). +- **Prompt awareness** – when the object editor is active your prompt number + changes to `6`, reminding you that every command must be prefixed with `/`. +- **Zone fit** – skim the OBJLIST guidelines to ensure the item type suits + the zone’s weapon/armor categories before you start shaping. + +## Starting a session + +| Command | When to use | Notes | +|---------|-------------|-------| +| `shape object ` | Edit an existing object | Loads the prototype from `world/obj/.obj` and places it in the editor buffer. | +| `shape object new ` | Begin a fresh object | Creates a blank template, points it at `world/obj/.obj`, and jumps into the creation sequence (`/49`). Follow up with `/add ` to assign the final vnum. | + +Once loaded you’ll see “You start shaping an object.” and the editor prompt +appears. All further commands must start with `/` (per the builder manual’s +GENERAL section). Inside the editor, `/help` or `/0` mirrors the `MAN SHAPE OBJ +` entries, so keep that manual handy for deep dives. + +## Session control commands (`extra_coms_obj`) + +| Command | Purpose & behaviour | +|---------|---------------------| +| `/create ` | Sets target files (`world/obj/.obj` and `world/obj/oldobjs/.obj`), allocates a blank template via `new_obj()`, and starts the creation chain. | +| `/load ` | Reads the specified object into the editor buffer (`load_object()`). Refuses if another object is already loaded. | +| `/save` | Writes the edited object back over its existing record (`replace_object()`), first backing up to `world/obj/oldobjs/.obj`. | +| `/add ` | Appends the buffer as a brand-new vnum at the end of the zone file (`append_object()`), also producing a backup copy. Use this after `shape object new`. | +| `/delete` | Two-step safety. First call arms deletion and asks for “yes”. Typing `yes` immediately afterward flags the next `/save` to remove the object from disk. | +| `/implement` | Calls `implement_object()` to push the edited object into the live `obj_proto[]` array without touching disk. Useful for testing changes right away. | +| `/done` | Convenience action: `/save`, `/implement`, then `/free`. Ends the session cleanly. | +| `/free` | Releases the editor buffer (`free_object()`), clears shaping flags, and restores your normal prompt/position. Always free before switching targets. | + +Entering any other word after `/` prints the supported verbs and leaves you in +edit mode. + +## Editing workflow + +Type `/0` (or any non-digit) to display the numeric menu (`list_help_obj()`), +then run `/1`, `/2`, etc. to change fields. Inputs fall into four patterns: + +1. **Text entry** (`LINECHANGE` / `DESCRCHANGE`) uses the standard `%f`/`%e` + editor. `%q` cancels and keeps the previous string. +2. **Single-value prompts** (`DIGITCHANGE`) leverage `string_to_new_value()`, so + you can enter absolute numbers (`123`), add/subtract (`+5`, `-2`), or toggle + bit positions (`p7`, `m3`). Blank lines keep the old value. +3. **Multi-value prompts** (e.g., `/12`, `/19`) expect space-separated numbers + on one line. Press Enter with no input to abort. +4. **Creation chain** (`/49`) toggles `SHAPE_CHAIN` and automatically steps you + through the recommended sequence (`obj_chain[]`). It’s a nice guided tour + when drafting new gear. + +## Field reference + +### Text fields + +| `/n` | Field | Notes | +|------|-------|-------| +| `/1` | Aliases | Space-separated keywords players type (`get axe`). For drink containers, make the liquid name the first alias (`beer mug`). | +| `/2` | Reference description | Short description shown in inventory lists (“a steel longsword”). No trailing period. | +| `/3` | Full (in-room) description | How the object appears in a room (“A steel longsword lies here.”). Capitalize and end with a period. | +| `/4` | Action description | Multi-line `look` text (paragraph). Treat like a room description with `%f` formatting. For ITEM_NOTE objects this is the readable body of the note. | + +### Extra descriptions + +| `/n` | Behaviour | +|------|-----------| +| `/5` | Push a new extra description onto the list (no input). Automatically chains to `/6` and `/7`. | +| `/6` | Edit the keyword list for the current extra description (lowercase words, space-separated, avoid punctuation). | +| `/7` | Edit the text for the current extra description. Use `%f` if needed. | +| `/8` | Remove the current/last extra description. | + +Extra descriptions work as a stack. Run `/5`, then `/6`/`/7` to populate the +new record. `/8` removes the most recent entry. + +### Flags and wear slots + +| `/n` | Field | Notes | +|------|-------|-------| +| `/9` | Type flag | See the `ITEM_*` constants in `structs.h`. Determines how `/12` values are interpreted. | +| `/10` | Extra flags | Bitvector (glow, humming, magic, nodrop, etc.). Use `p`/`m` to toggle. | +| `/11` | Wear flags | Bitvector (TAKE, FINGER, NECK, …). At minimum set `TAKE` for portable items and `WIELD` for weapons. | + +Extra-flag bits: + +- `0` (`1`) GLOW +- `1` (`2`) HUMMING +- `2` (`4`) DARK +- `3` (`8`) BREAKABLE (keys, brittle items) +- `4` (`16`) EVIL +- `5` (`32`) INVISIBLE +- `6` (`64`) MAGIC +- `7` (`128`) NODROP +- `8` (`256`) BROKEN +- `9` (`512`) ANTI_GOOD (avoid unless directed) +- `10` (`1024`) ANTI_EVIL (avoid) +- `11` (`2048`) ANTI_NEUTRAL (avoid) +- `12` (`4096`) NORENT + +Wear-flag bits: + +`0` TAKE, `1` FINGER, `2` NECK, `3` BODY, `4` HEAD, `5` LEGS, `6` FEET, `7` +HANDS, `8` ARMS, `9` SHIELD, `10` ABOUT BODY, `11` WAIST, `12` WRIST, `13` +WIELD, `14` HOLD, `15` THROW, `16` LIGHT-SOURCE, `17` BELT. + +### Core stats & metadata + +| `/n` | Field | Notes | +|------|-------|-------| +| `/12` | Values[0..4] | Five integers whose meaning depends on the type flag. Enter five numbers at once. See the “Object values” section below. | +| `/13` | Weight | Stored in hundredths of a kilogram. A one-kilogram item is `100`. Pickable objects **must** have a non-zero weight; non-takeable props may stay `0`. Use the shapetable guideline: wielding two-handed roughly requires Strength equal to the weight in kg (so 8 kg takes STR 8), while one-handed wielding needs double that. | +| `/14` | Cost | Shop price guideline (typically `10 * level^2` for levels ≤10, doubling thereafter). | +| `/15` | Rent / cost per day | Suggested rent per in-game hour (`level^2` for ≤5, otherwise `(level^3)/5`). Matches the original “cost per day” design. | +| `/16` | Level | Represents the quality/tier of the item. Keep it near the mobs that drop it. | +| `/17` | Rarity | Reserved for future random generators; leave at `0` unless directed otherwise. | +| `/18` | Material | Integer index into `object_materials[]` (`cloth`, `leather`, `metal`, etc.). | + +Common material ids: +`0` usual, `1` cloth, `2` leather, `3` chain, `4` metal, `5` wood, `6` stone, +`7` crystal, `8` gold, `9` silver, `10` mithril, `11` fur, `12` glass, +`13` plant. + +### Affects and scripts + +| `/n` | Field | Notes | +|------|-------|-------| +| `/19` | Object affects | Enter pairs like `( 18 10 ) ( 17 5 )` to apply +10 OB and +5 dodge. Slots beyond `MAX_OBJ_AFFECT` are ignored. Use `(0 0)` fillers if you’re not sure. | +| `/20` | Program number | Legacy prog hook; almost never used. Only touch if a senior implementor asks you to. | +| `/21` | Script number | Slots the object into the mudlle/script subsystem. Also restricted to special cases. | +| `/49` | Creation sequence | Walks you through aliases → descriptions → flags → stats using `obj_chain[]`. Great for new items. | +| `/50` | List | Calls `list_object()` and prints every field for auditing. | + +## Object value reference (command `/12`) + +Because `/12` edits five raw integers, you **must** consult the per-type +definitions below. New objects start with zeros (the TRASH +defaults), so adjust every slot unless you truly want a junk item. The table +below summarizes every entry documented there: + +| Entry | Value meanings | +|-------|----------------| +| LIGHT (type 1) | `value[2]` = burn hours (`0` = burnt out, `<0` = eternal). All other slots unused. | +| WEAPON (5) | `value[0]` OB, `value[1]` parry bonus, `value[2]` bulk (≈2/3 feet), `value[3]` attack category (2=whip, 8=axe, 11=pierce…), `value[4]` damage. | +| ARMOR (9) | `value[0]` `0` for auto absorb, `-1` to disable; `value[1]` min absorb; `value[2]` encumbrance; `value[3]` dodge bonus; `value[4]` reserved. | +| WORN (11) | Deprecated catch-all. Leave unused—create light armor with zeroed armor values instead. | +| OTHER (12) | All zeros. Use when no other category fits. | +| TRASH (13) | All zeros. Pure flavour items. | +| CONTAINER (15) | `value[0]` capacity (hundredths of kg), `value[1]` flags (1 closeable / 2 pickproof / 4 closed / 8 locked), `value[2]` key vnum (`-1` none), `value[3]` corpse rot timer, `value[4]` unused. | +| NOTE (16) | `value[0]` language id (tongue). Others unused. | +| DRINKCON (17) | `value[0]` max units, `value[1]` current units, `value[2]` liquid type (`LIQ_WATER` … `LIQ_CLEARWATER`), `value[3]` poison flag, `value[4]` unused. | +| KEY (18) | `value[0]` key/lock id (match door’s lock vnum). Others unused. | +| FOOD (19) | `value[0]` hours of fullness; `value[3]` poison flag; rest unused. | +| MONEY (20) | `value[0]` number of coins. Others unused. | +| BOAT (22) | No special values; leave zeros. | +| FOUNTAIN (23) | Same layout as DRINKCON. | +| SHIELD (24) | `value[0]` dodge bonus, `value[1]` parry bonus, `value[2]` encumbrance, `value[3]` shield block coefficient, `value[4]` reserved. | +| LEVER (25) | `value[0]` room vnum containing the door, `value[1]` direction (0–5 for N/E/S/W/U/D). Always mark levers as NOTAKE and set the matching door flag. | + +Copy values from similar objects with `/50` when in doubt, and stick to the +attack-category guidelines listed under the weapon summary above when selecting +message types. + +### Liquid type ids + +| Name | Id | Drunkness | Fullness | Thirst | +|------|----|-----------|----------|--------| +| LIQ_WATER | 0 | 0 | 1 | 10 | +| LIQ_BEER | 1 | 3 | 2 | 5 | +| LIQ_WINE | 2 | 5 | 2 | 5 | +| LIQ_ALE | 3 | 2 | 2 | 5 | +| LIQ_DARKALE | 4 | 1 | 2 | 5 | +| LIQ_WHISKY | 5 | 6 | 1 | 4 | +| LIQ_LEMONADE | 6 | 0 | 1 | 8 | +| LIQ_FIREBRT | 7 | 10 | 0 | 0 | +| LIQ_LOCALSPC | 8 | 3 | 3 | 3 | +| LIQ_SLIME | 9 | 0 | 4 | -8 | +| LIQ_MILK | 10 | 0 | 3 | 6 | +| LIQ_TEA | 11 | 0 | 1 | 6 | +| LIQ_COFFE | 12 | 0 | 1 | 6 | +| LIQ_BLOOD | 13 | 0 | 2 | -1 | +| LIQ_SALTWATER | 14 | 0 | 1 | -2 | +| LIQ_CLEARWATER | 15 | 0 | 0 | 13 | + +### Bitvector reference (`OBJ 19` / `OBJ BITVECTOR`) + +- `/19` expects `(location modifier)` pairs. `location` corresponds to the + APPLY table below (OB, dodge, regen, spell bonuses, etc.). +- For `APPLY_BITVECTOR` (location `28`), `modifier` represents the bit number + shown in the affect-bit table (e.g., `AFF_DETECT_HIDDEN = 0`, + `AFF_SANCTUARY = 7`). Use `p`/`m` syntax when editing extra/wear flags, but + stick to `(location modifier)` tuples for `/19`. +- For `APPLY_SPELL` (location `27`), encode `modifier` as `256 * spell_level + + spell_number`. Location `30` (RESISTANCE) and `31` (VULNERABILITY) treat the + modifier as the bit position documented in `MAN SHAPE MOB 36`. +- Keep a reference of the effect list handy; when unsure, default to `(0 0)` and + ask an implementor before granting powerful affects like sanctuary or haste. + The original table cautions that some flags do not behave as expected, so test + thoroughly before shipping unusual combinations. + +The affect-bit reference: + +| Bit # | Affect | +|-------|--------| +| 0 | AFF_DETECT_HIDDEN | +| 1 | AFF_INFRARED | +| 2 | AFF_SNEAK | +| 3 | AFF_HIDE | +| 4 | AFF_DETECT_MAGIC | +| 5 | AFF_CHARM | +| 6 | AFF_CURSE | +| 7 | AFF_SANCTUARY | +| 8 | AFF_TWOHANDED | +| 9 | AFF_INVISIBLE | +| 10 | AFF_MOONVISION | +| 11 | AFF_POISON | +| 12 | AFF_PROTECT_EVIL | +| 13 | AFF_PARALYSIS | +| 14 | AFF_GROUP | +| 15 | AFF_CONFUSE | +| 16 | AFF_SLEEP | +| 17 | AFF_BASH | +| 18 | AFF_DETECT_EVIL | +| 19 | AFF_DETECT_INVISIBLE | +| 20 | AFF_FEAR | +| 21 | AFF_BLIND | +| 22 | AFF_FOLLOW | +| 23 | AFF_SWIM | +| 24 | AFF_HUNT | +| 25 | AFF_EVASION | +| 26 | AFF_WAITING | +| 27 | AFF_WAITWHEEL | +| 28 | AFF_ORC_DELAY | +| 29 | AFF_CONCENTRATION | +| 30 | AFF_HAZE | + +Use the following `APPLY_*` codes when filling the `(location modifier)` pairs: + +| Code | Applies to | Notes | +|------|------------|-------| +| 0 | APPLY_NONE | Placeholder, no effect. | +| 1 | APPLY_STR | Strength | +| 2 | APPLY_DEX | Dexterity | +| 3 | APPLY_INT | Intelligence | +| 4 | APPLY_WIS | Wisdom | +| 5 | APPLY_CON | Constitution | +| 6 | APPLY_LEA | Leadership | +| 7 | APPLY_PROF | Proficiency points | +| 8 | APPLY_LEVEL | Character level | +| 9 | APPLY_AGE | Age | +| 10 | APPLY_CHAR_WEIGHT | Weight | +| 11 | APPLY_CHAR_HEIGHT | Height | +| 12 | APPLY_MANA | Stamina/mana | +| 13 | APPLY_HIT | Hit points | +| 14 | APPLY_MOVE | Movement points | +| 15 | APPLY_GOLD | Money | +| 16 | APPLY_EXP | Experience | +| 17 | APPLY_DODGE | Dodge bonus | +| 18 | APPLY_OB | Offensive bonus | +| 19 | APPLY_DAMROLL | Damage bonus | +| 20 | APPLY_SAVING_SPELL | Saving throws | +| 21 | APPLY_WILLPOWER | Will | +| 22 | APPLY_REGEN | Energy regen | +| 23 | APPLY_VISION | Positive values give infravision, negatives blind | +| 24 | APPLY_SPEED | Initiative/speed | +| 25 | APPLY_PERCEPTION | Search/listen | +| 26 | APPLY_ARMOR | Generic armor modifier | +| 27 | APPLY_SPELL | Encodes spell/level via `256*level + spell_number` | +| 28 | APPLY_BITVECTOR | Adds/removes affect bits (see table above) | +| 29 | APPLY_MANA_REGEN | Mana regen per tick | +| 30 | APPLY_RESISTANCE | Bitvector from `MAN SHAPE MOB 36` | +| 31 | APPLY_VULNERABILITY | Bitvector from `MAN SHAPE MOB 36` | + +## Example workflows + +### Modify an existing weapon + +``` +shape object 2503 # load an existing longsword +/50 # inspect current stats +/1 +mithril greatsword +/2 +a gleaming mithril greatsword +/3 +A gleaming mithril greatsword has been left here. +/4 + Etched runes crawl along the blade, humming with latent fire. +%f +%e +/9 +5 # ensure it's still a weapon +/12 +110 30 6 8 32 # OB 110, parry 30, bulk 6, axe slash, damage 32 +/13 +650 # 6.5 kg two-hander +/16 +45 +/18 +10 # mithril material +/19 +( 18 12 ) ( 17 5 ) # +12 OB, +5 dodge +/14 +20250 +/15 +3645 +/save +/implement +/done +``` + +### Create a brand-new quest note + +``` +register # get the next open vnum, say 4205 +shape object new 42 # start a template in zone 42 +/49 # run through the guided sequence +/1 +note parchment +/2 +a sealed parchment note +/3 +A sealed parchment note flutters here. +/4 + Wax stamped with a silver falcon holds the parchment closed. +%f +%e +/9 +16 # ITEM_NOTE +/12 +5 0 0 0 0 # language 5 (Sindarin) +/13 +5 # light as paper +/14 +500 +/15 +125 +/16 +10 +/18 +1 # cloth +/19 +( 0 0 ) ( 0 0 ) # no magical affects +/add 42 # assign the next object vnum in zone 42 +/save +/implement +/done +``` + +### Retune a drink container instead of duplicating one + +``` +shape object 13302 # waterskin full of water +/5 # add an extra desc for the scent +/6 +brew smell +/7 + A sweet scent of mulled cider rises from the mouth of the flask. +%f +%e +/12 +40 40 2 0 0 # 5 drinks of cider (LIQ_WINE=2) +/18 +5 # leather +/19 +( 19 2 ) ( 0 0 ) # +2 damage (maybe the brew inspires courage) +/14 +800 +/15 +160 +/save +/implement +/done +``` + +These sequences show both modification and net-new creation: inspect with `/50`, +update text fields, tune stats/values, adjust special fields, then `/save`, +`/implement`, `/done`. + +## Troubleshooting & tips + +- “No object loaded for shaping” – you issued a numeric command before + `shape object …` or `/load`. Run `/load ` or restart. +- “You released an object and stopped shaping” – you may have typed `/free` + accidentally. Reload and resume editing. +- Remember that `/12` overwrites all five values. If you only want to change + one, re-enter all five numbers, or use `/49` to walk the defaults again. +- When duplicating an item, load the source, `/save` it under a new vnum using + `/add`, then immediately change aliases/descriptions to avoid identical + objects. +- Keep an eye on weight vs. wear slots: weapons need `WIELD`, shields need + `SHIELD`, armour must include the appropriate body slot plus `TAKE`. +- Only assign programs/scripts if you have mudlle support in place. Ordinary + builder items should leave `/20` and `/21` at `0`. + +Documenting `shape object` alongside `shape room` creates a consistent reference +for builders. Future sections (mob, zone, script) can point back here for slash +command etiquette and numeric input conventions. diff --git a/docs/shape_room.md b/docs/shape_room.md new file mode 100644 index 00000000..d5cb253c --- /dev/null +++ b/docs/shape_room.md @@ -0,0 +1,295 @@ +# Shape Room Command + +Room shaping is the online builder workflow for creating or editing rooms in +place without recompiling. The `shape room` entry point lives in +`src/shapemob.cpp` (`ACMD(do_shape)`), while the editor logic is implemented in +`src/shaperom.cpp`. This guide documents how to enter the mode, which `/` +commands are available, and what each numeric editor option changes. + +> The broader shaping system (objects, mobiles, zones, programs) works the same +> way. We are starting the documentation effort with rooms, so future sections +> can reuse the terminology established here. + +## Prerequisites + +- Builder permissions: `do_shape` only lets non-gods shape rooms. Higher level + staff can shape any prototype, but you still need zone permissions before + writing (`get_permission` in `create_room()` / `replace_room()`). +- Location: `shape room current` uses your current room number, so ensure you + are standing in the room you want to copy before starting. +- Prompt: once shaping, your prompt changes to include the builder mode number + so you know which interpreter is active. + +## Room Writing Guidelines + +### Level 91 (Lower Maias) + +- Room titles start at column 0, use title case (“Woods in the Valley”), and + never end with a period. +- Describe the location, not the visitor. Avoid implying actions (“You shiver”), + emotions, racial biases, or times of day unless the room enforces them. +- Keep a neutral voice and avoid second-person pronouns, exclamation points, or + sentence fragments. +- Ensure descriptions are at least four lines long, each indented with three + spaces. Run `%f` to wrap them neatly. +- Stay lore-friendly: Fourth Age Middle-earth allows creative flora/fauna but + not cars, firearms, or modern tech. Death traps are banned. +- Door keywords should be single lowercase words. Use the `exit_width` field for + unusual widths (default `0` lets the sector decide). + +### Level 93 (Maias) + +- Populate the zone’s metadata in `shape zone` as soon as you claim an area. +- Unless directed otherwise, lay out zones as rectangles—8 rooms north/south by + 5 or 10 rooms east/west—so future connectors are straightforward. + +## Starting a room shaping session + +| Command | When to use | Notes | +|---------|-------------|-------| +| `shape room current` | Edit the room you are standing in | `do_shape` converts `current` into the real room number and runs `load ` for you (`src/shapemob.cpp:1998-2015`). | +| `shape room 1234` | Edit any existing room by vnum | Replace `1234` with the virtual room number. The loader reads from `world/wld/.wld` (see `SHAPE_ROM_DIR`). | +| `shape room new ` | Create a blank room at the end of a zone file | Calls `create_room()` which checks zone permissions, opens `world/wld/.wld`, and prepares a new `room_data`. Immediately `/add ` afterward to persist it. | + +Once executed, you receive “You start shaping a room.” and your prompt number +switches to `4` to indicate the room editor is active. All subsequent commands +must be prefixed with `/` (per the builder manual’s GENERAL section). + +## Session control commands + +While shaping, entering any non-numeric `/command` routes through +`extra_coms_room()` (`src/shaperom.cpp:1629-1778`). These drive the lifecycle: + +| Command | Purpose & behaviour | +|---------|---------------------| +| `/load ` | Calls `load_room()` to populate `SHAPE_ROOM(ch)->room` with another vnum while leaving the editor running. Useful for quickly hopping between adjacent rooms. | +| `/create ` | Allocates a blank `room_data`, remembers `world/wld/.wld` as the working file, and marks the slot as dirty so `/add` or `/save` knows where to write. | +| `/save` | Runs `replace_room()`: copies the source `.wld` file to `world/wld/oldroms/.wld`, then rewrites the original entry with your edited data. Keeps the existing vnum. | +| `/add ` | Runs `append_room()`: same backup process as `/save`, but appends your new room to the end of the zone file and assigns the next available vnum. | +| `/delete` | First invocation arms deletion and prompts for confirmation. Typing `yes` immediately afterward toggles `SHAPE_DELETE_ACTIVE`, so the next `/save` removes the room from disk. Any other response cancels the delete. | +| `/implement` | Calls `implement_room()` to push the in-memory struct into the live `world[]` array without touching disk. Use this after `/save` to see your updates instantly in game. | +| `/done` | Convenience macro: if a room is loaded it performs `/save`, then `/implement`, then `/free`. Ends the session with one command. | +| `/free` | Calls `free_room()`, releases all allocated descriptions/exits/affects, resets prompts, and moves your character back to their previous position. Always free the editor before switching to another shaper target. | + +If you enter something else, the helper prints the allowed keywords (“save, +delete, implement, done, free”) and leaves you in edit mode. + +## Shaping workflow tips + +- Every shaping command (besides the initial `shape room …`) must start with `/`. + `/help`, `/0`, and `/50` are always available reminders. +- `/imp` shows what you’ve built; `/50` prints the current field values. +- `/free` quits without saving. `/save` writes to disk, `/implement` syncs the + live world, and `/done` performs save → implement → free in one shot. +- Always `/save` before `/free` unless you intend to discard edits. +- FAQ nuggets: + - `%e` must be on its own line to finish multiline text. `%q` aborts an edit. + - `/50` lists most commands and field states; `/help` or `/0` lists the rest. + - `/save` followed by `/done` is redundant because `/done` already saves and + implements, but running `/save` first gives you an explicit confirmation. +- `/free` ends shaping immediately—use `/done` if you want to save as you exit. +- Always indent descriptions manually (three spaces), run `%f`, then `%e`. +- Mob/object population limits are defined in the zone script (`L` commands). + If you need “exactly one mob” logic, update the zone data rather than the + room itself. + +## Editing workflow + +Type `/0` or any non-digit to display all numeric editor commands (handled by +`list_help_room()`), then use `/` to edit a field. Inputs fall into three +categories: + +1. Text entry (`LINECHANGE` / `DESCRCHANGE` macros) uses the standard `%f`/`%e` + editor; `%q` keeps the previous value. +2. Numeric entry (`DIGITCHANGE`) accepts absolute numbers, delta modifiers + (`+17`, `-2`), or bit toggles (`p5`, `m3`) just like the rest of the shaping + system. +3. Selection prompts temporarily change your prompt to ask for an exit + direction (letters `N`, `S`, `E`, `W`, `U`, `D`). + +`string_to_new_value()` backs every numeric prompt, so inputs like `p1` or `m4` +edit individual bits, while plain integers overwrite the whole field. Leaving +the prompt blank keeps the previous value. + +### Bitvector input cheat sheet + +Use these formats to manipulate flags: + +- `17` — set the full value to 17. +- `+17` / `-17` — add or subtract. +- `p17` — set bit 17 (`1 << 17`). +- `m17` — clear bit 17. + +Example: to set SENTINEL (`2`) and WIMPY (`128`) on a mob flag, either enter +`138` once or run `p1` then `p7`. `/50` after each change to confirm the result. + +### Room field commands + +| `/n` | Field | Description | +|------|-------|-------------| +| `/1` | Name | One-line room title. Stored verbatim, so follow `GUIDELINES` (title case, no trailing punctuation). | +| `/2` | Description | Multiline description. The editor swaps `#`→`+` and `~`→`-` automatically to keep `.wld` files intact; run `%f` before `%e` for proper wrapping. | +| `/3` | Room flags | Bitvector; use `p`/`m` to toggle individual bits or enter summed values directly (see `ROOM_*` in `structs.h`). `p7` sets flag 7, `m2` clears flag 2, `+4` adds 4, etc. | +| `/4` | Sector type | Numeric sector id from `sector_types` (inside, city, forest, mountain...). Values live in `constants.cpp`. | +| `/17` | Room level | Integer stored in `room_data::level` for quest tooling and scaling. | +| `/18` | Top room affect | Rewrites the first `struct affected_type` entry using four integers: `type location modifier bitvector`. Duration is forced to `-1`, so the effect is permanent until removed. | +| `/19` | Add affect | Pushes a fresh affect struct onto the list and enables chaining so `/18` runs next. | +| `/20` | Remove affect | Pops the top affect entry. Repeat to remove multiple entries. | +| `/50` | List | Prints the current state of every editable field, including the selected exit, extra descriptions, and the first affect block. | + +Text commands (`/1`, `/2`, `/13`, `/14`) honour the `%q` shortcut to cancel +edits. Numeric commands remember the previous value, so submitting a blank line +keeps the old value. + +### Exit commands + +1. `/5` — Select exit direction (must run this before editing exit-specific + fields). Accepts `n`, `s`, `e`, `w`, `u`, or `d`. If no exit exists the + editor allocates empty keyword/description strings so you can build it from + scratch. +2. `/6` — Exit flags (`room_direction_data::exit_info`). Supports all + combinations including hidden/no-look/heavy doors. Flag bits live in + `src/structs.h` (`EX_ISDOOR`, `EX_CLOSED`, `EX_LOCKED`, `EX_NOFLEE`, + `EX_PICKPROOF`, `EX_DOORISHEAVY`, `EX_NO_LOOK`, `EX_ISHIDDEN`, etc.). Use + numeric additions or `p`/`m` to toggle bits—for example `p0 p1 p9` + makes a closed, hidden, no-flee door. +3. `/7` — Remove the selected exit entirely and clear `exit_chosen`. Use this + when deleting links or cleaning up auto-generated exits. +4. `/8` — Exit keyword list. Provide space-separated words (e.g., `door hatch + trapdoor`). +5. `/9` — Exit description text (shows when players look at the door). `%f` + works here as well. +6. `/10` — Key vnum for locked exits. Set to `0` if no key is required. +7. `/11` — Destination room vnum. Enter the virtual number of the target room. + Remember to create the reverse exit manually. +8. `/12` — Exit width. Defaults to `0` (derived from sector type). Override + when you need narrow crawlways or oversized gates. + +Selecting an exit automatically creates placeholder `room_direction_data` +structures if one does not exist (`src/shaperom.cpp:718-734`), so you can +configure brand-new doors without leaving the editor. + +### Extra description commands + +| `/13` | Edit keyword (space-separated) for the current extra description record. | +| `/14` | Edit the corresponding description text. | +| `/15` | Push a new extra description onto the stack. The editor automatically sets `SHAPE_CHAIN`, so `/13` and `/14` fire next without retyping the numbers. | +| `/16` | Remove the current extra description (or the only one if it is the last). | + +Extra descriptions behave like a stack: `/15` adds to the top, `/16` pops it. +Use `/50` after `/15`/`/16` to confirm you are editing the intended entry. + +### Room affects + +- `/18` expects four integers separated by spaces: `type location modifier + bitvector`. For `ROOMAFF_SPELL` entries the `location` is the spell number + (see `skills[]`). The editor forces `duration = -1`, so affects persist until + someone removes them. +- `/19` appends a blank affect node to the head of the list, prints “A new + affection added.”, and enables chaining so `/18` triggers immediately. Use + this combo to add fog, damage auras, or `ROOMAFF_TRAP` behaviours. +- `/20` removes the head node. Run it repeatedly to clear the list from top to + bottom. + +If you try `/18` without any affect data present the shaper prints “No room +affections found.”, so remember to `/19` first. + +### `list` snapshot + +`/50` calls `list_room()` and prints: + +- Room name, description, flags, sector, and selected exit details. +- Exit keyword/description/key/destination/width for the currently selected exit + (run `/5` first to choose). +- The first extra description (keyword + text) and the first affect record if + present. + +Use it before `/save` as a final sanity check or after `/load` to understand an +existing room’s structure. + +## Example workflows + +### Modify an existing room + +```text +shape room current # load the room you are standing in +/5 # choose which exit to edit +n # at the prompt, enter “n” to pick the north exit +/11 # set the destination vnum +1605 # send the new room number +/8 # change the door keywords +oak door +%e +/2 # edit the room description + You stand before a weathered oak door... +%f +%e +/15 # add an extra description for the door +/13 +door oak door +/14 + The door is banded with iron. +%f +%e +/save +/implement +/done +``` + +This sequence highlights the typical cadence: select an exit, edit linked fields +in any order, review with `/50`, and save/implement when finished. + +### Create a new room from scratch + +```text +shape room new 16 # create a template (zone 16 covers rooms 1600-1699) +/49 # optional: walk the chained command list +/1 +Mist-Draped Bridge +/2 + Wisps of mist cling to the old stone bridge, hiding the drop below. +%f +%e +/3 +p0 p4 # example: DARK + NOMOB flags +/4 +3 # SECT_FIELD (adjust to taste) +/5 +n # select the north exit +/11 +1602 # point to the destination room +/8 +arch doorway bridge +/10 +0 # no key +/12 +180 # narrow exit width +/13 +%q # no extra description yet +/17 +35 # room level +/save # writes to world/wld/16xx.wld and backs up +/implement # updates the live world array +/done +``` + +Repeat for the south/east/west exits as needed, then `/imp` to double-check +your work in-game. Every new zone already includes 40 blank rooms, so stay +within your allocated number range. + +## Troubleshooting tips + +- “You have nothing to shape” — you ran a numeric command before loading a + room. Use `/load ` or restart with `shape room current`. +- “You are already shaping something” — you forgot to `/free` your previous + object/mob/room. Either finish and `/done`, or `/free` to start fresh. +- “You may not create room here” — the zone does not grant you permission (see + `get_permission()`), or you mistyped the zone number. Contact the zone owner + or the implementor. +- Accidentally deleted an exit or description? Because `/save` makes a backup in + `lib/backups/rooms/`, you can copy the `.bak` back in place or reload the room + without saving to revert to the last known state. + +With `shape room` documented, future scripting-doc sections can reference this +file rather than repeating the basics of prompts, `/save` vs `/implement`, and +the slash command syntax. diff --git a/docs/shape_script.md b/docs/shape_script.md new file mode 100644 index 00000000..2196a7e0 --- /dev/null +++ b/docs/shape_script.md @@ -0,0 +1,456 @@ +# Shape Script Command + +Scripts are short command sequences that attach to mobiles (and, partially, to +objects) and execute when a trigger fires: someone enters a room, speaks, wears +an item, etc. They are stored in `world/scr/.scr` and edited entirely +in-game. This guide replaces the legacy `scr_tbl` so builders can shape scripts +without hunting through old text files. + +## Prerequisites + +- Immortal access plus zone permissions (`get_permission(zone, ch)`). +- A reserved script vnum (matching the zone number, e.g., script 4205 lives in + `world/scr/42.scr`). Coordinate with an implementor if unsure. +- Target mobile: scripts currently run on mobiles; object hooks exist but only + a subset of triggers honor them. Assign the script vnum to a mobile via + `shape mob /38` and clear the `SPECIAL` flag unless combining it with a + hard-coded proc. + +## Working with `shape script` + +1. `shape script ` to load or create a program. New scripts get a blank + header but no commands. +2. Use the numeric menu (`/0`): + - `/1` show previous/current/next command. + - `/2` set a mask (filter) by command letter, room, etc. + - `/3` change the current command type. + - `/4` edit parameters for the current command. + - `/5` edit the one-line comment/description attached to the current command. + - `/6` / `/7` move to the next/previous command (respecting the “current + room” filter set via `/12`). + - `/8` jump to a specific command number. + - `/9` delete the current command (prompts for `y/n`). + - `/10` insert a new command after the current one. `/11` inserts before. + - `/12` set the “current room” for filtering (`0` = show entire script). + - `/13` swap the current command with the next. + - `/14` run a syntax check (flags unterminated `BEGIN/END`, etc.). + - `/20` change the script name; `/21` change the script description. + - `/50` list the entire script. +3. Editing fields uses the same conventions as other shapings: + - Text entry (`/5`, `/20`, `/21`) opens the `%f/%e` editor. + - Numeric prompts accept direct values (e.g., `42`), offsets (`+5`), or + bit toggles (`p3` sets bit 3, `m3` clears). Blank lines keep the old value. +4. `/save` writes the script back to disk (after backing up to + `world/scr/old/`). `/implement` copies the temporary version into the live + `script_table` if the script existed when the MUD booted. `/done` performs + `/implement`, `/save`, then `/free`. New scripts require a reboot before + they can be implemented. +5. `/free` abandons changes and exits shaping. Always `/save` first if you care + about the edits. + +### Script file structure + +Each entry in a `.scr` file looks like: + +``` +#