|
|
@@ -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) |
|
|
|
} |