Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2704e64e38 | ||
|
|
e97e5f73d5 | ||
|
|
d4a906a0f1 | ||
|
|
693663103b | ||
|
|
a8502fcd3f | ||
|
|
b5e8094039 | ||
|
|
eea2266920 | ||
|
|
17294e5179 | ||
|
|
b91c169b16 | ||
|
|
3ed5f41d58 | ||
|
|
8a32efeaa3 | ||
|
|
633142eb29 | ||
|
|
fadd10602a | ||
|
|
e4de7c2391 | ||
|
|
de0bd74dc2 | ||
|
|
59ea469fb6 | ||
|
|
4971ad9d42 | ||
|
|
192b777ac3 | ||
|
|
7dbcb18ebf | ||
|
|
0ead2da2da | ||
|
|
6cb179b553 | ||
|
|
405a578e8c | ||
|
|
0d6539a87b | ||
|
|
1f95f7c042 | ||
|
|
723fe48969 |
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
/.github/
|
||||||
|
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
/.data/
|
||||||
|
/secret.key
|
||||||
|
|
||||||
|
/rogueserver*
|
||||||
|
!/rogueserver.go
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.os_name }})
|
||||||
|
env:
|
||||||
|
GO_VERSION: 1.22
|
||||||
|
GOOS: ${{ matrix.os_name }}
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
os_name: linux
|
||||||
|
arch: amd64
|
||||||
|
- os: windows-latest
|
||||||
|
os_name: windows
|
||||||
|
arch: amd64
|
||||||
|
# TODO macos needs universal binary!
|
||||||
|
# - os: macos-latest
|
||||||
|
# os_name: macos
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Go ${{ env.GO_VERSION }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: go mod download
|
||||||
|
- name: Test
|
||||||
|
run: go test -v
|
||||||
|
- name: Build
|
||||||
|
run: go build -v
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rogueserver-${{ matrix.os_name }}-${{ matrix.arch }}-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
rogueserver*
|
||||||
|
!rogueserver.go
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Publish to GHCR
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and publish to GHCR
|
||||||
|
if: github.repository == 'pagefaultgames/rogueserver'
|
||||||
|
env:
|
||||||
|
GO_VERSION: 1.22
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Setup Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Log into container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ github.token }}
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository }}
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
GO_VERSION=${{ env.GO_VERSION }}
|
||||||
|
VERSION=${{ github.ref_name }}-SNAPSHOT
|
||||||
|
COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }}
|
||||||
+12
-1
@@ -1,5 +1,16 @@
|
|||||||
|
|
||||||
# no extension on linux, .exe on windows
|
# no extension on linux, .exe on windows
|
||||||
rogueserver*
|
rogueserver*
|
||||||
userdata/*
|
!/rogueserver/*
|
||||||
|
/userdata/*
|
||||||
secret.key
|
secret.key
|
||||||
|
|
||||||
|
# local testing
|
||||||
|
/.data/
|
||||||
|
|
||||||
|
# Jetbrains IDEs
|
||||||
|
/.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.vscode/launch.json
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
ARG GO_VERSION=1.22
|
||||||
|
|
||||||
|
FROM golang:${GO_VERSION} AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY ./go.mod /src/
|
||||||
|
COPY ./go.sum /src/
|
||||||
|
|
||||||
|
RUN go mod download && go mod verify
|
||||||
|
|
||||||
|
COPY . /src/
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 \
|
||||||
|
go build -o rogueserver
|
||||||
|
|
||||||
|
RUN chmod +x /src/rogueserver
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /src/rogueserver .
|
||||||
|
|
||||||
|
EXPOSE 8001
|
||||||
|
|
||||||
|
ENTRYPOINT ["./rogueserver"]
|
||||||
@@ -19,6 +19,7 @@ package account
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
)
|
)
|
||||||
@@ -34,13 +35,13 @@ const (
|
|||||||
ArgonKeySize = 32
|
ArgonKeySize = 32
|
||||||
ArgonSaltSize = 16
|
ArgonSaltSize = 16
|
||||||
|
|
||||||
ArgonMaxInstances = 16
|
|
||||||
|
|
||||||
UUIDSize = 16
|
UUIDSize = 16
|
||||||
TokenSize = 32
|
TokenSize = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ArgonMaxInstances = runtime.NumCPU()
|
||||||
|
|
||||||
isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString
|
isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString
|
||||||
semaphore = make(chan bool, ArgonMaxInstances)
|
semaphore = make(chan bool, ArgonMaxInstances)
|
||||||
)
|
)
|
||||||
|
|||||||
+13
-17
@@ -18,11 +18,7 @@
|
|||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/pagefaultgames/rogueserver/db"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pagefaultgames/rogueserver/defs"
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,24 +29,24 @@ type InfoResponse struct {
|
|||||||
|
|
||||||
// /account/info - get account info
|
// /account/info - get account info
|
||||||
func Info(username string, uuid []byte) (InfoResponse, error) {
|
func Info(username string, uuid []byte) (InfoResponse, error) {
|
||||||
var latestSave time.Time
|
response := InfoResponse{Username: username, LastSessionSlot: -1}
|
||||||
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))
|
highest := -1
|
||||||
|
for i := 0; i < defs.SessionSlotCount; i++ {
|
||||||
|
data, err := db.ReadSessionSaveData(uuid, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if stat.ModTime().After(latestSave) {
|
if data.Timestamp > highest {
|
||||||
latestSave = stat.ModTime()
|
highest = data.Timestamp
|
||||||
latestSaveID = id
|
response.LastSessionSlot = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return InfoResponse{Username: username, LastSessionSlot: latestSaveID}, nil
|
if response.LastSessionSlot < 0 || response.LastSessionSlot >= defs.SessionSlotCount {
|
||||||
|
response.LastSessionSlot = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,6 @@ package account
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pagefaultgames/rogueserver/db"
|
"github.com/pagefaultgames/rogueserver/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,10 +50,5 @@ func Register(username, password string) error {
|
|||||||
return fmt.Errorf("failed to add account record: %s", err)
|
return fmt.Errorf("failed to add account record: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(fmt.Sprintf("userdata/%x", uuid), 0755)
|
|
||||||
if err != nil && !os.IsExist(err) {
|
|
||||||
return fmt.Errorf(fmt.Sprintf("failed to create userdata folder: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
+1
-1
@@ -40,7 +40,6 @@ func Init(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("GET /account/logout", handleAccountLogout)
|
mux.HandleFunc("GET /account/logout", handleAccountLogout)
|
||||||
|
|
||||||
// game
|
// game
|
||||||
mux.HandleFunc("GET /game/playercount", handleGamePlayerCount)
|
|
||||||
mux.HandleFunc("GET /game/titlestats", handleGameTitleStats)
|
mux.HandleFunc("GET /game/titlestats", handleGameTitleStats)
|
||||||
mux.HandleFunc("GET /game/classicsessioncount", handleGameClassicSessionCount)
|
mux.HandleFunc("GET /game/classicsessioncount", handleGameClassicSessionCount)
|
||||||
|
|
||||||
@@ -49,6 +48,7 @@ func Init(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("POST /savedata/update", handleSaveData)
|
mux.HandleFunc("POST /savedata/update", handleSaveData)
|
||||||
mux.HandleFunc("GET /savedata/delete", handleSaveData)
|
mux.HandleFunc("GET /savedata/delete", handleSaveData)
|
||||||
mux.HandleFunc("POST /savedata/clear", handleSaveData)
|
mux.HandleFunc("POST /savedata/clear", handleSaveData)
|
||||||
|
mux.HandleFunc("GET /savedata/newclear", handleNewClear)
|
||||||
|
|
||||||
// daily
|
// daily
|
||||||
mux.HandleFunc("GET /daily/seed", handleDailySeed)
|
mux.HandleFunc("GET /daily/seed", handleDailySeed)
|
||||||
|
|||||||
+11
-11
@@ -61,21 +61,26 @@ func Init() error {
|
|||||||
secret = newSecret
|
secret = newSecret
|
||||||
}
|
}
|
||||||
|
|
||||||
err = recordNewDaily()
|
seed, err := recordNewDaily()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Daily Run Seed: %s", Seed())
|
log.Printf("Daily Run Seed: %s", seed)
|
||||||
|
|
||||||
scheduler.AddFunc("@daily", func() {
|
_, err = scheduler.AddFunc("@daily", func() {
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
err := recordNewDaily()
|
seed, err = recordNewDaily()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error while recording new daily: %s", err)
|
log.Printf("error while recording new daily: %s", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Daily Run Seed: %s", seed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
scheduler.Start()
|
scheduler.Start()
|
||||||
|
|
||||||
@@ -95,11 +100,6 @@ func deriveSeed(seedTime time.Time) []byte {
|
|||||||
return hashedSeed[:]
|
return hashedSeed[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordNewDaily() error {
|
func recordNewDaily() (string, error) {
|
||||||
err := db.TryAddDailyRun(Seed())
|
return db.TryAddDailyRun(Seed())
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
+49
-10
@@ -18,6 +18,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -144,10 +145,6 @@ func handleAccountLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// game
|
// game
|
||||||
|
|
||||||
func handleGamePlayerCount(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte(strconv.Itoa(playerCount)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGameTitleStats(w http.ResponseWriter, r *http.Request) {
|
func handleGameTitleStats(w http.ResponseWriter, r *http.Request) {
|
||||||
err := json.NewEncoder(w).Encode(defs.TitleStats{
|
err := json.NewEncoder(w).Encode(defs.TitleStats{
|
||||||
PlayerCount: playerCount,
|
PlayerCount: playerCount,
|
||||||
@@ -284,6 +281,10 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/savedata/get":
|
case "/savedata/get":
|
||||||
save, err = savedata.Get(uuid, datatype, slot)
|
save, err = savedata.Get(uuid, datatype, slot)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
case "/savedata/update":
|
case "/savedata/update":
|
||||||
err = savedata.Update(uuid, slot, save)
|
err = savedata.Update(uuid, slot, save)
|
||||||
case "/savedata/delete":
|
case "/savedata/delete":
|
||||||
@@ -295,14 +296,15 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
s, ok := save.(defs.SessionSaveData)
|
var seed string
|
||||||
if !ok {
|
seed, err = db.GetDailyRunSeed()
|
||||||
err = fmt.Errorf("save data is not type SessionSaveData")
|
if err != nil {
|
||||||
break
|
httpError(w, r, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// doesn't return a save, but it works
|
// doesn't return a save, but it works
|
||||||
save, err = savedata.Clear(uuid, slot, daily.Seed(), s)
|
save, err = savedata.Clear(uuid, slot, seed, save.(defs.SessionSaveData))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, r, err, http.StatusInternalServerError)
|
httpError(w, r, err, http.StatusInternalServerError)
|
||||||
@@ -323,10 +325,47 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleNewClear(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newClear, err := savedata.NewClear(uuid, slot)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, r, fmt.Errorf("failed to read new clear: %s", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(newClear)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, r, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
// daily
|
// daily
|
||||||
|
|
||||||
func handleDailySeed(w http.ResponseWriter, r *http.Request) {
|
func handleDailySeed(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte(daily.Seed()))
|
seed, err := db.GetDailyRunSeed()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, r, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(seed))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDailyRankings(w http.ResponseWriter, r *http.Request) {
|
func handleDailyRankings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
+4
-12
@@ -18,11 +18,8 @@
|
|||||||
package savedata
|
package savedata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/pagefaultgames/rogueserver/db"
|
"github.com/pagefaultgames/rogueserver/db"
|
||||||
"github.com/pagefaultgames/rogueserver/defs"
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
@@ -60,20 +57,15 @@ func Clear(uuid []byte, slot int, seed string, save defs.SessionSaveData) (Clear
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sessionCompleted {
|
if sessionCompleted {
|
||||||
response.Success, err = db.TryAddDailyRunCompletion(uuid, save.Seed, int(save.GameMode))
|
response.Success, err = db.TryAddSeedCompletion(uuid, save.Seed, int(save.GameMode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to mark seed as completed: %s", err)
|
log.Printf("failed to mark seed as completed: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := "session"
|
err = db.DeleteSessionSaveData(uuid, slot)
|
||||||
if slot != 0 {
|
if err != nil {
|
||||||
fileName += strconv.Itoa(slot)
|
log.Printf("failed to delete session save data: %s", err)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return response, nil
|
||||||
|
|||||||
@@ -18,71 +18,9 @@
|
|||||||
package savedata
|
package savedata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
"github.com/pagefaultgames/rogueserver/defs"
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readSystemSaveData(uuid []byte) (defs.SystemSaveData, error) {
|
|
||||||
var system defs.SystemSaveData
|
|
||||||
|
|
||||||
file, err := os.Open("userdata/" + hex.EncodeToString(uuid) + "/system.pzs")
|
|
||||||
if err != nil {
|
|
||||||
return system, fmt.Errorf("failed to open save file for reading: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
zstdDecoder, err := zstd.NewReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return system, fmt.Errorf("failed to create zstd decoder: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer zstdDecoder.Close()
|
|
||||||
|
|
||||||
err = gob.NewDecoder(zstdDecoder).Decode(&system)
|
|
||||||
if err != nil {
|
|
||||||
return system, fmt.Errorf("failed to deserialize save: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return system, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readSessionSaveData(uuid []byte, slotID int) (defs.SessionSaveData, error) {
|
|
||||||
var session defs.SessionSaveData
|
|
||||||
|
|
||||||
fileName := "session"
|
|
||||||
if slotID != 0 {
|
|
||||||
fileName += strconv.Itoa(slotID)
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
|
|
||||||
if err != nil {
|
|
||||||
return session, fmt.Errorf("failed to open save file for reading: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
zstdDecoder, err := zstd.NewReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return session, fmt.Errorf("failed to create zstd decoder: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer zstdDecoder.Close()
|
|
||||||
|
|
||||||
err = gob.NewDecoder(zstdDecoder).Decode(&session)
|
|
||||||
if err != nil {
|
|
||||||
return session, fmt.Errorf("failed to deserialize save: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateSessionCompleted(session defs.SessionSaveData) bool {
|
func validateSessionCompleted(session defs.SessionSaveData) bool {
|
||||||
switch session.GameMode {
|
switch session.GameMode {
|
||||||
case 0:
|
case 0:
|
||||||
|
|||||||
+3
-19
@@ -19,12 +19,9 @@ package savedata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/pagefaultgames/rogueserver/db"
|
"github.com/pagefaultgames/rogueserver/db"
|
||||||
"github.com/pagefaultgames/rogueserver/defs"
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// /savedata/delete - delete save data
|
// /savedata/delete - delete save data
|
||||||
@@ -36,27 +33,14 @@ func Delete(uuid []byte, datatype, slot int) error {
|
|||||||
|
|
||||||
switch datatype {
|
switch datatype {
|
||||||
case 0: // System
|
case 0: // System
|
||||||
err := os.Remove(fmt.Sprintf("userdata/%x/system.pzs", uuid))
|
return db.DeleteSystemSaveData(uuid)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("failed to delete save file: %s", err)
|
|
||||||
}
|
|
||||||
case 1: // Session
|
case 1: // Session
|
||||||
if slot < 0 || slot >= defs.SessionSlotCount {
|
if slot < 0 || slot >= defs.SessionSlotCount {
|
||||||
return fmt.Errorf("slot id %d out of range", slot)
|
return fmt.Errorf("slot id %d out of range", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := "session"
|
return db.DeleteSessionSaveData(uuid, slot)
|
||||||
if slot != 0 {
|
|
||||||
fileName += strconv.Itoa(slot)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Remove(fmt.Sprintf("userdata/%x/%s.pzs", uuid, fileName))
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("failed to delete save file: %s", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid data type")
|
return fmt.Errorf("invalid data type")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
+6
-2
@@ -29,7 +29,11 @@ import (
|
|||||||
func Get(uuid []byte, datatype, slot int) (any, error) {
|
func Get(uuid []byte, datatype, slot int) (any, error) {
|
||||||
switch datatype {
|
switch datatype {
|
||||||
case 0: // System
|
case 0: // System
|
||||||
system, err := readSystemSaveData(uuid)
|
if slot != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid slot id for system data")
|
||||||
|
}
|
||||||
|
|
||||||
|
system, err := db.ReadSystemSaveData(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -49,7 +53,7 @@ func Get(uuid []byte, datatype, slot int) (any, error) {
|
|||||||
return nil, fmt.Errorf("slot id %d out of range", slot)
|
return nil, fmt.Errorf("slot id %d out of range", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := readSessionSaveData(uuid, slot)
|
session, err := db.ReadSessionSaveData(uuid, slot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package savedata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pagefaultgames/rogueserver/db"
|
||||||
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// /savedata/newclear - return whether a session is a new clear for its seed
|
||||||
|
func NewClear(uuid []byte, slot int) (bool, error) {
|
||||||
|
if slot < 0 || slot >= defs.SessionSlotCount {
|
||||||
|
return false, fmt.Errorf("slot id %d out of range", slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := db.ReadSessionSaveData(uuid, slot)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
completed, err := db.ReadSeedCompleted(uuid, session.Seed)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to read seed completed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return !completed, nil
|
||||||
|
}
|
||||||
+10
-30
@@ -18,11 +18,8 @@
|
|||||||
package savedata
|
package savedata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/gob"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
@@ -39,13 +36,6 @@ func Update(uuid []byte, slot int, save any) error {
|
|||||||
log.Print("failed to update account last activity")
|
log.Print("failed to update account last activity")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ideally should have been done at account creation
|
|
||||||
err = os.MkdirAll(fmt.Sprintf("userdata/%x", uuid), 0755)
|
|
||||||
if err != nil && !os.IsExist(err) {
|
|
||||||
return fmt.Errorf(fmt.Sprintf("failed to create userdata folder: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
var filename string
|
|
||||||
switch save := save.(type) {
|
switch save := save.(type) {
|
||||||
case defs.SystemSaveData: // System
|
case defs.SystemSaveData: // System
|
||||||
if save.TrainerId == 0 && save.SecretId == 0 {
|
if save.TrainerId == 0 && save.SecretId == 0 {
|
||||||
@@ -61,36 +51,26 @@ func Update(uuid []byte, slot int, save any) error {
|
|||||||
return fmt.Errorf("failed to update account stats: %s", err)
|
return fmt.Errorf("failed to update account stats: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = "system"
|
err = db.DeleteClaimedAccountCompensations(uuid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete claimed compensations: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.StoreSystemSaveData(uuid, save)
|
||||||
|
|
||||||
db.DeleteClaimedAccountCompensations(uuid)
|
|
||||||
case defs.SessionSaveData: // Session
|
case defs.SessionSaveData: // Session
|
||||||
if slot < 0 || slot >= defs.SessionSlotCount {
|
if slot < 0 || slot >= defs.SessionSlotCount {
|
||||||
return fmt.Errorf("slot id %d out of range", slot)
|
return fmt.Errorf("slot id %d out of range", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = "session"
|
filename := "session"
|
||||||
if slot != 0 {
|
if slot != 0 {
|
||||||
filename += strconv.Itoa(slot)
|
filename += strconv.Itoa(slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return db.StoreSessionSaveData(uuid, save, slot)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid data type")
|
return fmt.Errorf("invalid data type")
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = gob.NewEncoder(&buf).Encode(save)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to serialize save: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf.Len() == 0 {
|
|
||||||
return fmt.Errorf("tried to write empty save file")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(fmt.Sprintf("userdata/%x/%s.pzs", uuid, filename), zstdEncoder.EncodeAll(buf.Bytes(), nil), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to write save to disk: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
+16
-4
@@ -23,13 +23,25 @@ import (
|
|||||||
"github.com/pagefaultgames/rogueserver/defs"
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TryAddDailyRun(seed string) error {
|
func TryAddDailyRun(seed string) (string, error) {
|
||||||
_, err := handle.Exec("INSERT INTO dailyRuns (seed, date) VALUES (?, UTC_DATE()) ON DUPLICATE KEY UPDATE date = date", seed)
|
var actualSeed string
|
||||||
|
err := handle.QueryRow("INSERT INTO dailyRuns (seed, date) VALUES (?, UTC_DATE()) ON DUPLICATE KEY UPDATE date = date RETURNING seed", seed).Scan(&actualSeed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return actualSeed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDailyRunSeed() (string, error) {
|
||||||
|
var seed string
|
||||||
|
err := handle.QueryRow("SELECT seed FROM dailyRuns WHERE date = UTC_DATE()").Scan(&seed)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return seed, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddOrUpdateAccountDailyRun(uuid []byte, score int, wave int) error {
|
func AddOrUpdateAccountDailyRun(uuid []byte, score int, wave int) error {
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
@@ -34,7 +38,132 @@ func Init(username, password, protocol, address, database string) error {
|
|||||||
return fmt.Errorf("failed to open database connection: %s", err)
|
return fmt.Errorf("failed to open database connection: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.SetMaxOpenConns(1000)
|
conns := 1024
|
||||||
|
if protocol != "unix" {
|
||||||
|
conns = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.SetMaxOpenConns(conns)
|
||||||
|
handle.SetMaxIdleConns(conns / 4)
|
||||||
|
|
||||||
|
handle.SetConnMaxIdleTime(time.Second * 10)
|
||||||
|
|
||||||
|
tx, err := handle.Begin()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// accounts
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS accounts (uuid BINARY(16) NOT NULL PRIMARY KEY, username VARCHAR(16) UNIQUE NOT NULL, hash BINARY(32) NOT NULL, salt BINARY(16) NOT NULL, registered TIMESTAMP NOT NULL, lastLoggedIn TIMESTAMP DEFAULT NULL, lastActivity TIMESTAMP DEFAULT NULL, banned TINYINT(1) NOT NULL DEFAULT 0, trainerId SMALLINT(5) UNSIGNED DEFAULT 0, secretId SMALLINT(5) UNSIGNED DEFAULT 0)")
|
||||||
|
|
||||||
|
// sessions
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS sessions (token BINARY(32) NOT NULL PRIMARY KEY, uuid BINARY(16) NOT NULL, active TINYINT(1) NOT NULL DEFAULT 0, expire TIMESTAMP DEFAULT NULL, CONSTRAINT sessions_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
|
||||||
|
tx.Exec("CREATE INDEX IF NOT EXISTS sessionsByUuid ON sessions (uuid)")
|
||||||
|
|
||||||
|
// stats
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS accountStats (uuid BINARY(16) NOT NULL PRIMARY KEY, playTime INT(11) NOT NULL DEFAULT 0, battles INT(11) NOT NULL DEFAULT 0, classicSessionsPlayed INT(11) NOT NULL DEFAULT 0, sessionsWon INT(11) NOT NULL DEFAULT 0, highestEndlessWave INT(11) NOT NULL DEFAULT 0, highestLevel INT(11) NOT NULL DEFAULT 0, pokemonSeen INT(11) NOT NULL DEFAULT 0, pokemonDefeated INT(11) NOT NULL DEFAULT 0, pokemonCaught INT(11) NOT NULL DEFAULT 0, pokemonHatched INT(11) NOT NULL DEFAULT 0, eggsPulled INT(11) NOT NULL DEFAULT 0, regularVouchers INT(11) NOT NULL DEFAULT 0, plusVouchers INT(11) NOT NULL DEFAULT 0, premiumVouchers INT(11) NOT NULL DEFAULT 0, goldenVouchers INT(11) NOT NULL DEFAULT 0, CONSTRAINT accountStats_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
|
||||||
|
|
||||||
|
// compensations
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS accountCompensations (id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, uuid BINARY(16) NOT NULL, voucherType INT(11) NOT NULL, count INT(11) NOT NULL DEFAULT 1, claimed BIT(1) NOT NULL DEFAULT b'0', CONSTRAINT accountCompensations_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
|
||||||
|
tx.Exec("CREATE INDEX IF NOT EXISTS accountCompensationsByUuid ON accountCompensations (uuid)")
|
||||||
|
|
||||||
|
// daily runs
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS dailyRuns (date DATE NOT NULL PRIMARY KEY, seed CHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL)")
|
||||||
|
tx.Exec("CREATE INDEX IF NOT EXISTS dailyRunsByDateAndSeed ON dailyRuns (date, seed)")
|
||||||
|
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS dailyRunCompletions (uuid BINARY(16) NOT NULL, seed CHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, mode INT(11) NOT NULL DEFAULT 0, score INT(11) NOT NULL DEFAULT 0, timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (uuid, seed), CONSTRAINT dailyRunCompletions_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)")
|
||||||
|
tx.Exec("CREATE INDEX IF NOT EXISTS dailyRunCompletionsByUuidAndSeed ON dailyRunCompletions (uuid, seed)")
|
||||||
|
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS accountDailyRuns (uuid BINARY(16) NOT NULL, date DATE NOT NULL, score INT(11) NOT NULL DEFAULT 0, wave INT(11) NOT NULL DEFAULT 0, timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (uuid, date), CONSTRAINT accountDailyRuns_ibfk_1 FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT accountDailyRuns_ibfk_2 FOREIGN KEY (date) REFERENCES dailyRuns (date) ON DELETE NO ACTION ON UPDATE NO ACTION)")
|
||||||
|
tx.Exec("CREATE INDEX IF NOT EXISTS accountDailyRunsByDate ON accountDailyRuns (date)")
|
||||||
|
|
||||||
|
// save data
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS systemSaveData (uuid BINARY(16) PRIMARY KEY, data LONGBLOB, timestamp TIMESTAMP)")
|
||||||
|
tx.Exec("CREATE TABLE IF NOT EXISTS sessionSaveData (uuid BINARY(16), slot TINYINT, data LONGBLOB, timestamp TIMESTAMP, PRIMARY KEY (uuid, slot))")
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO temp code
|
||||||
|
_, err = os.Stat("userdata")
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) { // not found, do not migrate
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
log.Fatalf("failed to stat userdata directory: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir("userdata")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
uuidString := entry.Name()
|
||||||
|
uuid, err := hex.DecodeString(uuidString)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to decode uuid: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err = handle.QueryRow("SELECT COUNT(*) FROM systemSaveData WHERE uuid = ?", uuid).Scan(&count)
|
||||||
|
if err != nil || count != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// store new system data
|
||||||
|
systemData, err := LegacyReadSystemSaveData(uuid)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to read system save data for %v: %s", uuidString, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = StoreSystemSaveData(uuid, systemData)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to store system save data for %v: %s\n", uuidString, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete old system data
|
||||||
|
err = os.Remove("userdata/" + uuidString + "/system.pzs")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to remove legacy system save data for %v: %s", uuidString, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
sessionData, err := LegacyReadSessionSaveData(uuid, i)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to read session save data %v for %v: %s", i, uuidString, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// store new session data
|
||||||
|
err = StoreSessionSaveData(uuid, sessionData, i)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to store session save data for %v: %s\n", uuidString, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete old session data
|
||||||
|
filename := "session"
|
||||||
|
if i != 0 {
|
||||||
|
filename += fmt.Sprintf("%d", i)
|
||||||
|
}
|
||||||
|
err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", uuidString, filename))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to remove legacy session save data %v for %v: %s", i, uuidString, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LegacyReadSystemSaveData(uuid []byte) (defs.SystemSaveData, error) {
|
||||||
|
var system defs.SystemSaveData
|
||||||
|
|
||||||
|
file, err := os.Open("userdata/" + hex.EncodeToString(uuid) + "/system.pzs")
|
||||||
|
if err != nil {
|
||||||
|
return system, fmt.Errorf("failed to open save file for reading: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
zstdDecoder, err := zstd.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return system, fmt.Errorf("failed to create zstd decoder: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer zstdDecoder.Close()
|
||||||
|
|
||||||
|
err = gob.NewDecoder(zstdDecoder).Decode(&system)
|
||||||
|
if err != nil {
|
||||||
|
return system, fmt.Errorf("failed to deserialize save: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return system, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LegacyReadSessionSaveData(uuid []byte, slotID int) (defs.SessionSaveData, error) {
|
||||||
|
var session defs.SessionSaveData
|
||||||
|
|
||||||
|
fileName := "session"
|
||||||
|
if slotID != 0 {
|
||||||
|
fileName += strconv.Itoa(slotID)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
|
||||||
|
if err != nil {
|
||||||
|
return session, fmt.Errorf("failed to open save file for reading: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
zstdDecoder, err := zstd.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return session, fmt.Errorf("failed to create zstd decoder: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer zstdDecoder.Close()
|
||||||
|
|
||||||
|
err = gob.NewDecoder(zstdDecoder).Decode(&session)
|
||||||
|
if err != nil {
|
||||||
|
return session, fmt.Errorf("failed to deserialize save: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
+110
-1
@@ -17,7 +17,14 @@
|
|||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
func TryAddDailyRunCompletion(uuid []byte, seed string, mode int) (bool, error) {
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
|
||||||
|
"github.com/pagefaultgames/rogueserver/defs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TryAddSeedCompletion(uuid []byte, seed string, mode int) (bool, error) {
|
||||||
var count int
|
var count int
|
||||||
err := handle.QueryRow("SELECT COUNT(*) FROM dailyRunCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count)
|
err := handle.QueryRow("SELECT COUNT(*) FROM dailyRunCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,3 +40,105 @@ func TryAddDailyRunCompletion(uuid []byte, seed string, mode int) (bool, error)
|
|||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadSeedCompleted(uuid []byte, seed string) (bool, error) {
|
||||||
|
var count int
|
||||||
|
err := handle.QueryRow("SELECT COUNT(*) FROM dailyRunCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadSystemSaveData(uuid []byte) (defs.SystemSaveData, error) {
|
||||||
|
var system defs.SystemSaveData
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
err := handle.QueryRow("SELECT data FROM systemSaveData WHERE uuid = ?", uuid).Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
return system, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gob.NewDecoder(bytes.NewReader(data)).Decode(&system)
|
||||||
|
if err != nil {
|
||||||
|
return system, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return system, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StoreSystemSaveData(uuid []byte, data defs.SystemSaveData) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := gob.NewEncoder(&buf).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = handle.Exec("INSERT INTO systemSaveData (uuid, data, timestamp) VALUES (?, ?, UTC_TIMESTAMP()) ON DUPLICATE KEY UPDATE data = ?, timestamp = UTC_TIMESTAMP()", uuid, buf.Bytes(), buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSystemSaveData(uuid []byte) error {
|
||||||
|
_, err := handle.Exec("DELETE FROM systemSaveData WHERE uuid = ?", uuid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadSessionSaveData(uuid []byte, slot int) (defs.SessionSaveData, error) {
|
||||||
|
var session defs.SessionSaveData
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
err := handle.QueryRow("SELECT data FROM sessionSaveData WHERE uuid = ? AND slot = ?", uuid, slot).Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gob.NewDecoder(bytes.NewReader(data)).Decode(&session)
|
||||||
|
if err != nil {
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLatestSessionSaveDataSlot(uuid []byte) (int, error) {
|
||||||
|
var slot int
|
||||||
|
err := handle.QueryRow("SELECT slot FROM sessionSaveData WHERE uuid = ? ORDER BY timestamp DESC, slot ASC LIMIT 1", uuid).Scan(&slot)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return slot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StoreSessionSaveData(uuid []byte, data defs.SessionSaveData, slot int) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := gob.NewEncoder(&buf).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = handle.Exec("INSERT INTO sessionSaveData (uuid, slot, data, timestamp) VALUES (?, ?, ?, UTC_TIMESTAMP()) ON DUPLICATE KEY UPDATE data = ?, timestamp = UTC_TIMESTAMP()", uuid, slot, buf.Bytes(), buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSessionSaveData(uuid []byte, slot int) error {
|
||||||
|
_, err := handle.Exec("DELETE FROM sessionSaveData WHERE uuid = ? AND slot = ?", uuid, slot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+8
-7
@@ -52,13 +52,14 @@ type DexEntry struct {
|
|||||||
type StarterData map[int]StarterEntry
|
type StarterData map[int]StarterEntry
|
||||||
|
|
||||||
type StarterEntry struct {
|
type StarterEntry struct {
|
||||||
Moveset interface{} `json:"moveset"`
|
Moveset interface{} `json:"moveset"`
|
||||||
EggMoves int `json:"eggMoves"`
|
EggMoves int `json:"eggMoves"`
|
||||||
CandyCount int `json:"candyCount"`
|
CandyCount int `json:"candyCount"`
|
||||||
Friendship int `json:"friendship"`
|
Friendship int `json:"friendship"`
|
||||||
AbilityAttr int `json:"abilityAttr"`
|
AbilityAttr int `json:"abilityAttr"`
|
||||||
PassiveAttr int `json:"passiveAttr"`
|
PassiveAttr int `json:"passiveAttr"`
|
||||||
ValueReduction int `json:"valueReduction"`
|
ValueReduction int `json:"valueReduction"`
|
||||||
|
ClassicWinCount int `json:"classicWinCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StarterMoveData map[int]interface{}
|
type StarterMoveData map[int]interface{}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:11
|
||||||
|
container_name: pokerogue-db-local
|
||||||
|
restart: on-failure
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: admin
|
||||||
|
MYSQL_DATABASE: pokeroguedb
|
||||||
|
MYSQL_USER: pokerogue
|
||||||
|
MYSQL_PASSWORD: pokerogue
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- ./.data/db:/var/lib/mysql
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: ghcr.io/pagefaultgames/pokerogue:latest
|
||||||
|
command: --debug --dbaddr db:3306 --dbuser pokerogue --dbpass pokerogue --dbname pokeroguedb
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
|
db:
|
||||||
|
image: mariadb:11
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: admin
|
||||||
|
MYSQL_DATABASE: pokeroguedb
|
||||||
|
MYSQL_USER: pokerogue
|
||||||
|
MYSQL_PASSWORD: pokerogue
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/mysql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
+26
-4
@@ -34,10 +34,12 @@ func main() {
|
|||||||
debug := flag.Bool("debug", false, "use debug mode")
|
debug := flag.Bool("debug", false, "use debug mode")
|
||||||
|
|
||||||
proto := flag.String("proto", "tcp", "protocol for api to use (tcp, unix)")
|
proto := flag.String("proto", "tcp", "protocol for api to use (tcp, unix)")
|
||||||
addr := flag.String("addr", "0.0.0.0", "network address for api to listen on")
|
addr := flag.String("addr", "0.0.0.0:8001", "network address for api to listen on")
|
||||||
|
tlscert := flag.String("tlscert", "", "tls certificate path")
|
||||||
|
tlskey := flag.String("tlskey", "", "tls key path")
|
||||||
|
|
||||||
dbuser := flag.String("dbuser", "pokerogue", "database username")
|
dbuser := flag.String("dbuser", "pokerogue", "database username")
|
||||||
dbpass := flag.String("dbpass", "", "database password")
|
dbpass := flag.String("dbpass", "pokerogue", "database password")
|
||||||
dbproto := flag.String("dbproto", "tcp", "protocol for database connection")
|
dbproto := flag.String("dbproto", "tcp", "protocol for database connection")
|
||||||
dbaddr := flag.String("dbaddr", "localhost", "database address")
|
dbaddr := flag.String("dbaddr", "localhost", "database address")
|
||||||
dbname := flag.String("dbname", "pokeroguedb", "database name")
|
dbname := flag.String("dbname", "pokeroguedb", "database name")
|
||||||
@@ -66,10 +68,15 @@ func main() {
|
|||||||
api.Init(mux)
|
api.Init(mux)
|
||||||
|
|
||||||
// start web server
|
// start web server
|
||||||
|
handler := prodHandler(mux)
|
||||||
if *debug {
|
if *debug {
|
||||||
err = http.Serve(listener, debugHandler(mux))
|
handler = debugHandler(mux)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *tlscert == "" {
|
||||||
|
err = http.Serve(listener, handler)
|
||||||
} else {
|
} else {
|
||||||
err = http.Serve(listener, mux)
|
err = http.ServeTLS(listener, handler, *tlscert, *tlskey)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create http server or server errored: %s", err)
|
log.Fatalf("failed to create http server or server errored: %s", err)
|
||||||
@@ -93,6 +100,21 @@ func createListener(proto, addr string) (net.Listener, error) {
|
|||||||
return listener, nil
|
return listener, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prodHandler(router *http.ServeMux) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "https://pokerogue.net")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func debugHandler(router *http.ServeMux) http.Handler {
|
func debugHandler(router *http.ServeMux) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||||
|
|||||||
Reference in new issue
Block a user