diff --git a/api/account/info.go b/api/account/info.go index 6b3b94e..d76b58f 100644 --- a/api/account/info.go +++ b/api/account/info.go @@ -19,7 +19,6 @@ package account import ( "github.com/pagefaultgames/rogueserver/db" - "github.com/pagefaultgames/rogueserver/defs" ) type InfoResponse struct { @@ -29,24 +28,7 @@ type InfoResponse struct { // /account/info - get account info func Info(username string, uuid []byte) (InfoResponse, error) { - response := InfoResponse{Username: username, LastSessionSlot: -1} - - highest := -1 - for i := 0; i < defs.SessionSlotCount; i++ { - data, err := db.ReadSessionSaveData(uuid, i) - if err != nil { - continue - } - - if data.Timestamp > highest { - highest = data.Timestamp - response.LastSessionSlot = i - } - } - - if response.LastSessionSlot < 0 || response.LastSessionSlot >= defs.SessionSlotCount { - response.LastSessionSlot = -1 - } - - return response, nil + slot, _ := db.GetLatestSessionSaveDataSlot(uuid) + + return InfoResponse{Username: username, LastSessionSlot: slot}, nil } diff --git a/api/common.go b/api/common.go index 325ab6a..1e1ef74 100644 --- a/api/common.go +++ b/api/common.go @@ -52,11 +52,8 @@ func Init(mux *http.ServeMux) error { mux.HandleFunc("GET /game/classicsessioncount", handleGameClassicSessionCount) // savedata - mux.HandleFunc("GET /savedata/get", legacyHandleGetSaveData) - mux.HandleFunc("POST /savedata/update", legacyHandleSaveData) - mux.HandleFunc("GET /savedata/delete", legacyHandleSaveData) // TODO use deleteSystemSave - mux.HandleFunc("POST /savedata/clear", legacyHandleSaveData) // TODO use clearSessionData - mux.HandleFunc("GET /savedata/newclear", legacyHandleNewClear) + mux.HandleFunc("/savedata/session/{action}", handleSession) + mux.HandleFunc("/savedata/system/{action}", handleSystem) //run history mux.HandleFunc("GET /savedata/runHistory", handleGetRunHistory) @@ -64,9 +61,6 @@ func Init(mux *http.ServeMux) error { // new session mux.HandleFunc("POST /savedata/updateall", handleUpdateAll) - mux.HandleFunc("POST /savedata/system/verify", handleSystemVerify) - mux.HandleFunc("GET /savedata/system", handleGetSystemData) - mux.HandleFunc("GET /savedata/session", handleGetSessionData) // daily mux.HandleFunc("GET /daily/seed", handleDailySeed) diff --git a/api/daily/common.go b/api/daily/common.go index c340917..9c1df94 100644 --- a/api/daily/common.go +++ b/api/daily/common.go @@ -61,7 +61,7 @@ func Init() error { secret = newSecret } - seed, err := recordNewDaily() + seed, err := db.TryAddDailyRun(Seed()) if err != nil { log.Print(err) } @@ -71,7 +71,7 @@ func Init() error { _, err = scheduler.AddFunc("@daily", func() { time.Sleep(time.Second) - seed, err = recordNewDaily() + seed, err = db.TryAddDailyRun(Seed()) if err != nil { log.Printf("error while recording new daily: %s", err) } else { @@ -99,7 +99,3 @@ func deriveSeed(seedTime time.Time) []byte { return hashedSeed[:] } - -func recordNewDaily() (string, error) { - return db.TryAddDailyRun(Seed()) -} diff --git a/api/endpoints.go b/api/endpoints.go index 1381265..9472efe 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -43,7 +43,7 @@ import ( func handleAccountInfo(w http.ResponseWriter, r *http.Request) { uuid, err := uuidFromRequest(r) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, err, http.StatusUnauthorized) return } @@ -103,7 +103,7 @@ func handleAccountChangePW(w http.ResponseWriter, r *http.Request) { uuid, err := uuidFromRequest(r) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, err, http.StatusUnauthorized) return } @@ -125,7 +125,8 @@ func handleAccountLogout(w http.ResponseWriter, r *http.Request) { err = account.Logout(token) if err != nil { - httpError(w, r, err, http.StatusInternalServerError) + // also possible for InternalServerError but that's unlikely unless the server blew up + httpError(w, r, err, http.StatusUnauthorized) return } @@ -146,423 +147,116 @@ func handleGameClassicSessionCount(w http.ResponseWriter, r *http.Request) { w.Write([]byte(strconv.Itoa(classicSessionCount))) } -func handleGetSessionData(w http.ResponseWriter, r *http.Request) { +func handleSession(w http.ResponseWriter, r *http.Request) { uuid, err := uuidFromRequest(r) if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - - var slot int - if r.URL.Query().Has("slot") { - slot, err = strconv.Atoi(r.URL.Query().Get("slot")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - } - - if !r.URL.Query().Has("clientSessionId") { - httpError(w, r, fmt.Errorf("missing clientSessionId"), http.StatusBadRequest) - return - } - - err = db.UpdateActiveSession(uuid, r.URL.Query().Get("clientSessionId")) - if err != nil { - httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) - return - } - - var save any - save, err = savedata.Get(uuid, 1, slot) - if errors.Is(err, sql.ErrNoRows) { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) + httpError(w, r, err, http.StatusUnauthorized) return } - writeJSON(w, r, save) -} - -const legacyClientSessionId = "LEGACY_CLIENT" - -func legacyHandleGetSaveData(w http.ResponseWriter, r *http.Request) { - uuid, err := uuidFromRequest(r) + slot, err := strconv.Atoi(r.URL.Query().Get("slot")) if err != nil { httpError(w, r, err, http.StatusBadRequest) return } - datatype := -1 - if r.URL.Query().Has("datatype") { - datatype, err = strconv.Atoi(r.URL.Query().Get("datatype")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - } - - var slot int - if r.URL.Query().Has("slot") { - slot, err = strconv.Atoi(r.URL.Query().Get("slot")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - } - - var save any - if datatype == 0 { - err = db.UpdateActiveSession(uuid, legacyClientSessionId) // we dont have a client id - if err != nil { - httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) - return - } - } - - save, err = savedata.Get(uuid, datatype, slot) - if errors.Is(err, sql.ErrNoRows) { - http.Error(w, err.Error(), http.StatusNotFound) + if slot < 0 || slot >= defs.SessionSlotCount { + httpError(w, r, fmt.Errorf("slot id %d out of range", slot), http.StatusBadRequest) return } - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) + if !r.URL.Query().Has("clientSessionId") { + httpError(w, r, fmt.Errorf("missing clientSessionId"), http.StatusBadRequest) return } - writeJSON(w, r, save) -} - -// FIXME UNFINISHED!!! -/*func clearSessionData(w http.ResponseWriter, r *http.Request) { - uuid, err := uuidFromRequest(r) + err = db.UpdateActiveSession(uuid, r.URL.Query().Get("clientSessionId")) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) return } - var slot int - if r.URL.Query().Has("slot") { - slot, err = strconv.Atoi(r.URL.Query().Get("slot")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) + switch r.PathValue("action") { + case "get": + save, err := savedata.GetSession(uuid, slot) + if errors.Is(err, sql.ErrNoRows) { + http.Error(w, err.Error(), http.StatusNotFound) return } - } - var save any - var session defs.SessionSaveData - err = json.NewDecoder(r.Body).Decode(&session) - if err != nil { - httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) - return - } - - save = session - - var active bool - active, err = db.IsActiveSession(uuid, legacyClientSessionId) //TODO unfinished, read token from query - if err != nil { - httpError(w, r, fmt.Errorf("failed to check active session: %s", err), http.StatusBadRequest) - return - } - - var trainerId, secretId int - if r.URL.Query().Has("trainerId") && r.URL.Query().Has("secretId") { - trainerId, err = strconv.Atoi(r.URL.Query().Get("trainerId")) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, err, http.StatusInternalServerError) return } - secretId, err = strconv.Atoi(r.URL.Query().Get("secretId")) + writeJSON(w, r, save) + case "update": + var session defs.SessionSaveData + err = json.NewDecoder(r.Body).Decode(&session) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) return } - } - storedTrainerId, storedSecretId, err := db.FetchTrainerIds(uuid) - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) - return - } - - if storedTrainerId > 0 || storedSecretId > 0 { - if trainerId != storedTrainerId || secretId != storedSecretId { - httpError(w, r, fmt.Errorf("session out of date: stored trainer or secret ID does not match"), http.StatusBadRequest) - return - } - } else { - err = db.UpdateTrainerIds(trainerId, secretId, uuid) - if err != nil { - httpError(w, r, fmt.Errorf("unable to update trainer ID: %s", err), http.StatusInternalServerError) - return - } - } - - if !active { - save = savedata.ClearResponse{Error: "session out of date: not active"} - } - - var seed string - seed, err = db.GetDailyRunSeed() - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) - return - } - - response, err := savedata.Clear(uuid, slot, seed, save.(defs.SessionSaveData)) - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) - return - } - - jsonResponse(w, r, response) -} - -// FIXME UNFINISHED!!! -func deleteSystemSave(w http.ResponseWriter, r *http.Request) { - uuid, err := uuidFromRequest(r) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - - datatype := 0 - if r.URL.Query().Has("datatype") { - datatype, err = strconv.Atoi(r.URL.Query().Get("datatype")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) + existingSave, err := savedata.GetSession(uuid, slot) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpError(w, r, fmt.Errorf("failed to retrieve session save data: %s", err), http.StatusInternalServerError) return + } else { + if existingSave.Seed == session.Seed && existingSave.WaveIndex > session.WaveIndex { + httpError(w, r, fmt.Errorf("session out of date: existing wave index is greater"), http.StatusBadRequest) + return + } } - } - var slot int - if r.URL.Query().Has("slot") { - slot, err = strconv.Atoi(r.URL.Query().Get("slot")) + err = savedata.UpdateSession(uuid, slot, session) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, fmt.Errorf("failed to put session data: %s", err), http.StatusInternalServerError) return } - } - var active bool - active, err = db.IsActiveSession(uuid, legacyClientSessionId) //TODO unfinished, read token from query - if err != nil { - httpError(w, r, fmt.Errorf("failed to check active session: %s", err), http.StatusInternalServerError) - return - } - - if !active { - httpError(w, r, fmt.Errorf("session out of date: not active"), http.StatusBadRequest) - return - } - - var trainerId, secretId int - - if r.URL.Query().Has("trainerId") && r.URL.Query().Has("secretId") { - trainerId, err = strconv.Atoi(r.URL.Query().Get("trainerId")) + w.WriteHeader(http.StatusOK) + case "clear": + var session defs.SessionSaveData + err = json.NewDecoder(r.Body).Decode(&session) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) return } - secretId, err = strconv.Atoi(r.URL.Query().Get("secretId")) + seed, err := db.GetDailyRunSeed() if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - } - - storedTrainerId, storedSecretId, err := db.FetchTrainerIds(uuid) - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) - return - } - - if storedTrainerId > 0 || storedSecretId > 0 { - if trainerId != storedTrainerId || secretId != storedSecretId { - httpError(w, r, fmt.Errorf("session out of date: stored trainer or secret ID does not match"), http.StatusBadRequest) - return - } - } else { - if err := db.UpdateTrainerIds(trainerId, secretId, uuid); err != nil { httpError(w, r, err, http.StatusInternalServerError) return } - } - - err = savedata.Delete(uuid, datatype, slot) - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -}*/ - -func legacyHandleSaveData(w http.ResponseWriter, r *http.Request) { - uuid, err := uuidFromRequest(r) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - - datatype := -1 - if r.URL.Query().Has("datatype") { - datatype, err = strconv.Atoi(r.URL.Query().Get("datatype")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - } - - var slot int - if r.URL.Query().Has("slot") { - slot, err = strconv.Atoi(r.URL.Query().Get("slot")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - } - - clientSessionId := r.URL.Query().Get("clientSessionId") - if clientSessionId == "" { - clientSessionId = legacyClientSessionId - } - - var save any - // /savedata/get and /savedata/delete specify datatype, but don't expect data in body - if r.URL.Path != "/savedata/get" && r.URL.Path != "/savedata/delete" { - if datatype == 0 { - var system defs.SystemSaveData - err = json.NewDecoder(r.Body).Decode(&system) - if err != nil { - httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) - return - } - - save = system - // /savedata/clear doesn't specify datatype, it is assumed to be 1 (session) - } else if datatype == 1 || r.URL.Path == "/savedata/clear" { - var session defs.SessionSaveData - err = json.NewDecoder(r.Body).Decode(&session) - if err != nil { - httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) - return - } - - save = session - } - } - - var active bool - if r.URL.Path == "/savedata/get" { - if datatype == 0 { - err = db.UpdateActiveSession(uuid, clientSessionId) - if err != nil { - httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) - return - } - } - } else { - active, err = db.IsActiveSession(uuid, clientSessionId) - if err != nil { - httpError(w, r, fmt.Errorf("failed to check active session: %s", err), http.StatusBadRequest) - return - } - // TODO: make this not suck - if !active && r.URL.Path != "/savedata/clear" { - httpError(w, r, fmt.Errorf("session out of date: not active"), http.StatusBadRequest) - return - } - - var trainerId, secretId int - - if r.URL.Path != "/savedata/update" || datatype == 1 { - if r.URL.Query().Has("trainerId") && r.URL.Query().Has("secretId") { - trainerId, err = strconv.Atoi(r.URL.Query().Get("trainerId")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - - secretId, err = strconv.Atoi(r.URL.Query().Get("secretId")) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } - } - } else { - trainerId = save.(defs.SystemSaveData).TrainerId - secretId = save.(defs.SystemSaveData).SecretId - } - - storedTrainerId, storedSecretId, err := db.FetchTrainerIds(uuid) + resp, err := savedata.Clear(uuid, slot, seed, session) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return } - if storedTrainerId > 0 || storedSecretId > 0 { - if trainerId != storedTrainerId || secretId != storedSecretId { - httpError(w, r, fmt.Errorf("session out of date: stored trainer or secret ID does not match"), http.StatusBadRequest) - return - } - } else { - if err := db.UpdateTrainerIds(trainerId, secretId, uuid); err != nil { - httpError(w, r, err, http.StatusInternalServerError) - return - } - } - } - - switch r.URL.Path { - case "/savedata/get": - save, err = savedata.Get(uuid, datatype, slot) - if errors.Is(err, sql.ErrNoRows) { - http.Error(w, err.Error(), http.StatusNotFound) + writeJSON(w, r, resp) + case "newclear": + resp, err := savedata.NewClear(uuid, slot) + if err != nil { + httpError(w, r, fmt.Errorf("failed to read new clear: %s", err), http.StatusInternalServerError) return } - case "/savedata/update": - err = savedata.Update(uuid, slot, save) - case "/savedata/delete": - err = savedata.Delete(uuid, datatype, slot) - case "/savedata/clear": - if !active { - // TODO: make this not suck - save = savedata.ClearResponse{Error: "session out of date: not active"} - break - } - var seed string - seed, err = db.GetDailyRunSeed() + writeJSON(w, r, resp) + case "delete": + err := savedata.DeleteSession(uuid, slot) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return } - // doesn't return a save, but it works - save, err = savedata.Clear(uuid, slot, seed, save.(defs.SessionSaveData)) - } - if err != nil { - httpError(w, r, err, http.StatusInternalServerError) - return - } - - if save == nil || r.URL.Path == "/savedata/update" { w.WriteHeader(http.StatusOK) + default: + httpError(w, r, fmt.Errorf("unknown action"), http.StatusBadRequest) return } - - writeJSON(w, r, save) } //functions for run history @@ -621,7 +315,7 @@ type CombinedSaveData struct { func handleUpdateAll(w http.ResponseWriter, r *http.Request) { uuid, err := uuidFromRequest(r) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, err, http.StatusUnauthorized) return } @@ -631,8 +325,10 @@ func handleUpdateAll(w http.ResponseWriter, r *http.Request) { httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) return } + if data.ClientSessionId == "" { - data.ClientSessionId = legacyClientSessionId + httpError(w, r, fmt.Errorf("missing clientSessionId"), http.StatusBadRequest) + return } var active bool @@ -647,9 +343,6 @@ func handleUpdateAll(w http.ResponseWriter, r *http.Request) { return } - trainerId := data.System.TrainerId - secretId := data.System.SecretId - storedTrainerId, storedSecretId, err := db.FetchTrainerIds(uuid) if err != nil { httpError(w, r, err, http.StatusInternalServerError) @@ -657,148 +350,180 @@ func handleUpdateAll(w http.ResponseWriter, r *http.Request) { } if storedTrainerId > 0 || storedSecretId > 0 { - if trainerId != storedTrainerId || secretId != storedSecretId { + if data.System.TrainerId != storedTrainerId || data.System.SecretId != storedSecretId { httpError(w, r, fmt.Errorf("session out of date: stored trainer or secret ID does not match"), http.StatusBadRequest) return } } else { - if err = db.UpdateTrainerIds(trainerId, secretId, uuid); err != nil { + err = db.UpdateTrainerIds(data.System.TrainerId, data.System.SecretId, uuid) + if err != nil { httpError(w, r, err, http.StatusInternalServerError) return } } + existingPlaytime, err := db.RetrievePlaytime(uuid) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpError(w, r, fmt.Errorf("failed to retrieve playtime: %s", err), http.StatusInternalServerError) + return + } else { + playtime, ok := data.System.GameStats.(map[string]interface{})["playTime"].(float64) + if !ok { + httpError(w, r, fmt.Errorf("no playtime found"), http.StatusBadRequest) + return + } + + if float64(existingPlaytime) > playtime { + httpError(w, r, fmt.Errorf("session out of date: existing playtime is greater"), http.StatusBadRequest) + return + } + } + + existingSave, err := savedata.GetSession(uuid, data.SessionSlotId) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpError(w, r, fmt.Errorf("failed to retrieve session save data: %s", err), http.StatusInternalServerError) + return + } else { + if existingSave.Seed == data.Session.Seed && existingSave.WaveIndex > data.Session.WaveIndex { + httpError(w, r, fmt.Errorf("session out of date: existing wave index is greater"), http.StatusBadRequest) + return + } + } + err = savedata.Update(uuid, data.SessionSlotId, data.Session) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return } + err = savedata.Update(uuid, 0, data.System) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return } + w.WriteHeader(http.StatusOK) } type SystemVerifyResponse struct { - Valid bool `json:"valid"` - SystemData *defs.SystemSaveData `json:"systemData"` -} - -type SystemVerifyRequest struct { - ClientSessionId string `json:"clientSessionId"` + Valid bool `json:"valid"` + SystemData defs.SystemSaveData `json:"systemData"` } -func handleSystemVerify(w http.ResponseWriter, r *http.Request) { +func handleSystem(w http.ResponseWriter, r *http.Request) { uuid, err := uuidFromRequest(r) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, err, http.StatusUnauthorized) return } - var input SystemVerifyRequest - err = json.NewDecoder(r.Body).Decode(&input) - if err != nil { - httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) + var active bool + if !r.URL.Query().Has("clientSessionId") { + httpError(w, r, fmt.Errorf("missing clientSessionId"), http.StatusBadRequest) return } - var active bool - active, err = db.IsActiveSession(uuid, input.ClientSessionId) + active, err = db.IsActiveSession(uuid, r.URL.Query().Get("clientSessionId")) if err != nil { httpError(w, r, fmt.Errorf("failed to check active session: %s", err), http.StatusBadRequest) return } - response := SystemVerifyResponse{ - Valid: active, - } + switch r.PathValue("action") { + case "get": + if !active { + err = db.UpdateActiveSession(uuid, r.URL.Query().Get("clientSessionId")) + if err != nil { + httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) + return + } + } - // not valid, send server state - if !active { - err = db.UpdateActiveSession(uuid, input.ClientSessionId) + save, err := savedata.GetSystem(uuid) if err != nil { - httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) + if errors.Is(err, sql.ErrNoRows) { + http.Error(w, err.Error(), http.StatusNotFound) + } else { + httpError(w, r, err, http.StatusInternalServerError) + } + return } - var storedSaveData defs.SystemSaveData - storedSaveData, err = db.ReadSystemSaveData(uuid) - if err != nil { - httpError(w, r, fmt.Errorf("failed to read session save data: %s", err), http.StatusInternalServerError) + writeJSON(w, r, save) + case "update": + if !active { + httpError(w, r, fmt.Errorf("session out of date: not active"), http.StatusBadRequest) return } - response.SystemData = &storedSaveData - } - - err = db.UpdateAccountLastActivity(uuid) - if err != nil { - httpError(w, r, fmt.Errorf("failed to update account last activity: %s", err), http.StatusInternalServerError) - return - } - - writeJSON(w, r, response) -} + var system defs.SystemSaveData + err = json.NewDecoder(r.Body).Decode(&system) + if err != nil { + httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest) + return + } -func handleGetSystemData(w http.ResponseWriter, r *http.Request) { - uuid, err := uuidFromRequest(r) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } + existingPlaytime, err := db.RetrievePlaytime(uuid) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpError(w, r, fmt.Errorf("failed to retrieve playtime: %s", err), http.StatusInternalServerError) + return + } else { + playtime, ok := system.GameStats.(map[string]interface{})["playTime"].(float64) + if !ok { + httpError(w, r, fmt.Errorf("no playtime found"), http.StatusBadRequest) + return + } - if !r.URL.Query().Has("clientSessionId") { - httpError(w, r, fmt.Errorf("missing clientSessionId"), http.StatusBadRequest) - return - } + if float64(existingPlaytime) > playtime { + httpError(w, r, fmt.Errorf("session out of date: existing playtime is greater"), http.StatusBadRequest) + return + } + } - err = db.UpdateActiveSession(uuid, r.URL.Query().Get("clientSessionId")) - if err != nil { - httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) - return - } + err = savedata.UpdateSystem(uuid, system) + if err != nil { + httpError(w, r, fmt.Errorf("failed to put system data: %s", err), http.StatusInternalServerError) + return + } - var save any //TODO this is always system save data - save, err = savedata.Get(uuid, 0, 0) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - http.Error(w, err.Error(), http.StatusNotFound) - } else { - httpError(w, r, err, http.StatusInternalServerError) + w.WriteHeader(http.StatusNoContent) + case "verify": + response := SystemVerifyResponse{ + Valid: active, } - return - } - //TODO apply vouchers + // not valid, send server state + if !active { + err = db.UpdateActiveSession(uuid, r.URL.Query().Get("clientSessionId")) + if err != nil { + httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest) + return + } - writeJSON(w, r, save) -} + var storedSaveData defs.SystemSaveData + storedSaveData, err = db.ReadSystemSaveData(uuid) + if err != nil { + httpError(w, r, fmt.Errorf("failed to read session save data: %s", err), http.StatusInternalServerError) + return + } -func legacyHandleNewClear(w http.ResponseWriter, r *http.Request) { - uuid, err := uuidFromRequest(r) - if err != nil { - httpError(w, r, err, http.StatusBadRequest) - return - } + response.SystemData = storedSaveData + } - var slot int - if r.URL.Query().Has("slot") { - slot, err = strconv.Atoi(r.URL.Query().Get("slot")) + writeJSON(w, r, response) + case "delete": + err := savedata.DeleteSystem(uuid) if err != nil { - httpError(w, r, err, http.StatusBadRequest) + httpError(w, r, err, http.StatusInternalServerError) return } - } - newClear, err := savedata.NewClear(uuid, slot) - if err != nil { - httpError(w, r, fmt.Errorf("failed to read new clear: %s", err), http.StatusInternalServerError) + w.WriteHeader(http.StatusOK) + default: + httpError(w, r, fmt.Errorf("unknown action"), http.StatusBadRequest) return } - - writeJSON(w, r, newClear) } // daily diff --git a/api/savedata/delete.go b/api/savedata/delete.go index 7285328..5d98756 100644 --- a/api/savedata/delete.go +++ b/api/savedata/delete.go @@ -33,8 +33,6 @@ func Delete(uuid []byte, datatype, slot int) error { } switch datatype { - case 0: // System - err = db.DeleteSystemSaveData(uuid) case 1: // Session if slot < 0 || slot >= defs.SessionSlotCount { err = fmt.Errorf("slot id %d out of range", slot) @@ -48,6 +46,6 @@ func Delete(uuid []byte, datatype, slot int) error { if err != nil { return err } - + return nil } diff --git a/api/savedata/get.go b/api/savedata/get.go deleted file mode 100644 index d87abdf..0000000 --- a/api/savedata/get.go +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright (C) 2024 Pagefault Games - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package savedata - -import ( - "fmt" - "strconv" - - "github.com/pagefaultgames/rogueserver/db" - "github.com/pagefaultgames/rogueserver/defs" -) - -// /savedata/get - get save data -func Get(uuid []byte, datatype, slot int) (any, error) { - switch datatype { - case 0: // System - system, err := db.ReadSystemSaveData(uuid) - if err != nil { - return nil, err - } - - // TODO this should be a transaction - compensations, err := db.FetchAndClaimAccountCompensations(uuid) - if err != nil { - return nil, fmt.Errorf("failed to fetch compensations: %s", err) - } - - var needsUpdate bool - for compensationType, amount := range compensations { - system.VoucherCounts[strconv.Itoa(compensationType)] += amount - if amount > 0 { - needsUpdate = true - } - } - - if needsUpdate { - err = db.StoreSystemSaveData(uuid, system) - if err != nil { - return nil, fmt.Errorf("failed to update system save data: %s", err) - } - err = db.DeleteClaimedAccountCompensations(uuid) - if err != nil { - return nil, fmt.Errorf("failed to delete claimed compensations: %s", err) - } - - err = db.UpdateAccountStats(uuid, system.GameStats, system.VoucherCounts) - if err != nil { - return nil, fmt.Errorf("failed to update account stats: %s", err) - } - } - - 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 := db.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/session.go b/api/savedata/session.go new file mode 100644 index 0000000..78ff85d --- /dev/null +++ b/api/savedata/session.go @@ -0,0 +1,33 @@ +package savedata + +import ( + "github.com/pagefaultgames/rogueserver/db" + "github.com/pagefaultgames/rogueserver/defs" +) + +func GetSession(uuid []byte, slot int) (defs.SessionSaveData, error) { + session, err := db.ReadSessionSaveData(uuid, slot) + if err != nil { + return session, err + } + + return session, nil +} + +func UpdateSession(uuid []byte, slot int, data defs.SessionSaveData) error { + err := db.StoreSessionSaveData(uuid, data, slot) + if err != nil { + return err + } + + return nil +} + +func DeleteSession(uuid []byte, slot int) error { + err := db.DeleteSessionSaveData(uuid, slot) + if err != nil { + return err + } + + return nil +} diff --git a/api/savedata/system.go b/api/savedata/system.go new file mode 100644 index 0000000..408ea64 --- /dev/null +++ b/api/savedata/system.go @@ -0,0 +1,43 @@ +package savedata + +import ( + "fmt" + + "github.com/pagefaultgames/rogueserver/db" + "github.com/pagefaultgames/rogueserver/defs" +) + +func GetSystem(uuid []byte) (defs.SystemSaveData, error) { + system, err := db.ReadSystemSaveData(uuid) + if err != nil { + return system, err + } + + return system, nil +} + +func UpdateSystem(uuid []byte, data defs.SystemSaveData) error { + if data.TrainerId == 0 && data.SecretId == 0 { + return fmt.Errorf("invalid system data") + } + + if data.GameVersion != "1.0.4" { + return fmt.Errorf("client version out of date") + } + + err := db.UpdateAccountStats(uuid, data.GameStats, data.VoucherCounts) + if err != nil { + return fmt.Errorf("failed to update account stats: %s", err) + } + + return db.StoreSystemSaveData(uuid, data) +} + +func DeleteSystem(uuid []byte) error { + err := db.DeleteSystemSaveData(uuid) + if err != nil { + return err + } + + return nil +} diff --git a/api/savedata/update.go b/api/savedata/update.go index 6876b57..58eee72 100644 --- a/api/savedata/update.go +++ b/api/savedata/update.go @@ -47,11 +47,6 @@ func Update(uuid []byte, slot int, save any) error { return fmt.Errorf("failed to update account stats: %s", err) } - err = db.DeleteClaimedAccountCompensations(uuid) - if err != nil { - return fmt.Errorf("failed to delete claimed compensations: %s", err) - } - return db.StoreSystemSaveData(uuid, save) case defs.SessionSaveData: // Session diff --git a/db/account.go b/db/account.go index 9ac1ec2..6879c50 100644 --- a/db/account.go +++ b/db/account.go @@ -154,43 +154,6 @@ func SetAccountBanned(uuid []byte, banned bool) error { return nil } -func FetchAndClaimAccountCompensations(uuid []byte) (map[int]int, error) { - var compensations = make(map[int]int) - - results, err := handle.Query("SELECT voucherType, count FROM accountCompensations WHERE uuid = ?", uuid) - if err != nil { - return nil, err - } - - defer results.Close() - - for results.Next() { - var voucherType int - var count int - err := results.Scan(&voucherType, &count) - if err != nil { - return compensations, err - } - compensations[voucherType] = count - } - - _, err = handle.Exec("UPDATE accountCompensations SET claimed = 1 WHERE uuid = ?", uuid) - if err != nil { - return compensations, err - } - - return compensations, nil -} - -func DeleteClaimedAccountCompensations(uuid []byte) error { - _, err := handle.Exec("DELETE FROM accountCompensations WHERE uuid = ? AND claimed = 1", uuid) - if err != nil { - return err - } - - return nil -} - func FetchAccountKeySaltFromUsername(username string) ([]byte, []byte, error) { var key, salt []byte err := handle.QueryRow("SELECT hash, salt FROM accounts WHERE username = ?", username).Scan(&key, &salt) diff --git a/db/db.go b/db/db.go index 4c589e5..10af2b8 100644 --- a/db/db.go +++ b/db/db.go @@ -92,11 +92,13 @@ func setupDb(tx *sql.Tx) error { `ALTER TABLE sessions DROP COLUMN IF EXISTS active`, `CREATE TABLE IF NOT EXISTS activeClientSessions (uuid BINARY(16) NOT NULL PRIMARY KEY, clientSessionId VARCHAR(32) NOT NULL, FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)`, - // ---------------------------------- - //Migration 002 - `CREATE TABLE IF NOT EXISTS runHistoryData (uuid BINARY(16) PRIMARY KEY, data LONGBLOB, timestamp TIMESTAMP, FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)`, + // MIGRATION 002 + + `DROP TABLE accountCompensations`, + + `CREATE TABLE IF NOT EXISTS runHistoryData (uuid BINARY(16) PRIMARY KEY, data LONGBLOB, timestamp TIMESTAMP, FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)`, } for _, q := range queries { diff --git a/db/savedata.go b/db/savedata.go index 8df77f2..f8bd56e 100644 --- a/db/savedata.go +++ b/db/savedata.go @@ -20,7 +20,6 @@ package db import ( "bytes" "encoding/gob" - "errors" "github.com/pagefaultgames/rogueserver/defs" ) @@ -70,13 +69,8 @@ func ReadSystemSaveData(uuid []byte) (defs.SystemSaveData, error) { } func StoreSystemSaveData(uuid []byte, data defs.SystemSaveData) error { - systemData, err := ReadSystemSaveData(uuid) - if err == nil && systemData.Timestamp > data.Timestamp { - return errors.New("attempted to save an older system save") - } - var buf bytes.Buffer - err = gob.NewEncoder(&buf).Encode(data) + err := gob.NewEncoder(&buf).Encode(data) if err != nil { return err } @@ -126,13 +120,8 @@ func GetLatestSessionSaveDataSlot(uuid []byte) (int, error) { } func StoreSessionSaveData(uuid []byte, data defs.SessionSaveData, slot int) error { - session, err := ReadSessionSaveData(uuid, slot) - if err == nil && session.Seed == data.Seed && session.WaveIndex > data.WaveIndex { - return errors.New("attempted to save an older session") - } - var buf bytes.Buffer - err = gob.NewEncoder(&buf).Encode(data) + err := gob.NewEncoder(&buf).Encode(data) if err != nil { return err } @@ -154,6 +143,16 @@ func DeleteSessionSaveData(uuid []byte, slot int) error { return nil } +func RetrievePlaytime(uuid []byte) (int, error) { + var playtime int + err := handle.QueryRow("SELECT playTime FROM accountStats WHERE uuid = ?", uuid).Scan(&playtime) + if err != nil { + return 0, err + } + + return playtime, nil +} + func GetRunHistoryData(uuid []byte) (defs.RunHistoryData, error) { var runHistory defs.RunHistoryData var err error @@ -185,4 +184,4 @@ func UpdateRunHistoryData(uuid []byte, data defs.RunHistoryData) error { } return nil -} \ No newline at end of file +} diff --git a/defs/savedata.go b/defs/savedata.go index f434e1b..95554e3 100644 --- a/defs/savedata.go +++ b/defs/savedata.go @@ -79,10 +79,17 @@ type VoucherUnlocks map[string]int type VoucherCounts map[string]int type EggData struct { - Id int `json:"id"` - GachaType GachaType `json:"gachaType"` - HatchWaves int `json:"hatchWaves"` - Timestamp int `json:"timestamp"` + Id int `json:"id"` + GachaType GachaType `json:"gachaType"` + HatchWaves int `json:"hatchWaves"` + Timestamp int `json:"timestamp"` + Tier int `json:"tier"` + SourceType int `json:"sourceType"` + VariantTier int `json:"variantTier"` + IsShiny bool `json:"isShiny"` + Species int `json:"species"` + EggMoveIndex int `json:"eggMoveIndex"` + OverrideHiddenAbility bool `json:"overrideHiddenAbility"` } type GachaType int @@ -107,6 +114,13 @@ type SessionSaveData struct { Trainer TrainerData `json:"trainer"` GameVersion string `json:"gameVersion"` Timestamp int `json:"timestamp"` + Challenges []ChallengeData `json:"challenges"` +} + +type ChallengeData struct { + Id int `json:"id"` + Value int `json:"value"` + Severity int `json:"severity"` } type GameMode int