createObj stored the raw 0-f nibble as Object.Which with no bounds check, so wizard mode -> C -> / -> f made a wand numbered 15 against a 14-entry table and panicked in fixStick; input below 'a' or '0' went negative and panicked the same way. C's create_obj() was equally unchecked, but its consumers were either switches (defined for any value) or static-array reads past the end (undefined, and survivable in practice). Since one game is now one process, the Go panic kills the game outright and leaves the terminal in raw mode. Reject at the two boundaries a bad Which can enter through. createObj now refuses an out-of-range choice with a message drawn from C's own type_name() vocabulary and adds nothing to the pack, a deliberate divergence recorded in a comment because C had no defined behavior here to be faithful to. Restore refuses a snapshot describing such an object (ErrSaveCorrupt) rather than loading a game that would explode later. Behind those, whichLimit/hasValidWhich back defensive guards at every dispatch the issue names: the quaffHandler/readHandler/zapHandler accessors return no handler instead of indexing (for wands that is exactly what non-MASTER C did, matching no case and still running o_charges--), the callIt lore lookups, identifyType, armorClass for the a_class[] reads, initWeapon against the missing init_dam[] row for WeaponFlame, fixStick's ws_type[] read, and inventoryName and objectWorth, hoisted so one check each covers the whole family of per-kind name and appraisal tables. No in-range input changes behavior and no guard consumes a random number: the rejection precedes every rnd() call. TestSeedCompatItemTables stays green untouched. New game/wizard_test.go covers the exact reproducer, a rejection sweep over every indexed kind including both negative-input forms, an acceptance sweep proving valid choices still build the right item, one no-panic test per guarded family, the fixStick crash site, the corrupt-save rejection, and a check that whichLimit still agrees with the table sizes. Each guard was confirmed load-bearing by reverting it and watching the test panic. TODO.md records the step; Next Step is deliberately left alone, since this arrived out of band via an issue.
246 lines
5.6 KiB
Go
246 lines
5.6 KiB
Go
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
|
package game
|
|
|
|
// wizard.c — special wizard commands, some of which are also non-wizard
|
|
// commands under strange circumstances. The DES password check is not
|
|
// ported: wizard mode is enabled by configuration instead.
|
|
|
|
// createObj is the wizard command for getting anything he wants (wizard.c
|
|
// create_obj).
|
|
func (g *RogueGame) createObj() {
|
|
obj := newObject()
|
|
|
|
g.msg("type of item: ")
|
|
obj.Kind = objectKindForGlyph(g.readchar())
|
|
g.Msgs.Mpos = 0
|
|
g.msg("which %c do you want? (0-f)", obj.Kind.Glyph())
|
|
|
|
ch := g.readchar()
|
|
if isDigit(ch) {
|
|
obj.Which = int(ch - '0')
|
|
} else {
|
|
obj.Which = int(ch-'a') + 10
|
|
}
|
|
|
|
obj.Group = 0
|
|
obj.Count = 1
|
|
g.Msgs.Mpos = 0
|
|
|
|
// Deliberate divergence from 5.4.4: C stored this nibble unchecked, so
|
|
// 'a'-'f' (and anything below '0' or 'a', which goes negative) indexed
|
|
// straight past the ends of the per-kind static tables. That was
|
|
// undefined behavior C happened to survive by reading adjacent memory;
|
|
// in Go it is a panic that kills the process with the terminal still in
|
|
// raw mode. C had no defined behavior here to be faithful to, so the
|
|
// choice is rejected outright rather than emulating a garbage read. The
|
|
// check precedes every rnd() call below, so the RNG sequence is
|
|
// untouched either way.
|
|
if !obj.wizardCanCreate() {
|
|
g.msg("there is no such %s", obj.Kind)
|
|
|
|
return
|
|
}
|
|
|
|
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
|
switch obj.Kind {
|
|
case KindWeapon, KindArmor:
|
|
g.createWeaponArmor(obj)
|
|
case KindRing:
|
|
g.createRing(obj)
|
|
case KindWand:
|
|
g.fixStick(obj)
|
|
case KindGold:
|
|
g.msg("how much?")
|
|
|
|
buf := ""
|
|
if g.getStr(&buf, g.scr.Std) == Norm {
|
|
obj.GoldValue = cAtoi(buf)
|
|
}
|
|
}
|
|
|
|
g.addPack(obj, false)
|
|
}
|
|
|
|
// createWeaponArmor sets up a wizard-created weapon or armor with an
|
|
// optional blessing (the weapon/armor arm of wizard.c create_obj).
|
|
func (g *RogueGame) createWeaponArmor(obj *Object) {
|
|
g.msg("blessing? (+,-,n)")
|
|
bless := g.readchar()
|
|
g.Msgs.Mpos = 0
|
|
|
|
if bless == '-' {
|
|
obj.Flags.Set(Cursed)
|
|
}
|
|
|
|
if obj.Kind == KindWeapon {
|
|
g.initWeapon(obj, WeaponKind(obj.Which))
|
|
|
|
if bless == '-' {
|
|
obj.HPlus -= g.rnd(3) + 1
|
|
}
|
|
|
|
if bless == '+' {
|
|
obj.HPlus += g.rnd(3) + 1
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
obj.ArmorClass = g.data.armorClass(obj.Which)
|
|
if bless == '-' {
|
|
obj.ArmorClass += g.rnd(3) + 1
|
|
}
|
|
|
|
if bless == '+' {
|
|
obj.ArmorClass -= g.rnd(3) + 1
|
|
}
|
|
}
|
|
|
|
// createRing sets up a wizard-created ring, prompting for a bonus on
|
|
// the bonus rings (the ring arm of wizard.c create_obj).
|
|
func (g *RogueGame) createRing(obj *Object) {
|
|
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
|
switch obj.RingKind() {
|
|
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
|
|
g.msg("blessing? (+,-,n)")
|
|
bless := g.readchar()
|
|
g.Msgs.Mpos = 0
|
|
|
|
if bless == '-' {
|
|
obj.Flags.Set(Cursed)
|
|
obj.Bonus = -1
|
|
} else {
|
|
obj.Bonus = g.rnd(2) + 1
|
|
}
|
|
case RingAggravateMonsters, RingTeleportation:
|
|
obj.Flags.Set(Cursed)
|
|
}
|
|
}
|
|
|
|
// showMap prints out the whole map for the wizard (wizard.c show_map).
|
|
func (g *RogueGame) showMap() {
|
|
hw := g.scr.Hw
|
|
hw.Clear()
|
|
|
|
for y := 1; y < NumLines-1; y++ {
|
|
for x := range NumCols {
|
|
isReal := g.Level.FlagsAt(y, x).Has(FReal)
|
|
if !isReal {
|
|
hw.Standout(true)
|
|
}
|
|
|
|
hw.MvAddCh(y, x, g.Level.Char(y, x))
|
|
|
|
if !isReal {
|
|
hw.Standout(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
g.showWin("---More (level map)---")
|
|
}
|
|
|
|
// whatis identifies what a certain object is (wizard.c whatis).
|
|
func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
|
|
p := &g.Player
|
|
if len(p.Pack) == 0 {
|
|
g.msg("you don't have anything in your pack to identify")
|
|
|
|
return
|
|
}
|
|
|
|
obj, ok := g.whatisPick(insist, kind)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
|
switch obj.Kind {
|
|
case KindScroll:
|
|
setKnow(obj, g.Items.Scrolls[:])
|
|
case KindPotion:
|
|
setKnow(obj, g.Items.Potions[:])
|
|
case KindWand:
|
|
setKnow(obj, g.Items.Sticks[:])
|
|
case KindWeapon, KindArmor:
|
|
obj.Flags.Set(Known)
|
|
case KindRing:
|
|
setKnow(obj, g.Items.Rings[:])
|
|
}
|
|
|
|
g.msg("%s", g.inventoryName(obj, false))
|
|
}
|
|
|
|
// whatisPick prompts for the item to identify, re-asking until a
|
|
// matching one is chosen when insist is set; ok is false when the
|
|
// player gives up (the prompt loop of wizard.c whatis).
|
|
func (g *RogueGame) whatisPick(insist bool, kind ObjectKind) (*Object, bool) {
|
|
for {
|
|
obj, _ := g.promptPackItem("identify", kind)
|
|
|
|
if !insist {
|
|
return obj, obj != nil
|
|
}
|
|
|
|
if g.NObjs == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
if obj == nil {
|
|
g.msg("you must identify something")
|
|
|
|
continue
|
|
}
|
|
|
|
if !matchesFilter(kind, obj) {
|
|
g.msg("you must identify a %s", kind)
|
|
|
|
continue
|
|
}
|
|
|
|
return obj, true
|
|
}
|
|
}
|
|
|
|
// setKnow sets things up when we really know what a thing is (wizard.c
|
|
// set_know).
|
|
func setKnow(obj *Object, info []ObjInfo) {
|
|
info[obj.Which].Know = true
|
|
obj.Flags.Set(Known)
|
|
info[obj.Which].Guess = ""
|
|
}
|
|
|
|
// The C type_name()/tlist table is gone: ObjectKind.String() carries the
|
|
// same vocabulary.
|
|
|
|
// teleport bamfs the hero someplace else (wizard.c teleport).
|
|
func (g *RogueGame) teleport() {
|
|
p := &g.Player
|
|
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
|
|
|
|
c, _ := g.findFloor(true)
|
|
if g.roomIn(c) != p.Room {
|
|
g.leaveRoom(p.Pos)
|
|
p.Pos = c
|
|
g.enterRoom(p.Pos)
|
|
} else {
|
|
p.Pos = c
|
|
|
|
g.look(true)
|
|
}
|
|
|
|
g.mvaddch(p.Pos.Y, p.Pos.X, PlayerCh)
|
|
// turn off ISHELD in case teleportation was done while fighting a
|
|
// Flytrap
|
|
if p.On(Held) {
|
|
p.Flags.Clear(Held)
|
|
p.VfHit = 0
|
|
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
|
|
}
|
|
|
|
g.NoMove = 0
|
|
g.Count = 0
|
|
g.Running = false
|
|
g.flushType()
|
|
}
|