From f3e30415be6f9383a4a81d323b290049825ccc3a Mon Sep 17 00:00:00 2001 From: Matthew Faltys Date: Thu, 21 Nov 2019 19:01:40 +0000 Subject: [PATCH] Add recovery utility --- config.gcfg | 1 + deps/nethackrc | 128 +++---------------------------- deps/nethackrc.full | 166 ++++++++++++++++++++++++++++++++++++++++ deps/server_config.gcfg | 1 + nethack-launcher.go | 150 +++++++++++++++++++++++++++++++++++- 5 files changed, 327 insertions(+), 119 deletions(-) create mode 100644 deps/nethackrc.full diff --git a/config.gcfg b/config.gcfg index b02020c..e0e36ce 100644 --- a/config.gcfg +++ b/config.gcfg @@ -4,6 +4,7 @@ nethackversion = "3.6.1" hackdir = "savedata/hack" nhdatlocation = "/usr/lib/games/nethack/nhdat" + recoverbinary = "/usr/lib/games/nethack/recover" bootstrapdelay = 1 [redis] diff --git a/deps/nethackrc b/deps/nethackrc index 0250414..95b6b4c 100644 --- a/deps/nethackrc +++ b/deps/nethackrc @@ -2,13 +2,11 @@ # good luck escaping # jk, its vim -#OPTIONS=showexp,showscore,time,color,!autopickup -#OPTIONS=autodig,fruit:slime mold,boulder:0 - +# standard options OPTIONS=menucolors OPTIONS=autodig OPTIONS=autoopen -#OPTIONS=autopickup,pickup_types:$!/="+? +OPTIONS=autopickup,pickup_types:$!/="+? OPTIONS=autoquiver OPTIONS=bones OPTIONS=boulder:0 @@ -20,20 +18,23 @@ OPTIONS=hilite_pet OPTIONS=hilite_pile OPTIONS=msg_window:reverse,msghistory:1000 OPTIONS=lit_corridor - + +# statusbar options OPTIONS=showrace OPTIONS=showexp,time OPTIONS=sortloot:full + +# curses interface +#OPTIONS=windowtype:curses +#OPTIONS=perm_invent -## Change symbols -SYMBOLS=S_ghost:X -SYMBOLS=S_mimic_def:m -SYMBOLS=S_water:~ -SYMBOLS=S_lava:~ +### +# COLORS +### ## Use status hilites OPTIONS=statushilites -# HP +## HP OPTIONS=hitpointbar OPTIONS=hilite_status:hitpoints/100%/grey&normal OPTIONS=hilite_status:hitpoints/<100%/green&normal @@ -59,108 +60,3 @@ OPTIONS=hilite_status:condition/stun/red&bold OPTIONS=hilite_status:condition/termill/red&inverse OPTIONS=hilite_status:condition/foodpois/red&inverse OPTIONS=hilite_status:condition/slime/red&inverse -# Str, Con, Dex, Int, Wis, Cha -OPTIONS=hilite_status:characteristics/up/green/down/red -# gold -OPTIONS=hilite_status: gold/up/yellow/down/brown - -# FORCE COLORS -MENUCOLOR="\{R\}"=red -MENUCOLOR="\{B\}"=blue -MENUCOLOR="\{C\}"=cyan -MENUCOLOR="\{G\}"=green -MENUCOLOR="\{Y\}"=yellow -MENUCOLOR="\{[MP]\}"=magenta -MENUCOLOR="\{O\}"=orange -MENUCOLOR="\{W\}"=white -MENUCOLOR="\{L\}"=black -MENUCOLOR="\{y\}"=gray -MENUCOLOR="\{g\}"=lightgreen -MENUCOLOR="\{b\}"=lightblue -MENUCOLOR="\{[mp]\}"=lightmagenta -MENUCOLOR="\{c\}"=lightcyan - -# GOODIES -MENUCOLOR=" bag .* holding"=magenta -MENUCOLOR=" luck(stone)?($| )"=magenta -MENUCOLOR=" wand .* wish(ing)?($| )"=magenta -MENUCOLOR=" wand .* tele(port(ation)?)?($| )"=magenta -MENUCOLOR=" wand .* polymorph"=magenta -MENUCOLOR=" wand .* death"=magenta -MENUCOLOR=" gain level"=magenta -MENUCOLOR=" full healing"=magenta -MENUCOLOR=" magic marker"=magenta -MENUCOLOR=" magic lamp|lamp .* magic"=magenta - -MENUCOLOR=" unicorn horn[^[]*$"=magenta -#this doesn't color the #enhance unicorn - -MENUCOLOR=" tinning kit"=magenta -MENUCOLOR=" ring .* regen(eration)?($| )"=magenta -MENUCOLOR=" ring .* conflict"=magenta -MENUCOLOR=" ring .* (FA|free action)($| )"=magenta -MENUCOLOR=" ring .* (TC|teleport control)($| )"=magenta -MENUCOLOR=" ring .* lev(itation)?($| )"=magenta -MENUCOLOR=" scrolls? .* genocide($| )"=magenta -MENUCOLOR=" scrolls? .* charging($| )"=magenta -MENUCOLOR=" scrolls? .* identify($| )"=magenta -MENUCOLOR=" amulet .* (life ?saving|LS)($| )"=magenta -MENUCOLOR=" amulet .* ref(lection)?($| )"=magenta -MENUCOLOR=" c(o|hi)ckatrice (corpse|egg)"=magenta -MENUCOLOR=" egg .* cockatrice"=magenta -MENUCOLOR=" stethoscope"=magenta - -#instruments -MENUCOLOR="tooled horn|fire horn|frost horn|horn .* (tooled|fire|frost)"=magenta -MENUCOLOR=" harp"=magenta -MENUCOLOR=" bugle"=magenta -MENUCOLOR=" flute"=magenta - -# unidentified magic armor -MENUCOLOR="piece of cloth|opera cloak|ornamental cope|tattered cape"=magenta -MENUCOLOR="plumed helmet|etched helmet|crested helmet|visored helmet"=magenta -MENUCOLOR="(old|padded|riding|fencing) gloves"=magenta -MENUCOLOR="(mud|buckled|riding|snow|hiking|combat|jungle) boots"=magenta - -# FOOD CONDUCTS -# vegan -MENUCOLOR="(food |cram |K-|C-)ration|gunyoki"=green&bold -MENUCOLOR="lembas wafer|melon|carrot|tins? .* spinach"=green&bold -MENUCOLOR=" oranges?( named.*)?$"=green&bold -# avoid coloring orange DSM -MENUCOLOR=" pears?( named.*)?$"=green&bold -# avoid coloring pearl rings -MENUCOLOR=" (apple|banana|kelp|eucalyptus|garlic|wolfsbane)"=green&bold -MENUCOLOR=" (slime mold|berries)"=green&bold -## YOUR FRUIT HERE -MENUCOLOR="(tins? of )?(gelatinous cube|acid blob|quivering blob)( corpse)?"=green&bold -MENUCOLOR="(tins? of )?(blue|spotted|ochre) jelly( corpse)?"=green&bold -MENUCOLOR="(tins? of )?lichen|shrieker|violet fungus|(brown|yellow|green|red) mold( corpse)?"=green&bold - -# vegetarian -MENUCOLOR="egg|pancake|fortune cookie|candy bar|royal jelly|cream pie|candle"=green -MENUCOLOR="(tin of )?brown pudding|gray ooze( corpse)?"=green - -# WATER SPORTS -MENUCOLOR=" holy water"=cyan&bold -MENUCOLOR=" blessed clear potion"=cyan&bold -MENUCOLOR=" blessed potions? called water"=cyan&bold -MENUCOLOR=" clear potions? named (holy|blessed|B)($| )"=cyan&bold -MENUCOLOR=" potions? of water named (holy|blessed|B)($| )"=cyan&bold -MENUCOLOR=" potions? called water named (holy|blessed|B)($| )"=cyan&bold -MENUCOLOR=" unholy water"=orange -MENUCOLOR=" cursed clear potion"=orange -MENUCOLOR=" cursed potions? called water"=orange -MENUCOLOR=" potions? called water named (unholy|cursed|C)($| )"=orange - -# B/U/C -#MENUCOLOR="^[a-zA-Z$] - "=white #may want to enable this if regularly playing priest.. (priests don't see the 'uncursed') -MENUCOLOR=" !C"=white -MENUCOLOR=" !B"=white -MENUCOLOR=" !UC"=white -MENUCOLOR=" uncursed| UC?($| )"=white -MENUCOLOR=" \([-0-9]+:[-0-9]+\)"=white -#nethack doesn't display "uncursed" if charges are known -MENUCOLOR=" [+-][0-9]"=white -MENUCOLOR=" blessed| B($| )"=cyan -MENUCOLOR=" cursed| C($| )"=red diff --git a/deps/nethackrc.full b/deps/nethackrc.full new file mode 100644 index 0000000..08f6e80 --- /dev/null +++ b/deps/nethackrc.full @@ -0,0 +1,166 @@ +# welcome to the real unixvoid dungeon +# good luck escaping +# jk, its vim + +#OPTIONS=showexp,showscore,time,color,!autopickup +#OPTIONS=autodig,fruit:slime mold,boulder:0 + +OPTIONS=menucolors +OPTIONS=autodig +OPTIONS=autoopen +OPTIONS=autopickup,pickup_types:$!/="+? +OPTIONS=autoquiver +OPTIONS=bones +OPTIONS=boulder:0 +OPTIONS=checkpoint +OPTIONS=color +OPTIONS=confirm +OPTIONS=dark_room +OPTIONS=hilite_pet +OPTIONS=hilite_pile +OPTIONS=msg_window:reverse,msghistory:1000 +OPTIONS=lit_corridor + +OPTIONS=showrace +OPTIONS=showexp,time +OPTIONS=sortloot:full + +## Change symbols +SYMBOLS=S_ghost:X +SYMBOLS=S_mimic_def:m +SYMBOLS=S_water:~ +SYMBOLS=S_lava:~ + +## Use status hilites +OPTIONS=statushilites +# HP +OPTIONS=hitpointbar +OPTIONS=hilite_status:hitpoints/100%/grey&normal +OPTIONS=hilite_status:hitpoints/<100%/green&normal +OPTIONS=hilite_status:hitpoints/<66%/yellow&normal +OPTIONS=hilite_status:hitpoints/<50%/orange&normal +OPTIONS=hilite_status:hitpoints/<33%/red&bold +OPTIONS=hilite_status:hitpoints/<15%/red&inverse +## Pw +OPTIONS=hilite_status:power/100%/grey&normal +OPTIONS=hilite_status:power/<100%/green&normal +OPTIONS=hilite_status:power/<66%/yellow&normal +OPTIONS=hilite_status:power/<50%/orange&normal +OPTIONS=hilite_status:power/<33%/red&bold +## Carry +OPTIONS=hilite_status:cap/burdened/yellow/stressed/orange/strained/red&bold/overtaxed/red&inverse/overloaded/red&inverse&blink +## Hunger +OPTIONS=hilite_status:hunger/satiated/yellow/hungry/orange/weak/red&bold/fainting/red&inverse/fainted/red&inverse&blink +## Mental +OPTIONS=hilite_status:condition/hallu/yellow +OPTIONS=hilite_status:condition/conf/orange +OPTIONS=hilite_status:condition/stun/red&bold +## Health +OPTIONS=hilite_status:condition/termill/red&inverse +OPTIONS=hilite_status:condition/foodpois/red&inverse +OPTIONS=hilite_status:condition/slime/red&inverse +# Str, Con, Dex, Int, Wis, Cha +OPTIONS=hilite_status:characteristics/up/green/down/red +# gold +OPTIONS=hilite_status: gold/up/yellow/down/brown + +# FORCE COLORS +MENUCOLOR="\{R\}"=red +MENUCOLOR="\{B\}"=blue +MENUCOLOR="\{C\}"=cyan +MENUCOLOR="\{G\}"=green +MENUCOLOR="\{Y\}"=yellow +MENUCOLOR="\{[MP]\}"=magenta +MENUCOLOR="\{O\}"=orange +MENUCOLOR="\{W\}"=white +MENUCOLOR="\{L\}"=black +MENUCOLOR="\{y\}"=gray +MENUCOLOR="\{g\}"=lightgreen +MENUCOLOR="\{b\}"=lightblue +MENUCOLOR="\{[mp]\}"=lightmagenta +MENUCOLOR="\{c\}"=lightcyan + +# GOODIES +MENUCOLOR=" bag .* holding"=magenta +MENUCOLOR=" luck(stone)?($| )"=magenta +MENUCOLOR=" wand .* wish(ing)?($| )"=magenta +MENUCOLOR=" wand .* tele(port(ation)?)?($| )"=magenta +MENUCOLOR=" wand .* polymorph"=magenta +MENUCOLOR=" wand .* death"=magenta +MENUCOLOR=" gain level"=magenta +MENUCOLOR=" full healing"=magenta +MENUCOLOR=" magic marker"=magenta +MENUCOLOR=" magic lamp|lamp .* magic"=magenta + +MENUCOLOR=" unicorn horn[^[]*$"=magenta +#this doesn't color the #enhance unicorn + +MENUCOLOR=" tinning kit"=magenta +MENUCOLOR=" ring .* regen(eration)?($| )"=magenta +MENUCOLOR=" ring .* conflict"=magenta +MENUCOLOR=" ring .* (FA|free action)($| )"=magenta +MENUCOLOR=" ring .* (TC|teleport control)($| )"=magenta +MENUCOLOR=" ring .* lev(itation)?($| )"=magenta +MENUCOLOR=" scrolls? .* genocide($| )"=magenta +MENUCOLOR=" scrolls? .* charging($| )"=magenta +MENUCOLOR=" scrolls? .* identify($| )"=magenta +MENUCOLOR=" amulet .* (life ?saving|LS)($| )"=magenta +MENUCOLOR=" amulet .* ref(lection)?($| )"=magenta +MENUCOLOR=" c(o|hi)ckatrice (corpse|egg)"=magenta +MENUCOLOR=" egg .* cockatrice"=magenta +MENUCOLOR=" stethoscope"=magenta + +#instruments +MENUCOLOR="tooled horn|fire horn|frost horn|horn .* (tooled|fire|frost)"=magenta +MENUCOLOR=" harp"=magenta +MENUCOLOR=" bugle"=magenta +MENUCOLOR=" flute"=magenta + +# unidentified magic armor +MENUCOLOR="piece of cloth|opera cloak|ornamental cope|tattered cape"=magenta +MENUCOLOR="plumed helmet|etched helmet|crested helmet|visored helmet"=magenta +MENUCOLOR="(old|padded|riding|fencing) gloves"=magenta +MENUCOLOR="(mud|buckled|riding|snow|hiking|combat|jungle) boots"=magenta + +# FOOD CONDUCTS +# vegan +MENUCOLOR="(food |cram |K-|C-)ration|gunyoki"=green&bold +MENUCOLOR="lembas wafer|melon|carrot|tins? .* spinach"=green&bold +MENUCOLOR=" oranges?( named.*)?$"=green&bold +# avoid coloring orange DSM +MENUCOLOR=" pears?( named.*)?$"=green&bold +# avoid coloring pearl rings +MENUCOLOR=" (apple|banana|kelp|eucalyptus|garlic|wolfsbane)"=green&bold +MENUCOLOR=" (slime mold|berries)"=green&bold +## YOUR FRUIT HERE +MENUCOLOR="(tins? of )?(gelatinous cube|acid blob|quivering blob)( corpse)?"=green&bold +MENUCOLOR="(tins? of )?(blue|spotted|ochre) jelly( corpse)?"=green&bold +MENUCOLOR="(tins? of )?lichen|shrieker|violet fungus|(brown|yellow|green|red) mold( corpse)?"=green&bold + +# vegetarian +MENUCOLOR="egg|pancake|fortune cookie|candy bar|royal jelly|cream pie|candle"=green +MENUCOLOR="(tin of )?brown pudding|gray ooze( corpse)?"=green + +# WATER SPORTS +MENUCOLOR=" holy water"=cyan&bold +MENUCOLOR=" blessed clear potion"=cyan&bold +MENUCOLOR=" blessed potions? called water"=cyan&bold +MENUCOLOR=" clear potions? named (holy|blessed|B)($| )"=cyan&bold +MENUCOLOR=" potions? of water named (holy|blessed|B)($| )"=cyan&bold +MENUCOLOR=" potions? called water named (holy|blessed|B)($| )"=cyan&bold +MENUCOLOR=" unholy water"=orange +MENUCOLOR=" cursed clear potion"=orange +MENUCOLOR=" cursed potions? called water"=orange +MENUCOLOR=" potions? called water named (unholy|cursed|C)($| )"=orange + +# B/U/C +#MENUCOLOR="^[a-zA-Z$] - "=white #may want to enable this if regularly playing priest.. (priests don't see the 'uncursed') +MENUCOLOR=" !C"=white +MENUCOLOR=" !B"=white +MENUCOLOR=" !UC"=white +MENUCOLOR=" uncursed| UC?($| )"=white +MENUCOLOR=" \([-0-9]+:[-0-9]+\)"=white +#nethack doesn't display "uncursed" if charges are known +MENUCOLOR=" [+-][0-9]"=white +MENUCOLOR=" blessed| B($| )"=cyan +MENUCOLOR=" cursed| C($| )"=red diff --git a/deps/server_config.gcfg b/deps/server_config.gcfg index 1688c26..89b02c5 100644 --- a/deps/server_config.gcfg +++ b/deps/server_config.gcfg @@ -4,6 +4,7 @@ nethackversion = "3.6.1" hackdir = "/hack" nhdatlocation = "/usr/lib/games/nethack/nhdat" + recoverbinary = "/usr/lib/games/nethack/recover" bootstrapdelay = 1 [redis] diff --git a/nethack-launcher.go b/nethack-launcher.go index 6cb99bb..2bc51c0 100644 --- a/nethack-launcher.go +++ b/nethack-launcher.go @@ -1,14 +1,15 @@ package main -// TODO make poll time faster -// TODO password redo field +// TODO split up into multiple files import ( "bufio" "fmt" "io" + "io/ioutil" "os" "os/exec" + "path/filepath" "strconv" "strings" "sync" @@ -26,6 +27,7 @@ type Config struct { NethackVersion string HackDir string NhdatLocation string + RecoverBinary string BootstrapDelay time.Duration } @@ -167,6 +169,7 @@ func printUserScreen(redisClient *redis.Client, username string) string { println(" w) Watch games in progress") println(" h) View highscores") println(" e) Edit config") + println(" r) Recover from crash") fmt.Printf(" p) Play NetHack %s\n", config.NethackLauncher.NethackVersion) println(" q) Quit") println("") @@ -215,6 +218,9 @@ func printUserScreen(redisClient *redis.Client, username string) string { wg.Wait() close(watcher) printUserScreen(redisClient, username) + case "r": + clearScreen() + recoverSave(redisClient, username) case "q": exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() @@ -348,6 +354,9 @@ func printRegisterScreen(redisClient *redis.Client) { hackRCDest := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) exec.Command("cp", hackRCLoc, hackRCDest).Run() + // TODO: move the above creation code into the createUserFiles() function + createUserFiles(username) + // back to main screen printUserScreen(redisClient, username) } @@ -515,7 +524,7 @@ func runGame(username, timestamp string) { nh.Env = os.Environ() nh.Env = append(nh.Env, fmt.Sprintf("HOME=%s", homeDir)) nh.Env = append(nh.Env, fmt.Sprintf("USER=%s", username)) - nh.Env = append(nh.Env, fmt.Sprintf("NETHACKDIR=%s", config.NethackLauncher.HackDir)) + nh.Env = append(nh.Env, fmt.Sprintf("NETHACKDIR=%s/user/%s", config.NethackLauncher.HackDir, username)) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr @@ -590,6 +599,141 @@ func createInitialFiles() { } } +func createUserFiles(username string) { + // create necessary directories if they dont exist + userpath := fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username) + + if _, err := os.Stat(fmt.Sprintf("%s/dumps/", userpath)); os.IsNotExist(err) { + os.Mkdir(fmt.Sprintf("%s/dumps/", userpath), os.ModeDir) + } + if _, err := os.Stat(fmt.Sprintf("%s/save/", userpath)); os.IsNotExist(err) { + os.Mkdir(fmt.Sprintf("%s/save/", userpath), os.ModeDir) + } + + // create necessary files if they dont exist + os.OpenFile(fmt.Sprintf("%s/logfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/perm", userpath), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/record", userpath), os.O_RDONLY|os.O_CREATE, 0666) + os.OpenFile(fmt.Sprintf("%s/xlogfile", userpath), os.O_RDONLY|os.O_CREATE, 0666) + + // move in nhdat file if it does not exist + exec.Command("cp", config.NethackLauncher.NhdatLocation, userpath).Run() +} + +func recoverSave(redisClient *redis.Client, username string) { + // if no statefile exist, exit early as we cannot recover + matches, _ := filepath.Glob(fmt.Sprintf("%s/user/%s/*lock*", config.NethackLauncher.HackDir, username)) + if matches == nil { + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + println(" The dungeon crumbles around you, darkness remains...") + println(" Godspeed adventurer..") + println("") + println(" No statefiles exist. (press enter to return)") + println("") + fmt.Printf(">> ") + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + if scanner.Text() == "" { + printUserScreen(redisClient, username) + } + } + } + + fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) + println("") + println(" Recover utilty.") + println("") + // check if dir is empty + isEmpty, _ := checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) + if !isEmpty { + // save file exists, overwrite? + println(" Save file already exists!") + println(" Overwrite? (y/n)") + println("") + fmt.Printf(">> ") + } else { + println(" Attempt to recovery statefile? (y/n)") + println("") + fmt.Printf(">> ") + } + + // disable input buffering + exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() + // do not display entered characters on the screen + exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + + var b []byte = make([]byte, 1) + for { + os.Stdin.Read(b) + switch string(b) { + case "n": + clearScreen() + printUserScreen(redisClient, username) + case "y": + // set user path to save file candidates + userPath := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) + + // read in all files + candidates, err := ioutil.ReadDir(userPath) + if err != nil { + fmt.Println(err) + } + var newestFile string + var newestTime int64 = 0 + for _, f := range candidates { + // loop through save file candidates + if strings.Contains(f.Name(), "lock") { + fi, err := os.Stat(userPath + f.Name()) + if err != nil { + fmt.Println(err) + } + currTime := fi.ModTime().Unix() + if currTime > newestTime { + newestTime = currTime + newestFile = f.Name() + } + } + } + + // get prefix for latest file + latestFile := strings.Split(newestFile, ".") + //fmt.Printf("recover string: %s -d %s/user/%s %s", config.NethackLauncher.RecoverBinary, config.NethackLauncher.HackDir, username, latestFile[0]) + + // run recover on file + out, err := exec.Command(config.NethackLauncher.RecoverBinary, "-d", fmt.Sprintf("%s/user/%s", config.NethackLauncher.HackDir, username), latestFile[0]).Output() + if err != nil { + fmt.Println(out) + panic(err) + } + + // make sure save file exists before removing the extra locks + isEmpty, _ = checkDir(fmt.Sprintf("%s/user/%s/save/", config.NethackLauncher.HackDir, username)) + if !isEmpty { + // save file made it, clean up old cruft + files, err := filepath.Glob(fmt.Sprintf("%s/*lock*", userPath)) + if err != nil { + panic(err) + } + for _, f := range files { + if err := os.Remove(f); err != nil { + panic(err) + } + } + // alert user that the save file was recovered + println("") + println(" Statefile was recovered successfully! (press enter to return)") + fmt.Printf(">> ") + } + case "\n": + clearScreen() + printUserScreen(redisClient, username) + default: + } + } +} + func janitor(redisClient *redis.Client) { // loop through the set for {