Move endpoint categories into their own packages

fasthttp
maru 2024-04-14 20:03:53 -04:00
parent 522ce9f4fa
commit 1f0f38d38e
No known key found for this signature in database
GPG Key ID: 37689350E9CD0F0D
21 changed files with 533 additions and 452 deletions

View File

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

30
api/account/common.go Normal file
View File

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

39
api/account/info.go Normal file
View File

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

51
api/account/login.go Normal file
View File

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

26
api/account/logout.go Normal file
View File

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

45
api/account/register.go Normal file
View File

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

View File

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

View File

@ -5,9 +5,15 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/pagefaultgames/pokerogue-server/api/daily"
"github.com/pagefaultgames/pokerogue-server/db" "github.com/pagefaultgames/pokerogue-server/db"
) )
func Init() {
scheduleStatRefresh()
daily.Init()
}
func getUsernameFromRequest(r *http.Request) (string, error) { func getUsernameFromRequest(r *http.Request) (string, error) {
if r.Header.Get("Authorization") == "" { if r.Header.Get("Authorization") == "" {
return "", fmt.Errorf("missing token") return "", fmt.Errorf("missing token")

View File

@ -1,4 +1,4 @@
package api package daily
import ( import (
"crypto/md5" "crypto/md5"
@ -12,7 +12,6 @@ import (
"github.com/go-co-op/gocron" "github.com/go-co-op/gocron"
"github.com/pagefaultgames/pokerogue-server/db" "github.com/pagefaultgames/pokerogue-server/db"
"github.com/pagefaultgames/pokerogue-server/defs"
) )
const secondsPerDay = 60 * 60 * 24 const secondsPerDay = 60 * 60 * 24
@ -20,22 +19,9 @@ const secondsPerDay = 60 * 60 * 24
var ( var (
dailyRunScheduler = gocron.NewScheduler(time.UTC) dailyRunScheduler = gocron.NewScheduler(time.UTC)
dailyRunSecret []byte dailyRunSecret []byte
dailyRunSeed string
) )
func ScheduleDailyRunRefresh() { func Init() error {
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 {
secret, err := os.ReadFile("secret.key") secret, err := os.ReadFile("secret.key")
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
@ -58,18 +44,34 @@ func InitDailyRun() error {
dailyRunSecret = secret dailyRunSecret = secret
dailyRunSeed = base64.StdEncoding.EncodeToString(deriveDailyRunSeed(time.Now().UTC())) err = db.TryAddDailyRun(Seed())
err = db.TryAddDailyRun(dailyRunSeed)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
log.Printf("Daily Run Seed: %s", dailyRunSeed) log.Printf("Daily Run Seed: %s", Seed())
scheduleRefresh()
return nil 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 { func deriveDailyRunSeed(seedTime time.Time) []byte {
day := make([]byte, 8) day := make([]byte, 8)
binary.BigEndian.PutUint64(day, uint64(seedTime.Unix()/secondsPerDay)) binary.BigEndian.PutUint64(day, uint64(seedTime.Unix()/secondsPerDay))
@ -78,28 +80,3 @@ func deriveDailyRunSeed(seedTime time.Time) []byte {
return hashedSeed[:] 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
}

23
api/daily/rankings.go Normal file
View File

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

View File

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

View File

@ -9,6 +9,9 @@ import (
"net/http" "net/http"
"strconv" "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" "github.com/pagefaultgames/pokerogue-server/defs"
) )
@ -52,7 +55,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
response, err := handleAccountInfo(username, uuid) response, err := account.Info(username, uuid)
if err != nil { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) httpError(w, r, err, http.StatusInternalServerError)
return return
@ -64,14 +67,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
case "/account/register": case "/account/register":
var request AccountRegisterRequest var request account.RegisterRequest
err := json.NewDecoder(r.Body).Decode(&request) err := json.NewDecoder(r.Body).Decode(&request)
if err != nil { if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return return
} }
err = handleAccountRegister(request) err = account.Register(request)
if err != nil { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) httpError(w, r, err, http.StatusInternalServerError)
return return
@ -79,14 +82,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
case "/account/login": case "/account/login":
var request AccountLoginRequest var request account.LoginRequest
err := json.NewDecoder(r.Body).Decode(&request) err := json.NewDecoder(r.Body).Decode(&request)
if err != nil { if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return return
} }
response, err := handleAccountLogin(request) response, err := account.Login(request)
if err != nil { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) httpError(w, r, err, http.StatusInternalServerError)
return return
@ -104,7 +107,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
err = handleAccountLogout(token) err = account.Logout(token)
if err != nil { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) httpError(w, r, err, http.StatusInternalServerError)
return return
@ -180,14 +183,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case "/savedata/get": case "/savedata/get":
save, err = handleSavedataGet(uuid, datatype, slot) save, err = savedata.Get(uuid, datatype, slot)
case "/savedata/update": case "/savedata/update":
err = handleSavedataUpdate(uuid, slot, save) err = savedata.Update(uuid, slot, save)
case "/savedata/delete": case "/savedata/delete":
err = handleSavedataDelete(uuid, datatype, slot) err = savedata.Delete(uuid, datatype, slot)
case "/savedata/clear": case "/savedata/clear":
// doesn't return a save, but it works // 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 { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) httpError(w, r, err, http.StatusInternalServerError)
@ -207,7 +210,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// /daily // /daily
case "/daily/seed": case "/daily/seed":
w.Write([]byte(dailyRunSeed)) w.Write([]byte(daily.Seed()))
case "/daily/rankings": case "/daily/rankings":
uuid, err := getUUIDFromRequest(r) uuid, err := getUUIDFromRequest(r)
if err != nil { 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 { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) httpError(w, r, err, http.StatusInternalServerError)
return 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 { if err != nil {
httpError(w, r, err, http.StatusInternalServerError) 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) log.Printf("%s: %s\n", r.URL.Path, err)
http.Error(w, err.Error(), code) 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"`
}

View File

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

61
api/savedata/clear.go Normal file
View File

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

View File

@ -1,4 +1,4 @@
package api package savedata
import ( import (
"encoding/gob" "encoding/gob"

48
api/savedata/delete.go Normal file
View File

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

45
api/savedata/get.go Normal file
View File

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

99
api/savedata/update.go Normal file
View File

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

View File

@ -15,7 +15,7 @@ var (
classicSessionCount int classicSessionCount int
) )
func ScheduleStatRefresh() { func scheduleStatRefresh() {
statScheduler.Every(10).Second().Do(updateStats) statScheduler.Every(10).Second().Do(updateStats)
statScheduler.StartAsync() statScheduler.StartAsync()
} }

View File

@ -1,5 +1,7 @@
package defs package defs
const SessionSlotCount = 3
type SystemSaveData struct { type SystemSaveData struct {
TrainerId int `json:"trainerId"` TrainerId int `json:"trainerId"`
SecretId int `json:"secretId"` SecretId int `json:"secretId"`

View File

@ -43,9 +43,7 @@ func main() {
os.Chmod(*addr, 0777) os.Chmod(*addr, 0777)
} }
api.ScheduleStatRefresh() api.Init()
api.ScheduleDailyRunRefresh()
api.InitDailyRun()
err = http.Serve(listener, &api.Server{Debug: *debug}) err = http.Serve(listener, &api.Server{Debug: *debug})
if err != nil { if err != nil {