diff --git a/.gitignore b/.gitignore index acb9124..487180a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ go.sum # build binary -leadercraft-s +leadercraft-c # sqlite default db test.sqlite diff --git a/main.go b/main.go new file mode 100644 index 0000000..d7fb31e --- /dev/null +++ b/main.go @@ -0,0 +1,226 @@ +// 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) +}