diff --git a/.gitignore b/.gitignore index bd33cc1..9cb6c63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ pokerogue-server.exe userdata/* +key diff --git a/api/daily.go b/api/daily.go new file mode 100644 index 0000000..c275a46 --- /dev/null +++ b/api/daily.go @@ -0,0 +1,36 @@ +package api + +import ( + "encoding/base64" + "log" + "net/http" + "time" + + "github.com/Flashfyre/pokerogue-server/db" +) + +var ( + dailyRunSeed string +) + +func ScheduleDailyRunRefresh() { + scheduler.Every(1).Day().At("00:00").Do(func() { + InitDailyRun() + }) +} + +func InitDailyRun() { + dailyRunSeed = base64.StdEncoding.EncodeToString(SeedFromTime(time.Now().UTC())) + err := db.TryAddDailyRun(dailyRunSeed) + if err != nil { + log.Print(err.Error()) + } else { + log.Printf("Daily Run Seed: %s", dailyRunSeed) + } +} + +// /daily/seed - get daily run seed + +func (s *Server) HandleSeed(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(dailyRunSeed)) +} diff --git a/api/generic.go b/api/generic.go index c8f4c9f..7a9cce6 100644 --- a/api/generic.go +++ b/api/generic.go @@ -3,12 +3,19 @@ package api import ( "encoding/gob" "net/http" + "time" + + "github.com/go-co-op/gocron" ) type Server struct { Debug bool } +var ( + scheduler = gocron.NewScheduler(time.UTC) +) + func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.Debug { w.Header().Set("Access-Control-Allow-Headers", "*") @@ -40,6 +47,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.HandleSavedataUpdate(w, r) case "/savedata/delete": s.HandleSavedataDelete(w, r) + case "/savedata/clear": + s.HandleSavedataClear(w, r) + + case "/daily/seed": + s.HandleSeed(w, r) } } diff --git a/api/savedata-helper.go b/api/savedata-helper.go new file mode 100644 index 0000000..8c11381 --- /dev/null +++ b/api/savedata-helper.go @@ -0,0 +1,83 @@ +package api + +import ( + "bytes" + "encoding/gob" + "encoding/hex" + "fmt" + "os" + "strconv" + + "github.com/klauspost/compress/zstd" +) + +func GetSystemSaveData(uuid []byte) (SystemSaveData, error) { + var system SystemSaveData + + save, err := os.ReadFile("userdata/" + hex.EncodeToString(uuid) + "/system.pzs") + if err != nil { + return system, fmt.Errorf("failed to read save file: %s", err) + } + + zstdReader, err := zstd.NewReader(nil) + if err != nil { + return system, fmt.Errorf("failed to create zstd reader: %s", err) + } + + decompressed, err := zstdReader.DecodeAll(save, nil) + if err != nil { + return system, fmt.Errorf("failed to decompress save file: %s", err) + } + + gobDecoderBuf := bytes.NewBuffer(decompressed) + + err = gob.NewDecoder(gobDecoderBuf).Decode(&system) + if err != nil { + return system, fmt.Errorf("failed to deserialize save: %s", err) + } + + return system, nil +} + +func GetSessionSaveData(uuid []byte, slotId int) (SessionSaveData, error) { + var session SessionSaveData + + fileName := "session" + if slotId != 0 { + fileName += strconv.Itoa(slotId) + } + + save, err := os.ReadFile(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName)) + if err != nil { + return session, fmt.Errorf("failed to read save file: %s", err) + } + + zstdReader, err := zstd.NewReader(nil) + if err != nil { + return session, fmt.Errorf("failed to create zstd reader: %s", err) + } + + decompressed, err := zstdReader.DecodeAll(save, nil) + if err != nil { + return session, fmt.Errorf("failed to decompress save file: %s", err) + } + + gobDecoderBuf := bytes.NewBuffer(decompressed) + + err = gob.NewDecoder(gobDecoderBuf).Decode(&session) + if err != nil { + return session, fmt.Errorf("failed to deserialize save: %s", err) + } + + return session, nil +} + +func ValidateSessionCompleted(session SessionSaveData) bool { + switch session.GameMode { + case 0: + return session.WaveIndex == 200 + case 3: + return session.WaveIndex == 50 + } + return false +} diff --git a/api/savedata.go b/api/savedata.go index 5dedc38..bb7b8d6 100644 --- a/api/savedata.go +++ b/api/savedata.go @@ -26,34 +26,11 @@ func (s *Server) HandleSavedataGet(w http.ResponseWriter, r *http.Request) { return } - hexUuid := hex.EncodeToString(uuid) - switch r.URL.Query().Get("datatype") { case "0": // System - save, err := os.ReadFile("userdata/" + hexUuid + "/system.pzs") + system, err := GetSystemSaveData(uuid) if err != nil { - http.Error(w, fmt.Sprintf("failed to read save file: %s", err), http.StatusInternalServerError) - return - } - - zstdReader, err := zstd.NewReader(nil) - if err != nil { - http.Error(w, fmt.Sprintf("failed to create zstd reader: %s", err), http.StatusInternalServerError) - return - } - - decompressed, err := zstdReader.DecodeAll(save, nil) - if err != nil { - http.Error(w, fmt.Sprintf("failed to decompress save file: %s", err), http.StatusInternalServerError) - return - } - - gobDecoderBuf := bytes.NewBuffer(decompressed) - - var system SystemSaveData - err = gob.NewDecoder(gobDecoderBuf).Decode(&system) - if err != nil { - http.Error(w, fmt.Sprintf("failed to deserialize save: %s", err), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -76,35 +53,9 @@ func (s *Server) HandleSavedataGet(w http.ResponseWriter, r *http.Request) { return } - fileName := "session" - if slotId != 0 { - fileName += strconv.Itoa(slotId) - } - - save, err := os.ReadFile(fmt.Sprintf("userdata/%s/%s.pzs", hexUuid, fileName)) + session, err := GetSessionSaveData(uuid, slotId) if err != nil { - http.Error(w, fmt.Sprintf("failed to read save file: %s", err), http.StatusInternalServerError) - return - } - - zstdReader, err := zstd.NewReader(nil) - if err != nil { - http.Error(w, fmt.Sprintf("failed to create zstd reader: %s", err), http.StatusInternalServerError) - return - } - - decompressed, err := zstdReader.DecodeAll(save, nil) - if err != nil { - http.Error(w, fmt.Sprintf("failed to decompress save file: %s", err), http.StatusInternalServerError) - return - } - - gobDecoderBuf := bytes.NewBuffer(decompressed) - - var session SessionSaveData - err = gob.NewDecoder(gobDecoderBuf).Decode(&session) - if err != nil { - http.Error(w, fmt.Sprintf("failed to deserialize save: %s", err), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -287,3 +238,70 @@ func (s *Server) HandleSavedataDelete(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + +type SavedataClearResponse struct { + Success bool `json:"success"` +} + +// /savedata/clear - mark session save data as cleared and delete + +func (s *Server) HandleSavedataClear(w http.ResponseWriter, r *http.Request) { + uuid, err := GetUuidFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = db.UpdateAccountLastActivity(uuid) + if err != nil { + log.Print("failed to update account last activity") + } + + slotId, err := strconv.Atoi(r.URL.Query().Get("slot")) + if err != nil { + http.Error(w, fmt.Sprintf("failed to convert slot id: %s", err), http.StatusBadRequest) + return + } + + if slotId < 0 || slotId >= sessionSlotCount { + http.Error(w, fmt.Sprintf("slot id %d out of range", slotId), http.StatusBadRequest) + return + } + + session, err := GetSessionSaveData(uuid, slotId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + sessionCompleted := ValidateSessionCompleted(session) + newCompletion := false + + if sessionCompleted { + newCompletion, err = db.TryAddSeedCompletion(uuid, session.Seed) + if err != nil { + log.Print("failed to mark seed as completed") + } + } + + response, err := json.Marshal(SavedataClearResponse{Success: newCompletion}) + if err != nil { + http.Error(w, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError) + return + } + + if sessionCompleted { + fileName := "session" + if slotId != 0 { + fileName += strconv.Itoa(slotId) + } + + err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName)) + if err != nil && !os.IsNotExist(err) { + http.Error(w, fmt.Sprintf("failed to delete save file: %s", err), http.StatusInternalServerError) + return + } + } + + w.Write(response) +} diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 0000000..aa91f94 --- /dev/null +++ b/api/utils.go @@ -0,0 +1,23 @@ +package api + +import ( + "crypto/md5" + "encoding/binary" + "math" + "time" +) + +var seedKey []byte // 32 bytes + +func SetSeedKey(key []byte) { + seedKey = key +} + +func SeedFromTime(seedTime time.Time) []byte { + day := make([]byte, 8) + binary.BigEndian.PutUint64(day, uint64(math.Floor(float64(seedTime.Unix())/float64(time.Hour*24)))) + + sum := md5.Sum(append(seedKey, day...)) + + return sum[:] +} diff --git a/db/daily.go b/db/daily.go new file mode 100644 index 0000000..6d8506a --- /dev/null +++ b/db/daily.go @@ -0,0 +1,10 @@ +package db + +func TryAddDailyRun(seed string) error { + _, err := handle.Exec("INSERT INTO dailyRuns (seed, date) VALUES (?, UTC_DATE()) ON DUPLICATE KEY UPDATE date = date", seed) + if err != nil { + return err + } + + return nil +} diff --git a/db/savedata.go b/db/savedata.go new file mode 100644 index 0000000..bc7fd80 --- /dev/null +++ b/db/savedata.go @@ -0,0 +1,24 @@ +package db + +func TryAddSeedCompletion(uuid []byte, seed string) (bool, error) { + if len(seed) < 24 { + for range 24 - len(seed) { + seed += "0" + } + } + + var count int + err := handle.QueryRow("SELECT COUNT(*) FROM seedCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count) + if err != nil { + return false, err + } else if count > 0 { + return false, nil + } + + _, err = handle.Exec("INSERT INTO seedCompletions (uuid, seed, timestamp) VALUES (?, ?, UTC_TIMESTAMP())", uuid, seed) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/go.mod b/go.mod index a93c242..56c6b19 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,15 @@ module github.com/Flashfyre/pokerogue-server go 1.22 require ( + github.com/go-co-op/gocron v1.37.0 github.com/go-sql-driver/mysql v1.7.1 github.com/klauspost/compress v1.17.4 golang.org/x/crypto v0.16.0 ) -require golang.org/x/sys v0.15.0 // indirect +require ( + github.com/google/uuid v1.4.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) diff --git a/go.sum b/go.sum index d95a3d3..cf32cb2 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,46 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= +github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pokerogue-server.go b/pokerogue-server.go index 7f747a8..0e14c32 100644 --- a/pokerogue-server.go +++ b/pokerogue-server.go @@ -11,6 +11,8 @@ import ( "github.com/Flashfyre/pokerogue-server/db" ) +var key []byte + func main() { debug := flag.Bool("debug", false, "debug mode") @@ -43,6 +45,15 @@ func main() { os.Chmod(*addr, 0777) } + key, err = os.ReadFile("key") + if err != nil { + log.Fatalf("failed to read key file!") + } + + api.SetSeedKey(key) + api.ScheduleDailyRunRefresh() + api.InitDailyRun() + err = http.Serve(listener, &api.Server{Debug: *debug}) if err != nil { log.Fatalf("failed to create http server or server errored: %s", err)