Add SQLite persistence, import-snapshot tool, and karma functionality

This commit is contained in:
Ilya Groshev
2026-04-20 09:57:47 +03:00
parent c9ad3fa4f4
commit c33e738fd5
70 changed files with 4151 additions and 833 deletions
+2 -4
View File
@@ -5,6 +5,7 @@
server/bin/
server/tmp/
server/lunar-tear
server/import-snapshot
__pycache__/
@@ -13,12 +14,9 @@ node_modules/
# Go
server/vendor/
# Certs (regenerate per-environment)
server/certs/
# Server assets (binary data, too large for git)
server/assets/
db/
# Snapshots (recorded user state)
snapshots/
+77 -9
View File
@@ -7,9 +7,14 @@ Discord server: https://discord.gg/MZAf5aVkJG
### Prerequisites
- Go 1.24+
- Go 1.25+
- [goose](https://github.com/pressly/goose) migration tool
- Populated `server/assets/` directory
```bash
go install github.com/pressly/goose/v3/cmd/goose@latest
```
### Regenerate protobuf stubs
```bash
@@ -17,14 +22,54 @@ cd server
make proto
```
### Database
Player state is stored in a SQLite database. Run migrations before starting the server:
```bash
cd server
make migrate
```
Or manually:
```bash
cd server
mkdir -p db
goose -dir migrations sqlite3 db/game.db up
```
### Importing a Snapshot
To import a JSON snapshot into the database, use the import tool. The `--uuid` flag must match the UUID your game client sends during authentication:
```bash
cd server
make import SNAPSHOT=snapshots/scene_1.json UUID=<your-client-uuid>
```
Or directly:
```bash
go run ./cmd/import-snapshot \
--snapshot snapshots/scene_1.json \
--uuid <your-client-uuid> \
--db db/game.db
```
| Flag | Default | Description |
| ------------ | ------------ | --------------------------------------------- |
| `--snapshot` | *(required)* | Path to JSON snapshot file |
| `--uuid` | *(required)* | UUID to assign (must match the client's UUID) |
| `--db` | `db/game.db` | SQLite database path |
### Run
```bash
cd server
sudo go run ./cmd/lunar-tear \
--host 10.0.2.2 \
--http-port 8080 \
--scene 13
--http-port 8080
```
`sudo` is needed because gRPC binds to port 443 (privileged). On Linux you can use `setcap` instead:
@@ -32,7 +77,7 @@ sudo go run ./cmd/lunar-tear \
```bash
go build -o lunar-tear ./cmd/lunar-tear
sudo setcap cap_net_bind_service=+ep ./lunar-tear
./lunar-tear --host 10.0.2.2 --http-port 8080 --scene 13
./lunar-tear --host 10.0.2.2 --http-port 8080
```
### Ports
@@ -44,11 +89,34 @@ sudo setcap cap_net_bind_service=+ep ./lunar-tear
### Flags
| Flag | Default | Description |
| ---------------------- | ------------------- | -------------------------------------------------------- |
| `--host` | `127.0.0.1` | hostname/IP given to the client |
| `--http-port` | `8080` | HTTP/Octo server port |
| `--scene` | `0` | bootstrap new users to scene N (0 = fresh start) |
| Flag | Default | Description |
| ------------- | ------------ | ------------------------------- |
| `--host` | `127.0.0.1` | hostname/IP given to the client |
| `--http-port` | `8080` | HTTP/Octo server port |
| `--db` | `db/game.db` | SQLite database path |
### Docker
Migrations run automatically on container start.
```bash
cd server
docker compose up -d
```
The `db/` directory is mounted as a volume so the database persists across restarts. Make sure `assets/` is populated before starting.
### Makefile Targets
All targets run from the `server/` directory.
| Target | Description |
| -------------- | ------------------------------------------------------- |
| `make proto` | Regenerate protobuf stubs |
| `make build` | Build the server binary |
| `make build-import` | Build the import-snapshot tool |
| `make migrate` | Run goose migrations on `db/game.db` |
| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) |
## ⚠️ Legal Disclaimer
+3
View File
@@ -13,6 +13,7 @@ RUN apk add --no-cache \
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest &&\
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest &&\
go install github.com/pressly/goose/v3/cmd/goose@latest &&\
PATH="$PATH:$(go env GOPATH)/bin" make proto &&\
go build -o lunar-tear ./cmd/lunar-tear &&\
setcap cap_net_bind_service=+ep ./lunar-tear
@@ -26,6 +27,8 @@ RUN chown 1000:1000 /opt/lunar-tear
USER 1000
COPY --from=builder /usr/local/src/lunar-tear .
COPY --from=builder /root/go/bin/goose /usr/local/bin/goose
COPY --from=builder /usr/local/src/migrations ./migrations
COPY entrypoint.sh .
+20 -1
View File
@@ -7,4 +7,23 @@ proto:
protoc -I . $(PROTO_USED) --go_out=. --go_opt=module=lunar-tear/server --go-grpc_out=. --go-grpc_opt=module=lunar-tear/server
@echo "Generated in gen/proto/"
.PHONY: proto
build:
go build -o lunar-tear ./cmd/lunar-tear
build-import:
go build -o import-snapshot ./cmd/import-snapshot
migrate:
mkdir -p db
goose -dir migrations sqlite3 db/game.db up
import:
ifndef SNAPSHOT
$(error SNAPSHOT is required, e.g. make import SNAPSHOT=snapshots/scene_1.json UUID=...)
endif
ifndef UUID
$(error UUID is required, e.g. make import SNAPSHOT=snapshots/scene_1.json UUID=...)
endif
go run ./cmd/import-snapshot --snapshot $(SNAPSHOT) --uuid $(UUID)
.PHONY: proto build build-import migrate import
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"encoding/json"
"flag"
"log"
"os"
"lunar-tear/server/internal/database"
"lunar-tear/server/internal/store"
"lunar-tear/server/internal/store/sqlite"
)
func main() {
dbPath := flag.String("db", "db/game.db", "SQLite database path")
snapshotPath := flag.String("snapshot", "", "Path to JSON snapshot file (required)")
userUuid := flag.String("uuid", "", "UUID to assign to the imported user (must match the client's UUID)")
flag.Parse()
if *snapshotPath == "" {
log.Fatal("--snapshot flag is required")
}
if *userUuid == "" {
log.Fatal("--uuid flag is required")
}
data, err := os.ReadFile(*snapshotPath)
if err != nil {
log.Fatalf("read snapshot: %v", err)
}
log.Printf("read %d bytes from %s", len(data), *snapshotPath)
var u store.UserState
if err := json.Unmarshal(data, &u); err != nil {
log.Fatalf("unmarshal snapshot: %v", err)
}
u.EnsureMaps()
u.Uuid = *userUuid
log.Printf("parsed user %d (uuid=%s, costumes=%d, weapons=%d, characters=%d, quests=%d)",
u.UserId, u.Uuid, len(u.Costumes), len(u.Weapons), len(u.Characters), len(u.Quests))
db, err := database.Open(*dbPath)
if err != nil {
log.Fatalf("open database: %v", err)
}
defer db.Close()
userStore := sqlite.New(db, nil)
if err := userStore.ImportUser(&u); err != nil {
log.Fatalf("import user: %v", err)
}
log.Printf("imported user %d successfully", u.UserId)
}
+14 -5
View File
@@ -12,7 +12,7 @@ import (
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/service"
"lunar-tear/server/internal/store/memory"
"lunar-tear/server/internal/store"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ -38,9 +38,13 @@ func (l loggingListener) Accept() (net.Conn, error) {
func startGRPC(
host string,
octoURL string,
userStore *memory.MemoryStore,
userStore interface {
store.UserRepository
store.SessionRepository
},
questEngine *questflow.QuestHandler,
gachaHandler *gacha.GachaHandler,
gachaEntries []store.GachaCatalogEntry,
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
loginBonusCatalog *masterdata.LoginBonusCatalog,
characterViewerCatalog *masterdata.CharacterViewerCatalog,
@@ -77,6 +81,7 @@ func startGRPC(
userStore,
questEngine,
gachaHandler,
gachaEntries,
cageOrnamentCatalog,
loginBonusCatalog,
characterViewerCatalog,
@@ -111,9 +116,13 @@ func registerServices(
srv *grpc.Server,
host string,
octoURL string,
userStore *memory.MemoryStore,
userStore interface {
store.UserRepository
store.SessionRepository
},
questEngine *questflow.QuestHandler,
gachaHandler *gacha.GachaHandler,
gachaEntries []store.GachaCatalogEntry,
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
loginBonusCatalog *masterdata.LoginBonusCatalog,
characterViewerCatalog *masterdata.CharacterViewerCatalog,
@@ -133,13 +142,13 @@ func registerServices(
sideStoryCatalog *masterdata.SideStoryCatalog,
bigHuntCatalog *masterdata.BigHuntCatalog,
) {
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(userStore))
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(gachaEntries))
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore))
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(host, int32(443), octoURL))
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine))
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, userStore, gachaHandler))
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, gachaEntries, gachaHandler))
pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore))
pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer())
pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, gimmickCatalog))
+10 -25
View File
@@ -3,23 +3,21 @@ package main
import (
"flag"
"log"
"os"
"strconv"
"strings"
"lunar-tear/server/internal/database"
"lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/store/memory"
"lunar-tear/server/internal/store/sqlite"
)
func main() {
httpPort := flag.Int("http-port", 8080, "HTTP server port (Octo API)")
host := flag.String("host", "127.0.0.1", "hostname the client will connect to")
scene := flag.Int("scene", 0, "Bootstrap to scene N (0 = fresh start)")
latestScene := flag.Bool("latest-scene", false, "Bootstrap from the most recently saved snapshot (overrides -scene)")
starterItems := flag.Bool("starter-items", false, "Grant starter items to new users")
dbPath := flag.String("db", "db/game.db", "SQLite database path")
flag.Parse()
octoURL := "http://" + *host + ":" + strconv.Itoa(*httpPort)
@@ -34,18 +32,12 @@ func main() {
go startHTTP(*httpPort, resourcesBaseURL)
snapshotDir := "snapshots"
if err := os.MkdirAll(snapshotDir, 0755); err != nil {
log.Fatalf("create snapshot dir: %v", err)
}
if *latestScene {
if id, ok := memory.LatestSnapshotSceneId(snapshotDir); ok {
*scene = int(id)
log.Printf("[latest-scene] auto-selected most recent snapshot: scene=%d", id)
} else {
log.Printf("[latest-scene] no snapshots found in %q; starting fresh", snapshotDir)
}
db, err := database.Open(*dbPath)
if err != nil {
log.Fatalf("open database: %v", err)
}
defer db.Close()
log.Printf("database opened: %s", *dbPath)
gameConfig, err := masterdata.LoadGameConfig()
if err != nil {
@@ -65,14 +57,7 @@ func main() {
log.Fatalf("load quest catalog: %v", err)
}
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
userStore := memory.New(gametime.Now,
memory.WithSnapshotDir(snapshotDir),
memory.WithSceneId(int32(*scene)),
memory.WithStarterItems(*starterItems),
)
if *scene != 0 {
log.Printf("bootstrap scene: %d (from snapshot)", *scene)
}
userStore := sqlite.New(db, gametime.Now)
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
if err != nil {
@@ -99,7 +84,6 @@ func main() {
gachaPool.BuildFeaturedMapping(gachaEntries)
gachaPool.BuildBannerPools(gachaEntries)
masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool)
userStore.ReplaceCatalog(gachaEntries)
dupExchange, err := masterdata.LoadDupExchange()
if err != nil {
@@ -185,6 +169,7 @@ func main() {
userStore,
questHandler,
gachaHandler,
gachaEntries,
cageOrnamentCatalog,
loginBonusCatalog,
characterViewerCatalog,
+1 -1
View File
@@ -5,9 +5,9 @@ services:
environment:
LUNAR_HOST: 127.0.0.1
LUNAR_HTTP_PORT: 8080
LUNAR_SCENE: 0
volumes:
- ./assets:/opt/lunar-tear/assets
- ./db:/opt/lunar-tear/db
ports:
- 443:443 # grpc, hardcoded by the client, not configurable
- 8080:8080
+4 -1
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env sh
set -e
./lunar-tear --host "${LUNAR_HOST}" --http-port "${LUNAR_HTTP_PORT}" --scene "${LUNAR_SCENE}"
mkdir -p db
goose -dir migrations sqlite3 db/game.db up
exec ./lunar-tear --host "${LUNAR_HOST}" --http-port "${LUNAR_HTTP_PORT}"
+22 -6
View File
@@ -1,17 +1,33 @@
module lunar-tear/server
go 1.24.2
go 1.25.0
require (
github.com/google/uuid v1.6.0
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/net v0.50.0
google.golang.org/grpc v1.79.1
google.golang.org/protobuf v1.36.11
)
require (
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pressly/goose/v3 v3.27.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.48.2 // indirect
)
+49 -14
View File
@@ -1,5 +1,9 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -10,33 +14,64 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
+42
View File
@@ -0,0 +1,42 @@
package database
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "modernc.org/sqlite"
)
func Open(path string) (*sql.DB, error) {
if dir := filepath.Dir(path); dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("create db directory %q: %w", dir, err)
}
}
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open sqlite %q: %w", path, err)
}
pragmas := []string{
"PRAGMA journal_mode=WAL",
"PRAGMA foreign_keys=ON",
"PRAGMA busy_timeout=5000",
}
for _, p := range pragmas {
if _, err := db.Exec(p); err != nil {
db.Close()
return nil, fmt.Errorf("exec %q: %w", p, err)
}
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("ping sqlite %q: %w", path, err)
}
return db, nil
}
+6
View File
@@ -35,6 +35,9 @@ type GameConfig struct {
QuestSkipMaxCountAtOnce int32
WeaponLimitBreakAvailableCount int32
CostumeLotteryEffectUnlockSlotConsumeGold int32
CostumeLotteryEffectDrawSlotConsumeGold int32
}
func LoadGameConfig() (*GameConfig, error) {
@@ -73,6 +76,9 @@ func LoadGameConfig() (*GameConfig, error) {
cfg.WeaponLimitBreakAvailableCount = parseInt32(kv, "WEAPON_LIMIT_BREAK_AVAILABLE_COUNT")
cfg.CostumeLotteryEffectUnlockSlotConsumeGold = parseInt32(kv, "COSTUME_LOTTERY_EFFECT_UNLOCK_SLOT_CONSUME_GOLD")
cfg.CostumeLotteryEffectDrawSlotConsumeGold = parseInt32(kv, "COSTUME_LOTTERY_EFFECT_DRAW_SLOT_CONSUME_GOLD")
return cfg, nil
}
+59
View File
@@ -78,6 +78,31 @@ type CostumeActiveSkillEnhanceMaterialRow struct {
SortOrder int32 `json:"SortOrder"`
}
type CostumeLotteryEffectRow struct {
CostumeId int32 `json:"CostumeId"`
SlotNumber int32 `json:"SlotNumber"`
CostumeLotteryEffectOddsGroupId int32 `json:"CostumeLotteryEffectOddsGroupId"`
CostumeLotteryEffectUnlockMaterialGroupId int32 `json:"CostumeLotteryEffectUnlockMaterialGroupId"`
CostumeLotteryEffectDrawMaterialGroupId int32 `json:"CostumeLotteryEffectDrawMaterialGroupId"`
CostumeLotteryEffectReleaseScheduleId int32 `json:"CostumeLotteryEffectReleaseScheduleId"`
}
type CostumeLotteryEffectMaterialGroupRow struct {
CostumeLotteryEffectMaterialGroupId int32 `json:"CostumeLotteryEffectMaterialGroupId"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
SortOrder int32 `json:"SortOrder"`
}
type CostumeLotteryEffectOddsRow struct {
CostumeLotteryEffectOddsGroupId int32 `json:"CostumeLotteryEffectOddsGroupId"`
OddsNumber int32 `json:"OddsNumber"`
Weight int32 `json:"Weight"`
CostumeLotteryEffectType int32 `json:"CostumeLotteryEffectType"`
CostumeLotteryEffectTargetId int32 `json:"CostumeLotteryEffectTargetId"`
RarityType int32 `json:"RarityType"`
}
type CostumeCatalog struct {
Costumes map[int32]CostumeMasterRow
Materials map[int32]MaterialRow
@@ -96,6 +121,10 @@ type CostumeCatalog struct {
ActiveSkillEnhanceMats map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel]
ActiveSkillMaxLevelByRarity map[int32]NumericalFunc
ActiveSkillCostByRarity map[int32]NumericalFunc
LotteryEffects map[[2]int32]CostumeLotteryEffectRow // key: [costumeId, slotNumber]
LotteryEffectMats map[int32][]CostumeLotteryEffectMaterialGroupRow // key: materialGroupId (both unlock and draw)
LotteryEffectOdds map[int32][]CostumeLotteryEffectOddsRow // key: oddsGroupId
}
func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
@@ -149,6 +178,19 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
return nil, fmt.Errorf("load costume active skill enhancement material table: %w", err)
}
lotteryEffectRows, err := utils.ReadJSON[CostumeLotteryEffectRow]("EntityMCostumeLotteryEffectTable.json")
if err != nil {
return nil, fmt.Errorf("load costume lottery effect table: %w", err)
}
lotteryEffectMatRows, err := utils.ReadJSON[CostumeLotteryEffectMaterialGroupRow]("EntityMCostumeLotteryEffectMaterialGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume lottery effect material group table: %w", err)
}
lotteryEffectOddsRows, err := utils.ReadJSON[CostumeLotteryEffectOddsRow]("EntityMCostumeLotteryEffectOddsGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume lottery effect odds group table: %w", err)
}
catalog := &CostumeCatalog{
Costumes: make(map[int32]CostumeMasterRow, len(costumes)),
Materials: matCatalog.ByType[model.MaterialTypeCostumeEnhancement],
@@ -167,6 +209,10 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
ActiveSkillEnhanceMats: make(map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow),
ActiveSkillMaxLevelByRarity: make(map[int32]NumericalFunc, len(rarities)),
ActiveSkillCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
LotteryEffects: make(map[[2]int32]CostumeLotteryEffectRow, len(lotteryEffectRows)),
LotteryEffectMats: make(map[int32][]CostumeLotteryEffectMaterialGroupRow),
LotteryEffectOdds: make(map[int32][]CostumeLotteryEffectOddsRow),
}
for _, row := range costumes {
@@ -242,5 +288,18 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
catalog.ActiveSkillEnhanceMats[key] = append(catalog.ActiveSkillEnhanceMats[key], row)
}
for _, row := range lotteryEffectRows {
key := [2]int32{row.CostumeId, row.SlotNumber}
catalog.LotteryEffects[key] = row
}
for _, row := range lotteryEffectMatRows {
gid := row.CostumeLotteryEffectMaterialGroupId
catalog.LotteryEffectMats[gid] = append(catalog.LotteryEffectMats[gid], row)
}
for _, row := range lotteryEffectOddsRows {
gid := row.CostumeLotteryEffectOddsGroupId
catalog.LotteryEffectOdds[gid] = append(catalog.LotteryEffectOdds[gid], row)
}
return catalog, nil
}
+8
View File
@@ -22,6 +22,14 @@ const (
CostumeAwakenEffectTypeItemAcquire CostumeAwakenEffectType = 3
)
type CostumeLotteryEffectType int32
const (
CostumeLotteryEffectTypeUnknown CostumeLotteryEffectType = 0
CostumeLotteryEffectTypeAbility CostumeLotteryEffectType = 1
CostumeLotteryEffectTypeStatusUp CostumeLotteryEffectType = 2
)
type WeaponAwakenEffectType int32
const (
+4 -2
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"log"
"github.com/google/uuid"
"lunar-tear/server/internal/gameutil"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
@@ -276,7 +278,7 @@ func (h *QuestHandler) grantCompanion(user *store.UserState, companionId int32,
return
}
}
key := fmt.Sprintf("reward-companion-%d", companionId)
key := uuid.New().String()
user.Companions[key] = store.CompanionState{
UserCompanionUuid: key,
CompanionId: companionId,
@@ -306,7 +308,7 @@ func (h *QuestHandler) grantParts(user *store.UserState, partsId int32, nowMilli
}
}
key := fmt.Sprintf("reward-parts-%d", partsId)
key := uuid.New().String()
user.Parts[key] = store.PartsState{
UserPartsUuid: key,
PartsId: partsId,
+4 -4
View File
@@ -11,15 +11,15 @@ import (
type BannerServiceServer struct {
pb.UnimplementedBannerServiceServer
gacha store.GachaRepository
catalog []store.GachaCatalogEntry
}
func NewBannerServiceServer(gacha store.GachaRepository) *BannerServiceServer {
return &BannerServiceServer{gacha: gacha}
func NewBannerServiceServer(catalog []store.GachaCatalogEntry) *BannerServiceServer {
return &BannerServiceServer{catalog: catalog}
}
func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) {
catalog, _ := s.gacha.SnapshotCatalog()
catalog := s.catalog
var termLimited []*pb.GachaBanner
var latestChapter *pb.GachaBanner
for _, entry := range catalog {
+2 -4
View File
@@ -43,8 +43,7 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R
s.granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
})
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
userdata.FullClientTableMap(user),
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user,
[]string{
"IUserMaterial", "IUserConsumableItem", "IUserGem",
"IUserCostume", "IUserCostumeActiveSkill", "IUserCharacter",
@@ -82,8 +81,7 @@ func (s *CageOrnamentServiceServer) RecordAccess(ctx context.Context, req *pb.Re
}
})
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
userdata.FullClientTableMap(user),
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user,
[]string{"IUserCageOrnamentReward"},
))
+2 -2
View File
@@ -35,7 +35,7 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq
return &pb.RebirthResponse{}, nil
}
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"})
@@ -78,7 +78,7 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq
}
rebirthTables := []string{"IUserCharacterRebirth", "IUserMaterial", "IUserConsumableItem"}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), rebirthTables)
tables := userdata.ProjectTables(snapshot, rebirthTables)
diff := tracker.Apply(snapshot, tables)
return &pb.RebirthResponse{DiffUserData: diff}, nil
+2 -2
View File
@@ -27,7 +27,7 @@ func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}).
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"})
@@ -54,7 +54,7 @@ func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.
"IUserConsumableItem",
"IUserGem",
}
tables := userdata.SelectTables(userdata.FullClientTableMap(user), boardTables)
tables := userdata.ProjectTables(user, boardTables)
diff := tracker.Apply(user, tables)
return &pb.ReleasePanelResponse{DiffUserData: diff}, nil
+1 -1
View File
@@ -29,7 +29,7 @@ func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _
log.Printf("[CharacterViewerService] CharacterViewerTop")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err))
}
+1 -2
View File
@@ -77,8 +77,7 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE
return nil, fmt.Errorf("companion enhance: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, companionDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, companionDiffTables))
return &pb.CompanionEnhanceResponse{
DiffUserData: diff,
+2 -2
View File
@@ -28,7 +28,7 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"})
@@ -66,7 +66,7 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab
return nil, fmt.Errorf("consumable item sell: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), []string{"IUserConsumableItem"})
tables := userdata.ProjectTables(snapshot, []string{"IUserConsumableItem"})
diff := tracker.Apply(snapshot, tables)
return &pb.ConsumableItemSellResponse{
+1 -2
View File
@@ -34,8 +34,7 @@ func (s *ContentsStoryServiceServer) RegisterPlayed(ctx context.Context, req *pb
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserContentsStory"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserContentsStory"}))
return &pb.ContentsStoryRegisterPlayedResponse{
DiffUserData: diff,
+219 -11
View File
@@ -4,6 +4,9 @@ import (
"context"
"fmt"
"log"
"math/rand"
"github.com/google/uuid"
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime"
@@ -95,8 +98,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
return nil, fmt.Errorf("costume enhance: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, costumeDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, costumeDiffTables))
return &pb.EnhanceResponse{
IsGreatSuccess: false,
@@ -177,8 +179,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
return nil, fmt.Errorf("costume awaken: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, awakenDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, awakenDiffTables))
return &pb.AwakenResponse{
DiffUserData: diff,
@@ -229,10 +230,12 @@ func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, ite
return
}
key := fmt.Sprintf("awaken-thought-%d", acq.PossessionId)
if _, exists := user.Thoughts[key]; exists {
return
for _, t := range user.Thoughts {
if t.ThoughtId == acq.PossessionId {
return
}
}
key := uuid.New().String()
user.Thoughts[key] = store.ThoughtState{
UserThoughtUuid: key,
ThoughtId: acq.PossessionId,
@@ -329,8 +332,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
return nil, fmt.Errorf("costume enhance active skill: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, activeSkillDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, activeSkillDiffTables))
return &pb.EnhanceActiveSkillResponse{
DiffUserData: diff,
@@ -387,10 +389,216 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
return nil, fmt.Errorf("costume limit break: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, costumeDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, costumeDiffTables))
return &pb.LimitBreakResponse{
DiffUserData: diff,
}, nil
}
var lotteryEffectDiffTables = []string{
"IUserCostume",
"IUserCostumeLotteryEffect",
"IUserCostumeLotteryEffectAbility",
"IUserCostumeLotteryEffectStatusUp",
"IUserCostumeLotteryEffectPending",
"IUserConsumableItem",
"IUserMaterial",
}
func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req *pb.UnlockLotteryEffectSlotRequest) (*pb.UnlockLotteryEffectSlotResponse, error) {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
costume, ok := user.Costumes[req.UserCostumeUuid]
if !ok {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: costume uuid=%s not found", req.UserCostumeUuid)
return
}
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
if !ok {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
return
}
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectUnlockSlotConsumeGold
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId]
for _, mat := range mats {
cur := user.Materials[mat.MaterialId]
cost := mat.Count
if cur < cost {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
cost = cur
}
user.Materials[mat.MaterialId] = cur - cost
}
key := store.CostumeLotteryEffectKey{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
}
user.CostumeLotteryEffects[key] = store.CostumeLotteryEffectState{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
OddsNumber: 0,
LatestVersion: nowMillis,
}
costume.CostumeLotteryEffectUnlockedSlotCount++
costume.LatestVersion = nowMillis
user.Costumes[req.UserCostumeUuid] = costume
log.Printf("[CostumeService] UnlockLotteryEffectSlot: costumeId=%d slot=%d unlocked slotCount=%d", costume.CostumeId, req.SlotNumber, costume.CostumeLotteryEffectUnlockedSlotCount)
})
if err != nil {
return nil, fmt.Errorf("costume unlock lottery effect slot: %w", err)
}
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, lotteryEffectDiffTables))
return &pb.UnlockLotteryEffectSlotResponse{
DiffUserData: diff,
}, nil
}
func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.DrawLotteryEffectRequest) (*pb.DrawLotteryEffectResponse, error) {
log.Printf("[CostumeService] DrawLotteryEffect: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}).
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"}).
Track("IUserCostumeLotteryEffectPending", oldUser, userdata.SortedCostumeLotteryEffectPendingRecords, []string{"userId", "userCostumeUuid"})
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
costume, ok := user.Costumes[req.UserCostumeUuid]
if !ok {
log.Printf("[CostumeService] DrawLotteryEffect: costume uuid=%s not found", req.UserCostumeUuid)
return
}
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
if !ok {
log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
return
}
oddsPool := s.catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId]
if len(oddsPool) == 0 {
log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId)
return
}
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectDrawSlotConsumeGold
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId]
for _, mat := range mats {
cur := user.Materials[mat.MaterialId]
cost := mat.Count
if cur < cost {
log.Printf("[CostumeService] DrawLotteryEffect: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
cost = cur
}
user.Materials[mat.MaterialId] = cur - cost
}
totalWeight := int32(0)
for _, row := range oddsPool {
totalWeight += row.Weight
}
roll := rand.Int31n(totalWeight)
var picked masterdata.CostumeLotteryEffectOddsRow
for _, row := range oddsPool {
roll -= row.Weight
if roll < 0 {
picked = row
break
}
}
key := store.CostumeLotteryEffectKey{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
}
existing := user.CostumeLotteryEffects[key]
if existing.OddsNumber == 0 {
existing.UserCostumeUuid = req.UserCostumeUuid
existing.SlotNumber = req.SlotNumber
existing.OddsNumber = picked.OddsNumber
existing.LatestVersion = nowMillis
user.CostumeLotteryEffects[key] = existing
} else {
user.CostumeLotteryEffectPending[req.UserCostumeUuid] = store.CostumeLotteryEffectPendingState{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
OddsNumber: picked.OddsNumber,
LatestVersion: nowMillis,
}
}
log.Printf("[CostumeService] DrawLotteryEffect: costumeId=%d slot=%d drew oddsNumber=%d type=%d targetId=%d firstDraw=%v",
costume.CostumeId, req.SlotNumber, picked.OddsNumber, picked.CostumeLotteryEffectType, picked.CostumeLotteryEffectTargetId, existing.OddsNumber == 0)
})
if err != nil {
return nil, fmt.Errorf("costume draw lottery effect: %w", err)
}
diff := tracker.Apply(snapshot, userdata.ProjectTables(snapshot, lotteryEffectDiffTables))
return &pb.DrawLotteryEffectResponse{
DiffUserData: diff,
}, nil
}
func (s *CostumeServiceServer) ConfirmLotteryEffect(ctx context.Context, req *pb.ConfirmLotteryEffectRequest) (*pb.ConfirmLotteryEffectResponse, error) {
log.Printf("[CostumeService] ConfirmLotteryEffect: uuid=%s accept=%v", req.UserCostumeUuid, req.IsAccept)
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserCostumeLotteryEffectPending", oldUser, userdata.SortedCostumeLotteryEffectPendingRecords, []string{"userId", "userCostumeUuid"})
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
pending, ok := user.CostumeLotteryEffectPending[req.UserCostumeUuid]
if !ok {
log.Printf("[CostumeService] ConfirmLotteryEffect: no pending for uuid=%s", req.UserCostumeUuid)
return
}
if req.IsAccept {
key := store.CostumeLotteryEffectKey{
UserCostumeUuid: pending.UserCostumeUuid,
SlotNumber: pending.SlotNumber,
}
effect := user.CostumeLotteryEffects[key]
effect.UserCostumeUuid = pending.UserCostumeUuid
effect.SlotNumber = pending.SlotNumber
effect.OddsNumber = pending.OddsNumber
effect.LatestVersion = nowMillis
user.CostumeLotteryEffects[key] = effect
log.Printf("[CostumeService] ConfirmLotteryEffect: accepted oddsNumber=%d for slot=%d", pending.OddsNumber, pending.SlotNumber)
} else {
log.Printf("[CostumeService] ConfirmLotteryEffect: rejected oddsNumber=%d for slot=%d", pending.OddsNumber, pending.SlotNumber)
}
delete(user.CostumeLotteryEffectPending, req.UserCostumeUuid)
})
if err != nil {
return nil, fmt.Errorf("costume confirm lottery effect: %w", err)
}
diff := tracker.Apply(snapshot, userdata.ProjectTables(snapshot, lotteryEffectDiffTables))
return &pb.ConfirmLotteryEffectResponse{
DiffUserData: diff,
}, nil
}
+2 -2
View File
@@ -42,12 +42,12 @@ func (s *DataServiceServer) GetUserData(ctx context.Context, req *pb.UserDataGet
log.Printf("[DataService] GetUserData: tables=%v", req.TableName)
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
defaults := userdata.FirstEntranceClientTableMap(user)
defaults := userdata.FullClientTableMap(user)
result := userdata.SelectTables(defaults, req.TableName)
return &pb.UserDataGetResponse{
UserDataJson: result,
+7 -7
View File
@@ -32,7 +32,7 @@ func (s *DeckServiceServer) UpdateName(ctx context.Context, req *pb.UpdateNameRe
user.Decks[deckKey] = deck
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserDeck"})
result := userdata.ProjectTables(user, []string{"IUserDeck"})
return &pb.UpdateNameResponse{
DiffUserData: userdata.BuildDiffFromTables(result),
}, nil
@@ -81,7 +81,7 @@ func (s *DeckServiceServer) RefreshDeckPower(ctx context.Context, req *pb.Refres
}
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote",
})
return &pb.RefreshDeckPowerResponse{
@@ -133,7 +133,7 @@ func (s *DeckServiceServer) RefreshMultiDeckPower(ctx context.Context, req *pb.R
}
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote",
})
return &pb.RefreshMultiDeckPowerResponse{
@@ -173,7 +173,7 @@ func (s *DeckServiceServer) ReplaceDeck(ctx context.Context, req *pb.ReplaceDeck
}
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords,
[]string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}).
@@ -189,7 +189,7 @@ func (s *DeckServiceServer) ReplaceDeck(ctx context.Context, req *pb.ReplaceDeck
store.ApplyDeckReplacement(user, model.DeckType(req.DeckType), req.UserDeckNumber, deckSlotsFromProto(req.Deck), gametime.NowMillis())
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup",
"IUserDeckCharacterDressupCostume",
})
@@ -202,7 +202,7 @@ func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.Repla
log.Printf("[DeckService] ReplaceTripleDeck: deckType=%d deckNumber=%d", req.DeckType, req.UserDeckNumber)
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords,
[]string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}).
@@ -231,7 +231,7 @@ func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.Repla
}
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup",
"IUserDeckCharacterDressupCostume",
})
+1 -2
View File
@@ -33,8 +33,7 @@ func (s *DokanServiceServer) RegisterDokanConfirmed(ctx context.Context, req *pb
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserDokan"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserDokan"}))
return &pb.RegisterDokanConfirmedResponse{
DiffUserData: diff,
+3 -6
View File
@@ -71,8 +71,7 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx
return nil, fmt.Errorf("start explore: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, exploreDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, exploreDiffTables))
return &pb.StartExploreResponse{
DiffUserData: diff,
@@ -124,8 +123,7 @@ func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.Finish
return nil, fmt.Errorf("finish explore: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, exploreFinishDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, exploreFinishDiffTables))
rewards := []*pb.ExploreReward{
{
@@ -161,8 +159,7 @@ func (s *ExploreServiceServer) RetireExplore(ctx context.Context, req *pb.Retire
return nil, fmt.Errorf("retire explore: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserExplore"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserExplore"}))
return &pb.RetireExploreResponse{
DiffUserData: diff,
+10 -13
View File
@@ -34,20 +34,20 @@ type GachaServiceServer struct {
pb.UnimplementedGachaServiceServer
users store.UserRepository
sessions store.SessionRepository
gacha store.GachaRepository
catalog []store.GachaCatalogEntry
handler *gacha.GachaHandler
}
func NewGachaServiceServer(
users store.UserRepository,
sessions store.SessionRepository,
gachaRepo store.GachaRepository,
catalog []store.GachaCatalogEntry,
handler *gacha.GachaHandler,
) *GachaServiceServer {
return &GachaServiceServer{
users: users,
sessions: sessions,
gacha: gachaRepo,
catalog: catalog,
handler: handler,
}
}
@@ -55,7 +55,7 @@ func NewGachaServiceServer(
func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) {
log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType)
catalog, _ := s.gacha.SnapshotCatalog()
catalog := s.catalog
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
@@ -132,10 +132,10 @@ func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, cat
func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) {
log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId)
catalog, _ := s.gacha.SnapshotCatalog()
catalog := s.catalog
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
@@ -160,8 +160,7 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque
func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) {
log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount)
catalog, _ := s.gacha.SnapshotCatalog()
entry := findCatalogEntry(catalog, req.GachaId)
entry := findCatalogEntry(s.catalog, req.GachaId)
if entry == nil {
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
}
@@ -293,8 +292,7 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
changedStoryIds := s.handler.Granter.DrainChangedStoryWeaponIds()
diffOrder := append(gachaDiffTables, "IUserWeaponStory")
allTables := userdata.FullClientTableMap(updatedUser)
diff := userdata.BuildDiffFromTablesOrdered(userdata.SelectTables(allTables, diffOrder), diffOrder)
diff := userdata.BuildDiffFromTablesOrdered(userdata.ProjectTables(updatedUser, diffOrder), diffOrder)
userdata.AddWeaponStoryDiff(diff, updatedUser, changedStoryIds)
return &pb.DrawResponse{
@@ -309,8 +307,7 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) {
log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId)
catalog, _ := s.gacha.SnapshotCatalog()
entry := findCatalogEntry(catalog, req.GachaId)
entry := findCatalogEntry(s.catalog, req.GachaId)
if entry == nil {
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
}
@@ -336,7 +333,7 @@ func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBox
func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Empty) (*pb.GetRewardGachaResponse, error) {
log.Printf("[GachaService] GetRewardGacha")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
+2 -2
View File
@@ -71,7 +71,7 @@ func (s *GiftServiceServer) GetGiftList(ctx context.Context, req *pb.GetGiftList
req.RewardKindType, req.ExpirationType, req.IsAscendingSort, req.NextCursor, req.PreviousCursor, req.GetCount)
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
@@ -108,7 +108,7 @@ func (s *GiftServiceServer) GetGiftList(ctx context.Context, req *pb.GetGiftList
func (s *GiftServiceServer) GetGiftReceiveHistoryList(ctx context.Context, req *emptypb.Empty) (*pb.GetGiftReceiveHistoryListResponse, error) {
log.Printf("[GiftService] GetGiftReceiveHistoryList")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
+4 -4
View File
@@ -38,7 +38,7 @@ func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.Updat
user.Gimmick.Sequences[key] = sequence
})
return &pb.UpdateSequenceResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserGimmickSequence"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserGimmickSequence"})),
}, nil
}
@@ -74,7 +74,7 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
GimmickOrnamentReward: []*pb.GimmickReward{},
IsSequenceCleared: false,
GimmickSequenceClearReward: []*pb.GimmickReward{},
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{
"IUserGimmick",
"IUserGimmickOrnamentProgress",
})),
@@ -98,7 +98,7 @@ func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *empt
}
})
return &pb.InitSequenceScheduleResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), gimmickDiffTables)),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, gimmickDiffTables)),
}, nil
}
@@ -119,6 +119,6 @@ func (s *GimmickServiceServer) Unlock(ctx context.Context, req *pb.UnlockRequest
}
})
return &pb.UnlockResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserGimmickUnlock"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserGimmickUnlock"})),
}, nil
}
+4 -4
View File
@@ -2,10 +2,11 @@ package service
import (
"context"
"fmt"
"log"
"time"
"github.com/google/uuid"
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
@@ -55,7 +56,7 @@ func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb
GrantDatetime: now,
},
ExpirationDatetime: now + int64(30*24*time.Hour/time.Millisecond),
UserGiftUuid: fmt.Sprintf("login-bonus-%d-%d", userId, nextStamp),
UserGiftUuid: uuid.New().String(),
})
user.Notifications.GiftNotReceiveCount = int32(len(user.Gifts.NotReceived))
user.LoginBonus.CurrentStampNumber = nextStamp
@@ -63,8 +64,7 @@ func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb
user.LoginBonus.LatestVersion = now
})
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
userdata.FullClientTableMap(user),
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user,
[]string{"IUserLoginBonus"},
))
setCommonResponseTrailers(ctx, diff, false)
+2 -2
View File
@@ -33,7 +33,7 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"})
@@ -71,7 +71,7 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe
return nil, fmt.Errorf("material sell: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), materialDiffTables)
tables := userdata.ProjectTables(snapshot, materialDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.MaterialSellResponse{
+2 -3
View File
@@ -24,13 +24,12 @@ func (s *MissionServiceServer) UpdateMissionProgress(ctx context.Context, req *p
log.Printf("[MissionService] UpdateMissionProgress: cage=%v pictureBook=%v", req.CageMeasurableValues, req.PictureBookMeasurableValues)
userId := currentUserId(ctx, s.users, s.sessions)
snapshot, err := s.users.SnapshotUser(userId)
snapshot, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserMission"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserMission"}))
return &pb.UpdateMissionProgressResponse{
DiffUserData: diff,
+1 -2
View File
@@ -36,8 +36,7 @@ func (s *MovieServiceServer) SaveViewedMovie(ctx context.Context, req *pb.SaveVi
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserMovie"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserMovie"}))
return &pb.SaveViewedMovieResponse{
DiffUserData: diff,
+1 -2
View File
@@ -31,8 +31,7 @@ func (s *NaviCutInServiceServer) RegisterPlayed(ctx context.Context, req *pb.Reg
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserNaviCutIn"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserNaviCutIn"}))
return &pb.RegisterPlayedResponse{
DiffUserData: diff,
+1 -1
View File
@@ -24,7 +24,7 @@ func NewNotificationServiceServer(users store.UserRepository, sessions store.Ses
func (s *NotificationServiceServer) GetHeaderNotification(ctx context.Context, req *emptypb.Empty) (*pb.GetHeaderNotificationResponse, error) {
log.Printf("[NotificationService] GetHeaderNotification")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetHeaderNotificationResponse{
GiftNotReceiveCount: 0,
+1 -2
View File
@@ -36,8 +36,7 @@ func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiD
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserOmikuji"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserOmikuji"}))
return &pb.OmikujiDrawResponse{
OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId),
+4 -6
View File
@@ -37,7 +37,7 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserParts", oldUser, userdata.SortedPartsRecords, []string{"userId", "userPartsUuid"})
@@ -81,7 +81,7 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
return nil, fmt.Errorf("parts sell: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), partsDiffTables)
tables := userdata.ProjectTables(snapshot, partsDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.PartsSellResponse{
@@ -158,8 +158,7 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
return nil, fmt.Errorf("parts enhance: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, partsDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, partsDiffTables))
return &pb.PartsEnhanceResponse{
IsSuccess: isSuccess,
@@ -187,8 +186,7 @@ func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsRep
return nil, fmt.Errorf("parts replace preset: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserPartsPreset"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserPartsPreset"}))
return &pb.PartsReplacePresetResponse{
DiffUserData: diff,
+1 -2
View File
@@ -30,8 +30,7 @@ func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Cont
user.PortalCageStatus.LatestVersion = now
})
tables := userdata.SelectTables(
userdata.FullClientTableMap(user),
tables := userdata.ProjectTables(user,
[]string{"IUserPortalCageStatus"},
)
return &pb.UpdatePortalCageSceneProgressResponse{
+2 -2
View File
@@ -42,7 +42,7 @@ var bigHuntDiffTables = []string{
}
func buildBigHuntDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
tables := userdata.ProjectTables(user, tableNames)
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
}
@@ -331,7 +331,7 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
log.Printf("[BigHuntService] GetBigHuntTopData")
userId := currentUserId(ctx, s.users, s.sessions)
user, _ := s.users.SnapshotUser(userId)
user, _ := s.users.LoadUser(userId)
nowMillis := gametime.NowMillis()
weeklyVersion := gametime.WeeklyVersion(nowMillis)
+1 -1
View File
@@ -29,7 +29,7 @@ func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRep
}
func buildSelectedQuestDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
tables := userdata.ProjectTables(user, tableNames)
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
}
+1 -1
View File
@@ -24,7 +24,7 @@ func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.S
}
func buildSideStoryDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
tables := userdata.ProjectTables(user, tableNames)
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
}
+1 -1
View File
@@ -106,7 +106,7 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty
weeklyScoreResults = []*pb.WeeklyScoreResult{}
}
tables := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
tables := userdata.ProjectTables(user, []string{
"IUserBigHuntWeeklyStatus",
"IUserBigHuntWeeklyMaxScore",
"IUserConsumableItem",
+5 -9
View File
@@ -89,8 +89,7 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
return nil, fmt.Errorf("shop buy: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
userdata.AddWeaponStoryDiff(diff, snapshot, s.granter.DrainChangedStoryWeaponIds())
return &pb.BuyResponse{
@@ -132,8 +131,7 @@ func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.Refresh
return nil, fmt.Errorf("shop refresh: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
return &pb.RefreshResponse{
DiffUserData: diff,
@@ -195,8 +193,7 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
txId := fmt.Sprintf("tx_%d_%d_%d", userId, req.ShopItemId, nowMillis)
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
return &pb.CreatePurchaseTransactionResponse{
PurchaseTransactionId: txId,
@@ -208,13 +205,12 @@ func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context,
log.Printf("[ShopService] PurchaseGooglePlayStoreProduct: txId=%s", req.PurchaseTransactionId)
userId := currentUserId(ctx, s.users, s.sessions)
snapshot, err := s.users.SnapshotUser(userId)
snapshot, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("purchase google play: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
return &pb.PurchaseGooglePlayStoreProductResponse{
OverflowPossession: []*pb.Possession{},
+1 -2
View File
@@ -15,7 +15,6 @@ var startedGameStartTables = []string{
"IUserWeapon",
"IUserWeaponSkill",
"IUserWeaponAbility",
"IUserWeaponStory",
"IUserCompanion",
"IUserDeckCharacter",
"IUserDeck",
@@ -47,7 +46,7 @@ var gimmickDiffTables = []string{
func currentUserId(ctx context.Context, users store.UserRepository, sessions store.SessionRepository) int64 {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if vals := md.Get("x-session-key"); len(vals) > 0 {
if vals := md.Get("x-apb-session-key"); len(vals) > 0 {
if userId, err := sessions.ResolveUserId(vals[0]); err == nil {
return userId
}
+4 -5
View File
@@ -37,11 +37,10 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb
ChoiceId: req.ChoiceId,
}
}
if req.TutorialType == int32(model.TutorialTypeMenuFirst) ||
req.TutorialType == int32(model.TutorialTypeMenuSecond) {
grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
if req.TutorialType == int32(model.TutorialTypeMenuFirst) && req.ProgressPhase == 20 {
store.EnsureDefaultDeck(user, nowMillis)
}
grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
})
tables := []string{"IUserTutorialProgress"}
if req.TutorialType == int32(model.TutorialTypeMenuFirst) ||
@@ -55,7 +54,7 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb
if len(grants) > 0 {
tables = append(tables, "IUserCompanion")
}
result := userdata.SelectTables(userdata.FullClientTableMap(user), tables)
result := userdata.ProjectTables(user, tables)
for _, t := range tables {
log.Printf("[TutorialService] DiffTable %s -> %s", t, result[t])
}
@@ -89,7 +88,7 @@ func (s *TutorialServiceServer) SetTutorialProgressAndReplaceDeck(ctx context.Co
}
})
return &pb.SetTutorialProgressAndReplaceDeckResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{
"IUserTutorialProgress",
"IUserDeck",
"IUserDeckCharacter",
+24 -17
View File
@@ -50,9 +50,13 @@ func setCommonResponseTrailers(ctx context.Context, diff map[string]*pb.DiffData
}
func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
user, err := s.users.EnsureUser(req.Uuid)
userId, err := s.users.CreateUser(req.Uuid)
if err != nil {
return nil, fmt.Errorf("ensure user: %w", err)
return nil, fmt.Errorf("create user: %w", err)
}
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("load user: %w", err)
}
log.Printf("[UserService] RegisterUser: uuid=%s terminalId=%s -> userId=%d", req.Uuid, req.TerminalId, user.UserId)
@@ -66,10 +70,14 @@ func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUs
func (s *UserServiceServer) Auth(ctx context.Context, req *pb.AuthUserRequest) (*pb.AuthUserResponse, error) {
log.Printf("[UserService] Auth: uuid=%s", req.Uuid)
user, session, err := s.sessions.CreateSession(req.Uuid, 24*time.Hour)
session, err := s.sessions.CreateSession(req.Uuid, 24*time.Hour)
if err != nil {
return nil, fmt.Errorf("create session: %w", err)
}
user, err := s.users.LoadUser(session.UserId)
if err != nil {
return nil, fmt.Errorf("load user: %w", err)
}
return &pb.AuthUserResponse{
SessionKey: session.SessionKey,
@@ -84,7 +92,7 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
log.Printf("[UserService] GameStart")
if md, ok := metadata.FromIncomingContext(ctx); ok {
if vals := md.Get("x-session-key"); len(vals) > 0 {
if vals := md.Get("x-apb-session-key"); len(vals) > 0 {
log.Printf("[UserService] GameStart session: %s", vals[0])
}
}
@@ -93,8 +101,7 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
user.GameStartDatetime = gametime.NowMillis()
})
fullTables := userdata.FullClientTableMap(user)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(fullTables, startedGameStartTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user, startedGameStartTables))
setCommonResponseTrailers(ctx, diff, true)
return &pb.GameStartResponse{
@@ -106,12 +113,12 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
func (s *UserServiceServer) TransferUser(ctx context.Context, req *pb.TransferUserRequest) (*pb.TransferUserResponse, error) {
log.Printf("[UserService] TransferUser")
user, err := s.users.EnsureUser(req.Uuid)
userId, err := s.users.CreateUser(req.Uuid)
if err != nil {
return nil, fmt.Errorf("ensure user: %w", err)
return nil, fmt.Errorf("create user: %w", err)
}
return &pb.TransferUserResponse{
UserId: user.UserId,
UserId: userId,
Signature: "transferred-sig",
DiffUserData: userdata.EmptyDiff(),
}, nil
@@ -126,7 +133,7 @@ func (s *UserServiceServer) SetUserName(ctx context.Context, req *pb.SetUserName
user.Profile.NameUpdateDatetime = nowMillis
})
return &pb.SetUserNameResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})),
}, nil
}
@@ -139,7 +146,7 @@ func (s *UserServiceServer) SetUserMessage(ctx context.Context, req *pb.SetUserM
user.Profile.MessageUpdateDatetime = nowMillis
})
return &pb.SetUserMessageResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})),
}, nil
}
@@ -152,7 +159,7 @@ func (s *UserServiceServer) SetUserFavoriteCostumeId(ctx context.Context, req *p
user.Profile.FavoriteCostumeIdUpdateDatetime = nowMillis
})
return &pb.SetUserFavoriteCostumeIdResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})),
}, nil
}
@@ -162,7 +169,7 @@ func (s *UserServiceServer) GetUserProfile(ctx context.Context, req *pb.GetUserP
if userId == 0 {
userId = currentUserId(ctx, s.users, s.sessions)
}
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetUserProfileResponse{DiffUserData: userdata.EmptyDiff()}, nil
}
@@ -219,7 +226,7 @@ func (s *UserServiceServer) SetBirthYearMonth(ctx context.Context, req *pb.SetBi
func (s *UserServiceServer) GetBirthYearMonth(ctx context.Context, _ *emptypb.Empty) (*pb.GetBirthYearMonthResponse, error) {
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetBirthYearMonthResponse{BirthYear: 2000, BirthMonth: 1, DiffUserData: userdata.EmptyDiff()}, nil
}
@@ -228,7 +235,7 @@ func (s *UserServiceServer) GetBirthYearMonth(ctx context.Context, _ *emptypb.Em
func (s *UserServiceServer) GetChargeMoney(ctx context.Context, _ *emptypb.Empty) (*pb.GetChargeMoneyResponse, error) {
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: 0, DiffUserData: userdata.EmptyDiff()}, nil
}
@@ -242,7 +249,7 @@ func (s *UserServiceServer) SetUserSetting(ctx context.Context, req *pb.SetUserS
user.Setting.IsNotifyPurchaseAlert = req.IsNotifyPurchaseAlert
})
return &pb.SetUserSettingResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserSetting"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserSetting"})),
}, nil
}
@@ -252,7 +259,7 @@ func (s *UserServiceServer) GetAndroidArgs(ctx context.Context, req *pb.GetAndro
func (s *UserServiceServer) GetBackupToken(ctx context.Context, req *pb.GetBackupTokenRequest) (*pb.GetBackupTokenResponse, error) {
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetBackupTokenResponse{BackupToken: "mock-backup-token", DiffUserData: userdata.EmptyDiff()}, nil
}
+14 -22
View File
@@ -71,8 +71,7 @@ func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectReques
}
})
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserWeapon"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserWeapon"}))
return &pb.ProtectResponse{DiffUserData: diff}, nil
}
@@ -95,8 +94,7 @@ func (s *WeaponServiceServer) Unprotect(ctx context.Context, req *pb.UnprotectRe
}
})
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserWeapon"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserWeapon"}))
return &pb.UnprotectResponse{DiffUserData: diff}, nil
}
@@ -165,8 +163,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
return nil, fmt.Errorf("weapon enhance by material: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds)
return &pb.EnhanceByMaterialResponse{
@@ -181,7 +178,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
@@ -229,7 +226,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
}
sellDiffTables := []string{"IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", "IUserWeaponAwaken", "IUserConsumableItem"}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), sellDiffTables)
tables := userdata.ProjectTables(snapshot, sellDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.SellResponse{DiffUserData: diff}, nil
@@ -307,8 +304,7 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
return nil, fmt.Errorf("weapon evolve: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds)
return &pb.EvolveResponse{DiffUserData: diff}, nil
@@ -407,8 +403,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
return nil, fmt.Errorf("weapon enhance skill: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
return &pb.EnhanceSkillResponse{DiffUserData: diff}, nil
}
@@ -506,8 +501,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
return nil, fmt.Errorf("weapon enhance ability: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
return &pb.EnhanceAbilityResponse{DiffUserData: diff}, nil
}
@@ -578,8 +572,7 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
return nil, fmt.Errorf("weapon limit break by material: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, limitBreakDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, limitBreakDiffTables))
return &pb.LimitBreakByMaterialResponse{DiffUserData: diff}, nil
}
@@ -590,7 +583,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
@@ -665,7 +658,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
return nil, fmt.Errorf("weapon limit break by weapon: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), limitBreakDiffTables)
tables := userdata.ProjectTables(snapshot, limitBreakDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.LimitBreakByWeaponResponse{DiffUserData: diff}, nil
@@ -677,7 +670,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
@@ -753,7 +746,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
return nil, fmt.Errorf("weapon enhance by weapon: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), weaponDiffTables)
tables := userdata.ProjectTables(snapshot, weaponDiffTables)
diff := tracker.Apply(snapshot, tables)
userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds)
@@ -864,8 +857,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
return nil, fmt.Errorf("weapon awaken: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponAwakenDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponAwakenDiffTables))
return &pb.WeaponAwakenResponse{DiffUserData: diff}, nil
}
@@ -1,12 +1,8 @@
package memory
package store
import (
"maps"
import "maps"
"lunar-tear/server/internal/store"
)
func cloneUserState(u store.UserState) store.UserState {
func CloneUserState(u UserState) UserState {
out := u
out.Tutorials = maps.Clone(u.Tutorials)
out.Characters = maps.Clone(u.Characters)
@@ -15,14 +11,14 @@ func cloneUserState(u store.UserState) store.UserState {
out.Companions = maps.Clone(u.Companions)
out.Thoughts = maps.Clone(u.Thoughts)
out.DeckCharacters = maps.Clone(u.DeckCharacters)
out.DeckSubWeapons = maps.Clone(u.DeckSubWeapons)
out.DeckSubWeapons = cloneSliceMap(u.DeckSubWeapons)
out.DeckParts = cloneSliceMap(u.DeckParts)
out.Decks = maps.Clone(u.Decks)
out.Quests = maps.Clone(u.Quests)
out.QuestMissions = maps.Clone(u.QuestMissions)
out.WeaponStories = maps.Clone(u.WeaponStories)
out.Missions = maps.Clone(u.Missions)
out.Gimmick = store.GimmickState{
out.Gimmick = GimmickState{
Progress: maps.Clone(u.Gimmick.Progress),
OrnamentProgress: maps.Clone(u.Gimmick.OrnamentProgress),
Sequences: maps.Clone(u.Gimmick.Sequences),
@@ -38,6 +34,7 @@ func cloneUserState(u store.UserState) store.UserState {
out.CostumeActiveSkills = maps.Clone(u.CostumeActiveSkills)
out.WeaponSkills = cloneSliceMap(u.WeaponSkills)
out.WeaponAbilities = cloneSliceMap(u.WeaponAbilities)
out.WeaponAwakens = maps.Clone(u.WeaponAwakens)
out.DeckTypeNotes = maps.Clone(u.DeckTypeNotes)
out.WeaponNotes = maps.Clone(u.WeaponNotes)
out.NaviCutInPlayed = maps.Clone(u.NaviCutInPlayed)
@@ -50,40 +47,46 @@ func cloneUserState(u store.UserState) store.UserState {
out.ShopReplaceableLineup = maps.Clone(u.ShopReplaceableLineup)
out.Explore = u.Explore
out.ExploreScores = maps.Clone(u.ExploreScores)
out.Gacha = store.GachaState{
out.Gacha = GachaState{
RewardAvailable: u.Gacha.RewardAvailable,
TodaysCurrentDrawCount: u.Gacha.TodaysCurrentDrawCount,
DailyMaxCount: u.Gacha.DailyMaxCount,
LastRewardDrawDate: u.Gacha.LastRewardDrawDate,
ConvertedGachaMedal: store.ConvertedGachaMedalState{
ConvertedMedalPossession: append([]store.ConsumableItemState(nil), u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession...),
ConvertedGachaMedal: ConvertedGachaMedalState{
ConvertedMedalPossession: append([]ConsumableItemState(nil), u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession...),
ObtainPossession: cloneConsumableItemPtr(u.Gacha.ConvertedGachaMedal.ObtainPossession),
},
BannerStates: cloneBannerStates(u.Gacha.BannerStates),
}
out.Gifts = store.GiftState{
out.Gifts = GiftState{
NotReceived: cloneNotReceivedGifts(u.Gifts.NotReceived),
Received: cloneReceivedGifts(u.Gifts.Received),
}
out.Battle = u.Battle
out.SideStoryQuests = maps.Clone(u.SideStoryQuests)
out.QuestLimitContentStatus = maps.Clone(u.QuestLimitContentStatus)
out.BigHuntMaxScores = maps.Clone(u.BigHuntMaxScores)
out.BigHuntStatuses = maps.Clone(u.BigHuntStatuses)
out.BigHuntScheduleMaxScores = maps.Clone(u.BigHuntScheduleMaxScores)
out.BigHuntWeeklyMaxScores = maps.Clone(u.BigHuntWeeklyMaxScores)
out.BigHuntWeeklyStatuses = maps.Clone(u.BigHuntWeeklyStatuses)
out.BigHuntBattleBinary = append([]byte(nil), u.BigHuntBattleBinary...)
out.CharacterBoards = maps.Clone(u.CharacterBoards)
out.CharacterBoardAbilities = maps.Clone(u.CharacterBoardAbilities)
out.CharacterBoardStatusUps = maps.Clone(u.CharacterBoardStatusUps)
out.CostumeAwakenStatusUps = maps.Clone(u.CostumeAwakenStatusUps)
out.CostumeLotteryEffects = maps.Clone(u.CostumeLotteryEffects)
out.CostumeLotteryEffectPending = maps.Clone(u.CostumeLotteryEffectPending)
out.AutoSaleSettings = maps.Clone(u.AutoSaleSettings)
out.CharacterRebirths = maps.Clone(u.CharacterRebirths)
return out
}
func cloneGachaCatalogEntry(entry store.GachaCatalogEntry) store.GachaCatalogEntry {
out := entry
out.PricePhases = append([]store.GachaPricePhaseEntry(nil), entry.PricePhases...)
out.PromotionItems = append([]store.GachaPromotionItem(nil), entry.PromotionItems...)
return out
}
func cloneBannerStates(m map[int32]store.GachaBannerState) map[int32]store.GachaBannerState {
func cloneBannerStates(m map[int32]GachaBannerState) map[int32]GachaBannerState {
if m == nil {
return nil
}
out := make(map[int32]store.GachaBannerState, len(m))
out := make(map[int32]GachaBannerState, len(m))
for k, v := range m {
bs := v
bs.BoxDrewCounts = maps.Clone(v.BoxDrewCounts)
@@ -92,7 +95,7 @@ func cloneBannerStates(m map[int32]store.GachaBannerState) map[int32]store.Gacha
return out
}
func cloneConsumableItemPtr(item *store.ConsumableItemState) *store.ConsumableItemState {
func cloneConsumableItemPtr(item *ConsumableItemState) *ConsumableItemState {
if item == nil {
return nil
}
@@ -100,11 +103,11 @@ func cloneConsumableItemPtr(item *store.ConsumableItemState) *store.ConsumableIt
return &out
}
func cloneNotReceivedGifts(gifts []store.NotReceivedGiftState) []store.NotReceivedGiftState {
out := make([]store.NotReceivedGiftState, len(gifts))
func cloneNotReceivedGifts(gifts []NotReceivedGiftState) []NotReceivedGiftState {
out := make([]NotReceivedGiftState, len(gifts))
for i, gift := range gifts {
out[i] = store.NotReceivedGiftState{
GiftCommon: store.GiftCommonState{
out[i] = NotReceivedGiftState{
GiftCommon: GiftCommonState{
PossessionType: gift.GiftCommon.PossessionType,
PossessionId: gift.GiftCommon.PossessionId,
Count: gift.GiftCommon.Count,
@@ -119,6 +122,24 @@ func cloneNotReceivedGifts(gifts []store.NotReceivedGiftState) []store.NotReceiv
return out
}
func cloneReceivedGifts(gifts []ReceivedGiftState) []ReceivedGiftState {
out := make([]ReceivedGiftState, len(gifts))
for i, gift := range gifts {
out[i] = ReceivedGiftState{
GiftCommon: GiftCommonState{
PossessionType: gift.GiftCommon.PossessionType,
PossessionId: gift.GiftCommon.PossessionId,
Count: gift.GiftCommon.Count,
GrantDatetime: gift.GiftCommon.GrantDatetime,
DescriptionGiftTextId: gift.GiftCommon.DescriptionGiftTextId,
EquipmentData: append([]byte(nil), gift.GiftCommon.EquipmentData...),
},
ReceivedDatetime: gift.ReceivedDatetime,
}
}
return out
}
func cloneSliceMap[T any](m map[string][]T) map[string][]T {
if m == nil {
return nil
@@ -129,21 +150,3 @@ func cloneSliceMap[T any](m map[string][]T) map[string][]T {
}
return out
}
func cloneReceivedGifts(gifts []store.ReceivedGiftState) []store.ReceivedGiftState {
out := make([]store.ReceivedGiftState, len(gifts))
for i, gift := range gifts {
out[i] = store.ReceivedGiftState{
GiftCommon: store.GiftCommonState{
PossessionType: gift.GiftCommon.PossessionType,
PossessionId: gift.GiftCommon.PossessionId,
Count: gift.GiftCommon.Count,
GrantDatetime: gift.GiftCommon.GrantDatetime,
DescriptionGiftTextId: gift.GiftCommon.DescriptionGiftTextId,
EquipmentData: append([]byte(nil), gift.GiftCommon.EquipmentData...),
},
ReceivedDatetime: gift.ReceivedDatetime,
}
}
return out
}
+30 -21
View File
@@ -5,6 +5,8 @@ import (
"log"
"sort"
"github.com/google/uuid"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model"
)
@@ -139,7 +141,7 @@ func (g *PossessionGranter) GrantCostume(user *UserState, costumeId int32, nowMi
}
}
}
key := fmt.Sprintf("reward-costume-%d", costumeId)
key := uuid.New().String()
user.Costumes[key] = CostumeState{
UserCostumeUuid: key,
CostumeId: costumeId,
@@ -155,16 +157,7 @@ func (g *PossessionGranter) GrantCostume(user *UserState, costumeId int32, nowMi
}
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
key := fmt.Sprintf("reward-weapon-%d-%d", weaponId, nowMillis)
if _, exists := user.Weapons[key]; exists {
for i := 2; ; i++ {
candidate := fmt.Sprintf("%s-%d", key, i)
if _, exists := user.Weapons[candidate]; !exists {
key = candidate
break
}
}
}
key := uuid.New().String()
user.Weapons[key] = WeaponState{
UserWeaponUuid: key,
WeaponId: weaponId,
@@ -269,16 +262,29 @@ func EnsureDefaultDeck(user *UserState, nowMillis int64) {
return
}
costumeUuid := FirstSortedKey(user.Costumes)
weaponUuid := FirstSortedKey(user.Weapons)
companionUuid := FirstSortedKey(user.Companions)
const rionCostumeId = int32(10100)
const rionWeaponId = int32(101001)
dcUuid := "default-deck-character-0001"
var costumeUuid, weaponUuid string
for k, v := range user.Costumes {
if v.CostumeId == rionCostumeId {
costumeUuid = k
break
}
}
for k, v := range user.Weapons {
if v.WeaponId == rionWeaponId {
weaponUuid = k
break
}
}
dcUuid := uuid.New().String()
user.DeckCharacters[dcUuid] = DeckCharacterState{
UserDeckCharacterUuid: dcUuid,
UserCompanionUuid: "",
UserCostumeUuid: costumeUuid,
MainUserWeaponUuid: weaponUuid,
UserCompanionUuid: companionUuid,
Power: 100,
LatestVersion: nowMillis,
}
@@ -324,14 +330,17 @@ func ApplyDeckReplacement(user *UserState, deckType model.DeckType, userDeckNumb
deck.Power = 100
}
uuids := []*string{&deck.UserDeckCharacterUuid01, &deck.UserDeckCharacterUuid02, &deck.UserDeckCharacterUuid03}
for i, uuid := range uuids {
uuidPtrs := []*string{&deck.UserDeckCharacterUuid01, &deck.UserDeckCharacterUuid02, &deck.UserDeckCharacterUuid03}
for i, uuidPtr := range uuidPtrs {
if i >= len(slots) || slots[i].UserCostumeUuid == "" {
*uuid = ""
*uuidPtr = ""
continue
}
slot := slots[i]
dcUuid := fmt.Sprintf("deck-%d-%d-%d", deckType, userDeckNumber, i+1)
dcUuid := *uuidPtr
if dcUuid == "" {
dcUuid = uuid.New().String()
}
dc := user.DeckCharacters[dcUuid]
dc.UserDeckCharacterUuid = dcUuid
dc.UserCostumeUuid = slot.UserCostumeUuid
@@ -343,7 +352,7 @@ func ApplyDeckReplacement(user *UserState, deckType model.DeckType, userDeckNumb
user.DeckCharacters[dcUuid] = dc
user.DeckSubWeapons[dcUuid] = slot.SubWeaponUuids
user.DeckParts[dcUuid] = slot.PartsUuids
*uuid = dcUuid
*uuidPtr = dcUuid
}
deck.LatestVersion = nowMillis
-198
View File
@@ -1,198 +0,0 @@
package memory
import (
"fmt"
"strings"
"sync"
"time"
"lunar-tear/server/internal/store"
)
type Option func(*MemoryStore)
func WithSnapshotDir(dir string) Option {
return func(s *MemoryStore) {
s.snapshotDir = dir
}
}
func WithSceneId(sceneId int32) Option {
return func(s *MemoryStore) {
s.bootstrapSceneId = sceneId
}
}
func WithStarterItems(v bool) Option {
return func(s *MemoryStore) {
s.starterItems = v
}
}
type MemoryStore struct {
mu sync.RWMutex
clock store.Clock
bootstrapSceneId int32
snapshotDir string
starterItems bool
lastSnapshotSceneId int32
nextUserId int64
users map[int64]*store.UserState
userIdsByUuid map[string]int64
sessionToUserId map[string]int64
sessions map[string]store.SessionState
gachaCatalog map[int32]store.GachaCatalogEntry
}
var (
_ store.UserRepository = (*MemoryStore)(nil)
_ store.SessionRepository = (*MemoryStore)(nil)
_ store.GachaRepository = (*MemoryStore)(nil)
)
func New(clock store.Clock, options ...Option) *MemoryStore {
if clock == nil {
clock = time.Now
}
s := &MemoryStore{
clock: clock,
nextUserId: defaultUserId,
users: make(map[int64]*store.UserState),
userIdsByUuid: make(map[string]int64),
sessionToUserId: make(map[string]int64),
sessions: make(map[string]store.SessionState),
gachaCatalog: make(map[int32]store.GachaCatalogEntry),
}
for _, opt := range options {
opt(s)
}
return s
}
func (s *MemoryStore) EnsureUser(uuid string) (store.UserState, error) {
s.mu.Lock()
defer s.mu.Unlock()
return cloneUserState(*s.getOrCreateLocked(normalizeUUID(uuid))), nil
}
func (s *MemoryStore) CreateSession(uuid string, ttl time.Duration) (store.UserState, store.SessionState, error) {
s.mu.Lock()
defer s.mu.Unlock()
user := s.getOrCreateLocked(normalizeUUID(uuid))
now := s.clock()
session := store.SessionState{
SessionKey: fmt.Sprintf("session_%d_%d", user.UserId, now.UnixNano()),
UserId: user.UserId,
Uuid: user.Uuid,
ExpireAt: now.Add(ttl),
}
s.sessionToUserId[session.SessionKey] = user.UserId
s.sessions[session.SessionKey] = session
return cloneUserState(*user), session, nil
}
func (s *MemoryStore) ResolveUserId(sessionKey string) (int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
userId, ok := s.sessionToUserId[sessionKey]
if !ok {
return 0, store.ErrNotFound
}
return userId, nil
}
func (s *MemoryStore) SnapshotUser(userId int64) (store.UserState, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[userId]
if !ok {
return store.UserState{}, store.ErrNotFound
}
return cloneUserState(*user), nil
}
func (s *MemoryStore) UpdateUser(userId int64, mutate func(*store.UserState)) (store.UserState, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, ok := s.users[userId]
if !ok {
return store.UserState{}, store.ErrNotFound
}
mutate(user)
sceneId := user.MainQuest.CurrentQuestSceneId
if s.snapshotDir != "" && sceneId != 0 && sceneId != s.lastSnapshotSceneId {
saveSnapshot(user, s.snapshotDir)
s.lastSnapshotSceneId = sceneId
}
return cloneUserState(*user), nil
}
func (s *MemoryStore) DefaultUserId() (int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if _, ok := s.users[defaultUserId]; ok {
return defaultUserId, nil
}
if len(s.users) == 0 {
return defaultUserId, nil
}
var minUserId int64
for userId := range s.users {
if minUserId == 0 || userId < minUserId {
minUserId = userId
}
}
return minUserId, nil
}
func (s *MemoryStore) SnapshotCatalog() ([]store.GachaCatalogEntry, error) {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]store.GachaCatalogEntry, 0, len(s.gachaCatalog))
for _, entry := range s.gachaCatalog {
out = append(out, cloneGachaCatalogEntry(entry))
}
return out, nil
}
func (s *MemoryStore) ReplaceCatalog(entries []store.GachaCatalogEntry) error {
s.mu.Lock()
defer s.mu.Unlock()
s.gachaCatalog = make(map[int32]store.GachaCatalogEntry, len(entries))
for _, entry := range entries {
s.gachaCatalog[entry.GachaId] = cloneGachaCatalogEntry(entry)
}
return nil
}
func (s *MemoryStore) getOrCreateLocked(uuid string) *store.UserState {
if userId, ok := s.userIdsByUuid[uuid]; ok {
return s.users[userId]
}
userId := s.nextUserId
s.nextUserId++
user := seedUserState(userId, uuid, s.clock().UnixMilli(), s.bootstrapSceneId, s.snapshotDir, s.starterItems)
s.users[userId] = user
s.userIdsByUuid[uuid] = userId
return user
}
func normalizeUUID(uuid string) string {
uuid = strings.TrimSpace(uuid)
if uuid == "" {
return defaultUUID
}
return uuid
}
-222
View File
@@ -1,222 +0,0 @@
package memory
import (
"fmt"
"log"
"time"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
const (
defaultUUID = "default-user"
defaultUserId = int64(1001)
starterMissionId = int32(1)
starterMainQuestRouteId = int32(1)
starterMainQuestSeasonId = int32(1)
missionInProgress = int32(1)
giftUUIDPrefix = "default-gift"
defaultBirthYear = int32(2000)
defaultBirthMonth = int32(1)
defaultBackupToken = "mock-backup-token"
defaultChargeMoneyThisMonth = int64(0)
)
type starterItemDef struct {
Type model.PossessionType
Id int32
Qty int32
}
var defaultStarterItems = []starterItemDef{
{Type: model.PossessionTypeFreeGem, Id: 0, Qty: 300},
{Type: model.PossessionTypeConsumableItem, Id: 9001, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: model.ConsumableIdChapterTicket, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 5001, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 5002, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 5003, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 1009, Qty: 1000},
}
func seedUserState(userId int64, uuid string, nowMillis int64, sceneId int32, snapshotDir string, grantStarterItems bool) *store.UserState {
if sceneId != 0 && snapshotDir != "" {
user, err := loadSnapshot(snapshotDir, sceneId)
if err != nil {
log.Fatalf("[bootstrap] no snapshot for scene=%d: %v", sceneId, err)
}
log.Printf("[bootstrap] loaded snapshot for scene=%d", sceneId)
if grantStarterItems {
applyStarterItems(user)
}
return user
}
user := &store.UserState{
UserId: userId,
Uuid: uuid,
PlayerId: userId,
OsType: 2,
PlatformType: 2,
UserRestrictionType: 0,
RegisterDatetime: nowMillis,
GameStartDatetime: nowMillis,
LatestVersion: 0,
BirthYear: defaultBirthYear,
BirthMonth: defaultBirthMonth,
BackupToken: defaultBackupToken,
ChargeMoneyThisMonth: defaultChargeMoneyThisMonth,
Setting: store.UserSettingState{
IsNotifyPurchaseAlert: false,
LatestVersion: 0,
},
Status: store.UserStatusState{
Level: 1,
Exp: 0,
StaminaMilliValue: 50000,
StaminaUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Gem: store.UserGemState{
PaidGem: 10000,
FreeGem: 10000,
},
Profile: store.UserProfileState{
Name: "",
NameUpdateDatetime: 0,
Message: "",
MessageUpdateDatetime: nowMillis,
FavoriteCostumeId: 0,
FavoriteCostumeIdUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Login: store.UserLoginState{
TotalLoginCount: 1,
ContinualLoginCount: 1,
MaxContinualLoginCount: 1,
LastLoginDatetime: nowMillis,
LastComebackLoginDatetime: 0,
LatestVersion: 0,
},
LoginBonus: store.UserLoginBonusState{
LoginBonusId: 1,
CurrentPageNumber: 1,
CurrentStampNumber: 0,
LatestRewardReceiveDatetime: 0,
LatestVersion: 0,
},
Tutorials: map[int32]store.TutorialProgressState{
1: {TutorialType: 1},
},
Battle: store.BattleState{},
Gifts: store.GiftState{
NotReceived: []store.NotReceivedGiftState{
{
GiftCommon: store.GiftCommonState{
PossessionType: int32(model.PossessionTypeFreeGem),
PossessionId: 0,
Count: 300,
GrantDatetime: nowMillis,
},
ExpirationDatetime: nowMillis + int64((7*24*time.Hour)/time.Millisecond),
UserGiftUuid: fmt.Sprintf("%s-%d-1", giftUUIDPrefix, userId),
},
},
Received: []store.ReceivedGiftState{},
},
Gacha: store.GachaState{
ConvertedGachaMedal: store.ConvertedGachaMedalState{
ConvertedMedalPossession: []store.ConsumableItemState{},
},
BannerStates: make(map[int32]store.GachaBannerState),
},
MainQuest: store.MainQuestState{
CurrentMainQuestRouteId: starterMainQuestRouteId,
MainQuestSeasonId: starterMainQuestSeasonId,
},
Notifications: store.NotificationState{
GiftNotReceiveCount: 1,
},
Characters: make(map[int32]store.CharacterState),
Costumes: make(map[string]store.CostumeState),
Weapons: make(map[string]store.WeaponState),
Companions: make(map[string]store.CompanionState),
DeckCharacters: make(map[string]store.DeckCharacterState),
Decks: make(map[store.DeckKey]store.DeckState),
DeckSubWeapons: make(map[string][]string),
DeckParts: make(map[string][]string),
Quests: make(map[int32]store.UserQuestState),
QuestMissions: make(map[store.QuestMissionKey]store.UserQuestMissionState),
SideStoryQuests: make(map[int32]store.SideStoryQuestProgress),
QuestLimitContentStatus: make(map[int32]store.QuestLimitContentStatus),
BigHuntMaxScores: make(map[int32]store.BigHuntMaxScore),
BigHuntStatuses: make(map[int32]store.BigHuntStatus),
BigHuntScheduleMaxScores: make(map[store.BigHuntScheduleScoreKey]store.BigHuntScheduleMaxScore),
BigHuntWeeklyMaxScores: make(map[store.BigHuntWeeklyScoreKey]store.BigHuntWeeklyMaxScore),
BigHuntWeeklyStatuses: make(map[int64]store.BigHuntWeeklyStatus),
WeaponStories: make(map[int32]store.WeaponStoryState),
Missions: map[int32]store.UserMissionState{
starterMissionId: {
MissionId: starterMissionId,
StartDatetime: nowMillis,
MissionProgressStatusType: missionInProgress,
},
},
Gimmick: store.GimmickState{
Progress: make(map[store.GimmickKey]store.GimmickProgressState),
OrnamentProgress: make(map[store.GimmickOrnamentKey]store.GimmickOrnamentProgressState),
Sequences: make(map[store.GimmickSequenceKey]store.GimmickSequenceState),
Unlocks: make(map[store.GimmickKey]store.GimmickUnlockState),
},
CageOrnamentRewards: make(map[int32]store.CageOrnamentRewardState),
ConsumableItems: make(map[int32]int32),
Materials: make(map[int32]int32),
Thoughts: make(map[string]store.ThoughtState),
Parts: make(map[string]store.PartsState),
PartsGroupNotes: make(map[int32]store.PartsGroupNoteState),
PartsPresets: make(map[int32]store.PartsPresetState),
ImportantItems: make(map[int32]int32),
CostumeActiveSkills: make(map[string]store.CostumeActiveSkillState),
WeaponSkills: make(map[string][]store.WeaponSkillState),
WeaponAbilities: make(map[string][]store.WeaponAbilityState),
DeckTypeNotes: make(map[model.DeckType]store.DeckTypeNoteState),
WeaponNotes: make(map[int32]store.WeaponNoteState),
NaviCutInPlayed: make(map[int32]bool),
ViewedMovies: make(map[int32]int64),
ContentsStories: make(map[int32]int64),
DrawnOmikuji: make(map[int32]int64),
PremiumItems: make(map[int32]int64),
DokanConfirmed: make(map[int32]bool),
ShopItems: make(map[int32]store.UserShopItemState),
ShopReplaceableLineup: make(map[int32]store.UserShopReplaceableLineupState),
ExploreScores: make(map[int32]store.ExploreScoreState),
CharacterBoards: make(map[int32]store.CharacterBoardState),
CharacterBoardAbilities: make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState),
CharacterBoardStatusUps: make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState),
CostumeAwakenStatusUps: make(map[store.CostumeAwakenStatusKey]store.CostumeAwakenStatusUpState),
AutoSaleSettings: make(map[int32]store.AutoSaleSettingState),
CharacterRebirths: make(map[int32]store.CharacterRebirthState),
}
store.EnsureDefaultDeck(user, nowMillis)
if grantStarterItems {
applyStarterItems(user)
}
return user
}
func applyStarterItems(user *store.UserState) {
for _, item := range defaultStarterItems {
switch item.Type {
case model.PossessionTypeFreeGem:
user.Gem.FreeGem += item.Qty
case model.PossessionTypeConsumableItem:
user.ConsumableItems[item.Id] += item.Qty
case model.PossessionTypeMaterial:
user.Materials[item.Id] += item.Qty
}
}
}
-95
View File
@@ -1,95 +0,0 @@
package memory
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"lunar-tear/server/internal/store"
)
func snapshotPath(dir string, sceneId int32) string {
return filepath.Join(dir, fmt.Sprintf("scene_%d.json", sceneId))
}
func saveSnapshot(user *store.UserState, dir string) {
sceneId := user.MainQuest.CurrentQuestSceneId
if sceneId == 0 {
return
}
data, err := json.MarshalIndent(user, "", " ")
if err != nil {
log.Printf("[snapshot] marshal error for scene=%d: %v", sceneId, err)
return
}
path := snapshotPath(dir, sceneId)
if err := os.WriteFile(path, data, 0644); err != nil {
log.Printf("[snapshot] write error for scene=%d: %v", sceneId, err)
return
}
log.Printf("[snapshot] saved scene=%d (%d bytes)", sceneId, len(data))
}
// parseSceneId extracts the numeric scene ID from a filename of the form "scene_<id>.json".
// Returns (0, false) if the name does not match the expected format.
func parseSceneId(name string) (int32, bool) {
if !strings.HasPrefix(name, "scene_") || !strings.HasSuffix(name, ".json") {
return 0, false
}
raw := strings.TrimSuffix(strings.TrimPrefix(name, "scene_"), ".json")
id, err := strconv.ParseInt(raw, 10, 32)
if err != nil {
return 0, false
}
return int32(id), true
}
// LatestSnapshotSceneId scans dir for scene_*.json files and returns the scene ID
// of the most recently modified snapshot. Returns (0, false) if none are found.
func LatestSnapshotSceneId(dir string) (int32, bool) {
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
var latestId int32
var latestMod int64
for _, e := range entries {
if e.IsDir() {
continue
}
id, ok := parseSceneId(e.Name())
if !ok {
continue
}
info, err := e.Info()
if err != nil {
continue
}
if mt := info.ModTime().UnixNano(); mt > latestMod {
latestMod = mt
latestId = id
}
}
if latestId == 0 {
return 0, false
}
return latestId, true
}
func loadSnapshot(dir string, sceneId int32) (*store.UserState, error) {
path := snapshotPath(dir, sceneId)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read snapshot scene=%d: %w", sceneId, err)
}
var user store.UserState
if err := json.Unmarshal(data, &user); err != nil {
return nil, fmt.Errorf("unmarshal snapshot scene=%d: %w", sceneId, err)
}
user.EnsureMaps()
return &user, nil
}
+157
View File
@@ -0,0 +1,157 @@
package store
import (
"lunar-tear/server/internal/model"
)
const (
starterMissionId = int32(1)
starterMainQuestRouteId = int32(1)
starterMainQuestSeasonId = int32(1)
missionInProgress = int32(1)
defaultBirthYear = int32(2000)
defaultBirthMonth = int32(1)
defaultBackupToken = "mock-backup-token"
defaultChargeMoneyThisMonth = int64(0)
)
func SeedUserState(userId int64, uuid string, nowMillis int64) *UserState {
user := &UserState{
UserId: userId,
Uuid: uuid,
PlayerId: userId,
OsType: 2,
PlatformType: 2,
UserRestrictionType: 0,
RegisterDatetime: nowMillis,
GameStartDatetime: nowMillis,
LatestVersion: 0,
BirthYear: defaultBirthYear,
BirthMonth: defaultBirthMonth,
BackupToken: defaultBackupToken,
ChargeMoneyThisMonth: defaultChargeMoneyThisMonth,
Setting: UserSettingState{
IsNotifyPurchaseAlert: false,
LatestVersion: 0,
},
Status: UserStatusState{
Level: 1,
Exp: 0,
StaminaMilliValue: 50000,
StaminaUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Gem: UserGemState{
PaidGem: 0,
FreeGem: 0,
},
Profile: UserProfileState{
Name: "",
NameUpdateDatetime: 0,
Message: "",
MessageUpdateDatetime: nowMillis,
FavoriteCostumeId: 0,
FavoriteCostumeIdUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Login: UserLoginState{
TotalLoginCount: 1,
ContinualLoginCount: 1,
MaxContinualLoginCount: 1,
LastLoginDatetime: nowMillis,
LastComebackLoginDatetime: 0,
LatestVersion: 0,
},
LoginBonus: UserLoginBonusState{
LoginBonusId: 1,
CurrentPageNumber: 1,
CurrentStampNumber: 0,
LatestRewardReceiveDatetime: 0,
LatestVersion: 0,
},
Tutorials: map[int32]TutorialProgressState{
1: {TutorialType: 1},
},
Battle: BattleState{},
Gifts: GiftState{
NotReceived: []NotReceivedGiftState{},
Received: []ReceivedGiftState{},
},
Gacha: GachaState{
ConvertedGachaMedal: ConvertedGachaMedalState{
ConvertedMedalPossession: []ConsumableItemState{},
},
BannerStates: make(map[int32]GachaBannerState),
},
MainQuest: MainQuestState{
CurrentMainQuestRouteId: starterMainQuestRouteId,
MainQuestSeasonId: starterMainQuestSeasonId,
},
Notifications: NotificationState{
GiftNotReceiveCount: 1,
},
Characters: make(map[int32]CharacterState),
Costumes: make(map[string]CostumeState),
Weapons: make(map[string]WeaponState),
Companions: make(map[string]CompanionState),
DeckCharacters: make(map[string]DeckCharacterState),
Decks: make(map[DeckKey]DeckState),
DeckSubWeapons: make(map[string][]string),
DeckParts: make(map[string][]string),
Quests: make(map[int32]UserQuestState),
QuestMissions: make(map[QuestMissionKey]UserQuestMissionState),
SideStoryQuests: make(map[int32]SideStoryQuestProgress),
QuestLimitContentStatus: make(map[int32]QuestLimitContentStatus),
BigHuntMaxScores: make(map[int32]BigHuntMaxScore),
BigHuntStatuses: make(map[int32]BigHuntStatus),
BigHuntScheduleMaxScores: make(map[BigHuntScheduleScoreKey]BigHuntScheduleMaxScore),
BigHuntWeeklyMaxScores: make(map[BigHuntWeeklyScoreKey]BigHuntWeeklyMaxScore),
BigHuntWeeklyStatuses: make(map[int64]BigHuntWeeklyStatus),
WeaponStories: make(map[int32]WeaponStoryState),
Missions: map[int32]UserMissionState{
starterMissionId: {
MissionId: starterMissionId,
StartDatetime: nowMillis,
MissionProgressStatusType: missionInProgress,
},
},
Gimmick: GimmickState{
Progress: make(map[GimmickKey]GimmickProgressState),
OrnamentProgress: make(map[GimmickOrnamentKey]GimmickOrnamentProgressState),
Sequences: make(map[GimmickSequenceKey]GimmickSequenceState),
Unlocks: make(map[GimmickKey]GimmickUnlockState),
},
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
ConsumableItems: make(map[int32]int32),
Materials: make(map[int32]int32),
Thoughts: make(map[string]ThoughtState),
Parts: make(map[string]PartsState),
PartsGroupNotes: make(map[int32]PartsGroupNoteState),
PartsPresets: make(map[int32]PartsPresetState),
ImportantItems: make(map[int32]int32),
CostumeActiveSkills: make(map[string]CostumeActiveSkillState),
WeaponSkills: make(map[string][]WeaponSkillState),
WeaponAbilities: make(map[string][]WeaponAbilityState),
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
WeaponNotes: make(map[int32]WeaponNoteState),
NaviCutInPlayed: make(map[int32]bool),
ViewedMovies: make(map[int32]int64),
ContentsStories: make(map[int32]int64),
DrawnOmikuji: make(map[int32]int64),
PremiumItems: make(map[int32]int64),
DokanConfirmed: make(map[int32]bool),
ShopItems: make(map[int32]UserShopItemState),
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
ExploreScores: make(map[int32]ExploreScoreState),
CharacterBoards: make(map[int32]CharacterBoardState),
CharacterBoardAbilities: make(map[CharacterBoardAbilityKey]CharacterBoardAbilityState),
CharacterBoardStatusUps: make(map[CharacterBoardStatusUpKey]CharacterBoardStatusUpState),
CostumeAwakenStatusUps: make(map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState),
AutoSaleSettings: make(map[int32]AutoSaleSettingState),
CharacterRebirths: make(map[int32]CharacterRebirthState),
}
return user
}
+724
View File
@@ -0,0 +1,724 @@
package sqlite
import (
"database/sql"
"fmt"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (s *SQLiteStore) LoadUser(userId int64) (store.UserState, error) {
var u store.UserState
err := s.db.QueryRow(`SELECT user_id, uuid, player_id, os_type, platform_type, user_restriction_type,
register_datetime, game_start_datetime, latest_version, birth_year, birth_month,
backup_token, charge_money_this_month FROM users WHERE user_id = ?`, userId).Scan(
&u.UserId, &u.Uuid, &u.PlayerId, &u.OsType, &u.PlatformType, &u.UserRestrictionType,
&u.RegisterDatetime, &u.GameStartDatetime, &u.LatestVersion, &u.BirthYear, &u.BirthMonth,
&u.BackupToken, &u.ChargeMoneyThisMonth)
if err == sql.ErrNoRows {
return u, store.ErrNotFound
}
if err != nil {
return u, fmt.Errorf("load users: %w", err)
}
initMaps(&u)
load1to1(s.db, userId, &u)
loadMapTables(s.db, userId, &u)
return u, nil
}
func initMaps(u *store.UserState) {
u.Tutorials = make(map[int32]store.TutorialProgressState)
u.Characters = make(map[int32]store.CharacterState)
u.Costumes = make(map[string]store.CostumeState)
u.Weapons = make(map[string]store.WeaponState)
u.Companions = make(map[string]store.CompanionState)
u.Thoughts = make(map[string]store.ThoughtState)
u.DeckCharacters = make(map[string]store.DeckCharacterState)
u.Decks = make(map[store.DeckKey]store.DeckState)
u.DeckSubWeapons = make(map[string][]string)
u.DeckParts = make(map[string][]string)
u.Quests = make(map[int32]store.UserQuestState)
u.QuestMissions = make(map[store.QuestMissionKey]store.UserQuestMissionState)
u.Missions = make(map[int32]store.UserMissionState)
u.WeaponStories = make(map[int32]store.WeaponStoryState)
u.WeaponNotes = make(map[int32]store.WeaponNoteState)
u.WeaponSkills = make(map[string][]store.WeaponSkillState)
u.WeaponAbilities = make(map[string][]store.WeaponAbilityState)
u.WeaponAwakens = make(map[string]store.WeaponAwakenState)
u.CostumeActiveSkills = make(map[string]store.CostumeActiveSkillState)
u.CostumeAwakenStatusUps = make(map[store.CostumeAwakenStatusKey]store.CostumeAwakenStatusUpState)
u.CostumeLotteryEffects = make(map[store.CostumeLotteryEffectKey]store.CostumeLotteryEffectState)
u.CostumeLotteryEffectPending = make(map[string]store.CostumeLotteryEffectPendingState)
u.Parts = make(map[string]store.PartsState)
u.PartsGroupNotes = make(map[int32]store.PartsGroupNoteState)
u.PartsPresets = make(map[int32]store.PartsPresetState)
u.DeckTypeNotes = make(map[model.DeckType]store.DeckTypeNoteState)
u.ConsumableItems = make(map[int32]int32)
u.Materials = make(map[int32]int32)
u.ImportantItems = make(map[int32]int32)
u.PremiumItems = make(map[int32]int64)
u.NaviCutInPlayed = make(map[int32]bool)
u.ViewedMovies = make(map[int32]int64)
u.ContentsStories = make(map[int32]int64)
u.DrawnOmikuji = make(map[int32]int64)
u.DokanConfirmed = make(map[int32]bool)
u.ShopItems = make(map[int32]store.UserShopItemState)
u.ShopReplaceableLineup = make(map[int32]store.UserShopReplaceableLineupState)
u.ExploreScores = make(map[int32]store.ExploreScoreState)
u.CageOrnamentRewards = make(map[int32]store.CageOrnamentRewardState)
u.CharacterBoards = make(map[int32]store.CharacterBoardState)
u.CharacterBoardAbilities = make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState)
u.CharacterBoardStatusUps = make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState)
u.CharacterRebirths = make(map[int32]store.CharacterRebirthState)
u.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState)
u.SideStoryQuests = make(map[int32]store.SideStoryQuestProgress)
u.QuestLimitContentStatus = make(map[int32]store.QuestLimitContentStatus)
u.BigHuntMaxScores = make(map[int32]store.BigHuntMaxScore)
u.BigHuntStatuses = make(map[int32]store.BigHuntStatus)
u.BigHuntScheduleMaxScores = make(map[store.BigHuntScheduleScoreKey]store.BigHuntScheduleMaxScore)
u.BigHuntWeeklyMaxScores = make(map[store.BigHuntWeeklyScoreKey]store.BigHuntWeeklyMaxScore)
u.BigHuntWeeklyStatuses = make(map[int64]store.BigHuntWeeklyStatus)
u.Gacha.BannerStates = make(map[int32]store.GachaBannerState)
u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession = []store.ConsumableItemState{}
u.Gifts.NotReceived = []store.NotReceivedGiftState{}
u.Gifts.Received = []store.ReceivedGiftState{}
u.Gimmick.Progress = make(map[store.GimmickKey]store.GimmickProgressState)
u.Gimmick.OrnamentProgress = make(map[store.GimmickOrnamentKey]store.GimmickOrnamentProgressState)
u.Gimmick.Sequences = make(map[store.GimmickSequenceKey]store.GimmickSequenceState)
u.Gimmick.Unlocks = make(map[store.GimmickKey]store.GimmickUnlockState)
}
func load1to1(db *sql.DB, uid int64, u *store.UserState) {
var b int
_ = db.QueryRow(`SELECT is_notify_purchase_alert, latest_version FROM user_setting WHERE user_id=?`, uid).
Scan(&b, &u.Setting.LatestVersion)
u.Setting.IsNotifyPurchaseAlert = b != 0
_ = db.QueryRow(`SELECT level, exp, stamina_milli_value, stamina_update_datetime, latest_version FROM user_status WHERE user_id=?`, uid).
Scan(&u.Status.Level, &u.Status.Exp, &u.Status.StaminaMilliValue, &u.Status.StaminaUpdateDatetime, &u.Status.LatestVersion)
_ = db.QueryRow(`SELECT paid_gem, free_gem FROM user_gem WHERE user_id=?`, uid).
Scan(&u.Gem.PaidGem, &u.Gem.FreeGem)
_ = db.QueryRow(`SELECT name, name_update_datetime, message, message_update_datetime, favorite_costume_id,
favorite_costume_id_update_datetime, latest_version FROM user_profile WHERE user_id=?`, uid).
Scan(&u.Profile.Name, &u.Profile.NameUpdateDatetime, &u.Profile.Message, &u.Profile.MessageUpdateDatetime,
&u.Profile.FavoriteCostumeId, &u.Profile.FavoriteCostumeIdUpdateDatetime, &u.Profile.LatestVersion)
_ = db.QueryRow(`SELECT total_login_count, continual_login_count, max_continual_login_count,
last_login_datetime, last_comeback_login_datetime, latest_version FROM user_login WHERE user_id=?`, uid).
Scan(&u.Login.TotalLoginCount, &u.Login.ContinualLoginCount, &u.Login.MaxContinualLoginCount,
&u.Login.LastLoginDatetime, &u.Login.LastComebackLoginDatetime, &u.Login.LatestVersion)
_ = db.QueryRow(`SELECT login_bonus_id, current_page_number, current_stamp_number,
latest_reward_receive_datetime, latest_version FROM user_login_bonus WHERE user_id=?`, uid).
Scan(&u.LoginBonus.LoginBonusId, &u.LoginBonus.CurrentPageNumber, &u.LoginBonus.CurrentStampNumber,
&u.LoginBonus.LatestRewardReceiveDatetime, &u.LoginBonus.LatestVersion)
_ = db.QueryRow(`SELECT current_quest_flow_type, current_main_quest_route_id, current_quest_scene_id,
head_quest_scene_id, is_reached_last_quest_scene, progress_quest_scene_id, progress_head_quest_scene_id,
progress_quest_flow_type, main_quest_season_id, latest_version, saved_current_quest_scene_id,
saved_head_quest_scene_id, replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id
FROM user_main_quest WHERE user_id=?`, uid).
Scan(&u.MainQuest.CurrentQuestFlowType, &u.MainQuest.CurrentMainQuestRouteId, &u.MainQuest.CurrentQuestSceneId,
&u.MainQuest.HeadQuestSceneId, &b, &u.MainQuest.ProgressQuestSceneId, &u.MainQuest.ProgressHeadQuestSceneId,
&u.MainQuest.ProgressQuestFlowType, &u.MainQuest.MainQuestSeasonId, &u.MainQuest.LatestVersion,
&u.MainQuest.SavedCurrentQuestSceneId, &u.MainQuest.SavedHeadQuestSceneId,
&u.MainQuest.ReplayFlowCurrentQuestSceneId, &u.MainQuest.ReplayFlowHeadQuestSceneId)
u.MainQuest.IsReachedLastQuestScene = b != 0
_ = db.QueryRow(`SELECT current_event_quest_chapter_id, current_quest_id, current_quest_scene_id,
head_quest_scene_id, latest_version FROM user_event_quest WHERE user_id=?`, uid).
Scan(&u.EventQuest.CurrentEventQuestChapterId, &u.EventQuest.CurrentQuestId,
&u.EventQuest.CurrentQuestSceneId, &u.EventQuest.HeadQuestSceneId, &u.EventQuest.LatestVersion)
_ = db.QueryRow(`SELECT current_quest_id, current_quest_scene_id, head_quest_scene_id, latest_version
FROM user_extra_quest WHERE user_id=?`, uid).
Scan(&u.ExtraQuest.CurrentQuestId, &u.ExtraQuest.CurrentQuestSceneId,
&u.ExtraQuest.HeadQuestSceneId, &u.ExtraQuest.LatestVersion)
_ = db.QueryRow(`SELECT current_side_story_quest_id, current_side_story_quest_scene_id, latest_version
FROM user_side_story_active WHERE user_id=?`, uid).
Scan(&u.SideStoryActiveProgress.CurrentSideStoryQuestId,
&u.SideStoryActiveProgress.CurrentSideStoryQuestSceneId, &u.SideStoryActiveProgress.LatestVersion)
var isDryRun int
_ = db.QueryRow(`SELECT current_big_hunt_boss_quest_id, current_big_hunt_quest_id, current_quest_scene_id,
is_dry_run, latest_version, deck_type, user_triple_deck_number, boss_knock_down_count,
max_combo_count, total_damage, deck_number, battle_binary
FROM user_big_hunt_state WHERE user_id=?`, uid).
Scan(&u.BigHuntProgress.CurrentBigHuntBossQuestId, &u.BigHuntProgress.CurrentBigHuntQuestId,
&u.BigHuntProgress.CurrentQuestSceneId, &isDryRun, &u.BigHuntProgress.LatestVersion,
&u.BigHuntBattleDetail.DeckType, &u.BigHuntBattleDetail.UserTripleDeckNumber,
&u.BigHuntBattleDetail.BossKnockDownCount, &u.BigHuntBattleDetail.MaxComboCount,
&u.BigHuntBattleDetail.TotalDamage, &u.BigHuntDeckNumber, &u.BigHuntBattleBinary)
u.BigHuntProgress.IsDryRun = isDryRun != 0
var isActive, isUnread int
_ = db.QueryRow(`SELECT is_active, start_count, finish_count, last_started_at, last_finished_at,
last_user_party_count, last_npc_party_count, last_battle_binary_size, last_elapsed_frame_count
FROM user_battle WHERE user_id=?`, uid).
Scan(&isActive, &u.Battle.StartCount, &u.Battle.FinishCount, &u.Battle.LastStartedAt,
&u.Battle.LastFinishedAt, &u.Battle.LastUserPartyCount, &u.Battle.LastNpcPartyCount,
&u.Battle.LastBattleBinarySize, &u.Battle.LastElapsedFrameCount)
u.Battle.IsActive = isActive != 0
_ = db.QueryRow(`SELECT gift_not_receive_count, friend_request_receive_count, is_exist_unread_information
FROM user_notification WHERE user_id=?`, uid).
Scan(&u.Notifications.GiftNotReceiveCount, &u.Notifications.FriendRequestReceiveCount, &isUnread)
u.Notifications.IsExistUnreadInformation = isUnread != 0
var isCP int
_ = db.QueryRow(`SELECT is_current_progress, drop_item_start_datetime, current_drop_item_count, latest_version
FROM user_portal_cage WHERE user_id=?`, uid).
Scan(&isCP, &u.PortalCageStatus.DropItemStartDatetime, &u.PortalCageStatus.CurrentDropItemCount,
&u.PortalCageStatus.LatestVersion)
u.PortalCageStatus.IsCurrentProgress = isCP != 0
_ = db.QueryRow(`SELECT start_datetime, open_minutes, daily_opened_count, latest_version
FROM user_guerrilla_free_open WHERE user_id=?`, uid).
Scan(&u.GuerrillaFreeOpen.StartDatetime, &u.GuerrillaFreeOpen.OpenMinutes,
&u.GuerrillaFreeOpen.DailyOpenedCount, &u.GuerrillaFreeOpen.LatestVersion)
var isTicket int
_ = db.QueryRow(`SELECT is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version
FROM user_explore WHERE user_id=?`, uid).
Scan(&isTicket, &u.Explore.PlayingExploreId, &u.Explore.LatestPlayDatetime, &u.Explore.LatestVersion)
u.Explore.IsUseExploreTicket = isTicket != 0
_ = db.QueryRow(`SELECT lineup_update_count, latest_lineup_update_datetime, latest_version
FROM user_shop_replaceable WHERE user_id=?`, uid).
Scan(&u.ShopReplaceable.LineupUpdateCount, &u.ShopReplaceable.LatestLineupUpdateDatetime,
&u.ShopReplaceable.LatestVersion)
var rewardAvail int
var obtainItemId, obtainCount sql.NullInt64
_ = db.QueryRow(`SELECT reward_available, todays_current_draw_count, daily_max_count,
last_reward_draw_date, obtain_consumable_item_id, obtain_count
FROM user_gacha WHERE user_id=?`, uid).
Scan(&rewardAvail, &u.Gacha.TodaysCurrentDrawCount, &u.Gacha.DailyMaxCount,
&u.Gacha.LastRewardDrawDate, &obtainItemId, &obtainCount)
u.Gacha.RewardAvailable = rewardAvail != 0
if obtainItemId.Valid {
u.Gacha.ConvertedGachaMedal.ObtainPossession = &store.ConsumableItemState{
ConsumableItemId: int32(obtainItemId.Int64),
Count: int32(obtainCount.Int64),
}
}
}
func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
queryRows(db, `SELECT character_id, level, exp, latest_version FROM user_characters WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterState
rows.Scan(&v.CharacterId, &v.Level, &v.Exp, &v.LatestVersion)
u.Characters[v.CharacterId] = v
})
queryRows(db, `SELECT user_costume_uuid, costume_id, limit_break_count, level, exp,
headup_display_view_id, acquisition_datetime, awaken_count,
costume_lottery_effect_unlocked_slot_count, latest_version
FROM user_costumes WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CostumeState
rows.Scan(&v.UserCostumeUuid, &v.CostumeId, &v.LimitBreakCount, &v.Level, &v.Exp,
&v.HeadupDisplayViewId, &v.AcquisitionDatetime, &v.AwakenCount,
&v.CostumeLotteryEffectUnlockedSlotCount, &v.LatestVersion)
u.Costumes[v.UserCostumeUuid] = v
})
queryRows(db, `SELECT user_weapon_uuid, weapon_id, level, exp, limit_break_count,
is_protected, acquisition_datetime, latest_version FROM user_weapons WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponState
var prot int
rows.Scan(&v.UserWeaponUuid, &v.WeaponId, &v.Level, &v.Exp, &v.LimitBreakCount,
&prot, &v.AcquisitionDatetime, &v.LatestVersion)
v.IsProtected = prot != 0
u.Weapons[v.UserWeaponUuid] = v
})
queryRows(db, `SELECT user_companion_uuid, companion_id, headup_display_view_id, level,
acquisition_datetime, latest_version FROM user_companions WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CompanionState
rows.Scan(&v.UserCompanionUuid, &v.CompanionId, &v.HeadupDisplayViewId, &v.Level,
&v.AcquisitionDatetime, &v.LatestVersion)
u.Companions[v.UserCompanionUuid] = v
})
queryRows(db, `SELECT user_thought_uuid, thought_id, acquisition_datetime, latest_version
FROM user_thoughts WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.ThoughtState
rows.Scan(&v.UserThoughtUuid, &v.ThoughtId, &v.AcquisitionDatetime, &v.LatestVersion)
u.Thoughts[v.UserThoughtUuid] = v
})
queryRows(db, `SELECT user_deck_character_uuid, user_costume_uuid, main_user_weapon_uuid,
user_companion_uuid, power, user_thought_uuid, dressup_costume_id, latest_version
FROM user_deck_characters WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.DeckCharacterState
rows.Scan(&v.UserDeckCharacterUuid, &v.UserCostumeUuid, &v.MainUserWeaponUuid,
&v.UserCompanionUuid, &v.Power, &v.UserThoughtUuid, &v.DressupCostumeId, &v.LatestVersion)
u.DeckCharacters[v.UserDeckCharacterUuid] = v
})
queryRows(db, `SELECT deck_type, user_deck_number, user_deck_character_uuid01, user_deck_character_uuid02,
user_deck_character_uuid03, name, power, latest_version FROM user_decks WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.DeckState
var dt int32
rows.Scan(&dt, &v.UserDeckNumber, &v.UserDeckCharacterUuid01, &v.UserDeckCharacterUuid02,
&v.UserDeckCharacterUuid03, &v.Name, &v.Power, &v.LatestVersion)
v.DeckType = model.DeckType(dt)
u.Decks[store.DeckKey{DeckType: v.DeckType, UserDeckNumber: v.UserDeckNumber}] = v
})
queryRows(db, `SELECT user_deck_character_uuid, ordinal, user_weapon_uuid
FROM user_deck_sub_weapons WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid,
func(rows *sql.Rows) {
var key, val string
var ord int
rows.Scan(&key, &ord, &val)
u.DeckSubWeapons[key] = append(u.DeckSubWeapons[key], val)
})
queryRows(db, `SELECT user_deck_character_uuid, ordinal, user_parts_uuid
FROM user_deck_parts WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid,
func(rows *sql.Rows) {
var key, val string
var ord int
rows.Scan(&key, &ord, &val)
u.DeckParts[key] = append(u.DeckParts[key], val)
})
queryRows(db, `SELECT quest_id, quest_state_type, is_battle_only, user_deck_number, latest_start_datetime,
clear_count, daily_clear_count, last_clear_datetime, shortest_clear_frames, is_reward_granted, latest_version
FROM user_quests WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserQuestState
var bo, rg int
rows.Scan(&v.QuestId, &v.QuestStateType, &bo, &v.UserDeckNumber, &v.LatestStartDatetime,
&v.ClearCount, &v.DailyClearCount, &v.LastClearDatetime, &v.ShortestClearFrames, &rg, &v.LatestVersion)
v.IsBattleOnly = bo != 0
v.IsRewardGranted = rg != 0
u.Quests[v.QuestId] = v
})
queryRows(db, `SELECT quest_id, quest_mission_id, progress_value, is_clear, latest_clear_datetime, latest_version
FROM user_quest_missions WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserQuestMissionState
var ic int
rows.Scan(&v.QuestId, &v.QuestMissionId, &v.ProgressValue, &ic, &v.LatestClearDatetime, &v.LatestVersion)
v.IsClear = ic != 0
u.QuestMissions[store.QuestMissionKey{QuestId: v.QuestId, QuestMissionId: v.QuestMissionId}] = v
})
queryRows(db, `SELECT mission_id, start_datetime, progress_value, mission_progress_status_type,
clear_datetime, latest_version FROM user_missions WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserMissionState
rows.Scan(&v.MissionId, &v.StartDatetime, &v.ProgressValue, &v.MissionProgressStatusType,
&v.ClearDatetime, &v.LatestVersion)
u.Missions[v.MissionId] = v
})
queryRows(db, `SELECT tutorial_type, progress_phase, choice_id, latest_version
FROM user_tutorials WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.TutorialProgressState
rows.Scan(&v.TutorialType, &v.ProgressPhase, &v.ChoiceId, &v.LatestVersion)
u.Tutorials[v.TutorialType] = v
})
queryRows(db, `SELECT side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version
FROM user_side_story_quests WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id, head, st int32
var lv int64
rows.Scan(&id, &head, &st, &lv)
u.SideStoryQuests[id] = store.SideStoryQuestProgress{
HeadSideStoryQuestSceneId: head, SideStoryQuestStateType: model.SideStoryQuestStateType(st), LatestVersion: lv,
}
})
queryRows(db, `SELECT limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version
FROM user_quest_limit_content_status WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id int32
var v store.QuestLimitContentStatus
rows.Scan(&id, &v.LimitContentQuestStatusType, &v.EventQuestChapterId, &v.LatestVersion)
u.QuestLimitContentStatus[id] = v
})
queryRows(db, `SELECT weapon_id, released_max_story_index, latest_version FROM user_weapon_stories WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponStoryState
rows.Scan(&v.WeaponId, &v.ReleasedMaxStoryIndex, &v.LatestVersion)
u.WeaponStories[v.WeaponId] = v
})
queryRows(db, `SELECT weapon_id, max_level, max_limit_break_count, first_acquisition_datetime, latest_version
FROM user_weapon_notes WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.WeaponNoteState
rows.Scan(&v.WeaponId, &v.MaxLevel, &v.MaxLimitBreakCount, &v.FirstAcquisitionDatetime, &v.LatestVersion)
u.WeaponNotes[v.WeaponId] = v
})
queryRows(db, `SELECT user_weapon_uuid, slot_number, level FROM user_weapon_skills WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponSkillState
rows.Scan(&v.UserWeaponUuid, &v.SlotNumber, &v.Level)
u.WeaponSkills[v.UserWeaponUuid] = append(u.WeaponSkills[v.UserWeaponUuid], v)
})
queryRows(db, `SELECT user_weapon_uuid, slot_number, level FROM user_weapon_abilities WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponAbilityState
rows.Scan(&v.UserWeaponUuid, &v.SlotNumber, &v.Level)
u.WeaponAbilities[v.UserWeaponUuid] = append(u.WeaponAbilities[v.UserWeaponUuid], v)
})
queryRows(db, `SELECT user_weapon_uuid, latest_version FROM user_weapon_awakens WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponAwakenState
rows.Scan(&v.UserWeaponUuid, &v.LatestVersion)
u.WeaponAwakens[v.UserWeaponUuid] = v
})
queryRows(db, `SELECT user_costume_uuid, level, acquisition_datetime, latest_version
FROM user_costume_active_skills WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CostumeActiveSkillState
rows.Scan(&v.UserCostumeUuid, &v.Level, &v.AcquisitionDatetime, &v.LatestVersion)
u.CostumeActiveSkills[v.UserCostumeUuid] = v
})
queryRows(db, `SELECT user_costume_uuid, status_calculation_type, hp, attack, vitality, agility,
critical_ratio, critical_attack, latest_version FROM user_costume_awaken_status_ups WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CostumeAwakenStatusUpState
var sct int32
rows.Scan(&v.UserCostumeUuid, &sct, &v.Hp, &v.Attack, &v.Vitality, &v.Agility,
&v.CriticalRatio, &v.CriticalAttack, &v.LatestVersion)
v.StatusCalculationType = model.StatusCalculationType(sct)
u.CostumeAwakenStatusUps[store.CostumeAwakenStatusKey{
UserCostumeUuid: v.UserCostumeUuid, StatusCalculationType: v.StatusCalculationType,
}] = v
})
queryRows(db, `SELECT user_costume_uuid, slot_number, odds_number, latest_version
FROM user_costume_lottery_effects WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CostumeLotteryEffectState
rows.Scan(&v.UserCostumeUuid, &v.SlotNumber, &v.OddsNumber, &v.LatestVersion)
u.CostumeLotteryEffects[store.CostumeLotteryEffectKey{
UserCostumeUuid: v.UserCostumeUuid, SlotNumber: v.SlotNumber,
}] = v
})
queryRows(db, `SELECT user_costume_uuid, slot_number, odds_number, latest_version
FROM user_costume_lottery_effect_pending WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CostumeLotteryEffectPendingState
rows.Scan(&v.UserCostumeUuid, &v.SlotNumber, &v.OddsNumber, &v.LatestVersion)
u.CostumeLotteryEffectPending[v.UserCostumeUuid] = v
})
queryRows(db, `SELECT user_parts_uuid, parts_id, level, parts_status_main_id, is_protected,
acquisition_datetime, latest_version FROM user_parts WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.PartsState
var prot int
rows.Scan(&v.UserPartsUuid, &v.PartsId, &v.Level, &v.PartsStatusMainId, &prot,
&v.AcquisitionDatetime, &v.LatestVersion)
v.IsProtected = prot != 0
u.Parts[v.UserPartsUuid] = v
})
queryRows(db, `SELECT parts_group_id, first_acquisition_datetime, latest_version
FROM user_parts_group_notes WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.PartsGroupNoteState
rows.Scan(&v.PartsGroupId, &v.FirstAcquisitionDatetime, &v.LatestVersion)
u.PartsGroupNotes[v.PartsGroupId] = v
})
queryRows(db, `SELECT user_parts_preset_number, user_parts_uuid01, user_parts_uuid02, user_parts_uuid03,
name, user_parts_preset_tag_number, latest_version FROM user_parts_presets WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.PartsPresetState
rows.Scan(&v.UserPartsPresetNumber, &v.UserPartsUuid01, &v.UserPartsUuid02, &v.UserPartsUuid03,
&v.Name, &v.UserPartsPresetTagNumber, &v.LatestVersion)
u.PartsPresets[v.UserPartsPresetNumber] = v
})
queryRows(db, `SELECT deck_type, max_deck_power, latest_version FROM user_deck_type_notes WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var dt int32
var v store.DeckTypeNoteState
rows.Scan(&dt, &v.MaxDeckPower, &v.LatestVersion)
v.DeckType = model.DeckType(dt)
u.DeckTypeNotes[v.DeckType] = v
})
loadSimpleMap(db, uid, `SELECT consumable_item_id, count FROM user_consumable_items WHERE user_id=?`, u.ConsumableItems)
loadSimpleMap(db, uid, `SELECT material_id, count FROM user_materials WHERE user_id=?`, u.Materials)
loadSimpleMap(db, uid, `SELECT important_item_id, count FROM user_important_items WHERE user_id=?`, u.ImportantItems)
queryRows(db, `SELECT premium_item_id, count FROM user_premium_items WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var k int32
var v int64
rows.Scan(&k, &v)
u.PremiumItems[k] = v
})
queryRows(db, `SELECT explore_id, max_score, max_score_update_datetime, latest_version
FROM user_explore_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.ExploreScoreState
rows.Scan(&v.ExploreId, &v.MaxScore, &v.MaxScoreUpdateDatetime, &v.LatestVersion)
u.ExploreScores[v.ExploreId] = v
})
queryRows(db, `SELECT possession_auto_sale_item_type, possession_auto_sale_item_value
FROM user_auto_sale_settings WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.AutoSaleSettingState
rows.Scan(&v.PossessionAutoSaleItemType, &v.PossessionAutoSaleItemValue)
u.AutoSaleSettings[v.PossessionAutoSaleItemType] = v
})
queryRows(db, `SELECT navi_cutin_id FROM user_navi_cutin_played WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var id int32
rows.Scan(&id)
u.NaviCutInPlayed[id] = true
})
loadTimestampMap(db, uid, `SELECT movie_id, timestamp FROM user_viewed_movies WHERE user_id=?`, u.ViewedMovies)
loadTimestampMap(db, uid, `SELECT contents_story_id, timestamp FROM user_contents_stories WHERE user_id=?`, u.ContentsStories)
loadTimestampMap(db, uid, `SELECT omikuji_id, timestamp FROM user_drawn_omikuji WHERE user_id=?`, u.DrawnOmikuji)
queryRows(db, `SELECT dokan_id FROM user_dokan_confirmed WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var id int32
rows.Scan(&id)
u.DokanConfirmed[id] = true
})
// Gifts
queryRows(db, `SELECT user_gift_uuid, is_received, possession_type, possession_id, count, grant_datetime,
description_gift_text_id, equipment_data, expiration_datetime, received_datetime
FROM user_gifts WHERE user_id=?`, uid, func(rows *sql.Rows) {
var uuid string
var isRecv int
var gc store.GiftCommonState
var expDt, recvDt sql.NullInt64
var equipData []byte
rows.Scan(&uuid, &isRecv, &gc.PossessionType, &gc.PossessionId, &gc.Count, &gc.GrantDatetime,
&gc.DescriptionGiftTextId, &equipData, &expDt, &recvDt)
gc.EquipmentData = equipData
if isRecv == 0 {
u.Gifts.NotReceived = append(u.Gifts.NotReceived, store.NotReceivedGiftState{
GiftCommon: gc, ExpirationDatetime: expDt.Int64, UserGiftUuid: uuid,
})
} else {
u.Gifts.Received = append(u.Gifts.Received, store.ReceivedGiftState{
GiftCommon: gc, ReceivedDatetime: recvDt.Int64,
})
}
})
// Gacha converted medals
queryRows(db, `SELECT consumable_item_id, count FROM user_gacha_converted_medals WHERE user_id=? ORDER BY ordinal`, uid,
func(rows *sql.Rows) {
var v store.ConsumableItemState
rows.Scan(&v.ConsumableItemId, &v.Count)
u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession = append(u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession, v)
})
// Gacha banners
queryRows(db, `SELECT gacha_id, medal_count, step_number, loop_count, draw_count, box_number
FROM user_gacha_banners WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.GachaBannerState
rows.Scan(&v.GachaId, &v.MedalCount, &v.StepNumber, &v.LoopCount, &v.DrawCount, &v.BoxNumber)
v.BoxDrewCounts = make(map[int32]int32)
u.Gacha.BannerStates[v.GachaId] = v
})
queryRows(db, `SELECT gacha_id, box_item_id, count FROM user_gacha_banner_box_drew_counts WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var gachaId, boxItemId, count int32
rows.Scan(&gachaId, &boxItemId, &count)
if bs, ok := u.Gacha.BannerStates[gachaId]; ok {
bs.BoxDrewCounts[boxItemId] = count
u.Gacha.BannerStates[gachaId] = bs
}
})
// Character boards
queryRows(db, `SELECT character_board_id, panel_release_bit1, panel_release_bit2, panel_release_bit3,
panel_release_bit4, latest_version FROM user_character_boards WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterBoardState
rows.Scan(&v.CharacterBoardId, &v.PanelReleaseBit1, &v.PanelReleaseBit2,
&v.PanelReleaseBit3, &v.PanelReleaseBit4, &v.LatestVersion)
u.CharacterBoards[v.CharacterBoardId] = v
})
queryRows(db, `SELECT character_id, ability_id, level, latest_version
FROM user_character_board_abilities WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CharacterBoardAbilityState
rows.Scan(&v.CharacterId, &v.AbilityId, &v.Level, &v.LatestVersion)
u.CharacterBoardAbilities[store.CharacterBoardAbilityKey{CharacterId: v.CharacterId, AbilityId: v.AbilityId}] = v
})
queryRows(db, `SELECT character_id, status_calculation_type, hp, attack, vitality, agility,
critical_ratio, critical_attack, latest_version FROM user_character_board_status_ups WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterBoardStatusUpState
rows.Scan(&v.CharacterId, &v.StatusCalculationType, &v.Hp, &v.Attack, &v.Vitality, &v.Agility,
&v.CriticalRatio, &v.CriticalAttack, &v.LatestVersion)
u.CharacterBoardStatusUps[store.CharacterBoardStatusUpKey{
CharacterId: v.CharacterId, StatusCalculationType: v.StatusCalculationType,
}] = v
})
queryRows(db, `SELECT character_id, rebirth_count, latest_version FROM user_character_rebirths WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterRebirthState
rows.Scan(&v.CharacterId, &v.RebirthCount, &v.LatestVersion)
u.CharacterRebirths[v.CharacterId] = v
})
queryRows(db, `SELECT cage_ornament_id, acquisition_datetime, latest_version
FROM user_cage_ornament_rewards WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CageOrnamentRewardState
rows.Scan(&v.CageOrnamentId, &v.AcquisitionDatetime, &v.LatestVersion)
u.CageOrnamentRewards[v.CageOrnamentId] = v
})
queryRows(db, `SELECT shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version
FROM user_shop_items WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserShopItemState
rows.Scan(&v.ShopItemId, &v.BoughtCount, &v.LatestBoughtCountChangedDatetime, &v.LatestVersion)
u.ShopItems[v.ShopItemId] = v
})
queryRows(db, `SELECT slot_number, shop_item_id, latest_version FROM user_shop_replaceable_lineup WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.UserShopReplaceableLineupState
rows.Scan(&v.SlotNumber, &v.ShopItemId, &v.LatestVersion)
u.ShopReplaceableLineup[v.SlotNumber] = v
})
// Gimmick tables
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
is_gimmick_cleared, start_datetime, latest_version FROM user_gimmick_progress WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.GimmickProgressState
var ic int
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId, &v.Key.GimmickId,
&ic, &v.StartDatetime, &v.LatestVersion)
v.IsGimmickCleared = ic != 0
u.Gimmick.Progress[v.Key] = v
})
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
gimmick_ornament_index, progress_value_bit, base_datetime, latest_version
FROM user_gimmick_ornament_progress WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.GimmickOrnamentProgressState
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId, &v.Key.GimmickId,
&v.Key.GimmickOrnamentIndex, &v.ProgressValueBit, &v.BaseDatetime, &v.LatestVersion)
u.Gimmick.OrnamentProgress[v.Key] = v
})
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id,
is_gimmick_sequence_cleared, clear_datetime, latest_version FROM user_gimmick_sequences WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.GimmickSequenceState
var ic int
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId,
&ic, &v.ClearDatetime, &v.LatestVersion)
v.IsGimmickSequenceCleared = ic != 0
u.Gimmick.Sequences[v.Key] = v
})
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
is_unlocked, latest_version FROM user_gimmick_unlocks WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.GimmickUnlockState
var iu int
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId, &v.Key.GimmickId,
&iu, &v.LatestVersion)
v.IsUnlocked = iu != 0
u.Gimmick.Unlocks[v.Key] = v
})
// Big hunt maps
queryRows(db, `SELECT big_hunt_boss_id, max_score, max_score_update_datetime, latest_version
FROM user_big_hunt_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id int32
var v store.BigHuntMaxScore
rows.Scan(&id, &v.MaxScore, &v.MaxScoreUpdateDatetime, &v.LatestVersion)
u.BigHuntMaxScores[id] = v
})
queryRows(db, `SELECT big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, latest_version
FROM user_big_hunt_statuses WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id int32
var v store.BigHuntStatus
rows.Scan(&id, &v.DailyChallengeCount, &v.LatestChallengeDatetime, &v.LatestVersion)
u.BigHuntStatuses[id] = v
})
queryRows(db, `SELECT big_hunt_schedule_id, big_hunt_boss_id, max_score, max_score_update_datetime, latest_version
FROM user_big_hunt_schedule_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var k store.BigHuntScheduleScoreKey
var v store.BigHuntScheduleMaxScore
rows.Scan(&k.BigHuntScheduleId, &k.BigHuntBossId, &v.MaxScore, &v.MaxScoreUpdateDatetime, &v.LatestVersion)
u.BigHuntScheduleMaxScores[k] = v
})
queryRows(db, `SELECT big_hunt_weekly_version, attribute_type, max_score, latest_version
FROM user_big_hunt_weekly_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var k store.BigHuntWeeklyScoreKey
var v store.BigHuntWeeklyMaxScore
rows.Scan(&k.BigHuntWeeklyVersion, &k.AttributeType, &v.MaxScore, &v.LatestVersion)
u.BigHuntWeeklyMaxScores[k] = v
})
queryRows(db, `SELECT big_hunt_weekly_version, is_received_weekly_reward, latest_version
FROM user_big_hunt_weekly_statuses WHERE user_id=?`, uid, func(rows *sql.Rows) {
var ver int64
var ir int
var lv int64
rows.Scan(&ver, &ir, &lv)
u.BigHuntWeeklyStatuses[ver] = store.BigHuntWeeklyStatus{IsReceivedWeeklyReward: ir != 0, LatestVersion: lv}
})
}
func queryRows(db *sql.DB, query string, uid int64, scan func(*sql.Rows)) {
rows, err := db.Query(query, uid)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
scan(rows)
}
}
func loadSimpleMap(db *sql.DB, uid int64, query string, m map[int32]int32) {
queryRows(db, query, uid, func(rows *sql.Rows) {
var k, v int32
rows.Scan(&k, &v)
m[k] = v
})
}
func loadTimestampMap(db *sql.DB, uid int64, query string, m map[int32]int64) {
queryRows(db, query, uid, func(rows *sql.Rows) {
var k int32
var v int64
rows.Scan(&k, &v)
m[k] = v
})
}
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
package sqlite
import (
"fmt"
"time"
"lunar-tear/server/internal/store"
)
func (s *SQLiteStore) CreateSession(uuid string, ttl time.Duration) (store.SessionState, error) {
var userId int64
err := s.db.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&userId)
if err != nil {
return store.SessionState{}, store.ErrNotFound
}
now := s.clock()
sessionKey := fmt.Sprintf("session_%d_%d", userId, now.UnixNano())
expireAt := now.Add(ttl)
_, err = s.db.Exec(
`INSERT INTO sessions (session_key, user_id, uuid, expire_at) VALUES (?, ?, ?, ?)`,
sessionKey, userId, uuid, expireAt.Format(time.RFC3339Nano),
)
if err != nil {
return store.SessionState{}, fmt.Errorf("insert session: %w", err)
}
return store.SessionState{
SessionKey: sessionKey,
UserId: userId,
Uuid: uuid,
ExpireAt: expireAt,
}, nil
}
func (s *SQLiteStore) ResolveUserId(sessionKey string) (int64, error) {
var userId int64
var expireStr string
err := s.db.QueryRow(
`SELECT user_id, expire_at FROM sessions WHERE session_key = ?`, sessionKey,
).Scan(&userId, &expireStr)
if err != nil {
return 0, store.ErrNotFound
}
expireAt, err := time.Parse(time.RFC3339Nano, expireStr)
if err != nil {
return 0, store.ErrNotFound
}
if s.clock().After(expireAt) {
return 0, store.ErrNotFound
}
return userId, nil
}
+25
View File
@@ -0,0 +1,25 @@
package sqlite
import (
"database/sql"
"time"
"lunar-tear/server/internal/store"
)
type SQLiteStore struct {
db *sql.DB
clock store.Clock
}
var (
_ store.UserRepository = (*SQLiteStore)(nil)
_ store.SessionRepository = (*SQLiteStore)(nil)
)
func New(db *sql.DB, clock store.Clock) *SQLiteStore {
if clock == nil {
clock = time.Now
}
return &SQLiteStore{db: db, clock: clock}
}
+218
View File
@@ -0,0 +1,218 @@
package sqlite
import (
"database/sql"
"fmt"
"lunar-tear/server/internal/store"
)
func (s *SQLiteStore) CreateUser(uuid string) (int64, error) {
tx, err := s.db.Begin()
if err != nil {
return 0, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
var existingId int64
err = tx.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&existingId)
if err == nil {
return existingId, nil
}
nowMillis := s.clock().UnixMilli()
res, err := tx.Exec(`INSERT INTO users (uuid, player_id, os_type, platform_type, user_restriction_type,
register_datetime, game_start_datetime, latest_version, birth_year, birth_month,
backup_token, charge_money_this_month) VALUES (?, 0, 2, 2, 0, ?, ?, 0, 2000, 1, 'mock-backup-token', 0)`,
uuid, nowMillis, nowMillis)
if err != nil {
return 0, fmt.Errorf("insert user: %w", err)
}
userId, err := res.LastInsertId()
if err != nil {
return 0, fmt.Errorf("last insert id: %w", err)
}
// player_id = user_id
if _, err := tx.Exec(`UPDATE users SET player_id = ? WHERE user_id = ?`, userId, userId); err != nil {
return 0, fmt.Errorf("update player_id: %w", err)
}
user := store.SeedUserState(userId, uuid, nowMillis)
if err := writeUserState(tx, userId, user); err != nil {
return 0, fmt.Errorf("write seed state: %w", err)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("commit: %w", err)
}
return userId, nil
}
func (s *SQLiteStore) GetUserByUUID(uuid string) (int64, error) {
var userId int64
err := s.db.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&userId)
if err == sql.ErrNoRows {
return 0, store.ErrNotFound
}
if err != nil {
return 0, fmt.Errorf("query user: %w", err)
}
return userId, nil
}
func (s *SQLiteStore) DefaultUserId() (int64, error) {
var userId int64
err := s.db.QueryRow(`SELECT min(user_id) FROM users`).Scan(&userId)
if err != nil || userId == 0 {
return 0, store.ErrNotFound
}
return userId, nil
}
// ImportUser replaces all data for u.UserId in the database with the
// contents of u. Any pre-existing rows for that user are deleted first.
func (s *SQLiteStore) ImportUser(u *store.UserState) error {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
uid := u.UserId
// Child tables in reverse-dependency order (matches schema's goose Down).
childTables := []string{
"user_cage_ornament_rewards",
"user_shop_replaceable_lineup",
"user_shop_items",
"user_gacha_banner_box_drew_counts",
"user_gacha_banners",
"user_gacha_converted_medals",
"user_gifts",
"user_dokan_confirmed",
"user_drawn_omikuji",
"user_contents_stories",
"user_viewed_movies",
"user_navi_cutin_played",
"user_auto_sale_settings",
"user_explore_scores",
"user_tutorials",
"user_premium_items",
"user_important_items",
"user_materials",
"user_consumable_items",
"user_gimmick_unlocks",
"user_gimmick_sequences",
"user_gimmick_ornament_progress",
"user_gimmick_progress",
"user_big_hunt_weekly_statuses",
"user_big_hunt_weekly_max_scores",
"user_big_hunt_schedule_max_scores",
"user_big_hunt_statuses",
"user_big_hunt_max_scores",
"user_quest_limit_content_status",
"user_side_story_quests",
"user_missions",
"user_quest_missions",
"user_quests",
"user_deck_type_notes",
"user_deck_parts",
"user_deck_sub_weapons",
"user_decks",
"user_deck_characters",
"user_parts_presets",
"user_parts_group_notes",
"user_parts",
"user_thoughts",
"user_companions",
"user_weapon_notes",
"user_weapon_stories",
"user_weapon_awakens",
"user_weapon_abilities",
"user_weapon_skills",
"user_weapons",
"user_costume_awaken_status_ups",
"user_costume_active_skills",
"user_costumes",
"user_character_rebirths",
"user_character_board_status_ups",
"user_character_board_abilities",
"user_character_boards",
"user_characters",
"user_gacha",
"user_shop_replaceable",
"user_explore",
"user_guerrilla_free_open",
"user_portal_cage",
"user_notification",
"user_battle",
"user_big_hunt_state",
"user_side_story_active",
"user_extra_quest",
"user_event_quest",
"user_main_quest",
"user_login_bonus",
"user_login",
"user_profile",
"user_gem",
"user_status",
"user_setting",
"sessions",
}
for _, t := range childTables {
if _, err := tx.Exec(fmt.Sprintf(`DELETE FROM %s WHERE user_id = ?`, t), uid); err != nil {
return fmt.Errorf("delete from %s: %w", t, err)
}
}
if _, err := tx.Exec(`DELETE FROM users WHERE user_id = ?`, uid); err != nil {
return fmt.Errorf("delete user: %w", err)
}
if _, err := tx.Exec(`INSERT INTO users (user_id, uuid, player_id, os_type, platform_type,
user_restriction_type, register_datetime, game_start_datetime, latest_version,
birth_year, birth_month, backup_token, charge_money_this_month)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
uid, u.Uuid, u.PlayerId, u.OsType, u.PlatformType, u.UserRestrictionType,
u.RegisterDatetime, u.GameStartDatetime, u.LatestVersion,
u.BirthYear, u.BirthMonth, u.BackupToken, u.ChargeMoneyThisMonth); err != nil {
return fmt.Errorf("insert user: %w", err)
}
if err := writeUserState(tx, uid, u); err != nil {
return fmt.Errorf("write user state: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit: %w", err)
}
return nil
}
func (s *SQLiteStore) UpdateUser(userId int64, mutate func(*store.UserState)) (store.UserState, error) {
before, err := s.LoadUser(userId)
if err != nil {
return store.UserState{}, err
}
after := store.CloneUserState(before)
mutate(&after)
tx, err := s.db.Begin()
if err != nil {
return store.UserState{}, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
if err := diffAndSave(tx, userId, &before, &after); err != nil {
return store.UserState{}, fmt.Errorf("diff and save: %w", err)
}
if err := tx.Commit(); err != nil {
return store.UserState{}, fmt.Errorf("commit: %w", err)
}
return after, nil
}
+4 -8
View File
@@ -10,18 +10,14 @@ var ErrNotFound = errors.New("store: not found")
type Clock func() time.Time
type UserRepository interface {
EnsureUser(uuid string) (UserState, error)
SnapshotUser(userId int64) (UserState, error)
CreateUser(uuid string) (int64, error)
GetUserByUUID(uuid string) (int64, error)
LoadUser(userId int64) (UserState, error)
UpdateUser(userId int64, mutate func(*UserState)) (UserState, error)
DefaultUserId() (int64, error)
}
type SessionRepository interface {
CreateSession(uuid string, ttl time.Duration) (UserState, SessionState, error)
CreateSession(uuid string, ttl time.Duration) (SessionState, error)
ResolveUserId(sessionKey string) (int64, error)
}
type GachaRepository interface {
SnapshotCatalog() ([]GachaCatalogEntry, error)
ReplaceCatalog(entries []GachaCatalogEntry) error
}
+59 -12
View File
@@ -107,9 +107,11 @@ type UserState struct {
CharacterBoardAbilities map[CharacterBoardAbilityKey]CharacterBoardAbilityState
CharacterBoardStatusUps map[CharacterBoardStatusUpKey]CharacterBoardStatusUpState
CostumeAwakenStatusUps map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState
AutoSaleSettings map[int32]AutoSaleSettingState
CharacterRebirths map[int32]CharacterRebirthState
CostumeAwakenStatusUps map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState
CostumeLotteryEffects map[CostumeLotteryEffectKey]CostumeLotteryEffectState
CostumeLotteryEffectPending map[string]CostumeLotteryEffectPendingState // key: userCostumeUuid
AutoSaleSettings map[int32]AutoSaleSettingState
CharacterRebirths map[int32]CharacterRebirthState
}
func (u *UserState) EnsureMaps() {
@@ -254,6 +256,12 @@ func (u *UserState) EnsureMaps() {
if u.CostumeAwakenStatusUps == nil {
u.CostumeAwakenStatusUps = make(map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState)
}
if u.CostumeLotteryEffects == nil {
u.CostumeLotteryEffects = make(map[CostumeLotteryEffectKey]CostumeLotteryEffectState)
}
if u.CostumeLotteryEffectPending == nil {
u.CostumeLotteryEffectPending = make(map[string]CostumeLotteryEffectPendingState)
}
if u.AutoSaleSettings == nil {
u.AutoSaleSettings = make(map[int32]AutoSaleSettingState)
}
@@ -358,15 +366,16 @@ type CharacterState struct {
}
type CostumeState struct {
UserCostumeUuid string
CostumeId int32
LimitBreakCount int32
Level int32
Exp int32
HeadupDisplayViewId int32
AcquisitionDatetime int64
AwakenCount int32
LatestVersion int64
UserCostumeUuid string
CostumeId int32
LimitBreakCount int32
Level int32
Exp int32
HeadupDisplayViewId int32
AcquisitionDatetime int64
AwakenCount int32
CostumeLotteryEffectUnlockedSlotCount int32
LatestVersion int64
}
type WeaponState struct {
@@ -1070,3 +1079,41 @@ type CharacterRebirthState struct {
RebirthCount int32
LatestVersion int64
}
type CostumeLotteryEffectKey struct {
UserCostumeUuid string
SlotNumber int32
}
func (k CostumeLotteryEffectKey) MarshalText() ([]byte, error) {
return fmt.Appendf(nil, "%s:%d", k.UserCostumeUuid, k.SlotNumber), nil
}
func (k *CostumeLotteryEffectKey) UnmarshalText(text []byte) error {
s := string(text)
idx := strings.LastIndex(s, ":")
if idx < 0 {
return fmt.Errorf("invalid CostumeLotteryEffectKey: %s", text)
}
k.UserCostumeUuid = s[:idx]
v, err := strconv.ParseInt(s[idx+1:], 10, 32)
if err != nil {
return err
}
k.SlotNumber = int32(v)
return nil
}
type CostumeLotteryEffectState struct {
UserCostumeUuid string
SlotNumber int32
OddsNumber int32
LatestVersion int64
}
type CostumeLotteryEffectPendingState struct {
UserCostumeUuid string
SlotNumber int32
OddsNumber int32
LatestVersion int64
}
+60 -12
View File
@@ -105,12 +105,18 @@ func init() {
s, _ := encodeJSONMaps(SortedWeaponAwakenRecords(user)...)
return s
})
register("IUserCostumeLotteryEffect", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCostumeLotteryEffectRecords(user)...)
return s
})
register("IUserCostumeLotteryEffectPending", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedCostumeLotteryEffectPendingRecords(user)...)
return s
})
registerStatic(
"IUserCostumeLevelBonusReleaseStatus",
"IUserCostumeLotteryEffect",
"IUserCostumeLotteryEffectAbility",
"IUserCostumeLotteryEffectStatusUp",
"IUserCostumeLotteryEffectPending",
"IUserPartsPresetTag",
"IUserPartsStatusSub",
)
@@ -143,16 +149,17 @@ func sortedCostumeRecords(user store.UserState) []map[string]any {
for _, key := range keys {
row := user.Costumes[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"costumeId": row.CostumeId,
"limitBreakCount": row.LimitBreakCount,
"level": row.Level,
"exp": row.Exp,
"headupDisplayViewId": row.HeadupDisplayViewId,
"acquisitionDatetime": row.AcquisitionDatetime,
"awakenCount": row.AwakenCount,
"latestVersion": row.LatestVersion,
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"costumeId": row.CostumeId,
"limitBreakCount": row.LimitBreakCount,
"level": row.Level,
"exp": row.Exp,
"headupDisplayViewId": row.HeadupDisplayViewId,
"acquisitionDatetime": row.AcquisitionDatetime,
"awakenCount": row.AwakenCount,
"costumeLotteryEffectUnlockedSlotCount": row.CostumeLotteryEffectUnlockedSlotCount,
"latestVersion": row.LatestVersion,
})
}
return records
@@ -619,3 +626,44 @@ func sortedCageOrnamentRewardRecords(user store.UserState) []map[string]any {
}
return records
}
func SortedCostumeLotteryEffectPendingRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.CostumeLotteryEffectPending)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.CostumeLotteryEffectPending[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"slotNumber": row.SlotNumber,
"oddsNumber": row.OddsNumber,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedCostumeLotteryEffectRecords(user store.UserState) []map[string]any {
keys := make([]store.CostumeLotteryEffectKey, 0, len(user.CostumeLotteryEffects))
for k := range user.CostumeLotteryEffects {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].UserCostumeUuid != keys[j].UserCostumeUuid {
return keys[i].UserCostumeUuid < keys[j].UserCostumeUuid
}
return keys[i].SlotNumber < keys[j].SlotNumber
})
records := make([]map[string]any, 0, len(keys))
for _, k := range keys {
row := user.CostumeLotteryEffects[k]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"slotNumber": row.SlotNumber,
"oddsNumber": row.OddsNumber,
"latestVersion": row.LatestVersion,
})
}
return records
}
@@ -138,6 +138,14 @@ func SelectTables(all map[string]string, requested []string) map[string]string {
return selected
}
func ProjectTables(user store.UserState, requested []string) map[string]string {
result := make(map[string]string, len(requested))
for _, table := range requested {
result[table] = projectTable(table, user)
}
return result
}
func BuildDiffFromTables(tables map[string]string) map[string]*pb.DiffData {
diff := make(map[string]*pb.DiffData, len(tables))
for table, payload := range tables {
@@ -0,0 +1,874 @@
-- +goose Up
PRAGMA foreign_keys = ON;
-- =============================================================================
-- 1. Identity and Sessions
-- =============================================================================
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL UNIQUE,
player_id INTEGER NOT NULL DEFAULT 0,
os_type INTEGER NOT NULL DEFAULT 0,
platform_type INTEGER NOT NULL DEFAULT 0,
user_restriction_type INTEGER NOT NULL DEFAULT 0,
register_datetime INTEGER NOT NULL DEFAULT 0,
game_start_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
birth_year INTEGER NOT NULL DEFAULT 0,
birth_month INTEGER NOT NULL DEFAULT 0,
backup_token TEXT NOT NULL DEFAULT '',
charge_money_this_month INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE sessions (
session_key TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id),
uuid TEXT NOT NULL,
expire_at TEXT NOT NULL
);
-- =============================================================================
-- 1b. Per-User 1:1 State Tables
-- =============================================================================
CREATE TABLE user_setting (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
is_notify_purchase_alert INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_status (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
level INTEGER NOT NULL DEFAULT 0,
exp INTEGER NOT NULL DEFAULT 0,
stamina_milli_value INTEGER NOT NULL DEFAULT 0,
stamina_update_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_gem (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
paid_gem INTEGER NOT NULL DEFAULT 0,
free_gem INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_profile (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
name TEXT NOT NULL DEFAULT '',
name_update_datetime INTEGER NOT NULL DEFAULT 0,
message TEXT NOT NULL DEFAULT '',
message_update_datetime INTEGER NOT NULL DEFAULT 0,
favorite_costume_id INTEGER NOT NULL DEFAULT 0,
favorite_costume_id_update_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_login (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
total_login_count INTEGER NOT NULL DEFAULT 0,
continual_login_count INTEGER NOT NULL DEFAULT 0,
max_continual_login_count INTEGER NOT NULL DEFAULT 0,
last_login_datetime INTEGER NOT NULL DEFAULT 0,
last_comeback_login_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_login_bonus (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
login_bonus_id INTEGER NOT NULL DEFAULT 0,
current_page_number INTEGER NOT NULL DEFAULT 0,
current_stamp_number INTEGER NOT NULL DEFAULT 0,
latest_reward_receive_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_main_quest (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
current_quest_flow_type INTEGER NOT NULL DEFAULT 0,
current_main_quest_route_id INTEGER NOT NULL DEFAULT 0,
current_quest_scene_id INTEGER NOT NULL DEFAULT 0,
head_quest_scene_id INTEGER NOT NULL DEFAULT 0,
is_reached_last_quest_scene INTEGER NOT NULL DEFAULT 0,
progress_quest_scene_id INTEGER NOT NULL DEFAULT 0,
progress_head_quest_scene_id INTEGER NOT NULL DEFAULT 0,
progress_quest_flow_type INTEGER NOT NULL DEFAULT 0,
main_quest_season_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
saved_current_quest_scene_id INTEGER NOT NULL DEFAULT 0,
saved_head_quest_scene_id INTEGER NOT NULL DEFAULT 0,
replay_flow_current_quest_scene_id INTEGER NOT NULL DEFAULT 0,
replay_flow_head_quest_scene_id INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_event_quest (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
current_event_quest_chapter_id INTEGER NOT NULL DEFAULT 0,
current_quest_id INTEGER NOT NULL DEFAULT 0,
current_quest_scene_id INTEGER NOT NULL DEFAULT 0,
head_quest_scene_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_extra_quest (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
current_quest_id INTEGER NOT NULL DEFAULT 0,
current_quest_scene_id INTEGER NOT NULL DEFAULT 0,
head_quest_scene_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_side_story_active (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
current_side_story_quest_id INTEGER NOT NULL DEFAULT 0,
current_side_story_quest_scene_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_big_hunt_state (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
current_big_hunt_boss_quest_id INTEGER NOT NULL DEFAULT 0,
current_big_hunt_quest_id INTEGER NOT NULL DEFAULT 0,
current_quest_scene_id INTEGER NOT NULL DEFAULT 0,
is_dry_run INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
deck_type INTEGER NOT NULL DEFAULT 0,
user_triple_deck_number INTEGER NOT NULL DEFAULT 0,
boss_knock_down_count INTEGER NOT NULL DEFAULT 0,
max_combo_count INTEGER NOT NULL DEFAULT 0,
total_damage INTEGER NOT NULL DEFAULT 0,
deck_number INTEGER NOT NULL DEFAULT 0,
battle_binary BLOB
);
CREATE TABLE user_battle (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
is_active INTEGER NOT NULL DEFAULT 0,
start_count INTEGER NOT NULL DEFAULT 0,
finish_count INTEGER NOT NULL DEFAULT 0,
last_started_at INTEGER NOT NULL DEFAULT 0,
last_finished_at INTEGER NOT NULL DEFAULT 0,
last_user_party_count INTEGER NOT NULL DEFAULT 0,
last_npc_party_count INTEGER NOT NULL DEFAULT 0,
last_battle_binary_size INTEGER NOT NULL DEFAULT 0,
last_elapsed_frame_count INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_notification (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
gift_not_receive_count INTEGER NOT NULL DEFAULT 0,
friend_request_receive_count INTEGER NOT NULL DEFAULT 0,
is_exist_unread_information INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_portal_cage (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
is_current_progress INTEGER NOT NULL DEFAULT 0,
drop_item_start_datetime INTEGER NOT NULL DEFAULT 0,
current_drop_item_count INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_guerrilla_free_open (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
start_datetime INTEGER NOT NULL DEFAULT 0,
open_minutes INTEGER NOT NULL DEFAULT 0,
daily_opened_count INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_explore (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
is_use_explore_ticket INTEGER NOT NULL DEFAULT 0,
playing_explore_id INTEGER NOT NULL DEFAULT 0,
latest_play_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_shop_replaceable (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
lineup_update_count INTEGER NOT NULL DEFAULT 0,
latest_lineup_update_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE user_gacha (
user_id INTEGER PRIMARY KEY REFERENCES users(user_id),
reward_available INTEGER NOT NULL DEFAULT 0,
todays_current_draw_count INTEGER NOT NULL DEFAULT 0,
daily_max_count INTEGER NOT NULL DEFAULT 0,
last_reward_draw_date INTEGER NOT NULL DEFAULT 0,
obtain_consumable_item_id INTEGER,
obtain_count INTEGER
);
-- =============================================================================
-- 2. Characters and Progression
-- =============================================================================
CREATE TABLE user_characters (
user_id INTEGER NOT NULL REFERENCES users(user_id),
character_id INTEGER NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
exp INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, character_id)
);
CREATE TABLE user_character_boards (
user_id INTEGER NOT NULL REFERENCES users(user_id),
character_board_id INTEGER NOT NULL,
panel_release_bit1 INTEGER NOT NULL DEFAULT 0,
panel_release_bit2 INTEGER NOT NULL DEFAULT 0,
panel_release_bit3 INTEGER NOT NULL DEFAULT 0,
panel_release_bit4 INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, character_board_id)
);
CREATE TABLE user_character_board_abilities (
user_id INTEGER NOT NULL REFERENCES users(user_id),
character_id INTEGER NOT NULL,
ability_id INTEGER NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, character_id, ability_id)
);
CREATE TABLE user_character_board_status_ups (
user_id INTEGER NOT NULL REFERENCES users(user_id),
character_id INTEGER NOT NULL,
status_calculation_type INTEGER NOT NULL,
hp INTEGER NOT NULL DEFAULT 0,
attack INTEGER NOT NULL DEFAULT 0,
vitality INTEGER NOT NULL DEFAULT 0,
agility INTEGER NOT NULL DEFAULT 0,
critical_ratio INTEGER NOT NULL DEFAULT 0,
critical_attack INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, character_id, status_calculation_type)
);
CREATE TABLE user_character_rebirths (
user_id INTEGER NOT NULL REFERENCES users(user_id),
character_id INTEGER NOT NULL,
rebirth_count INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, character_id)
);
-- =============================================================================
-- 3. Equipment (UUID-keyed)
-- =============================================================================
CREATE TABLE user_costumes (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_costume_uuid TEXT NOT NULL,
costume_id INTEGER NOT NULL,
limit_break_count INTEGER NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 0,
exp INTEGER NOT NULL DEFAULT 0,
headup_display_view_id INTEGER NOT NULL DEFAULT 0,
acquisition_datetime INTEGER NOT NULL DEFAULT 0,
awaken_count INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_costume_uuid)
);
CREATE TABLE user_costume_active_skills (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_costume_uuid TEXT NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_costume_uuid)
);
CREATE TABLE user_costume_awaken_status_ups (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_costume_uuid TEXT NOT NULL,
status_calculation_type INTEGER NOT NULL,
hp INTEGER NOT NULL DEFAULT 0,
attack INTEGER NOT NULL DEFAULT 0,
vitality INTEGER NOT NULL DEFAULT 0,
agility INTEGER NOT NULL DEFAULT 0,
critical_ratio INTEGER NOT NULL DEFAULT 0,
critical_attack INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_costume_uuid, status_calculation_type)
);
CREATE TABLE user_weapons (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_weapon_uuid TEXT NOT NULL,
weapon_id INTEGER NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
exp INTEGER NOT NULL DEFAULT 0,
limit_break_count INTEGER NOT NULL DEFAULT 0,
is_protected INTEGER NOT NULL DEFAULT 0,
acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_weapon_uuid)
);
CREATE TABLE user_weapon_skills (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_weapon_uuid TEXT NOT NULL,
slot_number INTEGER NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_weapon_uuid, slot_number)
);
CREATE TABLE user_weapon_abilities (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_weapon_uuid TEXT NOT NULL,
slot_number INTEGER NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_weapon_uuid, slot_number)
);
CREATE TABLE user_weapon_awakens (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_weapon_uuid TEXT NOT NULL,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_weapon_uuid)
);
CREATE TABLE user_weapon_stories (
user_id INTEGER NOT NULL REFERENCES users(user_id),
weapon_id INTEGER NOT NULL,
released_max_story_index INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, weapon_id)
);
CREATE TABLE user_weapon_notes (
user_id INTEGER NOT NULL REFERENCES users(user_id),
weapon_id INTEGER NOT NULL,
max_level INTEGER NOT NULL DEFAULT 0,
max_limit_break_count INTEGER NOT NULL DEFAULT 0,
first_acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, weapon_id)
);
CREATE TABLE user_companions (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_companion_uuid TEXT NOT NULL,
companion_id INTEGER NOT NULL,
headup_display_view_id INTEGER NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 0,
acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_companion_uuid)
);
CREATE TABLE user_thoughts (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_thought_uuid TEXT NOT NULL,
thought_id INTEGER NOT NULL,
acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_thought_uuid)
);
CREATE TABLE user_parts (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_parts_uuid TEXT NOT NULL,
parts_id INTEGER NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
parts_status_main_id INTEGER NOT NULL DEFAULT 0,
is_protected INTEGER NOT NULL DEFAULT 0,
acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_parts_uuid)
);
CREATE TABLE user_parts_group_notes (
user_id INTEGER NOT NULL REFERENCES users(user_id),
parts_group_id INTEGER NOT NULL,
first_acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, parts_group_id)
);
CREATE TABLE user_parts_presets (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_parts_preset_number INTEGER NOT NULL,
user_parts_uuid01 TEXT NOT NULL DEFAULT '',
user_parts_uuid02 TEXT NOT NULL DEFAULT '',
user_parts_uuid03 TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
user_parts_preset_tag_number INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_parts_preset_number)
);
-- =============================================================================
-- 4. Deck System
-- =============================================================================
CREATE TABLE user_deck_characters (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_deck_character_uuid TEXT NOT NULL,
user_costume_uuid TEXT NOT NULL DEFAULT '',
main_user_weapon_uuid TEXT NOT NULL DEFAULT '',
user_companion_uuid TEXT NOT NULL DEFAULT '',
power INTEGER NOT NULL DEFAULT 0,
user_thought_uuid TEXT NOT NULL DEFAULT '',
dressup_costume_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_deck_character_uuid)
);
CREATE TABLE user_decks (
user_id INTEGER NOT NULL REFERENCES users(user_id),
deck_type INTEGER NOT NULL,
user_deck_number INTEGER NOT NULL,
user_deck_character_uuid01 TEXT NOT NULL DEFAULT '',
user_deck_character_uuid02 TEXT NOT NULL DEFAULT '',
user_deck_character_uuid03 TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
power INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, deck_type, user_deck_number)
);
CREATE TABLE user_deck_sub_weapons (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_deck_character_uuid TEXT NOT NULL,
ordinal INTEGER NOT NULL,
user_weapon_uuid TEXT NOT NULL,
PRIMARY KEY (user_id, user_deck_character_uuid, ordinal)
);
CREATE TABLE user_deck_parts (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_deck_character_uuid TEXT NOT NULL,
ordinal INTEGER NOT NULL,
user_parts_uuid TEXT NOT NULL,
PRIMARY KEY (user_id, user_deck_character_uuid, ordinal)
);
CREATE TABLE user_deck_type_notes (
user_id INTEGER NOT NULL REFERENCES users(user_id),
deck_type INTEGER NOT NULL,
max_deck_power INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, deck_type)
);
-- =============================================================================
-- 5. Quests
-- =============================================================================
CREATE TABLE user_quests (
user_id INTEGER NOT NULL REFERENCES users(user_id),
quest_id INTEGER NOT NULL,
quest_state_type INTEGER NOT NULL DEFAULT 0,
is_battle_only INTEGER NOT NULL DEFAULT 0,
user_deck_number INTEGER NOT NULL DEFAULT 0,
latest_start_datetime INTEGER NOT NULL DEFAULT 0,
clear_count INTEGER NOT NULL DEFAULT 0,
daily_clear_count INTEGER NOT NULL DEFAULT 0,
last_clear_datetime INTEGER NOT NULL DEFAULT 0,
shortest_clear_frames INTEGER NOT NULL DEFAULT 0,
is_reward_granted INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, quest_id)
);
CREATE TABLE user_quest_missions (
user_id INTEGER NOT NULL REFERENCES users(user_id),
quest_id INTEGER NOT NULL,
quest_mission_id INTEGER NOT NULL,
progress_value INTEGER NOT NULL DEFAULT 0,
is_clear INTEGER NOT NULL DEFAULT 0,
latest_clear_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, quest_id, quest_mission_id)
);
CREATE TABLE user_missions (
user_id INTEGER NOT NULL REFERENCES users(user_id),
mission_id INTEGER NOT NULL,
start_datetime INTEGER NOT NULL DEFAULT 0,
progress_value INTEGER NOT NULL DEFAULT 0,
mission_progress_status_type INTEGER NOT NULL DEFAULT 0,
clear_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, mission_id)
);
CREATE TABLE user_side_story_quests (
user_id INTEGER NOT NULL REFERENCES users(user_id),
side_story_quest_id INTEGER NOT NULL,
head_side_story_quest_scene_id INTEGER NOT NULL DEFAULT 0,
side_story_quest_state_type INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, side_story_quest_id)
);
CREATE TABLE user_quest_limit_content_status (
user_id INTEGER NOT NULL REFERENCES users(user_id),
limit_content_id INTEGER NOT NULL,
limit_content_quest_status_type INTEGER NOT NULL DEFAULT 0,
event_quest_chapter_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, limit_content_id)
);
-- =============================================================================
-- 6. Big Hunt
-- =============================================================================
CREATE TABLE user_big_hunt_max_scores (
user_id INTEGER NOT NULL REFERENCES users(user_id),
big_hunt_boss_id INTEGER NOT NULL,
max_score INTEGER NOT NULL DEFAULT 0,
max_score_update_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, big_hunt_boss_id)
);
CREATE TABLE user_big_hunt_statuses (
user_id INTEGER NOT NULL REFERENCES users(user_id),
big_hunt_boss_id INTEGER NOT NULL,
daily_challenge_count INTEGER NOT NULL DEFAULT 0,
latest_challenge_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, big_hunt_boss_id)
);
CREATE TABLE user_big_hunt_schedule_max_scores (
user_id INTEGER NOT NULL REFERENCES users(user_id),
big_hunt_schedule_id INTEGER NOT NULL,
big_hunt_boss_id INTEGER NOT NULL,
max_score INTEGER NOT NULL DEFAULT 0,
max_score_update_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, big_hunt_schedule_id, big_hunt_boss_id)
);
CREATE TABLE user_big_hunt_weekly_max_scores (
user_id INTEGER NOT NULL REFERENCES users(user_id),
big_hunt_weekly_version INTEGER NOT NULL,
attribute_type INTEGER NOT NULL,
max_score INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, big_hunt_weekly_version, attribute_type)
);
CREATE TABLE user_big_hunt_weekly_statuses (
user_id INTEGER NOT NULL REFERENCES users(user_id),
big_hunt_weekly_version INTEGER NOT NULL,
is_received_weekly_reward INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, big_hunt_weekly_version)
);
-- =============================================================================
-- 7. Gimmicks
-- =============================================================================
CREATE TABLE user_gimmick_progress (
user_id INTEGER NOT NULL REFERENCES users(user_id),
gimmick_sequence_schedule_id INTEGER NOT NULL,
gimmick_sequence_id INTEGER NOT NULL,
gimmick_id INTEGER NOT NULL,
is_gimmick_cleared INTEGER NOT NULL DEFAULT 0,
start_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id)
);
CREATE TABLE user_gimmick_ornament_progress (
user_id INTEGER NOT NULL REFERENCES users(user_id),
gimmick_sequence_schedule_id INTEGER NOT NULL,
gimmick_sequence_id INTEGER NOT NULL,
gimmick_id INTEGER NOT NULL,
gimmick_ornament_index INTEGER NOT NULL,
progress_value_bit INTEGER NOT NULL DEFAULT 0,
base_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id, gimmick_ornament_index)
);
CREATE TABLE user_gimmick_sequences (
user_id INTEGER NOT NULL REFERENCES users(user_id),
gimmick_sequence_schedule_id INTEGER NOT NULL,
gimmick_sequence_id INTEGER NOT NULL,
is_gimmick_sequence_cleared INTEGER NOT NULL DEFAULT 0,
clear_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, gimmick_sequence_schedule_id, gimmick_sequence_id)
);
CREATE TABLE user_gimmick_unlocks (
user_id INTEGER NOT NULL REFERENCES users(user_id),
gimmick_sequence_schedule_id INTEGER NOT NULL,
gimmick_sequence_id INTEGER NOT NULL,
gimmick_id INTEGER NOT NULL,
is_unlocked INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id)
);
-- =============================================================================
-- 8. Inventory
-- =============================================================================
CREATE TABLE user_consumable_items (
user_id INTEGER NOT NULL REFERENCES users(user_id),
consumable_item_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, consumable_item_id)
);
CREATE TABLE user_materials (
user_id INTEGER NOT NULL REFERENCES users(user_id),
material_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, material_id)
);
CREATE TABLE user_important_items (
user_id INTEGER NOT NULL REFERENCES users(user_id),
important_item_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, important_item_id)
);
CREATE TABLE user_premium_items (
user_id INTEGER NOT NULL REFERENCES users(user_id),
premium_item_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, premium_item_id)
);
CREATE TABLE user_tutorials (
user_id INTEGER NOT NULL REFERENCES users(user_id),
tutorial_type INTEGER NOT NULL,
progress_phase INTEGER NOT NULL DEFAULT 0,
choice_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, tutorial_type)
);
CREATE TABLE user_explore_scores (
user_id INTEGER NOT NULL REFERENCES users(user_id),
explore_id INTEGER NOT NULL,
max_score INTEGER NOT NULL DEFAULT 0,
max_score_update_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, explore_id)
);
CREATE TABLE user_auto_sale_settings (
user_id INTEGER NOT NULL REFERENCES users(user_id),
possession_auto_sale_item_type INTEGER NOT NULL,
possession_auto_sale_item_value TEXT NOT NULL DEFAULT '',
PRIMARY KEY (user_id, possession_auto_sale_item_type)
);
-- =============================================================================
-- 9. Simple Progress Maps
-- =============================================================================
CREATE TABLE user_navi_cutin_played (
user_id INTEGER NOT NULL REFERENCES users(user_id),
navi_cutin_id INTEGER NOT NULL,
PRIMARY KEY (user_id, navi_cutin_id)
);
CREATE TABLE user_viewed_movies (
user_id INTEGER NOT NULL REFERENCES users(user_id),
movie_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, movie_id)
);
CREATE TABLE user_contents_stories (
user_id INTEGER NOT NULL REFERENCES users(user_id),
contents_story_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, contents_story_id)
);
CREATE TABLE user_drawn_omikuji (
user_id INTEGER NOT NULL REFERENCES users(user_id),
omikuji_id INTEGER NOT NULL,
timestamp INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, omikuji_id)
);
CREATE TABLE user_dokan_confirmed (
user_id INTEGER NOT NULL REFERENCES users(user_id),
dokan_id INTEGER NOT NULL,
PRIMARY KEY (user_id, dokan_id)
);
-- =============================================================================
-- 10. Gifts
-- =============================================================================
CREATE TABLE user_gifts (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_gift_uuid TEXT NOT NULL,
is_received INTEGER NOT NULL DEFAULT 0,
possession_type INTEGER NOT NULL DEFAULT 0,
possession_id INTEGER NOT NULL DEFAULT 0,
count INTEGER NOT NULL DEFAULT 0,
grant_datetime INTEGER NOT NULL DEFAULT 0,
description_gift_text_id INTEGER NOT NULL DEFAULT 0,
equipment_data BLOB,
expiration_datetime INTEGER,
received_datetime INTEGER,
PRIMARY KEY (user_id, user_gift_uuid)
);
-- =============================================================================
-- 11. Gacha
-- =============================================================================
CREATE TABLE user_gacha_converted_medals (
user_id INTEGER NOT NULL REFERENCES users(user_id),
ordinal INTEGER NOT NULL,
consumable_item_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, ordinal)
);
CREATE TABLE user_gacha_banners (
user_id INTEGER NOT NULL REFERENCES users(user_id),
gacha_id INTEGER NOT NULL,
medal_count INTEGER NOT NULL DEFAULT 0,
step_number INTEGER NOT NULL DEFAULT 0,
loop_count INTEGER NOT NULL DEFAULT 0,
draw_count INTEGER NOT NULL DEFAULT 0,
box_number INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, gacha_id)
);
CREATE TABLE user_gacha_banner_box_drew_counts (
user_id INTEGER NOT NULL REFERENCES users(user_id),
gacha_id INTEGER NOT NULL,
box_item_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, gacha_id, box_item_id)
);
-- =============================================================================
-- 12. Shop
-- =============================================================================
CREATE TABLE user_shop_items (
user_id INTEGER NOT NULL REFERENCES users(user_id),
shop_item_id INTEGER NOT NULL,
bought_count INTEGER NOT NULL DEFAULT 0,
latest_bought_count_changed_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, shop_item_id)
);
CREATE TABLE user_shop_replaceable_lineup (
user_id INTEGER NOT NULL REFERENCES users(user_id),
slot_number INTEGER NOT NULL,
shop_item_id INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, slot_number)
);
-- =============================================================================
-- 13. Cage Ornaments
-- =============================================================================
CREATE TABLE user_cage_ornament_rewards (
user_id INTEGER NOT NULL REFERENCES users(user_id),
cage_ornament_id INTEGER NOT NULL,
acquisition_datetime INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, cage_ornament_id)
);
-- +goose Down
DROP TABLE IF EXISTS user_cage_ornament_rewards ;
DROP TABLE IF EXISTS user_shop_replaceable_lineup ;
DROP TABLE IF EXISTS user_shop_items ;
DROP TABLE IF EXISTS user_gacha_banner_box_drew_counts;
DROP TABLE IF EXISTS user_gacha_banners ;
DROP TABLE IF EXISTS user_gacha_converted_medals ;
DROP TABLE IF EXISTS user_gifts ;
DROP TABLE IF EXISTS user_dokan_confirmed ;
DROP TABLE IF EXISTS user_drawn_omikuji ;
DROP TABLE IF EXISTS user_contents_stories ;
DROP TABLE IF EXISTS user_viewed_movies ;
DROP TABLE IF EXISTS user_navi_cutin_played ;
DROP TABLE IF EXISTS user_auto_sale_settings ;
DROP TABLE IF EXISTS user_explore_scores ;
DROP TABLE IF EXISTS user_tutorials ;
DROP TABLE IF EXISTS user_premium_items ;
DROP TABLE IF EXISTS user_important_items ;
DROP TABLE IF EXISTS user_materials ;
DROP TABLE IF EXISTS user_consumable_items ;
DROP TABLE IF EXISTS user_gimmick_unlocks ;
DROP TABLE IF EXISTS user_gimmick_sequences ;
DROP TABLE IF EXISTS user_gimmick_ornament_progress ;
DROP TABLE IF EXISTS user_gimmick_progress ;
DROP TABLE IF EXISTS user_big_hunt_weekly_statuses ;
DROP TABLE IF EXISTS user_big_hunt_weekly_max_scores ;
DROP TABLE IF EXISTS user_big_hunt_schedule_max_scores;
DROP TABLE IF EXISTS user_big_hunt_statuses ;
DROP TABLE IF EXISTS user_big_hunt_max_scores ;
DROP TABLE IF EXISTS user_quest_limit_content_status ;
DROP TABLE IF EXISTS user_side_story_quests ;
DROP TABLE IF EXISTS user_missions ;
DROP TABLE IF EXISTS user_quest_missions ;
DROP TABLE IF EXISTS user_quests ;
DROP TABLE IF EXISTS user_deck_type_notes ;
DROP TABLE IF EXISTS user_deck_parts ;
DROP TABLE IF EXISTS user_deck_sub_weapons ;
DROP TABLE IF EXISTS user_decks ;
DROP TABLE IF EXISTS user_deck_characters ;
DROP TABLE IF EXISTS user_parts_presets ;
DROP TABLE IF EXISTS user_parts_group_notes ;
DROP TABLE IF EXISTS user_parts ;
DROP TABLE IF EXISTS user_thoughts ;
DROP TABLE IF EXISTS user_companions ;
DROP TABLE IF EXISTS user_weapon_notes ;
DROP TABLE IF EXISTS user_weapon_stories ;
DROP TABLE IF EXISTS user_weapon_awakens ;
DROP TABLE IF EXISTS user_weapon_abilities ;
DROP TABLE IF EXISTS user_weapon_skills ;
DROP TABLE IF EXISTS user_weapons ;
DROP TABLE IF EXISTS user_costume_awaken_status_ups ;
DROP TABLE IF EXISTS user_costume_active_skills ;
DROP TABLE IF EXISTS user_costumes ;
DROP TABLE IF EXISTS user_character_rebirths ;
DROP TABLE IF EXISTS user_character_board_status_ups ;
DROP TABLE IF EXISTS user_character_board_abilities ;
DROP TABLE IF EXISTS user_character_boards ;
DROP TABLE IF EXISTS user_characters ;
DROP TABLE IF EXISTS user_gacha ;
DROP TABLE IF EXISTS user_shop_replaceable ;
DROP TABLE IF EXISTS user_explore ;
DROP TABLE IF EXISTS user_guerrilla_free_open ;
DROP TABLE IF EXISTS user_portal_cage ;
DROP TABLE IF EXISTS user_notification ;
DROP TABLE IF EXISTS user_battle ;
DROP TABLE IF EXISTS user_big_hunt_state ;
DROP TABLE IF EXISTS user_side_story_active ;
DROP TABLE IF EXISTS user_extra_quest ;
DROP TABLE IF EXISTS user_event_quest ;
DROP TABLE IF EXISTS user_main_quest ;
DROP TABLE IF EXISTS user_login_bonus ;
DROP TABLE IF EXISTS user_login ;
DROP TABLE IF EXISTS user_profile ;
DROP TABLE IF EXISTS user_gem ;
DROP TABLE IF EXISTS user_status ;
DROP TABLE IF EXISTS user_setting ;
DROP TABLE IF EXISTS sessions ;
DROP TABLE IF EXISTS users ;
@@ -0,0 +1,13 @@
-- +goose Up
-- Delete deck characters with empty weapons (always a bug).
DELETE FROM user_deck_characters
WHERE main_user_weapon_uuid = '';
-- Delete decks that reference deleted deck characters.
DELETE FROM user_decks
WHERE user_deck_character_uuid01 NOT IN (SELECT user_deck_character_uuid FROM user_deck_characters)
AND user_deck_character_uuid01 != '';
-- +goose Down
-- No rollback needed: EnsureDefaultDeck recreates decks on next SetTutorialProgress call.
@@ -0,0 +1,15 @@
-- +goose Up
CREATE TABLE user_costume_lottery_effects (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_costume_uuid TEXT NOT NULL,
slot_number INTEGER NOT NULL,
odds_number INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_costume_uuid, slot_number)
);
ALTER TABLE user_costumes ADD COLUMN costume_lottery_effect_unlocked_slot_count INTEGER NOT NULL DEFAULT 0;
-- +goose Down
ALTER TABLE user_costumes DROP COLUMN costume_lottery_effect_unlocked_slot_count;
DROP TABLE IF EXISTS user_costume_lottery_effects;
@@ -0,0 +1,12 @@
-- +goose Up
CREATE TABLE user_costume_lottery_effect_pending (
user_id INTEGER NOT NULL REFERENCES users(user_id),
user_costume_uuid TEXT NOT NULL,
slot_number INTEGER NOT NULL,
odds_number INTEGER NOT NULL DEFAULT 0,
latest_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, user_costume_uuid)
);
-- +goose Down
DROP TABLE IF EXISTS user_costume_lottery_effect_pending;