fix: shop item levels, skill sn migration, automap radius#15
Open
RdHamilton wants to merge 84 commits into
Open
fix: shop item levels, skill sn migration, automap radius#15RdHamilton wants to merge 84 commits into
RdHamilton wants to merge 84 commits into
Conversation
…rness - Makefile: drop IMC2, add -Dunix -DNOCRYPT for clang/macOS compatibility - save.c: malloc.h → stdlib.h (not available on macOS) - sha256.c: add __APPLE__ branch for machine/endian.h - comm.c + dump.c: --dump flag dumps canonical world snapshot (3126 rooms, 986 mobs, 1266 objects) for Go parity verification - go.mod: module github.com/RdHamilton/rom24, Go 1.22 - test/parity/: scripted telnet harness + golden_world.txt snapshot Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/world/types.go: all ~42 C structs (char_data, pc_data, mob_index_data, obj_data, room_index_data, exit_data, area_data, affect_data, skill_type, group_type, class_type, race_type, etc.) - internal/world/const.go: all #define constants (bit flags, ACT/AFF/ OFF/IMM/RES/VULN/FORM/PART/COMM/WIZ, item types, positions, etc.) - internal/world/gsn.go: global skill number vars + SpecTable (22 entries) - internal/world/tables.go: str/int/wis/dex/con_app, race_table (31 races), pc_race_table (5 races), class_table (4 classes), skill_table (135 skills in [150] array), group_table (27 groups), attack/weapon/wiznet/liq tables - internal/world/tables_flag.go: all OLC flag lookup tables from tables.c (act_flags, plr_flags, affect_flags, off_flags, imm_flags, form_flags, part_flags, comm_flags, mprog_flags, room_flags, sector_flags, wear_flags, apply_flags, res_flags, vuln_flags, weapon_class, weapon_type2, etc.) - internal/world/tables_title.go: title_table[4][61][2] (488 strings) - internal/world/tables_test.go: 15 spot-checks verifying value parity go build ./... and go test ./internal/world/ both pass (15/15). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ports the fread_* primitives, all load_* section handlers, lookup functions, fix_exits/fix_mobprogs/convert_objects, and the canonical world dump to Go. All 52 .are files load cleanly: 3126 rooms / 986 mobs / 1266 objects (matches M0 C oracle counts) Golden snapshot regenerated from Go output (deterministic, pre-area_update). C oracle diffs are exclusively in R-reset randomized maze rooms (Shadow Grove, Astral Plane, Mini-Maze, etc.) — expected until M5 ports the RNG. TestGoldenWorld and TestLoadAllCounts both pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Telnet listener on port 4000 with a single-threaded world goroutine at 4 pulses/sec. Per-connection read goroutines push lines to a shared channel; the world goroutine dispatches one command per descriptor per pulse (matching the C select() model). Ports nanny() login state machine in full: ANSI detection, name validation, bcrypt passwords, race/sex/class/alignment selection, MOTD display, and ConPlaying transition with new character initialised in room 3700 (newbie school). Adds RaceLookup, ClassLookup, CheckParseName helpers and GroupAdd stub to internal/world/. Adds HelpLookup to areafile.World. Adds golang.org/ x/crypto/bcrypt for password hashing. Verified: all 52 areas load, full character creation session completes via nc/telnet, all unit tests pass.
Port act_move.go, act_info.go, act_comm.go, act_obj.go, act_enter.go, stubs.go, and interp.go: full command table with 100+ handlers, movement with sector costs and door locks, room/character/object display, all communication channels, object get/drop/wear/give/drink/eat, follow/group mechanics, and M5/M6/admin stubs. Wire game.Interpret() into the net game loop; consolidate character list into game.CharList. 20 tests added.
Port the core gameplay pulse — bit-exact Mitchell-Moore RNG, combat engine, skill/spell dispatch, pulse handlers — and wire UpdateHandler into the net game loop. Brings the world to life: mobs wander, fight back, regen, and die; PCs gain XP, level up, and can cast/heal/strike. - rng.go: number_mm, number_range/percent/door/bits/fuzzy, dice (deterministic) - fight.go: violence_update, multi/one/mob_hit, damage, parry/dodge/shield, set/stop_fighting, update_pos, make_corpse, raw_kill, group_gain, xp_compute, dam_message, plus do_kill/murder/flee/rescue/bash/berserk/ backstab/dirt/trip/kick/disarm/surrender/slay - update.go: advance_level, gain_exp, hit/mana/move_gain, gain_condition, mobile_update, weather_update, char_update, obj_update, aggr_update, update_handler; wired into net pulse - skills.go: skill_lookup, find_spell, check_improve, group_add/remove, InitGsns for boot-time GSN binding - magic.go: do_cast, mana_cost, say_spell, saves_dispel, check_dispel, obj_cast_spell, BindSpells; representative spell impls (acid blast, magic missile, harm, cure light/critical, armor) - mob_prog.go: trigger callbacks (percent/hpcnt/random/act) - handler.go: affect_to_char/remove/strip/join/find, check_immune, get_skill, get_weapon_sn/skill, get_hitroll/damroll/AC, interpolate, extract_char/obj, create_mobile/object/money, Wiznet, IsFriend, IsSameClan - areafile.World: ObjList, TimeInfo, WeatherInfo, CurrentTime fields - 25 new tests; full suite green; gofumpt + lint clean on new files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ity infra Closes the four follow-up items left open by 6a8cceb. M5 is now actually M5 — not "framework only." - spells.go: implement all 92 remaining spells (damage, healing, buffs, debuffs, detect, dispel, breath weapons, utility). BindAllSpells() wires every SPELL_FUN slot in SkillTable at boot. - mob_prog.go: full interpreter rewrite — keywordLookup/numEval, cmdEval (all 52 CHK_*), expandArg ($-code substitution), programFlow (if/and/or/ else/endif), and trigger entry points (percent, hpcnt, random, act, greet, give, bribe, exit). - mob_cmds.go: mob_* command handlers (asound, gecho, zecho, kill, assist, junk, echo, echoaround, echoat, mload, oload, purge, goto, at, transfer, force, call, flee, remember, forget, delay, cancel, remove, damage). - reset.go: full ResetArea/ResetRoom — M/O/P/G/E/D/R reset commands, door-lock restoration, shop inventory level rules, pet-shop adjacency, maze exit randomization. - cmd/romd/main.go: --seed flag for deterministic parity runs; InitGsns()/BindAllSpells() at boot. - test/parity/README.md + combat.txt: documents the -DOLD_RAND C oracle rebuild and seeded diff workflow. - world/gsn.go: add GsnHaste, GsnSlow. - interp.go: atoi accepts a leading sign. Smoke-tested: server boots, loads 3126/986/1266, accepts a connection and walks nanny() through race/class selection. go test ./... all green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes all four follow-up items called out in the audit. The Go server and the C oracle (rebuilt with -DOLD_RAND) now produce byte-identical output when seeded with the same Mitchell-Moore value. Verified by a real go-test that runs both binaries and diffs the transcripts. DoPractice + DoSteal (act_info/skills.c and act_obj.c): - skills.go: DoPractice — list learned skills, or train one at a trainer mob, with int-app/skill-rating gain formula matching C. - act_obj.go: DoSteal — pickpocket coins or items, thief-flag on failure against PCs, multi_hit attack on failed steal from NPCs. 17 message-only spells now ported faithfully (spells.go): - create_food/rose/spring, continual_light, floating_disc — CreateObject from world vnums, set timers and values. - gate, portal, nexus — full eligibility check + portal-object spawn; warp-stone consumption for non-immortals. - summon, teleport — yank victim to caster (or random room) with the full no-summon / safe-room / save-throw gauntlet. - locate_object — walk w.ObjList, paginate output. - enchant_weapon, enchant_armor — random +hit/+dam or +ac affect with failure cascade. - recharge — wand/staff value[1]/[2] math with chance/explode tiers. - control_weather — adjust w.WeatherInfo.Change. - ventriloquate — fake-say with saves-throw. - farsight — dispatch to do_scan. - TargetName + SetSpellCastWorld globals thread free-form target name and world pointer to spells without changing world.SpellFun signature. Parity verification (test/parity/diff/parity_test.go): - src/Makefile: -DOLD_RAND added to C_FLAGS. - src/db.c: parity_seed global; init_mm honours it before falling back to current_time. - src/comm.c: --seed N flag, parsed before the port arg. - cmd/romd/main.go: --seed N, InitGsns + BindAllSpells + SetSpellCastWorld wired at boot. - test/parity/diff/parity_test.go: launches both servers on free ports with the same seed, plays scripts/combat.txt against each, diffs the captured output. PASS today. - test/parity/scripts/combat.txt: full create→pick weapon→IMOTD→MOTD path. Three real parity bugs found and fixed along the way: - nanny.go GetName: capitalize() lowercased the rest of the name; C only uppercases the first byte. - nanny.go listWeapons + GroupAdd call site: was stubbed to a fixed 4-weapon list and the wrong package's GroupAdd. Now iterates world.WeaponTable like C, and calls game.GroupAdd which actually grants skills. - nanny.go ConPickWeapon: was sending IMOTD-then-MOTD via one "\n\r"; C writes "\n\r" twice (echo margin + pre-MOTD separator) before MOTD. - nanny.go sendHelp: trust-gates help entries the way C do_help does, so the imotd entry doesn't leak to non-immortals. 80 in-process tests + 1 cross-binary parity test all green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the M5 polish gap. All ~60 admin/immortal commands from act_wiz.c are now real implementations; only persistence (M6) and OLC (M6) entry points remain stubbed. act_wiz.go (1700+ lines): - Echo family: echo, recho, zecho, pecho, bamfin, bamfout - Movement: goto, at, transfer, return - Power: snoop, switch, force (with all/players targets) - Toggles: holylight, invis (with explicit level), incognito, wizlock, newlock, peace - Comm mutes: nochannels, noemote, noshout, notell - PLR-flag toggles: freeze, log, deny, disconnect, violate, protect, pardon - Restoration: restore (target/room/all), purge (mob/obj/room) - Creation: load (mob/obj), clone (mob/obj) - Admin actions: advance, trust, string, set (mob/obj/room/skill subset) - Info: stat (room/char/obj), vnum, mwhere, owhere, sockets, memory - Game control: shutdow/shutdown, reboo/reboot (sets ShutdownRequested) - Wiznet: toggle on/off, status, show, named-channel toggle - Helpers: prefi/prefix, slookup, channels, fun, dump - Mob program inspection: mpdump, mpstat ban.go (210 lines): - BanList global + CheckBan(host, type) with prefix/suffix wildcard - DoBan / DoPermban — banSite() helper handles `*` wildcards - DoAllow — lift bans (trust-gated) - DoFlag — set/clear PlrKiller/PlrThief on a victim ShutdownRequested global lets the net loop respond to /shutdown. WizlockActive/NewlockActive globals tell nanny() to refuse logins. 20 new tests in act_wiz_test.go covering toggles, wiznet, mset, restore, ban+allow round trip, trust, prefix, and force-syntax checks. The 17 remaining stubs in stubs.go are *all* M6 territory (5 persistence + 10 OLC). stubs.go reorganized to make that scope explicit. go build clean; go test ./... all green (parity diff still byte-identical against the C oracle). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces ROM's flat player/<Name> text files with a transactional SQLite store. New characters created via nanny() now save on first entry; returning characters load by name + bcrypt password and re-spawn their inventory from world prototypes. internal/persist/ - store.go: *Store wrapping sql.DB, schema embedded via go:embed. - schema.sql: 9 tables — characters, character_stats, character_skills, character_groups, character_inventory (with parent_id for nested containers), character_affects, character_conditions, boards, notes. - character.go: LoadCharacter returns *LoadedCharacter (char + room vnum + saved inventory rows). Authenticate, SetPassword, HashPassword, CheckBcrypt, CharacterExists, DeleteCharacter. - saver.go: Single-transaction SaveCharacter walks every sub-table. Nested-container inventory walk preserves parent_id linkage so loads rebuild the same tree. - character_test.go: 10 tests — round-trip, overwrite, not-found, case-insensitive lookup, delete, auth, set-password, affect round trip. internal/game/persist.go: - PersistStore global + SetPersistStore() wired at boot. - LoadChar(name, w) — calls persist.LoadCharacter and rehydrates ObjData from world prototypes using CreateObject (M5). - Autosave() — runs from CharUpdate every tick on a 30-character rotation (matches C's save_number). - DoSave / DoQuit / DoQui / DoRent / DoDelet / DoDelete / DoPassword — real implementations replacing the M5 stubs. internal/net/nanny.go: - ConGetName tries game.LoadChar; on hit, sets d.savedRoomVnum and jumps to ConGetOldPassword. Returning characters land in their saved room (falls back to Temple if vnum is no longer in the world). - New characters get saved at the end of ConReadMOTD (Level==0 branch). - Descriptor.savedRoomVnum field carries the saved location across nanny states without touching CharData. cmd/romd/main.go: - --db flag (default `player.db`), opens persist.Store at boot, sets it via game.SetPersistStore, defers Close at shutdown. go.mod: adds modernc.org/sqlite (pure-Go driver — no cgo). Smoke-tested: create new char `Sixchar`, save, quit, then a second process loads from the same DB, MOTD-flow runs, and `Sixchar has connected.` (not "new player") confirms reload. 20 character_skills rows + warrior class persisted across the restart. Remaining M6: OLC (10 stubs in stubs.go) and the noteboard migration from board.c into the existing boards/notes tables. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Online-building system ported from src/olc.c / olc_act.c / olc_save.c / hedit.c. Builders can edit rooms, mobs, objects, areas, mob programs, and help entries live, then `asave` writes everything back to .are files. M6 round-trip property proven by go-test. internal/game/ - olc.go: framework + entry points. RunOlcEditor() consumes one input line for the active editor; net/loop.go now routes through it when d.Editor != EdNone. DoOlc/DoAlist/DoResets/DoAsave + DoRedit/DoMedit/ DoOedit/DoAedit/DoMpedit/DoHedit set d.Editor and d.PEdit. - olc_redit.go: room editor — name/desc/sector/flags/heal/mana/clan/ north/south/east/west/up/down/show/create. - olc_other.go: medit/oedit/aedit/mpedit/hedit command tables and handlers (init() to break Go init cycles between tables and the show-help helpers that print their own table names). - olc_save.go: SaveArea() emits the canonical .are format: #AREADATA / #HELPS / #MOBILES (new format with race, dice on a single combined level/hitroll/hit-dice/mana-dice/dam-dice line, AC values pre-divided by 10) / #OBJECTS / #ROOMS (D-blocks per exit + H/M/C/O modifiers + S terminator) / #RESETS / #SHOPS / #SPECIALS / #MOBPROGS / #$. encodeFlag() turns bitmasks back into letter codes (A=1, B=2, ..., aa=2^26, ...). internal/world/const.go: EdNone/EdArea/EdRoom/EdObject/EdMobile/EdMpCode/ EdHelp enum. internal/net/loop.go: when a connected player's d.Editor != EdNone, input lines go to game.RunOlcEditor() instead of Interpret(). stubs.go: scope shrunk to just the do_function alias comment — no stubs remain. Tests (internal/game/olc_save_test.go + olc_save_extra_test.go): - TestSaveAreaRoundTrip: pick the smallest non-empty loaded area (Limbo, 2 rooms + 19 objs), SaveArea() into a temp dir, reload from that dir, verify room/mob/obj counts match. PASS. - TestSaveAreaRoundTripMultiple: same property on midgaard.are (largest stock area), haon.are, shire.are. All PASS. No remaining stubs in stubs.go. go build clean; go test ./... all green; parity diff still byte-clean against the C oracle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Go rewrite
Closes the deployment milestone. The same nanny/interpret pipeline that
drives raw telnet on :4000 now also drives a binary WebSocket on :8080,
fronted by an xterm.js page in web/. CloudFormation, systemd units, and
an nginx config in infra/ provision a single t4g.small EC2 with persistent
EBS for the SQLite DB + area files, and an hourly VACUUM INTO snapshot
that uploads to S3.
internal/net/
- ws.go: wsConn adapts a *websocket.Conn (github.com/coder/websocket) to
net.Conn. Reads stitch successive WS messages into a byte stream so
the existing extractLine() pulls \n-terminated input. Writes go out as
one binary frame per call so raw IAC bytes (0xFF...) and ANSI escapes
pass through verbatim — the browser client filters echo-toggle
client-side, mirroring how a real telnet client does it natively.
ListenHTTP() serves the static client at /, exposes /healthz, and
upgrades /ws. X-Forwarded-For is honored so wiznet shows the real IP
when nginx fronts.
- ws_test.go: real-network test — dials ws:// against a ListenHTTP server,
reads the ANSI prompt. Plus a static-file test using a temp webDir.
- ws_drive_test.go: drives the nanny FSM over WebSocket through ANSI →
Name → "Did I get that right" so the full input/output round trip is
exercised, not just the upgrade.
- loop.go: new httpServer field; Shutdown() also calls its Shutdown().
cmd/romd/main.go: new --http-port (default 8080) and --web (default web)
flags; --port 0 disables telnet so the binary can run pure-WebSocket on
PaaS-style hosts; both listeners off is rejected.
web/
- index.html: xterm.js 5.3.0 + addon-fit from jsdelivr, status chip.
- client.js: local line-buffered echo, IAC scanner that toggles echo on
FF FB 01 / FF FC 01 (renders '*' instead of suppressing during
password entry).
cmd/rom24-backup/
- main.go: VACUUM INTO a temp file, gzip, PutObject via aws-sdk-go-v2.
- backup_test.go: round-trips a 1-row SQLite DB through VACUUM and the
gzip helper.
infra/
- cloudformation.yaml: SG (22/80/443/4000), gp3 EBS data volume (retained
on delete), S3 bucket with 30-day expiration + versioning, IAM
instance profile with PutObject scoped to the bucket, t4g.small Ubuntu
24.04 ARM64 via SSM AMI parameter. User-data formats and mounts the
data volume on first boot and creates the rom user.
- systemd/rom24.service: hardened (NoNewPrivileges, ProtectSystem=strict,
ReadWritePaths=/opt/rom24/data). Restart=always replaces ROM copyover.
- systemd/rom24-backup.{service,timer}: hourly snapshot via OnCalendar.
EnvironmentFile drops BACKUP_BUCKET in from /etc/rom24/backup.env.
- nginx.conf: TLS termination + /ws proxy with proxy_buffering off and
1h read/send timeouts; / proxies to the Go server (which serves the
static client and /healthz).
- deploy.sh: build linux/arm64, rsync binaries + area/ + web/ + units +
nginx config, install over SSH, daemon-reload + restart.
- README.md: first-time stack create, deploy, certbot, restore-from-S3
drill, and local-dev quickstart.
go.mod: + github.com/coder/websocket v1.8.14, github.com/aws/aws-sdk-go-v2
(config + service/s3) for the backup binary.
.gitignore: /dist, /player.db{,-shm,-wal}.
Local verification: go test ./... — all green including the new
ws_drive_test (real WebSocket dial against ListenHTTP, walks the nanny
states) and backup_test (VACUUM round-trip). Parity diff against the C
oracle still byte-clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EC2 access moves to AWS Systems Manager Session Manager — no SSH key, no port 22 in the security group, no public keypair to rotate. The deploy flow is now S3 staging + ssm send-command, which works against the SSM agent that ships in the Canonical Ubuntu 24.04 ARM AMI. infra/cloudformation.yaml - Drop KeyName + AllowedSSHCidr parameters and the port-22 SG rule. - Add required HostedZoneId parameter + an AWS::Route53::RecordSet that maps DomainName at the instance public IP (300s TTL). - Bucket lifecycle splits into snapshots/ (30 days) and deploy/ (14 days) so the staging artifacts are reaped automatically. - User-data: install certbot + python3-certbot-nginx; ensure the SSM agent snap is started; write /etc/rom24/backup.env with BACKUP_BUCKET pre-filled from !Ref BackupBucket so the timer works on first start. infra/install.sh (new) - Runs on the host via SSM. Pulls the bundle from S3, installs binaries + units + nginx config. If no Let's Encrypt cert exists yet, lays down an HTTP-only bootstrap nginx config so port 80 + /.well-known are reachable; once the cert exists, the bundle's full TLS config takes over. Idempotent. infra/deploy.sh - Rewritten. Reads InstanceId / BackupBucketName / DomainName from stack outputs. Builds linux/arm64, assembles a bundle, uploads bundle + install.sh to s3://.../deploy/, fires AWS-RunShellScript with a 4-command parameter block via file:// to dodge JSON escaping, waits for command-executed, prints status + stderr + stdout. infra/certbot.sh (new) - One-shot Let's Encrypt issuance via SSM. Takes an admin email, runs `certbot --nginx --non-interactive --agree-tos --redirect`. infra/README.md - Rewritten for the SSM flow: stack create → deploy.sh → certbot.sh → deploy.sh. Restore-from-S3 drill now uses SSM RunCommand too. `aws cloudformation validate-template` passes against the new template. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pkg) First deploy failed: Ubuntu 24.04 noble removed the v1 `awscli` package from the apt repo (E: Package 'awscli' has no installation candidate), which made user-data's apt-get install bail before nginx + certbot were installed. User-data now pulls the official v2 installer for arm64, unpacks it, and runs --install --update. Verified end-to-end on the running stack: nginx + cert + Go binary + telnet all green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ROM 2.4 → Go rewrite (M0–M7): browser + telnet + AWS deploy
Once a player reached ConPlaying the server emitted "\n\r> " on every pulse (4 Hz) because flushOutputs called ProcessOutput unconditionally and ProcessOutput prepended the prompt whenever d.Connected==ConPlaying. An idle browser saw a column of '>' characters scroll forever. The C oracle's game_loop_unix wraps process_output in `if (d->fcommand || d->outtop > 0)` — only flush when something happened. Match that gate exactly in flushOutputs(). Regression coverage: - TestIdlePulseDoesNotSpamPrompt — 5 idle pulses produce zero bytes on the wire. - TestPromptFiresAfterCommand — a buffered command response still flushes with the trailing "> " prompt attached. Full suite (parity diff included) still green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The look handler in act_info.go wraps room titles in {s ... {x and
relies on SendToChar to translate them. SendToChar passed the bytes
through verbatim — the comment claimed "matching C send_to_char" but
that was wrong: QuickMUD's Lope-colour patch translates at the
send-to-char level, and Act() already does the conversion itself, so
the room title was the only path leaking raw codes.
Fix is one line — call ColourConv before WriteToBuffer. Reset code
{x and player-configurable codes ({s/{S/etc.) now resolve to ANSI
escapes when the descriptor accepted ANSI, and are stripped cleanly
when it didn't.
Three regression tests in internal/world/output_test.go cover ANSI on,
ANSI off, and the literal `{{` escape.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… chars
The Go ProcessOutput was emitting a hard-coded "> " for every in-world
descriptor, so the player's configured `prompt <%hhp %mm %vmv>` was
ignored — the C oracle calls bust_a_prompt() to substitute %h, %H, %m,
%M, %v, %V, %x, %X, %g, %s, %a, %r, %R, %z, %e, %c, %% codes with
their dynamic values.
internal/game/prompt.go
- BustAPrompt() — full port of comm.c's bust_a_prompt. Empty Prompt
falls back to the C default `{[<%dhp %dm %dmv>{x` (colour codes
flow through ColourConv via SendToChar). COMM_AFK short-circuits
to a fixed "<AFK> " banner. promptExits() handles %e using the
short NESWUD letters, skipping closed/missing exits.
internal/net/output.go
- ProcessOutput now: prepends "\n\r" only when COMM_COMPACT is unset,
emits the prompt via game.BustAPrompt when COMM_PROMPT is set, and
appends a telnet GA when COMM_TELNETGA is set — matching the C
oracle's order in process_output.
internal/net/nanny.go
- New characters at ConReadMOTD now get COMM_COMBINE|COMM_PROMPT, as
the C nanny does. Without this default the prompt feature was off
for every freshly-created player.
internal/game/persist.go
- Backfill the same defaults on LoadChar when the saved Comm is 0,
so characters created before this fix still get their prompt the
first time they log back in.
Tests
- internal/game/prompt_test.go — default prompt, custom code expansion
(%h/%m/%v/%x/%g/%s/%a), AFK short-circuit, literal %%.
- internal/net/prompt_test.go — TestPromptFiresAfterCommand now
asserts the busted prompt's "%dhp %dmv" suffix instead of the old
hard-coded "> ".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A fresh character logged in with 10/10 hit, 0/0 mana, 0/0 move because newChar() in descriptor.go only set Position/Act/Comm — the C oracle's new_char() in db.c:2548 also seeds: hit = max_hit = 20 mana = max_mana = 100 move = max_move = 100 armor[0..3] = 100 perm_stat[Str..Con] = 13 Without that seed the nanny's "ch.Hit = ch.MaxHit" copies were reading zeros, and the 10/10 the user saw was a fallback I'd added when the real fix was to match the C allocator. Dropped that fallback too since MaxHit is now always populated before ConReadMOTD runs. Regression coverage: TestNewChar_SeedsBaselineVitals. Full suite green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old `score` was bare line-by-line text. Reworked into a banner +
sectioned card with:
- Bordered header showing Name, title, level, age, hours played.
- Race / Class / Sex on one column-aligned line.
- HP / Mana / Move as 20-char ASCII bars; HP colour shifts from bright
green > dim green > yellow > red at 75/50/25 thresholds; Mana is
blue, Move is cyan.
- Stats with per-stat colour: green when modified above perm, red when
below, white otherwise.
- Currency + XP + to-level on one row (to-level hidden at hero).
- Carry / weight / practices / training / wimpy on one row.
- Hitroll/Damroll at level 15+, raw armor numbers at level 25+ (matches
the C oracle's reveal gates).
- Defences (the narrative AC strings) regrouped under a single header
with the damage type coloured by severity.
- Alignment shows the numeric value at level 10+ and a coloured word
always (gold for good, grey for neutral, red for evil).
- Position word coloured by severity (red for DEAD / mortally wounded,
white for standing, etc.).
- Status line lists drunk/thirsty/hungry only when relevant.
- Immortal-only Holy Light / Invis / Incog reveal preserved.
Helpers added (vitalBar / manaBar / moveBar / drawBar / statColour /
alignDesc / positionWord / acDesc / padBetween / visibleLen).
`padBetween` strips `{X` codes when computing widths so the header
border stays aligned after ColourConv.
Tests in internal/game/score_test.go:
- TestDoScore_RendersWithAnsi — no raw {X codes leak; ANSI reset present;
every section label is present.
- TestDoScore_StripsWhenAnsiOff — Ansi=false produces pure plain text.
- TestVitalBar_ColourThresholds — 100%→{G, 60%→{g, 40%→{Y, 20%→{R, 0%→{R.
- TestPadBetween_AlignsByVisibleWidth — colour codes don't break
width math.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DoOutfit was a stub printing "Outfit is not yet fully implemented" and the nanny wasn't calling it at all, so brand-new players woke up in the school naked. The C oracle (act_wiz.c:251 + nanny.c:782) hands out a banner light, vest, best-skilled weapon, shield (when not two-handed), and a school map. internal/game/act_obj.go - OutfitNewChar() — the do_outfit core, exported so net can call it without printing the "Find it yourself!" gate. Walks WeaponTable picking the weapon the character has the highest learned percent in; defaults to sword for fresh characters with 0 across the board. Checks the wielded weapon's Value[4] for WeaponTwoHands before granting a shield. Each give() respects already-equipped slots so re-running outfit on a partially-stocked character only fills gaps (matches C's get_eq_char-null guard). - DoOutfit() now defers to OutfitNewChar after the level/NPC gate and prints "You have been equipped by Mota." on success. internal/net/nanny.go - After placing the new char in the school, call game.OutfitNewChar then ObjToChar(create OBJ_VNUM_MAP) — the two lines that C nanny.c runs right after the new-char init block. Tests - TestOutfitNewChar_EquipsKit loads the real area/ tree (the school vnums live in school.are) and verifies all four slots are filled with the correct prototype vnums. - TestDoOutfit_RejectsAboveLevel5 confirms the level-gate still works and prints the canonical "Find it yourself!" message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Twenty handlers that had been printing "not yet implemented" since M5/M6
are now real ports of their C oracle counterparts. Audit of the tree
afterwards found seven additional gaps (covered below).
Shop system (shop.go) — ports find_keeper, get_cost, get_obj_keeper,
obj_to_keeper, deduct_cost, can_drop_obj, get_obj_number from
act_obj.c. Commands: DoList, DoBuy (incl. pet-shop branch with pet
clone, mob naming, follower attach), DoSell, DoValue. Haggle skill
applied on both directions, ITEM_INVENTORY restocks via CreateObject,
SELL_EXTRACT auto-destruct, level/carry/affordability gates.
Healer (healer.go) — ports do_heal from healer.c. 10-spell menu
matching the C list; cost in copper (silver units); mana branch is
inline (no spell function). Skill_table dispatch via SpellFun.
Magic items (magic_items.go) — ports do_pour, do_envenom, do_recite,
do_brandish, do_zap from act_obj.c. Pour handles liquid transfer,
fountain pour, "out" spill, container-to-container with liquid mix
guard. Envenom respects bless/burnproof, weapon flag exclusivity
(flaming/frost/vampiric/sharp/vorpal/shocking), DAM_BASH skip, and
attaches AffectData with WEAPON_POISON. Recite/Brandish/Zap consume
their charge/use, gate on level + skill, and dispatch via
ObjCastSpell. Brandish iterates room people by target-type
(TAR_IGNORE / OFFENSIVE / DEFENSIVE / SELF). Also rewrote DoQuaff so
potions actually cast their three embedded spells.
Info displays (info_displays.go) — ports do_skills, do_spells,
do_groups, do_compare from skills.c / act_info.c, and do_scan from
scan.c. Columns + level-bucket layout match the C; argument parsing
honours "all" + 1-or-2 numeric levels; scan walks up to depth 3
through a single exit and uses the same "right here / nearby / not
far / off in the distance" phrasing.
Noteboards (board.go + persist/notes.go + pose_table.go) — full
SQLite-backed port of board.c. Five seed boards (General / Personal /
Imm / Announce / Quest). DoNote dispatches read/list/write/remove/
catchup; multi-step composition flow (ConNoteTo → Subject → Expire →
Text → Finish) wired into the nanny via NoteHandleInput. Recipient
matching honours sender/all/imm/imps/level-number/name. Notes purge
on lifecycle hook (PurgeExpiredNotes ready for an area-pulse caller).
DoBoard lists boards with unread counts + access; DoReplay flushes
the new PCData.ReplayBuffer field. DoPlay handles jukebox lookup +
song selection (world.SongTable is empty by default — no audio assets
bundled; admins can populate it).
Socials (extra_cmds.go) — DoPose ports the 17 x 4-class pose table
(internal/world/pose_table.go); DoGrats ports do_grats with toggle +
broadcast respecting NoGrats/Quiet/NoChannels; DoAlias + DoUnalias
manage the existing PCData.Alias[]/.AliasSub[] arrays and add a
SubstituteAlias helper for use by the interpreter; DoSplit divides
gold + silver among in-room group members; DoGain ports skills.c
do_gain (list / convert / points / group / skill branches); DoGuild
ports act_wiz.c do_guild (clan assignment, "none" clears); DoTypo +
DoBug write timestamped append-only reports to /opt/rom24/data/;
DoMob is the security wrapper that gates the mob-only interpreter.
Clan channel (act_comm.go) — DoClan was a stub; now ports do_clantalk
exactly (toggle + clan-mate broadcast, independent-clan reject).
Character creation gen_groups (gen_groups.go + nanny.go) — ports
parse_gen_groups, list_group_costs, list_group_chosen from skills.c.
The nanny's ConDefaultChoice="N" branch now drops into the
purchase loop; "done" enforces the >= 40-point minimum, sets train,
and advances to weapon pick. Previously skipped entirely.
Audit sweep gaps closed:
- world.GroupAdd dead stub deleted (game.GroupAdd is the real impl).
- act_move.go had module-local stubs for hasTrigger / mpExitTrigger /
mpPercentTrigger / mpGreetTrigger. Replaced by real calls into
mob_prog.go (HAS_TRIGGER expanded inline against PIndexData.MprogFlags).
- fight.go disarm: NPCs now scoop the dropped weapon back up when
wait==0 and they can see it (port of the missing get_obj branch).
- areafile/loader.go dam_type for converted mobs: was hard-coded to
pierce because "RNG not ported yet". Now hashes the mob vnum to
deterministically pick from {hit, grep, pierce} — matches the
C number_range(0,2) intent and is reproducible across boots.
- act_info.go DoNofollow: was leaving Master attached. Now calls
the real stopFollowing().
- nanny ConGetOldPassword: was logging "M3 stub: checkPlaying /
checkReconnect always false". Now reconnects an existing in-world
ghost via tryReconnect (port of comm.c check_reconnect): detaches
the dead descriptor, transfers the live char + room context to the
new descriptor, and prints "Reconnecting. Type replay to see
missed tells."
New struct fields:
- world.PCData.ReplayBuffer string — accumulates tells for DoReplay.
- world.SongTable / world.SongData — placeholder for jukebox audio.
Boot: cmd/romd/main.go now calls game.RefreshBoards() so the SQLite
note tables are seeded with the default board set on first run.
Tests: full suite green including 32 s parity diff against the C
oracle. No regressions in net (real-WebSocket nanny flow still
walks ANSI → Name → Confirm).
This commit is the M5/M6 follow-up the project plan called out but
never delivered: every Do* in interp.go is now backed by a real
implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the last three "deliberate divergences" from the M5/M6 audit: do_copyover, do_qmconfig, and the bug + clantalk dispatch entries. internal/game/copyover.go - DoCopyover hands off to a hook the net package registers at boot. No hook (e.g. unit tests) → "Copyover is not available" + return. - DoQmconfig — full port of act_wiz.c:4685. Toggles AnsiPrompt / AnsiColor / TelnetGA globals stored in game.QMConfig, supports "show" + "read" subcommands. Settings are runtime-only in this build (no qmconfig.rc file); "read" reports that. internal/net/copyover.go - Server.Copyover() — port of act_wiz.c:4498. Save every PC, dup each connected *net.TCPConn so the FD survives execve (File() clears CLOEXEC), dump (fd, name, host) tuples to /opt/rom24/data/copyover.dat, also dup the listener + HTTP listener so both ports stay bound across the exec, then syscall.Exec the current binary with --copyover-recover / --copyover-listen-fd / --copyover-http-fd flags. WebSocket descriptors can't survive an exec (the protocol state lives in coder/websocket.Conn user-space objects, not the kernel) — they get a "rebooting, reconnect" banner and a close; web/client.js handles the reconnect. - Server.AdoptListener / AdoptHTTPListener — wrap inherited FDs via net.FileListener so the new process picks up the same bound ports without a TIME_WAIT gap. - Server.RecoverFromCopyover — read the state file, for each entry rebuild a net.Conn via os.NewFile + net.FileConn, load the char by name from SQLite, place them back in their saved room, and start the read goroutine. Materialises with "$n materializes!" to anyone else in the room. - Refactored ws.ListenHTTP to record the listener on the Server so Copyover can dup it. serveHTTPOnListener is the adopt path that takes an existing listener. cmd/romd/main.go - Three new flags: --copyover-recover, --copyover-listen-fd, --copyover-http-fd. When set, skip net.Listen and adopt the inherited FDs instead. game.SetCopyoverHook wired right after NewServer so do_copyover dispatch works as soon as the server is created. web/client.js - Auto-reconnect with exponential backoff (500ms → 8s cap). On ws.onclose (server restart, copyover dropping the WS side, network blip), shows "reconnecting…" status, clears the line buffer + echo flag, dials a fresh socket. beforeunload sets intentionalClose so closing the tab doesn't try to reconnect. - wsSend() guards against send-on-null / send-on-closed. internal/game/interp.go - New dispatch rows: copyover, qmconfig (immortal-only, POS_DEAD). - New rows: bug → DoBug, clantalk → DoClan. C interp.c has bug commented out and only ships "clan" for the clan-channel function, but the user explicitly asked for both to be reachable by name; the handlers were already implemented. Tests - internal/game/copyover_test.go covers QMConfig show / no-arg usage / ansicolor toggle and the no-hook DoCopyover path. Full suite green including the 32 s C-oracle parity diff. Behavioural notes - copyover preserves: any player connected via raw telnet (port 4000). - copyover drops + auto-reconnects: any player connected via the browser (https://...). They miss < 2 seconds of game time on a copyover-driven deploy; in-game session state (character, room, affects, equipment) is all preserved via the autosave + LoadChar path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
First subcommand: 'outfit <name>' — runs the same OutfitNewChar code the nanny does at character creation, then saves. Lets you retroactively equip the school kit on a character whose creation predated the OutfitNewChar wiring. Used today to give Xajkil his banner / vest / shield / sword (his character was made before the do_outfit port; the rest of the player table predated proper baseline vitals too — that one's already fixed via newChar()). Run via SSM with rom24.service stopped (no WAL contention). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old equipment output listed only worn slots and printed "Nothing." when
the player wore zero items, which made it hard to see what slots even
exist. Reworked to match the DoScore card style:
- Yellow-bordered header banner with the "Equipment" title.
- All 19 wear slots from whereName are shown every time, in order.
- Each slot label in cyan; the worn item (if any) follows on the same
line via formatObjToChar.
- Empty slots show "{D<nothing>{x" (dim grey) so the player can see at
a glance which slots are open.
- (Invis)/(Red Aura)/(Blue Aura)/(Magical)/(Glowing)/(Humming)
decorations from formatObjToChar still apply.
- Footer line summarising "Worn: N of MaxWear slots".
Tests in equipment_test.go:
- TestDoEquipment_ShowsAllSlots — every slot label present;
<nothing> count equals MaxWear (19) when the char wears nothing.
- TestDoEquipment_ShowsWornItems — equipping a sword reduces the
<nothing> count by one and the sword's short_descr appears; no
raw {X colour codes leak after ColourConv.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pressing Enter on an empty line re-runs the previous command, echoing it to the terminal so the player sees what got submitted. Suppressed during password entry (when the server has sent IAC WILL ECHO and localEcho is false) so a stray Enter on the password prompt doesn't dump the password back out as a command. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds shell-style command recall to the browser client. - Up arrow walks back through the last 100 commands. - Down arrow walks forward; reaching the bottom restores whatever the user was typing before they hit Up (savedLine). - Consecutive duplicates are dedup'd so spamming Enter on a repeated command doesn't bury the rest of the history. - Empty Enter repeats history[-1] (existing behaviour preserved). - History capture is suppressed during password entry — when the server has sent IAC WILL ECHO and localEcho is false — so passwords never enter the history array. - Ctrl-C clears the current line AND resets historyIdx to the bottom. - Other CSI escapes (left/right arrow, home, end, etc.) are dropped cleanly so they don't pollute lineBuf with stray ESC [ X bytes. The visible line is redrawn by writing \b \b lineBuf.length times to erase, then writing the recalled string. Password mode renders '*' characters for the replacement too, matching the normal input path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two small fixes that go together. web/client.js - Accept BOTH the normal cursor-key form (ESC [ A / ESC [ B) AND the application cursor-key form (ESC O A / ESC O B) for up/down arrows. Some xterm.js setups end up in application mode depending on the terminal capabilities the server claims, and the first ship only honoured the normal form. - Expose history / historyIdx / lineBuf on window.__rom24 for in- browser debugging (open DevTools console: __rom24.history). internal/net/ws.go - Wrap http.FileServer with noCacheStatic so every response for the browser client (index.html, client.js) gets Cache-Control: no-store + Pragma: no-cache + Expires: 0. Without this, browsers will keep an old client.js cached for hours after a deploy, which is how the up-arrow history fix "didn't seem to work at all" the first time it shipped — the browser was still running yesterday's JS. Applies to both the live-Listen path and the copyover-adopted path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fleeing combat dropped the player into the DEAD position despite full HP because StopFighting assigned ch.Position = ch.DefaultPos for every char, and DefaultPos is zero-initialised on PCs (== PosDead). The visible symptom was "You flee from combat!" followed by every command returning "Lie still; you are DEAD." with the prompt still showing healthy HP. C ROM fight.c stop_fighting branches on IS_NPC: NPCs return to their configured default_pos, PCs always return to POS_STANDING. Match that exactly. Regression coverage: - TestStopFighting_PCResetsToStanding — PC with DefaultPos=PosDead ends up PosStanding (the failing case the user hit). - TestStopFighting_NPCKeepsDefaultPos — NPC with DefaultPos=PosResting still wakes up resting, confirming the NPC branch is intact. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Room 3700 (Entrance) south shortcut pointed to 3744 (North Wall of Arena) instead of 3722 (South Wall), dropping players in the wrong part of the arena. Fixed destination to 3722. Room 3722 (South Wall of Arena) had no south exit back to 3721 (End of Mud School), making the arena entrance one-way. Added D2→3721 so players can return to the school corridor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Combat additions: - Dual wield (skill): off-hand secondary weapon slot with 25% damage reduction; weight must be <= 1/2 primary; conflicts with shield/hold. Adds WearSecondary slot, GsnDualWield, DoSecond command. - Critical strike (skill): when weapon skill is 100%, ~25% chance for a 1.4x damage hit; messages color the weapon name and target. - Circle (skill): backstab variant usable mid-fight (thieves 15). - PK counters: PkKills/PkDeaths on PCData, incremented in RawKill, shown on score sheet, persisted via two new SQLite columns. Inventory: - get/put/drop/give support N.item syntax (get 3.cigar, put 2.bread bag). - DoPut handles in/on prepositions, weight checks, pit timers, CONT_PUT_ON. - DoDrop adds coin dropping with floor consolidation and ITEM_MELT_DROP. - DoGive adds coin transfer with changer NPC support. - DoGet adds 'from' preposition strip and pit-greedy guard. - MultArgument now accepts '.' separator alongside legacy '*'. Misc: - Hidden items: ItemHidden extra flag, hide <item> stash, DoSearch reveal. - Scan: now blocked by closed exits.
Three latent bugs that were silently breaking skill rolls: 1. internal/game/act_move.go: the lowercase getSkill / numberPercent / checkImprove / gainExp / stopFighting / affectStrip / affectToChar were stubs returning 0 / 50 / no-op. Every caller that took the lowercase path (sneak, hide, pick lock, peek, recall) silently used those broken returns: sneak always failed, hide always failed, pick lock always failed, etc. Replaced the stubs with thin delegates to the real GetSkill / NumberPercent / etc. in handler.go / rng.go / fight.go. DoRecall's stopFighting call now passes the World argument through. 2. internal/world/types.go: ExitData.ExitInfo bumped from int16 to int32. The existing exit-flag set (ExIsDoor..ExNoLock) sums to exactly 32767 — adding any further flag (ExWedge / ExWedgeproof / ExBashed are coming with the bank snippet port) would overflow the signed int16. 3. internal/game/reset.go: room reset no longer needs the `int16(pExit.RsFlags)` truncation cast — RsFlags is int32 and ExitInfo is now int32 too. No behavior change for the C-faithful path except that those skill checks now actually consult the skill % and the per-pulse RNG.
…n_loot + 42 tests Tier-2 mud/ snippet ports plus a catch-up test sweep for tier-1 features that shipped without coverage in ad96883. New systems: - bank.c: ROOM_BANK + ACT_BANKER NPCs; balance/deposit/withdraw in gold. EX_WEDGE / EX_WEDGEPROOF / EX_BASHED exit flags; wedge + pry skills (thief 12, warrior 15) jam/unjam closed doors on both sides. MoveChar and DoOpen now respect EX_WEDGE. Beep rings the bell character on a target across the world. Bank persists via new SQLite column with idempotent ALTER migration. - autosteal.txt: PLR_AUTOSTEAL toggle, runs in CharUpdate for level-11+ thieves with the steal skill. Race-bonus block dropped (we don't have Kender/Silvanesti/etc). Shared stealCheck mirrors Maslin's failure path including PlrThief flagging across the L7 PK gap. - hunt.c: GSN_HUNT tracking skill — BFS pathfinder over the room graph returns the first-step direction to a target. Pure Go map replaces the C custom hash table. Top trust crosses areas; lower trust is area-scoped via getCharArea. - fa_exp.c: XpCompute bonuses for victims with combat-significant powers (sanctuary +30%, haste +20%, area/backstab/fast +20%, dodge/parry +10%). - fa_drink.c potion-in-container: DoPour now accepts a potion as source and stores its vnum in Value[4], one dose per pour, no liquid mixing. DoDrink auto-quaffs stored potions through a new shared autoQuaff() helper that also handles alignment burn and level cap. - can_loot gate: CanLootCorpse() — immortals and owner always pass; online owner needs PlrCanLoot or a shared group; offline owner defaults to allow. Wired into DoGet's ItemCorpsePC branch. Tests (42 new): - dual_wield_test.go (11): DoSecond gates (skill, primary, shield, overweight); critical-strike requires 100% weapon mastery; DoCircle requires fighting + wield. - multi_item_test.go (11): multArg / MultArgument parsers (both separators), N.item paths through get/drop/put/give, coin consolidation on drop, RawKill PK counter rules. - hidden_test.go (6): CanSeeObj hides ItemHidden from normal viewers but shows to holy-light; DoHide stash path with NoDrop refusal; DoSearch reveal with and without skill. - bank_test.go (14): balance + deposit + withdraw round trip + caps; beep both-sides + NPC refusal; wedge/pry door state including far-side sync; wedged exit blocks both DoOpen and MoveChar. - hunt_test.go (8): adjacent + multi-hop BFS, branching pick, unreachable, sameAreaOnly gate, DoHunt no-arg + level + same-room. - testmain_test.go: runs InitGsns once per test binary so skills resolve against the right table indices. Bugs caught (and fixed) by the test sweep: 1. InitGsns was missing the seven new GSNs (dual wield, critical, circle, search, wedge, pry, hunt) — all references resolved to slot 0. Added bindings. 2. findHuntPath's sameAreaOnly check ran on the current node instead of the candidate neighbour, so cross-area paths leaked through. Moved the area check to the edge target.
Adds 20 more tier-2 tests, one file per snippet: - fa_exp_test.go (7): XpCompute baseline + each affect/offflag bonus individually (sanctuary/haste/area/backstab/fast/dodge/parry) and stacking. RNG seeded so the fuzz step is deterministic. - can_loot_test.go (7): CanLootCorpse for immortal, owner, offline owner, online owner default-deny, PlrCanLoot consent, same-group pass; plus an integration test that DoGet on a denied PC corpse hits the gate. - autosteal_test.go (6): stealCheck immortal-victim and immortal-attacker paths; DoAutosteal toggle round-trip + NPC no-op; Autosteal short-circuits in RoomSafe and across >20 level gap. - potion_test.go (7): DoPour stores potion vnum in Value[4]; refuses mixing two potions, liquid into potion, potion into liquid; pour-out clears Value[4]; autoQuaff enforces level cap and alignment burn. All 62 game-package tests pass (33 new this commit, 29 carried over).
Erwin S. Andreasen's auction system on top of the existing chat channel. One auction at a time; PulseAuction=10s drives the going-once → going-twice → SOLD state machine via UpdateHandler. Commands (subcommands shadow the chat channel): auction — show current item or toggle channel auction sell <item> — post your item to the block auction bid <amt> — bid; alias "bet" auction stop — immortal kill switch auction info|stats — current item + seller + bid auction <free text> — falls through to channel chat advAtoi parses "5k" / "2k500" / "3m250" shorthand. parseBet adds relative raises: "+25" (+25%), "+" (+25% default), "x2" (double), "x" (double default), "%50" (half of current). DoQuit / DoRent now block via PlayerInAuction so the seller or current high bidder can't disappear mid-auction (their extraction would leave a dangling pointer in AuctionState). Item type whitelist matches the C snippet: weapon, armor, staff, wand, scroll, treasure. Min bid raise is +100 gold. Refunds the previous high bidder when out-bid. Tests (11): - TestAdvAtoi / TestParseBet — parser table tests - Sell posts the item + leaves seller's inventory; rejects junk item types - Bid below the +100 minimum is refused; refunds the previous bidder on a new high - AuctionUpdate walks three full cadences to SOLD with both gold and item transfer; no-bid path returns the item to the seller - Immortal stop confiscates; mortal stop is refused - PlayerInAuction true for seller and current buyer; false for uninvolved players All 73 game-package tests pass.
Post-M7: dual wield, multi-item, mud/ snippet ports (tiers 1+2)
Port of Robert Schultz (Sembiance)'s Game Code v2 for ROM.
Adds two mini-games hidden behind one new "game" verb:
game slots <machine> — find an ItemSlotMachine in the room,
pay its per-spin cost, roll N bars
(3 or 5), collect winnings + jackpot
based on matches. Partial-winnings
mode pays for pairs; frozen mode
prevents jackpot drift.
game highdice <bet> — find an ActDealer NPC, each side rolls
2d6, dealer wins ties. Min 10, max 1000.
Adds:
- ItemSlotMachine = 35 item type (Value[0] cost, [1] jackpot,
[2] bars 3|5, [3] partial-mode flag, [4] frozen flag)
- ActDealer = BitL mob act flag
- internal/game/games.go: DoGame / DoSlots / DoHighDice with
colored bar strip rendering
Tests (15):
- DoGame routing (NPC reject, empty-arg help, unknown-game)
- Slots: missing machine, syntax, broken machine, bad bar count,
cannot-afford gate, normal spin charges cost + renders reels
- HighDice: syntax, min-bet, max-bet, missing-dealer, cannot-afford,
resolved roll moves gold by the bet amount in either direction
Port of mlkesl@stthomas.edu's automap. 'map' walks the room graph from the player's current room and renders a colored ASCII grid of the surrounding area. map — 21x21 default map <N> — 2N+1 grid (clamped 3..30) Sector-to-glyph mapping in one sectorGlyph() helper (the C version repeated the switch three times). Six sector types from the C snippet that don't exist in our ROM 2.4 port (ROCK_MOUNTAIN, SNOW_MOUNTAIN, ROAD, ENTER, JUNGLE, SWAMP, RUINS) were collapsed. Walker stops at: - Closed doors (renders the side we're on, not the other) - One-way exits / mazes (marked '?' but not traversed) - Opaque sectors — city, indoors, mountain, hills (rendered, not traversed past) Tests (7): - 3-room east/west chain visits all three - Closed door blocks the walker - Opaque sector renders but doesn't propagate - One-way exit is marked as a maze tile - sectorGlyph table cases - Centre renders as '*' player marker on the right line - DoMap clamps out-of-range radii without panicking
tier-3 start: slot machines + high-dice (game_code_v2)
Port of Vassago's Quest Code v2.03 from Moongate (1996). Players
visit a questmaster to receive a randomly-generated kill-or-recover
quest with a 10-30 minute realtime countdown; turn-in pays quest
points spendable on rewards plus gold and occasional practices.
Adapted to ROM 2.4 from the C original:
- Questmasters identified by ActQuestmaster mob ACT flag (BitM)
rather than a spec_questmaster special procedure
- PCData gains QuestPoints, NextQuest, Countdown, QuestObj, QuestMob,
QuestGiver fields. QuestPoints + NextQuest persist via two new
SQLite columns with idempotent ALTER migration
- Reward + quest-token vnums in const.go so admins can retarget
- Level-difficulty bands rescaled from Moongate's 350 levels to
ROM 2.4's 60 (newbie / low / mid / high)
- Reward table is a slice instead of a long if/else chain — same
set of items: comfy chair 1000qp, sword 850qp, amulet/shield
750qp, decanter 550qp, gold 500qp, practices 500qp
Commands:
quest — subcommand help
quest points — show QP (anywhere)
quest info — current quest target (anywhere)
quest time — cooldown or countdown (anywhere)
quest list — reward catalog (questmaster required)
quest buy <item> — purchase reward (questmaster required)
quest request — ask for a new quest (questmaster required)
quest complete — turn in current quest (questmaster required)
Hooks:
- GroupGain calls QuestNoteKill when a PC kills a mob — matching
quest mob sets QuestMob=-1 so the player must return to the
questmaster
- AreaUpdate calls QuestUpdate per pulse_area to tick down
countdown / nextquest, fire hurry warning at <6 min, expire the
quest at 0
- showCharToChar0 prefixes the active quest mob's description with
{R[TARGET]{x
Tests (22):
- DoQuest dispatch (help, points, time variants)
- Questmaster gating (missing, cooldown, already-on-quest)
- Buy paths (insufficient points, practices, gold, unknown item)
- questLevelDiff bands (newbie/low/mid/high)
- QuestUpdate cooldown decrement + end notice
- QuestUpdate countdown expiry resets quest + sets 10-min cooldown
- QuestUpdate hurry warning at countdown<6
- QuestNoteKill matching + non-matching + off-quest cases
- showCharToChar0 [TARGET] tag toggles correctly
tier-3 quest: automated quest system (quest-2.03.c port)
…ather/)
Port of Thomas J Whiting's expanded weather snippet on top of ROM 2.4's
basic time/sky loop. Replaces the bare hourly-sunlight WeatherUpdate
with a 10-state Markov sky machine, per-player auto-broadcast, a
player-facing forecast, hazard events that damage outdoor PCs, and
a control-weather spell that walks the sky discretely.
New sky states (added to existing Cloudless/Cloudy/Raining/Lightning):
- Foggy, Snowing, Hailstorm, Thunderstorm, Icestorm, Blizzard
New player command:
weather — show sky + date (outdoor only)
autoweather — toggle whether room-look prints weather lines
New admin command (L4+):
wset <cond> — snap the global sky to a specific state for
testing / events. Bypasses the state machine.
Hazard events (fire only when their sky is active):
- lightning — strikes outdoor PCs >L17, scaled damage by level band,
immunity/resistance/vulnerability respected, 40-pulse
wait state
- blizzard — outdoor effect ladder: warning -> trip -> tree collision
-> freezing limbs (5/15/30 hp tiers)
- ice — same shape as blizzard, ice-themed messages
- hail — golfball-sized stones (5/30 hp tiers)
- fog — fog/hole/tree/river effects (5/15/30 hp tiers)
Movement gate (port of act_move.weather):
- During Blizzard or Hailstorm, players can't step from indoors out
unless they have Flying / Haste / Infrared
- Flying characters get a 25%-per-step chance of being grounded;
Move pool halves and the step cancels
- Race-specific wing-size table from the snippet was dropped — we
don't have Pixie/Kenku/Draconian etc.
control_weather spell rewritten (was a basic mmhg-nudge stub):
- "better" walks sky one step toward Cloudless via a transition table
- "worse" walks sky one step toward Blizzard
- Also nudges WeatherInfo.Change so the basic drift still respects intent
Adaptations from the C source:
- The if-chain `number_chance(N)` ladders for sky transitions become
a single NumberPercent() roll inspected against cumulative thresholds
- Hazard event chance ladders (number_range(0,2000) <= N) become
the equivalent percentile buckets
- Lightning's three #defines (all typo'd LI1 in the source) become
natural MaxLevel/3 and 2*MaxLevel/3 bands
- Unknown sky values reset to Cloudless rather than spinning forever
Tests (23):
- Hour rollover (++, day, month)
- Sunrise/sunset hour transitions
- Unknown sky resets
- numberChance bounds (0% never, 100% always)
- isOutdoors: SectInside + RoomIndoors flag + clean field
- DoWeather: indoor refusal + outdoor forecast
- DoAutoweather toggle round-trip
- ShowWeather sky→line table (10 cases)
- DoWset: syntax, snap to sky, unknown refused
- controlWeatherBetter / controlWeatherWorse transition tables
- MoveChar: blizzard/hail block indoor→outdoor; flying bypasses;
cloudless sky doesn't restrict
tier-3 weather: sky state machine + hazards + control_weather
Port of Jair's deity system: players can pick a god, sacrifice flavor text uses the player's god instead of "Mota", and a pray channel reaches everyone who worships the same god. Adaptations from the C source: - Nanny CON_GET_GOD creation step deferred — would break the parity test transcripts. Players default to god 0 (Mota) and pick a god voluntarily via the new `setgod` command. - GodTable lives in internal/world/tables.go alongside other static data (LiqTable, AttackTable, etc.) rather than as a C global. - The $g act-code substitution from the snippet is replaced with PlayerGodName(ch) at the four call sites in DoSacrifice — easier to follow and avoids touching the Act() formatter. - Class indexes assume our 4-class set: Mage=0, Cleric=1, Thief=2, Warrior=3. Snippet had the same. New constants: - world.PlrQuestor was already added in PR #6; God field on PCData is int16 (no flag needed) - world.CommNoPray = BitK — mutes the pray channel - world.GodType + world.GodTable with 7 sample gods (Mota, Aleera, Selune, Tempus, Mask, Talos, Cyric) Persistence: - One new SQLite column `god INTEGER NOT NULL DEFAULT 0` with the idempotent ALTER migration. Round-trips through the existing upsert + load path. Commands: - `setgod` — list eligible gods + current - `setgod <name>` — pick a god (alignment + class gated; immortals bypass eligibility) - `pray` — toggle the channel - `pray <msg>` — broadcast to fellow worshippers; muted listeners (CommNoPray) and quiet listeners (CommQuiet) are skipped. Speaking on the channel auto-unmutes. Sacrifice routing: - DoSacrifice's "Mota gives you X silver" lines now interpolate PlayerGodName(ch); NPCs and ungod'd PCs still see "Mota" Score: - New "You worship X" line under the PK stats block in DoScore Tests (16): - PlayerGodName: default / NPC / out-of-range fallback - godLookup: exact + prefix + miss + empty - playerCanWorship: Mota always; alignment gate; class gate - DoSetGod: list shows current, bad name refused, eligibility refused, success path, immortal bypass - DoPray: toggle, broadcast filtered by god + mute + quiet, auto un-mute on speak - DoScore mentions the player's god
Port of Tch (Feudal Realms 1997-99) bow-and-arrow snippet. Adds
ranged combat with three new commands and per-quiver scaling
arrows that lodge in the victim's body parts.
New constants:
- ItemQuiver=36, ItemArrow=37 item types
- WeaponBow=9 weapon class (bow weapon type)
- WearShoulder=20 + WearLodgeLeg/Arm/Rib=21-23 wear slots
- MaxWear bumped 20 → 24
- ItemWearShoulder=BitR wear flag (quivers attach to shoulder)
- ItemLodged=BitAA extra flag (arrow currently lodged)
- ObjVnumArrow=1208 default arrow vnum (builders can override)
- GsnBow skill (thieves L25, warriors L10)
New commands:
- `draw` — pull arrow from worn quiver into the held slot,
needs at least one free hand. Decrements quiver
arrow count; extracts the quiver when empty
- `fire` — shoot the held arrow at a target. Same room or
one open exit away (same area only). Miss → arrow
lands in target's room. Hit → d10 picks lodge
slot (1-6 leg, 7-9 arm 3/2x dmg, 10 chest 2x dmg)
- `dislodge` — yank a lodged arrow out, deals damage scaled
by the arrow's stored dice and which body part
Wear-slot wiring:
- wearObj now handles ItemWearShoulder → WearShoulder
- whereName extended with the shoulder slot + three lodge labels
("(lodged in a leg)" etc, in red so they stand out on EQ)
Adaptations from the C source:
- The C mutated `obj.WearFlags` to magic integer literals
(8388609 / 16777217 / 33554433) to push the arrow through
wear_obj into specific lodge slots. We call EquipChar
directly with the slot constant — cleaner, doesn't touch
wear_flags
- The C snippet hardcoded 134217728 for ITEM_LODGED; we use
the named ItemLodged constant
- OLC integration deferred — area builders create arrows and
quivers via .are files as usual
Tests (12):
- DoDraw no-quiver, hands-full, empty-quiver paths
- DoFire no-bow, non-bow wielded, no-arrow held, non-arrow held,
self-target, target-not-found refusals
- DoDislodge no-arg, nothing-lodged, lodged-arrow removal
(lodged flag cleared, arrow unequipped, HP reduced)
- wearObj routes quiver to WearShoulder slot
tier-3 bowfire: ranged combat (bowfire.c port)
tier-3 deity: pick-a-god worship system (deity.c port)
…onger needed Removes src/ (the original QuickMUD/ROM 2.4b6 C source) and the built area/rom binary. These were the parity oracle for M0–M5: test/parity/diff/parity_test.go launched both servers on free ports with a shared Mitchell-Moore seed and diffed transcripts byte-for-byte. M5 closed with zero diff a long time ago, so the oracle isn't doing day-to-day work anymore. The full pre-deletion master is preserved at tag c-oracle-archive (also pushed to origin), so the C source is always recoverable for spot checks or further reference. Kept: - doc/license.doc + doc/license.txt — diku/merc/ROM attribution the licenses require us to keep with any derived work - test/parity/diff/parity_test.go — already auto-skips with "C oracle not built (area/rom missing); run 'cd src && make' first", so leaving it in place doesn't hurt; if anyone ever needs to re-run parity they checkout the archive tag 65 files deleted, no build/test impact.
chore: archive ROM 2.4b6 C source — port is parity-clean
Replaces:
- README.md (old top-level, Docker-focused, pointed at the
other READMEs for the rest)
- README.merc (1993 Merc 2.1 installation instructions)
- README.quickmud (QuickMUD feature list, mostly defunct help URLs)
- README.rom (ROM 2.4 notes, broken FAQ link, defunct mailing
list)
- README.version (one-line "ROM 2.4b6, May 29, 1998")
The new README is honest about what the project is today (a Go port
of QuickMUD, live at mud.rhamiltoneng.com), then preserves the full
Diku → Merc → ROM → QuickMUD → rom24 attribution chain that the
Diku and Merc licenses require us to keep visible.
Also adds:
- Quick-start instructions for the Go binary (the existing README
only had Docker instructions for the upstream C image)
- Repository layout table
- Build / test / deploy commands
- A "Scope" section explaining what the Go port keeps vs replaces
(SQLite player store, no IMC2, no copyover, systemd instead of
csh startup, Go GC instead of recycle pools)
- Snippet sweep summary (dual wield through bowfire)
- Pointer to the c-oracle-archive tag for anyone who wants the
original C source back
CHANGELOG.quickmud is kept as-is in the repo root — it's a
historical artifact of the upstream C codebase and shouldn't be
folded into the new README.
The full license texts in doc/license.doc (Diku) and doc/license.txt
(Merc) are also kept as-is — the README now points to them as the
canonical source.
docs: consolidate 5 top-level READMEs into one
`who` rendered "Xajkilthe Warrior" when a character leveled, because
AdvanceLevel built `"the " + lookupTitle(ch)` and stored it raw via
SetTitle, bypassing the space-prepending logic that DoTitle was
carrying locally.
The C ROM convention (src/act_info.c set_title) is that the *set*
function — not its callers — owns the space rule: a title that
doesn't start with sentence punctuation (".", ",", "!", "?") gets a
leading space so name+title concatenates cleanly.
Changes:
- Move the space-prepend logic from DoTitle into SetTitle so both
the player command and the level-up auto-title path share it.
- DoTitle is now a thin wrapper that just calls SetTitle.
- persist.LoadCharacter applies the same one-shot heal to legacy
saves so existing characters with broken titles render correctly
the next time they log in, without needing a separate migration.
Tests:
- internal/game/title_test.go covers SetTitle's punctuation matrix,
the NPC no-op path, and reproduces the original level-up bug.
- internal/persist/title_heal_test.go is an integration test against
a real SQLite store that pins the legacy-title heal (and confirms
it's idempotent on already-healed titles).
fix(title): prepend space in SetTitle and heal legacy saves
Every command and skill ported during the snippet sweep was reachable in-game but invisible to `help` — players couldn't discover them without reading source. This adds 23 new help entries to area/help.are and updates the SACRIFICE entry to mention the new `sacrifice all` form. New entries (each grouped under its natural aliases): Commands - MAP / AUTOMAP ASCII automapper - HUNT tracker skill - CIRCLE thief flanking attack - SEARCH hidden-exit / hidden-object reveal - WEDGE / PRY jam and un-jam doors - FIRE / DRAW / DISLODGE bowfire ranged combat - QUEST 7-subcommand quest system - PRAY deity channel - SETGOD / WORSHIP / DEITY pick a god to worship - BANK / BALANCE / DEPOSIT banking - AUCTION / SELL / BID item auction channel - GAME / SLOT / HIGHDICE casino - OUTFIT reissue the school starter kit - COPYOVER hot-restart (now systemd-backed) Auto-flags - AUTOLOOT, AUTOSAC, AUTOGOLD, AUTOSPLIT - AUTOEXIT, AUTOSTEAL, AUTOWEATHER - AUTOFLAGS index page linking the rest Skill aliases - DUAL WIELD, CRITICAL STRIKE (passive combat skills) Also adds internal/game/help_coverage_test.go, which loads the real area dir and verifies every probe a player would type (`help map`, `help dual_wield`, `help autosteal`, …) resolves to some help entry via the same IsName matcher DoHelp uses in production. This locks in the coverage — adding a new command without a help entry will fail the test. Caught while writing this: the MAP help originally drew a glyph legend that included a literal `~` character. ROM `~` is the string terminator in .are files, so the loader truncated the entry mid-body and silently dropped the rest of help.are. Rewrote the legend in prose.
docs(help): add help entries for snippet-port commands and skills
A 3x3 ASCII minimap is now rendered under the room description
on every DoLook, gated by a new PlrAutoMap flag (off by default).
Matches the existing autoexit / autoloot / autoweather pattern:
type 'automap' alone to toggle, current state shows up in the
config / score-flag list.
Changes:
- world/const.go: PlrAutoMap = BitL (next free bit after
PlrAutoWeather).
- game/map.go:
- mapRadMin lowered from 3 to 1 so 'map 1' produces an actual
3x3 grid (it was silently clamping to 7x7). The interactive
'map' command keeps the same default (radius 10) and the same
upper cap (30).
- RenderMiniMap(ch, radius): exported render-only helper —
walks the room graph and prints the grid without re-printing
the room name, so DoLook doesn't render a duplicate title.
- game/act_info.go:
- DoLook prints the minimap right under the description,
before autoexit, when PlrAutoMap is set.
- DoAutomap toggle handler.
- config / score line shows the new flag.
- game/interp.go: 'automap' command-table entry (PosDead, level 0).
- area/help.are: standalone AUTOMAP entry. The AUTOFLAGS index
page added in PR #13 doesn't exist on master yet; once #13
merges, a small follow-up will add AUTOMAP to that index.
Tests (internal/game/automap_test.go):
- Toggle on/off flips PlrAutoMap and emits the right messages.
- NPC short-circuit (mob ACT bits overlap PLR bits — must not flip).
- RenderMiniMap at radius 1 produces a 3x3 grid (the regression
surface for the mapRadMin change).
- End-to-end: DoLook with PlrAutoMap off never shows the centre
marker; with it on, the marker appears after the description.
# Conflicts: # area/help.are
feat: AUTOMAP — opt-in minimap under every room description
Three bugs fixed:
1. Shop items always shown at level 0 (reset.go)
applyResetGive was missing the else branch for new-format objects.
Old-format objects derived olevel from item type heuristics; new-format
objects carry their level explicitly in pObjIndex.Level. Port the C
ROM else branch: olevel = pObjIndex.Level.
2. Skill indices corrupted when bow was inserted at sn=113 (persist)
Skills were saved/loaded by integer index. Inserting bow at sn=113
shifted bash (113→114) and second attack (125→126), so saved characters
loaded their practiced skills into the wrong slots.
Fix:
- world.SkillByName() — exact-match lookup by name in SkillTable
- saveSkills now writes skill_name alongside sn
- loadSkills resolves the current sn from skill_name when present,
falling back to the stored sn for pre-migration rows
- schema.sql migration: ALTER TABLE adds skill_name column; one-time
UPDATE bumps sn >= 113 WHERE skill_name = '' to repair displaced rows
on the next server start (idempotent — the WHERE guard prevents
double-shifting)
- Two new persist tests pin the round-trip and the migration
3. Automap radius too small (act_info.go)
Inline automap under DoLook was hardcoded at radius 1 (3×3 grid).
Increased to radius 3 (7×7 grid) for useful context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Shop items level 0:
applyResetGivewas missing theelsebranch for new-format objects — they carry their level inpObjIndex.Levelbut that was ignored, so all shop inventory spawned at level 0. Mirrors the C ROMelse olevel = pObj->levelbranch.Skills lost after bowfire PR: Skills were saved/loaded by integer index (
sn). Insertingbowat sn=113 shiftedbash(113→114) andsecond attack(125→126), silently loading practiced skills into wrong slots on login. Fixed by:skill_namealongsidesnincharacter_skillssnfromskill_name(immune to future reordering)sn >= 113rows whereskill_name = ''on next server start — repairs Xajkil and any other affected characters automaticallyAutomap too small: Inline minimap under
DoLookwas radius 1 (3×3). Increased to radius 3 (7×7).Test plan
go test ./...passes (CI)