diff --git a/api/account.go b/api/account.go deleted file mode 100644 index d966efb..0000000 --- a/api/account.go +++ /dev/null @@ -1,141 +0,0 @@ -package api - -import ( - "bytes" - "crypto/rand" - "database/sql" - "encoding/base64" - "fmt" - "os" - "regexp" - "strconv" - "time" - - "github.com/pagefaultgames/pokerogue-server/db" -) - -const ( - UUIDSize = 16 - TokenSize = 32 -) - -var isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString - -type AccountInfoResponse struct { - Username string `json:"username"` - LastSessionSlot int `json:"lastSessionSlot"` -} - -// /account/info - get account info -func handleAccountInfo(username string, uuid []byte) (AccountInfoResponse, error) { - var latestSave time.Time - latestSaveID := -1 - for id := range sessionSlotCount { - fileName := "session" - if id != 0 { - fileName += strconv.Itoa(id) - } - - stat, err := os.Stat(fmt.Sprintf("userdata/%x/%s.pzs", uuid, fileName)) - if err != nil { - continue - } - - if stat.ModTime().After(latestSave) { - latestSave = stat.ModTime() - latestSaveID = id - } - } - - return AccountInfoResponse{Username: username, LastSessionSlot: latestSaveID}, nil -} - -type AccountRegisterRequest GenericAuthRequest - -// /account/register - register account -func handleAccountRegister(request AccountRegisterRequest) error { - if !isValidUsername(request.Username) { - return fmt.Errorf("invalid username") - } - - if len(request.Password) < 6 { - return fmt.Errorf("invalid password") - } - - uuid := make([]byte, UUIDSize) - _, err := rand.Read(uuid) - if err != nil { - return fmt.Errorf("failed to generate uuid: %s", err) - } - - salt := make([]byte, ArgonSaltSize) - _, err = rand.Read(salt) - if err != nil { - return fmt.Errorf(fmt.Sprintf("failed to generate salt: %s", err)) - } - - err = db.AddAccountRecord(uuid, request.Username, deriveArgon2IDKey([]byte(request.Password), salt), salt) - if err != nil { - return fmt.Errorf("failed to add account record: %s", err) - } - - return nil -} - -type AccountLoginRequest GenericAuthRequest -type AccountLoginResponse GenericAuthResponse - -// /account/login - log into account -func handleAccountLogin(request AccountLoginRequest) (AccountLoginResponse, error) { - if !isValidUsername(request.Username) { - return AccountLoginResponse{}, fmt.Errorf("invalid username") - } - - if len(request.Password) < 6 { - return AccountLoginResponse{}, fmt.Errorf("invalid password") - } - - key, salt, err := db.FetchAccountKeySaltFromUsername(request.Username) - if err != nil { - if err == sql.ErrNoRows { - return AccountLoginResponse{}, fmt.Errorf("account doesn't exist") - } - - return AccountLoginResponse{}, err - } - - if !bytes.Equal(key, deriveArgon2IDKey([]byte(request.Password), salt)) { - return AccountLoginResponse{}, fmt.Errorf("password doesn't match") - } - - token := make([]byte, TokenSize) - _, err = rand.Read(token) - if err != nil { - return AccountLoginResponse{}, fmt.Errorf("failed to generate token: %s", err) - } - - err = db.AddAccountSession(request.Username, token) - if err != nil { - return AccountLoginResponse{}, fmt.Errorf("failed to add account session") - } - - return AccountLoginResponse{Token: base64.StdEncoding.EncodeToString(token)}, nil -} - -// /account/logout - log out of account -func handleAccountLogout(token []byte) error { - if len(token) != TokenSize { - return fmt.Errorf("invalid token") - } - - err := db.RemoveSessionFromToken(token) - if err != nil { - if err == sql.ErrNoRows { - return fmt.Errorf("token not found") - } - - return fmt.Errorf("failed to remove account session") - } - - return nil -} diff --git a/api/account/common.go b/api/account/common.go new file mode 100644 index 0000000..93f17ed --- /dev/null +++ b/api/account/common.go @@ -0,0 +1,30 @@ +package account + +import ( + "regexp" + + "golang.org/x/crypto/argon2" +) + +type GenericAuthRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type GenericAuthResponse struct { + Token string `json:"token"` +} + +const ( + ArgonTime = 1 + ArgonMemory = 256 * 1024 + ArgonThreads = 4 + ArgonKeySize = 32 + ArgonSaltSize = 16 +) + +var isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString + +func deriveArgon2IDKey(password, salt []byte) []byte { + return argon2.IDKey(password, salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize) +} diff --git a/api/account/info.go b/api/account/info.go new file mode 100644 index 0000000..de8c6fc --- /dev/null +++ b/api/account/info.go @@ -0,0 +1,39 @@ +package account + +import ( + "fmt" + "os" + "strconv" + "time" + + "github.com/pagefaultgames/pokerogue-server/defs" +) + +type InfoResponse struct { + Username string `json:"username"` + LastSessionSlot int `json:"lastSessionSlot"` +} + +// /account/info - get account info +func Info(username string, uuid []byte) (InfoResponse, error) { + var latestSave time.Time + latestSaveID := -1 + for id := range defs.SessionSlotCount { + fileName := "session" + if id != 0 { + fileName += strconv.Itoa(id) + } + + stat, err := os.Stat(fmt.Sprintf("userdata/%x/%s.pzs", uuid, fileName)) + if err != nil { + continue + } + + if stat.ModTime().After(latestSave) { + latestSave = stat.ModTime() + latestSaveID = id + } + } + + return InfoResponse{Username: username, LastSessionSlot: latestSaveID}, nil +} diff --git a/api/account/login.go b/api/account/login.go new file mode 100644 index 0000000..9bddb2c --- /dev/null +++ b/api/account/login.go @@ -0,0 +1,51 @@ +package account + +import ( + "bytes" + "crypto/rand" + "database/sql" + "encoding/base64" + "fmt" + + "github.com/pagefaultgames/pokerogue-server/db" +) + +type LoginRequest GenericAuthRequest +type LoginResponse GenericAuthResponse + +// /account/login - log into account +func Login(request LoginRequest) (LoginResponse, error) { + if !isValidUsername(request.Username) { + return LoginResponse{}, fmt.Errorf("invalid username") + } + + if len(request.Password) < 6 { + return LoginResponse{}, fmt.Errorf("invalid password") + } + + key, salt, err := db.FetchAccountKeySaltFromUsername(request.Username) + if err != nil { + if err == sql.ErrNoRows { + return LoginResponse{}, fmt.Errorf("account doesn't exist") + } + + return LoginResponse{}, err + } + + if !bytes.Equal(key, deriveArgon2IDKey([]byte(request.Password), salt)) { + return LoginResponse{}, fmt.Errorf("password doesn't match") + } + + token := make([]byte, TokenSize) + _, err = rand.Read(token) + if err != nil { + return LoginResponse{}, fmt.Errorf("failed to generate token: %s", err) + } + + err = db.AddAccountSession(request.Username, token) + if err != nil { + return LoginResponse{}, fmt.Errorf("failed to add account session") + } + + return LoginResponse{Token: base64.StdEncoding.EncodeToString(token)}, nil +} diff --git a/api/account/logout.go b/api/account/logout.go new file mode 100644 index 0000000..db6c6fb --- /dev/null +++ b/api/account/logout.go @@ -0,0 +1,26 @@ +package account + +import ( + "database/sql" + "fmt" + + "github.com/pagefaultgames/pokerogue-server/db" +) + +// /account/logout - log out of account +func Logout(token []byte) error { + if len(token) != TokenSize { + return fmt.Errorf("invalid token") + } + + err := db.RemoveSessionFromToken(token) + if err != nil { + if err == sql.ErrNoRows { + return fmt.Errorf("token not found") + } + + return fmt.Errorf("failed to remove account session") + } + + return nil +} diff --git a/api/account/register.go b/api/account/register.go new file mode 100644 index 0000000..664dea8 --- /dev/null +++ b/api/account/register.go @@ -0,0 +1,45 @@ +package account + +import ( + "crypto/rand" + "fmt" + + "github.com/pagefaultgames/pokerogue-server/db" +) + +const ( + UUIDSize = 16 + TokenSize = 32 +) + +type RegisterRequest GenericAuthRequest + +// /account/register - register account +func Register(request RegisterRequest) error { + if !isValidUsername(request.Username) { + return fmt.Errorf("invalid username") + } + + if len(request.Password) < 6 { + return fmt.Errorf("invalid password") + } + + uuid := make([]byte, UUIDSize) + _, err := rand.Read(uuid) + if err != nil { + return fmt.Errorf("failed to generate uuid: %s", err) + } + + salt := make([]byte, ArgonSaltSize) + _, err = rand.Read(salt) + if err != nil { + return fmt.Errorf(fmt.Sprintf("failed to generate salt: %s", err)) + } + + err = db.AddAccountRecord(uuid, request.Username, deriveArgon2IDKey([]byte(request.Password), salt), salt) + if err != nil { + return fmt.Errorf("failed to add account record: %s", err) + } + + return nil +} diff --git a/api/argon2.go b/api/argon2.go deleted file mode 100644 index a595511..0000000 --- a/api/argon2.go +++ /dev/null @@ -1,15 +0,0 @@ -package api - -import "golang.org/x/crypto/argon2" - -const ( - ArgonTime = 1 - ArgonMemory = 256 * 1024 - ArgonThreads = 4 - ArgonKeySize = 32 - ArgonSaltSize = 16 -) - -func deriveArgon2IDKey(password, salt []byte) []byte { - return argon2.IDKey(password, salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize) -} diff --git a/api/account-helper.go b/api/common.go similarity index 91% rename from api/account-helper.go rename to api/common.go index 5290e74..69082fa 100644 --- a/api/account-helper.go +++ b/api/common.go @@ -5,9 +5,15 @@ import ( "fmt" "net/http" + "github.com/pagefaultgames/pokerogue-server/api/daily" "github.com/pagefaultgames/pokerogue-server/db" ) +func Init() { + scheduleStatRefresh() + daily.Init() +} + func getUsernameFromRequest(r *http.Request) (string, error) { if r.Header.Get("Authorization") == "" { return "", fmt.Errorf("missing token") diff --git a/api/daily.go b/api/daily/common.go similarity index 55% rename from api/daily.go rename to api/daily/common.go index 453950d..ce36f44 100644 --- a/api/daily.go +++ b/api/daily/common.go @@ -1,4 +1,4 @@ -package api +package daily import ( "crypto/md5" @@ -12,7 +12,6 @@ import ( "github.com/go-co-op/gocron" "github.com/pagefaultgames/pokerogue-server/db" - "github.com/pagefaultgames/pokerogue-server/defs" ) const secondsPerDay = 60 * 60 * 24 @@ -20,22 +19,9 @@ const secondsPerDay = 60 * 60 * 24 var ( dailyRunScheduler = gocron.NewScheduler(time.UTC) dailyRunSecret []byte - dailyRunSeed string ) -func ScheduleDailyRunRefresh() { - dailyRunScheduler.Every(1).Day().At("00:00").Do(func() error { - err := InitDailyRun() - if err != nil { - log.Fatal(err) - } - - return nil - }()) - dailyRunScheduler.StartAsync() -} - -func InitDailyRun() error { +func Init() error { secret, err := os.ReadFile("secret.key") if err != nil { if !os.IsNotExist(err) { @@ -58,18 +44,34 @@ func InitDailyRun() error { dailyRunSecret = secret - dailyRunSeed = base64.StdEncoding.EncodeToString(deriveDailyRunSeed(time.Now().UTC())) - - err = db.TryAddDailyRun(dailyRunSeed) + err = db.TryAddDailyRun(Seed()) if err != nil { log.Print(err) } - log.Printf("Daily Run Seed: %s", dailyRunSeed) + log.Printf("Daily Run Seed: %s", Seed()) + + scheduleRefresh() return nil } +func Seed() string { + return base64.StdEncoding.EncodeToString(deriveDailyRunSeed(time.Now().UTC())) +} + +func scheduleRefresh() { + dailyRunScheduler.Every(1).Day().At("00:00").Do(func() error { + err := Init() + if err != nil { + log.Fatal(err) + } + + return nil + }()) + dailyRunScheduler.StartAsync() +} + func deriveDailyRunSeed(seedTime time.Time) []byte { day := make([]byte, 8) binary.BigEndian.PutUint64(day, uint64(seedTime.Unix()/secondsPerDay)) @@ -78,28 +80,3 @@ func deriveDailyRunSeed(seedTime time.Time) []byte { return hashedSeed[:] } - -// /daily/rankings - fetch daily rankings -func handleRankings(uuid []byte, category, page int) ([]defs.DailyRanking, error) { - err := db.UpdateAccountLastActivity(uuid) - if err != nil { - log.Print("failed to update account last activity") - } - - rankings, err := db.FetchRankings(category, page) - if err != nil { - log.Print("failed to retrieve rankings") - } - - return rankings, nil -} - -// /daily/rankingpagecount - fetch daily ranking page count -func handleRankingPageCount(category int) (int, error) { - pageCount, err := db.FetchRankingPageCount(category) - if err != nil { - log.Print("failed to retrieve ranking page count") - } - - return pageCount, nil -} diff --git a/api/daily/rankings.go b/api/daily/rankings.go new file mode 100644 index 0000000..2e5ba38 --- /dev/null +++ b/api/daily/rankings.go @@ -0,0 +1,23 @@ +package daily + +import ( + "log" + + "github.com/pagefaultgames/pokerogue-server/db" + "github.com/pagefaultgames/pokerogue-server/defs" +) + +// /daily/rankings - fetch daily rankings +func Rankings(uuid []byte, category, page int) ([]defs.DailyRanking, error) { + err := db.UpdateAccountLastActivity(uuid) + if err != nil { + log.Print("failed to update account last activity") + } + + rankings, err := db.FetchRankings(category, page) + if err != nil { + log.Print("failed to retrieve rankings") + } + + return rankings, nil +} diff --git a/api/daily/rankingspagecount.go b/api/daily/rankingspagecount.go new file mode 100644 index 0000000..a1f4ac2 --- /dev/null +++ b/api/daily/rankingspagecount.go @@ -0,0 +1,17 @@ +package daily + +import ( + "log" + + "github.com/pagefaultgames/pokerogue-server/db" +) + +// /daily/rankingpagecount - fetch daily ranking page count +func RankingPageCount(category int) (int, error) { + pageCount, err := db.FetchRankingPageCount(category) + if err != nil { + log.Print("failed to retrieve ranking page count") + } + + return pageCount, nil +} diff --git a/api/generic.go b/api/endpoints.go similarity index 89% rename from api/generic.go rename to api/endpoints.go index b341838..3dc6e10 100644 --- a/api/generic.go +++ b/api/endpoints.go @@ -9,6 +9,9 @@ import ( "net/http" "strconv" + "github.com/pagefaultgames/pokerogue-server/api/account" + "github.com/pagefaultgames/pokerogue-server/api/daily" + "github.com/pagefaultgames/pokerogue-server/api/savedata" "github.com/pagefaultgames/pokerogue-server/defs" ) @@ -52,7 +55,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - response, err := handleAccountInfo(username, uuid) + response, err := account.Info(username, uuid) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return @@ -64,14 +67,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } case "/account/register": - var request AccountRegisterRequest + var request account.RegisterRequest err := json.NewDecoder(r.Body).Decode(&request) if err != nil { httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) return } - err = handleAccountRegister(request) + err = account.Register(request) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return @@ -79,14 +82,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) case "/account/login": - var request AccountLoginRequest + var request account.LoginRequest err := json.NewDecoder(r.Body).Decode(&request) if err != nil { httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) return } - response, err := handleAccountLogin(request) + response, err := account.Login(request) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return @@ -104,7 +107,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - err = handleAccountLogout(token) + err = account.Logout(token) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return @@ -180,14 +183,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/savedata/get": - save, err = handleSavedataGet(uuid, datatype, slot) + save, err = savedata.Get(uuid, datatype, slot) case "/savedata/update": - err = handleSavedataUpdate(uuid, slot, save) + err = savedata.Update(uuid, slot, save) case "/savedata/delete": - err = handleSavedataDelete(uuid, datatype, slot) + err = savedata.Delete(uuid, datatype, slot) case "/savedata/clear": // doesn't return a save, but it works - save, err = handleSavedataClear(uuid, slot, save.(defs.SessionSaveData)) + save, err = savedata.Clear(uuid, slot, daily.Seed(), save.(defs.SessionSaveData)) } if err != nil { httpError(w, r, err, http.StatusInternalServerError) @@ -207,7 +210,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // /daily case "/daily/seed": - w.Write([]byte(dailyRunSeed)) + w.Write([]byte(daily.Seed())) case "/daily/rankings": uuid, err := getUUIDFromRequest(r) if err != nil { @@ -233,7 +236,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - rankings, err := handleRankings(uuid, category, page) + rankings, err := daily.Rankings(uuid, category, page) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return @@ -255,7 +258,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - count, err := handleRankingPageCount(category) + count, err := daily.RankingPageCount(category) if err != nil { httpError(w, r, err, http.StatusInternalServerError) } @@ -268,14 +271,3 @@ func httpError(w http.ResponseWriter, r *http.Request, err error, code int) { log.Printf("%s: %s\n", r.URL.Path, err) http.Error(w, err.Error(), code) } - -// auth - -type GenericAuthRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type GenericAuthResponse struct { - Token string `json:"token"` -} diff --git a/api/savedata.go b/api/savedata.go deleted file mode 100644 index 130ec29..0000000 --- a/api/savedata.go +++ /dev/null @@ -1,222 +0,0 @@ -package api - -import ( - "encoding/gob" - "encoding/hex" - "fmt" - "log" - "os" - "strconv" - - "github.com/klauspost/compress/zstd" - "github.com/pagefaultgames/pokerogue-server/db" - "github.com/pagefaultgames/pokerogue-server/defs" -) - -const sessionSlotCount = 3 - -// /savedata/get - get save data -func handleSavedataGet(uuid []byte, datatype, slot int) (any, error) { - switch datatype { - case 0: // System - system, err := readSystemSaveData(uuid) - if err != nil { - return nil, err - } - - compensations, err := db.FetchAndClaimAccountCompensations(uuid) - if err != nil { - return nil, fmt.Errorf("failed to fetch compensations: %s", err) - } - - for k, v := range compensations { - typeKey := strconv.Itoa(k) - system.VoucherCounts[typeKey] += v - } - - return system, nil - case 1: // Session - if slot < 0 || slot >= sessionSlotCount { - return nil, fmt.Errorf("slot id %d out of range", slot) - } - - session, err := readSessionSaveData(uuid, slot) - if err != nil { - return nil, err - } - - return session, nil - default: - return nil, fmt.Errorf("invalid data type") - } -} - -// /savedata/update - update save data -func handleSavedataUpdate(uuid []byte, slot int, save any) error { - err := db.UpdateAccountLastActivity(uuid) - if err != nil { - log.Print("failed to update account last activity") - } - - hexUUID := hex.EncodeToString(uuid) - - switch save := save.(type) { - case defs.SystemSaveData: // System - if save.TrainerId == 0 && save.SecretId == 0 { - return fmt.Errorf("invalid system data") - } - - err = db.UpdateAccountStats(uuid, save.GameStats, save.VoucherCounts) - if err != nil { - return fmt.Errorf("failed to update account stats: %s", err) - } - - err = os.MkdirAll("userdata/"+hexUUID, 0755) - if err != nil && !os.IsExist(err) { - return fmt.Errorf("failed to create userdata folder: %s", err) - } - - file, err := os.OpenFile("userdata/"+hexUUID+"/system.pzs", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("failed to open save file for writing: %s", err) - } - - defer file.Close() - - zstdEncoder, err := zstd.NewWriter(file) - if err != nil { - return fmt.Errorf("failed to create zstd encoder: %s", err) - } - - defer zstdEncoder.Close() - - err = gob.NewEncoder(zstdEncoder).Encode(save) - if err != nil { - return fmt.Errorf("failed to serialize save: %s", err) - } - - db.DeleteClaimedAccountCompensations(uuid) - case defs.SessionSaveData: // Session - if slot < 0 || slot >= sessionSlotCount { - return fmt.Errorf("slot id %d out of range", slot) - } - - fileName := "session" - if slot != 0 { - fileName += strconv.Itoa(slot) - } - - err = os.MkdirAll("userdata/"+hexUUID, 0755) - if err != nil && !os.IsExist(err) { - return fmt.Errorf(fmt.Sprintf("failed to create userdata folder: %s", err)) - } - - file, err := os.OpenFile(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return fmt.Errorf("failed to open save file for writing: %s", err) - } - - defer file.Close() - - zstdEncoder, err := zstd.NewWriter(file) - if err != nil { - return fmt.Errorf("failed to create zstd encoder: %s", err) - } - - defer zstdEncoder.Close() - - err = gob.NewEncoder(zstdEncoder).Encode(save) - if err != nil { - return fmt.Errorf("failed to serialize save: %s", err) - } - default: - return fmt.Errorf("invalid data type") - } - - return nil -} - -// /savedata/delete - delete save data -func handleSavedataDelete(uuid []byte, datatype, slot int) error { - err := db.UpdateAccountLastActivity(uuid) - if err != nil { - log.Print("failed to update account last activity") - } - - hexUUID := hex.EncodeToString(uuid) - - switch datatype { - case 0: // System - err := os.Remove("userdata/" + hexUUID + "/system.pzs") - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to delete save file: %s", err) - } - case 1: // Session - if slot < 0 || slot >= sessionSlotCount { - return fmt.Errorf("slot id %d out of range", slot) - } - - fileName := "session" - if slot != 0 { - fileName += strconv.Itoa(slot) - } - - err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName)) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to delete save file: %s", err) - } - default: - return fmt.Errorf("invalid data type") - } - - return nil -} - -type SavedataClearResponse struct { - Success bool `json:"success"` -} - -// /savedata/clear - mark session save data as cleared and delete -func handleSavedataClear(uuid []byte, slot int, save defs.SessionSaveData) (SavedataClearResponse, error) { - var response SavedataClearResponse - err := db.UpdateAccountLastActivity(uuid) - if err != nil { - log.Print("failed to update account last activity") - } - - if slot < 0 || slot >= sessionSlotCount { - return response, fmt.Errorf("slot id %d out of range", slot) - } - - sessionCompleted := validateSessionCompleted(save) - - if save.GameMode == 3 && save.Seed == dailyRunSeed { - waveCompleted := save.WaveIndex - if !sessionCompleted { - waveCompleted-- - } - err = db.AddOrUpdateAccountDailyRun(uuid, save.Score, waveCompleted) - if err != nil { - log.Printf("failed to add or update daily run record: %s", err) - } - } - - if sessionCompleted { - response.Success, err = db.TryAddSeedCompletion(uuid, save.Seed, int(save.GameMode)) - if err != nil { - log.Printf("failed to mark seed as completed: %s", err) - } - } - - fileName := "session" - if slot != 0 { - fileName += strconv.Itoa(slot) - } - - err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName)) - if err != nil && !os.IsNotExist(err) { - return response, fmt.Errorf("failed to delete save file: %s", err) - } - - return response, nil -} diff --git a/api/savedata/clear.go b/api/savedata/clear.go new file mode 100644 index 0000000..879243f --- /dev/null +++ b/api/savedata/clear.go @@ -0,0 +1,61 @@ +package savedata + +import ( + "encoding/hex" + "fmt" + "log" + "os" + "strconv" + + "github.com/pagefaultgames/pokerogue-server/db" + "github.com/pagefaultgames/pokerogue-server/defs" +) + +type ClearResponse struct { + Success bool `json:"success"` +} + +// /savedata/clear - mark session save data as cleared and delete +func Clear(uuid []byte, slot int, seed string, save defs.SessionSaveData) (ClearResponse, error) { + var response ClearResponse + err := db.UpdateAccountLastActivity(uuid) + if err != nil { + log.Print("failed to update account last activity") + } + + if slot < 0 || slot >= defs.SessionSlotCount { + return response, fmt.Errorf("slot id %d out of range", slot) + } + + sessionCompleted := validateSessionCompleted(save) + + if save.GameMode == 3 && save.Seed == seed { + waveCompleted := save.WaveIndex + if !sessionCompleted { + waveCompleted-- + } + err = db.AddOrUpdateAccountDailyRun(uuid, save.Score, waveCompleted) + if err != nil { + log.Printf("failed to add or update daily run record: %s", err) + } + } + + if sessionCompleted { + response.Success, err = db.TryAddSeedCompletion(uuid, save.Seed, int(save.GameMode)) + if err != nil { + log.Printf("failed to mark seed as completed: %s", err) + } + } + + fileName := "session" + if slot != 0 { + fileName += strconv.Itoa(slot) + } + + err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName)) + if err != nil && !os.IsNotExist(err) { + return response, fmt.Errorf("failed to delete save file: %s", err) + } + + return response, nil +} diff --git a/api/savedata-helper.go b/api/savedata/common.go similarity index 99% rename from api/savedata-helper.go rename to api/savedata/common.go index c27c582..bca7064 100644 --- a/api/savedata-helper.go +++ b/api/savedata/common.go @@ -1,4 +1,4 @@ -package api +package savedata import ( "encoding/gob" diff --git a/api/savedata/delete.go b/api/savedata/delete.go new file mode 100644 index 0000000..cdaa3b0 --- /dev/null +++ b/api/savedata/delete.go @@ -0,0 +1,48 @@ +package savedata + +import ( + "encoding/hex" + "fmt" + "log" + "os" + "strconv" + + "github.com/pagefaultgames/pokerogue-server/db" + "github.com/pagefaultgames/pokerogue-server/defs" +) + +// /savedata/delete - delete save data +func Delete(uuid []byte, datatype, slot int) error { + err := db.UpdateAccountLastActivity(uuid) + if err != nil { + log.Print("failed to update account last activity") + } + + hexUUID := hex.EncodeToString(uuid) + + switch datatype { + case 0: // System + err := os.Remove("userdata/" + hexUUID + "/system.pzs") + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to delete save file: %s", err) + } + case 1: // Session + if slot < 0 || slot >= defs.SessionSlotCount { + return fmt.Errorf("slot id %d out of range", slot) + } + + fileName := "session" + if slot != 0 { + fileName += strconv.Itoa(slot) + } + + err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName)) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to delete save file: %s", err) + } + default: + return fmt.Errorf("invalid data type") + } + + return nil +} diff --git a/api/savedata/get.go b/api/savedata/get.go new file mode 100644 index 0000000..6e120ad --- /dev/null +++ b/api/savedata/get.go @@ -0,0 +1,45 @@ +package savedata + +import ( + "fmt" + "strconv" + + "github.com/pagefaultgames/pokerogue-server/db" + "github.com/pagefaultgames/pokerogue-server/defs" +) + +// /savedata/get - get save data +func Get(uuid []byte, datatype, slot int) (any, error) { + switch datatype { + case 0: // System + system, err := readSystemSaveData(uuid) + if err != nil { + return nil, err + } + + compensations, err := db.FetchAndClaimAccountCompensations(uuid) + if err != nil { + return nil, fmt.Errorf("failed to fetch compensations: %s", err) + } + + for k, v := range compensations { + typeKey := strconv.Itoa(k) + system.VoucherCounts[typeKey] += v + } + + return system, nil + case 1: // Session + if slot < 0 || slot >= defs.SessionSlotCount { + return nil, fmt.Errorf("slot id %d out of range", slot) + } + + session, err := readSessionSaveData(uuid, slot) + if err != nil { + return nil, err + } + + return session, nil + default: + return nil, fmt.Errorf("invalid data type") + } +} diff --git a/api/savedata/update.go b/api/savedata/update.go new file mode 100644 index 0000000..433ec4a --- /dev/null +++ b/api/savedata/update.go @@ -0,0 +1,99 @@ +package savedata + +import ( + "encoding/gob" + "encoding/hex" + "fmt" + "log" + "os" + "strconv" + + "github.com/klauspost/compress/zstd" + "github.com/pagefaultgames/pokerogue-server/db" + "github.com/pagefaultgames/pokerogue-server/defs" +) + +// /savedata/update - update save data +func Update(uuid []byte, slot int, save any) error { + err := db.UpdateAccountLastActivity(uuid) + if err != nil { + log.Print("failed to update account last activity") + } + + hexUUID := hex.EncodeToString(uuid) + + switch save := save.(type) { + case defs.SystemSaveData: // System + if save.TrainerId == 0 && save.SecretId == 0 { + return fmt.Errorf("invalid system data") + } + + err = db.UpdateAccountStats(uuid, save.GameStats, save.VoucherCounts) + if err != nil { + return fmt.Errorf("failed to update account stats: %s", err) + } + + err = os.MkdirAll("userdata/"+hexUUID, 0755) + if err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create userdata folder: %s", err) + } + + file, err := os.OpenFile("userdata/"+hexUUID+"/system.pzs", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open save file for writing: %s", err) + } + + defer file.Close() + + zstdEncoder, err := zstd.NewWriter(file) + if err != nil { + return fmt.Errorf("failed to create zstd encoder: %s", err) + } + + defer zstdEncoder.Close() + + err = gob.NewEncoder(zstdEncoder).Encode(save) + if err != nil { + return fmt.Errorf("failed to serialize save: %s", err) + } + + db.DeleteClaimedAccountCompensations(uuid) + case defs.SessionSaveData: // Session + if slot < 0 || slot >= defs.SessionSlotCount { + return fmt.Errorf("slot id %d out of range", slot) + } + + fileName := "session" + if slot != 0 { + fileName += strconv.Itoa(slot) + } + + err = os.MkdirAll("userdata/"+hexUUID, 0755) + if err != nil && !os.IsExist(err) { + return fmt.Errorf(fmt.Sprintf("failed to create userdata folder: %s", err)) + } + + file, err := os.OpenFile(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open save file for writing: %s", err) + } + + defer file.Close() + + zstdEncoder, err := zstd.NewWriter(file) + if err != nil { + return fmt.Errorf("failed to create zstd encoder: %s", err) + } + + defer zstdEncoder.Close() + + err = gob.NewEncoder(zstdEncoder).Encode(save) + if err != nil { + return fmt.Errorf("failed to serialize save: %s", err) + } + default: + return fmt.Errorf("invalid data type") + } + + return nil +} diff --git a/api/game.go b/api/stats.go similarity index 95% rename from api/game.go rename to api/stats.go index da5bdfd..4d8ac8c 100644 --- a/api/game.go +++ b/api/stats.go @@ -15,7 +15,7 @@ var ( classicSessionCount int ) -func ScheduleStatRefresh() { +func scheduleStatRefresh() { statScheduler.Every(10).Second().Do(updateStats) statScheduler.StartAsync() } diff --git a/defs/savedata.go b/defs/savedata.go index fe48aab..c6fcaba 100644 --- a/defs/savedata.go +++ b/defs/savedata.go @@ -1,5 +1,7 @@ package defs +const SessionSlotCount = 3 + type SystemSaveData struct { TrainerId int `json:"trainerId"` SecretId int `json:"secretId"` diff --git a/pokerogue-server.go b/pokerogue-server.go index 52e1270..d9a144a 100644 --- a/pokerogue-server.go +++ b/pokerogue-server.go @@ -43,9 +43,7 @@ func main() { os.Chmod(*addr, 0777) } - api.ScheduleStatRefresh() - api.ScheduleDailyRunRefresh() - api.InitDailyRun() + api.Init() err = http.Serve(listener, &api.Server{Debug: *debug}) if err != nil {