package main import ( "bufio" "fmt" "io" "io/ioutil" "os" "os/exec" "strconv" "strings" "sync" "time" "github.com/unixvoid/glogger" "golang.org/x/crypto/sha3" "gopkg.in/gcfg.v1" "gopkg.in/redis.v5" ) type Config struct { NethackLauncher struct { Loglevel string ServerDisplay string NethackVersion string HackDir string NhdatLocation string BootstrapDelay time.Duration } Redis struct { Host string Password string } } var ( config = Config{} wg sync.WaitGroup ) func main() { // read conf file readConf() // init config file and logger initLogger() // init redis connection redisClient, redisErr := initRedisConnection() if redisErr != nil { glogger.Debug.Printf("redis connection cannot be made, trying again in %s second(s)\n", config.NethackLauncher.BootstrapDelay*time.Second) time.Sleep(config.NethackLauncher.BootstrapDelay * time.Second) redisClient, redisErr = initRedisConnection() if redisErr != nil { glogger.Error.Println("redis connection cannot be made, exiting.") panic(redisErr) } } else { glogger.Debug.Println("connection to redis succeeded.") } // create initial files needed by nethack createInitialFiles() // start janitor go janitor(redisClient) // start homescreen screenFunction := printWelcomeScreen(redisClient) fmt.Printf("screen %s recieved\n", screenFunction) } func readConf() { // init config file err := gcfg.ReadFileInto(&config, "config.gcfg") if err != nil { panic(fmt.Sprintf("Could not load config.gcfg, error: %s\n", err)) } } func initLogger() { // init logger if config.NethackLauncher.Loglevel == "debug" { glogger.LogInit(os.Stdout, os.Stdout, os.Stdout, os.Stderr) } else if config.NethackLauncher.Loglevel == "cluster" { glogger.LogInit(os.Stdout, os.Stdout, ioutil.Discard, os.Stderr) } else if config.NethackLauncher.Loglevel == "info" { glogger.LogInit(os.Stdout, ioutil.Discard, ioutil.Discard, os.Stderr) } else { glogger.LogInit(ioutil.Discard, ioutil.Discard, ioutil.Discard, os.Stderr) } } func initRedisConnection() (*redis.Client, error) { // init redis connection redisClient := redis.NewClient(&redis.Options{ Addr: config.Redis.Host, Password: config.Redis.Password, DB: 0, }) _, redisErr := redisClient.Ping().Result() return redisClient, redisErr } func checkFiles() { // make sure record file exists if _, err := os.Stat(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir)); os.IsNotExist(err) { glogger.Info.Printf("record file not found in %s/record\n", config.NethackLauncher.HackDir) fmt.Printf("%s\n", err) os.Exit(1) } // make sure initial rcfile exists hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) if _, err := os.Stat(hackRCLoc); os.IsNotExist(err) { glogger.Info.Printf("initial config file not found at: %s\n", hackRCLoc) fmt.Printf("%s\n", err) os.Exit(1) } } func clearScreen() { cmd := exec.Command("clear") cmd.Stdout = os.Stdout cmd.Run() } func printWelcomeScreen(redisClient *redis.Client) string { clearScreen() fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" Not logged in.") println("") println(" l) Login") println(" r) Register new user") println(" w) Watch games in progress") println(" h) View highscores") println(" q) Quit") 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 "l": // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() printLoginScreen(redisClient) case "r": // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() printRegisterScreen(redisClient) case "w": clearScreen() printProgressScreen(redisClient, "") case "h": clearScreen() printHighScores(redisClient, "") case "q": exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() os.Exit(0) default: } } } func printUserScreen(redisClient *redis.Client, username string) string { clearScreen() fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") fmt.Printf(" Logged in as: %s\n", username) println("") println(" l) Logout") println(" w) Watch games in progress") println(" h) View highscores") println(" e) Edit config") fmt.Printf(" p) Play NetHack %s\n", config.NethackLauncher.NethackVersion) println(" q) Quit") 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 "l": // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() printWelcomeScreen(redisClient) case "e": hackRCLoc := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("vim", "-Z", hackRCLoc) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() clearScreen() printUserScreen(redisClient, username) case "w": clearScreen() printProgressScreen(redisClient, username) case "h": clearScreen() printHighScores(redisClient, username) case "p": wg.Add(1) currentTime := time.Now().UTC() fulltime := currentTime.Format("2006-01-02.03:04:05") go runGame(username, fulltime) watcher := startWatcher(username, fulltime, redisClient) wg.Wait() fmt.Println("CLOSING") close(watcher) printUserScreen(redisClient, username) case "q": exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() os.Exit(0) default: } } } func printLoginScreen(redisClient *redis.Client) { fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" Please enter your username. (blank entry aborts)") println("") fmt.Printf(">> ") scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { if scanner.Text() == "" { printWelcomeScreen(redisClient) } // check redis for user username := scanner.Text() storedHash, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() if err != nil { // user does not exist fmt.Printf(" There was a problem with your last entry.\n>> ") } else { // get password from user and compare // turn off echo display exec.Command("stty", "-F", "/dev/tty", "-echo").Run() noPass := true for noPass { fmt.Printf("\n Please enter your password (blank entry aborts).\n>> ") reader := bufio.NewReader(os.Stdin) typedAuth, _ := reader.ReadString('\n') typedAuth = strings.Replace(typedAuth, "\n", "", -1) if typedAuth == "" { // exit to main menu printWelcomeScreen(redisClient) } // get hash of typedAuth typedHash := sha3.Sum512([]byte(typedAuth)) if fmt.Sprintf("%x", typedHash) == storedHash { // user authed printUserScreen(redisClient, username) noPass = false } else { fmt.Printf("\n There was a problem with your last entry.") } } } } if err := scanner.Err(); err != nil { fmt.Printf("%s\n", err) } } func printRegisterScreen(redisClient *redis.Client) { // TODO : configure password restriction checking fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") println(" Welcome new user. Please enter a username") println(" Only characters and numbers are allowed, with no spaces.") println(" 20 characters max. (blank entry aborts)") println("") fmt.Printf(">> ") scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { if scanner.Text() == "" { printWelcomeScreen(redisClient) } // check redis for user username := scanner.Text() _, err := redisClient.Get(fmt.Sprintf("user:%s", username)).Result() if err != redis.Nil { // user already exists fmt.Printf("There was a problem with your last entry.\n>> ") } else { // set up user // turn off echo display exec.Command("stty", "-F", "/dev/tty", "-echo").Run() reader := bufio.NewReader(os.Stdin) noPass := true sec := "" for noPass { // pull pass the first time fmt.Printf(" Please enter your password (blank entry aborts).\n>> ") sec0, _ := reader.ReadString('\n') sec0 = strings.Replace(sec0, "\n", "", -1) if sec0 == "" { printWelcomeScreen(redisClient) } // pull pass the second time fmt.Printf("\n Please enter your password again.\n>> ") sec1, _ := reader.ReadString('\n') sec1 = strings.Replace(sec1, "\n", "", -1) // make sure passwords match if sec0 == sec1 { sec = sec0 noPass = false } else { fmt.Println("Lets try that again.") } } // reset display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() // set user in redis secHash := sha3.Sum512([]byte(sec)) redisClient.Set(fmt.Sprintf("user:%s", username), fmt.Sprintf("%x", secHash), 0).Err() // create user directories userPath := fmt.Sprintf("%s/user/%s/ttyrec/", config.NethackLauncher.HackDir, username) exec.Command("mkdir", "-p", userPath).Run() // copy in rc file hackRCLoc := fmt.Sprintf("%s/.nethackrc", config.NethackLauncher.HackDir) hackRCDest := fmt.Sprintf("%s/user/%s/.nethackrc", config.NethackLauncher.HackDir, username) exec.Command("cp", hackRCLoc, hackRCDest).Run() // back to main screen printUserScreen(redisClient, username) } } if err := scanner.Err(); err != nil { fmt.Printf("%s\n", err) } } func printProgressScreen(redisClient *redis.Client, username string) { // print header fmt.Printf(" %s\n", config.NethackLauncher.ServerDisplay) println("") // check directory for live players isNotEmpty, err := redisClient.Exists("inprogress").Result() if err != nil { panic(err) } if isNotEmpty { println(" Choose a player to spectate ('enter' without selection returns)") } else { println(" No live players currently (blank entry returns)") } println("") inProg := make(map[int]string) inProgTimer := 1 inprogress, err := redisClient.SMembers("inprogress").Result() if err != nil { panic(err) } for _, i := range inprogress { // loop through users fmt.Printf(" %d) %s\n", inProgTimer, i) // add user to line map inProg[inProgTimer] = i inProgTimer++ } println("") fmt.Printf(">> ") // start user input // 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) s, _ := strconv.Atoi(string(b)) // check if user is trying to navigate if string(b) == "\n" { exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() if username == "" { printWelcomeScreen(redisClient) } else { printUserScreen(redisClient, username) } } // check if selection is in out map if inProg[s] != "" { user := strings.Split(inProg[s], ":") fmt.Printf("going to spectate '%s'\n", user[0]) // set ttyrec path ttyName, _ := redisClient.Get(fmt.Sprintf("inprogress:%s", user[0])).Result() ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, user[0], ttyName) // restart display exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("ttyplay", "-p", ttyrecPath) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() if username == "" { printWelcomeScreen(redisClient) } else { printUserScreen(redisClient, username) } // TODO fix bug where user has to when game exists } } } func runGame(username, timestamp string) { // put together users home dir homeDir := fmt.Sprintf("%s/user/%s/", config.NethackLauncher.HackDir, username) nhCommand := fmt.Sprintf("HOME=%s nethack -d %s -u %s\n", homeDir, config.NethackLauncher.HackDir, username) ttyrecPath := fmt.Sprintf("%s/user/%s/ttyrec/%s.ttyrec", config.NethackLauncher.HackDir, username, timestamp) exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("ttyrec", ttyrecPath, "-e", nhCommand) nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() exec.Command("exit").Run() wg.Done() } func startWatcher(username, timestamp string, redisClient *redis.Client) chan struct{} { // create initial keys redisClient.SAdd("inprogress", username) redisClient.Set(fmt.Sprintf("inprogress:%s", username), timestamp, 0) ch := make(chan struct{}) // enter inital inprogress yet go func() { for { select { case <-ch: return default: redisClient.Expire(fmt.Sprintf("inprogress:%s", username), 10*time.Second) time.Sleep(8 * time.Second) } } }() return ch } func createInitialFiles() { // create necessary directories if they dont exist if _, err := os.Stat(config.NethackLauncher.HackDir); os.IsNotExist(err) { os.Mkdir(config.NethackLauncher.HackDir, os.ModeDir) } if _, err := os.Stat(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { os.Mkdir(fmt.Sprintf("%s/dumps/", config.NethackLauncher.HackDir), os.ModeDir) } if _, err := os.Stat(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir)); os.IsNotExist(err) { os.Mkdir(fmt.Sprintf("%s/save/", config.NethackLauncher.HackDir), os.ModeDir) } // create necessary files if they dont exist os.OpenFile(fmt.Sprintf("%s/logfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/perm", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/record", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) os.OpenFile(fmt.Sprintf("%s/xlogfile", config.NethackLauncher.HackDir), os.O_RDONLY|os.O_CREATE, 0666) // move in nhdat file if it does not exist if _, err := os.Stat(fmt.Sprintf("%s/nhdat", config.NethackLauncher.HackDir)); os.IsNotExist(err) { exec.Command("cp", config.NethackLauncher.NhdatLocation, config.NethackLauncher.HackDir).Run() } } func janitor(redisClient *redis.Client) { // loop through the set for { inprogress, err := redisClient.SMembers("inprogress").Result() if err != nil { panic(err) } for _, i := range inprogress { // for each user, make sure the expire key exists exists, err := redisClient.Exists(fmt.Sprintf("inprogress:%s", i)).Result() if err != nil { panic(err) } if !exists { redisClient.SRem("inprogress", i) } } time.Sleep(10 * time.Second) } } func printHighScores(redisClient *redis.Client, username string) { exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() nh := exec.Command("nethack", "-d", config.NethackLauncher.HackDir, "-s") nh.Stdout = os.Stdout nh.Stdin = os.Stdin nh.Stderr = os.Stderr nh.Run() println("") println(" Press enter to return to menu") println("") fmt.Printf(">> ") // allow user back to home screen // 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) // check if user is trying to navigate if string(b) == "\n" { exec.Command("stty", "-F", "/dev/tty", "echo", "-cbreak").Run() clearScreen() if username == "" { printWelcomeScreen(redisClient) } else { printUserScreen(redisClient, username) } } } } func checkDir(dirName string) (bool, error) { f, err := os.Open(dirName) if err != nil { return false, err } defer f.Close() _, err = f.Readdirnames(1) if err == io.EOF { return true, nil } return false, err }