diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5376648..f7915ec 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,4 +1,4 @@ -name: Build and Push lunar-tear to Docker Hub +name: Build and Push Docker images to Docker Hub on: push: @@ -17,10 +17,26 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Docker image + - name: Build and push game server image uses: docker/build-push-action@v5 with: context: ./server file: ./server/Dockerfile push: true tags: kretts/lunar-tear:latest + + - name: Build and push CDN image + uses: docker/build-push-action@v5 + with: + context: ./server + file: ./server/Dockerfile.cdn + push: true + tags: kretts/octo-cdn:latest + + - name: Build and push auth server image + uses: docker/build-push-action@v5 + with: + context: ./server + file: ./server/Dockerfile.auth + push: true + tags: kretts/auth-server:latest diff --git a/.gitignore b/.gitignore index e95f0c7..6313ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ server/bin/ server/tmp/ server/lunar-tear server/import-snapshot +server/auth-server +server/claim-account +server/octo-cdn +server/dev __pycache__/ diff --git a/README.md b/README.md index 0f2dbc8..42ada2a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Or manually: ```bash cd server mkdir -p db -goose -dir migrations sqlite3 db/game.db up +goose -dir migrations -allow-missing sqlite3 db/game.db up ``` ### Importing a Snapshot @@ -65,48 +65,124 @@ go run ./cmd/import-snapshot \ ### Run +The server is split into two binaries: a gRPC game server and an HTTP asset CDN. Both must be running for the client to work. + +**Start the CDN** (serves asset bundles, list.bin, master data, web pages): + +```bash +cd server +go run ./cmd/octo-cdn \ + --listen 0.0.0.0:8080 \ + --public-addr 10.0.2.2:8080 +``` + +**Start the game server** (gRPC, points the client at the CDN): + ```bash cd server go run ./cmd/lunar-tear \ - --host 10.0.2.2 \ - --http-port 8080 \ - --grpc-port 8003 + --listen 0.0.0.0:8003 \ + --public-addr 10.0.2.2:8003 \ + --octo-url http://10.0.2.2:8080 ``` -The default gRPC port is 443, which requires `sudo` (privileged port). Use `--grpc-port` with a high port to avoid this. If you do need port 443, either use `sudo` or grant the binary the capability on Linux: +The default listen address is `0.0.0.0:443`, which requires `sudo` (privileged port). Use `--listen` with a high port to avoid this. If you do need port 443, either use `sudo` or grant the binary the capability on Linux: ```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 +./lunar-tear --public-addr 10.0.2.2:443 --octo-url http://10.0.2.2:8080 ``` +The CDN can run on a completely separate machine — just set `--octo-url` on the game server and `--public-addr` on the CDN to the externally-reachable address. + +### Run All Services At Once + +Instead of starting each service individually, use the dev runner to launch all three (auth, CDN, game server) with a single command. No Docker required — works on macOS, Linux, and Windows. + +```bash +cd server +make dev +``` + +Or directly: + +```bash +cd server +go run ./cmd/dev +``` + +Each service's output is prefixed with a colored label (`[auth]`, `[cdn]`, `[grpc]`). Press Ctrl+C to shut everything down. + +Override defaults with namespaced flags: + +```bash +go run ./cmd/dev --grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000 --cdn.public-addr 192.168.1.50:8080 +``` + +Or via `make`: + +```bash +make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000" +``` + +| Flag | Default | Description | +| --------------------- | ------------------ | ---------------------------------------- | +| `--auth.listen` | `0.0.0.0:3000` | auth-server listen address | +| `--auth.db` | `db/auth.db` | auth-server SQLite database path | +| `--cdn.listen` | `0.0.0.0:8080` | octo-cdn local bind address | +| `--cdn.public-addr` | `10.0.2.2:8080` | octo-cdn externally-reachable addr | +| `--grpc.listen` | `0.0.0.0:8003` | lunar-tear gRPC listen address | +| `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr | +| `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear | +| `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear | +| `--no-color` | `false` | disable colored output | + ### Ports -| Protocol | Port | Notes | -| -------- | ---- | ----------------------------------------------------------- | -| gRPC | 443 | default; configurable with `--grpc-port` (requires patched client) | -| HTTP | 8080 | Octo asset API + game web pages (`--http-port` flag) | +| Protocol | Port | Binary | Notes | +| -------- | ---- | ------------- | ----------------------------------------------------------- | +| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) | +| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages | -### Flags +### Game Server Flags (`lunar-tear`) -| Flag | Default | Description | -| ------------- | ------------ | ---------------------------------------------------- | -| `--host` | `127.0.0.1` | hostname/IP given to the client | -| `--http-port` | `8080` | HTTP/Octo server port | -| `--grpc-port` | `443` | gRPC server port (client must be patched to match) | -| `--db` | `db/game.db` | SQLite database path | +| Flag | Default | Description | +| --------------- | ----------------- | ---------------------------------------------------- | +| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | +| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | +| `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | +| `--db` | `db/game.db` | SQLite database path | +| `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | + +### CDN Flags (`octo-cdn`) + +| Flag | Default | Description | +| --------------- | ----------------- | -------------------------------------------------------- | +| `--listen` | `0.0.0.0:8080` | local bind address | +| `--public-addr` | `127.0.0.1:8080` | externally-reachable address (used in list.bin rewriting) | +| `--assets-dir` | `.` | root directory containing the `assets/` tree | ### Docker -Migrations run automatically on container start. +Three services are available via Docker Compose: the game server (`lunar-tear`), the CDN (`octo-cdn`), and the auth server (`auth-server`). Migrations run automatically on game server 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. +The `db/` directory is mounted as a volume so both `game.db` and `auth.db` persist across restarts. Make sure `assets/` is populated before starting. + +Each service has its own image and can be deployed independently: + +| Service | Image | Default Port | Notes | +| -------- | --------------------------- | ------------ | ------------------------------ | +| `server` | `kretts/lunar-tear:latest` | 8003 | gRPC game server | +| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN | +| `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login | + +The game server is configured via environment variables in the compose file: `LUNAR_LISTEN` (bind address), `LUNAR_PUBLIC_ADDR` (client-facing address), `LUNAR_OCTO_URL`, and `LUNAR_AUTH_URL`. Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. ### Makefile Targets @@ -115,11 +191,54 @@ All targets run from the `server/` directory. | Target | Description | | -------------- | ------------------------------------------------------- | | `make proto` | Regenerate protobuf stubs | -| `make build` | Build the server binary | +| `make build` | Build the game server binary | +| `make build-cdn` | Build the CDN binary | +| `make build-auth` | Build the auth server binary | | `make build-import` | Build the import-snapshot tool | +| `make build-claim-account` | Build the claim-account tool | +| `make dev` | Run all three services with one command | | `make migrate` | Run goose migrations on `db/game.db` | | `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) | +## Claim Account + +Transfers an existing game account to the most recently connected client. Looks up a player by their in-game name, assigns the new client's UUID to that account, and deletes the empty account the new client created. + +Useful when a new client connects and creates a throwaway account, but you want it to load an existing account instead. + +```bash +cd server +go run ./cmd/claim-account --name "PlayerName" --db db/game.db +``` + +| Flag | Default | Description | +| -------- | ------------ | ---------------------------------------------------- | +| `--name` | *(required)* | In-game player name to claim | +| `--db` | `db/game.db` | SQLite database path | + +## Auth Server + +A separate HTTP server that handles player account registration and login. The patched client's Facebook login button is redirected to this server, which presents a username/password form. Tokens issued here are validated by the game server to link or recover accounts. + +### Run + +```bash +cd server +go run ./cmd/auth-server \ + --listen 0.0.0.0:3000 \ + --db db/auth.db +``` + +The `--secret` flag accepts a hex-encoded HMAC key. If omitted, a random key is generated on startup and printed to the console — pass it back on the next restart to keep existing tokens valid. + +### Flags + +| Flag | Default | Description | +| ---------- | --------------- | -------------------------------------------- | +| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) | +| `--db` | `db/auth.db` | SQLite database path for auth users | +| `--secret` | *(generated)* | Hex-encoded HMAC secret for token signing | + ## ⚠️ Legal Disclaimer **Lunar Tear** is a fan-made, non-commercial **preservation and research project** dedicated to keeping a certain discontinued mobile game playable for educational and archival purposes. diff --git a/server/Dockerfile.auth b/server/Dockerfile.auth new file mode 100644 index 0000000..b5d57c5 --- /dev/null +++ b/server/Dockerfile.auth @@ -0,0 +1,20 @@ +FROM alpine:latest AS builder + +WORKDIR /usr/local/src +COPY . . + +RUN apk add --no-cache go + +RUN go build -o auth-server ./cmd/auth-server + +FROM alpine:latest + +WORKDIR /opt/auth-server + +RUN chown 1000:1000 /opt/auth-server + +USER 1000 + +COPY --from=builder /usr/local/src/auth-server . + +ENTRYPOINT ["./auth-server"] diff --git a/server/Dockerfile.cdn b/server/Dockerfile.cdn new file mode 100644 index 0000000..3cc92d4 --- /dev/null +++ b/server/Dockerfile.cdn @@ -0,0 +1,30 @@ +FROM alpine:latest AS builder + +WORKDIR /usr/local/src +COPY . . + +RUN apk add --no-cache \ + protobuf \ + protobuf-dev \ + protoc \ + make \ + go \ + libcap + +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest &&\ + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest &&\ + PATH="$PATH:$(go env GOPATH)/bin" make proto &&\ + go build -o octo-cdn ./cmd/octo-cdn &&\ + setcap cap_net_bind_service=+ep ./octo-cdn + +FROM alpine:latest + +WORKDIR /opt/octo-cdn + +RUN chown 1000:1000 /opt/octo-cdn + +USER 1000 + +COPY --from=builder /usr/local/src/octo-cdn . + +ENTRYPOINT ["./octo-cdn"] diff --git a/server/Makefile b/server/Makefile index 91347ee..be9f0f4 100644 --- a/server/Makefile +++ b/server/Makefile @@ -3,19 +3,40 @@ # (generating all would put them in one package and cause name clashes). PROTO_USED = proto/banner.proto proto/battle.proto proto/bighunt.proto proto/cageornament.proto proto/character.proto proto/characterboard.proto proto/characterviewer.proto proto/companion.proto proto/config.proto proto/consumableitem.proto proto/contentsstory.proto proto/costume.proto proto/data.proto proto/deck.proto proto/dokan.proto proto/explore.proto proto/friend.proto proto/gacha.proto proto/gameplay.proto proto/gift.proto proto/gimmick.proto proto/labyrinth.proto proto/loginbonus.proto proto/material.proto proto/mission.proto proto/movie.proto proto/navicutin.proto proto/omikuji.proto proto/notification.proto proto/parts.proto proto/portalcage.proto proto/pvp.proto proto/quest.proto proto/reward.proto proto/shop.proto proto/sidestoryquest.proto proto/tutorial.proto proto/user.proto proto/weapon.proto +EXE = +ifeq ($(OS),Windows_NT) + EXE = .exe +endif + 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/" build: - go build -o lunar-tear ./cmd/lunar-tear + go build -o lunar-tear$(EXE) ./cmd/lunar-tear + +build-cdn: + go build -o octo-cdn$(EXE) ./cmd/octo-cdn build-import: - go build -o import-snapshot ./cmd/import-snapshot + go build -o import-snapshot$(EXE) ./cmd/import-snapshot + +build-auth: + go build -o auth-server$(EXE) ./cmd/auth-server + +build-claim-account: + go build -o claim-account$(EXE) ./cmd/claim-account + +dev: + go run ./cmd/dev $(ARGS) migrate: +ifeq ($(OS),Windows_NT) + if not exist db mkdir db +else mkdir -p db - goose -dir migrations sqlite3 db/game.db up +endif + goose -dir migrations -allow-missing sqlite3 db/game.db up import: ifndef SNAPSHOT @@ -26,4 +47,4 @@ ifndef UUID endif go run ./cmd/import-snapshot --snapshot $(SNAPSHOT) --uuid $(UUID) -.PHONY: proto build build-import migrate import +.PHONY: proto build build-cdn build-auth build-import build-claim-account dev migrate import diff --git a/server/cmd/auth-server/handlers.go b/server/cmd/auth-server/handlers.go new file mode 100644 index 0000000..d71375b --- /dev/null +++ b/server/cmd/auth-server/handlers.go @@ -0,0 +1,212 @@ +package main + +import ( + "embed" + "encoding/base64" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "net/url" + "strconv" + "strings" +) + +//go:embed login.html +var loginFS embed.FS + +var loginTmpl = template.Must(template.ParseFS(loginFS, "login.html")) + +type Handlers struct { + store *AuthStore + tok *TokenService +} + +func NewHandlers(store *AuthStore, tok *TokenService) *Handlers { + return &Handlers{store: store, tok: tok} +} + +type loginPageData struct { + RedirectURI string + State string + Error string + Username string +} + +func isOAuthPath(path string) bool { + // Match /v{N}/dialog/oauth or /v{N}.{M}/dialog/oauth + parts := strings.Split(strings.TrimPrefix(path, "/"), "/") + if len(parts) != 3 { + return false + } + return strings.HasPrefix(parts[0], "v") && parts[1] == "dialog" && parts[2] == "oauth" +} + +func isMePath(path string) bool { + p := strings.TrimPrefix(path, "/") + if p == "me" { + return true + } + parts := strings.Split(p, "/") + return len(parts) == 2 && strings.HasPrefix(parts[0], "v") && parts[1] == "me" +} + +func (h *Handlers) HandleOAuth(w http.ResponseWriter, r *http.Request) { + if isMePath(r.URL.Path) { + h.HandleMe(w, r) + return + } + + if !isOAuthPath(r.URL.Path) { + http.NotFound(w, r) + return + } + + switch r.Method { + case http.MethodGet: + h.oauthGet(w, r) + case http.MethodPost: + h.oauthPost(w, r) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (h *Handlers) oauthGet(w http.ResponseWriter, r *http.Request) { + data := loginPageData{ + RedirectURI: r.URL.Query().Get("redirect_uri"), + State: r.URL.Query().Get("state"), + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := loginTmpl.Execute(w, data); err != nil { + log.Printf("render login page: %v", err) + } +} + +func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form", http.StatusBadRequest) + return + } + + username := strings.TrimSpace(r.FormValue("username")) + password := r.FormValue("password") + action := r.FormValue("action") + redirectURI := r.FormValue("redirect_uri") + state := r.FormValue("state") + + renderErr := func(msg string) { + data := loginPageData{ + RedirectURI: redirectURI, + State: state, + Error: msg, + Username: username, + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + if err := loginTmpl.Execute(w, data); err != nil { + log.Printf("render login page: %v", err) + } + } + + if username == "" || password == "" { + renderErr("Username and password are required.") + return + } + + var user AuthUser + var err error + + switch action { + case "register": + user, err = h.store.CreateUser(username, password) + if err == ErrUserExists { + renderErr("Username is already taken.") + return + } + if err != nil { + log.Printf("create user: %v", err) + renderErr("Server error. Try again.") + return + } + log.Printf("registered user %q (id=%d)", user.Username, user.ID) + + case "login": + user, err = h.store.VerifyUser(username, password) + if err == ErrInvalidCreds { + renderErr("Invalid username or password.") + return + } + if err != nil { + log.Printf("verify user: %v", err) + renderErr("Server error. Try again.") + return + } + log.Printf("authenticated user %q (id=%d)", user.Username, user.ID) + + default: + renderErr("Invalid action.") + return + } + + token, err := h.tok.Generate(user) + if err != nil { + log.Printf("generate token: %v", err) + renderErr("Server error. Try again.") + return + } + + payload := fmt.Sprintf(`{"user_id":"%d"}`, user.ID) + b64 := base64.StdEncoding.EncodeToString([]byte(payload)) + + fragment := url.Values{} + fragment.Set("access_token", token) + fragment.Set("token_type", "bearer") + fragment.Set("expires_in", strconv.FormatInt(int64(tokenTTL.Seconds()), 10)) + fragment.Set("signed_request", "0."+b64) + if state != "" { + fragment.Set("state", state) + } + + target := redirectURI + "?" + fragment.Encode() + log.Printf("redirecting to %s", target) + http.Redirect(w, r, target, http.StatusFound) +} + +func (h *Handlers) HandleCheckUsername(w http.ResponseWriter, r *http.Request) { + username := strings.TrimSpace(r.URL.Query().Get("username")) + w.Header().Set("Content-Type", "application/json") + if username == "" { + json.NewEncoder(w).Encode(map[string]bool{"exists": false}) + return + } + json.NewEncoder(w).Encode(map[string]bool{"exists": h.store.UserExists(username)}) +} + +func (h *Handlers) HandleMe(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("access_token") + if token == "" { + auth := r.Header.Get("Authorization") + if strings.HasPrefix(auth, "Bearer ") { + token = auth[7:] + } + } + + if token == "" { + http.Error(w, `{"error":{"message":"missing access_token","type":"OAuthException","code":190}}`, http.StatusUnauthorized) + return + } + + claims, err := h.tok.Validate(token) + if err != nil { + http.Error(w, fmt.Sprintf(`{"error":{"message":"%s","type":"OAuthException","code":190}}`, err), http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "id": strconv.FormatInt(claims.Sub, 10), + "name": claims.Name, + }) +} diff --git a/server/cmd/auth-server/login.html b/server/cmd/auth-server/login.html new file mode 100644 index 0000000..362ea9c --- /dev/null +++ b/server/cmd/auth-server/login.html @@ -0,0 +1,199 @@ + + + + + +Lunar Tear – Login + + + +
+

Lunar Tear

+
Authentication
+ + {{if .Error}} +
{{.Error}}
+ {{end}} + + + + + + + + + + +
+ + +
+
+ + + diff --git a/server/cmd/auth-server/main.go b/server/cmd/auth-server/main.go new file mode 100644 index 0000000..feeaca3 --- /dev/null +++ b/server/cmd/auth-server/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "crypto/rand" + "database/sql" + "encoding/hex" + "flag" + "log" + "net/http" + + _ "modernc.org/sqlite" +) + +func main() { + listen := flag.String("listen", "0.0.0.0:3000", "HTTP listen address (host:port)") + dbPath := flag.String("db", "db/auth.db", "SQLite database path for auth users") + secret := flag.String("secret", "", "HMAC secret for tokens (auto-generated if empty)") + flag.Parse() + + hmacSecret := []byte(*secret) + if len(hmacSecret) == 0 { + hmacSecret = make([]byte, 32) + if _, err := rand.Read(hmacSecret); err != nil { + log.Fatalf("generate secret: %v", err) + } + log.Printf("generated HMAC secret: %s", hex.EncodeToString(hmacSecret)) + log.Printf("pass --secret=%s to reuse across restarts", hex.EncodeToString(hmacSecret)) + } + + db, err := sql.Open("sqlite", *dbPath) + if err != nil { + log.Fatalf("open database: %v", err) + } + defer db.Close() + + store, err := NewAuthStore(db) + if err != nil { + log.Fatalf("init auth store: %v", err) + } + + tok := NewTokenService(hmacSecret) + h := NewHandlers(store, tok) + + mux := http.NewServeMux() + mux.HandleFunc("/", h.HandleOAuth) + mux.HandleFunc("/me", h.HandleMe) + mux.HandleFunc("/check-username", h.HandleCheckUsername) + + log.Printf("auth server listening on %s", *listen) + if err := http.ListenAndServe(*listen, mux); err != nil { + log.Fatalf("listen: %v", err) + } +} diff --git a/server/cmd/auth-server/store.go b/server/cmd/auth-server/store.go new file mode 100644 index 0000000..960255c --- /dev/null +++ b/server/cmd/auth-server/store.go @@ -0,0 +1,103 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "time" + + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrUserExists = errors.New("username already taken") + ErrInvalidCreds = errors.New("invalid username or password") +) + +type AuthUser struct { + ID int64 + Username string +} + +type AuthStore struct { + db *sql.DB +} + +func NewAuthStore(db *sql.DB) (*AuthStore, error) { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS auth_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password BLOB NOT NULL, + created_at TEXT NOT NULL + ) + `) + if err != nil { + return nil, fmt.Errorf("create auth_users table: %w", err) + } + return &AuthStore{db: db}, nil +} + +func (s *AuthStore) CreateUser(username, password string) (AuthUser, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return AuthUser{}, fmt.Errorf("hash password: %w", err) + } + + res, err := s.db.Exec( + `INSERT INTO auth_users (username, password, created_at) VALUES (?, ?, ?)`, + username, hash, time.Now().UTC().Format(time.RFC3339), + ) + if err != nil { + if isUniqueViolation(err) { + return AuthUser{}, ErrUserExists + } + return AuthUser{}, fmt.Errorf("insert user: %w", err) + } + + id, _ := res.LastInsertId() + return AuthUser{ID: id, Username: username}, nil +} + +func (s *AuthStore) VerifyUser(username, password string) (AuthUser, error) { + var id int64 + var hash []byte + err := s.db.QueryRow( + `SELECT id, password FROM auth_users WHERE username = ?`, username, + ).Scan(&id, &hash) + if err != nil { + return AuthUser{}, ErrInvalidCreds + } + + if err := bcrypt.CompareHashAndPassword(hash, []byte(password)); err != nil { + return AuthUser{}, ErrInvalidCreds + } + + return AuthUser{ID: id, Username: username}, nil +} + +func (s *AuthStore) UserExists(username string) bool { + var n int + err := s.db.QueryRow(`SELECT 1 FROM auth_users WHERE username = ?`, username).Scan(&n) + return err == nil +} + +func isUniqueViolation(err error) bool { + return err != nil && (errors.Is(err, sql.ErrNoRows) || + // modernc.org/sqlite returns error strings containing "UNIQUE constraint failed" + fmt.Sprintf("%v", err) == fmt.Sprintf("%v", err) && + contains(err.Error(), "UNIQUE constraint failed")) +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && searchStr(s, substr) +} + +func searchStr(s, sub string) bool { + for i := 0; i <= len(s)-len(sub); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} diff --git a/server/cmd/auth-server/token.go b/server/cmd/auth-server/token.go new file mode 100644 index 0000000..30ded2a --- /dev/null +++ b/server/cmd/auth-server/token.go @@ -0,0 +1,102 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "time" +) + +const tokenTTL = 24 * time.Hour + +var ( + ErrTokenInvalid = errors.New("invalid token") + ErrTokenExpired = errors.New("token expired") +) + +type TokenClaims struct { + Sub int64 `json:"sub"` + Name string `json:"name"` + Iat int64 `json:"iat"` + Exp int64 `json:"exp"` +} + +type TokenService struct { + secret []byte +} + +func NewTokenService(secret []byte) *TokenService { + return &TokenService{secret: secret} +} + +func (t *TokenService) Generate(user AuthUser) (string, error) { + now := time.Now().Unix() + claims := TokenClaims{ + Sub: user.ID, + Name: user.Username, + Iat: now, + Exp: now + int64(tokenTTL.Seconds()), + } + + payload, err := json.Marshal(claims) + if err != nil { + return "", fmt.Errorf("marshal claims: %w", err) + } + + enc := base64.RawURLEncoding + payloadB64 := enc.EncodeToString(payload) + + mac := hmac.New(sha256.New, t.secret) + mac.Write(payload) + sig := enc.EncodeToString(mac.Sum(nil)) + + return payloadB64 + "." + sig, nil +} + +func (t *TokenService) Validate(token string) (TokenClaims, error) { + dot := -1 + for i := range token { + if token[i] == '.' { + dot = i + break + } + } + if dot < 0 { + return TokenClaims{}, ErrTokenInvalid + } + + payloadB64 := token[:dot] + sigB64 := token[dot+1:] + + enc := base64.RawURLEncoding + + payload, err := enc.DecodeString(payloadB64) + if err != nil { + return TokenClaims{}, ErrTokenInvalid + } + + sig, err := enc.DecodeString(sigB64) + if err != nil { + return TokenClaims{}, ErrTokenInvalid + } + + mac := hmac.New(sha256.New, t.secret) + mac.Write(payload) + if !hmac.Equal(mac.Sum(nil), sig) { + return TokenClaims{}, ErrTokenInvalid + } + + var claims TokenClaims + if err := json.Unmarshal(payload, &claims); err != nil { + return TokenClaims{}, ErrTokenInvalid + } + + if time.Now().Unix() > claims.Exp { + return TokenClaims{}, ErrTokenExpired + } + + return claims, nil +} diff --git a/server/cmd/claim-account/main.go b/server/cmd/claim-account/main.go new file mode 100644 index 0000000..f93a32f --- /dev/null +++ b/server/cmd/claim-account/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + + "lunar-tear/server/internal/database" +) + +var 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", +} + +func main() { + dbPath := flag.String("db", "db/game.db", "SQLite database path") + name := flag.String("name", "", "In-game player name to look up in user_profile (required)") + flag.Parse() + + if *name == "" { + log.Fatal("--name flag is required") + } + + db, err := database.Open(*dbPath) + if err != nil { + log.Fatalf("open database: %v", err) + } + defer db.Close() + + var targetId int64 + err = db.QueryRow(`SELECT user_id FROM user_profile WHERE name = ?`, *name).Scan(&targetId) + if err == sql.ErrNoRows { + log.Fatalf("no user found with name %q", *name) + } + if err != nil { + log.Fatalf("query user_profile: %v", err) + } + + var targetUuid string + err = db.QueryRow(`SELECT uuid FROM users WHERE user_id = ?`, targetId).Scan(&targetUuid) + if err != nil { + log.Fatalf("query target uuid: %v", err) + } + + var latestId int64 + var latestUuid string + err = db.QueryRow(`SELECT user_id, uuid FROM users ORDER BY user_id DESC LIMIT 1`).Scan(&latestId, &latestUuid) + if err != nil { + log.Fatalf("query latest user: %v", err) + } + + if targetId == latestId { + log.Printf("user %q (id=%d) is already the most recent user, nothing to do", *name, targetId) + return + } + + log.Printf("target: id=%d uuid=%s (name=%q)", targetId, targetUuid, *name) + log.Printf("latest: id=%d uuid=%s (will be deleted)", latestId, latestUuid) + + tx, err := db.Begin() + if err != nil { + log.Fatalf("begin transaction: %v", err) + } + defer tx.Rollback() + + for _, t := range childTables { + if _, err := tx.Exec(fmt.Sprintf(`DELETE FROM %s WHERE user_id = ?`, t), latestId); err != nil { + log.Fatalf("delete from %s: %v", t, err) + } + } + if _, err := tx.Exec(`DELETE FROM users WHERE user_id = ?`, latestId); err != nil { + log.Fatalf("delete latest user: %v", err) + } + + if _, err := tx.Exec(`UPDATE users SET uuid = ? WHERE user_id = ?`, latestUuid, targetId); err != nil { + log.Fatalf("update target uuid: %v", err) + } + + if _, err := tx.Exec(`DELETE FROM sessions WHERE user_id = ?`, targetId); err != nil { + log.Fatalf("delete stale sessions: %v", err) + } + + if err := tx.Commit(); err != nil { + log.Fatalf("commit: %v", err) + } + + fmt.Printf("claimed account:\n") + fmt.Printf(" user %d (%s): uuid changed %s -> %s\n", targetId, *name, targetUuid, latestUuid) + fmt.Printf(" user %d: deleted\n", latestId) +} diff --git a/server/cmd/dev/color_unix.go b/server/cmd/dev/color_unix.go new file mode 100644 index 0000000..20eb45b --- /dev/null +++ b/server/cmd/dev/color_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package main + +import ( + "os" + + "golang.org/x/term" +) + +func colorSupported() bool { + return term.IsTerminal(int(os.Stdout.Fd())) +} diff --git a/server/cmd/dev/color_windows.go b/server/cmd/dev/color_windows.go new file mode 100644 index 0000000..671e10c --- /dev/null +++ b/server/cmd/dev/color_windows.go @@ -0,0 +1,22 @@ +//go:build windows + +package main + +import ( + "os" + + "golang.org/x/sys/windows" + "golang.org/x/term" +) + +func colorSupported() bool { + fd := os.Stdout.Fd() + if !term.IsTerminal(int(fd)) { + return false + } + var mode uint32 + if windows.GetConsoleMode(windows.Handle(fd), &mode) != nil { + return false + } + return windows.SetConsoleMode(windows.Handle(fd), mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil +} diff --git a/server/cmd/dev/main.go b/server/cmd/dev/main.go new file mode 100644 index 0000000..707d5b4 --- /dev/null +++ b/server/cmd/dev/main.go @@ -0,0 +1,147 @@ +package main + +import ( + "bufio" + "context" + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "os/signal" + "sync" + "syscall" +) + +var ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorCyan = "\033[36m" +) + +type service struct { + label string + color string + cmd *exec.Cmd +} + +func main() { + // auth-server flags + authListen := flag.String("auth.listen", "0.0.0.0:3000", "auth-server listen address (host:port)") + authDB := flag.String("auth.db", "db/auth.db", "auth-server SQLite database path") + + // octo-cdn flags + cdnListen := flag.String("cdn.listen", "0.0.0.0:8080", "octo-cdn local bind address") + cdnPublicAddr := flag.String("cdn.public-addr", "10.0.2.2:8080", "octo-cdn externally-reachable address") + + // lunar-tear (grpc) flags + grpcListen := flag.String("grpc.listen", "0.0.0.0:8003", "lunar-tear gRPC listen address (host:port)") + grpcPublicAddr := flag.String("grpc.public-addr", "10.0.2.2:8003", "lunar-tear externally-reachable address") + grpcOctoURL := flag.String("grpc.octo-url", "", "Octo CDN base URL passed to lunar-tear (default: derived from cdn.public-addr)") + grpcAuthURL := flag.String("grpc.auth-url", "", "auth server base URL passed to lunar-tear (default: derived from auth.listen)") + + noColor := flag.Bool("no-color", false, "disable colored output") + flag.Parse() + + if *grpcOctoURL == "" { + *grpcOctoURL = fmt.Sprintf("http://%s", *cdnPublicAddr) + } + if *grpcAuthURL == "" { + *grpcAuthURL = fmt.Sprintf("http://%s", *authListen) + } + + if *noColor || !colorSupported() { + colorReset = "" + colorRed = "" + colorGreen = "" + colorYellow = "" + colorCyan = "" + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + services := []service{ + { + label: "auth", + color: colorGreen, + cmd: exec.CommandContext(ctx, "go", "run", "./cmd/auth-server", + "--listen", *authListen, + "--db", *authDB, + ), + }, + { + label: "cdn", + color: colorCyan, + cmd: exec.CommandContext(ctx, "go", "run", "./cmd/octo-cdn", + "--listen", *cdnListen, + "--public-addr", *cdnPublicAddr, + ), + }, + { + label: "grpc", + color: colorYellow, + cmd: exec.CommandContext(ctx, "go", "run", "./cmd/lunar-tear", + "--listen", *grpcListen, + "--public-addr", *grpcPublicAddr, + "--octo-url", *grpcOctoURL, + "--auth-url", *grpcAuthURL, + ), + }, + } + + var wg sync.WaitGroup + errCh := make(chan error, len(services)) + + for i := range services { + svc := &services[i] + stdout, err := svc.cmd.StdoutPipe() + if err != nil { + log.Fatalf("[%s] stdout pipe: %v", svc.label, err) + } + stderr, err := svc.cmd.StderrPipe() + if err != nil { + log.Fatalf("[%s] stderr pipe: %v", svc.label, err) + } + + if err := svc.cmd.Start(); err != nil { + log.Fatalf("[%s] start: %v", svc.label, err) + } + + prefix := fmt.Sprintf("%s[%s]%s ", svc.color, svc.label, colorReset) + wg.Add(2) + go prefixLines(&wg, prefix, stdout) + go prefixLines(&wg, prefix, stderr) + + wg.Add(1) + go func(s *service) { + defer wg.Done() + if err := s.cmd.Wait(); err != nil { + errCh <- fmt.Errorf("[%s] %w", s.label, err) + } + }(svc) + + log.Printf("%s%s started (pid %d)%s", svc.color, svc.label, svc.cmd.Process.Pid, colorReset) + } + + select { + case <-ctx.Done(): + log.Println("shutting down all services...") + case err := <-errCh: + log.Printf("%s%s%s", colorRed, err, colorReset) + stop() + } + + wg.Wait() +} + +func prefixLines(wg *sync.WaitGroup, prefix string, r io.Reader) { + defer wg.Done() + scanner := bufio.NewScanner(r) + for scanner.Scan() { + fmt.Printf("%s%s\n", prefix, scanner.Text()) + } +} diff --git a/server/cmd/lunar-tear/grpc.go b/server/cmd/lunar-tear/grpc.go index 7abd6a8..876f2a9 100644 --- a/server/cmd/lunar-tear/grpc.go +++ b/server/cmd/lunar-tear/grpc.go @@ -1,24 +1,20 @@ package main import ( - "context" - "fmt" "log" "net" + "strconv" pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gacha" - "lunar-tear/server/internal/gametime" + "lunar-tear/server/internal/interceptor" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/service" "lunar-tear/server/internal/store" "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" "google.golang.org/grpc/reflection" - "google.golang.org/grpc/status" ) type loggingListener struct { @@ -36,9 +32,10 @@ func (l loggingListener) Accept() (net.Conn, error) { } func startGRPC( - host string, - grpcPort int, + listenAddr string, + publicAddr string, octoURL string, + authURL string, userStore interface { store.UserRepository store.SessionRepository @@ -64,23 +61,23 @@ func startGRPC( gameConfig *masterdata.GameConfig, sideStoryCatalog *masterdata.SideStoryCatalog, bigHuntCatalog *masterdata.BigHuntCatalog, -) { - addr := fmt.Sprintf(":%d", grpcPort) - lis, err := net.Listen("tcp", addr) +) *grpc.Server { + lis, err := net.Listen("tcp", listenAddr) if err != nil { - log.Fatalf("failed to listen on %s: %v", addr, err) + log.Fatalf("failed to listen on %s: %v", listenAddr, err) } lis = loggingListener{Listener: lis} + diffInterceptor := interceptor.NewDiffInterceptor(userStore, userStore) grpcServer := grpc.NewServer( - grpc.ChainUnaryInterceptor(loggingInterceptor, timeSyncInterceptor), - grpc.UnknownServiceHandler(loggingUnknownService), + grpc.ChainUnaryInterceptor(interceptor.Platform, interceptor.Logging, diffInterceptor, interceptor.TimeSync), + grpc.UnknownServiceHandler(interceptor.UnknownService), ) registerServices(grpcServer, - host, - grpcPort, + publicAddr, octoURL, + authURL, userStore, questEngine, gachaHandler, @@ -107,19 +104,22 @@ func startGRPC( reflection.Register(grpcServer) - log.Printf("gRPC server listening on %s", addr) - log.Printf("client host address: %s:%d", host, grpcPort) + log.Printf("gRPC server listening on %s", lis.Addr()) + log.Printf("public address: %s", publicAddr) - if err := grpcServer.Serve(lis); err != nil { - log.Fatalf("failed to serve: %v", err) - } + go func() { + if err := grpcServer.Serve(lis); err != nil { + log.Printf("gRPC server stopped: %v", err) + } + }() + return grpcServer } func registerServices( srv *grpc.Server, - host string, - grpcPort int, + publicAddr string, octoURL string, + authURL string, userStore interface { store.UserRepository store.SessionRepository @@ -146,10 +146,13 @@ func registerServices( sideStoryCatalog *masterdata.SideStoryCatalog, bigHuntCatalog *masterdata.BigHuntCatalog, ) { + pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr) + pubPort, _ := strconv.Atoi(pubPortStr) + pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(gachaEntries)) - pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore)) + pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL)) pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore)) - pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(host, int32(grpcPort), octoURL)) + pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL)) pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore)) pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine)) pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, gachaEntries, gachaHandler)) @@ -184,39 +187,3 @@ func registerServices( pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine)) pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter)) } - -func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - log.Printf(">>> %s", info.FullMethod) - resp, err := handler(ctx, req) - if err != nil { - log.Printf("<<< %s ERROR: %v", info.FullMethod, err) - } else { - log.Printf("<<< %s OK", info.FullMethod) - } - return resp, err -} - -func timeSyncInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - resp, err := handler(ctx, req) - switch info.FullMethod { - case "/apb.api.user.UserService/Auth", - "/apb.api.user.UserService/RegisterUser", - "/apb.api.user.UserService/TransferUser": - default: - grpc.SetTrailer(ctx, metadata.Pairs( - "x-apb-response-datetime", fmt.Sprintf("%d", gametime.NowMillis()), - )) - } - return resp, err -} - -func loggingUnknownService(_ any, stream grpc.ServerStream) error { - fullMethod, ok := grpc.MethodFromServerStream(stream) - if !ok { - fullMethod = "" - } - log.Printf(">>> %s", fullMethod) - err := status.Errorf(codes.Unimplemented, "unknown service or method %s", fullMethod) - log.Printf("<<< %s ERROR: %v", fullMethod, err) - return err -} diff --git a/server/cmd/lunar-tear/http.go b/server/cmd/lunar-tear/http.go deleted file mode 100644 index c6e9ba6..0000000 --- a/server/cmd/lunar-tear/http.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - - "lunar-tear/server/internal/service" - - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" -) - -func startHTTP(port int, resourcesBaseURL string) { - octoServer := service.NewOctoHTTPServer(resourcesBaseURL) - h2s := &http2.Server{} - octoHandler := h2c.NewHandler(octoServer.Handler(), h2s) - log.Printf("Octo HTTP server listening on :%d (HTTP/1.1 + h2c)", port) - srv := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: octoHandler} - http2.ConfigureServer(srv, h2s) - if err := srv.ListenAndServe(); err != nil { - log.Fatalf("HTTP server on %d failed: %v", port, err) - } -} diff --git a/server/cmd/lunar-tear/main.go b/server/cmd/lunar-tear/main.go index 5d20bd4..69f1953 100644 --- a/server/cmd/lunar-tear/main.go +++ b/server/cmd/lunar-tear/main.go @@ -1,37 +1,41 @@ package main import ( + "context" "flag" "log" - "strconv" - "strings" + "os" + "os/signal" + "syscall" "lunar-tear/server/internal/database" "lunar-tear/server/internal/gacha" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/masterdata/memorydb" "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/store/sqlite" ) func main() { - httpPort := flag.Int("http-port", 8080, "HTTP server port (Octo API)") - grpcPort := flag.Int("grpc-port", 443, "gRPC server port") - host := flag.String("host", "127.0.0.1", "hostname the client will connect to") + listen := flag.String("listen", "0.0.0.0:443", "gRPC listen address (host:port)") + publicAddr := flag.String("public-addr", "127.0.0.1:443", "externally-reachable host:port advertised to clients") dbPath := flag.String("db", "db/game.db", "SQLite database path") + octoURL := flag.String("octo-url", "", "Octo CDN base URL the client will use for assets (e.g. http://10.0.2.2:8080)") + authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)") flag.Parse() - octoURL := "http://" + *host + ":" + strconv.Itoa(*httpPort) - prefix := octoURL + "/" - padLen := 43 - len(prefix) - resourcesBaseURL := "" - if padLen < 1 { - log.Printf("[config] host:port too long for 43-char resource URL; list.bin will be served unchanged") - } else { - resourcesBaseURL = prefix + strings.Repeat("r", padLen) + if *octoURL == "" { + log.Fatalf("--octo-url is required (e.g. http://10.0.2.2:8080)") } - go startHTTP(*httpPort, resourcesBaseURL) + if err := memorydb.Init("assets/release/20240404193219.bin.e"); err != nil { + log.Fatalf("load master data: %v", err) + } + log.Printf("master data loaded (%d tables)", memorydb.TableCount()) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() db, err := database.Open(*dbPath) if err != nil { @@ -164,10 +168,11 @@ func main() { sideStoryCatalog := masterdata.LoadSideStoryCatalog() bigHuntCatalog := masterdata.LoadBigHuntCatalog() - startGRPC( - *host, - *grpcPort, - octoURL, + grpcServer := startGRPC( + *listen, + *publicAddr, + *octoURL, + *authURL, userStore, questHandler, gachaHandler, @@ -191,4 +196,12 @@ func main() { sideStoryCatalog, bigHuntCatalog, ) + + <-ctx.Done() + log.Println("shutting down...") + + grpcServer.GracefulStop() + database.Checkpoint(db) + + log.Println("shutdown complete") } diff --git a/server/cmd/octo-cdn/main.go b/server/cmd/octo-cdn/main.go new file mode 100644 index 0000000..112d509 --- /dev/null +++ b/server/cmd/octo-cdn/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + + "lunar-tear/server/internal/service" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func main() { + listen := flag.String("listen", "0.0.0.0:8080", "local bind address (host:port)") + publicAddr := flag.String("public-addr", "127.0.0.1:8080", "externally-reachable host:port used for list.bin URL rewriting") + assetsDir := flag.String("assets-dir", ".", "root directory containing the assets/ tree") + flag.Parse() + + // Build resourcesBaseURL from public-addr (must be exactly 43 chars to fit in list.bin protobuf). + prefix := "http://" + *publicAddr + "/" + padLen := 43 - len(prefix) + resourcesBaseURL := "" + if padLen < 1 { + log.Printf("[config] public-addr too long for 43-char resource URL; list.bin will be served unchanged") + } else { + resourcesBaseURL = prefix + strings.Repeat("r", padLen) + } + + octoServer := service.NewOctoHTTPServer(resourcesBaseURL, *assetsDir) + h2s := &http2.Server{} + handler := h2c.NewHandler(octoServer.Handler(), h2s) + + srv := &http.Server{ + Addr: *listen, + Handler: handler, + } + http2.ConfigureServer(srv, h2s) + + // Resolve actual listen address for logging (useful when port is 0). + lis, err := net.Listen("tcp", *listen) + if err != nil { + log.Fatalf("failed to listen on %s: %v", *listen, err) + } + log.Printf("Octo CDN listening on %s (HTTP/1.1 + h2c)", lis.Addr()) + log.Printf("public address: %s", *publicAddr) + if *assetsDir != "." { + log.Printf("assets directory: %s", *assetsDir) + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + go func() { + if err := srv.Serve(lis); err != nil && err != http.ErrServerClosed { + fmt.Fprintf(os.Stderr, "HTTP server error: %v\n", err) + os.Exit(1) + } + }() + + <-ctx.Done() + log.Println("shutting down...") + srv.Shutdown(context.Background()) + log.Println("shutdown complete") +} diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml index d3b62cd..797528c 100644 --- a/server/docker-compose.yaml +++ b/server/docker-compose.yaml @@ -1,15 +1,41 @@ +name: lunar-tear + services: server: build: . image: kretts/lunar-tear:latest environment: - LUNAR_HOST: 127.0.0.1 - LUNAR_HTTP_PORT: 8080 - LUNAR_GRPC_PORT: 8003 + LUNAR_LISTEN: 0.0.0.0:8003 + LUNAR_PUBLIC_ADDR: 127.0.0.1:8003 + LUNAR_OCTO_URL: http://cdn:8080 + LUNAR_AUTH_URL: http://auth:3000 volumes: - - ./assets:/opt/lunar-tear/assets - ./db:/opt/lunar-tear/db + - ./assets:/opt/lunar-tear/assets ports: - 8003:8003 + depends_on: + - cdn + - auth + + cdn: + build: + context: . + dockerfile: Dockerfile.cdn + image: kretts/octo-cdn:latest + command: ["--listen", "0.0.0.0:8080", "--public-addr", "10.0.2.2:8080"] + volumes: + - ./assets:/opt/octo-cdn/assets + ports: - 8080:8080 + auth: + build: + context: . + dockerfile: Dockerfile.auth + image: kretts/auth-server:latest + command: ["--listen", "0.0.0.0:3000", "--db", "db/auth.db"] + volumes: + - ./db:/opt/auth-server/db + ports: + - 3000:3000 diff --git a/server/entrypoint.sh b/server/entrypoint.sh index 1906180..df7ee90 100755 --- a/server/entrypoint.sh +++ b/server/entrypoint.sh @@ -4,4 +4,13 @@ set -e mkdir -p db goose -dir migrations sqlite3 db/game.db up -exec ./lunar-tear --host "${LUNAR_HOST}" --http-port "${LUNAR_HTTP_PORT}" --grpc-port "${LUNAR_GRPC_PORT:-443}" +AUTH_FLAG="" +if [ -n "${LUNAR_AUTH_URL}" ]; then + AUTH_FLAG="--auth-url ${LUNAR_AUTH_URL}" +fi + +exec ./lunar-tear \ + --listen "${LUNAR_LISTEN:-0.0.0.0:443}" \ + --public-addr "${LUNAR_PUBLIC_ADDR}" \ + --octo-url "${LUNAR_OCTO_URL}" \ + ${AUTH_FLAG} diff --git a/server/go.mod b/server/go.mod index 2fdd93a..71c5ac6 100644 --- a/server/go.mod +++ b/server/go.mod @@ -4,30 +4,28 @@ go 1.25.0 require ( github.com/google/uuid v1.6.0 + github.com/pierrec/lz4/v4 v4.1.26 github.com/vmihailenco/msgpack/v5 v5.4.1 - golang.org/x/net v0.50.0 + golang.org/x/crypto v0.50.0 + golang.org/x/net v0.52.0 + golang.org/x/sys v0.43.0 + golang.org/x/term v0.42.0 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 + modernc.org/sqlite v1.48.2 ) require ( 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 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 + golang.org/x/text v0.36.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 ) diff --git a/server/go.sum b/server/go.sum index e52cbf7..17f46ef 100644 --- a/server/go.sum +++ b/server/go.sum @@ -12,22 +12,22 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= 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= @@ -46,17 +46,23 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= 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/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= @@ -67,11 +73,31 @@ google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBN 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/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= 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/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/server/internal/database/database.go b/server/internal/database/database.go index 7649895..06330fd 100644 --- a/server/internal/database/database.go +++ b/server/internal/database/database.go @@ -3,6 +3,7 @@ package database import ( "database/sql" "fmt" + "log" "os" "path/filepath" @@ -40,3 +41,9 @@ func Open(path string) (*sql.DB, error) { return db, nil } + +func Checkpoint(db *sql.DB) { + if _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { + log.Printf("WAL checkpoint: %v", err) + } +} diff --git a/server/internal/interceptor/diff.go b/server/internal/interceptor/diff.go new file mode 100644 index 0000000..21be977 --- /dev/null +++ b/server/internal/interceptor/diff.go @@ -0,0 +1,130 @@ +package interceptor + +import ( + "context" + "log" + "reflect" + "sort" + "strings" + "sync" + + pb "lunar-tear/server/gen/proto" + "lunar-tear/server/internal/service" + "lunar-tear/server/internal/store" + "lunar-tear/server/internal/userdata" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +type diffUserDataGetter interface { + GetDiffUserData() map[string]*pb.DiffData +} + +var ( + diffFieldCache sync.Map // map[reflect.Type]bool + diffDataMapTyp = reflect.TypeFor[map[string]*pb.DiffData]() +) + +func hasDiffField(resp any) bool { + t := reflect.TypeOf(resp) + if cached, ok := diffFieldCache.Load(t); ok { + return cached.(bool) + } + elem := t + if elem.Kind() == reflect.Pointer { + elem = elem.Elem() + } + f, ok := elem.FieldByName("DiffUserData") + result := ok && f.Type == diffDataMapTyp + diffFieldCache.Store(t, result) + return result +} + +func NewDiffInterceptor( + users store.UserRepository, + sessions store.SessionRepository, +) grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req any, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (any, error) { + if skipDiffForMethod(info.FullMethod) { + return handler(ctx, req) + } + + userId := service.CurrentUserId(ctx, users, sessions) + if userId == 0 { + return handler(ctx, req) + } + + before, err := users.LoadUser(userId) + if err != nil { + return handler(ctx, req) + } + + resp, handlerErr := handler(ctx, req) + if handlerErr != nil || resp == nil { + return resp, handlerErr + } + + if !hasDiffField(resp) { + return resp, nil + } + + if getter, ok := resp.(diffUserDataGetter); ok { + if existing := getter.GetDiffUserData(); len(existing) > 0 { + setUpdateNamesTrailer(ctx, existing) + return resp, nil + } + } + + after, err := users.LoadUser(userId) + if err != nil { + return resp, nil + } + + changed := userdata.ChangedTables(&before, &after) + if len(changed) == 0 { + return resp, nil + } + + diff := userdata.ComputeDelta(&before, &after, changed) + reflect.ValueOf(resp).Elem().FieldByName("DiffUserData").Set(reflect.ValueOf(diff)) + setUpdateNamesTrailer(ctx, diff) + + return resp, nil + } +} + +func skipDiffForMethod(method string) bool { + switch method { + case "/apb.api.user.UserService/Auth", + "/apb.api.user.UserService/RegisterUser", + "/apb.api.user.UserService/TransferUser", + "/apb.api.user.UserService/TransferUserByFacebook", + "/apb.api.config.ConfigService/GetConfig", + "/apb.api.data.DataService/GetLatestMasterDataVersion", + "/apb.api.data.DataService/GetUserDataNameV2", + "/apb.api.data.DataService/GetUserData": + return true + } + return false +} + +func setUpdateNamesTrailer(ctx context.Context, diff map[string]*pb.DiffData) { + if len(diff) == 0 { + return + } + keys := make([]string, 0, len(diff)) + for key := range diff { + keys = append(keys, key) + } + sort.Strings(keys) + value := strings.Join(keys, ",") + if err := grpc.SetTrailer(ctx, metadata.Pairs("x-apb-update-user-data-names", value)); err != nil { + log.Printf("[DiffInterceptor] failed to set trailer: %v", err) + } +} diff --git a/server/internal/interceptor/logging.go b/server/internal/interceptor/logging.go new file mode 100644 index 0000000..39dd6dd --- /dev/null +++ b/server/internal/interceptor/logging.go @@ -0,0 +1,32 @@ +package interceptor + +import ( + "context" + "log" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func Logging(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + log.Printf(">>> %s", info.FullMethod) + resp, err := handler(ctx, req) + if err != nil { + log.Printf("<<< %s ERROR: %v", info.FullMethod, err) + } else { + log.Printf("<<< %s OK", info.FullMethod) + } + return resp, err +} + +func UnknownService(_ any, stream grpc.ServerStream) error { + fullMethod, ok := grpc.MethodFromServerStream(stream) + if !ok { + fullMethod = "" + } + log.Printf(">>> %s", fullMethod) + err := status.Errorf(codes.Unimplemented, "unknown service or method %s", fullMethod) + log.Printf("<<< %s ERROR: %v", fullMethod, err) + return err +} diff --git a/server/internal/interceptor/platform.go b/server/internal/interceptor/platform.go new file mode 100644 index 0000000..6756e2f --- /dev/null +++ b/server/internal/interceptor/platform.go @@ -0,0 +1,15 @@ +package interceptor + +import ( + "context" + + "lunar-tear/server/internal/model" + + "google.golang.org/grpc" +) + +func Platform(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + p := model.ClientPlatformFromHeaders(ctx) + ctx = model.NewContextWithPlatform(ctx, p) + return handler(ctx, req) +} diff --git a/server/internal/interceptor/timesync.go b/server/internal/interceptor/timesync.go new file mode 100644 index 0000000..9f2b978 --- /dev/null +++ b/server/internal/interceptor/timesync.go @@ -0,0 +1,25 @@ +package interceptor + +import ( + "context" + "fmt" + + "lunar-tear/server/internal/gametime" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func TimeSync(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + resp, err := handler(ctx, req) + switch info.FullMethod { + case "/apb.api.user.UserService/Auth", + "/apb.api.user.UserService/RegisterUser", + "/apb.api.user.UserService/TransferUser": + default: + grpc.SetTrailer(ctx, metadata.Pairs( + "x-apb-response-datetime", fmt.Sprintf("%d", gametime.NowMillis()), + )) + } + return resp, err +} diff --git a/server/internal/masterdata/bighunt.go b/server/internal/masterdata/bighunt.go index 96100e2..6d24c4e 100644 --- a/server/internal/masterdata/bighunt.go +++ b/server/internal/masterdata/bighunt.go @@ -8,15 +8,6 @@ import ( "lunar-tear/server/internal/utils" ) -type bigHuntBossQuestRow struct { - BigHuntBossQuestId int32 `json:"BigHuntBossQuestId"` - BigHuntBossId int32 `json:"BigHuntBossId"` - BigHuntQuestGroupId int32 `json:"BigHuntQuestGroupId"` - BigHuntBossQuestScoreCoefficientId int32 `json:"BigHuntBossQuestScoreCoefficientId"` - BigHuntScoreRewardGroupScheduleId int32 `json:"BigHuntScoreRewardGroupScheduleId"` - DailyChallengeCount int32 `json:"DailyChallengeCount"` -} - type BigHuntBossQuestRow struct { BigHuntBossQuestId int32 BigHuntBossId int32 @@ -25,97 +16,39 @@ type BigHuntBossQuestRow struct { DailyChallengeCount int32 } -type bigHuntQuestRow struct { - BigHuntQuestId int32 `json:"BigHuntQuestId"` - QuestId int32 `json:"QuestId"` - BigHuntQuestScoreCoefficientId int32 `json:"BigHuntQuestScoreCoefficientId"` -} - type BigHuntQuestRow struct { BigHuntQuestId int32 QuestId int32 BigHuntQuestScoreCoefficientId int32 } -type bigHuntQuestScoreCoefficientRow struct { - BigHuntQuestScoreCoefficientId int32 `json:"BigHuntQuestScoreCoefficientId"` - ScoreDifficultBonusPermil int32 `json:"ScoreDifficultBonusPermil"` -} - -type bigHuntBossRow struct { - BigHuntBossId int32 `json:"BigHuntBossId"` - BigHuntBossGradeGroupId int32 `json:"BigHuntBossGradeGroupId"` - AttributeType int32 `json:"AttributeType"` -} - type BigHuntBossRow struct { BigHuntBossId int32 BigHuntBossGradeGroupId int32 AttributeType int32 } -type bigHuntBossGradeGroupRow struct { - BigHuntBossGradeGroupId int32 `json:"BigHuntBossGradeGroupId"` - NecessaryScore int64 `json:"NecessaryScore"` - AssetGradeIconId int32 `json:"AssetGradeIconId"` -} - type GradeThreshold struct { NecessaryScore int64 AssetGradeIconId int32 } -type bigHuntScheduleRow struct { - BigHuntScheduleId int32 `json:"BigHuntScheduleId"` - ChallengeStartDatetime int64 `json:"ChallengeStartDatetime"` - ChallengeEndDatetime int64 `json:"ChallengeEndDatetime"` -} - -type scoreRewardScheduleRow struct { - BigHuntScoreRewardGroupScheduleId int32 `json:"BigHuntScoreRewardGroupScheduleId"` - GroupIndex int32 `json:"GroupIndex"` - BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"` - StartDatetime int64 `json:"StartDatetime"` -} - type ScoreRewardScheduleEntry struct { BigHuntScoreRewardGroupId int32 StartDatetime int64 } -type scoreRewardGroupRow struct { - BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"` - NecessaryScore int64 `json:"NecessaryScore"` - BigHuntRewardGroupId int32 `json:"BigHuntRewardGroupId"` -} - type ScoreRewardThreshold struct { NecessaryScore int64 BigHuntRewardGroupId int32 } -type rewardGroupRow struct { - BigHuntRewardGroupId int32 `json:"BigHuntRewardGroupId"` - SortOrder int32 `json:"SortOrder"` - PossessionType int32 `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - type RewardItem struct { PossessionType int32 PossessionId int32 Count int32 } -type weeklyRewardScheduleRow struct { - BigHuntWeeklyAttributeScoreRewardGroupScheduleId int32 `json:"BigHuntWeeklyAttributeScoreRewardGroupScheduleId"` - AttributeType int32 `json:"AttributeType"` - GroupIndex int32 `json:"GroupIndex"` - BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"` - StartDatetime int64 `json:"StartDatetime"` -} - type BigHuntWeeklyRewardKey struct { ScheduleId int32 AttributeType int32 @@ -189,7 +122,7 @@ func (c *BigHuntCatalog) CollectNewRewards(scoreRewardGroupId int32, oldMax, new } func LoadBigHuntCatalog() *BigHuntCatalog { - bossQuestRows, err := utils.ReadJSON[bigHuntBossQuestRow]("EntityMBigHuntBossQuestTable.json") + bossQuestRows, err := utils.ReadTable[EntityMBigHuntBossQuest]("m_big_hunt_boss_quest") if err != nil { log.Fatalf("load big hunt boss quest table: %v", err) } @@ -204,7 +137,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { } } - questRows, err := utils.ReadJSON[bigHuntQuestRow]("EntityMBigHuntQuestTable.json") + questRows, err := utils.ReadTable[EntityMBigHuntQuest]("m_big_hunt_quest") if err != nil { log.Fatalf("load big hunt quest table: %v", err) } @@ -217,7 +150,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { } } - coeffRows, err := utils.ReadJSON[bigHuntQuestScoreCoefficientRow]("EntityMBigHuntQuestScoreCoefficientTable.json") + coeffRows, err := utils.ReadTable[EntityMBigHuntQuestScoreCoefficient]("m_big_hunt_quest_score_coefficient") if err != nil { log.Fatalf("load big hunt quest score coefficient table: %v", err) } @@ -226,7 +159,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { scoreCoefficients[r.BigHuntQuestScoreCoefficientId] = r.ScoreDifficultBonusPermil } - bossRows, err := utils.ReadJSON[bigHuntBossRow]("EntityMBigHuntBossTable.json") + bossRows, err := utils.ReadTable[EntityMBigHuntBoss]("m_big_hunt_boss") if err != nil { log.Fatalf("load big hunt boss table: %v", err) } @@ -239,7 +172,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { } } - gradeRows, err := utils.ReadJSON[bigHuntBossGradeGroupRow]("EntityMBigHuntBossGradeGroupTable.json") + gradeRows, err := utils.ReadTable[EntityMBigHuntBossGradeGroup]("m_big_hunt_boss_grade_group") if err != nil { log.Fatalf("load big hunt boss grade group table: %v", err) } @@ -256,7 +189,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { }) } - scheduleRows, err := utils.ReadJSON[bigHuntScheduleRow]("EntityMBigHuntScheduleTable.json") + scheduleRows, err := utils.ReadTable[EntityMBigHuntSchedule]("m_big_hunt_schedule") if err != nil { log.Fatalf("load big hunt schedule table: %v", err) } @@ -274,7 +207,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { } } - rewardSchedRows, err := utils.ReadJSON[scoreRewardScheduleRow]("EntityMBigHuntScoreRewardGroupScheduleTable.json") + rewardSchedRows, err := utils.ReadTable[EntityMBigHuntScoreRewardGroupSchedule]("m_big_hunt_score_reward_group_schedule") if err != nil { log.Fatalf("load big hunt score reward group schedule table: %v", err) } @@ -294,7 +227,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { }) } - rewardGroupRows, err := utils.ReadJSON[scoreRewardGroupRow]("EntityMBigHuntScoreRewardGroupTable.json") + rewardGroupRows, err := utils.ReadTable[EntityMBigHuntScoreRewardGroup]("m_big_hunt_score_reward_group") if err != nil { log.Fatalf("load big hunt score reward group table: %v", err) } @@ -314,7 +247,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { }) } - rewardItemRows, err := utils.ReadJSON[rewardGroupRow]("EntityMBigHuntRewardGroupTable.json") + rewardItemRows, err := utils.ReadTable[EntityMBigHuntRewardGroup]("m_big_hunt_reward_group") if err != nil { log.Fatalf("load big hunt reward group table: %v", err) } @@ -327,7 +260,7 @@ func LoadBigHuntCatalog() *BigHuntCatalog { }) } - weeklySchedRows, err := utils.ReadJSON[weeklyRewardScheduleRow]("EntityMBigHuntWeeklyAttributeScoreRewardGroupScheduleTable.json") + weeklySchedRows, err := utils.ReadTable[EntityMBigHuntWeeklyAttributeScoreRewardGroupSchedule]("m_big_hunt_weekly_attribute_score_reward_group_schedule") if err != nil { log.Fatalf("load big hunt weekly attribute score reward group schedule table: %v", err) } diff --git a/server/internal/masterdata/cageornament.go b/server/internal/masterdata/cageornament.go index b1fd85f..64645c7 100644 --- a/server/internal/masterdata/cageornament.go +++ b/server/internal/masterdata/cageornament.go @@ -5,18 +5,6 @@ import ( "lunar-tear/server/internal/utils" ) -type cageOrnament struct { - CageOrnamentId int32 `json:"CageOrnamentId"` - CageOrnamentRewardId int32 `json:"CageOrnamentRewardId"` -} - -type cageOrnamentRewardRow struct { - CageOrnamentRewardId int32 `json:"CageOrnamentRewardId"` - PossessionType int32 `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - type CageOrnamentReward struct { PossessionType int32 PossessionId int32 @@ -38,11 +26,11 @@ func (c *CageOrnamentCatalog) LookupReward(cageOrnamentId int32) (CageOrnamentRe } func LoadCageOrnamentCatalog() *CageOrnamentCatalog { - ornaments, err := utils.ReadJSON[cageOrnament]("EntityMCageOrnamentTable.json") + ornaments, err := utils.ReadTable[EntityMCageOrnament]("m_cage_ornament") if err != nil { log.Fatalf("load cage ornament table: %v", err) } - rewards, err := utils.ReadJSON[cageOrnamentRewardRow]("EntityMCageOrnamentRewardTable.json") + rewards, err := utils.ReadTable[EntityMCageOrnamentReward]("m_cage_ornament_reward") if err != nil { log.Fatalf("load cage ornament reward table: %v", err) } diff --git a/server/internal/masterdata/character_rebirth.go b/server/internal/masterdata/character_rebirth.go index 821e9da..0a67908 100644 --- a/server/internal/masterdata/character_rebirth.go +++ b/server/internal/masterdata/character_rebirth.go @@ -6,24 +6,6 @@ import ( "lunar-tear/server/internal/utils" ) -type CharacterRebirthRow struct { - CharacterId int32 `json:"CharacterId"` - CharacterRebirthStepGroupId int32 `json:"CharacterRebirthStepGroupId"` -} - -type CharacterRebirthStepRow struct { - CharacterRebirthStepGroupId int32 `json:"CharacterRebirthStepGroupId"` - BeforeRebirthCount int32 `json:"BeforeRebirthCount"` - CostumeLevelLimitUp int32 `json:"CostumeLevelLimitUp"` - CharacterRebirthMaterialGroupId int32 `json:"CharacterRebirthMaterialGroupId"` -} - -type CharacterRebirthMaterialRow struct { - CharacterRebirthMaterialGroupId int32 `json:"CharacterRebirthMaterialGroupId"` - MaterialId int32 `json:"MaterialId"` - Count int32 `json:"Count"` -} - type StepKey struct { GroupId int32 BeforeRebirthCount int32 @@ -31,22 +13,22 @@ type StepKey struct { type CharacterRebirthCatalog struct { StepGroupByCharacterId map[int32]int32 - StepByGroupAndCount map[StepKey]CharacterRebirthStepRow - MaterialsByGroupId map[int32][]CharacterRebirthMaterialRow + StepByGroupAndCount map[StepKey]EntityMCharacterRebirthStepGroup + MaterialsByGroupId map[int32][]EntityMCharacterRebirthMaterialGroup } func LoadCharacterRebirthCatalog() (*CharacterRebirthCatalog, error) { - rebirthRows, err := utils.ReadJSON[CharacterRebirthRow]("EntityMCharacterRebirthTable.json") + rebirthRows, err := utils.ReadTable[EntityMCharacterRebirth]("m_character_rebirth") if err != nil { return nil, fmt.Errorf("load character rebirth table: %w", err) } - stepRows, err := utils.ReadJSON[CharacterRebirthStepRow]("EntityMCharacterRebirthStepGroupTable.json") + stepRows, err := utils.ReadTable[EntityMCharacterRebirthStepGroup]("m_character_rebirth_step_group") if err != nil { return nil, fmt.Errorf("load character rebirth step group table: %w", err) } - materialRows, err := utils.ReadJSON[CharacterRebirthMaterialRow]("EntityMCharacterRebirthMaterialGroupTable.json") + materialRows, err := utils.ReadTable[EntityMCharacterRebirthMaterialGroup]("m_character_rebirth_material_group") if err != nil { return nil, fmt.Errorf("load character rebirth material group table: %w", err) } @@ -56,12 +38,12 @@ func LoadCharacterRebirthCatalog() (*CharacterRebirthCatalog, error) { stepGroupByCharacterId[r.CharacterId] = r.CharacterRebirthStepGroupId } - stepByGroupAndCount := make(map[StepKey]CharacterRebirthStepRow, len(stepRows)) + stepByGroupAndCount := make(map[StepKey]EntityMCharacterRebirthStepGroup, len(stepRows)) for _, s := range stepRows { stepByGroupAndCount[StepKey{GroupId: s.CharacterRebirthStepGroupId, BeforeRebirthCount: s.BeforeRebirthCount}] = s } - materialsByGroupId := make(map[int32][]CharacterRebirthMaterialRow) + materialsByGroupId := make(map[int32][]EntityMCharacterRebirthMaterialGroup) for _, m := range materialRows { materialsByGroupId[m.CharacterRebirthMaterialGroupId] = append(materialsByGroupId[m.CharacterRebirthMaterialGroupId], m) } diff --git a/server/internal/masterdata/characterboard.go b/server/internal/masterdata/characterboard.go index 9420a19..b41269a 100644 --- a/server/internal/masterdata/characterboard.go +++ b/server/internal/masterdata/characterboard.go @@ -7,66 +7,6 @@ import ( "lunar-tear/server/internal/utils" ) -type CharacterBoardPanelRow struct { - CharacterBoardPanelId int32 `json:"CharacterBoardPanelId"` - CharacterBoardId int32 `json:"CharacterBoardId"` - CharacterBoardPanelUnlockConditionGroupId int32 `json:"CharacterBoardPanelUnlockConditionGroupId"` - CharacterBoardPanelReleasePossessionGroupId int32 `json:"CharacterBoardPanelReleasePossessionGroupId"` - CharacterBoardPanelReleaseRewardGroupId int32 `json:"CharacterBoardPanelReleaseRewardGroupId"` - CharacterBoardPanelReleaseEffectGroupId int32 `json:"CharacterBoardPanelReleaseEffectGroupId"` - SortOrder int32 `json:"SortOrder"` - ParentCharacterBoardPanelId int32 `json:"ParentCharacterBoardPanelId"` - PlaceIndex int32 `json:"PlaceIndex"` -} - -type CharacterBoardReleasePossessionRow struct { - CharacterBoardPanelReleasePossessionGroupId int32 `json:"CharacterBoardPanelReleasePossessionGroupId"` - PossessionType int32 `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` - SortOrder int32 `json:"SortOrder"` -} - -type CharacterBoardReleaseEffectRow struct { - CharacterBoardPanelReleaseEffectGroupId int32 `json:"CharacterBoardPanelReleaseEffectGroupId"` - SortOrder int32 `json:"SortOrder"` - CharacterBoardEffectType int32 `json:"CharacterBoardEffectType"` - CharacterBoardEffectId int32 `json:"CharacterBoardEffectId"` - EffectValue int32 `json:"EffectValue"` -} - -type CharacterBoardRow struct { - CharacterBoardId int32 `json:"CharacterBoardId"` - CharacterBoardGroupId int32 `json:"CharacterBoardGroupId"` - CharacterBoardUnlockConditionGroupId int32 `json:"CharacterBoardUnlockConditionGroupId"` - ReleaseRank int32 `json:"ReleaseRank"` -} - -type CharacterBoardStatusUpRow struct { - CharacterBoardStatusUpId int32 `json:"CharacterBoardStatusUpId"` - CharacterBoardStatusUpType int32 `json:"CharacterBoardStatusUpType"` - CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"` -} - -type CharacterBoardAbilityRow struct { - CharacterBoardAbilityId int32 `json:"CharacterBoardAbilityId"` - CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"` - AbilityId int32 `json:"AbilityId"` -} - -type CharacterBoardAbilityMaxLevelRow struct { - CharacterId int32 `json:"CharacterId"` - AbilityId int32 `json:"AbilityId"` - MaxLevel int32 `json:"MaxLevel"` -} - -type CharacterBoardEffectTargetRow struct { - CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"` - GroupIndex int32 `json:"GroupIndex"` - CharacterBoardEffectTargetType int32 `json:"CharacterBoardEffectTargetType"` - TargetValue int32 `json:"TargetValue"` -} - type CharacterBoardAssignmentRow struct { CharacterId int32 `json:"CharacterId"` CharacterBoardCategoryId int32 `json:"CharacterBoardCategoryId"` @@ -83,68 +23,68 @@ type CharacterBoardGroupRow struct { } type CharacterBoardCatalog struct { - PanelById map[int32]CharacterBoardPanelRow - PanelsByBoardId map[int32][]CharacterBoardPanelRow - ReleaseCostsByGroupId map[int32][]CharacterBoardReleasePossessionRow - ReleaseEffectsByGroupId map[int32][]CharacterBoardReleaseEffectRow - StatusUpById map[int32]CharacterBoardStatusUpRow - AbilityById map[int32]CharacterBoardAbilityRow + PanelById map[int32]EntityMCharacterBoardPanel + PanelsByBoardId map[int32][]EntityMCharacterBoardPanel + ReleaseCostsByGroupId map[int32][]EntityMCharacterBoardPanelReleasePossessionGroup + ReleaseEffectsByGroupId map[int32][]EntityMCharacterBoardPanelReleaseEffectGroup + StatusUpById map[int32]EntityMCharacterBoardStatusUp + AbilityById map[int32]EntityMCharacterBoardAbility AbilityMaxLevel map[store.CharacterBoardAbilityKey]int32 - EffectTargetsByGroupId map[int32][]CharacterBoardEffectTargetRow - BoardById map[int32]CharacterBoardRow + EffectTargetsByGroupId map[int32][]EntityMCharacterBoardEffectTargetGroup + BoardById map[int32]EntityMCharacterBoard } func LoadCharacterBoardCatalog() (*CharacterBoardCatalog, error) { - panels, err := utils.ReadJSON[CharacterBoardPanelRow]("EntityMCharacterBoardPanelTable.json") + panels, err := utils.ReadTable[EntityMCharacterBoardPanel]("m_character_board_panel") if err != nil { return nil, fmt.Errorf("load character board panel table: %w", err) } - costs, err := utils.ReadJSON[CharacterBoardReleasePossessionRow]("EntityMCharacterBoardPanelReleasePossessionGroupTable.json") + costs, err := utils.ReadTable[EntityMCharacterBoardPanelReleasePossessionGroup]("m_character_board_panel_release_possession_group") if err != nil { return nil, fmt.Errorf("load character board release possession table: %w", err) } - effects, err := utils.ReadJSON[CharacterBoardReleaseEffectRow]("EntityMCharacterBoardPanelReleaseEffectGroupTable.json") + effects, err := utils.ReadTable[EntityMCharacterBoardPanelReleaseEffectGroup]("m_character_board_panel_release_effect_group") if err != nil { return nil, fmt.Errorf("load character board release effect table: %w", err) } - boards, err := utils.ReadJSON[CharacterBoardRow]("EntityMCharacterBoardTable.json") + boards, err := utils.ReadTable[EntityMCharacterBoard]("m_character_board") if err != nil { return nil, fmt.Errorf("load character board table: %w", err) } - statusUps, err := utils.ReadJSON[CharacterBoardStatusUpRow]("EntityMCharacterBoardStatusUpTable.json") + statusUps, err := utils.ReadTable[EntityMCharacterBoardStatusUp]("m_character_board_status_up") if err != nil { return nil, fmt.Errorf("load character board status up table: %w", err) } - abilities, err := utils.ReadJSON[CharacterBoardAbilityRow]("EntityMCharacterBoardAbilityTable.json") + abilities, err := utils.ReadTable[EntityMCharacterBoardAbility]("m_character_board_ability") if err != nil { return nil, fmt.Errorf("load character board ability table: %w", err) } - abilityMaxLevels, err := utils.ReadJSON[CharacterBoardAbilityMaxLevelRow]("EntityMCharacterBoardAbilityMaxLevelTable.json") + abilityMaxLevels, err := utils.ReadTable[EntityMCharacterBoardAbilityMaxLevel]("m_character_board_ability_max_level") if err != nil { return nil, fmt.Errorf("load character board ability max level table: %w", err) } - targets, err := utils.ReadJSON[CharacterBoardEffectTargetRow]("EntityMCharacterBoardEffectTargetGroupTable.json") + targets, err := utils.ReadTable[EntityMCharacterBoardEffectTargetGroup]("m_character_board_effect_target_group") if err != nil { return nil, fmt.Errorf("load character board effect target table: %w", err) } catalog := &CharacterBoardCatalog{ - PanelById: make(map[int32]CharacterBoardPanelRow, len(panels)), - PanelsByBoardId: make(map[int32][]CharacterBoardPanelRow), - ReleaseCostsByGroupId: make(map[int32][]CharacterBoardReleasePossessionRow), - ReleaseEffectsByGroupId: make(map[int32][]CharacterBoardReleaseEffectRow), - StatusUpById: make(map[int32]CharacterBoardStatusUpRow, len(statusUps)), - AbilityById: make(map[int32]CharacterBoardAbilityRow, len(abilities)), + PanelById: make(map[int32]EntityMCharacterBoardPanel, len(panels)), + PanelsByBoardId: make(map[int32][]EntityMCharacterBoardPanel), + ReleaseCostsByGroupId: make(map[int32][]EntityMCharacterBoardPanelReleasePossessionGroup), + ReleaseEffectsByGroupId: make(map[int32][]EntityMCharacterBoardPanelReleaseEffectGroup), + StatusUpById: make(map[int32]EntityMCharacterBoardStatusUp, len(statusUps)), + AbilityById: make(map[int32]EntityMCharacterBoardAbility, len(abilities)), AbilityMaxLevel: make(map[store.CharacterBoardAbilityKey]int32, len(abilityMaxLevels)), - EffectTargetsByGroupId: make(map[int32][]CharacterBoardEffectTargetRow), - BoardById: make(map[int32]CharacterBoardRow, len(boards)), + EffectTargetsByGroupId: make(map[int32][]EntityMCharacterBoardEffectTargetGroup), + BoardById: make(map[int32]EntityMCharacterBoard, len(boards)), } for _, p := range panels { diff --git a/server/internal/masterdata/characterviewer.go b/server/internal/masterdata/characterviewer.go index d086b38..29a7a50 100644 --- a/server/internal/masterdata/characterviewer.go +++ b/server/internal/masterdata/characterviewer.go @@ -9,11 +9,6 @@ import ( "lunar-tear/server/internal/utils" ) -type characterViewerField struct { - CharacterViewerFieldId int32 `json:"CharacterViewerFieldId"` - ReleaseEvaluateConditionId int32 `json:"ReleaseEvaluateConditionId"` -} - type characterViewerFieldEntry struct { FieldId int32 RequiredQuestId int32 @@ -39,7 +34,7 @@ func (c *CharacterViewerCatalog) ReleasedFieldIds(user store.UserState) []int32 } func LoadCharacterViewerCatalog(resolver *ConditionResolver) *CharacterViewerCatalog { - fields, err := utils.ReadJSON[characterViewerField]("EntityMCharacterViewerFieldTable.json") + fields, err := utils.ReadTable[EntityMCharacterViewerField]("m_character_viewer_field") if err != nil { log.Fatalf("load character viewer field table: %v", err) } diff --git a/server/internal/masterdata/companion.go b/server/internal/masterdata/companion.go index ba0ffcd..5509bf3 100644 --- a/server/internal/masterdata/companion.go +++ b/server/internal/masterdata/companion.go @@ -6,23 +6,6 @@ import ( "lunar-tear/server/internal/utils" ) -type companionRow struct { - CompanionId int32 `json:"CompanionId"` - CompanionCategoryType int32 `json:"CompanionCategoryType"` -} - -type companionCategoryRow struct { - CompanionCategoryType int32 `json:"CompanionCategoryType"` - EnhancementCostNumericalFunctionId int32 `json:"EnhancementCostNumericalFunctionId"` -} - -type companionEnhancementMaterialRow struct { - CompanionCategoryType int32 `json:"CompanionCategoryType"` - Level int32 `json:"Level"` - MaterialId int32 `json:"MaterialId"` - Count int32 `json:"Count"` -} - type CompanionLevelKey struct { CategoryType int32 Level int32 @@ -34,23 +17,23 @@ type CompanionMaterialCost struct { } type CompanionCatalog struct { - CompanionById map[int32]companionRow + CompanionById map[int32]EntityMCompanion GoldCostByCategory map[int32]NumericalFunc MaterialsByKey map[CompanionLevelKey]CompanionMaterialCost } func LoadCompanionCatalog() (*CompanionCatalog, error) { - companions, err := utils.ReadJSON[companionRow]("EntityMCompanionTable.json") + companions, err := utils.ReadTable[EntityMCompanion]("m_companion") if err != nil { return nil, fmt.Errorf("load companion table: %w", err) } - categories, err := utils.ReadJSON[companionCategoryRow]("EntityMCompanionCategoryTable.json") + categories, err := utils.ReadTable[EntityMCompanionCategory]("m_companion_category") if err != nil { return nil, fmt.Errorf("load companion category table: %w", err) } - materials, err := utils.ReadJSON[companionEnhancementMaterialRow]("EntityMCompanionEnhancementMaterialTable.json") + materials, err := utils.ReadTable[EntityMCompanionEnhancementMaterial]("m_companion_enhancement_material") if err != nil { return nil, fmt.Errorf("load companion enhancement material table: %w", err) } @@ -60,7 +43,7 @@ func LoadCompanionCatalog() (*CompanionCatalog, error) { return nil, fmt.Errorf("load function resolver: %w", err) } - companionById := make(map[int32]companionRow, len(companions)) + companionById := make(map[int32]EntityMCompanion, len(companions)) for _, c := range companions { companionById[c.CompanionId] = c } diff --git a/server/internal/masterdata/conditions.go b/server/internal/masterdata/conditions.go index aa30dd8..8014a72 100644 --- a/server/internal/masterdata/conditions.go +++ b/server/internal/masterdata/conditions.go @@ -7,19 +7,6 @@ import ( "lunar-tear/server/internal/utils" ) -type evaluateCondition struct { - EvaluateConditionId int32 `json:"EvaluateConditionId"` - EvaluateConditionFunctionType model.EvaluateConditionFunctionType `json:"EvaluateConditionFunctionType"` - EvaluateConditionEvaluateType model.EvaluateConditionEvaluateType `json:"EvaluateConditionEvaluateType"` - EvaluateConditionValueGroupId int32 `json:"EvaluateConditionValueGroupId"` -} - -type evaluateConditionValueGroup struct { - EvaluateConditionValueGroupId int32 `json:"EvaluateConditionValueGroupId"` - GroupIndex int32 `json:"GroupIndex"` - Value int64 `json:"Value"` -} - const defaultGroupIndex = 1 type ConditionResolver struct { @@ -27,16 +14,16 @@ type ConditionResolver struct { } func LoadConditionResolver() (*ConditionResolver, error) { - conditions, err := utils.ReadJSON[evaluateCondition]("EntityMEvaluateConditionTable.json") + conditions, err := utils.ReadTable[EntityMEvaluateCondition]("m_evaluate_condition") if err != nil { return nil, fmt.Errorf("load evaluate condition table: %w", err) } - valueGroups, err := utils.ReadJSON[evaluateConditionValueGroup]("EntityMEvaluateConditionValueGroupTable.json") + valueGroups, err := utils.ReadTable[EntityMEvaluateConditionValueGroup]("m_evaluate_condition_value_group") if err != nil { return nil, fmt.Errorf("load evaluate condition value group table: %w", err) } - condById := make(map[int32]evaluateCondition, len(conditions)) + condById := make(map[int32]EntityMEvaluateCondition, len(conditions)) for _, c := range conditions { condById[c.EvaluateConditionId] = c } @@ -52,8 +39,8 @@ func LoadConditionResolver() (*ConditionResolver, error) { resolved := make(map[int32]int32) for _, c := range conditions { - if c.EvaluateConditionFunctionType == model.EvaluateConditionFunctionTypeQuestClear && - c.EvaluateConditionEvaluateType == model.EvaluateConditionEvaluateTypeIdContain { + if model.EvaluateConditionFunctionType(c.EvaluateConditionFunctionType) == model.EvaluateConditionFunctionTypeQuestClear && + model.EvaluateConditionEvaluateType(c.EvaluateConditionEvaluateType) == model.EvaluateConditionEvaluateTypeIdContain { if questId, ok := vgByKey[vgKey{c.EvaluateConditionValueGroupId, defaultGroupIndex}]; ok { resolved[c.EvaluateConditionId] = int32(questId) } diff --git a/server/internal/masterdata/config.go b/server/internal/masterdata/config.go index 9e8f1b7..5b8eb35 100644 --- a/server/internal/masterdata/config.go +++ b/server/internal/masterdata/config.go @@ -7,11 +7,6 @@ import ( "lunar-tear/server/internal/utils" ) -type configRow struct { - ConfigKey string `json:"ConfigKey"` - Value string `json:"Value"` -} - type GameConfig struct { ConsumableItemIdForGold int32 ConsumableItemIdForMedal int32 @@ -41,7 +36,7 @@ type GameConfig struct { } func LoadGameConfig() (*GameConfig, error) { - rows, err := utils.ReadJSON[configRow]("EntityMConfigTable.json") + rows, err := utils.ReadTable[EntityMConfig]("m_config") if err != nil { return nil, fmt.Errorf("load config table: %w", err) } diff --git a/server/internal/masterdata/consumableitem.go b/server/internal/masterdata/consumableitem.go index 2e18691..34e1924 100644 --- a/server/internal/masterdata/consumableitem.go +++ b/server/internal/masterdata/consumableitem.go @@ -6,23 +6,18 @@ import ( "lunar-tear/server/internal/utils" ) -type ConsumableItemRow struct { - ConsumableItemId int32 `json:"ConsumableItemId"` - SellPrice int32 `json:"SellPrice"` -} - type ConsumableItemCatalog struct { - All map[int32]ConsumableItemRow + All map[int32]EntityMConsumableItem } func LoadConsumableItemCatalog() (*ConsumableItemCatalog, error) { - rows, err := utils.ReadJSON[ConsumableItemRow]("EntityMConsumableItemTable.json") + rows, err := utils.ReadTable[EntityMConsumableItem]("m_consumable_item") if err != nil { return nil, fmt.Errorf("load consumable item table: %w", err) } catalog := &ConsumableItemCatalog{ - All: make(map[int32]ConsumableItemRow, len(rows)), + All: make(map[int32]EntityMConsumableItem, len(rows)), } for _, row := range rows { catalog.All[row.ConsumableItemId] = row diff --git a/server/internal/masterdata/costume.go b/server/internal/masterdata/costume.go index 98b8eda..148eef6 100644 --- a/server/internal/masterdata/costume.go +++ b/server/internal/masterdata/costume.go @@ -8,132 +8,37 @@ import ( "lunar-tear/server/internal/utils" ) -type CostumeMasterRow struct { - CostumeId int32 `json:"CostumeId"` - CharacterId int32 `json:"CharacterId"` - SkillfulWeaponType int32 `json:"SkillfulWeaponType"` - RarityType int32 `json:"RarityType"` - CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"` - CostumeActiveSkillGroupId int32 `json:"CostumeActiveSkillGroupId"` -} - -type costumeRarityRow struct { - RarityType int32 `json:"RarityType"` - CostumeLimitBreakMaterialRarityGroupId int32 `json:"CostumeLimitBreakMaterialRarityGroupId"` - RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"` - EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"` - LimitBreakCostNumericalFunctionId int32 `json:"LimitBreakCostNumericalFunctionId"` - MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"` - ActiveSkillMaxLevelNumericalFunctionId int32 `json:"ActiveSkillMaxLevelNumericalFunctionId"` - ActiveSkillEnhancementCostNumericalFunctionId int32 `json:"ActiveSkillEnhancementCostNumericalFunctionId"` -} - -type CostumeAwakenRow struct { - CostumeId int32 `json:"CostumeId"` - CostumeAwakenEffectGroupId int32 `json:"CostumeAwakenEffectGroupId"` - CostumeAwakenStepMaterialGroupId int32 `json:"CostumeAwakenStepMaterialGroupId"` - CostumeAwakenPriceGroupId int32 `json:"CostumeAwakenPriceGroupId"` -} - -type costumeAwakenPriceRow struct { - CostumeAwakenPriceGroupId int32 `json:"CostumeAwakenPriceGroupId"` - AwakenStepLowerLimit int32 `json:"AwakenStepLowerLimit"` - Gold int32 `json:"Gold"` -} - -type CostumeAwakenEffectRow struct { - CostumeAwakenEffectGroupId int32 `json:"CostumeAwakenEffectGroupId"` - AwakenStep int32 `json:"AwakenStep"` - CostumeAwakenEffectType int32 `json:"CostumeAwakenEffectType"` - CostumeAwakenEffectId int32 `json:"CostumeAwakenEffectId"` -} - -type CostumeAwakenStatusUpRow struct { - CostumeAwakenStatusUpGroupId int32 `json:"CostumeAwakenStatusUpGroupId"` - SortOrder int32 `json:"SortOrder"` - StatusKindType int32 `json:"StatusKindType"` - StatusCalculationType int32 `json:"StatusCalculationType"` - EffectValue int32 `json:"EffectValue"` -} - -type CostumeAwakenItemAcquireRow struct { - CostumeAwakenItemAcquireId int32 `json:"CostumeAwakenItemAcquireId"` - PossessionType int32 `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - -type CostumeActiveSkillGroupRow struct { - CostumeActiveSkillGroupId int32 `json:"CostumeActiveSkillGroupId"` - CostumeLimitBreakCountLowerLimit int32 `json:"CostumeLimitBreakCountLowerLimit"` - CostumeActiveSkillId int32 `json:"CostumeActiveSkillId"` - CostumeActiveSkillEnhancementMaterialId int32 `json:"CostumeActiveSkillEnhancementMaterialId"` -} - -type CostumeActiveSkillEnhanceMaterialRow struct { - CostumeActiveSkillEnhancementMaterialId int32 `json:"CostumeActiveSkillEnhancementMaterialId"` - SkillLevel int32 `json:"SkillLevel"` - MaterialId int32 `json:"MaterialId"` - Count int32 `json:"Count"` - 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 + Costumes map[int32]EntityMCostume + Materials map[int32]EntityMMaterial ExpByRarity map[int32][]int32 EnhanceCostByRarity map[int32]NumericalFunc MaxLevelByRarity map[int32]NumericalFunc LimitBreakCostByRarity map[int32]NumericalFunc - AwakenByCostumeId map[int32]CostumeAwakenRow + AwakenByCostumeId map[int32]EntityMCostumeAwaken AwakenPriceByGroup map[int32]int32 - AwakenEffectsByGroupAndStep map[int32]map[int32]CostumeAwakenEffectRow - AwakenStatusUpByGroup map[int32][]CostumeAwakenStatusUpRow - AwakenItemAcquireById map[int32]CostumeAwakenItemAcquireRow + AwakenEffectsByGroupAndStep map[int32]map[int32]EntityMCostumeAwakenEffectGroup + AwakenStatusUpByGroup map[int32][]EntityMCostumeAwakenStatusUpGroup + AwakenItemAcquireById map[int32]EntityMCostumeAwakenItemAcquire - ActiveSkillGroupsByGroupId map[int32][]CostumeActiveSkillGroupRow // sorted by CostumeLimitBreakCountLowerLimit desc - ActiveSkillEnhanceMats map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel] + ActiveSkillGroupsByGroupId map[int32][]EntityMCostumeActiveSkillGroup // sorted by CostumeLimitBreakCountLowerLimit desc + ActiveSkillEnhanceMats map[[2]int32][]EntityMCostumeActiveSkillEnhancementMaterial // 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 + LotteryEffects map[[2]int32]EntityMCostumeLotteryEffect // key: [costumeId, slotNumber] + LotteryEffectMats map[int32][]EntityMCostumeLotteryEffectMaterialGroup // key: materialGroupId (both unlock and draw) + LotteryEffectOdds map[int32][]EntityMCostumeLotteryEffectOddsGroup // key: oddsGroupId } func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) { - costumes, err := utils.ReadJSON[CostumeMasterRow]("EntityMCostumeTable.json") + costumes, err := utils.ReadTable[EntityMCostume]("m_costume") if err != nil { return nil, fmt.Errorf("load costume table: %w", err) } - rarities, err := utils.ReadJSON[costumeRarityRow]("EntityMCostumeRarityTable.json") + rarities, err := utils.ReadTable[EntityMCostumeRarity]("m_costume_rarity") if err != nil { return nil, fmt.Errorf("load costume rarity table: %w", err) } @@ -148,71 +53,71 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) { return nil, fmt.Errorf("load function resolver: %w", err) } - awakenRows, err := utils.ReadJSON[CostumeAwakenRow]("EntityMCostumeAwakenTable.json") + awakenRows, err := utils.ReadTable[EntityMCostumeAwaken]("m_costume_awaken") if err != nil { return nil, fmt.Errorf("load costume awaken table: %w", err) } - awakenPriceRows, err := utils.ReadJSON[costumeAwakenPriceRow]("EntityMCostumeAwakenPriceGroupTable.json") + awakenPriceRows, err := utils.ReadTable[EntityMCostumeAwakenPriceGroup]("m_costume_awaken_price_group") if err != nil { return nil, fmt.Errorf("load costume awaken price table: %w", err) } - awakenEffectRows, err := utils.ReadJSON[CostumeAwakenEffectRow]("EntityMCostumeAwakenEffectGroupTable.json") + awakenEffectRows, err := utils.ReadTable[EntityMCostumeAwakenEffectGroup]("m_costume_awaken_effect_group") if err != nil { return nil, fmt.Errorf("load costume awaken effect table: %w", err) } - awakenStatusUpRows, err := utils.ReadJSON[CostumeAwakenStatusUpRow]("EntityMCostumeAwakenStatusUpGroupTable.json") + awakenStatusUpRows, err := utils.ReadTable[EntityMCostumeAwakenStatusUpGroup]("m_costume_awaken_status_up_group") if err != nil { return nil, fmt.Errorf("load costume awaken status up table: %w", err) } - awakenItemAcquireRows, err := utils.ReadJSON[CostumeAwakenItemAcquireRow]("EntityMCostumeAwakenItemAcquireTable.json") + awakenItemAcquireRows, err := utils.ReadTable[EntityMCostumeAwakenItemAcquire]("m_costume_awaken_item_acquire") if err != nil { return nil, fmt.Errorf("load costume awaken item acquire table: %w", err) } - activeSkillGroupRows, err := utils.ReadJSON[CostumeActiveSkillGroupRow]("EntityMCostumeActiveSkillGroupTable.json") + activeSkillGroupRows, err := utils.ReadTable[EntityMCostumeActiveSkillGroup]("m_costume_active_skill_group") if err != nil { return nil, fmt.Errorf("load costume active skill group table: %w", err) } - activeSkillMatRows, err := utils.ReadJSON[CostumeActiveSkillEnhanceMaterialRow]("EntityMCostumeActiveSkillEnhancementMaterialTable.json") + activeSkillMatRows, err := utils.ReadTable[EntityMCostumeActiveSkillEnhancementMaterial]("m_costume_active_skill_enhancement_material") if err != nil { return nil, fmt.Errorf("load costume active skill enhancement material table: %w", err) } - lotteryEffectRows, err := utils.ReadJSON[CostumeLotteryEffectRow]("EntityMCostumeLotteryEffectTable.json") + lotteryEffectRows, err := utils.ReadTable[EntityMCostumeLotteryEffect]("m_costume_lottery_effect") if err != nil { return nil, fmt.Errorf("load costume lottery effect table: %w", err) } - lotteryEffectMatRows, err := utils.ReadJSON[CostumeLotteryEffectMaterialGroupRow]("EntityMCostumeLotteryEffectMaterialGroupTable.json") + lotteryEffectMatRows, err := utils.ReadTable[EntityMCostumeLotteryEffectMaterialGroup]("m_costume_lottery_effect_material_group") if err != nil { return nil, fmt.Errorf("load costume lottery effect material group table: %w", err) } - lotteryEffectOddsRows, err := utils.ReadJSON[CostumeLotteryEffectOddsRow]("EntityMCostumeLotteryEffectOddsGroupTable.json") + lotteryEffectOddsRows, err := utils.ReadTable[EntityMCostumeLotteryEffectOddsGroup]("m_costume_lottery_effect_odds_group") 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)), + Costumes: make(map[int32]EntityMCostume, len(costumes)), Materials: matCatalog.ByType[model.MaterialTypeCostumeEnhancement], ExpByRarity: make(map[int32][]int32, len(rarities)), EnhanceCostByRarity: make(map[int32]NumericalFunc, len(rarities)), MaxLevelByRarity: make(map[int32]NumericalFunc, len(rarities)), LimitBreakCostByRarity: make(map[int32]NumericalFunc, len(rarities)), - AwakenByCostumeId: make(map[int32]CostumeAwakenRow, len(awakenRows)), + AwakenByCostumeId: make(map[int32]EntityMCostumeAwaken, len(awakenRows)), AwakenPriceByGroup: make(map[int32]int32, len(awakenPriceRows)), - AwakenEffectsByGroupAndStep: make(map[int32]map[int32]CostumeAwakenEffectRow), - AwakenStatusUpByGroup: make(map[int32][]CostumeAwakenStatusUpRow), - AwakenItemAcquireById: make(map[int32]CostumeAwakenItemAcquireRow, len(awakenItemAcquireRows)), + AwakenEffectsByGroupAndStep: make(map[int32]map[int32]EntityMCostumeAwakenEffectGroup), + AwakenStatusUpByGroup: make(map[int32][]EntityMCostumeAwakenStatusUpGroup), + AwakenItemAcquireById: make(map[int32]EntityMCostumeAwakenItemAcquire, len(awakenItemAcquireRows)), - ActiveSkillGroupsByGroupId: make(map[int32][]CostumeActiveSkillGroupRow), - ActiveSkillEnhanceMats: make(map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow), + ActiveSkillGroupsByGroupId: make(map[int32][]EntityMCostumeActiveSkillGroup), + ActiveSkillEnhanceMats: make(map[[2]int32][]EntityMCostumeActiveSkillEnhancementMaterial), 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), + LotteryEffects: make(map[[2]int32]EntityMCostumeLotteryEffect, len(lotteryEffectRows)), + LotteryEffectMats: make(map[int32][]EntityMCostumeLotteryEffectMaterialGroup), + LotteryEffectOdds: make(map[int32][]EntityMCostumeLotteryEffectOddsGroup), } for _, row := range costumes { @@ -259,7 +164,7 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) { for _, row := range awakenEffectRows { m, ok := catalog.AwakenEffectsByGroupAndStep[row.CostumeAwakenEffectGroupId] if !ok { - m = make(map[int32]CostumeAwakenEffectRow) + m = make(map[int32]EntityMCostumeAwakenEffectGroup) catalog.AwakenEffectsByGroupAndStep[row.CostumeAwakenEffectGroupId] = m } m[row.AwakenStep] = row diff --git a/server/internal/masterdata/dup_exchange.go b/server/internal/masterdata/dup_exchange.go index 9fa9c64..5c442d9 100644 --- a/server/internal/masterdata/dup_exchange.go +++ b/server/internal/masterdata/dup_exchange.go @@ -5,17 +5,10 @@ import ( "lunar-tear/server/internal/utils" ) -type costumeDupRow struct { - CostumeId int32 `json:"CostumeId"` - PossessionType int32 `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - func LoadDupExchange() (map[int32][]model.DupExchangeEntry, error) { result := make(map[int32][]model.DupExchangeEntry) - costumeRows, err := utils.ReadJSON[costumeDupRow]("EntityMCostumeDuplicationExchangePossessionGroupTable.json") + costumeRows, err := utils.ReadTable[EntityMCostumeDuplicationExchangePossessionGroup]("m_costume_duplication_exchange_possession_group") if err != nil { return nil, err } @@ -30,20 +23,10 @@ func LoadDupExchange() (map[int32][]model.DupExchangeEntry, error) { return result, nil } -type lbMaterialRow struct { - CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"` - MaterialId int32 `json:"MaterialId"` -} - -type costumeLBRef struct { - CostumeId int32 `json:"CostumeId"` - CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"` -} - const dupExchangeFallbackCount int32 = 10 func EnrichDupExchange(dupMap map[int32][]model.DupExchangeEntry, pool *GachaCatalog) (int, error) { - lbRows, err := utils.ReadJSON[lbMaterialRow]("EntityMCostumeLimitBreakMaterialGroupTable.json") + lbRows, err := utils.ReadTable[EntityMCostumeLimitBreakMaterialGroup]("m_costume_limit_break_material_group") if err != nil { return 0, err } @@ -52,7 +35,7 @@ func EnrichDupExchange(dupMap map[int32][]model.DupExchangeEntry, pool *GachaCat groupToMaterial[r.CostumeLimitBreakMaterialGroupId] = r.MaterialId } - costumeRows, err := utils.ReadJSON[costumeLBRef]("EntityMCostumeTable.json") + costumeRows, err := utils.ReadTable[EntityMCostume]("m_costume") if err != nil { return 0, err } diff --git a/server/internal/masterdata/entities.go b/server/internal/masterdata/entities.go new file mode 100644 index 0000000..66a1093 --- /dev/null +++ b/server/internal/masterdata/entities.go @@ -0,0 +1,1106 @@ +// Code generated by scripts/gen_entities.py from schemas.json. DO NOT EDIT. + +package masterdata + +// EntityMBattle is table key "m_battle". +type EntityMBattle struct { + _ struct{} `msgpack:",array"` + BattleId int32 + BattleNpcId int64 + DeckType int32 + BattleNpcDeckNumber int32 +} + +// EntityMBattleDropReward is table key "m_battle_drop_reward". +type EntityMBattleDropReward struct { + _ struct{} `msgpack:",array"` + BattleDropRewardId int32 + PossessionType int32 + PossessionId int32 + Count int32 +} + +// EntityMBattleGroup is table key "m_battle_group". +type EntityMBattleGroup struct { + _ struct{} `msgpack:",array"` + BattleGroupId int32 + WaveNumber int32 + BattleId int32 + WaveStartActAssetId int32 + WaveEndActAssetId int32 + BattleCameraControllerAssetId int32 + BattlePointIndex int32 + BattleStartCameraType int32 +} + +// EntityMBattleNpcDeck is table key "m_battle_npc_deck". +type EntityMBattleNpcDeck struct { + _ struct{} `msgpack:",array"` + BattleNpcId int64 + DeckType int32 + BattleNpcDeckNumber int32 + BattleNpcDeckCharacterUuid01 string + BattleNpcDeckCharacterUuid02 string + BattleNpcDeckCharacterUuid03 string + Name string + Power int32 +} + +// EntityMBattleNpcDeckCharacterDropCategory is table key "m_battle_npc_deck_character_drop_category". +type EntityMBattleNpcDeckCharacterDropCategory struct { + _ struct{} `msgpack:",array"` + BattleNpcId int64 + BattleNpcDeckCharacterUuid string + BattleDropCategoryId int32 +} + +// EntityMBattleRentalDeck is table key "m_battle_rental_deck". +type EntityMBattleRentalDeck struct { + _ struct{} `msgpack:",array"` + BattleGroupId int32 + BattleNpcId int64 + DeckType int32 + BattleNpcDeckNumber int32 +} + +// EntityMBigHuntBoss is table key "m_big_hunt_boss". +type EntityMBigHuntBoss struct { + _ struct{} `msgpack:",array"` + BigHuntBossId int32 + BigHuntBossGradeGroupId int32 + NameBigHuntBossTextId int32 + BigHuntBossAssetId int32 + AttributeType int32 +} + +// EntityMBigHuntBossGradeGroup is table key "m_big_hunt_boss_grade_group". +type EntityMBigHuntBossGradeGroup struct { + _ struct{} `msgpack:",array"` + BigHuntBossGradeGroupId int32 + NecessaryScore int64 + AssetGradeIconId int32 +} + +// EntityMBigHuntBossQuest is table key "m_big_hunt_boss_quest". +type EntityMBigHuntBossQuest struct { + _ struct{} `msgpack:",array"` + BigHuntBossQuestId int32 + BigHuntBossId int32 + BigHuntQuestGroupId int32 + BigHuntBossQuestScoreCoefficientId int32 + BigHuntScoreRewardGroupScheduleId int32 + BigHuntLinkId int32 + DailyChallengeCount int32 +} + +// EntityMBigHuntQuest is table key "m_big_hunt_quest". +type EntityMBigHuntQuest struct { + _ struct{} `msgpack:",array"` + BigHuntQuestId int32 + QuestId int32 + BigHuntQuestScoreCoefficientId int32 +} + +// EntityMBigHuntQuestScoreCoefficient is table key "m_big_hunt_quest_score_coefficient". +type EntityMBigHuntQuestScoreCoefficient struct { + _ struct{} `msgpack:",array"` + BigHuntQuestScoreCoefficientId int32 + ScoreDifficultBonusPermil int32 +} + +// EntityMBigHuntRewardGroup is table key "m_big_hunt_reward_group". +type EntityMBigHuntRewardGroup struct { + _ struct{} `msgpack:",array"` + BigHuntRewardGroupId int32 + SortOrder int32 + PossessionType int32 + PossessionId int32 + Count int32 +} + +// EntityMBigHuntSchedule is table key "m_big_hunt_schedule". +type EntityMBigHuntSchedule struct { + _ struct{} `msgpack:",array"` + BigHuntScheduleId int32 + NoticeStartDatetime int64 + ChallengeStartDatetime int64 + ChallengeEndDatetime int64 + SeasonAssetId int32 +} + +// EntityMBigHuntScoreRewardGroup is table key "m_big_hunt_score_reward_group". +type EntityMBigHuntScoreRewardGroup struct { + _ struct{} `msgpack:",array"` + BigHuntScoreRewardGroupId int32 + NecessaryScore int64 + BigHuntRewardGroupId int32 +} + +// EntityMBigHuntScoreRewardGroupSchedule is table key "m_big_hunt_score_reward_group_schedule". +type EntityMBigHuntScoreRewardGroupSchedule struct { + _ struct{} `msgpack:",array"` + BigHuntScoreRewardGroupScheduleId int32 + GroupIndex int32 + BigHuntScoreRewardGroupId int32 + StartDatetime int64 +} + +// EntityMBigHuntWeeklyAttributeScoreRewardGroupSchedule is table key "m_big_hunt_weekly_attribute_score_reward_group_schedule". +type EntityMBigHuntWeeklyAttributeScoreRewardGroupSchedule struct { + _ struct{} `msgpack:",array"` + BigHuntWeeklyAttributeScoreRewardGroupScheduleId int32 + AttributeType int32 + GroupIndex int32 + BigHuntScoreRewardGroupId int32 + StartDatetime int64 +} + +// EntityMCageOrnament is table key "m_cage_ornament". +type EntityMCageOrnament struct { + _ struct{} `msgpack:",array"` + CageOrnamentId int32 + StartDatetime int64 + EndDatetime int64 + CageOrnamentRewardId int32 +} + +// EntityMCageOrnamentReward is table key "m_cage_ornament_reward". +type EntityMCageOrnamentReward struct { + _ struct{} `msgpack:",array"` + CageOrnamentRewardId int32 + PossessionType int32 + PossessionId int32 + Count int32 +} + +// EntityMCatalogCostume is table key "m_catalog_costume". +type EntityMCatalogCostume struct { + _ struct{} `msgpack:",array"` + CostumeId int32 + SortOrder int32 + CatalogTermId int32 +} + +// EntityMCatalogWeapon is table key "m_catalog_weapon". +type EntityMCatalogWeapon struct { + _ struct{} `msgpack:",array"` + WeaponId int32 + SortOrder int32 + CatalogTermId int32 +} + +// EntityMCharacterBoard is table key "m_character_board". +type EntityMCharacterBoard struct { + _ struct{} `msgpack:",array"` + CharacterBoardId int32 + CharacterBoardGroupId int32 + CharacterBoardUnlockConditionGroupId int32 + ReleaseRank int32 +} + +// EntityMCharacterBoardAbility is table key "m_character_board_ability". +type EntityMCharacterBoardAbility struct { + _ struct{} `msgpack:",array"` + CharacterBoardAbilityId int32 + CharacterBoardEffectTargetGroupId int32 + AbilityId int32 +} + +// EntityMCharacterBoardAbilityMaxLevel is table key "m_character_board_ability_max_level". +type EntityMCharacterBoardAbilityMaxLevel struct { + _ struct{} `msgpack:",array"` + CharacterId int32 + AbilityId int32 + MaxLevel int32 +} + +// EntityMCharacterBoardEffectTargetGroup is table key "m_character_board_effect_target_group". +type EntityMCharacterBoardEffectTargetGroup struct { + _ struct{} `msgpack:",array"` + CharacterBoardEffectTargetGroupId int32 + GroupIndex int32 + CharacterBoardEffectTargetType int32 + TargetValue int32 +} + +// EntityMCharacterBoardPanel is table key "m_character_board_panel". +type EntityMCharacterBoardPanel struct { + _ struct{} `msgpack:",array"` + CharacterBoardPanelId int32 + CharacterBoardId int32 + CharacterBoardPanelUnlockConditionGroupId int32 + CharacterBoardPanelReleasePossessionGroupId int32 + CharacterBoardPanelReleaseRewardGroupId int32 + CharacterBoardPanelReleaseEffectGroupId int32 + SortOrder int32 + ParentCharacterBoardPanelId int32 + PlaceIndex int32 +} + +// EntityMCharacterBoardPanelReleaseEffectGroup is table key "m_character_board_panel_release_effect_group". +type EntityMCharacterBoardPanelReleaseEffectGroup struct { + _ struct{} `msgpack:",array"` + CharacterBoardPanelReleaseEffectGroupId int32 + SortOrder int32 + CharacterBoardEffectType int32 + CharacterBoardEffectId int32 + EffectValue int32 +} + +// EntityMCharacterBoardPanelReleasePossessionGroup is table key "m_character_board_panel_release_possession_group". +type EntityMCharacterBoardPanelReleasePossessionGroup struct { + _ struct{} `msgpack:",array"` + CharacterBoardPanelReleasePossessionGroupId int32 + PossessionType int32 + PossessionId int32 + Count int32 + SortOrder int32 +} + +// EntityMCharacterBoardStatusUp is table key "m_character_board_status_up". +type EntityMCharacterBoardStatusUp struct { + _ struct{} `msgpack:",array"` + CharacterBoardStatusUpId int32 + CharacterBoardStatusUpType int32 + CharacterBoardEffectTargetGroupId int32 +} + +// EntityMCharacterRebirth is table key "m_character_rebirth". +type EntityMCharacterRebirth struct { + _ struct{} `msgpack:",array"` + CharacterId int32 + CharacterRebirthStepGroupId int32 + SortOrder int32 + CharacterAssignmentType int32 +} + +// EntityMCharacterRebirthMaterialGroup is table key "m_character_rebirth_material_group". +type EntityMCharacterRebirthMaterialGroup struct { + _ struct{} `msgpack:",array"` + CharacterRebirthMaterialGroupId int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMCharacterRebirthStepGroup is table key "m_character_rebirth_step_group". +type EntityMCharacterRebirthStepGroup struct { + _ struct{} `msgpack:",array"` + CharacterRebirthStepGroupId int32 + BeforeRebirthCount int32 + CostumeLevelLimitUp int32 + CharacterRebirthMaterialGroupId int32 +} + +// EntityMCharacterViewerField is table key "m_character_viewer_field". +type EntityMCharacterViewerField struct { + _ struct{} `msgpack:",array"` + CharacterViewerFieldId int32 + ReleaseEvaluateConditionId int32 + PublishDatetime int64 + CharacterViewerFieldAssetId int32 + AssetBackgroundId int32 + SortOrder int32 +} + +// EntityMCompanion is table key "m_companion". +type EntityMCompanion struct { + _ struct{} `msgpack:",array"` + CompanionId int32 + AttributeType int32 + CompanionCategoryType int32 + CompanionBaseStatusId int32 + CompanionStatusCalculationId int32 + SkillId int32 + CompanionAbilityGroupId int32 + ActorId int32 + ActorSkeletonId int32 + AssetVariationId int32 + CharacterMoverBattleActorAiId int32 +} + +// EntityMCompanionCategory is table key "m_companion_category". +type EntityMCompanionCategory struct { + _ struct{} `msgpack:",array"` + CompanionCategoryType int32 + EnhancementCostNumericalFunctionId int32 +} + +// EntityMCompanionEnhancementMaterial is table key "m_companion_enhancement_material". +type EntityMCompanionEnhancementMaterial struct { + _ struct{} `msgpack:",array"` + CompanionCategoryType int32 + Level int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMConfig is table key "m_config". +type EntityMConfig struct { + _ struct{} `msgpack:",array"` + ConfigKey string + Value string +} + +// EntityMConsumableItem is table key "m_consumable_item". +type EntityMConsumableItem struct { + _ struct{} `msgpack:",array"` + ConsumableItemId int32 + ConsumableItemType int32 + SortOrder int32 + SellPrice int32 + ConsumableItemTermId int32 + AssetName string + AssetCategoryId int32 + AssetVariationId int32 +} + +// EntityMCostume is table key "m_costume". +type EntityMCostume struct { + _ struct{} `msgpack:",array"` + CostumeId int32 + CharacterId int32 + ActorId int32 + CostumeAssetCategoryType int32 + ActorSkeletonId int32 + AssetVariationId int32 + SkillfulWeaponType int32 + RarityType int32 + CostumeBaseStatusId int32 + CostumeStatusCalculationId int32 + CostumeLimitBreakMaterialGroupId int32 + CostumeAbilityGroupId int32 + CostumeActiveSkillGroupId int32 + CounterSkillDetailId int32 + CharacterMoverBattleActorAiId int32 + CostumeDefaultSkillGroupId int32 + CostumeLevelBonusId int32 + DefaultActorSkillAiId int32 + CostumeEmblemAssetId int32 + BattleActorSkillAiGroupId int32 +} + +// EntityMCostumeActiveSkillEnhancementMaterial is table key "m_costume_active_skill_enhancement_material". +type EntityMCostumeActiveSkillEnhancementMaterial struct { + _ struct{} `msgpack:",array"` + CostumeActiveSkillEnhancementMaterialId int32 + SkillLevel int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMCostumeActiveSkillGroup is table key "m_costume_active_skill_group". +type EntityMCostumeActiveSkillGroup struct { + _ struct{} `msgpack:",array"` + CostumeActiveSkillGroupId int32 + CostumeLimitBreakCountLowerLimit int32 + CostumeActiveSkillId int32 + CostumeActiveSkillEnhancementMaterialId int32 +} + +// EntityMCostumeAwaken is table key "m_costume_awaken". +type EntityMCostumeAwaken struct { + _ struct{} `msgpack:",array"` + CostumeId int32 + CostumeAwakenEffectGroupId int32 + CostumeAwakenStepMaterialGroupId int32 + CostumeAwakenPriceGroupId int32 +} + +// EntityMCostumeAwakenEffectGroup is table key "m_costume_awaken_effect_group". +type EntityMCostumeAwakenEffectGroup struct { + _ struct{} `msgpack:",array"` + CostumeAwakenEffectGroupId int32 + AwakenStep int32 + CostumeAwakenEffectType int32 + CostumeAwakenEffectId int32 +} + +// EntityMCostumeAwakenItemAcquire is table key "m_costume_awaken_item_acquire". +type EntityMCostumeAwakenItemAcquire struct { + _ struct{} `msgpack:",array"` + CostumeAwakenItemAcquireId int32 + PossessionType int32 + PossessionId int32 + Count int32 +} + +// EntityMCostumeAwakenPriceGroup is table key "m_costume_awaken_price_group". +type EntityMCostumeAwakenPriceGroup struct { + _ struct{} `msgpack:",array"` + CostumeAwakenPriceGroupId int32 + AwakenStepLowerLimit int32 + Gold int32 +} + +// EntityMCostumeAwakenStatusUpGroup is table key "m_costume_awaken_status_up_group". +type EntityMCostumeAwakenStatusUpGroup struct { + _ struct{} `msgpack:",array"` + CostumeAwakenStatusUpGroupId int32 + SortOrder int32 + StatusKindType int32 + StatusCalculationType int32 + EffectValue int32 +} + +// EntityMCostumeDuplicationExchangePossessionGroup is table key "m_costume_duplication_exchange_possession_group". +type EntityMCostumeDuplicationExchangePossessionGroup struct { + _ struct{} `msgpack:",array"` + CostumeId int32 + PossessionType int32 + PossessionId int32 + Count int32 +} + +// EntityMCostumeLimitBreakMaterialGroup is table key "m_costume_limit_break_material_group". +type EntityMCostumeLimitBreakMaterialGroup struct { + _ struct{} `msgpack:",array"` + CostumeLimitBreakMaterialGroupId int32 + MaterialId int32 + Count int32 + SortOrder int32 + CostumeOverflowExchangePossessionGroupId int32 +} + +// EntityMCostumeLotteryEffect is table key "m_costume_lottery_effect". +type EntityMCostumeLotteryEffect struct { + _ struct{} `msgpack:",array"` + CostumeId int32 + SlotNumber int32 + CostumeLotteryEffectOddsGroupId int32 + CostumeLotteryEffectUnlockMaterialGroupId int32 + CostumeLotteryEffectDrawMaterialGroupId int32 + CostumeLotteryEffectReleaseScheduleId int32 +} + +// EntityMCostumeLotteryEffectMaterialGroup is table key "m_costume_lottery_effect_material_group". +type EntityMCostumeLotteryEffectMaterialGroup struct { + _ struct{} `msgpack:",array"` + CostumeLotteryEffectMaterialGroupId int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMCostumeLotteryEffectOddsGroup is table key "m_costume_lottery_effect_odds_group". +type EntityMCostumeLotteryEffectOddsGroup struct { + _ struct{} `msgpack:",array"` + CostumeLotteryEffectOddsGroupId int32 + OddsNumber int32 + Weight int32 + CostumeLotteryEffectType int32 + CostumeLotteryEffectTargetId int32 + RarityType int32 +} + +// EntityMCostumeRarity is table key "m_costume_rarity". +type EntityMCostumeRarity struct { + _ struct{} `msgpack:",array"` + RarityType int32 + CostumeLimitBreakMaterialRarityGroupId int32 + EnhancementCostByMaterialNumericalFunctionId int32 + LimitBreakCostNumericalFunctionId int32 + MaxLevelNumericalFunctionId int32 + RequiredExpForLevelUpNumericalParameterMapId int32 + ActiveSkillMaxLevelNumericalFunctionId int32 + ActiveSkillEnhancementCostNumericalFunctionId int32 +} + +// EntityMEvaluateCondition is table key "m_evaluate_condition". +type EntityMEvaluateCondition struct { + _ struct{} `msgpack:",array"` + EvaluateConditionId int32 + EvaluateConditionFunctionType int32 + EvaluateConditionEvaluateType int32 + EvaluateConditionValueGroupId int32 + NameEvaluateConditionTextId int32 +} + +// EntityMEvaluateConditionValueGroup is table key "m_evaluate_condition_value_group". +type EntityMEvaluateConditionValueGroup struct { + _ struct{} `msgpack:",array"` + EvaluateConditionValueGroupId int32 + GroupIndex int32 + Value int64 +} + +// EntityMExplore is table key "m_explore". +type EntityMExplore struct { + _ struct{} `msgpack:",array"` + ExploreId int32 + ExploreUnlockConditionId int32 + StartDatetime int64 + ConsumeItemCount int32 + RewardLotteryCount int32 +} + +// EntityMExploreGradeAsset is table key "m_explore_grade_asset". +type EntityMExploreGradeAsset struct { + _ struct{} `msgpack:",array"` + ExploreGradeId int32 + AssetGradeIconId int32 +} + +// EntityMExploreGradeScore is table key "m_explore_grade_score". +type EntityMExploreGradeScore struct { + _ struct{} `msgpack:",array"` + ExploreId int32 + NecessaryScore int32 + ExploreGradeId int32 +} + +// EntityMGachaMedal is table key "m_gacha_medal". +type EntityMGachaMedal struct { + _ struct{} `msgpack:",array"` + GachaMedalId int32 + CeilingCount int32 + ConsumableItemId int32 + ShopTransitionGachaId int32 + AutoConvertDatetime int64 + ConversionRate int32 +} + +// EntityMGimmickSequenceSchedule is table key "m_gimmick_sequence_schedule". +type EntityMGimmickSequenceSchedule struct { + _ struct{} `msgpack:",array"` + GimmickSequenceScheduleId int32 + StartDatetime int64 + EndDatetime int64 + FirstGimmickSequenceId int32 + ReleaseEvaluateConditionId int32 +} + +// EntityMLoginBonusStamp is table key "m_login_bonus_stamp". +type EntityMLoginBonusStamp struct { + _ struct{} `msgpack:",array"` + LoginBonusId int32 + LowerPageNumber int32 + StampNumber int32 + RewardPossessionType int32 + RewardPossessionId int32 + RewardCount int32 +} + +// EntityMMainQuestChapter is table key "m_main_quest_chapter". +type EntityMMainQuestChapter struct { + _ struct{} `msgpack:",array"` + MainQuestChapterId int32 + MainQuestRouteId int32 + SortOrder int32 + MainQuestSequenceGroupId int32 + PortalCageCharacterGroupId int32 + StartDatetime int64 + IsInvisibleInLibrary bool + JoinLibraryChapterId int32 +} + +// EntityMMainQuestRoute is table key "m_main_quest_route". +type EntityMMainQuestRoute struct { + _ struct{} `msgpack:",array"` + MainQuestRouteId int32 + MainQuestSeasonId int32 + SortOrder int32 + CharacterId int32 +} + +// EntityMMainQuestSequence is table key "m_main_quest_sequence". +type EntityMMainQuestSequence struct { + _ struct{} `msgpack:",array"` + MainQuestSequenceId int32 + SortOrder int32 + QuestId int32 +} + +// EntityMMaterial is table key "m_material". +type EntityMMaterial struct { + _ struct{} `msgpack:",array"` + MaterialId int32 + MaterialType int32 + RarityType int32 + WeaponType int32 + AttributeType int32 + EffectValue int32 + SellPrice int32 + AssetName string + AssetCategoryId int32 + AssetVariationId int32 + MaterialSaleObtainPossessionId int32 +} + +// EntityMMomBanner is table key "m_mom_banner". +type EntityMMomBanner struct { + _ struct{} `msgpack:",array"` + MomBannerId int32 + SortOrderDesc int32 + DestinationDomainType int32 + DestinationDomainId int32 + BannerAssetName string + IsEmphasis bool + StartDatetime int64 + EndDatetime int64 + TargetUserStatusType int32 +} + +// EntityMNumericalFunction is table key "m_numerical_function". +type EntityMNumericalFunction struct { + _ struct{} `msgpack:",array"` + NumericalFunctionId int32 + NumericalFunctionType int32 + NumericalFunctionParameterGroupId int32 +} + +// EntityMNumericalFunctionParameterGroup is table key "m_numerical_function_parameter_group". +type EntityMNumericalFunctionParameterGroup struct { + _ struct{} `msgpack:",array"` + NumericalFunctionParameterGroupId int32 + ParameterIndex int32 + ParameterValue int32 +} + +// EntityMNumericalParameterMap is table key "m_numerical_parameter_map". +type EntityMNumericalParameterMap struct { + _ struct{} `msgpack:",array"` + NumericalParameterMapId int32 + ParameterKey int32 + ParameterValue int32 +} + +// EntityMOmikuji is table key "m_omikuji". +type EntityMOmikuji struct { + _ struct{} `msgpack:",array"` + OmikujiId int32 + StartDatetime int64 + EndDatetime int64 + OmikujiAssetId int32 +} + +// EntityMParts is table key "m_parts". +type EntityMParts struct { + _ struct{} `msgpack:",array"` + PartsId int32 + RarityType int32 + PartsGroupId int32 + PartsStatusMainLotteryGroupId int32 + PartsStatusSubLotteryGroupId int32 + PartsInitialLotteryId int32 +} + +// EntityMPartsLevelUpPriceGroup is table key "m_parts_level_up_price_group". +type EntityMPartsLevelUpPriceGroup struct { + _ struct{} `msgpack:",array"` + PartsLevelUpPriceGroupId int32 + LevelLowerLimit int32 + Gold int32 +} + +// EntityMPartsLevelUpRateGroup is table key "m_parts_level_up_rate_group". +type EntityMPartsLevelUpRateGroup struct { + _ struct{} `msgpack:",array"` + PartsLevelUpRateGroupId int32 + LevelLowerLimit int32 + SuccessRatePermil int32 +} + +// EntityMPartsRarity is table key "m_parts_rarity". +type EntityMPartsRarity struct { + _ struct{} `msgpack:",array"` + RarityType int32 + PartsLevelUpRateGroupId int32 + PartsLevelUpPriceGroupId int32 + SellPriceNumericalFunctionId int32 +} + +// EntityMQuest is table key "m_quest". +type EntityMQuest struct { + _ struct{} `msgpack:",array"` + QuestId int32 + NameQuestTextId int32 + PictureBookNameQuestTextId int32 + QuestReleaseConditionListId int32 + StoryQuestTextId int32 + QuestDisplayAttributeGroupId int32 + RecommendedDeckPower int32 + QuestFirstClearRewardGroupId int32 + QuestPickupRewardGroupId int32 + QuestDeckRestrictionGroupId int32 + QuestMissionGroupId int32 + Stamina int32 + UserExp int32 + CharacterExp int32 + CostumeExp int32 + Gold int32 + DailyClearableCount int32 + IsRunInTheBackground bool + IsCountedAsQuest bool + QuestBonusId int32 + IsNotShowAfterClear bool + IsBigWinTarget bool + IsUsableSkipTicket bool + QuestReplayFlowRewardGroupId int32 + InvisibleQuestMissionGroupId int32 + FieldEffectGroupId int32 +} + +// EntityMQuestFirstClearRewardGroup is table key "m_quest_first_clear_reward_group". +type EntityMQuestFirstClearRewardGroup struct { + _ struct{} `msgpack:",array"` + QuestFirstClearRewardGroupId int32 + QuestFirstClearRewardType int32 + SortOrder int32 + PossessionType int32 + PossessionId int32 + Count int32 + IsPickup bool +} + +// EntityMQuestFirstClearRewardSwitch is table key "m_quest_first_clear_reward_switch". +type EntityMQuestFirstClearRewardSwitch struct { + _ struct{} `msgpack:",array"` + QuestId int32 + QuestFirstClearRewardGroupId int32 + SwitchConditionClearQuestId int32 +} + +// EntityMQuestMission is table key "m_quest_mission". +type EntityMQuestMission struct { + _ struct{} `msgpack:",array"` + QuestMissionId int32 + QuestMissionConditionType int32 + ConditionValue int32 + QuestMissionRewardId int32 + QuestMissionConditionValueGroupId int32 +} + +// EntityMQuestMissionGroup is table key "m_quest_mission_group". +type EntityMQuestMissionGroup struct { + _ struct{} `msgpack:",array"` + QuestMissionGroupId int32 + SortOrder int32 + QuestMissionId int32 +} + +// EntityMQuestMissionReward is table key "m_quest_mission_reward". +type EntityMQuestMissionReward struct { + _ struct{} `msgpack:",array"` + QuestMissionRewardId int32 + PossessionType int32 + PossessionId int32 + Count int32 +} + +// EntityMQuestPickupRewardGroup is table key "m_quest_pickup_reward_group". +type EntityMQuestPickupRewardGroup struct { + _ struct{} `msgpack:",array"` + QuestPickupRewardGroupId int32 + SortOrder int32 + BattleDropRewardId int32 +} + +// EntityMQuestReplayFlowRewardGroup is table key "m_quest_replay_flow_reward_group". +type EntityMQuestReplayFlowRewardGroup struct { + _ struct{} `msgpack:",array"` + QuestReplayFlowRewardGroupId int32 + SortOrder int32 + PossessionType int32 + PossessionId int32 + Count int32 +} + +// EntityMQuestScene is table key "m_quest_scene". +type EntityMQuestScene struct { + _ struct{} `msgpack:",array"` + QuestSceneId int32 + QuestId int32 + SortOrder int32 + QuestSceneType int32 + AssetBackgroundId int32 + EventMapNumberUpper int32 + EventMapNumberLower int32 + IsMainFlowQuestTarget bool + IsBattleOnlyTarget bool + QuestResultType int32 + IsStorySkipTarget bool +} + +// EntityMQuestSceneBattle is table key "m_quest_scene_battle". +type EntityMQuestSceneBattle struct { + _ struct{} `msgpack:",array"` + QuestSceneId int32 + BattleGroupId int32 + BattleDropBoxGroupId int32 + BattleFieldLocaleSettingIndex int32 + BattleEventGroupId int32 + PostProcessConfigurationIndex int32 +} + +// EntityMShop is table key "m_shop". +type EntityMShop struct { + _ struct{} `msgpack:",array"` + ShopId int32 + ShopGroupType int32 + SortOrderInShopGroup int32 + ShopType int32 + NameShopTextId int32 + ShopUpdatableLabelType int32 + ShopExchangeType int32 + ShopItemCellGroupId int32 + RelatedMainFunctionType int32 + StartDatetime int64 + EndDatetime int64 + LimitedOpenId int32 +} + +// EntityMShopItem is table key "m_shop_item". +type EntityMShopItem struct { + _ struct{} `msgpack:",array"` + ShopItemId int32 + NameShopTextId int32 + DescriptionShopTextId int32 + ShopItemContentType int32 + PriceType int32 + PriceId int32 + Price int32 + RegularPrice int32 + ShopPromotionType int32 + ShopItemLimitedStockId int32 + AssetCategoryId int32 + AssetVariationId int32 + ShopItemDecorationType int32 +} + +// EntityMShopItemCell is table key "m_shop_item_cell". +type EntityMShopItemCell struct { + _ struct{} `msgpack:",array"` + ShopItemCellId int32 + StepNumber int32 + ShopItemId int32 +} + +// EntityMShopItemCellGroup is table key "m_shop_item_cell_group". +type EntityMShopItemCellGroup struct { + _ struct{} `msgpack:",array"` + ShopItemCellGroupId int32 + ShopItemCellId int32 + SortOrder int32 + ShopItemCellTermId int32 +} + +// EntityMShopItemContentEffect is table key "m_shop_item_content_effect". +type EntityMShopItemContentEffect struct { + _ struct{} `msgpack:",array"` + ShopItemId int32 + EffectTargetType int32 + EffectValueType int32 + EffectValue int32 +} + +// EntityMShopItemContentPossession is table key "m_shop_item_content_possession". +type EntityMShopItemContentPossession struct { + _ struct{} `msgpack:",array"` + ShopItemId int32 + PossessionType int32 + PossessionId int32 + SortOrder int32 + Count int32 +} + +// EntityMShopItemLimitedStock is table key "m_shop_item_limited_stock". +type EntityMShopItemLimitedStock struct { + _ struct{} `msgpack:",array"` + ShopItemLimitedStockId int32 + MaxCount int32 + ShopItemAutoResetType int32 + ShopItemAutoResetPeriod int32 +} + +// EntityMSideStoryQuestScene is table key "m_side_story_quest_scene". +type EntityMSideStoryQuestScene struct { + _ struct{} `msgpack:",array"` + SideStoryQuestId int32 + SideStoryQuestSceneId int32 + SortOrder int32 + AssetBackgroundId int32 + EventMapNumberUpper int32 + EventMapNumberLower int32 +} + +// EntityMTutorialUnlockCondition is table key "m_tutorial_unlock_condition". +type EntityMTutorialUnlockCondition struct { + _ struct{} `msgpack:",array"` + TutorialType int32 + TutorialUnlockConditionType int32 + ConditionValue int32 +} + +// EntityMUserLevel is table key "m_user_level". +type EntityMUserLevel struct { + _ struct{} `msgpack:",array"` + UserLevel int32 + MaxStamina int32 +} + +// EntityMUserQuestSceneGrantPossession is table key "m_user_quest_scene_grant_possession". +type EntityMUserQuestSceneGrantPossession struct { + _ struct{} `msgpack:",array"` + QuestSceneId int32 + PossessionType int32 + PossessionId int32 + Count int32 + IsGift bool + IsDebug bool +} + +// EntityMWeapon is table key "m_weapon". +type EntityMWeapon struct { + _ struct{} `msgpack:",array"` + WeaponId int32 + WeaponCategoryType int32 + WeaponType int32 + AssetVariationId int32 + RarityType int32 + AttributeType int32 + IsRestrictDiscard bool + WeaponBaseStatusId int32 + WeaponStatusCalculationId int32 + WeaponSkillGroupId int32 + WeaponAbilityGroupId int32 + WeaponEvolutionMaterialGroupId int32 + WeaponEvolutionGrantPossessionGroupId int32 + WeaponStoryReleaseConditionGroupId int32 + WeaponSpecificEnhanceId int32 + WeaponSpecificLimitBreakMaterialGroupId int32 + CharacterWalkaroundRangeType int32 + IsRecyclable bool +} + +// EntityMWeaponAbilityEnhancementMaterial is table key "m_weapon_ability_enhancement_material". +type EntityMWeaponAbilityEnhancementMaterial struct { + _ struct{} `msgpack:",array"` + WeaponAbilityEnhancementMaterialId int32 + AbilityLevel int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMWeaponAbilityGroup is table key "m_weapon_ability_group". +type EntityMWeaponAbilityGroup struct { + _ struct{} `msgpack:",array"` + WeaponAbilityGroupId int32 + SlotNumber int32 + AbilityId int32 + WeaponAbilityEnhancementMaterialId int32 +} + +// EntityMWeaponAwaken is table key "m_weapon_awaken". +type EntityMWeaponAwaken struct { + _ struct{} `msgpack:",array"` + WeaponId int32 + WeaponAwakenEffectGroupId int32 + WeaponAwakenMaterialGroupId int32 + ConsumeGold int32 + LevelLimitUp int32 +} + +// EntityMWeaponAwakenMaterialGroup is table key "m_weapon_awaken_material_group". +type EntityMWeaponAwakenMaterialGroup struct { + _ struct{} `msgpack:",array"` + WeaponAwakenMaterialGroupId int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMWeaponConsumeExchangeConsumableItemGroup is table key "m_weapon_consume_exchange_consumable_item_group". +type EntityMWeaponConsumeExchangeConsumableItemGroup struct { + _ struct{} `msgpack:",array"` + WeaponId int32 + ConsumableItemId int32 + Count int32 +} + +// EntityMWeaponEvolutionGroup is table key "m_weapon_evolution_group". +type EntityMWeaponEvolutionGroup struct { + _ struct{} `msgpack:",array"` + WeaponEvolutionGroupId int32 + EvolutionOrder int32 + WeaponId int32 +} + +// EntityMWeaponEvolutionMaterialGroup is table key "m_weapon_evolution_material_group". +type EntityMWeaponEvolutionMaterialGroup struct { + _ struct{} `msgpack:",array"` + WeaponEvolutionMaterialGroupId int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMWeaponRarity is table key "m_weapon_rarity". +type EntityMWeaponRarity struct { + _ struct{} `msgpack:",array"` + RarityType int32 + BaseEnhancementObtainedExp int32 + SellPriceNumericalFunctionId int32 + MaxLevelNumericalFunctionId int32 + MaxSkillLevelNumericalFunctionId int32 + MaxAbilityLevelNumericalFunctionId int32 + RequiredExpForLevelUpNumericalParameterMapId int32 + EnhancementCostByWeaponNumericalFunctionId int32 + EnhancementCostByMaterialNumericalFunctionId int32 + SkillEnhancementCostNumericalFunctionId int32 + AbilityEnhancementCostNumericalFunctionId int32 + LimitBreakCostByWeaponNumericalFunctionId int32 + LimitBreakCostByMaterialNumericalFunctionId int32 + EvolutionCostNumericalFunctionId int32 +} + +// EntityMWeaponSkillEnhancementMaterial is table key "m_weapon_skill_enhancement_material". +type EntityMWeaponSkillEnhancementMaterial struct { + _ struct{} `msgpack:",array"` + WeaponSkillEnhancementMaterialId int32 + SkillLevel int32 + MaterialId int32 + Count int32 + SortOrder int32 +} + +// EntityMWeaponSkillGroup is table key "m_weapon_skill_group". +type EntityMWeaponSkillGroup struct { + _ struct{} `msgpack:",array"` + WeaponSkillGroupId int32 + SlotNumber int32 + SkillId int32 + WeaponSkillEnhancementMaterialId int32 +} + +// EntityMWeaponSpecificEnhance is table key "m_weapon_specific_enhance". +type EntityMWeaponSpecificEnhance struct { + _ struct{} `msgpack:",array"` + WeaponSpecificEnhanceId int32 + BaseEnhancementObtainedExp int32 + SellPriceNumericalFunctionId int32 + MaxLevelNumericalFunctionId int32 + MaxSkillLevelNumericalFunctionId int32 + MaxAbilityLevelNumericalFunctionId int32 + RequiredExpForLevelUpNumericalParameterMapId int32 + EnhancementCostByWeaponNumericalFunctionId int32 + EnhancementCostByMaterialNumericalFunctionId int32 + SkillEnhancementCostNumericalFunctionId int32 + AbilityEnhancementCostNumericalFunctionId int32 + LimitBreakCostByWeaponNumericalFunctionId int32 + LimitBreakCostByMaterialNumericalFunctionId int32 + EvolutionCostNumericalFunctionId int32 +} + +// EntityMWeaponStoryReleaseConditionGroup is table key "m_weapon_story_release_condition_group". +type EntityMWeaponStoryReleaseConditionGroup struct { + _ struct{} `msgpack:",array"` + WeaponStoryReleaseConditionGroupId int32 + StoryIndex int32 + WeaponStoryReleaseConditionType int32 + ConditionValue int32 + WeaponStoryReleaseConditionOperationGroupId int32 +} diff --git a/server/internal/masterdata/explore.go b/server/internal/masterdata/explore.go index 0935087..83d2bb7 100644 --- a/server/internal/masterdata/explore.go +++ b/server/internal/masterdata/explore.go @@ -7,48 +7,31 @@ import ( "lunar-tear/server/internal/utils" ) -type ExploreRow struct { - ExploreId int32 `json:"ExploreId"` - ConsumeItemCount int32 `json:"ConsumeItemCount"` - RewardLotteryCount int32 `json:"RewardLotteryCount"` -} - -type ExploreGradeScoreRow struct { - ExploreId int32 `json:"ExploreId"` - NecessaryScore int32 `json:"NecessaryScore"` - ExploreGradeId int32 `json:"ExploreGradeId"` -} - -type ExploreGradeAssetRow struct { - ExploreGradeId int32 `json:"ExploreGradeId"` - AssetGradeIconId int32 `json:"AssetGradeIconId"` -} - type ExploreCatalog struct { - Explores map[int32]ExploreRow - GradeScores map[int32][]ExploreGradeScoreRow // keyed by ExploreId, sorted desc by NecessaryScore - GradeAssets map[int32]int32 // gradeId -> assetGradeIconId + Explores map[int32]EntityMExplore + GradeScores map[int32][]EntityMExploreGradeScore // keyed by ExploreId, sorted desc by NecessaryScore + GradeAssets map[int32]int32 // gradeId -> assetGradeIconId } func LoadExploreCatalog() (*ExploreCatalog, error) { - explores, err := utils.ReadJSON[ExploreRow]("EntityMExploreTable.json") + explores, err := utils.ReadTable[EntityMExplore]("m_explore") if err != nil { return nil, fmt.Errorf("load explore table: %w", err) } - gradeScores, err := utils.ReadJSON[ExploreGradeScoreRow]("EntityMExploreGradeScoreTable.json") + gradeScores, err := utils.ReadTable[EntityMExploreGradeScore]("m_explore_grade_score") if err != nil { return nil, fmt.Errorf("load explore grade score table: %w", err) } - gradeAssets, err := utils.ReadJSON[ExploreGradeAssetRow]("EntityMExploreGradeAssetTable.json") + gradeAssets, err := utils.ReadTable[EntityMExploreGradeAsset]("m_explore_grade_asset") if err != nil { return nil, fmt.Errorf("load explore grade asset table: %w", err) } catalog := &ExploreCatalog{ - Explores: make(map[int32]ExploreRow, len(explores)), - GradeScores: make(map[int32][]ExploreGradeScoreRow), + Explores: make(map[int32]EntityMExplore, len(explores)), + GradeScores: make(map[int32][]EntityMExploreGradeScore), GradeAssets: make(map[int32]int32, len(gradeAssets)), } diff --git a/server/internal/masterdata/gacha.go b/server/internal/masterdata/gacha.go index a42913f..0648fef 100644 --- a/server/internal/masterdata/gacha.go +++ b/server/internal/masterdata/gacha.go @@ -10,24 +10,6 @@ import ( "lunar-tear/server/internal/utils" ) -type gachaMedalRow struct { - GachaMedalId int32 `json:"GachaMedalId"` - ShopTransitionGachaId int32 `json:"ShopTransitionGachaId"` - ConsumableItemId int32 `json:"ConsumableItemId"` - AutoConvertDatetime int64 `json:"AutoConvertDatetime"` - ConversionRate int32 `json:"ConversionRate"` -} - -type momBannerRow struct { - MomBannerId int32 `json:"MomBannerId"` - SortOrderDesc int32 `json:"SortOrderDesc"` - DestinationDomainType int32 `json:"DestinationDomainType"` - DestinationDomainId int32 `json:"DestinationDomainId"` - BannerAssetName string `json:"BannerAssetName"` - StartDatetime int64 `json:"StartDatetime"` - EndDatetime int64 `json:"EndDatetime"` -} - type GachaMedalInfo struct { GachaMedalId int32 ConsumableItemId int32 @@ -38,16 +20,16 @@ type GachaMedalInfo struct { const chapterGachaIdBase int32 = 200000 func LoadGachaCatalog() ([]store.GachaCatalogEntry, map[int32]GachaMedalInfo, error) { - medals, err := utils.ReadJSON[gachaMedalRow]("EntityMGachaMedalTable.json") + medals, err := utils.ReadTable[EntityMGachaMedal]("m_gacha_medal") if err != nil { return nil, nil, fmt.Errorf("load gacha medal table: %w", err) } - banners, err := utils.ReadJSON[momBannerRow]("EntityMMomBannerTable.json") + banners, err := utils.ReadTable[EntityMMomBanner]("m_mom_banner") if err != nil { return nil, nil, fmt.Errorf("load mom banner table: %w", err) } - gachaToMedal := make(map[int32]gachaMedalRow) + gachaToMedal := make(map[int32]EntityMGachaMedal) medalInfoByGacha := make(map[int32]GachaMedalInfo) for _, m := range medals { gachaToMedal[m.ShopTransitionGachaId] = m @@ -59,7 +41,7 @@ func LoadGachaCatalog() ([]store.GachaCatalogEntry, map[int32]GachaMedalInfo, er } } - stepupSteps := make(map[int32][]momBannerRow) + stepupSteps := make(map[int32][]EntityMMomBanner) var entries []store.GachaCatalogEntry for _, b := range banners { diff --git a/server/internal/masterdata/gacha_pool.go b/server/internal/masterdata/gacha_pool.go index 6ea0c49..0f4d5bd 100644 --- a/server/internal/masterdata/gacha_pool.go +++ b/server/internal/masterdata/gacha_pool.go @@ -46,58 +46,28 @@ type GachaCatalog struct { ShopFeaturedByMedal map[int32][]ShopFeaturedEntry // consumableId -> paired entries } -type costumePoolRow struct { - CostumeId int32 `json:"CostumeId"` - CharacterId int32 `json:"CharacterId"` - SkillfulWeaponType int32 `json:"SkillfulWeaponType"` - RarityType int32 `json:"RarityType"` -} - -type weaponPoolRow struct { - WeaponId int32 `json:"WeaponId"` - WeaponType int32 `json:"WeaponType"` - RarityType int32 `json:"RarityType"` - IsRestrictDiscard bool `json:"IsRestrictDiscard"` -} - -type catalogCostumeRow struct { - CostumeId int32 `json:"CostumeId"` - CatalogTermId int32 `json:"CatalogTermId"` -} - -type catalogWeaponRow struct { - WeaponId int32 `json:"WeaponId"` - CatalogTermId int32 `json:"CatalogTermId"` -} - -type materialPoolRow struct { - MaterialId int32 `json:"MaterialId"` - MaterialType int32 `json:"MaterialType"` - RarityType int32 `json:"RarityType"` -} - func LoadGachaPool() (*GachaCatalog, error) { - costumes, err := utils.ReadJSON[costumePoolRow]("EntityMCostumeTable.json") + costumes, err := utils.ReadTable[EntityMCostume]("m_costume") if err != nil { return nil, fmt.Errorf("load costume table: %w", err) } - weapons, err := utils.ReadJSON[weaponPoolRow]("EntityMWeaponTable.json") + weapons, err := utils.ReadTable[EntityMWeapon]("m_weapon") if err != nil { return nil, fmt.Errorf("load weapon table: %w", err) } - catalogCostumes, err := utils.ReadJSON[catalogCostumeRow]("EntityMCatalogCostumeTable.json") + catalogCostumes, err := utils.ReadTable[EntityMCatalogCostume]("m_catalog_costume") if err != nil { return nil, fmt.Errorf("load catalog costume table: %w", err) } - catalogWeapons, err := utils.ReadJSON[catalogWeaponRow]("EntityMCatalogWeaponTable.json") + catalogWeapons, err := utils.ReadTable[EntityMCatalogWeapon]("m_catalog_weapon") if err != nil { return nil, fmt.Errorf("load catalog weapon table: %w", err) } - materials, err := utils.ReadJSON[materialPoolRow]("EntityMMaterialTable.json") + materials, err := utils.ReadTable[EntityMMaterial]("m_material") if err != nil { return nil, fmt.Errorf("load material table: %w", err) } - evoGroupRows, err := utils.ReadJSON[WeaponEvolutionGroupRow]("EntityMWeaponEvolutionGroupTable.json") + evoGroupRows, err := utils.ReadTable[EntityMWeaponEvolutionGroup]("m_weapon_evolution_group") if err != nil { return nil, fmt.Errorf("load weapon evolution group table: %w", err) } @@ -414,8 +384,8 @@ func (pool *GachaCatalog) BuildBannerPools(entries []store.GachaCatalogEntry) { len(pool.BannerPools), len(allFeaturedCostumes), len(allFeaturedWeapons)) } -func buildEvolvedWeaponSet(rows []WeaponEvolutionGroupRow) map[int32]bool { - grouped := make(map[int32][]WeaponEvolutionGroupRow) +func buildEvolvedWeaponSet(rows []EntityMWeaponEvolutionGroup) map[int32]bool { + grouped := make(map[int32][]EntityMWeaponEvolutionGroup) for _, r := range rows { grouped[r.WeaponEvolutionGroupId] = append(grouped[r.WeaponEvolutionGroupId], r) } diff --git a/server/internal/masterdata/gimmick.go b/server/internal/masterdata/gimmick.go index eb8ca76..1a320ee 100644 --- a/server/internal/masterdata/gimmick.go +++ b/server/internal/masterdata/gimmick.go @@ -9,14 +9,6 @@ import ( "lunar-tear/server/internal/utils" ) -type gimmickScheduleRow struct { - GimmickSequenceScheduleId int32 `json:"GimmickSequenceScheduleId"` - StartDatetime int64 `json:"StartDatetime"` - EndDatetime int64 `json:"EndDatetime"` - FirstGimmickSequenceId int32 `json:"FirstGimmickSequenceId"` - ReleaseEvaluateConditionId int32 `json:"ReleaseEvaluateConditionId"` -} - type gimmickScheduleEntry struct { ScheduleId int32 StartDatetime int64 @@ -30,7 +22,7 @@ type GimmickCatalog struct { } func LoadGimmickCatalog(resolver *ConditionResolver) (*GimmickCatalog, error) { - rows, err := utils.ReadJSON[gimmickScheduleRow]("EntityMGimmickSequenceScheduleTable.json") + rows, err := utils.ReadTable[EntityMGimmickSequenceSchedule]("m_gimmick_sequence_schedule") if err != nil { return nil, fmt.Errorf("load gimmick sequence schedule table: %w", err) } diff --git a/server/internal/masterdata/loginbonus.go b/server/internal/masterdata/loginbonus.go index 11122de..13387e2 100644 --- a/server/internal/masterdata/loginbonus.go +++ b/server/internal/masterdata/loginbonus.go @@ -5,15 +5,6 @@ import ( "lunar-tear/server/internal/utils" ) -type loginBonusStamp struct { - LoginBonusId int32 `json:"LoginBonusId"` - LowerPageNumber int32 `json:"LowerPageNumber"` - StampNumber int32 `json:"StampNumber"` - RewardPossessionType int32 `json:"RewardPossessionType"` - RewardPossessionId int32 `json:"RewardPossessionId"` - RewardCount int32 `json:"RewardCount"` -} - type loginBonusStampKey struct { LoginBonusId int32 LowerPageNumber int32 @@ -36,7 +27,7 @@ func (c *LoginBonusCatalog) LookupStampReward(loginBonusId, pageNumber, stampNum } func LoadLoginBonusCatalog() *LoginBonusCatalog { - stamps, err := utils.ReadJSON[loginBonusStamp]("EntityMLoginBonusStampTable.json") + stamps, err := utils.ReadTable[EntityMLoginBonusStamp]("m_login_bonus_stamp") if err != nil { log.Fatalf("load login bonus stamp table: %v", err) } diff --git a/server/internal/masterdata/material.go b/server/internal/masterdata/material.go index f1f13f0..fd8e4c1 100644 --- a/server/internal/masterdata/material.go +++ b/server/internal/masterdata/material.go @@ -7,29 +7,15 @@ import ( "lunar-tear/server/internal/utils" ) -type MaterialRow struct { - MaterialId int32 `json:"MaterialId"` - MaterialType model.MaterialType `json:"MaterialType"` - WeaponType int32 `json:"WeaponType"` - EffectValue int32 `json:"EffectValue"` - SellPrice int32 `json:"SellPrice"` -} - -type numericalParameterMapRow struct { - NumericalParameterMapId int32 `json:"NumericalParameterMapId"` - ParameterKey int32 `json:"ParameterKey"` - ParameterValue int32 `json:"ParameterValue"` -} - -func LoadParameterMap() ([]numericalParameterMapRow, error) { - rows, err := utils.ReadJSON[numericalParameterMapRow]("EntityMNumericalParameterMapTable.json") +func LoadParameterMap() ([]EntityMNumericalParameterMap, error) { + rows, err := utils.ReadTable[EntityMNumericalParameterMap]("m_numerical_parameter_map") if err != nil { return nil, fmt.Errorf("load numerical parameter map table: %w", err) } return rows, nil } -func BuildExpThresholds(paramMapRows []numericalParameterMapRow, mapId int32) []int32 { +func BuildExpThresholds(paramMapRows []EntityMNumericalParameterMap, mapId int32) []int32 { maxKey := int32(0) for _, r := range paramMapRows { if r.NumericalParameterMapId == mapId && r.ParameterKey > maxKey { @@ -46,26 +32,27 @@ func BuildExpThresholds(paramMapRows []numericalParameterMapRow, mapId int32) [] } type MaterialCatalog struct { - All map[int32]MaterialRow - ByType map[model.MaterialType]map[int32]MaterialRow + All map[int32]EntityMMaterial + ByType map[model.MaterialType]map[int32]EntityMMaterial } func LoadMaterialCatalog() (*MaterialCatalog, error) { - rows, err := utils.ReadJSON[MaterialRow]("EntityMMaterialTable.json") + rows, err := utils.ReadTable[EntityMMaterial]("m_material") if err != nil { return nil, fmt.Errorf("load material table: %w", err) } catalog := &MaterialCatalog{ - All: make(map[int32]MaterialRow, len(rows)), - ByType: make(map[model.MaterialType]map[int32]MaterialRow), + All: make(map[int32]EntityMMaterial, len(rows)), + ByType: make(map[model.MaterialType]map[int32]EntityMMaterial), } for _, row := range rows { catalog.All[row.MaterialId] = row - if catalog.ByType[row.MaterialType] == nil { - catalog.ByType[row.MaterialType] = make(map[int32]MaterialRow) + mt := model.MaterialType(row.MaterialType) + if catalog.ByType[mt] == nil { + catalog.ByType[mt] = make(map[int32]EntityMMaterial) } - catalog.ByType[row.MaterialType][row.MaterialId] = row + catalog.ByType[mt][row.MaterialId] = row } return catalog, nil } diff --git a/server/internal/masterdata/memorydb/memorydb.go b/server/internal/masterdata/memorydb/memorydb.go new file mode 100644 index 0000000..c62f6b6 --- /dev/null +++ b/server/internal/masterdata/memorydb/memorydb.go @@ -0,0 +1,257 @@ +package memorydb + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/binary" + "encoding/hex" + "fmt" + "os" + + "github.com/pierrec/lz4/v4" + "github.com/vmihailenco/msgpack/v5" +) + +var tables map[string][]byte + +const ( + aesKeyHex = "36436230313332314545356536624265" + aesIVHex = "45666341656634434165356536446141" + lz4ExtCode = int8(99) +) + +func Init(binPath string) error { + encrypted, err := os.ReadFile(binPath) + if err != nil { + return fmt.Errorf("read bin.e: %w", err) + } + + decrypted, err := decrypt(encrypted) + if err != nil { + return fmt.Errorf("decrypt: %w", err) + } + + toc, dataBlob, err := parseHeader(decrypted) + if err != nil { + return fmt.Errorf("parse header: %w", err) + } + + tables = make(map[string][]byte, len(toc)) + for name, offLen := range toc { + off := offLen[0] + length := offLen[1] + if off+length > len(dataBlob) { + return fmt.Errorf("table %q: offset %d + length %d exceeds data blob size %d", name, off, length, len(dataBlob)) + } + tables[name] = dataBlob[off : off+length] + } + + return nil +} + +func TableCount() int { + return len(tables) +} + +func TableBytes(key string) ([]byte, bool) { + b, ok := tables[key] + return b, ok +} + +func ReadTable[T any](key string) ([]T, error) { + raw, ok := TableBytes(key) + if !ok { + return nil, fmt.Errorf("table %q not found in master data", key) + } + return decompressAndUnmarshal[T](raw) +} + +func decrypt(data []byte) ([]byte, error) { + key, err := hex.DecodeString(aesKeyHex) + if err != nil { + return nil, fmt.Errorf("decode key: %w", err) + } + iv, err := hex.DecodeString(aesIVHex) + if err != nil { + return nil, fmt.Errorf("decode iv: %w", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("new cipher: %w", err) + } + + if len(data)%aes.BlockSize != 0 { + return nil, fmt.Errorf("ciphertext length %d is not a multiple of block size %d", len(data), aes.BlockSize) + } + + decrypted := make([]byte, len(data)) + cbc := cipher.NewCBCDecrypter(block, iv) + cbc.CryptBlocks(decrypted, data) + + decrypted, err = pkcs7Unpad(decrypted, aes.BlockSize) + if err != nil { + return nil, fmt.Errorf("unpad: %w", err) + } + return decrypted, nil +} + +func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) { + if len(data) == 0 { + return nil, fmt.Errorf("empty data") + } + padLen := int(data[len(data)-1]) + if padLen == 0 || padLen > blockSize || padLen > len(data) { + return nil, fmt.Errorf("invalid padding length %d", padLen) + } + for _, b := range data[len(data)-padLen:] { + if int(b) != padLen { + return nil, fmt.Errorf("invalid padding byte") + } + } + return data[:len(data)-padLen], nil +} + +func parseHeader(data []byte) (map[string][2]int, []byte, error) { + // Decode the header (first msgpack object) using Decode into interface{}, + // then compute how many bytes it consumed to find the data blob start. + r := bytes.NewReader(data) + dec := msgpack.NewDecoder(r) + dec.UseLooseInterfaceDecoding(true) + + var headerRaw interface{} + if err := dec.Decode(&headerRaw); err != nil { + return nil, nil, fmt.Errorf("decode header: %w", err) + } + + headerMap, ok := headerRaw.(map[string]interface{}) + if !ok { + return nil, nil, fmt.Errorf("header is not a map, got %T", headerRaw) + } + + toc := make(map[string][2]int, len(headerMap)) + for name, val := range headerMap { + arr, ok := val.([]interface{}) + if !ok || len(arr) != 2 { + return nil, nil, fmt.Errorf("table %q: expected [offset, length] array, got %T", name, val) + } + offset, err := toInt(arr[0]) + if err != nil { + return nil, nil, fmt.Errorf("table %q offset: %w", name, err) + } + length, err := toInt(arr[1]) + if err != nil { + return nil, nil, fmt.Errorf("table %q length: %w", name, err) + } + toc[name] = [2]int{offset, length} + } + + consumed := int(int64(len(data)) - int64(r.Len())) + return toc, data[consumed:], nil +} + +func toInt(v interface{}) (int, error) { + switch n := v.(type) { + case int8: + return int(n), nil + case int16: + return int(n), nil + case int32: + return int(n), nil + case int64: + return int(n), nil + case uint8: + return int(n), nil + case uint16: + return int(n), nil + case uint32: + return int(n), nil + case uint64: + return int(n), nil + default: + return 0, fmt.Errorf("cannot convert %T to int", v) + } +} + +func decompressAndUnmarshal[T any](raw []byte) ([]T, error) { + // Peek at the raw msgpack to check if it's an ext type (LZ4 compressed) + // or a plain array. + if len(raw) == 0 { + return nil, nil + } + + // Try to decode as ext type first + dec := msgpack.NewDecoder(bytes.NewReader(raw)) + code, extData, err := decodeExt(dec) + if err == nil && code == lz4ExtCode { + uncompressedSize, lz4Data, err := readLZ4ExtHeader(extData) + if err != nil { + return nil, fmt.Errorf("read lz4 ext header: %w", err) + } + + decompressed := make([]byte, uncompressedSize) + n, err := lz4.UncompressBlock(lz4Data, decompressed) + if err != nil { + return nil, fmt.Errorf("lz4 decompress: %w", err) + } + decompressed = decompressed[:n] + + var result []T + if err := msgpack.Unmarshal(decompressed, &result); err != nil { + return nil, fmt.Errorf("unmarshal decompressed table: %w", err) + } + return result, nil + } + + // Not LZ4 compressed, try as plain array + var result []T + if err := msgpack.Unmarshal(raw, &result); err != nil { + return nil, fmt.Errorf("unmarshal plain table: %w", err) + } + return result, nil +} + +func decodeExt(dec *msgpack.Decoder) (int8, []byte, error) { + var ext msgpack.RawMessage + if err := dec.Decode(&ext); err != nil { + return 0, nil, err + } + // ext is the full msgpack ext bytes including the header. + // Re-decode just the header to get the type code, then the body is the rest. + innerDec := msgpack.NewDecoder(bytes.NewReader(ext)) + extID, extLen, err := innerDec.DecodeExtHeader() + if err != nil { + return 0, nil, err + } + extData := make([]byte, extLen) + if _, err := innerDec.Buffered().Read(extData); err != nil { + return 0, nil, fmt.Errorf("read ext data: %w", err) + } + return extID, extData, nil +} + +func readLZ4ExtHeader(data []byte) (int, []byte, error) { + if len(data) == 0 { + return 0, nil, fmt.Errorf("empty ext data") + } + tag := data[0] + switch { + case tag == 0xd2: // big-endian int32 + if len(data) < 5 { + return 0, nil, fmt.Errorf("not enough data for int32 size") + } + size := int(int32(binary.BigEndian.Uint32(data[1:5]))) + return size, data[5:], nil + case tag == 0xce: // big-endian uint32 + if len(data) < 5 { + return 0, nil, fmt.Errorf("not enough data for uint32 size") + } + size := int(binary.BigEndian.Uint32(data[1:5])) + return size, data[5:], nil + case tag <= 0x7f: // positive fixint + return int(tag), data[1:], nil + default: + return 0, nil, fmt.Errorf("unexpected tag 0x%02x in LZ4 ext header", tag) + } +} diff --git a/server/internal/masterdata/numericalfunc.go b/server/internal/masterdata/numericalfunc.go index 0c4e3b6..55ad7fd 100644 --- a/server/internal/masterdata/numericalfunc.go +++ b/server/internal/masterdata/numericalfunc.go @@ -8,18 +8,6 @@ import ( "lunar-tear/server/internal/utils" ) -type numericalFunctionRow struct { - NumericalFunctionId int32 `json:"NumericalFunctionId"` - NumericalFunctionType int32 `json:"NumericalFunctionType"` - NumericalFunctionParameterGroupId int32 `json:"NumericalFunctionParameterGroupId"` -} - -type numericalFunctionParameterRow struct { - NumericalFunctionParameterGroupId int32 `json:"NumericalFunctionParameterGroupId"` - ParameterIndex int32 `json:"ParameterIndex"` - ParameterValue int32 `json:"ParameterValue"` -} - type NumericalFunc struct { Type model.NumericalFunctionType Params []int32 @@ -61,17 +49,17 @@ type FunctionResolver struct { } func LoadFunctionResolver() (*FunctionResolver, error) { - funcRows, err := utils.ReadJSON[numericalFunctionRow]("EntityMNumericalFunctionTable.json") + funcRows, err := utils.ReadTable[EntityMNumericalFunction]("m_numerical_function") if err != nil { return nil, fmt.Errorf("load numerical function table: %w", err) } - paramRows, err := utils.ReadJSON[numericalFunctionParameterRow]("EntityMNumericalFunctionParameterGroupTable.json") + paramRows, err := utils.ReadTable[EntityMNumericalFunctionParameterGroup]("m_numerical_function_parameter_group") if err != nil { return nil, fmt.Errorf("load numerical function parameter group table: %w", err) } - paramsByGroup := make(map[int32][]numericalFunctionParameterRow, len(paramRows)) + paramsByGroup := make(map[int32][]EntityMNumericalFunctionParameterGroup, len(paramRows)) for _, r := range paramRows { paramsByGroup[r.NumericalFunctionParameterGroupId] = append( paramsByGroup[r.NumericalFunctionParameterGroupId], r) diff --git a/server/internal/masterdata/omikuji.go b/server/internal/masterdata/omikuji.go index de2063d..546802f 100644 --- a/server/internal/masterdata/omikuji.go +++ b/server/internal/masterdata/omikuji.go @@ -5,11 +5,6 @@ import ( "lunar-tear/server/internal/utils" ) -type omikujiEntry struct { - OmikujiId int32 `json:"OmikujiId"` - OmikujiAssetId int32 `json:"OmikujiAssetId"` -} - type OmikujiCatalog struct { assetIds map[int32]int32 } @@ -22,7 +17,7 @@ func (c *OmikujiCatalog) LookupAssetId(omikujiId int32) int32 { } func LoadOmikujiCatalog() *OmikujiCatalog { - entries, err := utils.ReadJSON[omikujiEntry]("EntityMOmikujiTable.json") + entries, err := utils.ReadTable[EntityMOmikuji]("m_omikuji") if err != nil { log.Fatalf("load omikuji table: %v", err) } diff --git a/server/internal/masterdata/parts.go b/server/internal/masterdata/parts.go index ee7ce09..f2b00c8 100644 --- a/server/internal/masterdata/parts.go +++ b/server/internal/masterdata/parts.go @@ -7,63 +7,37 @@ import ( "lunar-tear/server/internal/utils" ) -type PartsRow struct { - PartsId int32 `json:"PartsId"` - RarityType model.RarityType `json:"RarityType"` - PartsGroupId int32 `json:"PartsGroupId"` - PartsStatusMainLotteryGroupId int32 `json:"PartsStatusMainLotteryGroupId"` -} - -type PartsRarityRow struct { - RarityType model.RarityType `json:"RarityType"` - PartsLevelUpRateGroupId int32 `json:"PartsLevelUpRateGroupId"` - PartsLevelUpPriceGroupId int32 `json:"PartsLevelUpPriceGroupId"` - SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"` -} - -type partsLevelUpRateRow struct { - PartsLevelUpRateGroupId int32 `json:"PartsLevelUpRateGroupId"` - LevelLowerLimit int32 `json:"LevelLowerLimit"` - SuccessRatePermil int32 `json:"SuccessRatePermil"` -} - -type partsLevelUpPriceRow struct { - PartsLevelUpPriceGroupId int32 `json:"PartsLevelUpPriceGroupId"` - LevelLowerLimit int32 `json:"LevelLowerLimit"` - Gold int32 `json:"Gold"` -} - type PartsCatalog struct { - PartsById map[int32]PartsRow + PartsById map[int32]EntityMParts DefaultPartsStatusMainByLotteryGroup map[int32]int32 - RarityByRarityType map[model.RarityType]PartsRarityRow + RarityByRarityType map[model.RarityType]EntityMPartsRarity RateByGroupAndLevel map[int32]map[int32]int32 PriceByGroupAndLevel map[int32]map[int32]int32 SellPriceByRarity map[model.RarityType]NumericalFunc } func LoadPartsCatalog() (*PartsCatalog, error) { - partsRows, err := utils.ReadJSON[PartsRow]("EntityMPartsTable.json") + partsRows, err := utils.ReadTable[EntityMParts]("m_parts") if err != nil { return nil, fmt.Errorf("load parts table: %w", err) } - rarityRows, err := utils.ReadJSON[PartsRarityRow]("EntityMPartsRarityTable.json") + rarityRows, err := utils.ReadTable[EntityMPartsRarity]("m_parts_rarity") if err != nil { return nil, fmt.Errorf("load parts rarity table: %w", err) } - rateRows, err := utils.ReadJSON[partsLevelUpRateRow]("EntityMPartsLevelUpRateGroupTable.json") + rateRows, err := utils.ReadTable[EntityMPartsLevelUpRateGroup]("m_parts_level_up_rate_group") if err != nil { return nil, fmt.Errorf("load parts level up rate table: %w", err) } - priceRows, err := utils.ReadJSON[partsLevelUpPriceRow]("EntityMPartsLevelUpPriceGroupTable.json") + priceRows, err := utils.ReadTable[EntityMPartsLevelUpPriceGroup]("m_parts_level_up_price_group") if err != nil { return nil, fmt.Errorf("load parts level up price table: %w", err) } - partsById := make(map[int32]PartsRow, len(partsRows)) + partsById := make(map[int32]EntityMParts, len(partsRows)) for _, p := range partsRows { partsById[p.PartsId] = p } @@ -84,7 +58,7 @@ func LoadPartsCatalog() (*PartsCatalog, error) { return nil, fmt.Errorf("load function resolver: %w", err) } - rarityByRarityType := make(map[model.RarityType]PartsRarityRow, len(rarityRows)) + rarityByRarityType := make(map[model.RarityType]EntityMPartsRarity, len(rarityRows)) sellPriceByRarity := make(map[model.RarityType]NumericalFunc, len(rarityRows)) for _, r := range rarityRows { rarityByRarityType[r.RarityType] = r diff --git a/server/internal/masterdata/quest.go b/server/internal/masterdata/quest.go index 4a65d69..9e2c5d7 100644 --- a/server/internal/masterdata/quest.go +++ b/server/internal/masterdata/quest.go @@ -4,214 +4,34 @@ import ( "fmt" "sort" - "lunar-tear/server/internal/model" "lunar-tear/server/internal/utils" ) -type QuestSceneRow struct { - QuestSceneId int32 `json:"QuestSceneId"` - QuestId int32 `json:"QuestId"` - SortOrder int32 `json:"SortOrder"` - QuestSceneType model.QuestSceneType `json:"QuestSceneType"` - AssetBackgroundId int32 `json:"AssetBackgroundId"` - EventMapNumberUpper int32 `json:"EventMapNumberUpper"` - EventMapNumberLower int32 `json:"EventMapNumberLower"` - IsMainFlowQuestTarget bool `json:"IsMainFlowQuestTarget"` - IsBattleOnlyTarget bool `json:"IsBattleOnlyTarget"` - QuestResultType model.QuestResultType `json:"QuestResultType"` - IsStorySkipTarget bool `json:"IsStorySkipTarget"` -} - -type QuestRow struct { - QuestId int32 `json:"QuestId"` - NameQuestTextId int32 `json:"NameQuestTextId"` - PictureBookNameQuestTextId int32 `json:"PictureBookNameQuestTextId"` - QuestReleaseConditionListId int32 `json:"QuestReleaseConditionListId"` - StoryQuestTextId int32 `json:"StoryQuestTextId"` - QuestDisplayAttributeGroupId int32 `json:"QuestDisplayAttributeGroupId"` - RecommendedDeckPower int32 `json:"RecommendedDeckPower"` - QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"` - QuestPickupRewardGroupId int32 `json:"QuestPickupRewardGroupId"` - QuestDeckRestrictionGroupId int32 `json:"QuestDeckRestrictionGroupId"` - QuestMissionGroupId int32 `json:"QuestMissionGroupId"` - Stamina int32 `json:"Stamina"` - UserExp int32 `json:"UserExp"` - CharacterExp int32 `json:"CharacterExp"` - CostumeExp int32 `json:"CostumeExp"` - Gold int32 `json:"Gold"` - DailyClearableCount int32 `json:"DailyClearableCount"` - IsRunInTheBackground bool `json:"IsRunInTheBackground"` - IsCountedAsQuest bool `json:"IsCountedAsQuest"` - QuestBonusId int32 `json:"QuestBonusId"` - IsNotShowAfterClear bool `json:"IsNotShowAfterClear"` - IsBigWinTarget bool `json:"IsBigWinTarget"` - IsUsableSkipTicket bool `json:"IsUsableSkipTicket"` - QuestReplayFlowRewardGroupId int32 `json:"QuestReplayFlowRewardGroupId"` - InvisibleQuestMissionGroupId int32 `json:"InvisibleQuestMissionGroupId"` - FieldEffectGroupId int32 `json:"FieldEffectGroupId"` -} - -type QuestMissionRow struct { - QuestMissionId int32 `json:"QuestMissionId"` - QuestMissionConditionType model.QuestMissionConditionType `json:"QuestMissionConditionType"` - QuestMissionRewardId int32 `json:"QuestMissionRewardId"` - QuestMissionConditionValueGroupId int32 `json:"QuestMissionConditionValueGroupId"` -} - -type QuestMissionGroupRow struct { - QuestMissionGroupId int32 `json:"QuestMissionGroupId"` - SortOrder int32 `json:"SortOrder"` - QuestMissionId int32 `json:"QuestMissionId"` -} - -type QuestMissionRewardRow struct { - QuestMissionRewardId int32 `json:"QuestMissionRewardId"` - PossessionType model.PossessionType `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - -type MainQuestSequenceRow struct { - MainQuestSequenceId int32 `json:"MainQuestSequenceId"` - SortOrder int32 `json:"SortOrder"` - QuestId int32 `json:"QuestId"` -} - -type MainQuestRouteRow struct { - MainQuestRouteId int32 `json:"MainQuestRouteId"` - MainQuestSeasonId int32 `json:"MainQuestSeasonId"` - SortOrder int32 `json:"SortOrder"` - CharacterId int32 `json:"CharacterId"` -} - -type MainQuestChapterRow struct { - MainQuestChapterId int32 `json:"MainQuestChapterId"` - MainQuestRouteId int32 `json:"MainQuestRouteId"` - SortOrder int32 `json:"SortOrder"` - MainQuestSequenceGroupId int32 `json:"MainQuestSequenceGroupId"` - PortalCageCharacterGroupId int32 `json:"PortalCageCharacterGroupId"` - StartDatetime int64 `json:"StartDatetime"` - IsInvisibleInLibrary bool `json:"IsInvisibleInLibrary"` - JoinLibraryChapterId int32 `json:"JoinLibraryChapterId"` -} - -type QuestFirstClearRewardSwitchRow struct { - QuestId int32 `json:"QuestId"` - QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"` - SwitchConditionClearQuestId int32 `json:"SwitchConditionClearQuestId"` -} - -type QuestFirstClearRewardGroupRow struct { - QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"` - QuestFirstClearRewardType int32 `json:"QuestFirstClearRewardType"` - SortOrder int32 `json:"SortOrder"` - PossessionType model.PossessionType `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` - IsPickup bool `json:"IsPickup"` -} - -type QuestReplayFlowRewardGroupRow struct { - QuestReplayFlowRewardGroupId int32 `json:"QuestReplayFlowRewardGroupId"` - SortOrder int32 `json:"SortOrder"` - PossessionType model.PossessionType `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - -type QuestSceneGrantRow struct { - QuestSceneId int32 `json:"QuestSceneId"` - PossessionType model.PossessionType `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - -type QuestPickupRewardGroupRow struct { - QuestPickupRewardGroupId int32 `json:"QuestPickupRewardGroupId"` - SortOrder int32 `json:"SortOrder"` - BattleDropRewardId int32 `json:"BattleDropRewardId"` -} - -type BattleDropRewardRow struct { - BattleDropRewardId int32 `json:"BattleDropRewardId"` - PossessionType model.PossessionType `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - -type QuestSceneBattleRow struct { - QuestSceneId int32 `json:"QuestSceneId"` - BattleGroupId int32 `json:"BattleGroupId"` -} - -type BattleGroupRow struct { - BattleGroupId int32 `json:"BattleGroupId"` - WaveNumber int32 `json:"WaveNumber"` - BattleId int32 `json:"BattleId"` -} - -type BattleRow struct { - BattleId int32 `json:"BattleId"` - BattleNpcId int32 `json:"BattleNpcId"` - DeckType model.DeckType `json:"DeckType"` - BattleNpcDeckNumber int32 `json:"BattleNpcDeckNumber"` -} - -type BattleNpcDeckRow struct { - BattleNpcId int32 `json:"BattleNpcId"` - DeckType model.DeckType `json:"DeckType"` - BattleNpcDeckNumber int32 `json:"BattleNpcDeckNumber"` - BattleNpcDeckCharacterUuid01 string `json:"BattleNpcDeckCharacterUuid01"` - BattleNpcDeckCharacterUuid02 string `json:"BattleNpcDeckCharacterUuid02"` - BattleNpcDeckCharacterUuid03 string `json:"BattleNpcDeckCharacterUuid03"` -} - -type BattleNpcDropCategoryRow struct { - BattleNpcId int32 `json:"BattleNpcId"` - BattleNpcDeckCharacterUuid string `json:"BattleNpcDeckCharacterUuid"` - BattleDropCategoryId int32 `json:"BattleDropCategoryId"` -} - type BattleDropInfo struct { QuestSceneId int32 BattleDropCategoryId int32 } -type TutorialUnlockConditionRow struct { - TutorialType int32 `json:"TutorialType"` - TutorialUnlockConditionType int32 `json:"TutorialUnlockConditionType"` - ConditionValue int32 `json:"ConditionValue"` -} - -type RentalDeckRow struct { - BattleGroupId int32 `json:"BattleGroupId"` -} - -type UserLevelRow struct { - UserLevel int32 `json:"UserLevel"` - MaxStamina int32 `json:"MaxStamina"` -} - type QuestCatalog struct { - SceneById map[int32]QuestSceneRow - MissionById map[int32]QuestMissionRow - QuestById map[int32]QuestRow + SceneById map[int32]EntityMQuestScene + MissionById map[int32]EntityMQuestMission + QuestById map[int32]EntityMQuest MissionIdsByQuestId map[int32][]int32 RouteIdByQuestId map[int32]int32 SceneIdsByQuestId map[int32][]int32 OrderedQuestIds []int32 - FirstClearRewardsByGroupId map[int32][]QuestFirstClearRewardGroupRow - FirstClearRewardSwitchesByQuestId map[int32][]QuestFirstClearRewardSwitchRow - MissionRewardsByMissionId map[int32][]QuestMissionRewardRow + FirstClearRewardsByGroupId map[int32][]EntityMQuestFirstClearRewardGroup + FirstClearRewardSwitchesByQuestId map[int32][]EntityMQuestFirstClearRewardSwitch + MissionRewardsByMissionId map[int32][]EntityMQuestMissionReward WeaponIdsByReleaseConditionGroupId map[int32][]int32 - ReleaseConditionsByGroupId map[int32][]WeaponStoryReleaseConditionRow - SceneGrantsBySceneId map[int32][]QuestSceneGrantRow - BattleDropRewardById map[int32]BattleDropRewardRow + ReleaseConditionsByGroupId map[int32][]EntityMWeaponStoryReleaseConditionGroup + SceneGrantsBySceneId map[int32][]EntityMUserQuestSceneGrantPossession + BattleDropRewardById map[int32]EntityMBattleDropReward PickupRewardIdsByGroupId map[int32][]int32 BattleDropsByQuestId map[int32][]BattleDropInfo - ReplayFlowRewardsByGroupId map[int32][]QuestReplayFlowRewardGroupRow + ReplayFlowRewardsByGroupId map[int32][]EntityMQuestReplayFlowRewardGroup RentalQuestIds map[int32]bool - TutorialUnlockConditions []TutorialUnlockConditionRow + TutorialUnlockConditions []EntityMTutorialUnlockCondition ChapterLastSceneByQuestId map[int32]int32 SeasonIdByRouteId map[int32]int32 @@ -221,8 +41,8 @@ type QuestCatalog struct { CostumeMaxLevelByRarity map[int32]NumericalFunc MaxStaminaByLevel map[int32]int32 - CostumeById map[int32]CostumeMasterRow - WeaponById map[int32]WeaponMasterRow + CostumeById map[int32]EntityMCostume + WeaponById map[int32]EntityMWeapon WeaponSkillSlots map[int32][]int32 WeaponAbilitySlots map[int32][]int32 @@ -231,7 +51,7 @@ type QuestCatalog struct { } func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { - scenes, err := utils.ReadJSON[QuestSceneRow]("EntityMQuestSceneTable.json") + scenes, err := utils.ReadTable[EntityMQuestScene]("m_quest_scene") if err != nil { return nil, fmt.Errorf("load quest scene table: %w", err) } @@ -245,17 +65,17 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return scenes[i].QuestSceneId < scenes[j].QuestSceneId }) - missions, err := utils.ReadJSON[QuestMissionRow]("EntityMQuestMissionTable.json") + missions, err := utils.ReadTable[EntityMQuestMission]("m_quest_mission") if err != nil { return nil, fmt.Errorf("load quest mission table: %w", err) } - quests, err := utils.ReadJSON[QuestRow]("EntityMQuestTable.json") + quests, err := utils.ReadTable[EntityMQuest]("m_quest") if err != nil { return nil, fmt.Errorf("load quest table: %w", err) } - missionGroups, err := utils.ReadJSON[QuestMissionGroupRow]("EntityMQuestMissionGroupTable.json") + missionGroups, err := utils.ReadTable[EntityMQuestMissionGroup]("m_quest_mission_group") if err != nil { return nil, fmt.Errorf("load quest mission group table: %w", err) } @@ -269,7 +89,7 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return missionGroups[i].QuestMissionId < missionGroups[j].QuestMissionId }) - sequences, err := utils.ReadJSON[MainQuestSequenceRow]("EntityMMainQuestSequenceTable.json") + sequences, err := utils.ReadTable[EntityMMainQuestSequence]("m_main_quest_sequence") if err != nil { return nil, fmt.Errorf("load main quest sequence table: %w", err) } @@ -283,12 +103,12 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return sequences[i].QuestId < sequences[j].QuestId }) - chapters, err := utils.ReadJSON[MainQuestChapterRow]("EntityMMainQuestChapterTable.json") + chapters, err := utils.ReadTable[EntityMMainQuestChapter]("m_main_quest_chapter") if err != nil { return nil, fmt.Errorf("load main quest chapter table: %w", err) } - routes, err := utils.ReadJSON[MainQuestRouteRow]("EntityMMainQuestRouteTable.json") + routes, err := utils.ReadTable[EntityMMainQuestRoute]("m_main_quest_route") if err != nil { return nil, fmt.Errorf("load main quest route table: %w", err) } @@ -297,12 +117,12 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId } - firstClearSwitches, err := utils.ReadJSON[QuestFirstClearRewardSwitchRow]("EntityMQuestFirstClearRewardSwitchTable.json") + firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch") if err != nil { return nil, fmt.Errorf("load quest first clear reward switch table: %w", err) } - firstClearRewards, err := utils.ReadJSON[QuestFirstClearRewardGroupRow]("EntityMQuestFirstClearRewardGroupTable.json") + firstClearRewards, err := utils.ReadTable[EntityMQuestFirstClearRewardGroup]("m_quest_first_clear_reward_group") if err != nil { return nil, fmt.Errorf("load quest first clear reward group table: %w", err) } @@ -316,7 +136,7 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return firstClearRewards[i].QuestFirstClearRewardType < firstClearRewards[j].QuestFirstClearRewardType }) - replayFlowRewards, err := utils.ReadJSON[QuestReplayFlowRewardGroupRow]("EntityMQuestReplayFlowRewardGroupTable.json") + replayFlowRewards, err := utils.ReadTable[EntityMQuestReplayFlowRewardGroup]("m_quest_replay_flow_reward_group") if err != nil { return nil, fmt.Errorf("load quest replay flow reward group table: %w", err) } @@ -327,52 +147,52 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return replayFlowRewards[i].SortOrder < replayFlowRewards[j].SortOrder }) - missionRewards, err := utils.ReadJSON[QuestMissionRewardRow]("EntityMQuestMissionRewardTable.json") + missionRewards, err := utils.ReadTable[EntityMQuestMissionReward]("m_quest_mission_reward") if err != nil { return nil, fmt.Errorf("load quest mission reward table: %w", err) } - weapons, err := utils.ReadJSON[WeaponMasterRow]("EntityMWeaponTable.json") + weapons, err := utils.ReadTable[EntityMWeapon]("m_weapon") if err != nil { return nil, fmt.Errorf("load weapon table: %w", err) } - weaponSkillGroups, err := utils.ReadJSON[WeaponSkillGroupRow]("EntityMWeaponSkillGroupTable.json") + weaponSkillGroups, err := utils.ReadTable[EntityMWeaponSkillGroup]("m_weapon_skill_group") if err != nil { return nil, fmt.Errorf("load weapon skill group table: %w", err) } - weaponAbilityGroups, err := utils.ReadJSON[WeaponAbilityGroupRow]("EntityMWeaponAbilityGroupTable.json") + weaponAbilityGroups, err := utils.ReadTable[EntityMWeaponAbilityGroup]("m_weapon_ability_group") if err != nil { return nil, fmt.Errorf("load weapon ability group table: %w", err) } - releaseConditions, err := utils.ReadJSON[WeaponStoryReleaseConditionRow]("EntityMWeaponStoryReleaseConditionGroupTable.json") + releaseConditions, err := utils.ReadTable[EntityMWeaponStoryReleaseConditionGroup]("m_weapon_story_release_condition_group") if err != nil { return nil, fmt.Errorf("load weapon story release condition table: %w", err) } - costumeMasters, err := utils.ReadJSON[CostumeMasterRow]("EntityMCostumeTable.json") + costumeMasters, err := utils.ReadTable[EntityMCostume]("m_costume") if err != nil { return nil, fmt.Errorf("load costume table: %w", err) } - costumeRarities, err := utils.ReadJSON[costumeRarityRow]("EntityMCostumeRarityTable.json") + costumeRarities, err := utils.ReadTable[EntityMCostumeRarity]("m_costume_rarity") if err != nil { return nil, fmt.Errorf("load costume rarity table: %w", err) } - sceneGrants, err := utils.ReadJSON[QuestSceneGrantRow]("EntityMUserQuestSceneGrantPossessionTable.json") + sceneGrants, err := utils.ReadTable[EntityMUserQuestSceneGrantPossession]("m_user_quest_scene_grant_possession") if err != nil { return nil, fmt.Errorf("load quest scene grant table: %w", err) } - battleDropRewards, err := utils.ReadJSON[BattleDropRewardRow]("EntityMBattleDropRewardTable.json") + battleDropRewards, err := utils.ReadTable[EntityMBattleDropReward]("m_battle_drop_reward") if err != nil { return nil, fmt.Errorf("load battle drop reward table: %w", err) } - pickupRewardGroups, err := utils.ReadJSON[QuestPickupRewardGroupRow]("EntityMQuestPickupRewardGroupTable.json") + pickupRewardGroups, err := utils.ReadTable[EntityMQuestPickupRewardGroup]("m_quest_pickup_reward_group") if err != nil { return nil, fmt.Errorf("load quest pickup reward group table: %w", err) } @@ -383,37 +203,37 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return pickupRewardGroups[i].SortOrder < pickupRewardGroups[j].SortOrder }) - sceneBattles, err := utils.ReadJSON[QuestSceneBattleRow]("EntityMQuestSceneBattleTable.json") + sceneBattles, err := utils.ReadTable[EntityMQuestSceneBattle]("m_quest_scene_battle") if err != nil { return nil, fmt.Errorf("load quest scene battle table: %w", err) } - battleGroups, err := utils.ReadJSON[BattleGroupRow]("EntityMBattleGroupTable.json") + battleGroups, err := utils.ReadTable[EntityMBattleGroup]("m_battle_group") if err != nil { return nil, fmt.Errorf("load battle group table: %w", err) } - battles, err := utils.ReadJSON[BattleRow]("EntityMBattleTable.json") + battles, err := utils.ReadTable[EntityMBattle]("m_battle") if err != nil { return nil, fmt.Errorf("load battle table: %w", err) } - npcDecks, err := utils.ReadJSON[BattleNpcDeckRow]("EntityMBattleNpcDeckTable.json") + npcDecks, err := utils.ReadTable[EntityMBattleNpcDeck]("m_battle_npc_deck") if err != nil { return nil, fmt.Errorf("load battle npc deck table: %w", err) } - npcDropCategories, err := utils.ReadJSON[BattleNpcDropCategoryRow]("EntityMBattleNpcDeckCharacterDropCategoryTable.json") + npcDropCategories, err := utils.ReadTable[EntityMBattleNpcDeckCharacterDropCategory]("m_battle_npc_deck_character_drop_category") if err != nil { return nil, fmt.Errorf("load battle npc drop category table: %w", err) } - rentalDecks, err := utils.ReadJSON[RentalDeckRow]("EntityMBattleRentalDeckTable.json") + rentalDecks, err := utils.ReadTable[EntityMBattleRentalDeck]("m_battle_rental_deck") if err != nil { return nil, fmt.Errorf("load battle rental deck table: %w", err) } - tutorialUnlockConds, err := utils.ReadJSON[TutorialUnlockConditionRow]("EntityMTutorialUnlockConditionTable.json") + tutorialUnlockConds, err := utils.ReadTable[EntityMTutorialUnlockCondition]("m_tutorial_unlock_condition") if err != nil { return nil, fmt.Errorf("load tutorial unlock condition table: %w", err) } @@ -423,7 +243,7 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return nil, err } - userLevels, err := utils.ReadJSON[UserLevelRow]("EntityMUserLevelTable.json") + userLevels, err := utils.ReadTable[EntityMUserLevel]("m_user_level") if err != nil { return nil, fmt.Errorf("load user level table: %w", err) } @@ -450,12 +270,12 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { } } - costumeById := make(map[int32]CostumeMasterRow, len(costumeMasters)) + costumeById := make(map[int32]EntityMCostume, len(costumeMasters)) for _, cm := range costumeMasters { costumeById[cm.CostumeId] = cm } - weaponById := make(map[int32]WeaponMasterRow, len(weapons)) + weaponById := make(map[int32]EntityMWeapon, len(weapons)) for _, w := range weapons { weaponById[w.WeaponId] = w } @@ -469,19 +289,19 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { abilitySlots[row.WeaponAbilityGroupId] = append(abilitySlots[row.WeaponAbilityGroupId], row.SlotNumber) } - sceneById := make(map[int32]QuestSceneRow, len(scenes)) + sceneById := make(map[int32]EntityMQuestScene, len(scenes)) sceneIdsByQuestId := make(map[int32][]int32) for _, scene := range scenes { sceneById[scene.QuestSceneId] = scene sceneIdsByQuestId[scene.QuestId] = append(sceneIdsByQuestId[scene.QuestId], scene.QuestSceneId) } - missionById := make(map[int32]QuestMissionRow, len(missions)) + missionById := make(map[int32]EntityMQuestMission, len(missions)) for _, mission := range missions { missionById[mission.QuestMissionId] = mission } - questById := make(map[int32]QuestRow, len(quests)) + questById := make(map[int32]EntityMQuest, len(quests)) for _, quest := range quests { questById[quest.QuestId] = quest } @@ -500,7 +320,7 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { missionIdsByQuestId[questId] = append([]int32(nil), missionIds...) } - chapterBySequenceId := make(map[int32]MainQuestChapterRow, len(chapters)) + chapterBySequenceId := make(map[int32]EntityMMainQuestChapter, len(chapters)) for _, chapter := range chapters { chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter } @@ -511,12 +331,12 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { } } - sortedChapters := make([]MainQuestChapterRow, len(chapters)) + sortedChapters := make([]EntityMMainQuestChapter, len(chapters)) copy(sortedChapters, chapters) sort.Slice(sortedChapters, func(i, j int) bool { return sortedChapters[i].SortOrder < sortedChapters[j].SortOrder }) - sequencesByGroupId := make(map[int32][]MainQuestSequenceRow) + sequencesByGroupId := make(map[int32][]EntityMMainQuestSequence) for _, seq := range sequences { sequencesByGroupId[seq.MainQuestSequenceId] = append(sequencesByGroupId[seq.MainQuestSequenceId], seq) } @@ -544,25 +364,25 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { } } - firstClearRewardsByGroupId := make(map[int32][]QuestFirstClearRewardGroupRow, len(firstClearRewards)) + firstClearRewardsByGroupId := make(map[int32][]EntityMQuestFirstClearRewardGroup, len(firstClearRewards)) for _, reward := range firstClearRewards { firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId] = append( firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId], reward) } - replayFlowRewardsByGroupId := make(map[int32][]QuestReplayFlowRewardGroupRow, len(replayFlowRewards)) + replayFlowRewardsByGroupId := make(map[int32][]EntityMQuestReplayFlowRewardGroup, len(replayFlowRewards)) for _, reward := range replayFlowRewards { replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId] = append( replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId], reward) } - firstClearRewardSwitchesByQuestId := make(map[int32][]QuestFirstClearRewardSwitchRow, len(firstClearSwitches)) + firstClearRewardSwitchesByQuestId := make(map[int32][]EntityMQuestFirstClearRewardSwitch, len(firstClearSwitches)) for _, switchRow := range firstClearSwitches { firstClearRewardSwitchesByQuestId[switchRow.QuestId] = append( firstClearRewardSwitchesByQuestId[switchRow.QuestId], switchRow) } - missionRewardsByMissionId := make(map[int32][]QuestMissionRewardRow, len(missionRewards)) + missionRewardsByMissionId := make(map[int32][]EntityMQuestMissionReward, len(missionRewards)) for _, reward := range missionRewards { missionRewardsByMissionId[reward.QuestMissionRewardId] = append( missionRewardsByMissionId[reward.QuestMissionRewardId], reward) @@ -576,18 +396,18 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { } } - releaseConditionsByGroupId := make(map[int32][]WeaponStoryReleaseConditionRow) + releaseConditionsByGroupId := make(map[int32][]EntityMWeaponStoryReleaseConditionGroup) for _, c := range releaseConditions { releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId] = append( releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId], c) } - sceneGrantsBySceneId := make(map[int32][]QuestSceneGrantRow) + sceneGrantsBySceneId := make(map[int32][]EntityMUserQuestSceneGrantPossession) for _, sg := range sceneGrants { sceneGrantsBySceneId[sg.QuestSceneId] = append(sceneGrantsBySceneId[sg.QuestSceneId], sg) } - battleDropRewardById := make(map[int32]BattleDropRewardRow, len(battleDropRewards)) + battleDropRewardById := make(map[int32]EntityMBattleDropReward, len(battleDropRewards)) for _, bdr := range battleDropRewards { battleDropRewardById[bdr.BattleDropRewardId] = bdr } @@ -609,22 +429,22 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { } type npcDeckKey struct { - BattleNpcId int32 - DeckType model.DeckType + BattleNpcId int64 + DeckType int32 BattleNpcDeckNumber int32 } - npcDeckByKey := make(map[npcDeckKey]BattleNpcDeckRow, len(npcDecks)) + npcDeckByKey := make(map[npcDeckKey]EntityMBattleNpcDeck, len(npcDecks)) for _, d := range npcDecks { npcDeckByKey[npcDeckKey{d.BattleNpcId, d.DeckType, d.BattleNpcDeckNumber}] = d } - battleByIdMap := make(map[int32]BattleRow, len(battles)) + battleByIdMap := make(map[int32]EntityMBattle, len(battles)) for _, b := range battles { battleByIdMap[b.BattleId] = b } type dropCatKey struct { - BattleNpcId int32 + BattleNpcId int64 Uuid string } dropCategoryByKey := make(map[dropCatKey]int32, len(npcDropCategories)) diff --git a/server/internal/masterdata/shop.go b/server/internal/masterdata/shop.go index bb29946..65f30be 100644 --- a/server/internal/masterdata/shop.go +++ b/server/internal/masterdata/shop.go @@ -8,96 +8,47 @@ import ( "lunar-tear/server/internal/utils" ) -type ShopItemRow struct { - ShopItemId int32 `json:"ShopItemId"` - PriceType int32 `json:"PriceType"` - PriceId int32 `json:"PriceId"` - Price int32 `json:"Price"` - ShopItemLimitedStockId int32 `json:"ShopItemLimitedStockId"` -} - -type ShopContentRow struct { - ShopItemId int32 `json:"ShopItemId"` - PossessionType int32 `json:"PossessionType"` - PossessionId int32 `json:"PossessionId"` - Count int32 `json:"Count"` -} - -type ShopContentEffectRow struct { - ShopItemId int32 `json:"ShopItemId"` - EffectTargetType int32 `json:"EffectTargetType"` - EffectValueType int32 `json:"EffectValueType"` - EffectValue int32 `json:"EffectValue"` -} - -type shopItemLimitedStockRow struct { - ShopItemLimitedStockId int32 `json:"ShopItemLimitedStockId"` - MaxCount int32 `json:"MaxCount"` -} - -type shopRow struct { - ShopId int32 `json:"ShopId"` - ShopGroupType int32 `json:"ShopGroupType"` - ShopItemCellGroupId int32 `json:"ShopItemCellGroupId"` -} - -type shopItemCellGroupRow struct { - ShopItemCellGroupId int32 `json:"ShopItemCellGroupId"` - ShopItemCellId int32 `json:"ShopItemCellId"` - SortOrder int32 `json:"SortOrder"` -} - -type shopItemCellRow struct { - ShopItemCellId int32 `json:"ShopItemCellId"` - ShopItemId int32 `json:"ShopItemId"` -} - type ExchangeShopCell struct { SortOrder int32 ShopItemId int32 } type ShopCatalog struct { - Items map[int32]ShopItemRow - Contents map[int32][]ShopContentRow - Effects map[int32][]ShopContentEffectRow + Items map[int32]EntityMShopItem + Contents map[int32][]EntityMShopItemContentPossession + Effects map[int32][]EntityMShopItemContentEffect MaxStaminaMillis map[int32]int32 // level -> max stamina in millis LimitedStock map[int32]int32 // stock id -> max count ItemShopPool []int32 // shop item IDs for the replaceable item shop, sorted by cell sort order ExchangeShopCells map[int32][]ExchangeShopCell // shopId -> sorted cells for exchange shops } -type userLevelEntry struct { - UserLevel int32 `json:"UserLevel"` - MaxStamina int32 `json:"MaxStamina"` -} - func LoadShopCatalog() (*ShopCatalog, error) { - items, err := utils.ReadJSON[ShopItemRow]("EntityMShopItemTable.json") + items, err := utils.ReadTable[EntityMShopItem]("m_shop_item") if err != nil { return nil, fmt.Errorf("load shop item table: %w", err) } - contents, err := utils.ReadJSON[ShopContentRow]("EntityMShopItemContentPossessionTable.json") + contents, err := utils.ReadTable[EntityMShopItemContentPossession]("m_shop_item_content_possession") if err != nil { return nil, fmt.Errorf("load shop content possession table: %w", err) } - effects, err := utils.ReadJSON[ShopContentEffectRow]("EntityMShopItemContentEffectTable.json") + effects, err := utils.ReadTable[EntityMShopItemContentEffect]("m_shop_item_content_effect") if err != nil { return nil, fmt.Errorf("load shop content effect table: %w", err) } - userLevels, err := utils.ReadJSON[userLevelEntry]("EntityMUserLevelTable.json") + userLevels, err := utils.ReadTable[EntityMUserLevel]("m_user_level") if err != nil { return nil, fmt.Errorf("load user level table: %w", err) } - stockRows, err := utils.ReadJSON[shopItemLimitedStockRow]("EntityMShopItemLimitedStockTable.json") + stockRows, err := utils.ReadTable[EntityMShopItemLimitedStock]("m_shop_item_limited_stock") if err != nil { return nil, fmt.Errorf("load shop item limited stock table: %w", err) } catalog := &ShopCatalog{ - Items: make(map[int32]ShopItemRow, len(items)), - Contents: make(map[int32][]ShopContentRow, len(contents)), - Effects: make(map[int32][]ShopContentEffectRow, len(effects)), + Items: make(map[int32]EntityMShopItem, len(items)), + Contents: make(map[int32][]EntityMShopItemContentPossession, len(contents)), + Effects: make(map[int32][]EntityMShopItemContentEffect, len(effects)), MaxStaminaMillis: make(map[int32]int32, len(userLevels)), LimitedStock: make(map[int32]int32, len(stockRows)), } @@ -117,15 +68,15 @@ func LoadShopCatalog() (*ShopCatalog, error) { catalog.LimitedStock[row.ShopItemLimitedStockId] = row.MaxCount } - shops, err := utils.ReadJSON[shopRow]("EntityMShopTable.json") + shops, err := utils.ReadTable[EntityMShop]("m_shop") if err != nil { return nil, fmt.Errorf("load shop table: %w", err) } - cellGroups, err := utils.ReadJSON[shopItemCellGroupRow]("EntityMShopItemCellGroupTable.json") + cellGroups, err := utils.ReadTable[EntityMShopItemCellGroup]("m_shop_item_cell_group") if err != nil { return nil, fmt.Errorf("load shop item cell group table: %w", err) } - cells, err := utils.ReadJSON[shopItemCellRow]("EntityMShopItemCellTable.json") + cells, err := utils.ReadTable[EntityMShopItemCell]("m_shop_item_cell") if err != nil { return nil, fmt.Errorf("load shop item cell table: %w", err) } @@ -135,7 +86,7 @@ func LoadShopCatalog() (*ShopCatalog, error) { cellIdToItemId[c.ShopItemCellId] = c.ShopItemId } - cellGroupByCGId := make(map[int32][]shopItemCellGroupRow, len(cellGroups)) + cellGroupByCGId := make(map[int32][]EntityMShopItemCellGroup, len(cellGroups)) for _, cg := range cellGroups { cellGroupByCGId[cg.ShopItemCellGroupId] = append(cellGroupByCGId[cg.ShopItemCellGroupId], cg) } diff --git a/server/internal/masterdata/sidestory.go b/server/internal/masterdata/sidestory.go index 57c9210..8485731 100644 --- a/server/internal/masterdata/sidestory.go +++ b/server/internal/masterdata/sidestory.go @@ -5,18 +5,12 @@ import ( "lunar-tear/server/internal/utils" ) -type sideStorySceneRow struct { - SideStoryQuestId int32 `json:"SideStoryQuestId"` - SideStoryQuestSceneId int32 `json:"SideStoryQuestSceneId"` - SortOrder int32 `json:"SortOrder"` -} - type SideStoryCatalog struct { FirstSceneByQuestId map[int32]int32 } func LoadSideStoryCatalog() *SideStoryCatalog { - scenes, err := utils.ReadJSON[sideStorySceneRow]("EntityMSideStoryQuestSceneTable.json") + scenes, err := utils.ReadTable[EntityMSideStoryQuestScene]("m_side_story_quest_scene") if err != nil { log.Fatalf("load side story quest scene table: %v", err) } diff --git a/server/internal/masterdata/weapon.go b/server/internal/masterdata/weapon.go index 3971f24..ac68c20 100644 --- a/server/internal/masterdata/weapon.go +++ b/server/internal/masterdata/weapon.go @@ -9,172 +9,55 @@ import ( "lunar-tear/server/internal/utils" ) -type WeaponMasterRow struct { - WeaponId int32 `json:"WeaponId"` - RarityType int32 `json:"RarityType"` - WeaponType int32 `json:"WeaponType"` - WeaponSpecificEnhanceId int32 `json:"WeaponSpecificEnhanceId"` - WeaponSkillGroupId int32 `json:"WeaponSkillGroupId"` - WeaponAbilityGroupId int32 `json:"WeaponAbilityGroupId"` - WeaponStoryReleaseConditionGroupId int32 `json:"WeaponStoryReleaseConditionGroupId"` - WeaponEvolutionMaterialGroupId int32 `json:"WeaponEvolutionMaterialGroupId"` -} - -type WeaponStoryReleaseConditionRow struct { - WeaponStoryReleaseConditionGroupId int32 `json:"WeaponStoryReleaseConditionGroupId"` - StoryIndex int32 `json:"StoryIndex"` - WeaponStoryReleaseConditionType model.WeaponStoryReleaseConditionType `json:"WeaponStoryReleaseConditionType"` - ConditionValue int32 `json:"ConditionValue"` - WeaponStoryReleaseConditionOperationGroupId int32 `json:"WeaponStoryReleaseConditionOperationGroupId"` -} - -type WeaponSkillGroupRow struct { - WeaponSkillGroupId int32 `json:"WeaponSkillGroupId"` - SlotNumber int32 `json:"SlotNumber"` - SkillId int32 `json:"SkillId"` - WeaponSkillEnhancementMaterialId int32 `json:"WeaponSkillEnhancementMaterialId"` -} - -type WeaponAbilityGroupRow struct { - WeaponAbilityGroupId int32 `json:"WeaponAbilityGroupId"` - SlotNumber int32 `json:"SlotNumber"` - AbilityId int32 `json:"AbilityId"` - WeaponAbilityEnhancementMaterialId int32 `json:"WeaponAbilityEnhancementMaterialId"` -} - -type weaponSpecificEnhanceRow struct { - WeaponSpecificEnhanceId int32 `json:"WeaponSpecificEnhanceId"` - BaseEnhancementObtainedExp int32 `json:"BaseEnhancementObtainedExp"` - SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"` - RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"` - EnhancementCostByWeaponNumericalFunctionId int32 `json:"EnhancementCostByWeaponNumericalFunctionId"` - EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"` - MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"` - EvolutionCostNumericalFunctionId int32 `json:"EvolutionCostNumericalFunctionId"` - LimitBreakCostByWeaponNumericalFunctionId int32 `json:"LimitBreakCostByWeaponNumericalFunctionId"` - LimitBreakCostByMaterialNumericalFunctionId int32 `json:"LimitBreakCostByMaterialNumericalFunctionId"` - MaxSkillLevelNumericalFunctionId int32 `json:"MaxSkillLevelNumericalFunctionId"` - SkillEnhancementCostNumericalFunctionId int32 `json:"SkillEnhancementCostNumericalFunctionId"` - MaxAbilityLevelNumericalFunctionId int32 `json:"MaxAbilityLevelNumericalFunctionId"` - AbilityEnhancementCostNumericalFunctionId int32 `json:"AbilityEnhancementCostNumericalFunctionId"` -} - -type weaponConsumeExchangeRow struct { - WeaponId int32 `json:"WeaponId"` - ConsumableItemId int32 `json:"ConsumableItemId"` - Count int32 `json:"Count"` -} - -type WeaponEvolutionGroupRow struct { - WeaponEvolutionGroupId int32 `json:"WeaponEvolutionGroupId"` - EvolutionOrder int32 `json:"EvolutionOrder"` - WeaponId int32 `json:"WeaponId"` -} - -type WeaponEvolutionMaterialRow struct { - WeaponEvolutionMaterialGroupId int32 `json:"WeaponEvolutionMaterialGroupId"` - MaterialId int32 `json:"MaterialId"` - Count int32 `json:"Count"` - SortOrder int32 `json:"SortOrder"` -} - -type WeaponSkillEnhanceMaterialRow struct { - WeaponSkillEnhancementMaterialId int32 `json:"WeaponSkillEnhancementMaterialId"` - SkillLevel int32 `json:"SkillLevel"` - MaterialId int32 `json:"MaterialId"` - Count int32 `json:"Count"` - SortOrder int32 `json:"SortOrder"` -} - -type WeaponAbilityEnhanceMaterialRow struct { - WeaponAbilityEnhancementMaterialId int32 `json:"WeaponAbilityEnhancementMaterialId"` - AbilityLevel int32 `json:"AbilityLevel"` - MaterialId int32 `json:"MaterialId"` - Count int32 `json:"Count"` - SortOrder int32 `json:"SortOrder"` -} - -type WeaponAwakenRow struct { - WeaponId int32 `json:"WeaponId"` - WeaponAwakenEffectGroupId int32 `json:"WeaponAwakenEffectGroupId"` - WeaponAwakenMaterialGroupId int32 `json:"WeaponAwakenMaterialGroupId"` - ConsumeGold int32 `json:"ConsumeGold"` - LevelLimitUp int32 `json:"LevelLimitUp"` -} - type WeaponAwakenEffectGroupRow struct { WeaponAwakenEffectGroupId int32 `json:"WeaponAwakenEffectGroupId"` WeaponAwakenEffectType int32 `json:"WeaponAwakenEffectType"` WeaponAwakenEffectId int32 `json:"WeaponAwakenEffectId"` } -type WeaponAwakenMaterialGroupRow struct { - WeaponAwakenMaterialGroupId int32 `json:"WeaponAwakenMaterialGroupId"` - MaterialId int32 `json:"MaterialId"` - Count int32 `json:"Count"` - SortOrder int32 `json:"SortOrder"` -} - -type weaponRarityEnhanceRow struct { - RarityType int32 `json:"RarityType"` - BaseEnhancementObtainedExp int32 `json:"BaseEnhancementObtainedExp"` - SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"` - RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"` - EnhancementCostByWeaponNumericalFunctionId int32 `json:"EnhancementCostByWeaponNumericalFunctionId"` - EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"` - MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"` - EvolutionCostNumericalFunctionId int32 `json:"EvolutionCostNumericalFunctionId"` - LimitBreakCostByWeaponNumericalFunctionId int32 `json:"LimitBreakCostByWeaponNumericalFunctionId"` - LimitBreakCostByMaterialNumericalFunctionId int32 `json:"LimitBreakCostByMaterialNumericalFunctionId"` - MaxSkillLevelNumericalFunctionId int32 `json:"MaxSkillLevelNumericalFunctionId"` - SkillEnhancementCostNumericalFunctionId int32 `json:"SkillEnhancementCostNumericalFunctionId"` - MaxAbilityLevelNumericalFunctionId int32 `json:"MaxAbilityLevelNumericalFunctionId"` - AbilityEnhancementCostNumericalFunctionId int32 `json:"AbilityEnhancementCostNumericalFunctionId"` -} - type WeaponCatalog struct { - Weapons map[int32]WeaponMasterRow - Materials map[int32]MaterialRow + Weapons map[int32]EntityMWeapon + Materials map[int32]EntityMMaterial ExpByEnhanceId map[int32][]int32 GoldCostByEnhanceId map[int32]NumericalFunc MaxLevelByEnhanceId map[int32]NumericalFunc SellPriceByEnhanceId map[int32]NumericalFunc MedalsByWeaponId map[int32]map[int32]int32 // WeaponId -> ConsumableItemId -> Count EvolutionNextWeaponId map[int32]int32 - EvolutionOrder map[int32]int32 // WeaponId -> 0-based position in evolution chain - EvolutionMaterials map[int32][]WeaponEvolutionMaterialRow // WeaponEvolutionMaterialGroupId -> materials + EvolutionOrder map[int32]int32 // WeaponId -> 0-based position in evolution chain + EvolutionMaterials map[int32][]EntityMWeaponEvolutionMaterialGroup // WeaponEvolutionMaterialGroupId -> materials EvolutionCostByEnhanceId map[int32]NumericalFunc AbilitySlots map[int32][]int32 // WeaponAbilityGroupId -> slot numbers - SkillGroupsByGroupId map[int32][]WeaponSkillGroupRow - SkillEnhanceMats map[[2]int32][]WeaponSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel] + SkillGroupsByGroupId map[int32][]EntityMWeaponSkillGroup + SkillEnhanceMats map[[2]int32][]EntityMWeaponSkillEnhancementMaterial // key: [enhancementMaterialId, skillLevel] SkillMaxLevelByEnhanceId map[int32]NumericalFunc SkillCostByEnhanceId map[int32]NumericalFunc - AbilityGroupsByGroupId map[int32][]WeaponAbilityGroupRow - AbilityEnhanceMats map[[2]int32][]WeaponAbilityEnhanceMaterialRow // key: [enhancementMaterialId, abilityLevel] + AbilityGroupsByGroupId map[int32][]EntityMWeaponAbilityGroup + AbilityEnhanceMats map[[2]int32][]EntityMWeaponAbilityEnhancementMaterial // key: [enhancementMaterialId, abilityLevel] AbilityMaxLevelByEnhanceId map[int32]NumericalFunc AbilityCostByEnhanceId map[int32]NumericalFunc EnhanceCostByWeaponByEnhanceId map[int32]NumericalFunc LimitBreakCostByWeaponByEnhanceId map[int32]NumericalFunc LimitBreakCostByMaterialByEnhanceId map[int32]NumericalFunc BaseExpByEnhanceId map[int32]int32 - ReleaseConditionsByGroupId map[int32][]WeaponStoryReleaseConditionRow + ReleaseConditionsByGroupId map[int32][]EntityMWeaponStoryReleaseConditionGroup - AwakenByWeaponId map[int32]WeaponAwakenRow - AwakenMaterialsByGroupId map[int32][]WeaponAwakenMaterialGroupRow + AwakenByWeaponId map[int32]EntityMWeaponAwaken + AwakenMaterialsByGroupId map[int32][]EntityMWeaponAwakenMaterialGroup } func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { - weapons, err := utils.ReadJSON[WeaponMasterRow]("EntityMWeaponTable.json") + weapons, err := utils.ReadTable[EntityMWeapon]("m_weapon") if err != nil { return nil, fmt.Errorf("load weapon table: %w", err) } - enhanceRows, err := utils.ReadJSON[weaponSpecificEnhanceRow]("EntityMWeaponSpecificEnhanceTable.json") + enhanceRows, err := utils.ReadTable[EntityMWeaponSpecificEnhance]("m_weapon_specific_enhance") if err != nil { return nil, fmt.Errorf("load weapon specific enhance table: %w", err) } - rarityEnhanceRows, err := utils.ReadJSON[weaponRarityEnhanceRow]("EntityMWeaponRarityTable.json") + rarityEnhanceRows, err := utils.ReadTable[EntityMWeaponRarity]("m_weapon_rarity") if err != nil { return nil, fmt.Errorf("load weapon rarity table: %w", err) } @@ -189,51 +72,51 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { return nil, fmt.Errorf("load function resolver: %w", err) } - exchangeRows, err := utils.ReadJSON[weaponConsumeExchangeRow]("EntityMWeaponConsumeExchangeConsumableItemGroupTable.json") + exchangeRows, err := utils.ReadTable[EntityMWeaponConsumeExchangeConsumableItemGroup]("m_weapon_consume_exchange_consumable_item_group") if err != nil { return nil, fmt.Errorf("load weapon consume exchange table: %w", err) } - evoGroupRows, err := utils.ReadJSON[WeaponEvolutionGroupRow]("EntityMWeaponEvolutionGroupTable.json") + evoGroupRows, err := utils.ReadTable[EntityMWeaponEvolutionGroup]("m_weapon_evolution_group") if err != nil { return nil, fmt.Errorf("load weapon evolution group table: %w", err) } - evoMatRows, err := utils.ReadJSON[WeaponEvolutionMaterialRow]("EntityMWeaponEvolutionMaterialGroupTable.json") + evoMatRows, err := utils.ReadTable[EntityMWeaponEvolutionMaterialGroup]("m_weapon_evolution_material_group") if err != nil { return nil, fmt.Errorf("load weapon evolution material group table: %w", err) } - abilityGroupRows, err := utils.ReadJSON[WeaponAbilityGroupRow]("EntityMWeaponAbilityGroupTable.json") + abilityGroupRows, err := utils.ReadTable[EntityMWeaponAbilityGroup]("m_weapon_ability_group") if err != nil { return nil, fmt.Errorf("load weapon ability group table: %w", err) } - skillGroupRows, err := utils.ReadJSON[WeaponSkillGroupRow]("EntityMWeaponSkillGroupTable.json") + skillGroupRows, err := utils.ReadTable[EntityMWeaponSkillGroup]("m_weapon_skill_group") if err != nil { return nil, fmt.Errorf("load weapon skill group table: %w", err) } - skillMatRows, err := utils.ReadJSON[WeaponSkillEnhanceMaterialRow]("EntityMWeaponSkillEnhancementMaterialTable.json") + skillMatRows, err := utils.ReadTable[EntityMWeaponSkillEnhancementMaterial]("m_weapon_skill_enhancement_material") if err != nil { return nil, fmt.Errorf("load weapon skill enhancement material table: %w", err) } - abilityMatRows, err := utils.ReadJSON[WeaponAbilityEnhanceMaterialRow]("EntityMWeaponAbilityEnhancementMaterialTable.json") + abilityMatRows, err := utils.ReadTable[EntityMWeaponAbilityEnhancementMaterial]("m_weapon_ability_enhancement_material") if err != nil { return nil, fmt.Errorf("load weapon ability enhancement material table: %w", err) } - releaseConditions, err := utils.ReadJSON[WeaponStoryReleaseConditionRow]("EntityMWeaponStoryReleaseConditionGroupTable.json") + releaseConditions, err := utils.ReadTable[EntityMWeaponStoryReleaseConditionGroup]("m_weapon_story_release_condition_group") if err != nil { return nil, fmt.Errorf("load weapon story release condition table: %w", err) } - awakenRows, err := utils.ReadJSON[WeaponAwakenRow]("EntityMWeaponAwakenTable.json") + awakenRows, err := utils.ReadTable[EntityMWeaponAwaken]("m_weapon_awaken") if err != nil { return nil, fmt.Errorf("load weapon awaken table: %w", err) } - awakenMatRows, err := utils.ReadJSON[WeaponAwakenMaterialGroupRow]("EntityMWeaponAwakenMaterialGroupTable.json") + awakenMatRows, err := utils.ReadTable[EntityMWeaponAwakenMaterialGroup]("m_weapon_awaken_material_group") if err != nil { return nil, fmt.Errorf("load weapon awaken material group table: %w", err) } catalog := &WeaponCatalog{ - Weapons: make(map[int32]WeaponMasterRow, len(weapons)), + Weapons: make(map[int32]EntityMWeapon, len(weapons)), Materials: matCatalog.ByType[model.MaterialTypeWeaponEnhancement], ExpByEnhanceId: make(map[int32][]int32, len(enhanceRows)), GoldCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), @@ -242,25 +125,25 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { MedalsByWeaponId: make(map[int32]map[int32]int32), EvolutionNextWeaponId: make(map[int32]int32), EvolutionOrder: make(map[int32]int32), - EvolutionMaterials: make(map[int32][]WeaponEvolutionMaterialRow), + EvolutionMaterials: make(map[int32][]EntityMWeaponEvolutionMaterialGroup), EvolutionCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), AbilitySlots: make(map[int32][]int32), - SkillGroupsByGroupId: make(map[int32][]WeaponSkillGroupRow), - SkillEnhanceMats: make(map[[2]int32][]WeaponSkillEnhanceMaterialRow), + SkillGroupsByGroupId: make(map[int32][]EntityMWeaponSkillGroup), + SkillEnhanceMats: make(map[[2]int32][]EntityMWeaponSkillEnhancementMaterial), SkillMaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), SkillCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), - AbilityGroupsByGroupId: make(map[int32][]WeaponAbilityGroupRow), - AbilityEnhanceMats: make(map[[2]int32][]WeaponAbilityEnhanceMaterialRow), + AbilityGroupsByGroupId: make(map[int32][]EntityMWeaponAbilityGroup), + AbilityEnhanceMats: make(map[[2]int32][]EntityMWeaponAbilityEnhancementMaterial), AbilityMaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), AbilityCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), EnhanceCostByWeaponByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), LimitBreakCostByWeaponByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), LimitBreakCostByMaterialByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), BaseExpByEnhanceId: make(map[int32]int32, len(enhanceRows)), - ReleaseConditionsByGroupId: make(map[int32][]WeaponStoryReleaseConditionRow), + ReleaseConditionsByGroupId: make(map[int32][]EntityMWeaponStoryReleaseConditionGroup), - AwakenByWeaponId: make(map[int32]WeaponAwakenRow, len(awakenRows)), - AwakenMaterialsByGroupId: make(map[int32][]WeaponAwakenMaterialGroupRow), + AwakenByWeaponId: make(map[int32]EntityMWeaponAwaken, len(awakenRows)), + AwakenMaterialsByGroupId: make(map[int32][]EntityMWeaponAwakenMaterialGroup), } for _, w := range weapons { @@ -338,7 +221,7 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { catalog.MedalsByWeaponId[ex.WeaponId][ex.ConsumableItemId] = ex.Count } - grouped := make(map[int32][]WeaponEvolutionGroupRow) + grouped := make(map[int32][]EntityMWeaponEvolutionGroup) for _, row := range evoGroupRows { grouped[row.WeaponEvolutionGroupId] = append(grouped[row.WeaponEvolutionGroupId], row) } @@ -399,7 +282,7 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { // Rarity-based enhancement fallback: for weapons with WeaponSpecificEnhanceId == 0, // use EntityMWeaponRarityTable curves via synthetic enhance IDs (-RarityType). - rarityByType := make(map[int32]weaponRarityEnhanceRow, len(rarityEnhanceRows)) + rarityByType := make(map[int32]EntityMWeaponRarity, len(rarityEnhanceRows)) for _, r := range rarityEnhanceRows { rarityByType[r.RarityType] = r } diff --git a/server/internal/model/platform.go b/server/internal/model/platform.go new file mode 100644 index 0000000..e4df295 --- /dev/null +++ b/server/internal/model/platform.go @@ -0,0 +1,77 @@ +package model + +import ( + "context" + "strconv" + + "google.golang.org/grpc/metadata" +) + +type ClientPlatform struct { + OsType int32 // 1=iOS, 2=Android + PlatformType int32 // 1=AppStore, 2=GooglePlay, 8=Amazon +} + +const ( + OsTypeIOS int32 = 1 + OsTypeAndroid int32 = 2 + + PlatformTypeAppStore int32 = 1 + PlatformTypeGooglePlayStore int32 = 2 + PlatformTypeAmazonAppStore int32 = 8 +) + +var DefaultPlatform = ClientPlatform{OsType: OsTypeAndroid, PlatformType: PlatformTypeGooglePlayStore} + +type platformKey struct{} + +func (p ClientPlatform) String() string { + os := "unknown" + switch p.OsType { + case OsTypeIOS: + os = "iOS" + case OsTypeAndroid: + os = "Android" + } + plat := "unknown" + switch p.PlatformType { + case PlatformTypeAppStore: + plat = "AppStore" + case PlatformTypeGooglePlayStore: + plat = "GooglePlay" + case PlatformTypeAmazonAppStore: + plat = "Amazon" + } + return os + "/" + plat +} + +func ClientPlatformFromHeaders(ctx context.Context) ClientPlatform { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return DefaultPlatform + } + + p := DefaultPlatform + if vals := md.Get("x-apb-os-type"); len(vals) > 0 { + if v, err := strconv.ParseInt(vals[0], 10, 32); err == nil { + p.OsType = int32(v) + } + } + if vals := md.Get("x-apb-platform-type"); len(vals) > 0 { + if v, err := strconv.ParseInt(vals[0], 10, 32); err == nil { + p.PlatformType = int32(v) + } + } + return p +} + +func NewContextWithPlatform(ctx context.Context, p ClientPlatform) context.Context { + return context.WithValue(ctx, platformKey{}, p) +} + +func ClientPlatformFromContext(ctx context.Context) ClientPlatform { + if p, ok := ctx.Value(platformKey{}).(ClientPlatform); ok { + return p + } + return DefaultPlatform +} diff --git a/server/internal/questflow/event_quest.go b/server/internal/questflow/event_quest.go index d23aec0..b1a508f 100644 --- a/server/internal/questflow/event_quest.go +++ b/server/internal/questflow/event_quest.go @@ -83,7 +83,7 @@ func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, ques h.applySceneGrants(user, questSceneId, nowMillis) - if scene.QuestResultType == model.QuestResultTypeHalfResult { + if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult { h.clearQuestMissions(user, scene.QuestId, nowMillis) } } diff --git a/server/internal/questflow/extra_quest.go b/server/internal/questflow/extra_quest.go index 58c6cb6..60e4ac2 100644 --- a/server/internal/questflow/extra_quest.go +++ b/server/internal/questflow/extra_quest.go @@ -80,7 +80,7 @@ func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, ques h.applySceneGrants(user, questSceneId, nowMillis) - if scene.QuestResultType == model.QuestResultTypeHalfResult { + if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult { h.clearQuestMissions(user, scene.QuestId, nowMillis) } } diff --git a/server/internal/questflow/handler.go b/server/internal/questflow/handler.go index 13c3eae..b9bcd93 100644 --- a/server/internal/questflow/handler.go +++ b/server/internal/questflow/handler.go @@ -53,7 +53,7 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter { for i, r := range rows { conds[i] = store.WeaponStoryReleaseCond{ StoryIndex: r.StoryIndex, - WeaponStoryReleaseConditionType: r.WeaponStoryReleaseConditionType, + WeaponStoryReleaseConditionType: model.WeaponStoryReleaseConditionType(r.WeaponStoryReleaseConditionType), ConditionValue: r.ConditionValue, } } diff --git a/server/internal/questflow/quest.go b/server/internal/questflow/quest.go index 500b115..b491276 100644 --- a/server/internal/questflow/quest.go +++ b/server/internal/questflow/quest.go @@ -23,7 +23,7 @@ func (h *QuestHandler) initQuestState(user *store.UserState, questId int32) { } } -func isMainQuestPlayable(quest masterdata.QuestRow) bool { +func isMainQuestPlayable(quest masterdata.EntityMQuest) bool { return !quest.IsRunInTheBackground && quest.IsCountedAsQuest } diff --git a/server/internal/questflow/rewards.go b/server/internal/questflow/rewards.go index d0608ed..32df3fc 100644 --- a/server/internal/questflow/rewards.go +++ b/server/internal/questflow/rewards.go @@ -20,10 +20,10 @@ func (h *QuestHandler) isQuestCleared(user *store.UserState, questId int32) bool return quest.QuestStateType == model.UserQuestStateTypeCleared } -func appendMissionRewards(dst []RewardGrant, src []masterdata.QuestMissionRewardRow) []RewardGrant { +func appendMissionRewards(dst []RewardGrant, src []masterdata.EntityMQuestMissionReward) []RewardGrant { for _, r := range src { dst = append(dst, RewardGrant{ - PossessionType: r.PossessionType, + PossessionType: model.PossessionType(r.PossessionType), PossessionId: r.PossessionId, Count: r.Count, }) @@ -31,7 +31,7 @@ func appendMissionRewards(dst []RewardGrant, src []masterdata.QuestMissionReward return dst } -func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef masterdata.QuestRow) int32 { +func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef masterdata.EntityMQuest) int32 { rewardGroupId := questDef.QuestFirstClearRewardGroupId for _, switchRow := range h.FirstClearRewardSwitchesByQuestId[questDef.QuestId] { if h.isQuestCleared(user, switchRow.SwitchConditionClearQuestId) { @@ -57,7 +57,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3 rewardGroupId := h.firstClearRewardGroupId(user, questDef) for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] { outcome.FirstClearRewards = append(outcome.FirstClearRewards, RewardGrant{ - PossessionType: reward.PossessionType, + PossessionType: model.PossessionType(reward.PossessionType), PossessionId: reward.PossessionId, Count: reward.Count, }) @@ -67,7 +67,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3 if user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow) && questDef.QuestReplayFlowRewardGroupId > 0 { for _, reward := range h.ReplayFlowRewardsByGroupId[questDef.QuestReplayFlowRewardGroupId] { outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{ - PossessionType: reward.PossessionType, + PossessionType: model.PossessionType(reward.PossessionType), PossessionId: reward.PossessionId, Count: reward.Count, }) @@ -78,7 +78,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3 regularMissionCount := 0 for _, questMissionId := range h.MissionIdsByQuestId[questId] { missionDef, ok := h.MissionById[questMissionId] - if !ok || missionDef.QuestMissionConditionType == model.QuestMissionConditionTypeComplete { + if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) == model.QuestMissionConditionTypeComplete { continue } regularMissionCount++ @@ -103,7 +103,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3 if allRegularWillClear { for _, questMissionId := range h.MissionIdsByQuestId[questId] { missionDef, ok := h.MissionById[questMissionId] - if !ok || missionDef.QuestMissionConditionType != model.QuestMissionConditionTypeComplete { + if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) != model.QuestMissionConditionTypeComplete { continue } key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId} @@ -122,7 +122,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3 return outcome } -func (h *QuestHandler) computeDropRewards(questDef masterdata.QuestRow) []RewardGrant { +func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest) []RewardGrant { if questDef.QuestPickupRewardGroupId == 0 { return nil } @@ -130,7 +130,7 @@ func (h *QuestHandler) computeDropRewards(questDef masterdata.QuestRow) []Reward for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] { if bdr, ok := h.BattleDropRewardById[dropId]; ok { drops = append(drops, RewardGrant{ - PossessionType: bdr.PossessionType, + PossessionType: model.PossessionType(bdr.PossessionType), PossessionId: bdr.PossessionId, Count: bdr.Count, }) @@ -257,7 +257,7 @@ func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, n rewardGroupId := h.firstClearRewardGroupId(user, questDef) for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] { - h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis) + h.applyRewardPossession(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis) } } @@ -365,7 +365,7 @@ func (h *QuestHandler) grantWeaponStoryUnlocksForQuestScene(user *store.UserStat } rewardGroupId := h.firstClearRewardGroupId(user, questDef) for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] { - if reward.PossessionType != model.PossessionTypeWeapon { + if model.PossessionType(reward.PossessionType) != model.PossessionTypeWeapon { continue } weaponId := reward.PossessionId @@ -375,7 +375,7 @@ func (h *QuestHandler) grantWeaponStoryUnlocksForQuestScene(user *store.UserStat } groupId := weapon.WeaponStoryReleaseConditionGroupId for _, cond := range h.ReleaseConditionsByGroupId[groupId] { - if cond.WeaponStoryReleaseConditionType == model.WeaponStoryReleaseConditionTypeAcquisition && cond.ConditionValue == 0 { + if model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) == model.WeaponStoryReleaseConditionTypeAcquisition && cond.ConditionValue == 0 { if h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) { changedIds = append(changedIds, weaponId) } @@ -387,7 +387,7 @@ func (h *QuestHandler) grantWeaponStoryUnlocksForQuestScene(user *store.UserStat if resultType == model.QuestResultTypeFullResult { for groupId, conditions := range h.ReleaseConditionsByGroupId { for _, cond := range conditions { - if cond.WeaponStoryReleaseConditionType == model.WeaponStoryReleaseConditionTypeQuestClear && cond.ConditionValue == questId { + if model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) == model.WeaponStoryReleaseConditionTypeQuestClear && cond.ConditionValue == questId { for _, weaponId := range h.WeaponIdsByReleaseConditionGroupId[groupId] { if h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) { changedIds = append(changedIds, weaponId) diff --git a/server/internal/questflow/scene.go b/server/internal/questflow/scene.go index b2d25a5..ae6ccc1 100644 --- a/server/internal/questflow/scene.go +++ b/server/internal/questflow/scene.go @@ -15,7 +15,7 @@ func (h *QuestHandler) applySceneGrants(user *store.UserState, questSceneId int3 return } for _, g := range grants { - h.applyRewardPossession(user, g.PossessionType, g.PossessionId, g.Count, nowMillis) + h.applyRewardPossession(user, model.PossessionType(g.PossessionType), g.PossessionId, g.Count, nowMillis) } } @@ -123,7 +123,7 @@ func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, quest } if isMainQuestPlayable(quest) { - if scene.QuestResultType == model.QuestResultTypeHalfResult { + if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult { nowMillis := gametime.NowMillis() h.clearQuestMissions(user, quest.QuestId, nowMillis) } diff --git a/server/internal/service/asset_resolver.go b/server/internal/service/asset_resolver.go index f9d56e7..fd5a5b5 100644 --- a/server/internal/service/asset_resolver.go +++ b/server/internal/service/asset_resolver.go @@ -20,7 +20,9 @@ type assetResolution struct { Candidates []assetCandidate } -type assetResolver struct{} +type assetResolver struct { + baseDir string +} func newRevisionTracker() *revisionTracker { return &revisionTracker{ @@ -28,8 +30,8 @@ func newRevisionTracker() *revisionTracker { } } -func newAssetResolver() *assetResolver { - return &assetResolver{} +func newAssetResolver(baseDir string) *assetResolver { + return &assetResolver{baseDir: baseDir} } func normalizeClientAddr(remoteAddr string) string { @@ -73,7 +75,7 @@ func (r *assetResolver) Resolve(objectId, assetType, activeRevision string) (ass resolution := assetResolution{ActiveRevision: activeRevision} revision := activeRevision - candidates, listSize, ok := objectIdToFilePathCandidates(revision, assetType, objectId) + candidates, listSize, ok := objectIdToFilePathCandidates(r.baseDir, revision, assetType, objectId) if ok && len(candidates) > 0 { resolution.ListRevision = revision resolution.ListSize = listSize @@ -94,6 +96,6 @@ func (r *assetResolver) Prewarm(activeRevision string) { if activeRevision == "" { return } - _, _ = loadListBinIndex(activeRevision) - _ = loadInfoIndex(activeRevision) + _, _ = loadListBinIndex(r.baseDir, activeRevision) + _ = loadInfoIndex(r.baseDir, activeRevision) } diff --git a/server/internal/service/banner.go b/server/internal/service/banner.go index 607d0e3..408d90e 100644 --- a/server/internal/service/banner.go +++ b/server/internal/service/banner.go @@ -6,7 +6,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type BannerServiceServer struct { @@ -44,6 +43,5 @@ func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMama TermLimitedGacha: termLimited, LatestChapterGacha: latestChapter, IsExistUnreadPop: false, - DiffUserData: userdata.EmptyDiff(), }, nil } diff --git a/server/internal/service/battle.go b/server/internal/service/battle.go index 1fa3832..d54320f 100644 --- a/server/internal/service/battle.go +++ b/server/internal/service/battle.go @@ -7,7 +7,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type BattleServiceServer struct { @@ -22,7 +21,7 @@ func NewBattleServiceServer(users store.UserRepository, sessions store.SessionRe func (s *BattleServiceServer) StartWave(ctx context.Context, req *pb.StartWaveRequest) (*pb.StartWaveResponse, error) { log.Printf("[BattleService] StartWave: userParty=%d npcParty=%d", len(req.UserPartyInitialInfoList), len(req.NpcPartyInitialInfoList)) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) s.users.UpdateUser(userId, func(user *store.UserState) { user.Battle.IsActive = true user.Battle.StartCount++ @@ -30,15 +29,13 @@ func (s *BattleServiceServer) StartWave(ctx context.Context, req *pb.StartWaveRe user.Battle.LastUserPartyCount = int32(len(req.UserPartyInitialInfoList)) user.Battle.LastNpcPartyCount = int32(len(req.NpcPartyInitialInfoList)) }) - return &pb.StartWaveResponse{ - DiffUserData: userdata.EmptyDiff(), - }, nil + return &pb.StartWaveResponse{}, nil } func (s *BattleServiceServer) FinishWave(ctx context.Context, req *pb.FinishWaveRequest) (*pb.FinishWaveResponse, error) { log.Printf("[BattleService] FinishWave: battleBinary=%d userParty=%d npcParty=%d elapsedFrames=%d", len(req.BattleBinary), len(req.UserPartyResultInfoList), len(req.NpcPartyResultInfoList), req.ElapsedFrameCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) s.users.UpdateUser(userId, func(user *store.UserState) { user.Battle.IsActive = false user.Battle.FinishCount++ @@ -48,7 +45,5 @@ func (s *BattleServiceServer) FinishWave(ctx context.Context, req *pb.FinishWave user.Battle.LastBattleBinarySize = int32(len(req.BattleBinary)) user.Battle.LastElapsedFrameCount = req.ElapsedFrameCount }) - return &pb.FinishWaveResponse{ - DiffUserData: userdata.EmptyDiff(), - }, nil + return &pb.FinishWaveResponse{}, nil } diff --git a/server/internal/service/cageornament.go b/server/internal/service/cageornament.go index f53960b..920857b 100644 --- a/server/internal/service/cageornament.go +++ b/server/internal/service/cageornament.go @@ -9,7 +9,6 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type CageOrnamentServiceServer struct { @@ -32,9 +31,9 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R log.Fatalf("[CageOrnamentService] ReceiveReward: no reward for cageOrnamentId=%d", req.CageOrnamentId) } - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { user.CageOrnamentRewards[req.CageOrnamentId] = store.CageOrnamentRewardState{ CageOrnamentId: req.CageOrnamentId, AcquisitionDatetime: nowMillis, @@ -43,17 +42,6 @@ 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.ProjectTables(user, - []string{ - "IUserMaterial", "IUserConsumableItem", "IUserGem", - "IUserCostume", "IUserCostumeActiveSkill", "IUserCharacter", - "IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", - "IUserWeaponNote", - "IUserCageOrnamentReward", - }, - )) - userdata.AddWeaponStoryDiff(diff, user, s.granter.DrainChangedStoryWeaponIds()) - return &pb.ReceiveRewardResponse{ CageOrnamentReward: []*pb.CageOrnamentReward{ { @@ -62,16 +50,15 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R Count: reward.Count, }, }, - DiffUserData: diff, }, nil } func (s *CageOrnamentServiceServer) RecordAccess(ctx context.Context, req *pb.RecordAccessRequest) (*pb.RecordAccessResponse, error) { log.Printf("[CageOrnamentService] RecordAccess: cageOrnamentId=%d", req.CageOrnamentId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { if _, exists := user.CageOrnamentRewards[req.CageOrnamentId]; !exists { user.CageOrnamentRewards[req.CageOrnamentId] = store.CageOrnamentRewardState{ CageOrnamentId: req.CageOrnamentId, @@ -81,11 +68,5 @@ func (s *CageOrnamentServiceServer) RecordAccess(ctx context.Context, req *pb.Re } }) - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user, - []string{"IUserCageOrnamentReward"}, - )) - - return &pb.RecordAccessResponse{ - DiffUserData: diff, - }, nil + return &pb.RecordAccessResponse{}, nil } diff --git a/server/internal/service/character.go b/server/internal/service/character.go index ae12367..5fb6ad1 100644 --- a/server/internal/service/character.go +++ b/server/internal/service/character.go @@ -8,7 +8,6 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type CharacterServiceServer struct { @@ -26,7 +25,7 @@ func NewCharacterServiceServer(users store.UserRepository, sessions store.Sessio func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthRequest) (*pb.RebirthResponse, error) { log.Printf("[CharacterService] Rebirth: characterId=%d rebirthCount=%d", req.CharacterId, req.RebirthCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() stepGroupId, ok := s.catalog.StepGroupByCharacterId[req.CharacterId] @@ -35,11 +34,7 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq return &pb.RebirthResponse{}, nil } - oldUser, _ := s.users.LoadUser(userId) - tracker := userdata.NewDeleteTracker(). - Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}) - - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { current := user.CharacterRebirths[req.CharacterId] currentCount := current.RebirthCount targetCount := currentCount + req.RebirthCount @@ -77,9 +72,5 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq return nil, err } - rebirthTables := []string{"IUserCharacterRebirth", "IUserMaterial", "IUserConsumableItem"} - tables := userdata.ProjectTables(snapshot, rebirthTables) - diff := tracker.Apply(snapshot, tables) - - return &pb.RebirthResponse{DiffUserData: diff}, nil + return &pb.RebirthResponse{}, nil } diff --git a/server/internal/service/characterboard.go b/server/internal/service/characterboard.go index e586d25..ebd7abc 100644 --- a/server/internal/service/characterboard.go +++ b/server/internal/service/characterboard.go @@ -8,7 +8,6 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type CharacterBoardServiceServer struct { @@ -25,14 +24,9 @@ func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.S func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) { log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - oldUser, _ := s.users.LoadUser(userId) - tracker := userdata.NewDeleteTracker(). - Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}). - Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"}) - - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { for _, panelId := range req.CharacterBoardPanelId { panel, ok := s.catalog.PanelById[panelId] if !ok { @@ -46,28 +40,17 @@ func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb. } }) - boardTables := []string{ - "IUserCharacterBoard", - "IUserCharacterBoardAbility", - "IUserCharacterBoardStatusUp", - "IUserMaterial", - "IUserConsumableItem", - "IUserGem", - } - tables := userdata.ProjectTables(user, boardTables) - diff := tracker.Apply(user, tables) - - return &pb.ReleasePanelResponse{DiffUserData: diff}, nil + return &pb.ReleasePanelResponse{}, nil } -func (s *CharacterBoardServiceServer) consumeCosts(user *store.UserState, panel masterdata.CharacterBoardPanelRow) { +func (s *CharacterBoardServiceServer) consumeCosts(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { costs := s.catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId] for _, cost := range costs { store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count) } } -func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel masterdata.CharacterBoardPanelRow) { +func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { boardId := panel.CharacterBoardId board := user.CharacterBoards[boardId] board.CharacterBoardId = boardId @@ -90,7 +73,7 @@ func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel user.CharacterBoards[boardId] = board } -func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel masterdata.CharacterBoardPanelRow) { +func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { effects := s.catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId] for _, eff := range effects { switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) { @@ -102,7 +85,7 @@ func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel } } -func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, eff masterdata.CharacterBoardReleaseEffectRow) { +func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { ability, ok := s.catalog.AbilityById[eff.CharacterBoardEffectId] if !ok { log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId) @@ -127,7 +110,7 @@ func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, user.CharacterBoardAbilities[key] = state } -func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, eff masterdata.CharacterBoardReleaseEffectRow) { +func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { statusUp, ok := s.catalog.StatusUpById[eff.CharacterBoardEffectId] if !ok { log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId) diff --git a/server/internal/service/characterviewer.go b/server/internal/service/characterviewer.go index d6ba310..5aff808 100644 --- a/server/internal/service/characterviewer.go +++ b/server/internal/service/characterviewer.go @@ -2,12 +2,10 @@ package service import ( "context" - "encoding/json" "fmt" "log" pb "lunar-tear/server/gen/proto" - "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" @@ -28,7 +26,7 @@ func NewCharacterViewerServiceServer(users store.UserRepository, sessions store. func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ *emptypb.Empty) (*pb.CharacterViewerTopResponse, error) { log.Printf("[CharacterViewerService] CharacterViewerTop") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err)) @@ -37,32 +35,7 @@ func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ released := s.catalog.ReleasedFieldIds(user) log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId) - now := gametime.NowMillis() - records := make([]map[string]any, 0, len(released)) - for _, fieldId := range released { - records = append(records, map[string]any{ - "userId": userId, - "characterViewerFieldId": fieldId, - "releaseDatetime": now, - "latestVersion": 0, - }) - } - - payload := "[]" - if len(records) > 0 { - data, _ := json.Marshal(records) - payload = string(data) - } - - diff := map[string]*pb.DiffData{ - "IUserCharacterViewerField": { - UpdateRecordsJson: payload, - DeleteKeysJson: "[]", - }, - } - return &pb.CharacterViewerTopResponse{ ReleaseCharacterViewerFieldId: released, - DiffUserData: diff, }, nil } diff --git a/server/internal/service/companion.go b/server/internal/service/companion.go index 69a1287..75e25a9 100644 --- a/server/internal/service/companion.go +++ b/server/internal/service/companion.go @@ -9,17 +9,10 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) const companionMaxLevel = int32(50) -var companionDiffTables = []string{ - "IUserCompanion", - "IUserMaterial", - "IUserConsumableItem", -} - type CompanionServiceServer struct { pb.UnimplementedCompanionServiceServer users store.UserRepository @@ -35,10 +28,10 @@ func NewCompanionServiceServer(users store.UserRepository, sessions store.Sessio func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionEnhanceRequest) (*pb.CompanionEnhanceResponse, error) { log.Printf("[CompanionService] Enhance: uuid=%s addLevel=%d", req.UserCompanionUuid, req.AddLevelCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { companion, ok := user.Companions[req.UserCompanionUuid] if !ok { log.Printf("[CompanionService] Enhance: companion uuid=%s not found", req.UserCompanionUuid) @@ -77,9 +70,5 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE return nil, fmt.Errorf("companion enhance: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, companionDiffTables)) - - return &pb.CompanionEnhanceResponse{ - DiffUserData: diff, - }, nil + return &pb.CompanionEnhanceResponse{}, nil } diff --git a/server/internal/service/config.go b/server/internal/service/config.go index 12c725d..25ec14a 100644 --- a/server/internal/service/config.go +++ b/server/internal/service/config.go @@ -5,7 +5,6 @@ import ( "log" pb "lunar-tear/server/gen/proto" - "lunar-tear/server/internal/userdata" "google.golang.org/protobuf/types/known/emptypb" ) @@ -14,7 +13,7 @@ type ConfigServiceServer struct { pb.UnimplementedConfigServiceServer GrpcHost string GrpcPort int32 - OctoURL string // HTTP base URL for Octo (list + assets); client uses this instead of default resources.app.nierreincarnation.com + OctoURL string } func NewConfigServiceServer(host string, port int32, octoURL string) *ConfigServiceServer { @@ -42,6 +41,5 @@ func (s *ConfigServiceServer) GetReviewServerConfig(ctx context.Context, _ *empt MasterData: &pb.MasterDataConfig{ UrlFormat: s.OctoURL + "/master-data/%s", }, - DiffUserData: userdata.EmptyDiff(), }, nil } diff --git a/server/internal/service/consumableitem.go b/server/internal/service/consumableitem.go index a9d061c..ee0c00a 100644 --- a/server/internal/service/consumableitem.go +++ b/server/internal/service/consumableitem.go @@ -8,7 +8,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type ConsumableItemServiceServer struct { @@ -26,13 +25,9 @@ func NewConsumableItemServiceServer(users store.UserRepository, sessions store.S func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) { log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession)) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - oldUser, _ := s.users.LoadUser(userId) - tracker := userdata.NewDeleteTracker(). - Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"}) - - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { totalGold := int32(0) for _, item := range req.ConsumableItemPossession { row, ok := s.catalog.All[item.ConsumableItemId] @@ -66,10 +61,5 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab return nil, fmt.Errorf("consumable item sell: %w", err) } - tables := userdata.ProjectTables(snapshot, []string{"IUserConsumableItem"}) - diff := tracker.Apply(snapshot, tables) - - return &pb.ConsumableItemSellResponse{ - DiffUserData: diff, - }, nil + return &pb.ConsumableItemSellResponse{}, nil } diff --git a/server/internal/service/contentsstory.go b/server/internal/service/contentsstory.go index 87ae421..d780c43 100644 --- a/server/internal/service/contentsstory.go +++ b/server/internal/service/contentsstory.go @@ -8,7 +8,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type ContentsStoryServiceServer struct { @@ -24,19 +23,15 @@ func NewContentsStoryServiceServer(users store.UserRepository, sessions store.Se func (s *ContentsStoryServiceServer) RegisterPlayed(ctx context.Context, req *pb.ContentsStoryRegisterPlayedRequest) (*pb.ContentsStoryRegisterPlayedResponse, error) { log.Printf("[ContentsStoryService] RegisterPlayed: contentsStoryId=%d", req.ContentsStoryId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { user.ContentsStories[req.ContentsStoryId] = nowMillis }) if err != nil { return nil, fmt.Errorf("update user: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserContentsStory"})) - - return &pb.ContentsStoryRegisterPlayedResponse{ - DiffUserData: diff, - }, nil + return &pb.ContentsStoryRegisterPlayedResponse{}, nil } diff --git a/server/internal/service/costume.go b/server/internal/service/costume.go index 50ebfe3..600ed9d 100644 --- a/server/internal/service/costume.go +++ b/server/internal/service/costume.go @@ -14,15 +14,8 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) -var costumeDiffTables = []string{ - "IUserCostume", - "IUserMaterial", - "IUserConsumableItem", -} - type CostumeServiceServer struct { pb.UnimplementedCostumeServiceServer users store.UserRepository @@ -38,10 +31,10 @@ func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionR func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceRequest) (*pb.EnhanceResponse, error) { log.Printf("[CostumeService] Enhance: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { costume, ok := user.Costumes[req.UserCostumeUuid] if !ok { log.Printf("[CostumeService] Enhance: costume uuid=%s not found", req.UserCostumeUuid) @@ -98,30 +91,19 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque return nil, fmt.Errorf("costume enhance: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, costumeDiffTables)) - return &pb.EnhanceResponse{ IsGreatSuccess: false, SurplusEnhanceMaterial: map[int32]int32{}, - DiffUserData: diff, }, nil } -var awakenDiffTables = []string{ - "IUserCostume", - "IUserMaterial", - "IUserConsumableItem", - "IUserCostumeAwakenStatusUp", - "IUserThought", -} - func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest) (*pb.AwakenResponse, error) { log.Printf("[CostumeService] Awaken: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { costume, ok := user.Costumes[req.UserCostumeUuid] if !ok { log.Printf("[CostumeService] Awaken: costume uuid=%s not found", req.UserCostumeUuid) @@ -179,11 +161,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest return nil, fmt.Errorf("costume awaken: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, awakenDiffTables)) - - return &pb.AwakenResponse{ - DiffUserData: diff, - }, nil + return &pb.AwakenResponse{}, nil } func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) { @@ -245,19 +223,13 @@ func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, ite log.Printf("[CostumeService] Awaken: granted thought id=%d", acq.PossessionId) } -var activeSkillDiffTables = []string{ - "IUserCostumeActiveSkill", - "IUserMaterial", - "IUserConsumableItem", -} - func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.EnhanceActiveSkillRequest) (*pb.EnhanceActiveSkillResponse, error) { log.Printf("[CostumeService] EnhanceActiveSkill: uuid=%s addLevel=%d", req.UserCostumeUuid, req.AddLevelCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { costume, ok := user.Costumes[req.UserCostumeUuid] if !ok { log.Printf("[CostumeService] EnhanceActiveSkill: costume uuid=%s not found", req.UserCostumeUuid) @@ -332,20 +304,16 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E return nil, fmt.Errorf("costume enhance active skill: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, activeSkillDiffTables)) - - return &pb.EnhanceActiveSkillResponse{ - DiffUserData: diff, - }, nil + return &pb.EnhanceActiveSkillResponse{}, nil } func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBreakRequest) (*pb.LimitBreakResponse, error) { log.Printf("[CostumeService] LimitBreak: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { costume, ok := user.Costumes[req.UserCostumeUuid] if !ok { log.Printf("[CostumeService] LimitBreak: costume uuid=%s not found", req.UserCostumeUuid) @@ -389,30 +357,16 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea return nil, fmt.Errorf("costume limit break: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, costumeDiffTables)) - - return &pb.LimitBreakResponse{ - DiffUserData: diff, - }, nil -} - -var lotteryEffectDiffTables = []string{ - "IUserCostume", - "IUserCostumeLotteryEffect", - "IUserCostumeLotteryEffectAbility", - "IUserCostumeLotteryEffectStatusUp", - "IUserCostumeLotteryEffectPending", - "IUserConsumableItem", - "IUserMaterial", + return &pb.LimitBreakResponse{}, nil } 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) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, 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) @@ -458,26 +412,16 @@ func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req return nil, fmt.Errorf("costume unlock lottery effect slot: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, lotteryEffectDiffTables)) - - return &pb.UnlockLotteryEffectSlotResponse{ - DiffUserData: diff, - }, nil + return &pb.UnlockLotteryEffectSlotResponse{}, 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) + 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) { + _, 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) @@ -514,7 +458,7 @@ func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.Dr totalWeight += row.Weight } roll := rand.Int31n(totalWeight) - var picked masterdata.CostumeLotteryEffectOddsRow + var picked masterdata.EntityMCostumeLotteryEffectOddsGroup for _, row := range oddsPool { roll -= row.Weight if roll < 0 { @@ -550,24 +494,16 @@ func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.Dr return nil, fmt.Errorf("costume draw lottery effect: %w", err) } - diff := tracker.Apply(snapshot, userdata.ProjectTables(snapshot, lotteryEffectDiffTables)) - - return &pb.DrawLotteryEffectResponse{ - DiffUserData: diff, - }, nil + return &pb.DrawLotteryEffectResponse{}, 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) + 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) { + _, 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) @@ -596,9 +532,5 @@ func (s *CostumeServiceServer) ConfirmLotteryEffect(ctx context.Context, req *pb return nil, fmt.Errorf("costume confirm lottery effect: %w", err) } - diff := tracker.Apply(snapshot, userdata.ProjectTables(snapshot, lotteryEffectDiffTables)) - - return &pb.ConfirmLotteryEffectResponse{ - DiffUserData: diff, - }, nil + return &pb.ConfirmLotteryEffectResponse{}, nil } diff --git a/server/internal/service/data.go b/server/internal/service/data.go index 1ccbcaa..52c7e1f 100644 --- a/server/internal/service/data.go +++ b/server/internal/service/data.go @@ -41,7 +41,7 @@ func (s *DataServiceServer) GetUserDataNameV2(ctx context.Context, _ *emptypb.Em func (s *DataServiceServer) GetUserData(ctx context.Context, req *pb.UserDataGetRequest) (*pb.UserDataGetResponse, error) { log.Printf("[DataService] GetUserData: tables=%v", req.TableName) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { return nil, fmt.Errorf("snapshot user: %w", err) diff --git a/server/internal/service/deck.go b/server/internal/service/deck.go index f1ae15a..3ff3dc2 100644 --- a/server/internal/service/deck.go +++ b/server/internal/service/deck.go @@ -8,7 +8,6 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type DeckServiceServer struct { @@ -23,26 +22,23 @@ func NewDeckServiceServer(users store.UserRepository, sessions store.SessionRepo func (s *DeckServiceServer) UpdateName(ctx context.Context, req *pb.UpdateNameRequest) (*pb.UpdateNameResponse, error) { log.Printf("[DeckService] UpdateName: deckType=%d deckNumber=%d name=%q", req.DeckType, req.UserDeckNumber, req.Name) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { deckKey := store.DeckKey{DeckType: model.DeckType(req.DeckType), UserDeckNumber: req.UserDeckNumber} deck := user.Decks[deckKey] deck.Name = req.Name user.Decks[deckKey] = deck }) - result := userdata.ProjectTables(user, []string{"IUserDeck"}) - return &pb.UpdateNameResponse{ - DiffUserData: userdata.BuildDiffFromTables(result), - }, nil + return &pb.UpdateNameResponse{}, nil } func (s *DeckServiceServer) RefreshDeckPower(ctx context.Context, req *pb.RefreshDeckPowerRequest) (*pb.RefreshDeckPowerResponse, error) { log.Printf("[DeckService] RefreshDeckPower: deckType=%d deckNumber=%d", req.DeckType, req.UserDeckNumber) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { if req.DeckPower == nil { log.Printf("[DeckService] RefreshDeckPower: deckPower is nil") return @@ -81,19 +77,14 @@ func (s *DeckServiceServer) RefreshDeckPower(ctx context.Context, req *pb.Refres } }) - result := userdata.ProjectTables(user, []string{ - "IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote", - }) - return &pb.RefreshDeckPowerResponse{ - DiffUserData: userdata.BuildDiffFromTables(result), - }, nil + return &pb.RefreshDeckPowerResponse{}, nil } func (s *DeckServiceServer) RefreshMultiDeckPower(ctx context.Context, req *pb.RefreshMultiDeckPowerRequest) (*pb.RefreshMultiDeckPowerResponse, error) { log.Printf("[DeckService] RefreshMultiDeckPower: %d entries", len(req.DeckPowerInfo)) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { for _, info := range req.DeckPowerInfo { if info.DeckPower == nil { continue @@ -133,12 +124,7 @@ func (s *DeckServiceServer) RefreshMultiDeckPower(ctx context.Context, req *pb.R } }) - result := userdata.ProjectTables(user, []string{ - "IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote", - }) - return &pb.RefreshMultiDeckPowerResponse{ - DiffUserData: userdata.BuildDiffFromTables(result), - }, nil + return &pb.RefreshMultiDeckPowerResponse{}, nil } func deckSlotsFromProto(deck *pb.Deck) []store.DeckCharacterInput { @@ -171,47 +157,23 @@ func (s *DeckServiceServer) ReplaceDeck(ctx context.Context, req *pb.ReplaceDeck i+1, ch.UserCostumeUuid, ch.MainUserWeaponUuid, ch.SubUserWeaponUuid, ch.UserCompanionUuid, ch.UserThoughtUuid) } } - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - oldUser, _ := s.users.LoadUser(userId) - tracker := userdata.NewDeleteTracker(). - Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords, - []string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}). - Track("IUserDeckPartsGroup", oldUser, userdata.DeckPartsGroupRecords, - []string{"userId", "userDeckCharacterUuid", "userPartsUuid"}). - Track("IUserDeckCharacterDressupCostume", oldUser, userdata.DeckDressupCostumeRecords, - []string{"userId", "userDeckCharacterUuid"}) - - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { if req.Deck == nil { return } store.ApplyDeckReplacement(user, model.DeckType(req.DeckType), req.UserDeckNumber, deckSlotsFromProto(req.Deck), gametime.NowMillis()) }) - result := userdata.ProjectTables(user, []string{ - "IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup", - "IUserDeckCharacterDressupCostume", - }) - return &pb.ReplaceDeckResponse{ - DiffUserData: tracker.Apply(user, result), - }, nil + return &pb.ReplaceDeckResponse{}, nil } func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.ReplaceTripleDeckRequest) (*pb.ReplaceTripleDeckResponse, error) { log.Printf("[DeckService] ReplaceTripleDeck: deckType=%d deckNumber=%d", req.DeckType, req.UserDeckNumber) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - oldUser, _ := s.users.LoadUser(userId) - tracker := userdata.NewDeleteTracker(). - Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords, - []string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}). - Track("IUserDeckPartsGroup", oldUser, userdata.DeckPartsGroupRecords, - []string{"userId", "userDeckCharacterUuid", "userPartsUuid"}). - Track("IUserDeckCharacterDressupCostume", oldUser, userdata.DeckDressupCostumeRecords, - []string{"userId", "userDeckCharacterUuid"}) - - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { nowMillis := gametime.NowMillis() for idx, detail := range []*pb.DeckDetail{req.DeckDetail01, req.DeckDetail02, req.DeckDetail03} { if detail == nil || detail.Deck == nil { @@ -231,11 +193,23 @@ func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.Repla } }) - result := userdata.ProjectTables(user, []string{ - "IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup", - "IUserDeckCharacterDressupCostume", - }) - return &pb.ReplaceTripleDeckResponse{ - DiffUserData: tracker.Apply(user, result), - }, nil + return &pb.ReplaceTripleDeckResponse{}, nil +} + +func (s *DeckServiceServer) ReplaceMultiDeck(ctx context.Context, req *pb.ReplaceMultiDeckRequest) (*pb.ReplaceMultiDeckResponse, error) { + log.Printf("[DeckService] ReplaceMultiDeck: %d entries", len(req.DeckDetail)) + userId := CurrentUserId(ctx, s.users, s.sessions) + + s.users.UpdateUser(userId, func(user *store.UserState) { + nowMillis := gametime.NowMillis() + for idx, detail := range req.DeckDetail { + if detail == nil || detail.Deck == nil { + continue + } + log.Printf("[DeckService] ReplaceMultiDeck detail %d: deckType=%d deckNumber=%d", idx+1, detail.DeckType, detail.UserDeckNumber) + store.ApplyDeckReplacement(user, model.DeckType(detail.DeckType), detail.UserDeckNumber, deckSlotsFromProto(detail.Deck), nowMillis) + } + }) + + return &pb.ReplaceMultiDeckResponse{}, nil } diff --git a/server/internal/service/dokan.go b/server/internal/service/dokan.go index 4fe12bf..a7e427e 100644 --- a/server/internal/service/dokan.go +++ b/server/internal/service/dokan.go @@ -7,7 +7,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type DokanServiceServer struct { @@ -23,8 +22,8 @@ func NewDokanServiceServer(users store.UserRepository, sessions store.SessionRep func (s *DokanServiceServer) RegisterDokanConfirmed(ctx context.Context, req *pb.RegisterDokanConfirmedRequest) (*pb.RegisterDokanConfirmedResponse, error) { log.Printf("[DokanService] RegisterDokanConfirmed: dokanIds=%v", req.DokanId) - userId := currentUserId(ctx, s.users, s.sessions) - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { for _, id := range req.DokanId { user.DokanConfirmed[id] = true } @@ -33,9 +32,5 @@ func (s *DokanServiceServer) RegisterDokanConfirmed(ctx context.Context, req *pb return nil, fmt.Errorf("update user: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserDokan"})) - - return &pb.RegisterDokanConfirmedResponse{ - DiffUserData: diff, - }, nil + return &pb.RegisterDokanConfirmedResponse{}, nil } diff --git a/server/internal/service/explore.go b/server/internal/service/explore.go index 619b976..6ffc673 100644 --- a/server/internal/service/explore.go +++ b/server/internal/service/explore.go @@ -10,7 +10,6 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) const ( @@ -19,18 +18,6 @@ const ( exploreRewardBaseCount = 1 ) -var exploreDiffTables = []string{ - "IUserExplore", - "IUserExploreScore", -} - -var exploreFinishDiffTables = []string{ - "IUserExplore", - "IUserExploreScore", - "IUserMaterial", - "IUserStatus", -} - type ExploreServiceServer struct { pb.UnimplementedExploreServiceServer users store.UserRepository @@ -49,10 +36,10 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx return nil, fmt.Errorf("explore id=%d not found", req.ExploreId) } - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { explore := s.catalog.Explores[req.ExploreId] if req.UseConsumableItemId > 0 && explore.ConsumeItemCount > 0 { cur := user.ConsumableItems[req.UseConsumableItemId] @@ -71,11 +58,7 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx return nil, fmt.Errorf("start explore: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, exploreDiffTables)) - - return &pb.StartExploreResponse{ - DiffUserData: diff, - }, nil + return &pb.StartExploreResponse{}, nil } func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.FinishExploreRequest) (*pb.FinishExploreResponse, error) { @@ -88,12 +71,12 @@ func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.Finish assetGradeIconId := s.catalog.GradeForScore(req.ExploreId, req.Score) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() rewardCount := int32(exploreRewardBaseCount) * explore.RewardLotteryCount - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { existing, exists := user.ExploreScores[req.ExploreId] if !exists || req.Score > existing.MaxScore { user.ExploreScores[req.ExploreId] = store.ExploreScoreState{ @@ -123,8 +106,6 @@ func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.Finish return nil, fmt.Errorf("finish explore: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, exploreFinishDiffTables)) - rewards := []*pb.ExploreReward{ { PossessionType: int32(model.PossessionTypeMaterial), @@ -137,17 +118,16 @@ func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.Finish AcquireStaminaCount: exploreStaminaRecovery, ExploreReward: rewards, AssetGradeIconId: assetGradeIconId, - DiffUserData: diff, }, nil } func (s *ExploreServiceServer) RetireExplore(ctx context.Context, req *pb.RetireExploreRequest) (*pb.RetireExploreResponse, error) { log.Printf("[ExploreService] RetireExplore: exploreId=%d", req.ExploreId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { user.Explore = store.ExploreState{ PlayingExploreId: 0, IsUseExploreTicket: false, @@ -159,9 +139,5 @@ func (s *ExploreServiceServer) RetireExplore(ctx context.Context, req *pb.Retire return nil, fmt.Errorf("retire explore: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserExplore"})) - - return &pb.RetireExploreResponse{ - DiffUserData: diff, - }, nil + return &pb.RetireExploreResponse{}, nil } diff --git a/server/internal/service/friend.go b/server/internal/service/friend.go index 15fbcba..ebb53da 100644 --- a/server/internal/service/friend.go +++ b/server/internal/service/friend.go @@ -6,7 +6,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -23,7 +22,7 @@ func NewFriendServiceServer(users store.UserRepository, sessions store.SessionRe func (s *FriendServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) { log.Printf("[FriendService] GetUser: playerId=%d", req.PlayerId) - return &pb.GetUserResponse{DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetUserResponse{}, nil } func (s *FriendServiceServer) GetFriendList(ctx context.Context, req *pb.GetFriendListRequest) (*pb.GetFriendListResponse, error) { @@ -32,22 +31,19 @@ func (s *FriendServiceServer) GetFriendList(ctx context.Context, req *pb.GetFrie FriendUser: []*pb.FriendUser{}, SendCheerCount: 0, ReceivedCheerCount: 0, - DiffUserData: userdata.EmptyDiff(), }, nil } func (s *FriendServiceServer) GetFriendRequestList(ctx context.Context, req *emptypb.Empty) (*pb.GetFriendRequestListResponse, error) { log.Printf("[FriendService] GetFriendRequestList") return &pb.GetFriendRequestListResponse{ - User: []*pb.User{}, - DiffUserData: userdata.EmptyDiff(), + User: []*pb.User{}, }, nil } func (s *FriendServiceServer) SearchRecommendedUsers(ctx context.Context, req *emptypb.Empty) (*pb.SearchRecommendedUsersResponse, error) { log.Printf("[FriendService] SearchRecommendedUsers") return &pb.SearchRecommendedUsersResponse{ - Users: []*pb.User{}, - DiffUserData: userdata.EmptyDiff(), + Users: []*pb.User{}, }, nil } diff --git a/server/internal/service/gacha.go b/server/internal/service/gacha.go index 174922e..499b4d5 100644 --- a/server/internal/service/gacha.go +++ b/server/internal/service/gacha.go @@ -11,25 +11,11 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" ) -var gachaDiffTables = []string{ - "IUserGem", - "IUserCostume", - "IUserWeapon", - "IUserConsumableItem", - "IUserCostumeActiveSkill", - "IUserWeaponNote", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserCharacter", - "IUserMaterial", -} - type GachaServiceServer struct { pb.UnimplementedGachaServiceServer users store.UserRepository @@ -56,7 +42,7 @@ func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaL log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType) catalog := s.catalog - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() user, err := s.users.UpdateUser(userId, func(user *store.UserState) { @@ -82,7 +68,6 @@ func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaL return &pb.GetGachaListResponse{ Gacha: gachaList, ConvertedGachaMedal: toProtoConvertedGachaMedal(user.Gacha.ConvertedGachaMedal), - DiffUserData: userdata.EmptyDiff(), }, nil } @@ -134,7 +119,7 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque catalog := s.catalog - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { return nil, fmt.Errorf("snapshot user: %w", err) @@ -152,8 +137,7 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque } return &pb.GetGachaResponse{ - Gacha: byId, - DiffUserData: userdata.EmptyDiff(), + Gacha: byId, }, nil } @@ -165,7 +149,7 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb return nil, fmt.Errorf("gacha %d not found", req.GachaId) } - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) execCount := req.ExecCount if execCount <= 0 { execCount = 1 @@ -290,17 +274,11 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb bs := updatedUser.Gacha.BannerStates[entry.GachaId] nextGacha := toProtoGacha(*entry, &bs) - changedStoryIds := s.handler.Granter.DrainChangedStoryWeaponIds() - diffOrder := append(gachaDiffTables, "IUserWeaponStory") - diff := userdata.BuildDiffFromTablesOrdered(userdata.ProjectTables(updatedUser, diffOrder), diffOrder) - userdata.AddWeaponStoryDiff(diff, updatedUser, changedStoryIds) - return &pb.DrawResponse{ NextGacha: nextGacha, GachaResult: gachaResults, GachaBonus: bonuses, MenuGachaBadgeInfo: []*pb.MenuGachaBadgeInfo{}, - DiffUserData: diff, }, nil } @@ -312,7 +290,7 @@ func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBox return nil, fmt.Errorf("gacha %d not found", req.GachaId) } - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { if resetErr := s.handler.HandleResetBox(user, *entry); resetErr != nil { log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr) @@ -325,14 +303,13 @@ func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBox bs := updatedUser.Gacha.BannerStates[entry.GachaId] return &pb.ResetBoxGachaResponse{ - Gacha: toProtoGacha(*entry, &bs), - DiffUserData: userdata.EmptyDiff(), + Gacha: toProtoGacha(*entry, &bs), }, nil } func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Empty) (*pb.GetRewardGachaResponse, error) { log.Printf("[GachaService] GetRewardGacha") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { return nil, fmt.Errorf("snapshot user: %w", err) @@ -353,14 +330,13 @@ func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Em Available: drawCount < maxCount, TodaysCurrentDrawCount: drawCount, DailyMaxCount: maxCount, - DiffUserData: userdata.EmptyDiff(), }, nil } func (s *GachaServiceServer) RewardDraw(ctx context.Context, req *pb.RewardDrawRequest) (*pb.RewardDrawResponse, error) { log.Printf("[GachaService] RewardDraw: placement=%q reward=%q amount=%q", req.PlacementName, req.RewardName, req.RewardAmount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) var items []gacha.DrawnItem updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { @@ -393,12 +369,8 @@ func (s *GachaServiceServer) RewardDraw(ctx context.Context, req *pb.RewardDrawR }) } - tables := userdata.FullClientTableMap(updatedUser) - diff := userdata.BuildDiffFromTables(tables) - return &pb.RewardDrawResponse{ RewardGachaResult: results, - DiffUserData: diff, }, nil } diff --git a/server/internal/service/gameplay.go b/server/internal/service/gameplay.go index a76c92e..ff4a089 100644 --- a/server/internal/service/gameplay.go +++ b/server/internal/service/gameplay.go @@ -5,7 +5,6 @@ import ( "log" pb "lunar-tear/server/gen/proto" - "lunar-tear/server/internal/userdata" ) type GameplayServiceServer struct { @@ -23,6 +22,5 @@ func (s *GameplayServiceServer) CheckBeforeGamePlay(ctx context.Context, req *pb return &pb.CheckBeforeGamePlayResponse{ IsExistUnreadPop: false, MenuGachaBadgeInfo: []*pb.MenuGachaBadgeInfo{}, - DiffUserData: userdata.EmptyDiff(), }, nil } diff --git a/server/internal/service/gift.go b/server/internal/service/gift.go index b54ccd8..0b72ff5 100644 --- a/server/internal/service/gift.go +++ b/server/internal/service/gift.go @@ -11,7 +11,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" @@ -30,7 +29,7 @@ func NewGiftServiceServer(users store.UserRepository, sessions store.SessionRepo func (s *GiftServiceServer) ReceiveGift(ctx context.Context, req *pb.ReceiveGiftRequest) (*pb.ReceiveGiftResponse, error) { log.Printf("[GiftService] ReceiveGift: giftUuids=%d", len(req.UserGiftUuid)) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) received := make([]string, 0, len(req.UserGiftUuid)) _, err := s.users.UpdateUser(userId, func(user *store.UserState) { nowMillis := gametime.NowMillis() @@ -54,7 +53,6 @@ func (s *GiftServiceServer) ReceiveGift(ctx context.Context, req *pb.ReceiveGift ReceivedGiftUuid: []string{}, ExpiredGiftUuid: []string{}, OverflowGiftUuid: []string{}, - DiffUserData: userdata.EmptyDiff(), }, nil } @@ -62,7 +60,6 @@ func (s *GiftServiceServer) ReceiveGift(ctx context.Context, req *pb.ReceiveGift ReceivedGiftUuid: received, ExpiredGiftUuid: []string{}, OverflowGiftUuid: []string{}, - DiffUserData: userdata.EmptyDiff(), }, nil } @@ -70,7 +67,7 @@ func (s *GiftServiceServer) GetGiftList(ctx context.Context, req *pb.GetGiftList log.Printf("[GiftService] GetGiftList: rewardKinds=%v expirationType=%d ascending=%v nextCursor=%d previousCursor=%d getCount=%d", req.RewardKindType, req.ExpirationType, req.IsAscendingSort, req.NextCursor, req.PreviousCursor, req.GetCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { return nil, fmt.Errorf("snapshot user: %w", err) @@ -101,13 +98,12 @@ func (s *GiftServiceServer) GetGiftList(ctx context.Context, req *pb.GetGiftList TotalPageCount: pageCount(len(user.Gifts.NotReceived), int(req.GetCount)), NextCursor: 0, PreviousCursor: 0, - DiffUserData: userdata.EmptyDiff(), }, nil } func (s *GiftServiceServer) GetGiftReceiveHistoryList(ctx context.Context, req *emptypb.Empty) (*pb.GetGiftReceiveHistoryListResponse, error) { log.Printf("[GiftService] GetGiftReceiveHistoryList") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { return nil, fmt.Errorf("snapshot user: %w", err) @@ -121,8 +117,7 @@ func (s *GiftServiceServer) GetGiftReceiveHistoryList(ctx context.Context, req * }) } return &pb.GetGiftReceiveHistoryListResponse{ - Gift: items, - DiffUserData: userdata.EmptyDiff(), + Gift: items, }, nil } diff --git a/server/internal/service/gimmick.go b/server/internal/service/gimmick.go index 103f7bd..4d8c4d2 100644 --- a/server/internal/service/gimmick.go +++ b/server/internal/service/gimmick.go @@ -8,7 +8,6 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -27,8 +26,8 @@ func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionR func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) { log.Printf("[GimmickService] UpdateSequence: scheduleId=%d sequenceId=%d", req.GimmickSequenceScheduleId, req.GimmickSequenceId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { key := store.GimmickSequenceKey{ GimmickSequenceScheduleId: req.GimmickSequenceScheduleId, GimmickSequenceId: req.GimmickSequenceId, @@ -37,16 +36,14 @@ func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.Updat sequence.Key = key user.Gimmick.Sequences[key] = sequence }) - return &pb.UpdateSequenceResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserGimmickSequence"})), - }, nil + return &pb.UpdateSequenceResponse{}, nil } func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *pb.UpdateGimmickProgressRequest) (*pb.UpdateGimmickProgressResponse, error) { log.Printf("[GimmickService] UpdateGimmickProgress: scheduleId=%d sequenceId=%d gimmickId=%d ornamentIndex=%d progressValueBit=%d flowType=%d", req.GimmickSequenceScheduleId, req.GimmickSequenceId, req.GimmickId, req.GimmickOrnamentIndex, req.ProgressValueBit, req.FlowType) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { nowMillis := gametime.NowMillis() progressKey := store.GimmickKey{ GimmickSequenceScheduleId: req.GimmickSequenceScheduleId, @@ -74,18 +71,14 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p GimmickOrnamentReward: []*pb.GimmickReward{}, IsSequenceCleared: false, GimmickSequenceClearReward: []*pb.GimmickReward{}, - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{ - "IUserGimmick", - "IUserGimmickOrnamentProgress", - })), }, nil } func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *emptypb.Empty) (*pb.InitSequenceScheduleResponse, error) { log.Printf("[GimmickService] InitSequenceSchedule") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) now := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { added := 0 for _, key := range s.gimmickCatalog.ActiveScheduleKeys(*user, now) { if _, exists := user.Gimmick.Sequences[key]; !exists { @@ -97,15 +90,13 @@ func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *empt log.Printf("[GimmickService] InitSequenceSchedule: added %d sequences (total %d)", added, len(user.Gimmick.Sequences)) } }) - return &pb.InitSequenceScheduleResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, gimmickDiffTables)), - }, nil + return &pb.InitSequenceScheduleResponse{}, nil } func (s *GimmickServiceServer) Unlock(ctx context.Context, req *pb.UnlockRequest) (*pb.UnlockResponse, error) { log.Printf("[GimmickService] Unlock: gimmickKeys=%d", len(req.GimmickKey)) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { for _, item := range req.GimmickKey { key := store.GimmickKey{ GimmickSequenceScheduleId: item.GimmickSequenceScheduleId, @@ -118,7 +109,5 @@ func (s *GimmickServiceServer) Unlock(ctx context.Context, req *pb.UnlockRequest user.Gimmick.Unlocks[key] = unlock } }) - return &pb.UnlockResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserGimmickUnlock"})), - }, nil + return &pb.UnlockResponse{}, nil } diff --git a/server/internal/service/listbin.go b/server/internal/service/listbin.go index ff6c13a..5211c74 100644 --- a/server/internal/service/listbin.go +++ b/server/internal/service/listbin.go @@ -208,7 +208,7 @@ func parseListBin(data []byte) listBinIndex { return idx } -func loadListBinIndex(revision string) (listBinIndex, bool) { +func loadListBinIndex(baseDir, revision string) (listBinIndex, bool) { listBinCacheMu.RLock() idx, ok := listBinCache[revision] listBinCacheMu.RUnlock() @@ -230,7 +230,7 @@ func loadListBinIndex(revision string) (listBinIndex, bool) { listBinInflight[revision] = load listBinCacheMu.Unlock() - filePath := filepath.Join("assets", "revisions", revision, "list.bin") + filePath := filepath.Join(baseDir, "assets", "revisions", revision, "list.bin") data, err := os.ReadFile(filePath) if err != nil { listBinCacheMu.Lock() @@ -250,7 +250,7 @@ func loadListBinIndex(revision string) (listBinIndex, bool) { return idx, true } -func loadInfoIndex(revision string) map[string]infoAlias { +func loadInfoIndex(baseDir, revision string) map[string]infoAlias { infoCacheMu.RLock() m, ok := infoCache[revision] infoCacheMu.RUnlock() @@ -272,7 +272,7 @@ func loadInfoIndex(revision string) map[string]infoAlias { infoInflight[revision] = load infoCacheMu.Unlock() - filePath := filepath.Join("assets", "revisions", revision, "info.json") + filePath := filepath.Join(baseDir, "assets", "revisions", revision, "info.json") data, err := os.ReadFile(filePath) if err != nil { infoCacheMu.Lock() @@ -378,7 +378,7 @@ func hasNonASCII(s string) bool { // an en locale fallback is appended (marked IsLocaleFallback so callers can skip MD5 validation). // For paths with non-ASCII characters, mojibake (double-encoded) and fullwidth-to-ASCII // variants are also tried. -func pathStrToFullPaths(revision, assetType, pathStr string) []pathCandidate { +func pathStrToFullPaths(baseDir, revision, assetType, pathStr string) []pathCandidate { fsPath := strings.ReplaceAll(pathStr, ")", "/") if strings.Contains(fsPath, "..") || filepath.IsAbs(fsPath) || strings.HasPrefix(fsPath, "/") { return nil @@ -402,7 +402,7 @@ func pathStrToFullPaths(revision, assetType, pathStr string) []pathCandidate { if strings.Contains(pathStr, ")ko)") { entries = append(entries, tagged{strings.ReplaceAll(pathStr, ")ko)", ")en)"), true}) } - base := filepath.Join("assets", "revisions", revision) + base := filepath.Join(baseDir, "assets", "revisions", revision) var out []pathCandidate seen := make(map[string]bool) for _, e := range entries { @@ -434,13 +434,13 @@ func appendUniqueCandidate(candidates []assetCandidate, seen map[string]bool, ca return append(candidates, candidate) } -func duplicateCandidatePath(candidate assetCandidate, assetType, targetRevision, targetBaseName string) string { - root := filepath.Join("assets", "revisions", candidate.Revision, assetType) +func duplicateCandidatePath(baseDir string, candidate assetCandidate, assetType, targetRevision, targetBaseName string) string { + root := filepath.Join(baseDir, "assets", "revisions", candidate.Revision, assetType) rel, err := filepath.Rel(root, candidate.Path) if err != nil || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) { return "" } - return filepath.Join("assets", "revisions", targetRevision, assetType, filepath.Dir(rel), targetBaseName) + return filepath.Join(baseDir, "assets", "revisions", targetRevision, assetType, filepath.Dir(rel), targetBaseName) } // objectIdToFilePathCandidates returns candidate file paths for the object: list.bin path, locale fallbacks @@ -448,8 +448,8 @@ func duplicateCandidatePath(candidate assetCandidate, assetType, targetRevision, // The original locale path is tried first (with MD5 validation); locale fallbacks are tried after // (without MD5 validation, since the hash in list.bin refers to the original locale's content). // Callers should try each path until one exists on disk. -func objectIdToFilePathCandidates(revision, assetType, objectId string) (candidates []assetCandidate, size int64, ok bool) { - idx, ok := loadListBinIndex(revision) +func objectIdToFilePathCandidates(baseDir, revision, assetType, objectId string) (candidates []assetCandidate, size int64, ok bool) { + idx, ok := loadListBinIndex(baseDir, revision) if !ok || idx == nil { return nil, 0, false } @@ -457,7 +457,7 @@ func objectIdToFilePathCandidates(revision, assetType, objectId string) (candida if !ok || entry.Path == "" { return nil, 0, false } - paths := pathStrToFullPaths(revision, assetType, entry.Path) + paths := pathStrToFullPaths(baseDir, revision, assetType, entry.Path) if len(paths) == 0 { return nil, 0, false } @@ -474,15 +474,14 @@ func objectIdToFilePathCandidates(revision, assetType, objectId string) (candida ExpectedMD5: md5, }) } - // Add paths from info.json: when requested file is a "from-name" (duplicate not included), serve "to-name" instead. - infoIndex := loadInfoIndex(revision) + infoIndex := loadInfoIndex(baseDir, revision) if len(infoIndex) > 0 { for _, c := range candidates { alias, ok := infoIndex[filepath.Base(c.Path)] if !ok || alias.ToName == "" { continue } - alt := duplicateCandidatePath(c, assetType, alias.ToRevision, alias.ToName) + alt := duplicateCandidatePath(baseDir, c, assetType, alias.ToRevision, alias.ToName) if alt == "" { continue } diff --git a/server/internal/service/loginbonus.go b/server/internal/service/loginbonus.go index cb90890..0fc6775 100644 --- a/server/internal/service/loginbonus.go +++ b/server/internal/service/loginbonus.go @@ -11,7 +11,6 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -29,9 +28,9 @@ func NewLoginBonusServiceServer(users store.UserRepository, sessions store.Sessi func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) { log.Printf("[LoginBonusService] ReceiveStamp") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { now := gametime.NowMillis() nextStamp := user.LoginBonus.CurrentStampNumber + 1 @@ -64,9 +63,5 @@ func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb user.LoginBonus.LatestVersion = now }) - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user, - []string{"IUserLoginBonus"}, - )) - setCommonResponseTrailers(ctx, diff, false) - return &pb.ReceiveStampResponse{DiffUserData: diff}, nil + return &pb.ReceiveStampResponse{}, nil } diff --git a/server/internal/service/material.go b/server/internal/service/material.go index 6b448a7..d9b0138 100644 --- a/server/internal/service/material.go +++ b/server/internal/service/material.go @@ -8,14 +8,8 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) -var materialDiffTables = []string{ - "IUserMaterial", - "IUserConsumableItem", -} - type MaterialServiceServer struct { pb.UnimplementedMaterialServiceServer users store.UserRepository @@ -31,13 +25,9 @@ func NewMaterialServiceServer(users store.UserRepository, sessions store.Session func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) { log.Printf("[MaterialService] Sell: %d item(s)", len(req.MaterialPossession)) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - oldUser, _ := s.users.LoadUser(userId) - tracker := userdata.NewDeleteTracker(). - Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}) - - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { totalGold := int32(0) for _, item := range req.MaterialPossession { mat, ok := s.catalog.All[item.MaterialId] @@ -71,10 +61,5 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe return nil, fmt.Errorf("material sell: %w", err) } - tables := userdata.ProjectTables(snapshot, materialDiffTables) - diff := tracker.Apply(snapshot, tables) - - return &pb.MaterialSellResponse{ - DiffUserData: diff, - }, nil + return &pb.MaterialSellResponse{}, nil } diff --git a/server/internal/service/mission.go b/server/internal/service/mission.go index e7721e2..c0a9c2f 100644 --- a/server/internal/service/mission.go +++ b/server/internal/service/mission.go @@ -7,7 +7,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type MissionServiceServer struct { @@ -23,15 +22,11 @@ func NewMissionServiceServer(users store.UserRepository, sessions store.SessionR func (s *MissionServiceServer) UpdateMissionProgress(ctx context.Context, req *pb.UpdateMissionProgressRequest) (*pb.UpdateMissionProgressResponse, error) { log.Printf("[MissionService] UpdateMissionProgress: cage=%v pictureBook=%v", req.CageMeasurableValues, req.PictureBookMeasurableValues) - userId := currentUserId(ctx, s.users, s.sessions) - snapshot, err := s.users.LoadUser(userId) + userId := CurrentUserId(ctx, s.users, s.sessions) + _, err := s.users.LoadUser(userId) if err != nil { return nil, fmt.Errorf("snapshot user: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserMission"})) - - return &pb.UpdateMissionProgressResponse{ - DiffUserData: diff, - }, nil + return &pb.UpdateMissionProgressResponse{}, nil } diff --git a/server/internal/service/movie.go b/server/internal/service/movie.go index 2feebdf..fc7a863 100644 --- a/server/internal/service/movie.go +++ b/server/internal/service/movie.go @@ -8,7 +8,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type MovieServiceServer struct { @@ -24,10 +23,10 @@ func NewMovieServiceServer(users store.UserRepository, sessions store.SessionRep func (s *MovieServiceServer) SaveViewedMovie(ctx context.Context, req *pb.SaveViewedMovieRequest) (*pb.SaveViewedMovieResponse, error) { log.Printf("[MovieService] SaveViewedMovie: movieIds=%v", req.MovieId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) now := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { for _, mid := range req.MovieId { user.ViewedMovies[mid] = now } @@ -36,9 +35,5 @@ func (s *MovieServiceServer) SaveViewedMovie(ctx context.Context, req *pb.SaveVi return nil, fmt.Errorf("update user: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserMovie"})) - - return &pb.SaveViewedMovieResponse{ - DiffUserData: diff, - }, nil + return &pb.SaveViewedMovieResponse{}, nil } diff --git a/server/internal/service/navicutin.go b/server/internal/service/navicutin.go index 84d058b..87004e0 100644 --- a/server/internal/service/navicutin.go +++ b/server/internal/service/navicutin.go @@ -7,7 +7,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type NaviCutInServiceServer struct { @@ -23,17 +22,13 @@ func NewNaviCutInServiceServer(users store.UserRepository, sessions store.Sessio func (s *NaviCutInServiceServer) RegisterPlayed(ctx context.Context, req *pb.RegisterPlayedRequest) (*pb.RegisterPlayedResponse, error) { log.Printf("[NaviCutInService] RegisterPlayed: naviCutId=%d", req.NaviCutId) - userId := currentUserId(ctx, s.users, s.sessions) - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { user.NaviCutInPlayed[req.NaviCutId] = true }) if err != nil { return nil, fmt.Errorf("update user: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserNaviCutIn"})) - - return &pb.RegisterPlayedResponse{ - DiffUserData: diff, - }, nil + return &pb.RegisterPlayedResponse{}, nil } diff --git a/server/internal/service/notification.go b/server/internal/service/notification.go index 9459c74..b4c78b6 100644 --- a/server/internal/service/notification.go +++ b/server/internal/service/notification.go @@ -6,7 +6,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -23,20 +22,18 @@ 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) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { return &pb.GetHeaderNotificationResponse{ GiftNotReceiveCount: 0, FriendRequestReceiveCount: 0, IsExistUnreadInformation: false, - DiffUserData: userdata.EmptyDiff(), }, nil } return &pb.GetHeaderNotificationResponse{ GiftNotReceiveCount: int32(len(user.Gifts.NotReceived)), FriendRequestReceiveCount: user.Notifications.FriendRequestReceiveCount, IsExistUnreadInformation: user.Notifications.IsExistUnreadInformation, - DiffUserData: userdata.EmptyDiff(), }, nil } diff --git a/server/internal/service/octo.go b/server/internal/service/octo.go index 9b10b06..8e50797 100644 --- a/server/internal/service/octo.go +++ b/server/internal/service/octo.go @@ -50,6 +50,7 @@ const resourcesURLOriginal = "https://resources.app.nierreincarnation.com" type OctoHTTPServer struct { mux *http.ServeMux ResourcesBaseURL string // if non-empty and exactly 43 chars, list.bin is rewritten to use this base for asset URLs + BaseDir string // root directory containing the assets/ tree; empty means current directory revisions *revisionTracker resolver *assetResolver } @@ -124,12 +125,13 @@ func fileMD5Hex(path string, info os.FileInfo) (string, error) { return sum, nil } -func NewOctoHTTPServer(resourcesBaseURL string) *OctoHTTPServer { +func NewOctoHTTPServer(resourcesBaseURL, baseDir string) *OctoHTTPServer { s := &OctoHTTPServer{ mux: http.NewServeMux(), ResourcesBaseURL: resourcesBaseURL, + BaseDir: baseDir, revisions: newRevisionTracker(), - resolver: newAssetResolver(), + resolver: newAssetResolver(baseDir), } s.resolver.Prewarm("0") s.mux.HandleFunc("/", s.handleAll) @@ -226,7 +228,7 @@ func (s *OctoHTTPServer) handleOctoV2(w http.ResponseWriter, r *http.Request, pa requestedRevision := parts[len(parts)-1] if requestedRevision != "" { revision := "0" - filePath := "assets/revisions/0/list.bin" + filePath := filepath.Join(s.BaseDir, "assets", "revisions", "0", "list.bin") if requestedRevision != revision { log.Printf("[OctoV2] Resource list request revision=%s canonicalized to revision=%s", requestedRevision, revision) } @@ -266,7 +268,7 @@ func (s *OctoHTTPServer) serveOctoV1List(w http.ResponseWriter, r *http.Request, requestedRevision = parts[len(parts)-1] } revision := "0" - filePath := "assets/revisions/0/list.bin" + filePath := filepath.Join(s.BaseDir, "assets", "revisions", "0", "list.bin") if requestedRevision != revision { log.Printf("[OctoV1] list request revision=%s canonicalized to revision=%s", requestedRevision, revision) } @@ -316,11 +318,11 @@ func (s *OctoHTTPServer) serveUnsoAsset(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusNotFound) return } - baseDir := filepath.Join("assets", "revisions") + revDir := filepath.Join(s.BaseDir, "assets", "revisions") var triedPaths []string var md5Mismatches []string for _, candidate := range resolution.Candidates { - rel, err := filepath.Rel(baseDir, candidate.Path) + rel, err := filepath.Rel(revDir, candidate.Path) if err != nil || strings.Contains(rel, "..") || filepath.IsAbs(rel) { continue } @@ -409,9 +411,9 @@ func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Reques break } } - filePath := "assets/release/database.bin.e" + filePath := filepath.Join(s.BaseDir, "assets", "release", "database.bin.e") if version != "" { - vPath := "assets/release/" + version + ".bin.e" + vPath := filepath.Join(s.BaseDir, "assets", "release", version+".bin.e") if _, err := os.Stat(vPath); err == nil { filePath = vPath } diff --git a/server/internal/service/omikuji.go b/server/internal/service/omikuji.go index 1b43699..816349d 100644 --- a/server/internal/service/omikuji.go +++ b/server/internal/service/omikuji.go @@ -9,7 +9,6 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type OmikujiServiceServer struct { @@ -26,21 +25,18 @@ func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionR func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiDrawRequest) (*pb.OmikujiDrawResponse, error) { log.Printf("[OmikujiService] OmikujiDraw: omikujiId=%d", req.OmikujiId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) now := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { user.DrawnOmikuji[req.OmikujiId] = now }) if err != nil { return nil, fmt.Errorf("update user: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserOmikuji"})) - return &pb.OmikujiDrawResponse{ OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId), OmikujiItem: []*pb.OmikujiItem{}, - DiffUserData: diff, }, nil } diff --git a/server/internal/service/parts.go b/server/internal/service/parts.go index b9a3754..b83d02c 100644 --- a/server/internal/service/parts.go +++ b/server/internal/service/parts.go @@ -10,16 +10,10 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) const partsMaxLevel = int32(15) -var partsDiffTables = []string{ - "IUserParts", - "IUserConsumableItem", -} - type PartsServiceServer struct { pb.UnimplementedPartsServiceServer users store.UserRepository @@ -35,13 +29,9 @@ func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRep func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) { log.Printf("[PartsService] Sell: %d part(s)", len(req.UserPartsUuid)) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - oldUser, _ := s.users.LoadUser(userId) - tracker := userdata.NewDeleteTracker(). - Track("IUserParts", oldUser, userdata.SortedPartsRecords, []string{"userId", "userPartsUuid"}) - - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { totalGold := int32(0) for _, uuid := range req.UserPartsUuid { part, ok := user.Parts[uuid] @@ -81,23 +71,18 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) return nil, fmt.Errorf("parts sell: %w", err) } - tables := userdata.ProjectTables(snapshot, partsDiffTables) - diff := tracker.Apply(snapshot, tables) - - return &pb.PartsSellResponse{ - DiffUserData: diff, - }, nil + return &pb.PartsSellResponse{}, nil } func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) { log.Printf("[PartsService] Enhance: uuid=%s", req.UserPartsUuid) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() isSuccess := false - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { part, ok := user.Parts[req.UserPartsUuid] if !ok { log.Printf("[PartsService] Enhance: part uuid=%s not found", req.UserPartsUuid) @@ -158,11 +143,8 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe return nil, fmt.Errorf("parts enhance: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, partsDiffTables)) - return &pb.PartsEnhanceResponse{ - IsSuccess: isSuccess, - DiffUserData: diff, + IsSuccess: isSuccess, }, nil } @@ -170,10 +152,10 @@ func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsRep log.Printf("[PartsService] ReplacePreset: preset=%d uuids=[%s, %s, %s]", req.UserPartsPresetNumber, req.UserPartsUuid01, req.UserPartsUuid02, req.UserPartsUuid03) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { preset := user.PartsPresets[req.UserPartsPresetNumber] preset.UserPartsPresetNumber = req.UserPartsPresetNumber preset.UserPartsUuid01 = req.UserPartsUuid01 @@ -186,9 +168,5 @@ func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsRep return nil, fmt.Errorf("parts replace preset: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserPartsPreset"})) - - return &pb.PartsReplacePresetResponse{ - DiffUserData: diff, - }, nil + return &pb.PartsReplacePresetResponse{}, nil } diff --git a/server/internal/service/portalcage.go b/server/internal/service/portalcage.go index b293c1c..18b17d7 100644 --- a/server/internal/service/portalcage.go +++ b/server/internal/service/portalcage.go @@ -7,7 +7,6 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type PortalCageServiceServer struct { @@ -23,17 +22,11 @@ func NewPortalCageServiceServer(users store.UserRepository, sessions store.Sessi func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Context, req *pb.UpdatePortalCageSceneProgressRequest) (*pb.UpdatePortalCageSceneProgressResponse, error) { log.Printf("[PortalCageService] UpdatePortalCageSceneProgress: portalCageSceneId=%d", req.PortalCageSceneId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { now := gametime.NowMillis() user.PortalCageStatus.IsCurrentProgress = true user.PortalCageStatus.LatestVersion = now }) - - tables := userdata.ProjectTables(user, - []string{"IUserPortalCageStatus"}, - ) - return &pb.UpdatePortalCageSceneProgressResponse{ - DiffUserData: userdata.BuildDiffFromTablesOrdered(tables, []string{"IUserPortalCageStatus"}), - }, nil + return &pb.UpdatePortalCageSceneProgressResponse{}, nil } diff --git a/server/internal/service/quest_bighunt.go b/server/internal/service/quest_bighunt.go index c6e8a47..0397e17 100644 --- a/server/internal/service/quest_bighunt.go +++ b/server/internal/service/quest_bighunt.go @@ -10,7 +10,6 @@ import ( "lunar-tear/server/internal/model" "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -32,25 +31,11 @@ func NewBigHuntServiceServer( return &BigHuntServiceServer{users: users, sessions: sessions, catalog: catalog, engine: engine} } -var bigHuntDiffTables = []string{ - "IUserBigHuntProgressStatus", - "IUserBigHuntMaxScore", - "IUserBigHuntStatus", - "IUserBigHuntScheduleMaxScore", - "IUserBigHuntWeeklyMaxScore", - "IUserBigHuntWeeklyStatus", -} - -func buildBigHuntDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData { - tables := userdata.ProjectTables(user, tableNames) - return userdata.BuildDiffFromTablesOrdered(tables, tableNames) -} - func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.StartBigHuntQuestRequest) (*pb.StartBigHuntQuestResponse, error) { log.Printf("[BigHuntService] StartBigHuntQuest: bossQuestId=%d questId=%d deckNumber=%d isDryRun=%v", req.BigHuntBossQuestId, req.BigHuntQuestId, req.UserDeckNumber, req.IsDryRun) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() bhQuest, ok := s.catalog.QuestById[req.BigHuntQuestId] @@ -58,7 +43,7 @@ func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.St log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId) } - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { if ok { s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis) } @@ -80,31 +65,27 @@ func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.St user.BigHuntStatuses[req.BigHuntBossQuestId] = st }) - return &pb.StartBigHuntQuestResponse{ - DiffUserData: buildBigHuntDiff(user, append([]string{"IUserQuest"}, bigHuntDiffTables...)), - }, nil + return &pb.StartBigHuntQuestResponse{}, nil } func (s *BigHuntServiceServer) UpdateBigHuntQuestSceneProgress(ctx context.Context, req *pb.UpdateBigHuntQuestSceneProgressRequest) (*pb.UpdateBigHuntQuestSceneProgressResponse, error) { log.Printf("[BigHuntService] UpdateBigHuntQuestSceneProgress: questSceneId=%d", req.QuestSceneId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { user.BigHuntProgress.CurrentQuestSceneId = req.QuestSceneId user.BigHuntProgress.LatestVersion = nowMillis }) - return &pb.UpdateBigHuntQuestSceneProgressResponse{ - DiffUserData: buildBigHuntDiff(user, []string{"IUserBigHuntProgressStatus"}), - }, nil + return &pb.UpdateBigHuntQuestSceneProgressResponse{}, nil } func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.FinishBigHuntQuestRequest) (*pb.FinishBigHuntQuestResponse, error) { log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v", req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() bhQuest := s.catalog.QuestById[req.BigHuntQuestId] @@ -114,7 +95,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F var scoreInfo *pb.BigHuntScoreInfo var scoreRewards []*pb.BigHuntReward - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleBigHuntQuestFinish(user, bhQuest.QuestId, req.IsRetired, false, nowMillis) if req.IsRetired || user.BigHuntProgress.IsDryRun { @@ -229,18 +210,13 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F BattleReport: &pb.BigHuntBattleReport{ BattleReportWave: []*pb.BigHuntBattleReportWave{}, }, - DiffUserData: buildBigHuntDiff(user, append([]string{ - "IUserQuest", - "IUserConsumableItem", - "IUserMaterial", - }, bigHuntDiffTables...)), }, nil } func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb.RestartBigHuntQuestRequest) (*pb.RestartBigHuntQuestResponse, error) { log.Printf("[BigHuntService] RestartBigHuntQuest: bossQuestId=%d questId=%d", req.BigHuntBossQuestId, req.BigHuntQuestId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() bhQuest := s.catalog.QuestById[req.BigHuntQuestId] @@ -248,7 +224,7 @@ func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb. var battleBinary []byte var deckNumber int32 - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, user.BigHuntDeckNumber, nowMillis) user.BigHuntProgress.CurrentQuestSceneId = 0 @@ -267,17 +243,16 @@ func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb. return &pb.RestartBigHuntQuestResponse{ BattleBinary: battleBinary, DeckNumber: deckNumber, - DiffUserData: buildBigHuntDiff(user, append([]string{"IUserQuest"}, bigHuntDiffTables...)), }, nil } func (s *BigHuntServiceServer) SkipBigHuntQuest(ctx context.Context, req *pb.SkipBigHuntQuestRequest) (*pb.SkipBigHuntQuestResponse, error) { log.Printf("[BigHuntService] SkipBigHuntQuest: bossQuestId=%d skipCount=%d", req.BigHuntBossQuestId, req.SkipCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { st := user.BigHuntStatuses[req.BigHuntBossQuestId] st.DailyChallengeCount += req.SkipCount st.LatestChallengeDatetime = nowMillis @@ -286,15 +261,14 @@ func (s *BigHuntServiceServer) SkipBigHuntQuest(ctx context.Context, req *pb.Ski }) return &pb.SkipBigHuntQuestResponse{ - ScoreReward: []*pb.BigHuntReward{}, - DiffUserData: buildBigHuntDiff(user, bigHuntDiffTables), + ScoreReward: []*pb.BigHuntReward{}, }, nil } func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *pb.SaveBigHuntBattleInfoRequest) (*pb.SaveBigHuntBattleInfoResponse, error) { log.Printf("[BigHuntService] SaveBigHuntBattleInfo: elapsedFrames=%d", req.ElapsedFrameCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() var totalDamage int64 @@ -306,7 +280,7 @@ func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *p } } - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { user.BigHuntBattleBinary = req.BattleBinary if req.BigHuntBattleDetail != nil { @@ -322,15 +296,13 @@ func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *p user.BigHuntProgress.LatestVersion = nowMillis }) - return &pb.SaveBigHuntBattleInfoResponse{ - DiffUserData: buildBigHuntDiff(user, []string{"IUserBigHuntProgressStatus"}), - }, nil + return &pb.SaveBigHuntBattleInfoResponse{}, nil } func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) { log.Printf("[BigHuntService] GetBigHuntTopData") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, _ := s.users.LoadUser(userId) nowMillis := gametime.NowMillis() @@ -368,7 +340,6 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb WeeklyScoreReward: weeklyRewards, IsReceivedWeeklyScoreReward: ws.IsReceivedWeeklyReward, LastWeekWeeklyScoreReward: lastWeekRewards, - DiffUserData: buildBigHuntDiff(user, bigHuntDiffTables), }, nil } diff --git a/server/internal/service/quest_event.go b/server/internal/service/quest_event.go index 5fb8323..113e5ed 100644 --- a/server/internal/service/quest_event.go +++ b/server/internal/service/quest_event.go @@ -8,7 +8,6 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -16,9 +15,9 @@ import ( func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) { log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) }) @@ -34,12 +33,6 @@ func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartE return &pb.StartEventQuestResponse{ BattleDropReward: pbDrops, - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserStatus", - "IUserQuest", - "IUserQuestMission", - "IUserEventQuestProgressStatus", - }), }, nil } @@ -47,34 +40,12 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated) nowMillis := gametime.NowMillis() - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { outcome = s.engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) }) - diff := buildSelectedQuestDiff(user, []string{ - "IUserQuest", - "IUserQuestMission", - "IUserEventQuestProgressStatus", - "IUserStatus", - "IUserGem", - "IUserCharacter", - "IUserCostume", - "IUserCostumeActiveSkill", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserWeaponNote", - "IUserCompanion", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", - "IUserParts", - "IUserPartsGroupNote", - }) - userdata.AddWeaponStoryDiff(diff, user, outcome.ChangedWeaponStoryIds) - return &pb.FinishEventQuestResponse{ DropReward: toProtoRewards(outcome.DropRewards), FirstClearReward: toProtoRewards(outcome.FirstClearRewards), @@ -84,55 +55,31 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis IsBigWin: outcome.IsBigWin, BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds, UserStatusCampaignReward: []*pb.QuestReward{}, - DiffUserData: diff, }, nil } func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.RestartEventQuestRequest) (*pb.RestartEventQuestResponse, error) { log.Printf("[QuestService] RestartEventQuest: chapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis()) }) return &pb.RestartEventQuestResponse{ BattleDropReward: []*pb.BattleDropReward{}, - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserQuest", - "IUserQuestMission", - "IUserEventQuestProgressStatus", - }), }, nil } func (s *QuestServiceServer) UpdateEventQuestSceneProgress(ctx context.Context, req *pb.UpdateEventQuestSceneProgressRequest) (*pb.UpdateEventQuestSceneProgressResponse, error) { log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) }) - diff := buildSelectedQuestDiff(user, []string{ - "IUserEventQuestProgressStatus", - "IUserCharacter", - "IUserCostume", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserCompanion", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", - "IUserParts", - "IUserPartsGroupNote", - }) - userdata.AddWeaponStoryDiff(diff, user, s.engine.Granter.DrainChangedStoryWeaponIds()) - - return &pb.UpdateEventQuestSceneProgressResponse{ - DiffUserData: diff, - }, nil + return &pb.UpdateEventQuestSceneProgressResponse{}, nil } const defaultGuerrillaFreeOpenMinutes = int32(60) @@ -140,16 +87,14 @@ const defaultGuerrillaFreeOpenMinutes = int32(60) func (s *QuestServiceServer) StartGuerrillaFreeOpen(ctx context.Context, req *emptypb.Empty) (*pb.StartGuerrillaFreeOpenResponse, error) { log.Printf("[QuestService] StartGuerrillaFreeOpen") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { user.GuerrillaFreeOpen.StartDatetime = nowMillis user.GuerrillaFreeOpen.OpenMinutes = defaultGuerrillaFreeOpenMinutes user.GuerrillaFreeOpen.DailyOpenedCount++ user.GuerrillaFreeOpen.LatestVersion = nowMillis }) - return &pb.StartGuerrillaFreeOpenResponse{ - DiffUserData: buildSelectedQuestDiff(user, []string{"IUserEventQuestGuerrillaFreeOpen"}), - }, nil + return &pb.StartGuerrillaFreeOpenResponse{}, nil } diff --git a/server/internal/service/quest_extra.go b/server/internal/service/quest_extra.go index e89ab69..8a4b0be 100644 --- a/server/internal/service/quest_extra.go +++ b/server/internal/service/quest_extra.go @@ -8,15 +8,14 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) func (s *QuestServiceServer) StartExtraQuest(ctx context.Context, req *pb.StartExtraQuestRequest) (*pb.StartExtraQuestResponse, error) { log.Printf("[QuestService] StartExtraQuest: questId=%d deckNumber=%d", req.QuestId, req.UserDeckNumber) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleExtraQuestStart(user, req.QuestId, req.UserDeckNumber, nowMillis) }) @@ -32,12 +31,6 @@ func (s *QuestServiceServer) StartExtraQuest(ctx context.Context, req *pb.StartE return &pb.StartExtraQuestResponse{ BattleDropReward: pbDrops, - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserStatus", - "IUserQuest", - "IUserQuestMission", - "IUserExtraQuestProgressStatus", - }), }, nil } @@ -45,34 +38,12 @@ func (s *QuestServiceServer) FinishExtraQuest(ctx context.Context, req *pb.Finis log.Printf("[QuestService] FinishExtraQuest: questId=%d isRetired=%v isAnnihilated=%v", req.QuestId, req.IsRetired, req.IsAnnihilated) nowMillis := gametime.NowMillis() - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { outcome = s.engine.HandleExtraQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) }) - diff := buildSelectedQuestDiff(user, []string{ - "IUserQuest", - "IUserQuestMission", - "IUserExtraQuestProgressStatus", - "IUserStatus", - "IUserGem", - "IUserCharacter", - "IUserCostume", - "IUserCostumeActiveSkill", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserWeaponNote", - "IUserCompanion", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", - "IUserParts", - "IUserPartsGroupNote", - }) - userdata.AddWeaponStoryDiff(diff, user, outcome.ChangedWeaponStoryIds) - return &pb.FinishExtraQuestResponse{ DropReward: toProtoRewards(outcome.DropRewards), FirstClearReward: toProtoRewards(outcome.FirstClearRewards), @@ -81,16 +52,17 @@ func (s *QuestServiceServer) FinishExtraQuest(ctx context.Context, req *pb.Finis IsBigWin: outcome.IsBigWin, BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds, UserStatusCampaignReward: []*pb.QuestReward{}, - DiffUserData: diff, }, nil } func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.RestartExtraQuestRequest) (*pb.RestartExtraQuestResponse, error) { log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + var deckNumber int32 + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis()) + deckNumber = user.Quests[req.QuestId].UserDeckNumber }) drops := s.engine.BattleDropRewards(req.QuestId) @@ -105,40 +77,17 @@ func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.Rest return &pb.RestartExtraQuestResponse{ BattleDropReward: pbDrops, - DeckNumber: user.Quests[req.QuestId].UserDeckNumber, - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserQuest", - "IUserQuestMission", - "IUserExtraQuestProgressStatus", - }), + DeckNumber: deckNumber, }, nil } func (s *QuestServiceServer) UpdateExtraQuestSceneProgress(ctx context.Context, req *pb.UpdateExtraQuestSceneProgressRequest) (*pb.UpdateExtraQuestSceneProgressResponse, error) { log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) }) - diff := buildSelectedQuestDiff(user, []string{ - "IUserExtraQuestProgressStatus", - "IUserCharacter", - "IUserCostume", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserCompanion", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", - "IUserParts", - "IUserPartsGroupNote", - }) - userdata.AddWeaponStoryDiff(diff, user, s.engine.Granter.DrainChangedStoryWeaponIds()) - - return &pb.UpdateExtraQuestSceneProgressResponse{ - DiffUserData: diff, - }, nil + return &pb.UpdateExtraQuestSceneProgressResponse{}, nil } diff --git a/server/internal/service/quest_main.go b/server/internal/service/quest_main.go index a16918c..0d5584d 100644 --- a/server/internal/service/quest_main.go +++ b/server/internal/service/quest_main.go @@ -9,7 +9,6 @@ import ( "lunar-tear/server/internal/model" "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -28,91 +27,45 @@ func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRep return &QuestServiceServer{users: users, sessions: sessions, engine: engine} } -func buildSelectedQuestDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData { - tables := userdata.ProjectTables(user, tableNames) - return userdata.BuildDiffFromTablesOrdered(tables, tableNames) -} - func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, req *pb.UpdateMainFlowSceneProgressRequest) (*pb.UpdateMainFlowSceneProgressResponse, error) { log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) }) - diff := buildSelectedQuestDiff(user, []string{ - "IUserMainQuestFlowStatus", - "IUserMainQuestMainFlowStatus", - "IUserMainQuestProgressStatus", - "IUserMainQuestSeasonRoute", - "IUserPortalCageStatus", - "IUserSideStoryQuestSceneProgressStatus", - "IUserQuest", - "IUserCharacter", - "IUserCostume", - "IUserCostumeActiveSkill", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserWeaponNote", - "IUserCompanion", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", - "IUserParts", - "IUserPartsGroupNote", - }) - userdata.AddWeaponStoryDiff(diff, user, s.engine.Granter.DrainChangedStoryWeaponIds()) - - return &pb.UpdateMainFlowSceneProgressResponse{ - DiffUserData: diff, - }, nil + return &pb.UpdateMainFlowSceneProgressResponse{}, nil } func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, req *pb.UpdateReplayFlowSceneProgressRequest) (*pb.UpdateReplayFlowSceneProgressResponse, error) { log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) }) - return &pb.UpdateReplayFlowSceneProgressResponse{ - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserMainQuestFlowStatus", - "IUserMainQuestReplayFlowStatus", - }), - }, nil + return &pb.UpdateReplayFlowSceneProgressResponse{}, nil } func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) { log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleMainQuestSceneProgress(user, req.QuestSceneId) }) - return &pb.UpdateMainQuestSceneProgressResponse{ - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserStatus", - "IUserCharacter", - "IUserQuest", - "IUserQuestMission", - "IUserMainQuestFlowStatus", - "IUserMainQuestMainFlowStatus", - "IUserMainQuestProgressStatus", - }), - }, nil + return &pb.UpdateMainQuestSceneProgressResponse{}, nil } func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) { log.Printf("[QuestService] StartMainQuest: %+v", req) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { if req.IsReplayFlow { s.engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) } else { @@ -132,16 +85,6 @@ func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMa return &pb.StartMainQuestResponse{ BattleDropReward: pbDrops, - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserStatus", - "IUserQuest", - "IUserQuestMission", - "IUserMainQuestFlowStatus", - "IUserMainQuestMainFlowStatus", - "IUserMainQuestProgressStatus", - "IUserMainQuestSeasonRoute", - "IUserMainQuestReplayFlowStatus", - }), }, nil } @@ -165,38 +108,12 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType) nowMillis := gametime.NowMillis() - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { outcome = s.engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) }) - diff := buildSelectedQuestDiff(user, []string{ - "IUserQuest", - "IUserQuestMission", - "IUserMainQuestFlowStatus", - "IUserMainQuestMainFlowStatus", - "IUserMainQuestProgressStatus", - "IUserMainQuestSeasonRoute", - "IUserMainQuestReplayFlowStatus", - "IUserStatus", - "IUserGem", - "IUserCharacter", - "IUserCostume", - "IUserCostumeActiveSkill", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserWeaponNote", - "IUserCompanion", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", - "IUserParts", - "IUserPartsGroupNote", - }) - userdata.AddWeaponStoryDiff(diff, user, outcome.ChangedWeaponStoryIds) - return &pb.FinishMainQuestResponse{ DropReward: toProtoRewards(outcome.DropRewards), FirstClearReward: toProtoRewards(outcome.FirstClearRewards), @@ -207,16 +124,17 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds, ReplayFlowFirstClearReward: toProtoRewards(outcome.ReplayFlowFirstClearRewards), UserStatusCampaignReward: []*pb.QuestReward{}, - DiffUserData: diff, }, nil } func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.RestartMainQuestRequest) (*pb.RestartMainQuestResponse, error) { log.Printf("[QuestService] RestartMainQuest: questId=%d isMainFlow=%v", req.QuestId, req.IsMainFlow) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + var deckNumber int32 + s.users.UpdateUser(userId, func(user *store.UserState) { s.engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis()) + deckNumber = user.Quests[req.QuestId].UserDeckNumber }) drops := s.engine.BattleDropRewards(req.QuestId) @@ -231,33 +149,22 @@ func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.Resta return &pb.RestartMainQuestResponse{ BattleDropReward: pbDrops, - DeckNumber: user.Quests[req.QuestId].UserDeckNumber, - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserStatus", - "IUserQuest", - "IUserQuestMission", - "IUserMainQuestFlowStatus", - "IUserMainQuestMainFlowStatus", - "IUserMainQuestProgressStatus", - "IUserMainQuestSeasonRoute", - }), + DeckNumber: deckNumber, }, nil } func (s *QuestServiceServer) FinishAutoOrbit(ctx context.Context, req *emptypb.Empty) (*pb.FinishAutoOrbitResponse, error) { log.Printf("[QuestService] FinishAutoOrbit") - return &pb.FinishAutoOrbitResponse{ - DiffUserData: userdata.EmptyDiff(), - }, nil + return &pb.FinishAutoOrbitResponse{}, nil } func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestRequest) (*pb.SkipQuestResponse, error) { log.Printf("[QuestService] SkipQuest: questId=%d skipCount=%d useEffectItems=%d", req.QuestId, req.SkipCount, len(req.UseEffectItem)) nowMillis := gametime.NowMillis() - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { for _, item := range req.UseEffectItem { log.Printf("[QuestService] SkipQuest UseEffectItem: consumableItemId=%d count=%d", item.ConsumableItemId, item.Count) user.ConsumableItems[item.ConsumableItemId] -= item.Count @@ -271,24 +178,14 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq return &pb.SkipQuestResponse{ DropReward: toProtoRewards(outcome.DropRewards), UserStatusCampaignReward: []*pb.QuestReward{}, - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserQuest", - "IUserStatus", - "IUserConsumableItem", - "IUserMaterial", - "IUserParts", - "IUserPartsGroupNote", - "IUserCharacter", - "IUserCostume", - }), }, nil } func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) { log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId if seasonId, ok := s.engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok { user.MainQuest.MainQuestSeasonId = seasonId @@ -298,30 +195,22 @@ func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteReque user.PortalCageStatus.LatestVersion = now }) - return &pb.SetRouteResponse{ - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserMainQuestSeasonRoute", - "IUserMainQuestMainFlowStatus", - "IUserPortalCageStatus", - }), - }, nil + return &pb.SetRouteResponse{}, nil } func (s *QuestServiceServer) SetQuestSceneChoice(ctx context.Context, req *pb.SetQuestSceneChoiceRequest) (*pb.SetQuestSceneChoiceResponse, error) { log.Printf("[QuestService] SetQuestSceneChoice: questSceneId=%d choiceNumber=%d", req.QuestSceneId, req.ChoiceNumber) - return &pb.SetQuestSceneChoiceResponse{ - DiffUserData: userdata.EmptyDiff(), - }, nil + return &pb.SetQuestSceneChoiceResponse{}, nil } func (s *QuestServiceServer) ResetLimitContentQuestProgress(ctx context.Context, req *pb.ResetLimitContentQuestProgressRequest) (*pb.ResetLimitContentQuestProgressResponse, error) { log.Printf("[QuestService] ResetLimitContentQuestProgress: eventQuestChapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { if _, exists := user.SideStoryQuests[req.QuestId]; exists { user.SideStoryQuests[req.QuestId] = store.SideStoryQuestProgress{ HeadSideStoryQuestSceneId: 0, @@ -339,20 +228,14 @@ func (s *QuestServiceServer) ResetLimitContentQuestProgress(ctx context.Context, } }) - return &pb.ResetLimitContentQuestProgressResponse{ - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserSideStoryQuest", - "IUserSideStoryQuestSceneProgressStatus", - "IUserQuestLimitContentStatus", - }), - }, nil + return &pb.ResetLimitContentQuestProgressResponse{}, nil } func (s *QuestServiceServer) SetAutoSaleSetting(ctx context.Context, req *pb.SetAutoSaleSettingRequest) (*pb.SetAutoSaleSettingResponse, error) { log.Printf("[QuestService] SetAutoSaleSetting: items=%d", len(req.AutoSaleSettingItem)) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { user.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState, len(req.AutoSaleSettingItem)) for itemType, itemValue := range req.AutoSaleSettingItem { user.AutoSaleSettings[itemType] = store.AutoSaleSettingState{ @@ -362,9 +245,5 @@ func (s *QuestServiceServer) SetAutoSaleSetting(ctx context.Context, req *pb.Set } }) - return &pb.SetAutoSaleSettingResponse{ - DiffUserData: buildSelectedQuestDiff(user, []string{ - "IUserAutoSaleSettingDetail", - }), - }, nil + return &pb.SetAutoSaleSettingResponse{}, nil } diff --git a/server/internal/service/quest_sidestory.go b/server/internal/service/quest_sidestory.go index 366a7a3..f5c711e 100644 --- a/server/internal/service/quest_sidestory.go +++ b/server/internal/service/quest_sidestory.go @@ -9,7 +9,6 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type SideStoryQuestServiceServer struct { @@ -23,19 +22,14 @@ func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.S return &SideStoryQuestServiceServer{users: users, sessions: sessions, catalog: catalog} } -func buildSideStoryDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData { - tables := userdata.ProjectTables(user, tableNames) - return userdata.BuildDiffFromTablesOrdered(tables, tableNames) -} - func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) { log.Printf("[SideStoryQuestService] MoveSideStoryQuestProgress: sideStoryQuestId=%d", req.SideStoryQuestId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() firstSceneId := s.catalog.FirstSceneByQuestId[req.SideStoryQuestId] - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { existing, exists := user.SideStoryQuests[req.SideStoryQuestId] var sceneId int32 @@ -58,21 +52,16 @@ func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Con } }) - return &pb.MoveSideStoryQuestResponse{ - DiffUserData: buildSideStoryDiff(user, []string{ - "IUserSideStoryQuest", - "IUserSideStoryQuestSceneProgressStatus", - }), - }, nil + return &pb.MoveSideStoryQuestResponse{}, nil } func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx context.Context, req *pb.UpdateSideStoryQuestSceneProgressRequest) (*pb.UpdateSideStoryQuestSceneProgressResponse, error) { log.Printf("[SideStoryQuestService] UpdateSideStoryQuestSceneProgress: sideStoryQuestId=%d sceneId=%d", req.SideStoryQuestId, req.SideStoryQuestSceneId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = req.SideStoryQuestSceneId user.SideStoryActiveProgress.LatestVersion = nowMillis @@ -84,10 +73,5 @@ func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx cont user.SideStoryQuests[req.SideStoryQuestId] = progress }) - return &pb.UpdateSideStoryQuestSceneProgressResponse{ - DiffUserData: buildSideStoryDiff(user, []string{ - "IUserSideStoryQuest", - "IUserSideStoryQuestSceneProgressStatus", - }), - }, nil + return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil } diff --git a/server/internal/service/reward.go b/server/internal/service/reward.go index 133dbb1..9adc07e 100644 --- a/server/internal/service/reward.go +++ b/server/internal/service/reward.go @@ -9,7 +9,6 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" emptypb "google.golang.org/protobuf/types/known/emptypb" ) @@ -34,7 +33,7 @@ func NewRewardServiceServer( func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveBigHuntRewardResponse, error) { log.Printf("[RewardService] ReceiveBigHuntReward") - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() weeklyVersion := gametime.WeeklyVersion(nowMillis) @@ -42,7 +41,7 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty var weeklyRewards []*pb.BigHuntReward isReceived := false - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { ws := user.BigHuntWeeklyStatuses[weeklyVersion] isReceived = ws.IsReceivedWeeklyReward @@ -106,19 +105,11 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty weeklyScoreResults = []*pb.WeeklyScoreResult{} } - tables := userdata.ProjectTables(user, []string{ - "IUserBigHuntWeeklyStatus", - "IUserBigHuntWeeklyMaxScore", - "IUserConsumableItem", - "IUserMaterial", - }) - return &pb.ReceiveBigHuntRewardResponse{ WeeklyScoreResult: weeklyScoreResults, WeeklyScoreReward: weeklyRewards, IsReceivedWeeklyScoreReward: isReceived, LastWeekWeeklyScoreReward: []*pb.BigHuntReward{}, - DiffUserData: userdata.BuildDiffFromTables(tables), }, nil } diff --git a/server/internal/service/shop.go b/server/internal/service/shop.go index ddf2dcd..2ebfed3 100644 --- a/server/internal/service/shop.go +++ b/server/internal/service/shop.go @@ -10,30 +10,10 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" "google.golang.org/protobuf/types/known/emptypb" ) -var shopDiffTables = []string{ - "IUserShopItem", - "IUserShopReplaceable", - "IUserShopReplaceableLineup", - "IUserGem", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", - "IUserPremiumItem", - "IUserStatus", - "IUserCostume", - "IUserCostumeActiveSkill", - "IUserCharacter", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserWeaponNote", -} - type ShopServiceServer struct { pb.UnimplementedShopServiceServer users store.UserRepository @@ -49,10 +29,10 @@ func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepo func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) { log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { for shopItemId, qty := range req.ShopItems { item, ok := s.catalog.Items[shopItemId] if !ok { @@ -88,23 +68,18 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu if err != nil { return nil, fmt.Errorf("shop buy: %w", err) } - - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables)) - userdata.AddWeaponStoryDiff(diff, snapshot, s.granter.DrainChangedStoryWeaponIds()) - return &pb.BuyResponse{ OverflowPossession: []*pb.Possession{}, - DiffUserData: diff, }, nil } func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) { log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { if len(user.ShopReplaceableLineup) == 0 && len(s.catalog.ItemShopPool) > 0 { for i, itemId := range s.catalog.ItemShopPool { slot := int32(i + 1) @@ -131,18 +106,13 @@ func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.Refresh return nil, fmt.Errorf("shop refresh: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables)) - - return &pb.RefreshResponse{ - DiffUserData: diff, - }, nil + return &pb.RefreshResponse{}, nil } func (s *ShopServiceServer) GetCesaLimit(_ context.Context, _ *emptypb.Empty) (*pb.GetCesaLimitResponse, error) { log.Printf("[ShopService] GetCesaLimit") return &pb.GetCesaLimitResponse{ - CesaLimit: []*pb.CesaLimit{}, - DiffUserData: userdata.EmptyDiff(), + CesaLimit: []*pb.CesaLimit{}, }, nil } @@ -150,10 +120,10 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req * log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s", req.ShopId, req.ShopItemId, req.ProductId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { item, ok := s.catalog.Items[req.ShopItemId] if !ok { log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId) @@ -193,28 +163,22 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req * txId := fmt.Sprintf("tx_%d_%d_%d", userId, req.ShopItemId, nowMillis) - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables)) - return &pb.CreatePurchaseTransactionResponse{ PurchaseTransactionId: txId, - DiffUserData: diff, }, nil } func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context, req *pb.PurchaseGooglePlayStoreProductRequest) (*pb.PurchaseGooglePlayStoreProductResponse, error) { log.Printf("[ShopService] PurchaseGooglePlayStoreProduct: txId=%s", req.PurchaseTransactionId) - userId := currentUserId(ctx, s.users, s.sessions) - snapshot, err := s.users.LoadUser(userId) + userId := CurrentUserId(ctx, s.users, s.sessions) + _, err := s.users.LoadUser(userId) if err != nil { return nil, fmt.Errorf("purchase google play: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables)) - return &pb.PurchaseGooglePlayStoreProductResponse{ OverflowPossession: []*pb.Possession{}, - DiffUserData: diff, }, nil } diff --git a/server/internal/service/state.go b/server/internal/service/state.go index 47b1c07..0bb2f01 100644 --- a/server/internal/service/state.go +++ b/server/internal/service/state.go @@ -8,43 +8,7 @@ import ( "google.golang.org/grpc/metadata" ) -var startedGameStartTables = []string{ - "IUserProfile", - "IUserCharacter", - "IUserCostume", - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserCompanion", - "IUserDeckCharacter", - "IUserDeck", - "IUserGem", - "IUserMission", - "IUserMainQuestFlowStatus", - "IUserMainQuestMainFlowStatus", - "IUserMainQuestProgressStatus", - "IUserMainQuestSeasonRoute", - "IUserQuest", - "IUserQuestMission", - "IUserTutorialProgress", - "IUserWeaponNote", - "IUserCostumeActiveSkill", - "IUserDeckTypeNote", - "IUserDeckSubWeaponGroup", - "IUserDeckPartsGroup", - "IUserConsumableItem", - "IUserMaterial", - "IUserImportantItem", -} - -var gimmickDiffTables = []string{ - "IUserGimmick", - "IUserGimmickOrnamentProgress", - "IUserGimmickSequence", - "IUserGimmickUnlock", -} - -func currentUserId(ctx context.Context, users store.UserRepository, sessions store.SessionRepository) int64 { +func CurrentUserId(ctx context.Context, users store.UserRepository, sessions store.SessionRepository) int64 { if md, ok := metadata.FromIncomingContext(ctx); ok { if vals := md.Get("x-apb-session-key"); len(vals) > 0 { if userId, err := sessions.ResolveUserId(vals[0]); err == nil { diff --git a/server/internal/service/tutorial.go b/server/internal/service/tutorial.go index 4f72592..e0a0a61 100644 --- a/server/internal/service/tutorial.go +++ b/server/internal/service/tutorial.go @@ -9,7 +9,6 @@ import ( "lunar-tear/server/internal/model" "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) type TutorialServiceServer struct { @@ -25,10 +24,10 @@ func NewTutorialServiceServer(users store.UserRepository, sessions store.Session func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb.SetTutorialProgressRequest) (*pb.SetTutorialProgressResponse, error) { log.Printf("[TutorialService] SetTutorialProgress: type=%d phase=%d choice=%d", req.TutorialType, req.ProgressPhase, req.ChoiceId) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() var grants []questflow.RewardGrant - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { existing, exists := user.Tutorials[req.TutorialType] if !exists || req.ProgressPhase >= existing.ProgressPhase { user.Tutorials[req.TutorialType] = store.TutorialProgressState{ @@ -42,22 +41,7 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb store.EnsureDefaultDeck(user, nowMillis) } }) - tables := []string{"IUserTutorialProgress"} - if req.TutorialType == int32(model.TutorialTypeMenuFirst) || - req.TutorialType == int32(model.TutorialTypeMenuSecond) { - tables = append(tables, - "IUserCharacter", "IUserCostume", "IUserWeapon", - "IUserWeaponSkill", "IUserWeaponAbility", - "IUserCompanion", "IUserDeckCharacter", "IUserDeck", - ) - } - if len(grants) > 0 { - tables = append(tables, "IUserCompanion") - } - result := userdata.ProjectTables(user, tables) - for _, t := range tables { - log.Printf("[TutorialService] DiffTable %s -> %s", t, result[t]) - } + rewards := make([]*pb.TutorialChoiceReward, len(grants)) for i, g := range grants { rewards[i] = &pb.TutorialChoiceReward{ @@ -68,14 +52,13 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb } return &pb.SetTutorialProgressResponse{ TutorialChoiceReward: rewards, - DiffUserData: userdata.BuildDiffFromTables(result), }, nil } func (s *TutorialServiceServer) SetTutorialProgressAndReplaceDeck(ctx context.Context, req *pb.SetTutorialProgressAndReplaceDeckRequest) (*pb.SetTutorialProgressAndReplaceDeckResponse, error) { log.Printf("[TutorialService] SetTutorialProgressAndReplaceDeck: type=%d phase=%d deckType=%d deckNumber=%d", req.TutorialType, req.ProgressPhase, req.DeckType, req.UserDeckNumber) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { existing, exists := user.Tutorials[req.TutorialType] if !exists || req.ProgressPhase >= existing.ProgressPhase { user.Tutorials[req.TutorialType] = store.TutorialProgressState{ @@ -87,12 +70,5 @@ func (s *TutorialServiceServer) SetTutorialProgressAndReplaceDeck(ctx context.Co store.ApplyDeckReplacement(user, model.DeckType(req.DeckType), req.UserDeckNumber, deckSlotsFromProto(req.Deck), gametime.NowMillis()) } }) - return &pb.SetTutorialProgressAndReplaceDeckResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{ - "IUserTutorialProgress", - "IUserDeck", - "IUserDeckCharacter", - "IUserDeckSubWeaponGroup", - })), - }, nil + return &pb.SetTutorialProgressAndReplaceDeckResponse{}, nil } diff --git a/server/internal/service/user.go b/server/internal/service/user.go index d322211..d41a3b5 100644 --- a/server/internal/service/user.go +++ b/server/internal/service/user.go @@ -2,73 +2,56 @@ package service import ( "context" + "encoding/json" "fmt" "log" - "sort" + "net/http" + "strconv" + "strings" "time" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" - - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" ) type UserServiceServer struct { pb.UnimplementedUserServiceServer users store.UserRepository sessions store.SessionRepository + authURL string } -func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository) *UserServiceServer { - return &UserServiceServer{users: users, sessions: sessions} -} - -func setCommonResponseTrailers(ctx context.Context, diff map[string]*pb.DiffData, includeUpdateNames bool) { - keys := make([]string, 0, len(diff)) - for key := range diff { - keys = append(keys, key) - } - sort.Strings(keys) - - var pairs []string - if includeUpdateNames && len(keys) > 0 { - pairs = append(pairs, "x-apb-update-user-data-names", keys[0]) - for _, key := range keys[1:] { - pairs[len(pairs)-1] += "," + key - } - } - - if err := grpc.SetTrailer(ctx, metadata.Pairs(pairs...)); err != nil { - log.Printf("[UserService] failed to set trailers: %v", err) +func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository, authURL string) *UserServiceServer { + if authURL != "" && !strings.Contains(authURL, "://") { + authURL = "http://" + authURL } + return &UserServiceServer{users: users, sessions: sessions, authURL: authURL} } func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) { - userId, err := s.users.CreateUser(req.Uuid) + platform := model.ClientPlatformFromContext(ctx) + userId, err := s.users.CreateUser(req.Uuid, platform) if err != nil { 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) + log.Printf("[UserService] RegisterUser: uuid=%s terminalId=%s platform=%s -> userId=%d", req.Uuid, req.TerminalId, platform, userId) return &pb.RegisterUserResponse{ - UserId: user.UserId, - Signature: fmt.Sprintf("sig_%d_%d", user.UserId, gametime.Now().Unix()), - DiffUserData: userdata.BuildDiffFromTables(userdata.FirstEntranceClientTableMap(user)), + UserId: userId, + Signature: fmt.Sprintf("sig_%d_%d", userId, gametime.Now().Unix()), }, nil } func (s *UserServiceServer) Auth(ctx context.Context, req *pb.AuthUserRequest) (*pb.AuthUserResponse, error) { - log.Printf("[UserService] Auth: uuid=%s", req.Uuid) + platform := model.ClientPlatformFromContext(ctx) + log.Printf("[UserService] Auth: uuid=%s platform=%s", req.Uuid, platform) session, err := s.sessions.CreateSession(req.Uuid, 24*time.Hour) if err != nil { @@ -84,7 +67,6 @@ func (s *UserServiceServer) Auth(ctx context.Context, req *pb.AuthUserRequest) ( ExpireDatetime: timestamppb.New(session.ExpireAt), Signature: req.Signature, UserId: user.UserId, - DiffUserData: userdata.BuildDiffFromTables(userdata.FirstEntranceClientTableMap(user)), }, nil } @@ -97,81 +79,69 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p } } - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { user.GameStartDatetime = gametime.NowMillis() }) - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user, startedGameStartTables)) - setCommonResponseTrailers(ctx, diff, true) - return &pb.GameStartResponse{ - // Apply only the starter outgame rows we need after title completion. - // Keep IUser and other risky core-account rows out of GameStart diff. - DiffUserData: diff, - }, nil + return &pb.GameStartResponse{}, nil } func (s *UserServiceServer) TransferUser(ctx context.Context, req *pb.TransferUserRequest) (*pb.TransferUserResponse, error) { - log.Printf("[UserService] TransferUser") - userId, err := s.users.CreateUser(req.Uuid) + platform := model.ClientPlatformFromContext(ctx) + log.Printf("[UserService] TransferUser: platform=%s", platform) + userId, err := s.users.CreateUser(req.Uuid, platform) if err != nil { return nil, fmt.Errorf("create user: %w", err) } return &pb.TransferUserResponse{ - UserId: userId, - Signature: "transferred-sig", - DiffUserData: userdata.EmptyDiff(), + UserId: userId, + Signature: "transferred-sig", }, nil } func (s *UserServiceServer) SetUserName(ctx context.Context, req *pb.SetUserNameRequest) (*pb.SetUserNameResponse, error) { log.Printf("[UserService] SetUserName: %s", req.Name) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { nowMillis := gametime.NowMillis() user.Profile.Name = req.Name user.Profile.NameUpdateDatetime = nowMillis }) - return &pb.SetUserNameResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})), - }, nil + return &pb.SetUserNameResponse{}, nil } func (s *UserServiceServer) SetUserMessage(ctx context.Context, req *pb.SetUserMessageRequest) (*pb.SetUserMessageResponse, error) { log.Printf("[UserService] SetUserMessage: %s", req.Message) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { nowMillis := gametime.NowMillis() user.Profile.Message = req.Message user.Profile.MessageUpdateDatetime = nowMillis }) - return &pb.SetUserMessageResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})), - }, nil + return &pb.SetUserMessageResponse{}, nil } func (s *UserServiceServer) SetUserFavoriteCostumeId(ctx context.Context, req *pb.SetUserFavoriteCostumeIdRequest) (*pb.SetUserFavoriteCostumeIdResponse, error) { log.Printf("[UserService] SetUserFavoriteCostumeId: %d", req.FavoriteCostumeId) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { nowMillis := gametime.NowMillis() user.Profile.FavoriteCostumeId = req.FavoriteCostumeId user.Profile.FavoriteCostumeIdUpdateDatetime = nowMillis }) - return &pb.SetUserFavoriteCostumeIdResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})), - }, nil + return &pb.SetUserFavoriteCostumeIdResponse{}, nil } func (s *UserServiceServer) GetUserProfile(ctx context.Context, req *pb.GetUserProfileRequest) (*pb.GetUserProfileResponse, error) { log.Printf("[UserService] GetUserProfile: playerId=%d", req.PlayerId) userId := req.PlayerId if userId == 0 { - userId = currentUserId(ctx, s.users, s.sessions) + userId = CurrentUserId(ctx, s.users, s.sessions) } user, err := s.users.LoadUser(userId) if err != nil { - return &pb.GetUserProfileResponse{DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetUserProfileResponse{}, nil } deckCharacters := []*pb.ProfileDeckCharacter{} @@ -210,66 +180,148 @@ func (s *UserServiceServer) GetUserProfile(ctx context.Context, req *pb.GetUserP HistoryItem: []*pb.PlayHistoryItem{}, HistoryCategoryGraphItem: []*pb.PlayHistoryCategoryGraphItem{}, }, - DiffUserData: userdata.EmptyDiff(), }, nil } func (s *UserServiceServer) SetBirthYearMonth(ctx context.Context, req *pb.SetBirthYearMonthRequest) (*pb.SetBirthYearMonthResponse, error) { log.Printf("[UserService] SetBirthYearMonth: %d/%d", req.BirthYear, req.BirthMonth) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) _, _ = s.users.UpdateUser(userId, func(user *store.UserState) { user.BirthYear = req.BirthYear user.BirthMonth = req.BirthMonth }) - return &pb.SetBirthYearMonthResponse{DiffUserData: userdata.EmptyDiff()}, nil + return &pb.SetBirthYearMonthResponse{}, nil } func (s *UserServiceServer) GetBirthYearMonth(ctx context.Context, _ *emptypb.Empty) (*pb.GetBirthYearMonthResponse, error) { - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { - return &pb.GetBirthYearMonthResponse{BirthYear: 2000, BirthMonth: 1, DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetBirthYearMonthResponse{BirthYear: 2000, BirthMonth: 1}, nil } - return &pb.GetBirthYearMonthResponse{BirthYear: user.BirthYear, BirthMonth: user.BirthMonth, DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetBirthYearMonthResponse{BirthYear: user.BirthYear, BirthMonth: user.BirthMonth}, nil } func (s *UserServiceServer) GetChargeMoney(ctx context.Context, _ *emptypb.Empty) (*pb.GetChargeMoneyResponse, error) { - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { - return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: 0, DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: 0}, nil } - return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: user.ChargeMoneyThisMonth, DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: user.ChargeMoneyThisMonth}, nil } func (s *UserServiceServer) SetUserSetting(ctx context.Context, req *pb.SetUserSettingRequest) (*pb.SetUserSettingResponse, error) { log.Printf("[UserService] SetUserSetting: isNotifyPurchaseAlert=%v", req.IsNotifyPurchaseAlert) - userId := currentUserId(ctx, s.users, s.sessions) - user, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + userId := CurrentUserId(ctx, s.users, s.sessions) + s.users.UpdateUser(userId, func(user *store.UserState) { user.Setting.IsNotifyPurchaseAlert = req.IsNotifyPurchaseAlert }) - return &pb.SetUserSettingResponse{ - DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserSetting"})), - }, nil + return &pb.SetUserSettingResponse{}, nil } func (s *UserServiceServer) GetAndroidArgs(ctx context.Context, req *pb.GetAndroidArgsRequest) (*pb.GetAndroidArgsResponse, error) { - return &pb.GetAndroidArgsResponse{Nonce: "Mama", ApiKey: "1234567890", DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetAndroidArgsResponse{Nonce: "Mama", ApiKey: "1234567890"}, nil } func (s *UserServiceServer) GetBackupToken(ctx context.Context, req *pb.GetBackupTokenRequest) (*pb.GetBackupTokenResponse, error) { - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) user, err := s.users.LoadUser(userId) if err != nil { - return &pb.GetBackupTokenResponse{BackupToken: "mock-backup-token", DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetBackupTokenResponse{BackupToken: "mock-backup-token"}, nil } - return &pb.GetBackupTokenResponse{BackupToken: user.BackupToken, DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetBackupTokenResponse{BackupToken: user.BackupToken}, nil } func (s *UserServiceServer) CheckTransferSetting(ctx context.Context, _ *emptypb.Empty) (*pb.CheckTransferSettingResponse, error) { - return &pb.CheckTransferSettingResponse{DiffUserData: userdata.EmptyDiff()}, nil + return &pb.CheckTransferSettingResponse{}, nil } func (s *UserServiceServer) GetUserGamePlayNote(ctx context.Context, req *pb.GetUserGamePlayNoteRequest) (*pb.GetUserGamePlayNoteResponse, error) { - return &pb.GetUserGamePlayNoteResponse{DiffUserData: userdata.EmptyDiff()}, nil + return &pb.GetUserGamePlayNoteResponse{}, nil +} + +func (s *UserServiceServer) resolveAuthToken(token string) (facebookId int64, err error) { + if s.authURL == "" { + return 0, status.Error(codes.FailedPrecondition, "auth server not configured (--auth-url)") + } + + resp, err := http.Get(s.authURL + "/me?access_token=" + token) + if err != nil { + return 0, status.Errorf(codes.Internal, "auth server unreachable: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, status.Error(codes.Unauthenticated, "invalid or expired token") + } + + var body struct { + ID string `json:"id"` + Name string `json:"name"` + } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return 0, status.Errorf(codes.Internal, "decode auth response: %v", err) + } + if body.ID == "" { + return 0, status.Error(codes.Unauthenticated, "auth server returned empty id") + } + + id, err := strconv.ParseInt(body.ID, 10, 64) + if err != nil { + return 0, status.Errorf(codes.Internal, "invalid auth id %q: %v", body.ID, err) + } + return id, nil +} + +func (s *UserServiceServer) SetFacebookAccount(ctx context.Context, req *pb.SetFacebookAccountRequest) (*pb.SetFacebookAccountResponse, error) { + log.Printf("[UserService] SetFacebookAccount") + + fbId, err := s.resolveAuthToken(req.Token) + if err != nil { + return nil, err + } + + userId := CurrentUserId(ctx, s.users, s.sessions) + if err := s.users.SetFacebookId(userId, fbId); err != nil { + return nil, fmt.Errorf("set facebook id: %w", err) + } + log.Printf("[UserService] linked facebook_id=%d to user_id=%d", fbId, userId) + return &pb.SetFacebookAccountResponse{}, nil +} + +func (s *UserServiceServer) UnsetFacebookAccount(ctx context.Context, _ *emptypb.Empty) (*pb.UnsetFacebookAccountResponse, error) { + log.Printf("[UserService] UnsetFacebookAccount") + + userId := CurrentUserId(ctx, s.users, s.sessions) + if err := s.users.ClearFacebookId(userId); err != nil { + return nil, fmt.Errorf("clear facebook id: %w", err) + } + log.Printf("[UserService] unlinked facebook from user_id=%d", userId) + return &pb.UnsetFacebookAccountResponse{}, nil +} + +func (s *UserServiceServer) TransferUserByFacebook(ctx context.Context, req *pb.TransferUserByFacebookRequest) (*pb.TransferUserByFacebookResponse, error) { + log.Printf("[UserService] TransferUserByFacebook: uuid=%s", req.Uuid) + + fbId, err := s.resolveAuthToken(req.Token) + if err != nil { + return nil, err + } + + userId, err := s.users.GetUserByFacebookId(fbId) + if err != nil { + return nil, status.Error(codes.NotFound, "no account linked to this login") + } + + if err := s.users.UpdateUUID(userId, req.Uuid); err != nil { + return nil, fmt.Errorf("update uuid: %w", err) + } + + log.Printf("[UserService] transferred facebook_id=%d -> user_id=%d with new uuid=%s", fbId, userId, req.Uuid) + + return &pb.TransferUserByFacebookResponse{ + UserId: userId, + Signature: fmt.Sprintf("fb_transfer_%d_%d", userId, gametime.Now().Unix()), + }, nil } diff --git a/server/internal/service/weapon.go b/server/internal/service/weapon.go index 14351e2..48427bc 100644 --- a/server/internal/service/weapon.go +++ b/server/internal/service/weapon.go @@ -11,35 +11,8 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" - "lunar-tear/server/internal/userdata" ) -var weaponDiffTables = []string{ - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserWeaponAwaken", - "IUserMaterial", - "IUserConsumableItem", -} - -var limitBreakDiffTables = []string{ - "IUserWeapon", - "IUserWeaponSkill", - "IUserWeaponAbility", - "IUserWeaponAwaken", - "IUserMaterial", - "IUserConsumableItem", - "IUserWeaponNote", -} - -var weaponAwakenDiffTables = []string{ - "IUserWeapon", - "IUserWeaponAwaken", - "IUserMaterial", - "IUserConsumableItem", -} - type WeaponServiceServer struct { pb.UnimplementedWeaponServiceServer users store.UserRepository @@ -55,10 +28,10 @@ func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRe func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectRequest) (*pb.ProtectResponse, error) { log.Printf("[WeaponService] Protect: uuids=%v", req.UserWeaponUuid) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { for _, uuid := range req.UserWeaponUuid { weapon, ok := user.Weapons[uuid] if !ok { @@ -71,17 +44,16 @@ func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectReques } }) - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserWeapon"})) - return &pb.ProtectResponse{DiffUserData: diff}, nil + return &pb.ProtectResponse{}, nil } func (s *WeaponServiceServer) Unprotect(ctx context.Context, req *pb.UnprotectRequest) (*pb.UnprotectResponse, error) { log.Printf("[WeaponService] Unprotect: uuids=%v", req.UserWeaponUuid) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, _ := s.users.UpdateUser(userId, func(user *store.UserState) { + s.users.UpdateUser(userId, func(user *store.UserState) { for _, uuid := range req.UserWeaponUuid { weapon, ok := user.Weapons[uuid] if !ok { @@ -94,18 +66,16 @@ func (s *WeaponServiceServer) Unprotect(ctx context.Context, req *pb.UnprotectRe } }) - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserWeapon"})) - return &pb.UnprotectResponse{DiffUserData: diff}, nil + return &pb.UnprotectResponse{}, nil } func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.EnhanceByMaterialRequest) (*pb.EnhanceByMaterialResponse, error) { log.Printf("[WeaponService] EnhanceByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - var changedStoryIds []int32 - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] EnhanceByMaterial: weapon uuid=%s not found", req.UserWeaponUuid) @@ -157,35 +127,24 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh user.Weapons[req.UserWeaponUuid] = weapon log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) - changedStoryIds = s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) + s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) }) if err != nil { return nil, fmt.Errorf("weapon enhance by material: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables)) - userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds) - return &pb.EnhanceByMaterialResponse{ IsGreatSuccess: false, SurplusEnhanceMaterial: map[int32]int32{}, - DiffUserData: diff, }, nil } func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) { log.Printf("[WeaponService] Sell: uuids=%v", req.UserWeaponUuid) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) - 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"}). - Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). - Track("IUserWeaponAwaken", oldUser, userdata.SortedWeaponAwakenRecords, []string{"userId", "userWeaponUuid"}) - - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { totalGold := int32(0) for _, uuid := range req.UserWeaponUuid { weapon, ok := user.Weapons[uuid] @@ -225,21 +184,16 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p return nil, fmt.Errorf("weapon sell: %w", err) } - sellDiffTables := []string{"IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", "IUserWeaponAwaken", "IUserConsumableItem"} - tables := userdata.ProjectTables(snapshot, sellDiffTables) - diff := tracker.Apply(snapshot, tables) - - return &pb.SellResponse{DiffUserData: diff}, nil + return &pb.SellResponse{}, nil } func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) (*pb.EvolveResponse, error) { log.Printf("[WeaponService] Evolve: uuid=%s", req.UserWeaponUuid) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - var changedStoryIds []int32 - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] Evolve: weapon uuid=%s not found", req.UserWeaponUuid) @@ -298,25 +252,22 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId) - changedStoryIds = s.checkWeaponStoryUnlocks(user, evolvedId, weapon.Level, nowMillis) + s.checkWeaponStoryUnlocks(user, evolvedId, weapon.Level, nowMillis) }) if err != nil { return nil, fmt.Errorf("weapon evolve: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables)) - userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds) - - return &pb.EvolveResponse{DiffUserData: diff}, nil + return &pb.EvolveResponse{}, nil } func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceSkillRequest) (*pb.EnhanceSkillResponse, error) { log.Printf("[WeaponService] EnhanceSkill: uuid=%s skillId=%d addLevel=%d", req.UserWeaponUuid, req.SkillId, req.AddLevelCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] EnhanceSkill: weapon uuid=%s not found", req.UserWeaponUuid) @@ -330,7 +281,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS } groupRows := s.catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId] - var skillGroup *masterdata.WeaponSkillGroupRow + var skillGroup *masterdata.EntityMWeaponSkillGroup for i := range groupRows { if groupRows[i].SkillId == req.SkillId { skillGroup = &groupRows[i] @@ -403,18 +354,16 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS return nil, fmt.Errorf("weapon enhance skill: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables)) - - return &pb.EnhanceSkillResponse{DiffUserData: diff}, nil + return &pb.EnhanceSkillResponse{}, nil } func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.EnhanceAbilityRequest) (*pb.EnhanceAbilityResponse, error) { log.Printf("[WeaponService] EnhanceAbility: uuid=%s abilityId=%d addLevel=%d", req.UserWeaponUuid, req.AbilityId, req.AddLevelCount) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] EnhanceAbility: weapon uuid=%s not found", req.UserWeaponUuid) @@ -428,7 +377,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc } groupRows := s.catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId] - var abilityGroup *masterdata.WeaponAbilityGroupRow + var abilityGroup *masterdata.EntityMWeaponAbilityGroup for i := range groupRows { if groupRows[i].AbilityId == req.AbilityId { abilityGroup = &groupRows[i] @@ -501,18 +450,16 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc return nil, fmt.Errorf("weapon enhance ability: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables)) - - return &pb.EnhanceAbilityResponse{DiffUserData: diff}, nil + return &pb.EnhanceAbilityResponse{}, nil } func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.LimitBreakByMaterialRequest) (*pb.LimitBreakByMaterialResponse, error) { log.Printf("[WeaponService] LimitBreakByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] LimitBreakByMaterial: weapon uuid=%s not found", req.UserWeaponUuid) @@ -572,25 +519,16 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb. return nil, fmt.Errorf("weapon limit break by material: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, limitBreakDiffTables)) - - return &pb.LimitBreakByMaterialResponse{DiffUserData: diff}, nil + return &pb.LimitBreakByMaterialResponse{}, nil } func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.LimitBreakByWeaponRequest) (*pb.LimitBreakByWeaponResponse, error) { log.Printf("[WeaponService] LimitBreakByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - 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"}). - Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). - Track("IUserWeaponAwaken", oldUser, userdata.SortedWeaponAwakenRecords, []string{"userId", "userWeaponUuid"}) - - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] LimitBreakByWeapon: weapon uuid=%s not found", req.UserWeaponUuid) @@ -658,27 +596,16 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li return nil, fmt.Errorf("weapon limit break by weapon: %w", err) } - tables := userdata.ProjectTables(snapshot, limitBreakDiffTables) - diff := tracker.Apply(snapshot, tables) - - return &pb.LimitBreakByWeaponResponse{DiffUserData: diff}, nil + return &pb.LimitBreakByWeaponResponse{}, nil } func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.EnhanceByWeaponRequest) (*pb.EnhanceByWeaponResponse, error) { log.Printf("[WeaponService] EnhanceByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - 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"}). - Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). - Track("IUserWeaponAwaken", oldUser, userdata.SortedWeaponAwakenRecords, []string{"userId", "userWeaponUuid"}) - - var changedStoryIds []int32 - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] EnhanceByWeapon: weapon uuid=%s not found", req.UserWeaponUuid) @@ -740,77 +667,63 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan user.Weapons[req.UserWeaponUuid] = weapon log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) - changedStoryIds = s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) + s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) }) if err != nil { return nil, fmt.Errorf("weapon enhance by weapon: %w", err) } - tables := userdata.ProjectTables(snapshot, weaponDiffTables) - diff := tracker.Apply(snapshot, tables) - userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds) - return &pb.EnhanceByWeaponResponse{ IsGreatSuccess: false, SurplusEnhanceWeapon: []string{}, - DiffUserData: diff, }, nil } -func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, weaponId, level int32, nowMillis int64) []int32 { +func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, weaponId, level int32, nowMillis int64) { wm, ok := s.catalog.Weapons[weaponId] if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 { - return nil + return } evoOrder, hasEvo := s.catalog.EvolutionOrder[weaponId] conditions := s.catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId] - changed := false for _, cond := range conditions { - granted := false - switch cond.WeaponStoryReleaseConditionType { + switch model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) { case model.WeaponStoryReleaseConditionTypeAcquisition: - granted = store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) + store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) case model.WeaponStoryReleaseConditionTypeReachSpecifiedLevel: if level >= cond.ConditionValue { - granted = store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) + store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) } case model.WeaponStoryReleaseConditionTypeReachInitialMaxLevel: if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if level >= maxFunc.Evaluate(0) { - granted = store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) + store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) } } case model.WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel: if hasEvo && evoOrder >= 1 { if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if level >= maxFunc.Evaluate(0) { - granted = store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) + store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) } } } case model.WeaponStoryReleaseConditionTypeReachSpecifiedEvolutionCount: if hasEvo && evoOrder >= cond.ConditionValue { - granted = store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) + store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) } } - if granted { - changed = true - } } - if changed { - return []int32{weaponId} - } - return nil } func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) { log.Printf("[WeaponService] Awaken: uuid=%s", req.UserWeaponUuid) - userId := currentUserId(ctx, s.users, s.sessions) + userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] if !ok { log.Printf("[WeaponService] Awaken: weapon uuid=%s not found", req.UserWeaponUuid) @@ -857,7 +770,5 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe return nil, fmt.Errorf("weapon awaken: %w", err) } - diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponAwakenDiffTables)) - - return &pb.WeaponAwakenResponse{DiffUserData: diff}, nil + return &pb.WeaponAwakenResponse{}, nil } diff --git a/server/internal/store/seed.go b/server/internal/store/seed.go index 4d996e4..4e61ca4 100644 --- a/server/internal/store/seed.go +++ b/server/internal/store/seed.go @@ -16,13 +16,13 @@ const ( defaultChargeMoneyThisMonth = int64(0) ) -func SeedUserState(userId int64, uuid string, nowMillis int64) *UserState { +func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.ClientPlatform) *UserState { user := &UserState{ UserId: userId, Uuid: uuid, PlayerId: userId, - OsType: 2, - PlatformType: 2, + OsType: platform.OsType, + PlatformType: platform.PlatformType, UserRestrictionType: 0, RegisterDatetime: nowMillis, GameStartDatetime: nowMillis, diff --git a/server/internal/store/sqlite/load.go b/server/internal/store/sqlite/load.go index 380022b..839d8bb 100644 --- a/server/internal/store/sqlite/load.go +++ b/server/internal/store/sqlite/load.go @@ -10,19 +10,21 @@ import ( func (s *SQLiteStore) LoadUser(userId int64) (store.UserState, error) { var u store.UserState + var fbId sql.NullInt64 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( + backup_token, charge_money_this_month, facebook_id 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) + &u.BackupToken, &u.ChargeMoneyThisMonth, &fbId) if err == sql.ErrNoRows { return u, store.ErrNotFound } if err != nil { return u, fmt.Errorf("load users: %w", err) } + u.FacebookId = fbId.Int64 initMaps(&u) diff --git a/server/internal/store/sqlite/user.go b/server/internal/store/sqlite/user.go index e45d4b2..5bf8217 100644 --- a/server/internal/store/sqlite/user.go +++ b/server/internal/store/sqlite/user.go @@ -4,10 +4,11 @@ import ( "database/sql" "fmt" + "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" ) -func (s *SQLiteStore) CreateUser(uuid string) (int64, error) { +func (s *SQLiteStore) CreateUser(uuid string, platform model.ClientPlatform) (int64, error) { tx, err := s.db.Begin() if err != nil { return 0, fmt.Errorf("begin tx: %w", err) @@ -24,8 +25,8 @@ func (s *SQLiteStore) CreateUser(uuid string) (int64, error) { 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) + backup_token, charge_money_this_month) VALUES (?, 0, ?, ?, 0, ?, ?, 0, 2000, 1, 'mock-backup-token', 0)`, + uuid, platform.OsType, platform.PlatformType, nowMillis, nowMillis) if err != nil { return 0, fmt.Errorf("insert user: %w", err) } @@ -39,7 +40,7 @@ func (s *SQLiteStore) CreateUser(uuid string) (int64, error) { return 0, fmt.Errorf("update player_id: %w", err) } - user := store.SeedUserState(userId, uuid, nowMillis) + user := store.SeedUserState(userId, uuid, nowMillis, platform) if err := writeUserState(tx, userId, user); err != nil { return 0, fmt.Errorf("write seed state: %w", err) } @@ -188,6 +189,52 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error { if err := tx.Commit(); err != nil { return fmt.Errorf("commit: %w", err) } + + return nil +} + +func (s *SQLiteStore) SetFacebookId(userId int64, facebookId int64) error { + _, err := s.db.Exec(`UPDATE users SET facebook_id = ? WHERE user_id = ?`, facebookId, userId) + if err != nil { + return fmt.Errorf("set facebook_id: %w", err) + } + return nil +} + +func (s *SQLiteStore) GetUserByFacebookId(facebookId int64) (int64, error) { + var userId int64 + err := s.db.QueryRow(`SELECT user_id FROM users WHERE facebook_id = ?`, facebookId).Scan(&userId) + if err == sql.ErrNoRows { + return 0, store.ErrNotFound + } + if err != nil { + return 0, fmt.Errorf("query user by facebook_id: %w", err) + } + return userId, nil +} + +func (s *SQLiteStore) GetFacebookId(userId int64) (int64, error) { + var fbId sql.NullInt64 + err := s.db.QueryRow(`SELECT facebook_id FROM users WHERE user_id = ?`, userId).Scan(&fbId) + if err != nil { + return 0, store.ErrNotFound + } + return fbId.Int64, nil +} + +func (s *SQLiteStore) ClearFacebookId(userId int64) error { + _, err := s.db.Exec(`UPDATE users SET facebook_id = NULL WHERE user_id = ?`, userId) + if err != nil { + return fmt.Errorf("clear facebook_id: %w", err) + } + return nil +} + +func (s *SQLiteStore) UpdateUUID(userId int64, newUuid string) error { + _, err := s.db.Exec(`UPDATE users SET uuid = ? WHERE user_id = ?`, newUuid, userId) + if err != nil { + return fmt.Errorf("update uuid: %w", err) + } return nil } diff --git a/server/internal/store/store.go b/server/internal/store/store.go index f75db72..32ea1ac 100644 --- a/server/internal/store/store.go +++ b/server/internal/store/store.go @@ -3,6 +3,8 @@ package store import ( "errors" "time" + + "lunar-tear/server/internal/model" ) var ErrNotFound = errors.New("store: not found") @@ -10,11 +12,16 @@ var ErrNotFound = errors.New("store: not found") type Clock func() time.Time type UserRepository interface { - CreateUser(uuid string) (int64, error) + CreateUser(uuid string, platform model.ClientPlatform) (int64, error) GetUserByUUID(uuid string) (int64, error) LoadUser(userId int64) (UserState, error) UpdateUser(userId int64, mutate func(*UserState)) (UserState, error) DefaultUserId() (int64, error) + SetFacebookId(userId int64, facebookId int64) error + GetUserByFacebookId(facebookId int64) (int64, error) + GetFacebookId(userId int64) (int64, error) + ClearFacebookId(userId int64) error + UpdateUUID(userId int64, newUuid string) error } type SessionRepository interface { diff --git a/server/internal/store/types.go b/server/internal/store/types.go index c1f3ab1..2b0f503 100644 --- a/server/internal/store/types.go +++ b/server/internal/store/types.go @@ -31,6 +31,7 @@ type UserState struct { BirthMonth int32 BackupToken string ChargeMoneyThisMonth int64 + FacebookId int64 Setting UserSettingState Status UserStatusState diff --git a/server/internal/userdata/changed_tables.go b/server/internal/userdata/changed_tables.go new file mode 100644 index 0000000..f219d78 --- /dev/null +++ b/server/internal/userdata/changed_tables.go @@ -0,0 +1,450 @@ +package userdata + +import ( + "encoding/json" + "maps" + "slices" + "sort" + "strings" + + pb "lunar-tear/server/gen/proto" + "lunar-tear/server/internal/store" +) + +func mapsEqualSimple[K comparable, V comparable](a, b map[K]V) bool { + if len(a) != len(b) { + return false + } + for k, va := range a { + if vb, ok := b[k]; !ok || va != vb { + return false + } + } + return true +} + +func mapsEqualStruct[K comparable, V comparable](a, b map[K]V) bool { + return mapsEqualSimple(a, b) +} + +func mapsEqualSliceValues[K comparable, V comparable](a, b map[K][]V) bool { + if len(a) != len(b) { + return false + } + for k, va := range a { + vb, ok := b[k] + if !ok || !slices.Equal(va, vb) { + return false + } + } + return true +} + +func gimmickStateEqual(a, b store.GimmickState) bool { + return mapsEqualStruct(a.Progress, b.Progress) && + mapsEqualStruct(a.OrnamentProgress, b.OrnamentProgress) && + mapsEqualStruct(a.Sequences, b.Sequences) && + mapsEqualStruct(a.Unlocks, b.Unlocks) +} + +func ChangedTables(before, after *store.UserState) []string { + var changed []string + add := func(name string) { changed = append(changed, name) } + + if before.UserId != after.UserId || before.PlayerId != after.PlayerId || + before.OsType != after.OsType || before.PlatformType != after.PlatformType || + before.UserRestrictionType != after.UserRestrictionType || + before.RegisterDatetime != after.RegisterDatetime || + before.GameStartDatetime != after.GameStartDatetime || + before.LatestVersion != after.LatestVersion { + add("IUser") + } + if before.Setting != after.Setting { + add("IUserSetting") + } + if before.Status != after.Status { + add("IUserStatus") + } + if before.Gem != after.Gem { + add("IUserGem") + } + if before.Profile != after.Profile { + add("IUserProfile") + } + if before.Login != after.Login { + add("IUserLogin") + } + if before.LoginBonus != after.LoginBonus { + add("IUserLoginBonus") + } + if before.PortalCageStatus != after.PortalCageStatus { + add("IUserPortalCageStatus") + } + if before.GuerrillaFreeOpen != after.GuerrillaFreeOpen { + add("IUserEventQuestGuerrillaFreeOpen") + } + if before.ShopReplaceable != after.ShopReplaceable { + add("IUserShopReplaceable") + } + if before.Explore != after.Explore { + add("IUserExplore") + } + if before.BigHuntProgress != after.BigHuntProgress { + add("IUserBigHuntProgressStatus") + } + if before.FacebookId != after.FacebookId { + add("IUserFacebook") + } + + if before.MainQuest != after.MainQuest { + add("IUserMainQuestFlowStatus") + add("IUserMainQuestMainFlowStatus") + add("IUserMainQuestProgressStatus") + add("IUserMainQuestSeasonRoute") + add("IUserMainQuestReplayFlowStatus") + } + if before.EventQuest != after.EventQuest { + add("IUserEventQuestProgressStatus") + } + if before.ExtraQuest != after.ExtraQuest { + add("IUserExtraQuestProgressStatus") + } + if before.SideStoryActiveProgress != after.SideStoryActiveProgress { + add("IUserSideStoryQuestSceneProgressStatus") + } + + if !mapsEqualStruct(before.Tutorials, after.Tutorials) { + add("IUserTutorialProgress") + } + if !mapsEqualStruct(before.Missions, after.Missions) { + add("IUserMission") + } + if !mapsEqualStruct(before.Characters, after.Characters) { + add("IUserCharacter") + } + if !mapsEqualStruct(before.Costumes, after.Costumes) { + add("IUserCostume") + } + if !mapsEqualStruct(before.Weapons, after.Weapons) { + add("IUserWeapon") + } + if !mapsEqualStruct(before.WeaponStories, after.WeaponStories) { + add("IUserWeaponStory") + } + if !mapsEqualStruct(before.WeaponNotes, after.WeaponNotes) { + add("IUserWeaponNote") + } + if !mapsEqualStruct(before.Companions, after.Companions) { + add("IUserCompanion") + } + if !mapsEqualStruct(before.Thoughts, after.Thoughts) { + add("IUserThought") + } + if !mapsEqualSimple(before.ConsumableItems, after.ConsumableItems) { + add("IUserConsumableItem") + } + if !mapsEqualSimple(before.Materials, after.Materials) { + add("IUserMaterial") + } + if !mapsEqualSimple(before.ImportantItems, after.ImportantItems) { + add("IUserImportantItem") + } + if !mapsEqualSimple(before.PremiumItems, after.PremiumItems) { + add("IUserPremiumItem") + } + if !mapsEqualStruct(before.Parts, after.Parts) { + add("IUserParts") + } + if !mapsEqualStruct(before.PartsGroupNotes, after.PartsGroupNotes) { + add("IUserPartsGroupNote") + } + if !mapsEqualStruct(before.PartsPresets, after.PartsPresets) { + add("IUserPartsPreset") + } + if !mapsEqualStruct(before.CostumeActiveSkills, after.CostumeActiveSkills) { + add("IUserCostumeActiveSkill") + } + if !mapsEqualSliceValues(before.WeaponSkills, after.WeaponSkills) { + add("IUserWeaponSkill") + } + if !mapsEqualSliceValues(before.WeaponAbilities, after.WeaponAbilities) { + add("IUserWeaponAbility") + } + if !mapsEqualStruct(before.WeaponAwakens, after.WeaponAwakens) { + add("IUserWeaponAwaken") + } + if !mapsEqualStruct(before.DeckTypeNotes, after.DeckTypeNotes) { + add("IUserDeckTypeNote") + } + if !mapsEqualStruct(before.DeckCharacters, after.DeckCharacters) { + add("IUserDeckCharacter") + add("IUserDeckCharacterDressupCostume") + } + if !mapsEqualStruct(before.Decks, after.Decks) { + add("IUserDeck") + } + if !mapsEqualSliceValues(before.DeckSubWeapons, after.DeckSubWeapons) { + add("IUserDeckSubWeaponGroup") + } + if !mapsEqualSliceValues(before.DeckParts, after.DeckParts) { + add("IUserDeckPartsGroup") + } + if !mapsEqualStruct(before.Quests, after.Quests) { + add("IUserQuest") + } + if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) { + add("IUserQuestMission") + } + if !mapsEqualStruct(before.SideStoryQuests, after.SideStoryQuests) { + add("IUserSideStoryQuest") + } + if !mapsEqualStruct(before.QuestLimitContentStatus, after.QuestLimitContentStatus) { + add("IUserQuestLimitContentStatus") + } + if !mapsEqualSimple(before.NaviCutInPlayed, after.NaviCutInPlayed) { + add("IUserNaviCutIn") + } + if !mapsEqualSimple(before.ViewedMovies, after.ViewedMovies) { + add("IUserMovie") + } + if !mapsEqualSimple(before.ContentsStories, after.ContentsStories) { + add("IUserContentsStory") + } + if !mapsEqualSimple(before.DrawnOmikuji, after.DrawnOmikuji) { + add("IUserOmikuji") + } + if !mapsEqualSimple(before.DokanConfirmed, after.DokanConfirmed) { + add("IUserDokan") + } + if !mapsEqualStruct(before.ShopItems, after.ShopItems) { + add("IUserShopItem") + } + if !mapsEqualStruct(before.ShopReplaceableLineup, after.ShopReplaceableLineup) { + add("IUserShopReplaceableLineup") + } + if !mapsEqualStruct(before.ExploreScores, after.ExploreScores) { + add("IUserExploreScore") + } + if !mapsEqualStruct(before.CharacterBoards, after.CharacterBoards) { + add("IUserCharacterBoard") + } + if !mapsEqualStruct(before.CharacterBoardAbilities, after.CharacterBoardAbilities) { + add("IUserCharacterBoardAbility") + } + if !mapsEqualStruct(before.CharacterBoardStatusUps, after.CharacterBoardStatusUps) { + add("IUserCharacterBoardStatusUp") + } + if !mapsEqualStruct(before.CostumeAwakenStatusUps, after.CostumeAwakenStatusUps) { + add("IUserCostumeAwakenStatusUp") + } + if !mapsEqualStruct(before.CostumeLotteryEffects, after.CostumeLotteryEffects) { + add("IUserCostumeLotteryEffect") + } + if !mapsEqualStruct(before.CostumeLotteryEffectPending, after.CostumeLotteryEffectPending) { + add("IUserCostumeLotteryEffectPending") + } + if !mapsEqualStruct(before.AutoSaleSettings, after.AutoSaleSettings) { + add("IUserAutoSaleSettingDetail") + } + if !mapsEqualStruct(before.CharacterRebirths, after.CharacterRebirths) { + add("IUserCharacterRebirth") + } + if !mapsEqualStruct(before.CageOrnamentRewards, after.CageOrnamentRewards) { + add("IUserCageOrnamentReward") + } + + if !mapsEqualStruct(before.BigHuntMaxScores, after.BigHuntMaxScores) { + add("IUserBigHuntMaxScore") + } + if !mapsEqualStruct(before.BigHuntStatuses, after.BigHuntStatuses) { + add("IUserBigHuntStatus") + } + if !mapsEqualStruct(before.BigHuntScheduleMaxScores, after.BigHuntScheduleMaxScores) { + add("IUserBigHuntScheduleMaxScore") + } + if !mapsEqualStruct(before.BigHuntWeeklyMaxScores, after.BigHuntWeeklyMaxScores) { + add("IUserBigHuntWeeklyMaxScore") + } + if !mapsEqualStruct(before.BigHuntWeeklyStatuses, after.BigHuntWeeklyStatuses) { + add("IUserBigHuntWeeklyStatus") + } + + if !gimmickStateEqual(before.Gimmick, after.Gimmick) { + if !mapsEqualStruct(before.Gimmick.Progress, after.Gimmick.Progress) { + add("IUserGimmick") + } + if !mapsEqualStruct(before.Gimmick.OrnamentProgress, after.Gimmick.OrnamentProgress) { + add("IUserGimmickOrnamentProgress") + } + if !mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) { + add("IUserGimmickSequence") + } + if !mapsEqualStruct(before.Gimmick.Unlocks, after.Gimmick.Unlocks) { + add("IUserGimmickUnlock") + } + } + + return changed +} + +func ComputeDelta(before, after *store.UserState, changedTables []string) map[string]*pb.DiffData { + diff := make(map[string]*pb.DiffData, len(changedTables)) + for _, table := range changedTables { + afterJSON := projectTable(table, *after) + deleteKeys := "[]" + if kf := keyFieldsForTable(table); len(kf) > 0 { + beforeJSON := projectTable(table, *before) + deleteKeys = ComputeDeleteKeys( + parseJSONRecords(beforeJSON), + parseJSONRecords(afterJSON), + kf, + ) + } + diff[table] = &pb.DiffData{ + UpdateRecordsJson: afterJSON, + DeleteKeysJson: deleteKeys, + } + } + return diff +} + +func AllTableNames() []string { + return slices.Sorted(maps.Keys(projectors)) +} + +func SortedChangedNames(tables []string) string { + sorted := make([]string, len(tables)) + copy(sorted, tables) + sort.Strings(sorted) + return strings.Join(sorted, ",") +} + +func parseJSONRecords(jsonStr string) []map[string]any { + if jsonStr == "" || jsonStr == "[]" { + return nil + } + var records []map[string]any + if err := json.Unmarshal([]byte(jsonStr), &records); err != nil { + return nil + } + return records +} + +func keyFieldsForTable(table string) []string { + switch table { + case "IUserWeapon": + return []string{"userId", "userWeaponUuid"} + case "IUserWeaponSkill": + return []string{"userId", "userWeaponUuid", "slotNumber"} + case "IUserWeaponAbility": + return []string{"userId", "userWeaponUuid", "slotNumber"} + case "IUserWeaponAwaken": + return []string{"userId", "userWeaponUuid"} + case "IUserCostume": + return []string{"userId", "userCostumeUuid"} + case "IUserCompanion": + return []string{"userId", "userCompanionUuid"} + case "IUserThought": + return []string{"userId", "userThoughtUuid"} + case "IUserParts": + return []string{"userId", "userPartsUuid"} + case "IUserDeckCharacter": + return []string{"userId", "userDeckCharacterUuid"} + case "IUserDeck": + return []string{"userId", "deckType", "userDeckNumber"} + case "IUserDeckSubWeaponGroup": + return []string{"userId", "userDeckCharacterUuid", "sortOrder"} + case "IUserDeckPartsGroup": + return []string{"userId", "userDeckCharacterUuid", "sortOrder"} + case "IUserDeckCharacterDressupCostume": + return []string{"userId", "userDeckCharacterUuid"} + case "IUserCharacter": + return []string{"userId", "characterId"} + case "IUserConsumableItem": + return []string{"userId", "consumableItemId"} + case "IUserMaterial": + return []string{"userId", "materialId"} + case "IUserImportantItem": + return []string{"userId", "importantItemId"} + case "IUserPremiumItem": + return []string{"userId", "premiumItemId"} + case "IUserQuest": + return []string{"userId", "questId"} + case "IUserQuestMission": + return []string{"userId", "questId", "questMissionId"} + case "IUserMission": + return []string{"userId", "missionId"} + case "IUserWeaponStory": + return []string{"userId", "weaponId"} + case "IUserWeaponNote": + return []string{"userId", "weaponId"} + case "IUserTutorialProgress": + return []string{"userId", "tutorialType"} + case "IUserGimmick": + return []string{"userId", "gimmickSequenceScheduleId", "gimmickSequenceId", "gimmickId"} + case "IUserGimmickOrnamentProgress": + return []string{"userId", "gimmickSequenceScheduleId", "gimmickSequenceId", "gimmickId", "gimmickOrnamentIndex"} + case "IUserGimmickSequence": + return []string{"userId", "gimmickSequenceScheduleId", "gimmickSequenceId"} + case "IUserGimmickUnlock": + return []string{"userId", "gimmickSequenceScheduleId", "gimmickSequenceId", "gimmickId"} + case "IUserCostumeActiveSkill": + return []string{"userId", "userCostumeUuid"} + case "IUserCostumeAwakenStatusUp": + return []string{"userId", "userCostumeUuid", "statusCalculationType"} + case "IUserCostumeLotteryEffect": + return []string{"userId", "userCostumeUuid", "slotNumber"} + case "IUserCostumeLotteryEffectPending": + return []string{"userId", "userCostumeUuid"} + case "IUserCharacterBoard": + return []string{"userId", "characterBoardId"} + case "IUserCharacterBoardAbility": + return []string{"userId", "characterId", "abilityId"} + case "IUserCharacterBoardStatusUp": + return []string{"userId", "characterId", "statusCalculationType"} + case "IUserExploreScore": + return []string{"userId", "exploreId"} + case "IUserPartsGroupNote": + return []string{"userId", "partsGroupId"} + case "IUserPartsPreset": + return []string{"userId", "userPartsPresetNumber"} + case "IUserCageOrnamentReward": + return []string{"userId", "cageOrnamentId"} + case "IUserAutoSaleSettingDetail": + return []string{"userId", "possessionAutoSaleItemType"} + case "IUserCharacterRebirth": + return []string{"userId", "characterId"} + case "IUserShopItem": + return []string{"userId", "shopItemId"} + case "IUserShopReplaceableLineup": + return []string{"userId", "slotNumber"} + case "IUserNaviCutIn": + return []string{"userId", "naviCutInId"} + case "IUserMovie": + return []string{"userId", "movieId"} + case "IUserContentsStory": + return []string{"userId", "contentsStoryId"} + case "IUserOmikuji": + return []string{"userId", "omikujiId"} + case "IUserDokan": + return []string{"userId", "dokanId"} + case "IUserSideStoryQuest": + return []string{"userId", "sideStoryQuestId"} + case "IUserQuestLimitContentStatus": + return []string{"userId", "questId"} + case "IUserBigHuntMaxScore": + return []string{"userId", "bigHuntBossId"} + case "IUserBigHuntStatus": + return []string{"userId", "bigHuntBossQuestId"} + case "IUserBigHuntScheduleMaxScore": + return []string{"userId", "bigHuntScheduleId", "bigHuntBossId"} + case "IUserBigHuntWeeklyMaxScore": + return []string{"userId", "bigHuntWeeklyVersion", "attributeType"} + case "IUserBigHuntWeeklyStatus": + return []string{"userId", "bigHuntWeeklyVersion"} + case "IUserDeckTypeNote": + return []string{"userId", "deckType"} + default: + return nil + } +} diff --git a/server/internal/userdata/proj_bighunt.go b/server/internal/userdata/proj_bighunt.go index 77c9c5e..235d1c7 100644 --- a/server/internal/userdata/proj_bighunt.go +++ b/server/internal/userdata/proj_bighunt.go @@ -4,11 +4,12 @@ import ( "sort" "lunar-tear/server/internal/store" + "lunar-tear/server/internal/utils" ) func init() { register("IUserBigHuntProgressStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentBigHuntBossQuestId": user.BigHuntProgress.CurrentBigHuntBossQuestId, "currentBigHuntQuestId": user.BigHuntProgress.CurrentBigHuntQuestId, @@ -39,7 +40,7 @@ func init() { "latestVersion": ms.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s }) @@ -63,7 +64,7 @@ func init() { "latestVersion": st.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s }) @@ -97,7 +98,7 @@ func init() { "latestVersion": ms.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s }) @@ -130,7 +131,7 @@ func init() { "latestVersion": ms.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s }) @@ -153,7 +154,7 @@ func init() { "latestVersion": ws.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s }) } diff --git a/server/internal/userdata/proj_characterboard.go b/server/internal/userdata/proj_characterboard.go index 34fcca6..258b98f 100644 --- a/server/internal/userdata/proj_characterboard.go +++ b/server/internal/userdata/proj_characterboard.go @@ -4,19 +4,20 @@ import ( "sort" "lunar-tear/server/internal/store" + "lunar-tear/server/internal/utils" ) func init() { register("IUserCharacterBoard", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCharacterBoardRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCharacterBoardRecords(user)...) return s }) register("IUserCharacterBoardAbility", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCharacterBoardAbilityRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCharacterBoardAbilityRecords(user)...) return s }) register("IUserCharacterBoardStatusUp", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCharacterBoardStatusUpRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCharacterBoardStatusUpRecords(user)...) return s }) registerStatic("IUserCharacterBoardCompleteReward") diff --git a/server/internal/userdata/proj_deck.go b/server/internal/userdata/proj_deck.go index 28f9f73..1b9f535 100644 --- a/server/internal/userdata/proj_deck.go +++ b/server/internal/userdata/proj_deck.go @@ -5,31 +5,32 @@ import ( "lunar-tear/server/internal/model" "lunar-tear/server/internal/store" + "lunar-tear/server/internal/utils" ) func init() { register("IUserDeck", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedDeckRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedDeckRecords(user)...) return s }) register("IUserDeckCharacter", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedDeckCharacterRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedDeckCharacterRecords(user)...) return s }) register("IUserDeckSubWeaponGroup", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedDeckSubWeaponGroupRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedDeckSubWeaponGroupRecords(user)...) return s }) register("IUserDeckTypeNote", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedDeckTypeNoteRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedDeckTypeNoteRecords(user)...) return s }) register("IUserDeckPartsGroup", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedDeckPartsGroupRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedDeckPartsGroupRecords(user)...) return s }) register("IUserDeckCharacterDressupCostume", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedDeckDressupCostumeRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedDeckDressupCostumeRecords(user)...) return s }) registerStatic( diff --git a/server/internal/userdata/proj_gimmick.go b/server/internal/userdata/proj_gimmick.go index 7cdfbd4..5e76c21 100644 --- a/server/internal/userdata/proj_gimmick.go +++ b/server/internal/userdata/proj_gimmick.go @@ -4,23 +4,24 @@ import ( "sort" "lunar-tear/server/internal/store" + "lunar-tear/server/internal/utils" ) func init() { register("IUserGimmick", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedGimmickRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedGimmickRecords(user)...) return s }) register("IUserGimmickOrnamentProgress", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedGimmickOrnamentProgressRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedGimmickOrnamentProgressRecords(user)...) return s }) register("IUserGimmickSequence", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedGimmickSequenceRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedGimmickSequenceRecords(user)...) return s }) register("IUserGimmickUnlock", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedGimmickUnlockRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedGimmickUnlockRecords(user)...) return s }) } diff --git a/server/internal/userdata/proj_inventory.go b/server/internal/userdata/proj_inventory.go index 3b27ef9..e4af5ef 100644 --- a/server/internal/userdata/proj_inventory.go +++ b/server/internal/userdata/proj_inventory.go @@ -6,111 +6,112 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/store" + "lunar-tear/server/internal/utils" ) func init() { register("IUserCharacter", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCharacterRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCharacterRecords(user)...) return s }) register("IUserCostume", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCostumeRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCostumeRecords(user)...) return s }) register("IUserWeapon", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedWeaponRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedWeaponRecords(user)...) return s }) register("IUserWeaponStory", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedWeaponStoryRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedWeaponStoryRecords(user)...) return s }) register("IUserWeaponNote", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedWeaponNoteRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedWeaponNoteRecords(user)...) return s }) register("IUserCompanion", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCompanionRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCompanionRecords(user)...) return s }) register("IUserThought", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedThoughtRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedThoughtRecords(user)...) return s }) register("IUserConsumableItem", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedConsumableItemRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedConsumableItemRecords(user)...) return s }) register("IUserMaterial", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedMaterialRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedMaterialRecords(user)...) return s }) register("IUserImportantItem", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedImportantItemRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedImportantItemRecords(user)...) return s }) register("IUserPremiumItem", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedPremiumItemRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedPremiumItemRecords(user)...) return s }) register("IUserParts", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedPartsRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedPartsRecords(user)...) return s }) register("IUserCostumeActiveSkill", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCostumeActiveSkillRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCostumeActiveSkillRecords(user)...) return s }) register("IUserWeaponSkill", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedWeaponSkillRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedWeaponSkillRecords(user)...) return s }) register("IUserWeaponAbility", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedWeaponAbilityRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedWeaponAbilityRecords(user)...) return s }) register("IUserExplore", func(user store.UserState) string { - s, _ := encodeJSONMaps(exploreRecord(user)) + s, _ := utils.EncodeJSONMaps(exploreRecord(user)) return s }) register("IUserExploreScore", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedExploreScoreRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedExploreScoreRecords(user)...) return s }) register("IUserPartsGroupNote", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedPartsGroupNoteRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedPartsGroupNoteRecords(user)...) return s }) register("IUserPartsPreset", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedPartsPresetRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedPartsPresetRecords(user)...) return s }) register("IUserCostumeAwakenStatusUp", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCostumeAwakenStatusUpRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCostumeAwakenStatusUpRecords(user)...) return s }) register("IUserAutoSaleSettingDetail", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedAutoSaleSettingRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedAutoSaleSettingRecords(user)...) return s }) register("IUserCharacterRebirth", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCharacterRebirthRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCharacterRebirthRecords(user)...) return s }) register("IUserCageOrnamentReward", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCageOrnamentRewardRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCageOrnamentRewardRecords(user)...) return s }) register("IUserWeaponAwaken", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedWeaponAwakenRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedWeaponAwakenRecords(user)...) return s }) register("IUserCostumeLotteryEffect", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedCostumeLotteryEffectRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedCostumeLotteryEffectRecords(user)...) return s }) register("IUserCostumeLotteryEffectPending", func(user store.UserState) string { - s, _ := encodeJSONMaps(SortedCostumeLotteryEffectPendingRecords(user)...) + s, _ := utils.EncodeJSONMaps(SortedCostumeLotteryEffectPendingRecords(user)...) return s }) registerStatic( @@ -285,7 +286,7 @@ func WeaponStoryRecordsForIds(user store.UserState, weaponIds []int32) string { "latestVersion": row.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s } diff --git a/server/internal/userdata/proj_quest.go b/server/internal/userdata/proj_quest.go index 4060757..981d43f 100644 --- a/server/internal/userdata/proj_quest.go +++ b/server/internal/userdata/proj_quest.go @@ -4,6 +4,7 @@ import ( "sort" "lunar-tear/server/internal/store" + "lunar-tear/server/internal/utils" ) func sortedQuestRecords(user store.UserState) []map[string]any { @@ -60,15 +61,15 @@ func sortedQuestMissionRecords(user store.UserState) []map[string]any { func init() { register("IUserQuest", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedQuestRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedQuestRecords(user)...) return s }) register("IUserQuestMission", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedQuestMissionRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedQuestMissionRecords(user)...) return s }) register("IUserMainQuestFlowStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentQuestFlowType": user.MainQuest.CurrentQuestFlowType, "latestVersion": user.MainQuest.LatestVersion, @@ -76,7 +77,7 @@ func init() { return s }) register("IUserMainQuestMainFlowStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentMainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId, "currentQuestSceneId": user.MainQuest.CurrentQuestSceneId, @@ -87,7 +88,7 @@ func init() { return s }) register("IUserMainQuestProgressStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentQuestSceneId": user.MainQuest.ProgressQuestSceneId, "headQuestSceneId": user.MainQuest.ProgressHeadQuestSceneId, @@ -97,7 +98,7 @@ func init() { return s }) register("IUserMainQuestSeasonRoute", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "mainQuestSeasonId": user.MainQuest.MainQuestSeasonId, "mainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId, @@ -106,7 +107,7 @@ func init() { return s }) register("IUserEventQuestProgressStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentEventQuestChapterId": user.EventQuest.CurrentEventQuestChapterId, "currentQuestId": user.EventQuest.CurrentQuestId, @@ -117,7 +118,7 @@ func init() { return s }) register("IUserExtraQuestProgressStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentQuestId": user.ExtraQuest.CurrentQuestId, "currentQuestSceneId": user.ExtraQuest.CurrentQuestSceneId, @@ -127,7 +128,7 @@ func init() { return s }) register("IUserMainQuestReplayFlowStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentHeadQuestSceneId": user.MainQuest.ReplayFlowHeadQuestSceneId, "currentQuestSceneId": user.MainQuest.ReplayFlowCurrentQuestSceneId, @@ -136,7 +137,7 @@ func init() { return s }) register("IUserSideStoryQuestSceneProgressStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "currentSideStoryQuestId": user.SideStoryActiveProgress.CurrentSideStoryQuestId, "currentSideStoryQuestSceneId": user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId, @@ -164,7 +165,7 @@ func init() { "latestVersion": progress.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s }) register("IUserQuestLimitContentStatus", func(user store.UserState) string { @@ -187,7 +188,7 @@ func init() { "latestVersion": st.LatestVersion, }) } - s, _ := encodeJSONMaps(records...) + s, _ := utils.EncodeJSONMaps(records...) return s }) registerStatic( diff --git a/server/internal/userdata/proj_user.go b/server/internal/userdata/proj_user.go index 54095f1..b841220 100644 --- a/server/internal/userdata/proj_user.go +++ b/server/internal/userdata/proj_user.go @@ -5,11 +5,12 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/store" + "lunar-tear/server/internal/utils" ) func init() { register("IUser", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "playerId": user.PlayerId, "osType": user.OsType, @@ -22,15 +23,15 @@ func init() { return s }) register("IUserSetting", func(user store.UserState) string { - s, _ := encodeJSONRecords(&EntityIUserSetting{ - UserId: user.UserId, - IsNotifyPurchaseAlert: user.Setting.IsNotifyPurchaseAlert, - LatestVersion: user.Setting.LatestVersion, + s, _ := utils.EncodeJSONMaps(map[string]any{ + "userId": user.UserId, + "isNotifyPurchaseAlert": user.Setting.IsNotifyPurchaseAlert, + "latestVersion": user.Setting.LatestVersion, }) return s }) register("IUserStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "level": user.Status.Level, "exp": user.Status.Exp, @@ -41,16 +42,16 @@ func init() { return s }) register("IUserGem", func(user store.UserState) string { - s, _ := encodeJSONRecords(&EntityIUserGem{ - UserId: user.UserId, - PaidGem: user.Gem.PaidGem, - FreeGem: user.Gem.FreeGem, - LatestVersion: gametime.NowMillis(), + s, _ := utils.EncodeJSONMaps(map[string]any{ + "userId": user.UserId, + "paidGem": user.Gem.PaidGem, + "freeGem": user.Gem.FreeGem, + "latestVersion": gametime.NowMillis(), }) return s }) register("IUserProfile", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "name": user.Profile.Name, "nameUpdateDatetime": user.Profile.NameUpdateDatetime, @@ -63,58 +64,58 @@ func init() { return s }) register("IUserLogin", func(user store.UserState) string { - s, _ := encodeJSONRecords(&EntityIUserLogin{ - UserId: user.UserId, - TotalLoginCount: user.Login.TotalLoginCount, - ContinualLoginCount: user.Login.ContinualLoginCount, - MaxContinualLoginCount: user.Login.MaxContinualLoginCount, - LastLoginDatetime: user.Login.LastLoginDatetime, - LastComebackLoginDatetime: user.Login.LastComebackLoginDatetime, - LatestVersion: user.Login.LatestVersion, + s, _ := utils.EncodeJSONMaps(map[string]any{ + "userId": user.UserId, + "totalLoginCount": user.Login.TotalLoginCount, + "continualLoginCount": user.Login.ContinualLoginCount, + "maxContinualLoginCount": user.Login.MaxContinualLoginCount, + "lastLoginDatetime": user.Login.LastLoginDatetime, + "lastComebackLoginDatetime": user.Login.LastComebackLoginDatetime, + "latestVersion": user.Login.LatestVersion, }) return s }) register("IUserLoginBonus", func(user store.UserState) string { - s, _ := encodeJSONRecords(&EntityIUserLoginBonus{ - UserId: user.UserId, - LoginBonusId: user.LoginBonus.LoginBonusId, - CurrentPageNumber: user.LoginBonus.CurrentPageNumber, - CurrentStampNumber: user.LoginBonus.CurrentStampNumber, - LatestRewardReceiveDatetime: user.LoginBonus.LatestRewardReceiveDatetime, - LatestVersion: user.LoginBonus.LatestVersion, + s, _ := utils.EncodeJSONMaps(map[string]any{ + "userId": user.UserId, + "loginBonusId": user.LoginBonus.LoginBonusId, + "currentPageNumber": user.LoginBonus.CurrentPageNumber, + "currentStampNumber": user.LoginBonus.CurrentStampNumber, + "latestRewardReceiveDatetime": user.LoginBonus.LatestRewardReceiveDatetime, + "latestVersion": user.LoginBonus.LatestVersion, }) return s }) register("IUserTutorialProgress", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedTutorialRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedTutorialRecords(user)...) return s }) register("IUserMission", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedMissionRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedMissionRecords(user)...) return s }) register("IUserNaviCutIn", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedNaviCutInRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedNaviCutInRecords(user)...) return s }) register("IUserMovie", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedMovieRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedMovieRecords(user)...) return s }) register("IUserContentsStory", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedContentsStoryRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedContentsStoryRecords(user)...) return s }) register("IUserOmikuji", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedOmikujiRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedOmikujiRecords(user)...) return s }) register("IUserDokan", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedDokanRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedDokanRecords(user)...) return s }) register("IUserPortalCageStatus", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "isCurrentProgress": user.PortalCageStatus.IsCurrentProgress, "dropItemStartDatetime": user.PortalCageStatus.DropItemStartDatetime, @@ -124,7 +125,7 @@ func init() { return s }) register("IUserEventQuestGuerrillaFreeOpen", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "startDatetime": user.GuerrillaFreeOpen.StartDatetime, "openMinutes": user.GuerrillaFreeOpen.OpenMinutes, @@ -135,11 +136,11 @@ func init() { }) register("IUserShopItem", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedShopItemRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedShopItemRecords(user)...) return s }) register("IUserShopReplaceable", func(user store.UserState) string { - s, _ := encodeJSONMaps(map[string]any{ + s, _ := utils.EncodeJSONMaps(map[string]any{ "userId": user.UserId, "lineupUpdateCount": user.ShopReplaceable.LineupUpdateCount, "latestLineupUpdateDatetime": user.ShopReplaceable.LatestLineupUpdateDatetime, @@ -148,11 +149,26 @@ func init() { return s }) register("IUserShopReplaceableLineup", func(user store.UserState) string { - s, _ := encodeJSONMaps(sortedShopReplaceableLineupRecords(user)...) + s, _ := utils.EncodeJSONMaps(sortedShopReplaceableLineupRecords(user)...) return s }) - registerStatic() + register("IUserFacebook", func(user store.UserState) string { + return ProjectFacebook(user.UserId, user.FacebookId) + }) + registerStatic("IUserApple") +} + +func ProjectFacebook(userId int64, facebookId int64) string { + if facebookId == 0 { + return "[]" + } + s, _ := utils.EncodeJSONMaps(map[string]any{ + "userId": userId, + "facebookId": facebookId, + "latestVersion": gametime.NowMillis(), + }) + return s } func sortedTutorialRecords(user store.UserState) []map[string]any { diff --git a/server/internal/userdata/state_projection.go b/server/internal/userdata/state_projection.go index a852001..980b3b7 100644 --- a/server/internal/userdata/state_projection.go +++ b/server/internal/userdata/state_projection.go @@ -99,6 +99,8 @@ func FullClientTableMap(user store.UserState) map[string]string { "IUserBigHuntScheduleMaxScore": projectTable("IUserBigHuntScheduleMaxScore", user), "IUserBigHuntWeeklyMaxScore": projectTable("IUserBigHuntWeeklyMaxScore", user), "IUserBigHuntWeeklyStatus": projectTable("IUserBigHuntWeeklyStatus", user), + "IUserFacebook": projectTable("IUserFacebook", user), + "IUserApple": projectTable("IUserApple", user), } } diff --git a/server/internal/userdata/userdata.go b/server/internal/userdata/userdata.go deleted file mode 100644 index f71b179..0000000 --- a/server/internal/userdata/userdata.go +++ /dev/null @@ -1,302 +0,0 @@ -package userdata - -import ( - "encoding/base64" - "encoding/json" - "fmt" - - "lunar-tear/server/internal/gametime" - - "github.com/vmihailenco/msgpack/v5" -) - -// EntityIUser mirrors the game's EntityIUser [MessagePackObject] with [Key(0..7)]. -// Serialized as a MessagePack array of 8 elements. -type EntityIUser struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - PlayerId int64 // Key(1) - OsType int32 // Key(2) — 2 = Android - PlatformType int32 // Key(3) — 2 = GooglePlay - UserRestrictionType int32 // Key(4) — 0 = None - RegisterDatetime int64 // Key(5) — unix millis - GameStartDatetime int64 // Key(6) — unix millis - LatestVersion int64 // Key(7) -} - -// EntityIUserSetting mirrors EntityIUserSetting [Key(0..2)]. -type EntityIUserSetting struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 `json:"userId"` // Key(0) - IsNotifyPurchaseAlert bool `json:"isNotifyPurchaseAlert"` // Key(1) - LatestVersion int64 `json:"latestVersion"` // Key(2) -} - -// EntityIUserTutorialProgress mirrors EntityIUserTutorialProgress [Key(0..4)]. -type EntityIUserTutorialProgress struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - TutorialType int32 // Key(1) - ProgressPhase int32 // Key(2) - ChoiceId int32 // Key(3) - LatestVersion int64 // Key(4) -} - -// EntityIUserQuest mirrors EntityIUserQuest [Key(0..9)]. -type EntityIUserQuest struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - QuestId int32 // Key(1) - QuestStateType int32 // Key(2) — 2 = Cleared - IsBattleOnly bool // Key(3) - LatestStartDatetime int64 // Key(4) — unix millis - ClearCount int32 // Key(5) - DailyClearCount int32 // Key(6) - LastClearDatetime int64 // Key(7) — unix millis - ShortestClearFrames int32 // Key(8) - LatestVersion int64 // Key(9) -} - -// EntityIUserMainQuestFlowStatus mirrors EntityIUserMainQuestFlowStatus [Key(0..2)]. -type EntityIUserMainQuestFlowStatus struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - CurrentQuestFlowType int32 // Key(1) // QuestFlowType: 0=UNKNOWN, 1=MAIN_FLOW, 2=SUB_FLOW, 3=REPLAY_FLOW, 4=ANOTHER_ROUTE_REPLAY_FLOW - LatestVersion int64 // Key(2) -} - -// EntityIUserMainQuestMainFlowStatus mirrors EntityIUserMainQuestMainFlowStatus [Key(0..5)]. -type EntityIUserMainQuestMainFlowStatus struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - CurrentMainQuestRouteId int32 // Key(1) - CurrentQuestSceneId int32 // Key(2) - HeadQuestSceneId int32 // Key(3) - IsReachedLastQuestScene bool // Key(4) - LatestVersion int64 // Key(5) -} - -// EntityIUserMainQuestProgressStatus mirrors EntityIUserMainQuestProgressStatus [Key(0..4)]. -// This table is used by ActivePlayerToEntityPlayingMainQuestStatus (0x2AB4A48). -type EntityIUserMainQuestProgressStatus struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - CurrentQuestSceneId int32 // Key(1) - HeadQuestSceneId int32 // Key(2) - CurrentQuestFlowType int32 // Key(3) // QuestFlowType: 0=UNKNOWN, 1=MAIN_FLOW, 2=SUB_FLOW, 3=REPLAY_FLOW, 4=ANOTHER_ROUTE_REPLAY_FLOW - LatestVersion int64 // Key(4) -} - -// EntityIUserMainQuestSeasonRoute mirrors EntityIUserMainQuestSeasonRoute [Key(0..3)]. -type EntityIUserMainQuestSeasonRoute struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - MainQuestSeasonId int32 // Key(1) - MainQuestRouteId int32 // Key(2) - LatestVersion int64 // Key(3) -} - -// EntityIUserStatus mirrors EntityIUserStatus [Key(0..5)]. -type EntityIUserStatus struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - Level int32 // Key(1) - Exp int32 // Key(2) - StaminaMilliValue int32 // Key(3) - StaminaUpdateDatetime int64 // Key(4) - LatestVersion int64 // Key(5) -} - -// EntityIUserGem mirrors EntityIUserGem [Key(0..3)]. -type EntityIUserGem struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 `json:"userId"` // Key(0) - PaidGem int32 `json:"paidGem"` // Key(1) - FreeGem int32 `json:"freeGem"` // Key(2) - LatestVersion int64 `json:"latestVersion"` // Key(3) -} - -// EntityIUserProfile mirrors EntityIUserProfile [Key(0..7)]. -type EntityIUserProfile struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - Name string // Key(1) - NameUpdateDatetime int64 // Key(2) - Message string // Key(3) - MessageUpdateDatetime int64 // Key(4) - FavoriteCostumeId int32 // Key(5) - FavoriteCostumeIdUpdateDatetime int64 // Key(6) - LatestVersion int64 // Key(7) -} - -// EntityIUserCharacter mirrors EntityIUserCharacter [Key(0..4)]. -type EntityIUserCharacter struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - CharacterId int32 // Key(1) - Level int32 // Key(2) - Exp int32 // Key(3) - LatestVersion int64 // Key(4) -} - -// EntityIUserCostume mirrors EntityIUserCostume [Key(0..9)]. -type EntityIUserCostume struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - UserCostumeUuid string // Key(1) - CostumeId int32 // Key(2) - LimitBreakCount int32 // Key(3) - Level int32 // Key(4) - Exp int32 // Key(5) - HeadupDisplayViewId int32 // Key(6) - AcquisitionDatetime int64 // Key(7) - AwakenCount int32 // Key(8) - LatestVersion int64 // Key(9) -} - -// EntityIUserWeapon mirrors EntityIUserWeapon [Key(0..8)]. -type EntityIUserWeapon struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - UserWeaponUuid string // Key(1) - WeaponId int32 // Key(2) - Level int32 // Key(3) - Exp int32 // Key(4) - LimitBreakCount int32 // Key(5) - IsProtected bool // Key(6) - AcquisitionDatetime int64 // Key(7) - LatestVersion int64 // Key(8) -} - -// EntityIUserCompanion mirrors EntityIUserCompanion [Key(0..6)]. -type EntityIUserCompanion struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - UserCompanionUuid string // Key(1) - CompanionId int32 // Key(2) - HeadupDisplayViewId int32 // Key(3) - Level int32 // Key(4) - AcquisitionDatetime int64 // Key(5) - LatestVersion int64 // Key(6) -} - -// EntityIUserDeckCharacter mirrors EntityIUserDeckCharacter [Key(0..7)]. -type EntityIUserDeckCharacter struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - UserDeckCharacterUuid string // Key(1) - UserCostumeUuid string // Key(2) - MainUserWeaponUuid string // Key(3) - UserCompanionUuid string // Key(4) - Power int32 // Key(5) - UserThoughtUuid string // Key(6) - LatestVersion int64 // Key(7) -} - -// EntityIUserDeck mirrors EntityIUserDeck [Key(0..8)]. -type EntityIUserDeck struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - DeckType int32 // Key(1) - UserDeckNumber int32 // Key(2) - UserDeckCharacterUuid01 string // Key(3) - UserDeckCharacterUuid02 string // Key(4) - UserDeckCharacterUuid03 string // Key(5) - Name string // Key(6) - Power int32 // Key(7) - LatestVersion int64 // Key(8) -} - -// EntityIUserLogin mirrors EntityIUserLogin [Key(0..6)]. -type EntityIUserLogin struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 `json:"userId"` // Key(0) - TotalLoginCount int32 `json:"totalLoginCount"` // Key(1) - ContinualLoginCount int32 `json:"continualLoginCount"` // Key(2) - MaxContinualLoginCount int32 `json:"maxContinualLoginCount"` // Key(3) - LastLoginDatetime int64 `json:"lastLoginDatetime"` // Key(4) - LastComebackLoginDatetime int64 `json:"lastComebackLoginDatetime"` // Key(5) - LatestVersion int64 `json:"latestVersion"` // Key(6) -} - -// EntityIUserLoginBonus mirrors EntityIUserLoginBonus [Key(0..5)]. -type EntityIUserLoginBonus struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 `json:"userId"` // Key(0) - LoginBonusId int32 `json:"loginBonusId"` // Key(1) - CurrentPageNumber int32 `json:"currentPageNumber"` // Key(2) - CurrentStampNumber int32 `json:"currentStampNumber"` // Key(3) - LatestRewardReceiveDatetime int64 `json:"latestRewardReceiveDatetime"` // Key(4) - LatestVersion int64 `json:"latestVersion"` // Key(5) -} - -// EntityIUserMission mirrors EntityIUserMission [Key(0..6)]. -type EntityIUserMission struct { - _msgpack struct{} `msgpack:",asArray"` - UserId int64 // Key(0) - MissionId int32 // Key(1) - StartDatetime int64 // Key(2) - ProgressValue int32 // Key(3) - MissionProgressStatusType int32 // Key(4) - ClearDatetime int64 // Key(5) - LatestVersion int64 // Key(6) -} - -// EncodeRecords serializes a slice of entities to the client-expected format: -// a JSON array of base64-encoded MessagePack byte strings. -func EncodeRecords(entities ...any) (string, error) { - b64List := make([]string, 0, len(entities)) - for _, e := range entities { - data, err := msgpack.Marshal(e) - if err != nil { - return "", fmt.Errorf("msgpack marshal: %w", err) - } - b64List = append(b64List, base64.StdEncoding.EncodeToString(data)) - } - jsonBytes, err := json.Marshal(b64List) - if err != nil { - return "", fmt.Errorf("json marshal: %w", err) - } - return string(jsonBytes), nil -} - -func encodeJSONRecords(entities ...any) (string, error) { - jsonBytes, err := json.Marshal(entities) - if err != nil { - return "", fmt.Errorf("json marshal records: %w", err) - } - return string(jsonBytes), nil -} - -func encodeJSONMaps(records ...map[string]any) (string, error) { - jsonBytes, err := json.Marshal(records) - if err != nil { - return "", fmt.Errorf("json marshal maps: %w", err) - } - return string(jsonBytes), nil -} - -// DefaultUserData returns pre-built user data tables for a fresh user. -// We provide BOTH msgpack-encoded (base64) and plain JSON variants. -// The server tries msgpack first; if the client doesn't accept it, switch to JSON. -func DefaultUserData(userId int64) map[string]string { - now := gametime.Now().Unix() - - userRecord, _ := EncodeRecords(&EntityIUser{ - UserId: userId, - PlayerId: userId, - OsType: 2, - PlatformType: 2, - RegisterDatetime: now, - }) - - settingRecord, _ := EncodeRecords(&EntityIUserSetting{ - UserId: userId, - }) - - data := map[string]string{ - "user": userRecord, - "user_setting": settingRecord, - } - return data -} diff --git a/server/internal/utils/utils.go b/server/internal/utils/utils.go index 889f04d..74b4db2 100644 --- a/server/internal/utils/utils.go +++ b/server/internal/utils/utils.go @@ -3,19 +3,20 @@ package utils import ( "encoding/json" "fmt" - "os" - "path/filepath" + "lunar-tear/server/internal/masterdata/memorydb" ) -func ReadJSON[T any](filename string) ([]T, error) { - path := filepath.Join("assets", "master_data", filename) - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read %s: %w", path, err) - } - var out []T - if err := json.Unmarshal(data, &out); err != nil { - return nil, fmt.Errorf("unmarshal %s: %w", path, err) - } - return out, nil +// ReadTable deserializes a master data table from the in-memory binary store. +// The key is the snake_case table name as it appears in the binary header +// (e.g. "m_weapon", "m_costume"). +func ReadTable[T any](key string) ([]T, error) { + return memorydb.ReadTable[T](key) +} + +func EncodeJSONMaps(records ...map[string]any) (string, error) { + jsonBytes, err := json.Marshal(records) + if err != nil { + return "", fmt.Errorf("json marshal maps: %w", err) + } + return string(jsonBytes), nil } diff --git a/server/migrations/20260417165746_add_facebook_id.sql b/server/migrations/20260417165746_add_facebook_id.sql new file mode 100644 index 0000000..8d91811 --- /dev/null +++ b/server/migrations/20260417165746_add_facebook_id.sql @@ -0,0 +1,8 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN facebook_id INTEGER; +CREATE UNIQUE INDEX idx_users_facebook_id ON users(facebook_id) WHERE facebook_id IS NOT NULL; + +-- +goose Down +DROP INDEX IF EXISTS idx_users_facebook_id; +ALTER TABLE users DROP COLUMN facebook_id; +