// NGnius 2020-02-27 package main import ( "bytes" "encoding/json" "flag" "fmt" "io/ioutil" "net/http" "os" "os/signal" "path/filepath" "strconv" ) const ( defaultPassword = "" defaultEntryURL = "http://localhost:1337/record" defaultPort = "9000" defaultDir = "criterias" defaultCriteriaJson = "criteria.json" ) var ( // cli params password string entryURL string port string dir string // internal handler http.Handler server *http.Server client *http.Client isClosing bool referenceCriteria map[string]Criteria ) // json structs // NewEntryJSON a new entry to be saved to a leaderboard type NewEntryJSON struct { // from leadercraft-s Score int64 PlayerID int64 BoardID int64 Password string } // Criteria a set of values that must be met type Criteria struct { Location [2][3]float64 // (min coords (x,y,z), max coords (x,y,z)) Time int64 // time since start of game (seconds) GameID int64 // game/board id in leadercraft-s and in Gamecraft (workshop ID?) PlayerID int64 // player id in leadercraft-s and in Gamecraft (steam ID) Coefficient float64 // coefficient for calculating the score Complete bool // game has been completed Points int64 // points scored in game ScoreMode string // score calculation mode } func (c *Criteria) Meets(c2 *Criteria) bool { meets := false if c2.Location[0][0] != 0.0 && c2.Location[0][1] != 0.0 && c2.Location[0][2] != 0.0 && c2.Location[1][0] != 0.0 && c2.Location[1][1] != 0.0 && c2.Location[1][2] != 0.0 { // criteria is location-based meets = c.Location[0][0] >= c2.Location[0][0] && c.Location[0][1] >= c2.Location[0][1] && c.Location[0][2] >= c2.Location[0][2] meets = meets && c.Location[1][0] <= c2.Location[1][0] && c.Location[1][1] <= c2.Location[1][1] && c.Location[1][2] <= c2.Location[1][2] return meets } if c2.Complete { return c.Complete && (c.Points >= c2.Points) } return meets } func (c *Criteria) Score(c2 *Criteria) int64 { if c2.ScoreMode == "time" { return int64(c2.Coefficient / float64(c.Time)) } if c2.ScoreMode == "points" { return int64(c2.Coefficient * float64(c.Points)) } return 0 } func init() { flag.StringVar(&password, "entry-pwd", defaultPassword, "Password for new entry POST requests") flag.StringVar(&entryURL, "url", defaultEntryURL, "URL for new entry POST requests") flag.StringVar(&port, "port", defaultPort, "Port to listen on") flag.StringVar(&dir, "dir", defaultDir, "Working directory") } func main() { flag.Parse() serverMux := http.NewServeMux() serverMux.HandleFunc("/criteria", criteriaHandler) signalChan := make(chan os.Signal) signal.Notify(signalChan, os.Interrupt) go func() { s := <-signalChan fmt.Println("Received terminate signal " + s.String()) isClosing = true server.Close() }() server = &http.Server{ Addr: ":" + port, Handler: handler, } client = &http.Client{} fmt.Println("Starting on " + server.Addr) err := server.ListenAndServe() if err != nil && !isClosing { fmt.Println(err) } } // criteria POST request handler // this also sends a new entry to leadercraft-s when the criteria is met func criteriaHandler(w http.ResponseWriter, r *http.Request) { // TODO if r.Method != "POST" { w.WriteHeader(405) return } authHeader := r.Header.Get("Authorization") w.Header().Add("Content-Type", "application/json") //w.Header().Add("Access-Control-Allow-Origin", "*") data, readErr := ioutil.ReadAll(r.Body) if readErr != nil { // body could not be read properly w.WriteHeader(500) return } reqCriteria := &Criteria{} unmarshErr := json.Unmarshal(data, reqCriteria) if unmarshErr != nil { // body could not be interpreted as json w.WriteHeader(400) return } // TODO load criteria for game id criteriaFilename := filepath.Join(dir, fmt.Sprintf("%d", reqCriteria.GameID)+".json") realCriteria := &Criteria{} realCriteriaF, openErr := os.Open(criteriaFilename) if openErr != nil { w.WriteHeader(404) return } realCritData, realCritReadErr := ioutil.ReadAll(realCriteriaF) if realCritReadErr != nil { w.WriteHeader(404) return } unmarshCritErr := json.Unmarshal(realCritData, realCriteria) if unmarshCritErr != nil { // criteria file is invalid json w.WriteHeader(404) return } if reqCriteria.GameID > 1 { f, fileErr := os.Open(filepath.Join(dir, "criteria-"+strconv.Itoa(int(reqCriteria.GameID))+".json")) if fileErr != nil { // file not found w.WriteHeader(404) return } data, readErr = ioutil.ReadAll(f) if readErr != nil { // file could not be read properly (file doesn't exist?) w.WriteHeader(404) return } unmarshErr = json.Unmarshal(data, &realCriteria) if unmarshErr != nil { // data could not be interpreted as json w.WriteHeader(500) return } } else { // Game ID cannot exist w.WriteHeader(404) return } // TODO check if criteria matches if !realCriteria.Meets(reqCriteria) { // if criteria does not match, stop w.WriteHeader(400) return } // if criteria matches, send new entry to leadercraft-s entry := NewEntryJSON{ Score: realCriteria.Score(reqCriteria), PlayerID: reqCriteria.PlayerID, BoardID: realCriteria.GameID, Password: password, } echoData, marshErr := json.Marshal(entry) if marshErr != nil { w.WriteHeader(500) return } echoBody := bytes.NewReader(echoData) entryReq, reqErr := http.NewRequest("POST", entryURL, echoBody) if reqErr != nil { // malformed request parameters w.WriteHeader(500) return } entryReq.Header.Add("Authorization", authHeader) entryReq.Header.Add("Content-Type", "application/json") echoResp, postErr := client.Do(entryReq) if postErr != nil { // bad communication or malformed request w.WriteHeader(500) return } // echo new entry request response to original sender w.WriteHeader(echoResp.StatusCode) echoRespData, echoReadErr := ioutil.ReadAll(echoResp.Body) if echoReadErr != nil { // body read error (should never occur) return } w.Write(echoRespData) }