Compare commits

...

65 Commits

Author SHA1 Message Date
Flashfyre df92ff8b6f Add last activity update on verify 2024-05-15 13:37:36 -04:00
Up 509ca8df12
better errors 2024-05-15 07:28:49 +02:00
Up f31f130c14
also check for empty 2024-05-15 06:47:06 +02:00
Up c17c583321
update active session if none is found 2024-05-15 06:46:08 +02:00
Up 76e7ba02ad
always delete claimed vouchers 2024-05-15 04:27:53 +02:00
Up a063b1740c
add missing foreign key declarations 2024-05-15 04:12:20 +02:00
Up e4713e6ea3
fall back to legacy save ID 2024-05-15 04:08:42 +02:00
Up 2aab022ce3
properly use client session ID 2024-05-15 04:07:47 +02:00
Up e7cff35d69
delete vouchers when claiming them 2024-05-15 00:50:50 +02:00
Up 174b962f19
update verify endpoint 2024-05-15 00:00:38 +02:00
Up e2efcd550c
fix faulty sql query 2024-05-14 23:17:14 +02:00
Up 834d1e62a0
add foreign key constraint on client session table 2024-05-14 14:33:53 +02:00
Up c0aade2e65
simplify ID handling 2024-05-14 14:30:04 +02:00
Up 983e17c894
continue if linting fails 2024-05-14 13:23:22 +02:00
Up b88b3f6fab
explicitly define linter config 2024-05-14 13:18:39 +02:00
Up c24d006f88
move linter args to config file 2024-05-14 13:07:07 +02:00
Up 12b0a0df0b
try make linter ignore unused stuff 2024-05-14 13:01:29 +02:00
Up 436fce8759
add client session ID tokens 2024-05-14 12:54:06 +02:00
Up 8439519d8e
start on splitting api call handler function 2024-05-12 20:42:07 +02:00
Up d70c082aa9
simplify json response writing 2024-05-12 20:05:46 +02:00
Up f0c283af42
merge token and uuid lookups to reduce roundtrips 2024-05-12 19:44:04 +02:00
Up 81853b1863
forgot to update stats column 2024-05-12 08:38:57 +02:00
Up a44a6c382f
save data when applying vouchers 2024-05-12 08:30:46 +02:00
Up c06b1496a3
add watchtower in example compose file 2024-05-12 08:11:36 +02:00
Up 4430a18dae
update example compose file 2024-05-12 08:05:40 +02:00
Up 5d6bfe0c22
add accounts activity index 2024-05-12 05:12:46 +02:00
Up 2700afafdb
cleanup 2024-05-12 04:41:56 +02:00
Up 94df201bf7
update endpoint name 2024-05-12 03:42:35 +02:00
Up 884bb88cd3
add combined update endpoint 2024-05-12 03:34:08 +02:00
Up ab69d940e6
update action step name 2024-05-12 01:23:29 +02:00
Up 43f2f5a163
Merge pull request #11 from slsyy/run-golangci-lint-in-CI 2024-05-12 00:28:46 +02:00
Krystian Chmura 03865f9b94
run golangci-lint in CI 2024-05-11 14:41:25 +02:00
maru 36f353b8a6
Clean up db.go 2024-05-11 02:30:20 -04:00
maru 5656fb96d1
Use log.Fatal isntead of panic 2024-05-11 02:24:20 -04:00
Up dd013a1626
Merge pull request #8 from cgnetsec/master 2024-05-11 05:44:57 +02:00
ser3n1ty 8c209163db
Update README.md 2024-05-10 20:38:28 -07:00
ser3n1ty 1cec1d313d
Update README.md 2024-05-10 20:36:03 -07:00
Flashfyre 2704e64e38 Add newclear endpoint 2024-05-10 18:07:14 -04:00
maru e97e5f73d5
Update ghcr.yml 2024-05-10 16:00:47 -04:00
maru d4a906a0f1
Move HTTPS-related flags in rogueserver.go 2024-05-10 15:50:19 -04:00
maru 693663103b
Run formatter on files 2024-05-10 15:49:26 -04:00
Up a8502fcd3f
add GitHub actions workflows and build docker image (#9)
* add default values for CLI args

* add development docker compose file

* prevent crash if userdata dir does not exist

* accounts, acccountStats

* account stats and create db indices

* compensations and daily runs

* ensure uniqueness of daily seed

* start on port 8001 by default for client parity

* add GitHub actions scripts and dockerfile

* add os architecture

* only build docker image on main repo

* add example compose file
2024-05-10 15:47:22 -04:00
maru b5e8094039
Don't return INVALID on seed-related function error 2024-05-10 15:44:35 -04:00
maru eea2266920
Remove unneeded assertion check in handleSaveData 2024-05-10 15:40:22 -04:00
maru 17294e5179
Fix handleDailySeed 2024-05-10 15:37:24 -04:00
maru b91c169b16
endpoints.go consistency 2024-05-10 15:33:37 -04:00
Up 3ed5f41d58
make server automatically create DB schema if not exists (#5)
* add default values for CLI args

* add development docker compose file

* prevent crash if userdata dir does not exist

* accounts, acccountStats

* account stats and create db indices

* compensations and daily runs

* ensure uniqueness of daily seed

* start on port 8001 by default for client parity

* make generated schema match production

* sort imports
2024-05-10 15:30:47 -04:00
maru 8a32efeaa3
Clean up rogueserver.go 2024-05-10 13:40:00 -04:00
maru 633142eb29
Allow serving HTTPS 2024-05-10 13:16:35 -04:00
maru fadd10602a
Don't log ErrNoRows in savedata 2024-05-09 14:26:54 -04:00
maru e4de7c2391
Update database limiting code more 2024-05-09 14:22:20 -04:00
maru de0bd74dc2
Update database limits 2024-05-09 14:13:19 -04:00
maru 59ea469fb6
Don't import legacy saves if system exists in database 2024-05-09 05:59:48 -04:00
ser3n1ty e01364e1a6
Update README.md 2024-05-09 01:44:04 -07:00
ser3n1ty d5e7b438f3
Update README.md
added updated information for the docker compose container that is being implemented
2024-05-09 00:57:07 -07:00
ser3n1ty b00321f501
Update README.md 2024-05-09 00:51:44 -07:00
maru 4971ad9d42
Add new database limits 2024-05-08 20:19:33 -04:00
maru 192b777ac3
Set ArgonMaxInstances to number of cores 2024-05-08 20:08:10 -04:00
maru 7dbcb18ebf
Use INSERT instead of REPLACE for savedata storage functions 2024-05-08 17:23:07 -04:00
maru 0ead2da2da
Remove unused endpoint game/playercount 2024-05-08 15:47:56 -04:00
maru 6cb179b553
Clean up savedata.go 2024-05-07 20:38:12 -04:00
Up 405a578e8c manually parse all save states for now 2024-05-07 19:33:21 -04:00
Up 0d6539a87b
continue on existing save (#3)
* long blob

* fix continue

* one slot only?

* fallback if there is no slot data yet

* Revert "one slot only?"

This reverts commit 20997e9cd8.
2024-05-07 19:14:07 -04:00
Up 1f95f7c042
hotfix: move save data to DB (#2)
* start reading save data from DB

* finish migration code

* remove leftover dir creation

* fix import cycle

* remove more dir creation

* update gitignore

* fix pk

* better table creation

* use REPLACE INTO

* fix typo
2024-05-07 17:10:33 -04:00
Flashfyre 723fe48969 Add classic win count to starter data 2024-05-07 00:16:00 -04:00
31 changed files with 1377 additions and 279 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
/.github/
Dockerfile*
docker-compose*.yml
/.data/
/secret.key
/rogueserver*
!/rogueserver.go

52
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,52 @@
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: Lint Codebase
continue-on-error: true
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --config .golangci.yml
- 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

38
.github/workflows/ghcr.yml vendored Normal file
View File

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

13
.gitignore vendored
View File

@ -1,5 +1,16 @@
# no extension on linux, .exe on windows
rogueserver*
userdata/*
!/rogueserver/*
/userdata/*
secret.key
# local testing
/.data/
# Jetbrains IDEs
/.idea/
*.iml
*.ipr
*.iws
.vscode/launch.json

8
.golangci.yml Normal file
View File

@ -0,0 +1,8 @@
run:
timeout: 10m
severity:
default-severity: error
rules:
- linters:
- unused
severity: info

29
Dockerfile Normal file
View File

@ -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"]

View File

@ -1 +1,83 @@
# rogueserver
# rogueserver
# Hosting in Docker
It is advised that you host this in a docker container as it will be much easier to manage.
There is a sample docker-compose file for setting up a docker container to setup this server.
# Self Hosting outside of Docker:
## Required Tools:
- Golang
- Node: **18.3.0**
- npm: [how to install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
## Installation:
The docker compose file should automatically implement a container with mariadb with an empty database and the default user and password combo of pokerogue:pokerogue
### src/utils.ts:224-225 (in pokerogue)
Replace both URLs (one on each line) with the local API server address from rogueserver.go (0.0.0.0:8001) (or whatever port you picked)
# If you are on Windows
Now that all of the files are configured: start up powershell as administrator:
```
cd C:\api\server\location\
go build .
.\rogueserver.exe --debug --dbuser yourusername --dbpass yourpassword
```
The other available flags are located in rogueserver.go:34-43.
Then in another run this the first time then run `npm run start` from the rogueserver location from then on:
```
powershell -ep bypass
cd C:\server\location\
npm install
npm run start
```
You will need to allow the port youre running the API (8001) on and port 8000 to accept inbound connections through the [Windows Advanced Firewall](https://www.youtube.com/watch?v=9llH5_CON-Y).
# If you are on Linux
In whatever shell you prefer, run the following:
```
cd /api/server/location/
go build .
./rogueserver --debug --dbuser yourusername --dbpass yourpassword &
cd /server/location/
npm run start
```
If you have a firewall running such as ufw on your linux machine, make sure to allow inbound connections on the ports youre running the API and the pokerogue server (8000,8001).
An example to allow incoming connections using UFW:
```
sudo ufw allow 8000,8001/tcp
```
This should allow you to reach the game from other computers on the same network.
## Tying to a Domain
If you want to tie it to a domain like I did and make it publicly accessible, there is some extra work to be done.
I setup caddy and would recommend using it as a reverse proxy.
[caddy installation](https://caddyserver.com/docs/install)
once its installed setup a config file for caddy:
```
pokerogue.exampledomain.com {
reverse_proxy localhost:8000
}
pokeapi.exampledomain.com {
reverse_proxy localhost:8001
}
```
Preferably set up caddy as a service from [here.](https://caddyserver.com/docs/running)
Once this is good to go, take your API url (https://pokeapi.exampledomain.com) and paste it on
### src/utils.ts:224-225
in place of the previous 0.0.0.0:8001 address
Make sure that both 8000 and 8001 are portforwarded on your router.
Test that the server's game and game authentication works from other machines both in and outside of the network. Once this is complete, enjoy!

View File

@ -19,6 +19,7 @@ package account
import (
"regexp"
"runtime"
"golang.org/x/crypto/argon2"
)
@ -34,13 +35,13 @@ const (
ArgonKeySize = 32
ArgonSaltSize = 16
ArgonMaxInstances = 16
UUIDSize = 16
TokenSize = 32
)
var (
ArgonMaxInstances = runtime.NumCPU()
isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString
semaphore = make(chan bool, ArgonMaxInstances)
)

View File

@ -18,11 +18,7 @@
package account
import (
"fmt"
"os"
"strconv"
"time"
"github.com/pagefaultgames/rogueserver/db"
"github.com/pagefaultgames/rogueserver/defs"
)
@ -33,24 +29,24 @@ type InfoResponse struct {
// /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)
}
response := InfoResponse{Username: username, LastSessionSlot: -1}
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 {
continue
}
if stat.ModTime().After(latestSave) {
latestSave = stat.ModTime()
latestSaveID = id
if data.Timestamp > highest {
highest = data.Timestamp
response.LastSessionSlot = i
}
}
return InfoResponse{Username: username, LastSessionSlot: latestSaveID}, nil
if response.LastSessionSlot < 0 || response.LastSessionSlot >= defs.SessionSlotCount {
response.LastSessionSlot = -1
}
return response, nil
}

View File

@ -19,6 +19,7 @@ package account
import (
"database/sql"
"errors"
"fmt"
"github.com/pagefaultgames/rogueserver/db"
@ -28,7 +29,7 @@ import (
func Logout(token []byte) error {
err := db.RemoveSessionFromToken(token)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("token not found")
}

View File

@ -20,8 +20,6 @@ package account
import (
"crypto/rand"
"fmt"
"os"
"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)
}
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
}

View File

@ -19,18 +19,22 @@ package api
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/pagefaultgames/rogueserver/api/account"
"github.com/pagefaultgames/rogueserver/api/daily"
"github.com/pagefaultgames/rogueserver/db"
"log"
"net/http"
)
func Init(mux *http.ServeMux) {
scheduleStatRefresh()
daily.Init()
func Init(mux *http.ServeMux) error {
if err := scheduleStatRefresh(); err != nil {
return err
}
if err := daily.Init(); err != nil {
return err
}
// account
mux.HandleFunc("GET /account/info", handleAccountInfo)
@ -40,20 +44,27 @@ func Init(mux *http.ServeMux) {
mux.HandleFunc("GET /account/logout", handleAccountLogout)
// game
mux.HandleFunc("GET /game/playercount", handleGamePlayerCount)
mux.HandleFunc("GET /game/titlestats", handleGameTitleStats)
mux.HandleFunc("GET /game/classicsessioncount", handleGameClassicSessionCount)
// savedata
mux.HandleFunc("GET /savedata/get", handleSaveData)
mux.HandleFunc("POST /savedata/update", handleSaveData)
mux.HandleFunc("GET /savedata/delete", handleSaveData)
mux.HandleFunc("POST /savedata/clear", handleSaveData)
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)
// 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)
mux.HandleFunc("GET /daily/rankings", handleDailyRankings)
mux.HandleFunc("GET /daily/rankingpagecount", handleDailyRankingPageCount)
return nil
}
func tokenFromRequest(r *http.Request) ([]byte, error) {
@ -74,20 +85,34 @@ func tokenFromRequest(r *http.Request) ([]byte, error) {
}
func uuidFromRequest(r *http.Request) ([]byte, error) {
_, uuid, err := tokenAndUuidFromRequest(r)
return uuid, err
}
func tokenAndUuidFromRequest(r *http.Request) ([]byte, []byte, error) {
token, err := tokenFromRequest(r)
if err != nil {
return nil, err
return nil, nil, err
}
uuid, err := db.FetchUUIDFromToken(token)
if err != nil {
return nil, fmt.Errorf("failed to validate token: %s", err)
return nil, nil, fmt.Errorf("failed to validate token: %s", err)
}
return uuid, nil
return token, uuid, nil
}
func httpError(w http.ResponseWriter, r *http.Request, err error, code int) {
log.Printf("%s: %s\n", r.URL.Path, err)
http.Error(w, err.Error(), code)
}
}
func jsonResponse(w http.ResponseWriter, r *http.Request, data any) {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(data)
if err != nil {
httpError(w, r, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
return
}
}

View File

@ -61,21 +61,26 @@ func Init() error {
secret = newSecret
}
err = recordNewDaily()
seed, err := recordNewDaily()
if err != nil {
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)
err := recordNewDaily()
seed, err = recordNewDaily()
if err != nil {
log.Printf("error while recording new daily: %s", err)
} else {
log.Printf("Daily Run Seed: %s", seed)
}
})
if err != nil {
return err
}
scheduler.Start()
@ -95,11 +100,6 @@ func deriveSeed(seedTime time.Time) []byte {
return hashedSeed[:]
}
func recordNewDaily() error {
err := db.TryAddDailyRun(Seed())
if err != nil {
return err
}
return nil
func recordNewDaily() (string, error) {
return db.TryAddDailyRun(Seed())
}

View File

@ -18,7 +18,9 @@
package api
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
@ -57,13 +59,7 @@ func handleAccountInfo(w http.ResponseWriter, r *http.Request) {
return
}
err = json.NewEncoder(w).Encode(response)
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")
jsonResponse(w, r, response)
}
func handleAccountRegister(w http.ResponseWriter, r *http.Request) {
@ -95,13 +91,7 @@ func handleAccountLogin(w http.ResponseWriter, r *http.Request) {
return
}
err = json.NewEncoder(w).Encode(response)
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")
jsonResponse(w, r, response)
}
func handleAccountChangePW(w http.ResponseWriter, r *http.Request) {
@ -143,29 +133,66 @@ func handleAccountLogout(w http.ResponseWriter, r *http.Request) {
}
// game
func handleGamePlayerCount(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(strconv.Itoa(playerCount)))
}
func handleGameTitleStats(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(defs.TitleStats{
stats := defs.TitleStats{
PlayerCount: playerCount,
BattleCount: battleCount,
})
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")
jsonResponse(w, r, stats)
}
func handleGameClassicSessionCount(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(strconv.Itoa(classicSessionCount)))
_, _ = w.Write([]byte(strconv.Itoa(classicSessionCount)))
}
func handleSaveData(w http.ResponseWriter, r *http.Request) {
func handleGetSessionData(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
}
}
var clientSessionId string
if r.URL.Query().Has("clientSessionId") {
clientSessionId = r.URL.Query().Get("clientSessionId")
} else {
httpError(w, r, fmt.Errorf("missing clientSessionId"), http.StatusBadRequest)
}
err = db.UpdateActiveSession(uuid, 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)
return
}
jsonResponse(w, r, save)
}
const legacyClientSessionId = "LEGACY_CLIENT"
func legacyHandleGetSaveData(w http.ResponseWriter, r *http.Request) {
uuid, err := uuidFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
@ -190,6 +217,231 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
}
}
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)
return
}
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
jsonResponse(w, r, save)
}
// FIXME UNFINISHED!!!
func clearSessionData(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
}
}
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)
return
}
secretId, err = strconv.Atoi(r.URL.Query().Get("secretId"))
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 {
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)
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 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"))
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
}
}
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
}
}
var clientSessionId string
if r.URL.Query().Has("clientSessionId") {
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" {
@ -215,24 +467,17 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
}
}
var token []byte
token, err = tokenFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
var active bool
if r.URL.Path == "/savedata/get" {
if datatype == 0 {
err = db.UpdateActiveSession(uuid, token)
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(token)
active, err = db.IsActiveSession(uuid, clientSessionId)
if err != nil {
httpError(w, r, fmt.Errorf("failed to check active session: %s", err), http.StatusBadRequest)
return
@ -240,7 +485,7 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
// TODO: make this not suck
if !active && r.URL.Path != "/savedata/clear" {
httpError(w, r, fmt.Errorf("session out of date"), http.StatusBadRequest)
httpError(w, r, fmt.Errorf("session out of date: not active"), http.StatusBadRequest)
return
}
@ -273,17 +518,24 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
if storedTrainerId > 0 || storedSecretId > 0 {
if trainerId != storedTrainerId || secretId != storedSecretId {
httpError(w, r, fmt.Errorf("session out of date"), http.StatusBadRequest)
httpError(w, r, fmt.Errorf("session out of date: stored trainer or secret ID does not match"), http.StatusBadRequest)
return
}
} else {
db.UpdateTrainerIds(trainerId, secretId, uuid)
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 err == sql.ErrNoRows {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
case "/savedata/update":
err = savedata.Update(uuid, slot, save)
case "/savedata/delete":
@ -291,18 +543,19 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
case "/savedata/clear":
if !active {
// TODO: make this not suck
save = savedata.ClearResponse{Error: "session out of date"}
save = savedata.ClearResponse{Error: "session out of date: not active"}
break
}
s, ok := save.(defs.SessionSaveData)
if !ok {
err = fmt.Errorf("save data is not type SessionSaveData")
break
var seed string
seed, err = db.GetDailyRunSeed()
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
// 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 {
httpError(w, r, err, http.StatusInternalServerError)
@ -314,19 +567,214 @@ func handleSaveData(w http.ResponseWriter, r *http.Request) {
return
}
err = json.NewEncoder(w).Encode(save)
jsonResponse(w, r, save)
}
type CombinedSaveData struct {
System defs.SystemSaveData `json:"system"`
Session defs.SessionSaveData `json:"session"`
SessionSlotId int `json:"sessionSlotId"`
ClientSessionId string `json:"clientSessionId"`
}
// TODO wrap this in a transaction
func handleUpdateAll(w http.ResponseWriter, r *http.Request) {
uuid, err := uuidFromRequest(r)
if err != nil {
httpError(w, r, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
httpError(w, r, err, http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
var data CombinedSaveData
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
if data.ClientSessionId == "" {
data.ClientSessionId = legacyClientSessionId
}
var active bool
active, err = db.IsActiveSession(uuid, data.ClientSessionId)
if err != nil {
httpError(w, r, fmt.Errorf("failed to check active session: %s", err), http.StatusBadRequest)
return
}
if !active {
httpError(w, r, fmt.Errorf("session out of date: not active"), http.StatusBadRequest)
return
}
trainerId := data.System.TrainerId
secretId := data.System.SecretId
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.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"`
}
func handleSystemVerify(w http.ResponseWriter, r *http.Request) {
uuid, err := uuidFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
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)
return
}
var active bool
active, err = db.IsActiveSession(uuid, input.ClientSessionId)
if err != nil {
httpError(w, r, fmt.Errorf("failed to check active session: %s", err), http.StatusBadRequest)
return
}
response := SystemVerifyResponse{
Valid: active,
}
// not valid, send server state
if !active {
err = db.UpdateActiveSession(uuid, input.ClientSessionId)
if err != nil {
httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest)
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)
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
}
jsonResponse(w, r, response)
}
func handleGetSystemData(w http.ResponseWriter, r *http.Request) {
uuid, err := uuidFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
var clientSessionId string
if r.URL.Query().Has("clientSessionId") {
clientSessionId = r.URL.Query().Get("clientSessionId")
} else {
httpError(w, r, fmt.Errorf("missing clientSessionId"), http.StatusBadRequest)
}
err = db.UpdateActiveSession(uuid, clientSessionId)
if err != nil {
httpError(w, r, fmt.Errorf("failed to update active session: %s", err), http.StatusBadRequest)
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)
}
return
}
//TODO apply vouchers
jsonResponse(w, r, save)
}
func legacyHandleNewClear(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
}
jsonResponse(w, r, newClear)
}
// daily
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
}
_, err = w.Write([]byte(seed))
if err != nil {
httpError(w, r, fmt.Errorf("failed to write seed: %s", err), http.StatusInternalServerError)
}
}
func handleDailyRankings(w http.ResponseWriter, r *http.Request) {
@ -356,13 +804,7 @@ func handleDailyRankings(w http.ResponseWriter, r *http.Request) {
return
}
err = json.NewEncoder(w).Encode(rankings)
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")
jsonResponse(w, r, rankings)
}
func handleDailyRankingPageCount(w http.ResponseWriter, r *http.Request) {
@ -381,5 +823,5 @@ func handleDailyRankingPageCount(w http.ResponseWriter, r *http.Request) {
httpError(w, r, err, http.StatusInternalServerError)
}
w.Write([]byte(strconv.Itoa(count)))
_, _ = w.Write([]byte(strconv.Itoa(count)))
}

View File

@ -18,11 +18,8 @@
package savedata
import (
"encoding/hex"
"fmt"
"log"
"os"
"strconv"
"github.com/pagefaultgames/rogueserver/db"
"github.com/pagefaultgames/rogueserver/defs"
@ -60,20 +57,15 @@ func Clear(uuid []byte, slot int, seed string, save defs.SessionSaveData) (Clear
}
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 {
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)
err = db.DeleteSessionSaveData(uuid, slot)
if err != nil {
log.Printf("failed to delete session save data: %s", err)
}
return response, nil

View File

@ -18,71 +18,9 @@
package savedata
import (
"encoding/gob"
"encoding/hex"
"fmt"
"os"
"strconv"
"github.com/klauspost/compress/zstd"
"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 {
switch session.GameMode {
case 0:

View File

@ -19,12 +19,9 @@ package savedata
import (
"fmt"
"log"
"os"
"strconv"
"github.com/pagefaultgames/rogueserver/db"
"github.com/pagefaultgames/rogueserver/defs"
"log"
)
// /savedata/delete - delete save data
@ -36,27 +33,14 @@ func Delete(uuid []byte, datatype, slot int) error {
switch datatype {
case 0: // System
err := os.Remove(fmt.Sprintf("userdata/%x/system.pzs", uuid))
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete save file: %s", err)
}
return db.DeleteSystemSaveData(uuid)
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/%x/%s.pzs", uuid, fileName))
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete save file: %s", err)
}
return db.DeleteSessionSaveData(uuid, slot)
default:
return fmt.Errorf("invalid data type")
}
return nil
}

View File

@ -29,18 +29,43 @@ import (
func Get(uuid []byte, datatype, slot int) (any, error) {
switch datatype {
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 {
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)
}
needsUpdate := false
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
@ -49,7 +74,7 @@ func Get(uuid []byte, datatype, slot int) (any, error) {
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 {
return nil, err
}

44
api/savedata/newclear.go Normal file
View File

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

View File

@ -18,20 +18,13 @@
package savedata
import (
"bytes"
"encoding/gob"
"fmt"
"log"
"os"
"strconv"
"github.com/klauspost/compress/zstd"
"github.com/pagefaultgames/rogueserver/db"
"github.com/pagefaultgames/rogueserver/defs"
)
var zstdEncoder, _ = zstd.NewWriter(nil)
// /savedata/update - update save data
func Update(uuid []byte, slot int, save any) error {
err := db.UpdateAccountLastActivity(uuid)
@ -39,13 +32,6 @@ func Update(uuid []byte, slot int, save any) error {
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) {
case defs.SystemSaveData: // System
if save.TrainerId == 0 && save.SecretId == 0 {
@ -61,36 +47,20 @@ func Update(uuid []byte, slot int, save any) error {
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
if slot < 0 || slot >= defs.SessionSlotCount {
return fmt.Errorf("slot id %d out of range", slot)
}
return db.StoreSessionSaveData(uuid, save, slot)
filename = "session"
if slot != 0 {
filename += strconv.Itoa(slot)
}
default:
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
}

View File

@ -32,15 +32,19 @@ var (
classicSessionCount int
)
func scheduleStatRefresh() {
scheduler.AddFunc("@every 30s", func() {
func scheduleStatRefresh() error {
_, err := scheduler.AddFunc("@every 30s", func() {
err := updateStats()
if err != nil {
log.Printf("failed to update stats: %s", err)
}
})
if err != nil {
return err
}
scheduler.Start()
return nil
}
func updateStats() error {

View File

@ -18,6 +18,8 @@
package db
import (
"database/sql"
"errors"
"fmt"
"slices"
@ -40,11 +42,6 @@ func AddAccountSession(username string, token []byte) error {
return err
}
_, err = handle.Exec("UPDATE sessions s JOIN accounts a ON a.uuid = s.uuid SET s.active = 1 WHERE a.username = ? AND a.lastLoggedIn IS NULL", username)
if err != nil {
return err
}
_, err = handle.Exec("UPDATE accounts SET lastLoggedIn = UTC_TIMESTAMP() WHERE username = ?", username)
if err != nil {
return err
@ -213,18 +210,25 @@ func UpdateTrainerIds(trainerId, secretId int, uuid []byte) error {
return nil
}
func IsActiveSession(token []byte) (bool, error) {
var active int
err := handle.QueryRow("SELECT `active` FROM sessions WHERE token = ?", token).Scan(&active)
func IsActiveSession(uuid []byte, clientSessionId string) (bool, error) {
var storedId string
err := handle.QueryRow("SELECT clientSessionId FROM activeClientSessions WHERE uuid = ?", uuid).Scan(&storedId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
err = UpdateActiveSession(uuid, clientSessionId)
if err != nil {
return false, err
}
return true, nil
}
return false, err
}
return active == 1, nil
return storedId == "" || storedId == clientSessionId, nil
}
func UpdateActiveSession(uuid []byte, token []byte) error {
_, err := handle.Exec("UPDATE sessions SET `active` = CASE WHEN token = ? THEN 1 ELSE 0 END WHERE uuid = ?", token, uuid)
func UpdateActiveSession(uuid []byte, clientSessionId string) error {
_, err := handle.Exec("REPLACE INTO activeClientSessions VALUES (?, ?)", uuid, clientSessionId)
if err != nil {
return err
}

View File

@ -23,13 +23,25 @@ import (
"github.com/pagefaultgames/rogueserver/defs"
)
func TryAddDailyRun(seed string) error {
_, err := handle.Exec("INSERT INTO dailyRuns (seed, date) VALUES (?, UTC_DATE()) ON DUPLICATE KEY UPDATE date = date", seed)
func TryAddDailyRun(seed string) (string, error) {
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 {
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 {

150
db/db.go
View File

@ -19,7 +19,11 @@ package db
import (
"database/sql"
"encoding/hex"
"fmt"
"log"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
)
@ -34,7 +38,151 @@ func Init(username, password, protocol, address, database string) error {
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 {
log.Fatal(err)
}
err = setupDb(tx)
if err != nil {
_ = tx.Rollback()
log.Fatal(err)
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
// TODO temp code
_, err = os.Stat("userdata")
if err != nil {
if !os.IsNotExist(err) { // not found, do not migrate
log.Fatalf("failed to stat userdata directory: %s", err)
}
return nil
}
entries, err := os.ReadDir("userdata")
if err != nil {
log.Fatal(err)
}
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)
}
// 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
}
func setupDb(tx *sql.Tx) error {
queries := []string{
// MIGRATION 000
`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)`,
`CREATE INDEX IF NOT EXISTS accountsByActivity ON accounts (lastActivity)`,
`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)`,
`CREATE INDEX IF NOT EXISTS sessionsByUuid ON sessions (uuid)`,
`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)`,
`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)`,
`CREATE INDEX IF NOT EXISTS accountCompensationsByUuid ON accountCompensations (uuid)`,
`CREATE TABLE IF NOT EXISTS dailyRuns (date DATE NOT NULL PRIMARY KEY, seed CHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL)`,
`CREATE INDEX IF NOT EXISTS dailyRunsByDateAndSeed ON dailyRuns (date, seed)`,
`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)`,
`CREATE INDEX IF NOT EXISTS dailyRunCompletionsByUuidAndSeed ON dailyRunCompletions (uuid, seed)`,
`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)`,
`CREATE INDEX IF NOT EXISTS accountDailyRunsByDate ON accountDailyRuns (date)`,
`CREATE TABLE IF NOT EXISTS systemSaveData (uuid BINARY(16) PRIMARY KEY, data LONGBLOB, timestamp TIMESTAMP, FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)`,
`CREATE TABLE IF NOT EXISTS sessionSaveData (uuid BINARY(16), slot TINYINT, data LONGBLOB, timestamp TIMESTAMP, PRIMARY KEY (uuid, slot), FOREIGN KEY (uuid) REFERENCES accounts (uuid) ON DELETE CASCADE ON UPDATE CASCADE)`,
// ----------------------------------
// MIGRATION 001
`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)`,
}
for _, q := range queries {
_, err := tx.Exec(q)
if err != nil {
return fmt.Errorf("failed to execute query: %w, query: %s", err, q)
}
}
return nil
}

View File

@ -29,7 +29,7 @@ func FetchPlayerCount() (int, error) {
func FetchBattleCount() (int, error) {
var battleCount int
err := handle.QueryRow("SELECT COALESCE(SUM(battles), 0) FROM accountStats").Scan(&battleCount)
err := handle.QueryRow("SELECT COALESCE(SUM(s.battles), 0) FROM accountStats s JOIN accounts a ON a.uuid = s.uuid WHERE a.banned = 0").Scan(&battleCount)
if err != nil {
return 0, err
}
@ -39,7 +39,7 @@ func FetchBattleCount() (int, error) {
func FetchClassicSessionCount() (int, error) {
var classicSessionCount int
err := handle.QueryRow("SELECT COALESCE(SUM(classicSessionsPlayed), 0) FROM accountStats").Scan(&classicSessionCount)
err := handle.QueryRow("SELECT COALESCE(SUM(s.classicSessionsPlayed), 0) FROM accountStats s JOIN accounts a ON a.uuid = s.uuid WHERE a.banned = 0").Scan(&classicSessionCount)
if err != nil {
return 0, err
}

84
db/legacy.go Normal file
View File

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

View File

@ -17,7 +17,14 @@
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
err := handle.QueryRow("SELECT COUNT(*) FROM dailyRunCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count)
if err != nil {
@ -33,3 +40,105 @@ func TryAddDailyRunCompletion(uuid []byte, seed string, mode int) (bool, error)
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
}

View File

@ -52,13 +52,14 @@ type DexEntry struct {
type StarterData map[int]StarterEntry
type StarterEntry struct {
Moveset interface{} `json:"moveset"`
EggMoves int `json:"eggMoves"`
CandyCount int `json:"candyCount"`
Friendship int `json:"friendship"`
AbilityAttr int `json:"abilityAttr"`
PassiveAttr int `json:"passiveAttr"`
ValueReduction int `json:"valueReduction"`
Moveset interface{} `json:"moveset"`
EggMoves int `json:"eggMoves"`
CandyCount int `json:"candyCount"`
Friendship int `json:"friendship"`
AbilityAttr int `json:"abilityAttr"`
PassiveAttr int `json:"passiveAttr"`
ValueReduction int `json:"valueReduction"`
ClassicWinCount int `json:"classicWinCount"`
}
type StarterMoveData map[int]interface{}

View File

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

View File

@ -0,0 +1,54 @@
services:
server:
command: --debug --dbaddr db --dbuser pokerogue --dbpass pokerogue --dbname pokeroguedb
image: ghcr.io/pagefaultgames/rogueserver:master
restart: unless-stopped
depends_on:
db:
condition: service_healthy
networks:
- internal
ports:
- "8001:8001"
db:
image: mariadb:11
restart: unless-stopped
healthcheck:
test: [ "CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized" ]
start_period: 10s
start_interval: 10s
interval: 1m
timeout: 5s
retries: 3
environment:
MYSQL_ROOT_PASSWORD: admin
MYSQL_DATABASE: pokeroguedb
MYSQL_USER: pokerogue
MYSQL_PASSWORD: pokerogue
volumes:
- database:/var/lib/mysql
networks:
- internal
# Watchtower is a service that will automatically update your running containers
# when a new image is available. This is useful for keeping your server up-to-date.
# see https://containrrr.dev/watchtower/ for more information.
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: always
security_opt:
- no-new-privileges:true
environment:
WATCHTOWER_CLEANUP: true
WATCHTOWER_SCHEDULE: "@midnight"
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock
volumes:
database:
networks:
internal:

View File

@ -34,10 +34,12 @@ func main() {
debug := flag.Bool("debug", false, "use debug mode")
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")
dbpass := flag.String("dbpass", "", "database password")
dbpass := flag.String("dbpass", "pokerogue", "database password")
dbproto := flag.String("dbproto", "tcp", "protocol for database connection")
dbaddr := flag.String("dbaddr", "localhost", "database address")
dbname := flag.String("dbname", "pokeroguedb", "database name")
@ -63,13 +65,20 @@ func main() {
mux := http.NewServeMux()
// init api
api.Init(mux)
if err := api.Init(mux); err != nil {
log.Fatal(err)
}
// start web server
handler := prodHandler(mux)
if *debug {
err = http.Serve(listener, debugHandler(mux))
handler = debugHandler(mux)
}
if *tlscert == "" {
err = http.Serve(listener, handler)
} else {
err = http.Serve(listener, mux)
err = http.ServeTLS(listener, handler, *tlscert, *tlskey)
}
if err != nil {
log.Fatalf("failed to create http server or server errored: %s", err)
@ -87,12 +96,30 @@ func createListener(proto, addr string) (net.Listener, error) {
}
if proto == "unix" {
os.Chmod(addr, 0777)
if err := os.Chmod(addr, 0777); err != nil {
listener.Close()
return nil, err
}
}
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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Headers", "*")