mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Compare commits
36 Commits
8abd5d007e
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d0c0d8ef0 | |||
| 810adcf990 | |||
| ef69c54949 | |||
| b65c1c5fce | |||
| ab5a999ffe | |||
| 8520b67a8b | |||
| 42ff8ec88f | |||
| 2cf0c153e1 | |||
| 956dbfaefd | |||
| 0d46ee4557 | |||
| fa2a124d47 | |||
| 25cbe8635f | |||
| 1dc5b8fd7c | |||
| c9a1929279 | |||
| fb111cf1ec | |||
| 26c10ac429 | |||
| 15beefb5b8 | |||
| dd00cadc18 | |||
| ae884b4060 | |||
| fa5d023f58 | |||
| 00817684ef | |||
| 44a03d222b | |||
| 23f0d26fcd | |||
| cc9dc4f1c5 | |||
| 479ace5c8e | |||
| 6c9e3c45f0 | |||
| 9a2cc92a6f | |||
| 60e0402525 | |||
| 5645740099 | |||
| b414db7339 | |||
| 20d8e4d3df | |||
| 3fe564cb1d | |||
| 9be0df4c30 | |||
| 9001b52b90 | |||
| f96bd7a88b | |||
| fc3836d502 |
@@ -0,0 +1,6 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.go text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: Build and Publish Release Binaries
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.goos }}/${{ matrix.goarch }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { goos: linux, goarch: amd64, archive: tar.gz }
|
||||||
|
- { goos: linux, goarch: arm64, archive: tar.gz }
|
||||||
|
- { goos: darwin, goarch: amd64, archive: tar.gz }
|
||||||
|
- { goos: darwin, goarch: arm64, archive: tar.gz }
|
||||||
|
- { goos: windows, goarch: amd64, archive: zip }
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
cache-dependency-path: server/go.sum
|
||||||
|
|
||||||
|
- name: Install protoc
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
|
||||||
|
|
||||||
|
- name: Install protoc-gen-go plugins
|
||||||
|
run: |
|
||||||
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Generate proto stubs
|
||||||
|
working-directory: server
|
||||||
|
run: make proto
|
||||||
|
|
||||||
|
- name: Cross-compile binaries
|
||||||
|
working-directory: server
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
stage="staging"
|
||||||
|
mkdir -p "$stage/bin"
|
||||||
|
|
||||||
|
ext=""
|
||||||
|
if [ "$GOOS" = "windows" ]; then ext=".exe"; fi
|
||||||
|
|
||||||
|
build() {
|
||||||
|
local name="$1" dest="$2"
|
||||||
|
go build -trimpath -ldflags="-s -w" -o "$dest/${name}${ext}" "./cmd/${name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wizard sits at the root so end-users see it immediately.
|
||||||
|
build wizard "$stage"
|
||||||
|
|
||||||
|
# Sub-services and admin tools go in bin/ to match the runtime layout.
|
||||||
|
for name in dev lunar-tear octo-cdn auth-server import-snapshot claim-account register-account wizard-restore; do
|
||||||
|
build "$name" "$stage/bin"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Stage docs
|
||||||
|
working-directory: server
|
||||||
|
run: |
|
||||||
|
cp ../README.md staging/README.md
|
||||||
|
cp ../LICENSE staging/LICENSE
|
||||||
|
|
||||||
|
- name: Archive
|
||||||
|
working-directory: server
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
name="lunar-tear-server-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
mv staging "$name"
|
||||||
|
if [ "${{ matrix.archive }}" = "zip" ]; then
|
||||||
|
zip -r "${name}.zip" "$name"
|
||||||
|
else
|
||||||
|
tar -czf "${name}.tar.gz" "$name"
|
||||||
|
fi
|
||||||
|
ls -lh "${name}".*
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lunar-tear-server-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: server/lunar-tear-server-*.${{ matrix.archive }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Attach archives to GitHub Release
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: List downloaded artifacts
|
||||||
|
run: ls -lh artifacts/
|
||||||
|
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: artifacts/*
|
||||||
|
generate_release_notes: true
|
||||||
|
draft: true
|
||||||
@@ -11,6 +11,7 @@ server/claim-account
|
|||||||
server/octo-cdn
|
server/octo-cdn
|
||||||
server/dev
|
server/dev
|
||||||
server/wizard
|
server/wizard
|
||||||
|
server/wizard-restore
|
||||||
server/.wizard.json
|
server/.wizard.json
|
||||||
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ Discord server: https://discord.gg/MZAf5aVkJG
|
|||||||
|
|
||||||
## How To Launch The Server
|
## How To Launch The Server
|
||||||
|
|
||||||
### Prerequisites
|
### Download & Run (no setup)
|
||||||
|
|
||||||
|
Prebuilt binaries are published for Linux, macOS, and Windows on the [Releases page](https://github.com/Walter-Sparrow/lunar-tear/releases).
|
||||||
|
|
||||||
|
1. Download the archive for your OS/arch (`lunar-tear-server-<version>-<os>-<arch>.{tar.gz,zip}`).
|
||||||
|
2. Run `./wizard` (macOS/Linux) or double-click `wizard.exe` (Windows).
|
||||||
|
|
||||||
|
### Prerequisites (build from source)
|
||||||
|
|
||||||
- Go 1.25+
|
- Go 1.25+
|
||||||
- [goose](https://github.com/pressly/goose) migration tool
|
- [goose](https://github.com/pressly/goose) migration tool
|
||||||
@@ -40,12 +47,13 @@ By default the wizard uses ports 8003 (gRPC), 8080 (CDN), and 3000 (auth). Overr
|
|||||||
go run ./cmd/wizard --grpc-port 9003 --cdn-port 9080
|
go run ./cmd/wizard --grpc-port 9003 --cdn-port 9080
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ---------------- | ------- | ---------------------------------- |
|
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `--prefer-saved` | `false` | Reuse saved config without prompting |
|
| `--prefer-saved` | `false` | Reuse saved config without prompting |
|
||||||
| `--grpc-port` | `8003` | gRPC server port |
|
| `--grpc-port` | `8003` | gRPC server port |
|
||||||
| `--cdn-port` | `8080` | CDN server port |
|
| `--cdn-port` | `8080` | CDN server port |
|
||||||
| `--auth-port` | `3000` | Auth server port |
|
| `--auth-port` | `3000` | Auth server port |
|
||||||
|
| `--admin-port` | `0` | Admin webhook port (`0` = disabled). Bound on `127.0.0.1`; only takes effect when `LUNAR_ADMIN_TOKEN` is set in the env. |
|
||||||
|
|
||||||
Custom ports are saved to `.wizard.json` alongside your other settings. On the next run the saved ports are reused automatically — no need to pass the flags again. If you later pass different port flags, the wizard warns you that the ports changed and asks for confirmation before continuing.
|
Custom ports are saved to `.wizard.json` alongside your other settings. On the next run the saved ports are reused automatically — no need to pass the flags again. If you later pass different port flags, the wizard warns you that the ports changed and asks for confirmation before continuing.
|
||||||
|
|
||||||
@@ -73,6 +81,17 @@ mkdir -p db
|
|||||||
goose -dir migrations -allow-missing sqlite3 db/game.db up
|
goose -dir migrations -allow-missing sqlite3 db/game.db up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Backups & Restore
|
||||||
|
|
||||||
|
The wizard backs up your save every time you launch it. To roll back to an earlier save:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
make restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Pick a backup from the list and confirm.
|
||||||
|
|
||||||
### Importing a Snapshot
|
### Importing a Snapshot
|
||||||
|
|
||||||
To import a JSON snapshot into the database, use the import tool. The `--uuid` flag must match the UUID your game client sends during authentication:
|
To import a JSON snapshot into the database, use the import tool. The `--uuid` flag must match the UUID your game client sends during authentication:
|
||||||
@@ -93,9 +112,9 @@ go run ./cmd/import-snapshot \
|
|||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ------------ | ------------ | --------------------------------------------- |
|
| ------------ | ------------ | --------------------------------------------- |
|
||||||
| `--snapshot` | *(required)* | Path to JSON snapshot file |
|
| `--snapshot` | _(required)_ | Path to JSON snapshot file |
|
||||||
| `--uuid` | *(required)* | UUID to assign (must match the client's UUID) |
|
| `--uuid` | _(required)_ | UUID to assign (must match the client's UUID) |
|
||||||
| `--db` | `db/game.db` | SQLite database path |
|
| `--db` | `db/game.db` | SQLite database path |
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
|
||||||
@@ -162,42 +181,68 @@ Or via `make`:
|
|||||||
make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000"
|
make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000"
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| --------------------- | ------------------ | ---------------------------------------- |
|
| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `--auth.listen` | `0.0.0.0:3000` | auth-server listen address |
|
| `--auth.listen` | `0.0.0.0:3000` | auth-server listen address |
|
||||||
| `--auth.db` | `db/auth.db` | auth-server SQLite database path |
|
| `--auth.db` | `db/auth.db` | auth-server SQLite database path |
|
||||||
| `--cdn.listen` | `0.0.0.0:8080` | octo-cdn local bind address |
|
| `--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 |
|
| `--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.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.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.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 |
|
| `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear |
|
||||||
| `--no-color` | `false` | disable colored output |
|
| `--no-register` | `false` | disable new user registrations (only already registered users can connect). |
|
||||||
|
| `--admin.listen` | _(empty)_ | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. |
|
||||||
|
| `--no-color` | `false` | disable colored output |
|
||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
|
|
||||||
| Protocol | Port | Binary | Notes |
|
| Protocol | Port | Binary | Notes |
|
||||||
| -------- | ---- | ------------- | ----------------------------------------------------------- |
|
| -------- | ---- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||||
| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) |
|
| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) |
|
||||||
| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages |
|
| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages |
|
||||||
|
| HTTP | 8082 | `lunar-tear` | admin webhook (`/api/admin/master-data/reload`); loopback by default, only binds when `LUNAR_ADMIN_TOKEN` is set |
|
||||||
|
| HTTP | 3000 | `auth-server` | account registration and login |
|
||||||
|
|
||||||
### Game Server Flags (`lunar-tear`)
|
### Game Server Flags (`lunar-tear`)
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| --------------- | ----------------- | ---------------------------------------------------- |
|
| ---------------- | ---------------- | --------------------------------------------------------------------------- |
|
||||||
| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) |
|
| `--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 |
|
| `--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`) |
|
| `--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 |
|
| `--db` | `db/game.db` | SQLite database path |
|
||||||
| `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) |
|
| `--auth-url` | _(empty)_ | Auth server base URL (e.g. `http://localhost:3000`) |
|
||||||
|
| `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. |
|
||||||
|
| `--no-register` | `false` | Disable new user registrations (only already registered users can connect). |
|
||||||
|
|
||||||
|
### Live Master Data Reload
|
||||||
|
|
||||||
|
The game server reads its master data from `assets/release/20240404193219.bin.e` at startup. To swap in updated content **without restarting** the server:
|
||||||
|
|
||||||
|
1. Replace `assets/release/20240404193219.bin.e` on disk with your edited copy.
|
||||||
|
2. POST to the admin webhook with a Bearer token matching `LUNAR_ADMIN_TOKEN`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: Bearer ${LUNAR_ADMIN_TOKEN}" \
|
||||||
|
http://127.0.0.1:8082/api/admin/master-data/reload
|
||||||
|
```
|
||||||
|
|
||||||
|
The server re-reads the file, atomically swaps every in-memory catalog and derived handler, and bumps the file's mtime. The mtime is folded into `GetLatestMasterDataVersion`, so connected clients see a new version string and re-download the file from the CDN on their next poll.
|
||||||
|
|
||||||
|
Security defaults are fail-closed:
|
||||||
|
|
||||||
|
- `LUNAR_ADMIN_TOKEN` **must** be set in the environment, or the webhook listener never binds.
|
||||||
|
- `--admin-listen` defaults to `127.0.0.1:8082` (loopback only). Bind to `0.0.0.0` only if you intend to expose it.
|
||||||
|
- Authentication uses constant-time Bearer-token comparison.
|
||||||
|
|
||||||
### CDN Flags (`octo-cdn`)
|
### CDN Flags (`octo-cdn`)
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| --------------- | ----------------- | -------------------------------------------------------- |
|
| --------------- | ---------------- | --------------------------------------------------------- |
|
||||||
| `--listen` | `0.0.0.0:8080` | local bind address |
|
| `--listen` | `0.0.0.0:8080` | local bind address |
|
||||||
| `--public-addr` | `127.0.0.1:8080` | externally-reachable address (used in list.bin rewriting) |
|
| `--public-addr` | `127.0.0.1:8080` | externally-reachable address (used in list.bin rewriting) |
|
||||||
| `--assets-dir` | `.` | root directory containing the `assets/` tree |
|
| `--assets-dir` | `.` | root directory containing the `assets/` tree |
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
@@ -212,32 +257,45 @@ The `db/` directory is mounted as a volume so both `game.db` and `auth.db` persi
|
|||||||
|
|
||||||
Each service has its own image and can be deployed independently:
|
Each service has its own image and can be deployed independently:
|
||||||
|
|
||||||
| Service | Image | Default Port | Notes |
|
| Service | Image | Default Port | Notes |
|
||||||
| -------- | --------------------------- | ------------ | ------------------------------ |
|
| -------- | --------------------------- | ------------ | -------------------------------- |
|
||||||
| `server` | `kretts/lunar-tear:latest` | 8003 | gRPC game server |
|
| `server` | `kretts/lunar-tear:latest` | 8003, 8082 | gRPC game server + admin webhook |
|
||||||
| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN |
|
| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN |
|
||||||
| `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login |
|
| `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.
|
The game server is configured via environment variables in the compose file:
|
||||||
|
|
||||||
|
| Env var | Description |
|
||||||
|
| -------------------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| `LUNAR_LISTEN` | gRPC bind address |
|
||||||
|
| `LUNAR_PUBLIC_ADDR` | Client-facing address advertised to the game |
|
||||||
|
| `LUNAR_OCTO_URL` | CDN base URL the client uses for assets |
|
||||||
|
| `LUNAR_AUTH_URL` | Auth server base URL (optional) |
|
||||||
|
| `LUNAR_ADMIN_LISTEN` | Admin webhook bind address inside the container (compose default: `0.0.0.0:8082`) |
|
||||||
|
| `LUNAR_ADMIN_TOKEN` | Bearer token for the admin webhook. **The webhook does not bind unless this is set.** |
|
||||||
|
|
||||||
|
Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. The admin webhook is published to `127.0.0.1:8082` on the host so the master-data reload endpoint stays loopback-only by default; set `LUNAR_ADMIN_TOKEN` (e.g. via a `.env` file) before bringing the stack up.
|
||||||
|
|
||||||
### Makefile Targets
|
### Makefile Targets
|
||||||
|
|
||||||
All targets run from the `server/` directory.
|
All targets run from the `server/` directory.
|
||||||
|
|
||||||
| Target | Description |
|
| Target | Description |
|
||||||
| -------------- | ------------------------------------------------------- |
|
| ----------------------------- | ------------------------------------------------------ |
|
||||||
| `make proto` | Regenerate protobuf stubs |
|
| `make proto` | Regenerate protobuf stubs |
|
||||||
| `make build` | Build the game server binary |
|
| `make build` | Build the game server binary |
|
||||||
| `make build-cdn` | Build the CDN binary |
|
| `make build-cdn` | Build the CDN binary |
|
||||||
| `make build-auth` | Build the auth server binary |
|
| `make build-auth` | Build the auth server binary |
|
||||||
| `make build-dev` | Build the dev runner binary to `bin/` |
|
| `make build-dev` | Build the dev runner binary to `bin/` |
|
||||||
| `make build-all` | Build all service binaries to `bin/` |
|
| `make build-all` | Build all service binaries to `bin/` |
|
||||||
| `make build-import` | Build the import-snapshot tool |
|
| `make build-import` | Build the import-snapshot tool |
|
||||||
| `make build-claim-account` | Build the claim-account tool |
|
| `make build-claim-account` | Build the claim-account tool |
|
||||||
| `make clean` | Remove the `bin/` directory |
|
| `make build-register-account` | Build the register-account tool |
|
||||||
| `make dev` | Run all three services with one command |
|
| `make clean` | Remove the `bin/` directory |
|
||||||
| `make migrate` | Run goose migrations on `db/game.db` |
|
| `make dev` | Run all three services with one command |
|
||||||
| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) |
|
| `make migrate` | Run goose migrations on `db/game.db` |
|
||||||
|
| `make restore` | Interactive restore of `db/game.db` from `db/backups/` |
|
||||||
|
| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) |
|
||||||
|
|
||||||
## Claim Account
|
## Claim Account
|
||||||
|
|
||||||
@@ -250,10 +308,10 @@ cd server
|
|||||||
go run ./cmd/claim-account --name "PlayerName" --db db/game.db
|
go run ./cmd/claim-account --name "PlayerName" --db db/game.db
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| -------- | ------------ | ---------------------------------------------------- |
|
| -------- | ------------ | ---------------------------- |
|
||||||
| `--name` | *(required)* | In-game player name to claim |
|
| `--name` | _(required)_ | In-game player name to claim |
|
||||||
| `--db` | `db/game.db` | SQLite database path |
|
| `--db` | `db/game.db` | SQLite database path |
|
||||||
|
|
||||||
## Auth Server
|
## Auth Server
|
||||||
|
|
||||||
@@ -272,11 +330,31 @@ The `--secret` flag accepts a hex-encoded HMAC key. If omitted, a random key is
|
|||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ---------- | --------------- | -------------------------------------------- |
|
| --------------- | -------------- | -------------------------------------------------------------------------- |
|
||||||
| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) |
|
| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) |
|
||||||
| `--db` | `db/auth.db` | SQLite database path for auth users |
|
| `--db` | `db/auth.db` | SQLite database path for auth users |
|
||||||
| `--secret` | *(generated)* | Hex-encoded HMAC secret for token signing |
|
| `--secret` | _(generated)_ | Hex-encoded HMAC secret for token signing |
|
||||||
|
| `--no-register` | `false` | Disable new user registrations (only already registered users can log in). |
|
||||||
|
|
||||||
|
## Create account
|
||||||
|
|
||||||
|
This tool creates a fresh account in main db and new account in Auth Server store with given name & password and automatically binds them together.
|
||||||
|
A primary mean of registering new accounts when `--no-register` flag is passed to lunar-tear for controlled server access.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/register-account --name "AccountName" --password "AccountPassword" --platform "android"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
| ------------ | ------------ | ------------------------------------------------- |
|
||||||
|
| `--name` | _(required)_ | Auth Server account nickname to be registered |
|
||||||
|
| `--password` | _(required)_ | Auth Server account password to be registered |
|
||||||
|
| `--platform` | `android` | Platform of new user account (`android` or `ios`) |
|
||||||
|
| `--db` | `db/game.db` | SQLite main database path |
|
||||||
|
| `--auth-db` | `db/auth.db` | SQLite Auth Server database path |
|
||||||
|
|
||||||
|
This only sets the nickname of Auth Server account, a player can choose their in-game nickname upon first login!
|
||||||
|
|
||||||
## ⚠️ Legal Disclaimer
|
## ⚠️ Legal Disclaimer
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,59 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-05-16
|
||||||
|
|
||||||
|
### Working
|
||||||
|
|
||||||
|
- `--no-register` flag and `register-account` CLI tool
|
||||||
|
- Database backup support in the wizard
|
||||||
|
- Subjugation Quests — rewards, battle report, and triple-deck preset persistence
|
||||||
|
- `MaterialSaleObtainPossession` item grants when selling materials
|
||||||
|
- Memoir protect / unprotect
|
||||||
|
- Effect item usage (`UseEffectItem`)
|
||||||
|
- Recollections of Dusk
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Login bonus
|
||||||
|
- Main quest replay (Map replay)
|
||||||
|
- Weapon awaken level cap
|
||||||
|
- Gacha pool overhaul
|
||||||
|
- Quest mission rewards
|
||||||
|
- Menu-pick quest start — wrong state from replay flow, black screen on quests with no difficulty, and normal-difficulty handling
|
||||||
|
|
||||||
|
## 2026-05-02
|
||||||
|
|
||||||
|
### Working
|
||||||
|
|
||||||
|
- Cross-platform asset/CDN pipeline (iOS and Android)
|
||||||
|
- Master data hot-reload: loopback admin webhook (`/api/admin/master-data/reload`) with `LUNAR_ADMIN_TOKEN`; runtime build registry; wizard, dev runner, and Docker wiring
|
||||||
|
- Scripts repo: Octo **`list.bin` patcher** (`assetbundles/patch_listbin.py`) — refresh catalog `size` / `md5` / `crc` after swapping bundles or resources
|
||||||
|
- Scripts repo: **asset bundle encrypt/decrypt** for Octo-masked UnityFS (`assetbundles/encrypt_assetbundle.py`, `decrypt_assetbundle.py`, `assetbundle_crypt`)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Stale weapon story unlocks and incorrect post-evolution weapon state
|
||||||
|
|
||||||
|
## 2026-04-25
|
||||||
|
|
||||||
|
### Working
|
||||||
|
|
||||||
|
- SQLite persistence layer with snapshot import tool
|
||||||
|
- Authentication server with login UI
|
||||||
|
- Wizard CLI for guided first-time setup
|
||||||
|
- Dev runner (`make dev`) with automatic service builds
|
||||||
|
- Memoir sub-status system with level-based unlocks
|
||||||
|
- Companion and parts granting from the shop
|
||||||
|
- `CopyDeck` / `RemoveDeck` deck management
|
||||||
|
- Karma functionality
|
||||||
|
- Docker multi-service orchestration (auth, CDN, gRPC) with cross-platform improvements
|
||||||
|
- `--grpc-port` CLI flag
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Gate desync on quest-finish crash — scene now advances atomically
|
||||||
|
- Equipment duplication in deck management
|
||||||
|
|
||||||
## 2026-04-18
|
## 2026-04-18
|
||||||
|
|
||||||
### Working
|
### Working
|
||||||
|
|||||||
+7
-1
@@ -30,6 +30,9 @@ build-auth:
|
|||||||
build-claim-account:
|
build-claim-account:
|
||||||
go build -o claim-account$(EXE) ./cmd/claim-account
|
go build -o claim-account$(EXE) ./cmd/claim-account
|
||||||
|
|
||||||
|
build-register-account:
|
||||||
|
go build -o register-account$(EXE) ./cmd/register-account
|
||||||
|
|
||||||
build-dev:
|
build-dev:
|
||||||
go build -o bin/dev$(EXE) ./cmd/dev
|
go build -o bin/dev$(EXE) ./cmd/dev
|
||||||
|
|
||||||
@@ -48,6 +51,9 @@ dev:
|
|||||||
migrate:
|
migrate:
|
||||||
$(GOOSE) -dir migrations -allow-missing sqlite3 db/game.db up
|
$(GOOSE) -dir migrations -allow-missing sqlite3 db/game.db up
|
||||||
|
|
||||||
|
restore:
|
||||||
|
go run ./cmd/wizard-restore
|
||||||
|
|
||||||
import:
|
import:
|
||||||
ifndef SNAPSHOT
|
ifndef SNAPSHOT
|
||||||
$(error SNAPSHOT is required, e.g. make import SNAPSHOT=snapshots/scene_1.json UUID=...)
|
$(error SNAPSHOT is required, e.g. make import SNAPSHOT=snapshots/scene_1.json UUID=...)
|
||||||
@@ -57,4 +63,4 @@ ifndef UUID
|
|||||||
endif
|
endif
|
||||||
go run ./cmd/import-snapshot --snapshot $(SNAPSHOT) --uuid $(UUID)
|
go run ./cmd/import-snapshot --snapshot $(SNAPSHOT) --uuid $(UUID)
|
||||||
|
|
||||||
.PHONY: proto build build-cdn build-auth build-import build-claim-account build-dev build-all clean dev migrate import
|
.PHONY: proto build build-cdn build-auth build-import build-claim-account build-register-account build-dev build-all clean dev migrate restore import
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
"lunar-tear/server/internal/auth"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -18,18 +19,40 @@ var loginFS embed.FS
|
|||||||
|
|
||||||
var loginTmpl = template.Must(template.ParseFS(loginFS, "login.html"))
|
var loginTmpl = template.Must(template.ParseFS(loginFS, "login.html"))
|
||||||
|
|
||||||
|
// oauthRedirectTmpl drives the fbconnect:// hand-off via a renderer-initiated
|
||||||
|
// navigation instead of a server-side 302. Android WebView does NOT invoke
|
||||||
|
// WebViewClient.shouldOverrideUrlLoading for 302 redirects from POST form
|
||||||
|
// submissions to non-http schemes (documented Chromium WebView limitation,
|
||||||
|
// Stack Overflow #6738328 / Google issuetracker #36918490). Returning a 200
|
||||||
|
// HTML page with both <meta http-equiv="refresh"> and window.location.replace()
|
||||||
|
// makes the cross-scheme navigation renderer-initiated, which DOES invoke
|
||||||
|
// shouldOverrideUrlLoading, so the FB SDK can extract access_token from the
|
||||||
|
// URL fragment and complete its login flow. html/template auto-escapes {{.}}
|
||||||
|
// correctly for the meta URL-attribute context and the JS string-literal
|
||||||
|
// context inside <script>.
|
||||||
|
var oauthRedirectTmpl = template.Must(template.New("oauthRedirect").Parse(
|
||||||
|
`<!doctype html><html><head><meta charset="utf-8">
|
||||||
|
<meta http-equiv="refresh" content="0;url={{.}}">
|
||||||
|
<script>window.location.replace({{.}});</script>
|
||||||
|
</head><body>
|
||||||
|
<noscript><a href="{{.}}">Continue</a></noscript>
|
||||||
|
</body></html>
|
||||||
|
`))
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
store *AuthStore
|
store *auth.AuthStore
|
||||||
tok *TokenService
|
tok *auth.TokenService
|
||||||
|
noRegister bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandlers(store *AuthStore, tok *TokenService) *Handlers {
|
func NewHandlers(store *auth.AuthStore, tok *auth.TokenService, noRegister bool) *Handlers {
|
||||||
return &Handlers{store: store, tok: tok}
|
return &Handlers{store: store, tok: tok, noRegister: noRegister}
|
||||||
}
|
}
|
||||||
|
|
||||||
type loginPageData struct {
|
type loginPageData struct {
|
||||||
RedirectURI string
|
RedirectURI string
|
||||||
State string
|
State string
|
||||||
|
Scope string
|
||||||
Error string
|
Error string
|
||||||
Username string
|
Username string
|
||||||
}
|
}
|
||||||
@@ -77,6 +100,7 @@ func (h *Handlers) oauthGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
data := loginPageData{
|
data := loginPageData{
|
||||||
RedirectURI: r.URL.Query().Get("redirect_uri"),
|
RedirectURI: r.URL.Query().Get("redirect_uri"),
|
||||||
State: r.URL.Query().Get("state"),
|
State: r.URL.Query().Get("state"),
|
||||||
|
Scope: r.URL.Query().Get("scope"),
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
if err := loginTmpl.Execute(w, data); err != nil {
|
if err := loginTmpl.Execute(w, data); err != nil {
|
||||||
@@ -95,11 +119,13 @@ func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
action := r.FormValue("action")
|
action := r.FormValue("action")
|
||||||
redirectURI := r.FormValue("redirect_uri")
|
redirectURI := r.FormValue("redirect_uri")
|
||||||
state := r.FormValue("state")
|
state := r.FormValue("state")
|
||||||
|
scope := r.FormValue("scope")
|
||||||
|
|
||||||
renderErr := func(msg string) {
|
renderErr := func(msg string) {
|
||||||
data := loginPageData{
|
data := loginPageData{
|
||||||
RedirectURI: redirectURI,
|
RedirectURI: redirectURI,
|
||||||
State: state,
|
State: state,
|
||||||
|
Scope: scope,
|
||||||
Error: msg,
|
Error: msg,
|
||||||
Username: username,
|
Username: username,
|
||||||
}
|
}
|
||||||
@@ -115,13 +141,18 @@ func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var user AuthUser
|
var user auth.AuthUser
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "register":
|
case "register":
|
||||||
|
if h.noRegister {
|
||||||
|
renderErr("This server does not accept user registrations.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user, err = h.store.CreateUser(username, password)
|
user, err = h.store.CreateUser(username, password)
|
||||||
if err == ErrUserExists {
|
if err == auth.ErrUserExists {
|
||||||
renderErr("Username is already taken.")
|
renderErr("Username is already taken.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -134,7 +165,7 @@ func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
case "login":
|
case "login":
|
||||||
user, err = h.store.VerifyUser(username, password)
|
user, err = h.store.VerifyUser(username, password)
|
||||||
if err == ErrInvalidCreds {
|
if err == auth.ErrInvalidCreds {
|
||||||
renderErr("Invalid username or password.")
|
renderErr("Invalid username or password.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -158,20 +189,31 @@ func (h *Handlers) oauthPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload := fmt.Sprintf(`{"user_id":"%d"}`, user.ID)
|
payload := fmt.Sprintf(`{"user_id":"%d"}`, user.ID)
|
||||||
b64 := base64.StdEncoding.EncodeToString([]byte(payload))
|
b64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||||
|
|
||||||
fragment := url.Values{}
|
fragment := url.Values{}
|
||||||
fragment.Set("access_token", token)
|
fragment.Set("access_token", token)
|
||||||
fragment.Set("token_type", "bearer")
|
fragment.Set("token_type", "bearer")
|
||||||
fragment.Set("expires_in", strconv.FormatInt(int64(tokenTTL.Seconds()), 10))
|
fragment.Set("expires_in", strconv.FormatInt(int64(auth.TokenTTL.Seconds()), 10))
|
||||||
fragment.Set("signed_request", "0."+b64)
|
fragment.Set("signed_request", "0."+b64)
|
||||||
|
// iOS FBSDKLoginManager treats an empty granted_scopes set as a cancelled login
|
||||||
|
// (LoginManager.swift -> getSuccessResult -> getCancelledResult). Echo back the
|
||||||
|
// scope the SDK sent so parameters.permissions is non-empty and the SDK fires
|
||||||
|
// its success path. Android tolerates either way.
|
||||||
|
if scope != "" {
|
||||||
|
fragment.Set("granted_scopes", scope)
|
||||||
|
fragment.Set("denied_scopes", "")
|
||||||
|
}
|
||||||
if state != "" {
|
if state != "" {
|
||||||
fragment.Set("state", state)
|
fragment.Set("state", state)
|
||||||
}
|
}
|
||||||
|
|
||||||
target := redirectURI + "?" + fragment.Encode()
|
target := redirectURI + "?" + fragment.Encode()
|
||||||
log.Printf("redirecting to %s", target)
|
log.Printf("redirecting to %s", target)
|
||||||
http.Redirect(w, r, target, http.StatusFound)
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if err := oauthRedirectTmpl.Execute(w, target); err != nil {
|
||||||
|
log.Printf("render oauth redirect: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) HandleCheckUsername(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) HandleCheckUsername(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
|
|
||||||
<input type="hidden" name="redirect_uri" value="{{.RedirectURI}}">
|
<input type="hidden" name="redirect_uri" value="{{.RedirectURI}}">
|
||||||
<input type="hidden" name="state" value="{{.State}}">
|
<input type="hidden" name="state" value="{{.State}}">
|
||||||
|
<input type="hidden" name="scope" value="{{.Scope}}">
|
||||||
|
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" id="username" name="username" value="{{.Username}}" autocomplete="username" autofocus required>
|
<input type="text" id="username" name="username" value="{{.Username}}" autocomplete="username" autofocus required>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/auth"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ func main() {
|
|||||||
listen := flag.String("listen", "0.0.0.0:3000", "HTTP listen address (host:port)")
|
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")
|
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)")
|
secret := flag.String("secret", "", "HMAC secret for tokens (auto-generated if empty)")
|
||||||
|
noRegister := flag.Bool("no-register", false, "Disallow new account registrations for clients, when present. Default = false")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
hmacSecret := []byte(*secret)
|
hmacSecret := []byte(*secret)
|
||||||
@@ -33,13 +36,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
store, err := NewAuthStore(db)
|
store, err := auth.NewAuthStore(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("init auth store: %v", err)
|
log.Fatalf("init auth store: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tok := NewTokenService(hmacSecret)
|
tok := auth.NewTokenService(hmacSecret)
|
||||||
h := NewHandlers(store, tok)
|
h := NewHandlers(store, tok, *noRegister)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", h.HandleOAuth)
|
mux.HandleFunc("/", h.HandleOAuth)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ var childTables = []string{
|
|||||||
"user_big_hunt_max_scores",
|
"user_big_hunt_max_scores",
|
||||||
"user_quest_limit_content_status",
|
"user_quest_limit_content_status",
|
||||||
"user_side_story_quests",
|
"user_side_story_quests",
|
||||||
|
"user_main_quest_season_routes",
|
||||||
"user_missions",
|
"user_missions",
|
||||||
"user_quest_missions",
|
"user_quest_missions",
|
||||||
"user_quests",
|
"user_quests",
|
||||||
|
|||||||
+44
-7
@@ -93,7 +93,16 @@ func main() {
|
|||||||
grpcOctoURL := flag.String("grpc.octo-url", "", "Octo CDN base URL passed to lunar-tear (default: derived from cdn.public-addr)")
|
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)")
|
grpcAuthURL := flag.String("grpc.auth-url", "", "auth server base URL passed to lunar-tear (default: derived from auth.listen)")
|
||||||
|
|
||||||
|
// admin webhook is opt-in; empty leaves lunar-tear's own default in place
|
||||||
|
// (the listener still only binds if LUNAR_ADMIN_TOKEN is set in the env).
|
||||||
|
adminListen := flag.String("admin.listen", "", "lunar-tear admin webhook listen address (host:port). Empty = leave default; webhook only binds when LUNAR_ADMIN_TOKEN is set in the env.")
|
||||||
|
|
||||||
|
// Controlled server access
|
||||||
|
noRegister := flag.Bool("no-register", false, "Disallow new account registrations for clients, when present. Default = false")
|
||||||
|
|
||||||
|
// dev utility output config
|
||||||
noColor := flag.Bool("no-color", false, "disable colored output")
|
noColor := flag.Bool("no-color", false, "disable colored output")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *grpcOctoURL == "" {
|
if *grpcOctoURL == "" {
|
||||||
@@ -111,13 +120,22 @@ func main() {
|
|||||||
colorCyan = ""
|
colorCyan = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("building services...")
|
if _, err := os.Stat("go.mod"); err == nil {
|
||||||
buildAll()
|
log.Println("building services...")
|
||||||
|
buildAll()
|
||||||
|
} else {
|
||||||
|
log.Println("prebuilt mode: skipping build, using bin/ from archive")
|
||||||
|
}
|
||||||
|
|
||||||
ext := binExt()
|
ext := binExt()
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
|
noreg_s := ""
|
||||||
|
if *noRegister {
|
||||||
|
noreg_s = "--no-register"
|
||||||
|
}
|
||||||
|
|
||||||
services := []service{
|
services := []service{
|
||||||
{
|
{
|
||||||
label: "auth",
|
label: "auth",
|
||||||
@@ -125,6 +143,7 @@ func main() {
|
|||||||
cmd: exec.CommandContext(ctx, filepath.Join("bin", "auth-server"+ext),
|
cmd: exec.CommandContext(ctx, filepath.Join("bin", "auth-server"+ext),
|
||||||
"--listen", *authListen,
|
"--listen", *authListen,
|
||||||
"--db", *authDB,
|
"--db", *authDB,
|
||||||
|
noreg_s,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -139,11 +158,7 @@ func main() {
|
|||||||
label: "grpc",
|
label: "grpc",
|
||||||
color: colorYellow,
|
color: colorYellow,
|
||||||
cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext),
|
cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext),
|
||||||
"--listen", *grpcListen,
|
grpcArgs(*grpcListen, *grpcPublicAddr, *grpcDB, *grpcOctoURL, *grpcAuthURL, *adminListen, *noRegister)...,
|
||||||
"--public-addr", *grpcPublicAddr,
|
|
||||||
"--db", *grpcDB,
|
|
||||||
"--octo-url", *grpcOctoURL,
|
|
||||||
"--auth-url", *grpcAuthURL,
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -200,3 +215,25 @@ func prefixLines(wg *sync.WaitGroup, prefix string, r io.Reader) {
|
|||||||
fmt.Printf("%s%s\n", prefix, scanner.Text())
|
fmt.Printf("%s%s\n", prefix, scanner.Text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// grpcArgs assembles the argv for the lunar-tear subprocess. The admin flag
|
||||||
|
// is appended only when --admin.listen was supplied so we don't override
|
||||||
|
// lunar-tear's own default when the operator hasn't opted in.
|
||||||
|
func grpcArgs(listen, publicAddr, db, octoURL, authURL, adminListen string, noRegister bool) []string {
|
||||||
|
args := []string{
|
||||||
|
"--listen", listen,
|
||||||
|
"--public-addr", publicAddr,
|
||||||
|
"--db", db,
|
||||||
|
"--octo-url", octoURL,
|
||||||
|
"--auth-url", authURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminListen != "" {
|
||||||
|
args = append(args, "--admin-listen", adminListen)
|
||||||
|
}
|
||||||
|
|
||||||
|
if noRegister {
|
||||||
|
args = append(args, "--no-register")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// startAdmin spins up the admin webhook used by external content tools to
|
||||||
|
// trigger an in-place re-read of assets/release/20240404193219.bin.e.
|
||||||
|
//
|
||||||
|
// Authentication: Bearer token via the LUNAR_ADMIN_TOKEN environment variable.
|
||||||
|
// If LUNAR_ADMIN_TOKEN is unset or empty the listener does not bind at all
|
||||||
|
// (fail closed), so a fresh deploy never exposes an unauthenticated endpoint.
|
||||||
|
//
|
||||||
|
// The default --admin-listen is 127.0.0.1:8082 so the webhook is only
|
||||||
|
// reachable via loopback unless the operator opts in by binding to 0.0.0.0.
|
||||||
|
func startAdmin(listen string, holder *runtime.Holder) {
|
||||||
|
token := os.Getenv("LUNAR_ADMIN_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
log.Println("[admin] disabled (no LUNAR_ADMIN_TOKEN set)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expected := []byte("Bearer " + token)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/api/admin/master-data/reload", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got := []byte(r.Header.Get("Authorization"))
|
||||||
|
if len(got) != len(expected) || subtle.ConstantTimeCompare(got, expected) != 1 {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := holder.Reload(); err != nil {
|
||||||
|
log.Printf("[admin] master-data reload failed: %v", err)
|
||||||
|
http.Error(w, "master-data reload failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[admin] master-data reloaded by %s", r.RemoteAddr)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("[admin] webhook listener on %s (token-gated)", listen)
|
||||||
|
go func() {
|
||||||
|
if err := http.ListenAndServe(listen, mux); err != nil {
|
||||||
|
log.Printf("[admin] webhook listener failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -6,10 +6,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gacha"
|
|
||||||
"lunar-tear/server/internal/interceptor"
|
"lunar-tear/server/internal/interceptor"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/questflow"
|
|
||||||
"lunar-tear/server/internal/service"
|
"lunar-tear/server/internal/service"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
@@ -40,27 +38,8 @@ func startGRPC(
|
|||||||
store.UserRepository
|
store.UserRepository
|
||||||
store.SessionRepository
|
store.SessionRepository
|
||||||
},
|
},
|
||||||
questEngine *questflow.QuestHandler,
|
holder *runtime.Holder,
|
||||||
gachaHandler *gacha.GachaHandler,
|
noRegister bool,
|
||||||
gachaEntries []store.GachaCatalogEntry,
|
|
||||||
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
|
|
||||||
loginBonusCatalog *masterdata.LoginBonusCatalog,
|
|
||||||
characterViewerCatalog *masterdata.CharacterViewerCatalog,
|
|
||||||
shopCatalog *masterdata.ShopCatalog,
|
|
||||||
costumeCatalog *masterdata.CostumeCatalog,
|
|
||||||
omikujiCatalog *masterdata.OmikujiCatalog,
|
|
||||||
weaponCatalog *masterdata.WeaponCatalog,
|
|
||||||
exploreCatalog *masterdata.ExploreCatalog,
|
|
||||||
gimmickCatalog *masterdata.GimmickCatalog,
|
|
||||||
characterBoardCatalog *masterdata.CharacterBoardCatalog,
|
|
||||||
partsCatalog *masterdata.PartsCatalog,
|
|
||||||
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
|
|
||||||
companionCatalog *masterdata.CompanionCatalog,
|
|
||||||
materialCatalog *masterdata.MaterialCatalog,
|
|
||||||
consumableItemCatalog *masterdata.ConsumableItemCatalog,
|
|
||||||
gameConfig *masterdata.GameConfig,
|
|
||||||
sideStoryCatalog *masterdata.SideStoryCatalog,
|
|
||||||
bigHuntCatalog *masterdata.BigHuntCatalog,
|
|
||||||
) *grpc.Server {
|
) *grpc.Server {
|
||||||
lis, err := net.Listen("tcp", listenAddr)
|
lis, err := net.Listen("tcp", listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,39 +53,17 @@ func startGRPC(
|
|||||||
grpc.UnknownServiceHandler(interceptor.UnknownService),
|
grpc.UnknownServiceHandler(interceptor.UnknownService),
|
||||||
)
|
)
|
||||||
|
|
||||||
registerServices(grpcServer,
|
registerServices(grpcServer, publicAddr, octoURL, authURL, userStore, holder, noRegister)
|
||||||
publicAddr,
|
|
||||||
octoURL,
|
|
||||||
authURL,
|
|
||||||
userStore,
|
|
||||||
questEngine,
|
|
||||||
gachaHandler,
|
|
||||||
gachaEntries,
|
|
||||||
cageOrnamentCatalog,
|
|
||||||
loginBonusCatalog,
|
|
||||||
characterViewerCatalog,
|
|
||||||
shopCatalog,
|
|
||||||
costumeCatalog,
|
|
||||||
omikujiCatalog,
|
|
||||||
weaponCatalog,
|
|
||||||
exploreCatalog,
|
|
||||||
gimmickCatalog,
|
|
||||||
characterBoardCatalog,
|
|
||||||
partsCatalog,
|
|
||||||
characterRebirthCatalog,
|
|
||||||
companionCatalog,
|
|
||||||
materialCatalog,
|
|
||||||
consumableItemCatalog,
|
|
||||||
gameConfig,
|
|
||||||
sideStoryCatalog,
|
|
||||||
bigHuntCatalog,
|
|
||||||
)
|
|
||||||
|
|
||||||
reflection.Register(grpcServer)
|
reflection.Register(grpcServer)
|
||||||
|
|
||||||
log.Printf("gRPC server listening on %s", lis.Addr())
|
log.Printf("gRPC server listening on %s", lis.Addr())
|
||||||
log.Printf("public address: %s", publicAddr)
|
log.Printf("public address: %s", publicAddr)
|
||||||
|
|
||||||
|
if noRegister {
|
||||||
|
log.Print("[!!WARNING!!] The gRPC server is running in NO-REGISTER mode. All new user registrations are denied, only existing accounts and auth-server logins are permitted.")
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := grpcServer.Serve(lis); err != nil {
|
if err := grpcServer.Serve(lis); err != nil {
|
||||||
log.Printf("gRPC server stopped: %v", err)
|
log.Printf("gRPC server stopped: %v", err)
|
||||||
@@ -124,66 +81,48 @@ func registerServices(
|
|||||||
store.UserRepository
|
store.UserRepository
|
||||||
store.SessionRepository
|
store.SessionRepository
|
||||||
},
|
},
|
||||||
questEngine *questflow.QuestHandler,
|
holder *runtime.Holder,
|
||||||
gachaHandler *gacha.GachaHandler,
|
noRegister bool,
|
||||||
gachaEntries []store.GachaCatalogEntry,
|
|
||||||
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
|
|
||||||
loginBonusCatalog *masterdata.LoginBonusCatalog,
|
|
||||||
characterViewerCatalog *masterdata.CharacterViewerCatalog,
|
|
||||||
shopCatalog *masterdata.ShopCatalog,
|
|
||||||
costumeCatalog *masterdata.CostumeCatalog,
|
|
||||||
omikujiCatalog *masterdata.OmikujiCatalog,
|
|
||||||
weaponCatalog *masterdata.WeaponCatalog,
|
|
||||||
exploreCatalog *masterdata.ExploreCatalog,
|
|
||||||
gimmickCatalog *masterdata.GimmickCatalog,
|
|
||||||
characterBoardCatalog *masterdata.CharacterBoardCatalog,
|
|
||||||
partsCatalog *masterdata.PartsCatalog,
|
|
||||||
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
|
|
||||||
companionCatalog *masterdata.CompanionCatalog,
|
|
||||||
materialCatalog *masterdata.MaterialCatalog,
|
|
||||||
consumableItemCatalog *masterdata.ConsumableItemCatalog,
|
|
||||||
gameConfig *masterdata.GameConfig,
|
|
||||||
sideStoryCatalog *masterdata.SideStoryCatalog,
|
|
||||||
bigHuntCatalog *masterdata.BigHuntCatalog,
|
|
||||||
) {
|
) {
|
||||||
pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr)
|
pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr)
|
||||||
pubPort, _ := strconv.Atoi(pubPortStr)
|
pubPort, _ := strconv.Atoi(pubPortStr)
|
||||||
|
|
||||||
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(gachaEntries))
|
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(holder))
|
||||||
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL))
|
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, holder, authURL, noRegister))
|
||||||
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
|
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
|
||||||
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL))
|
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL))
|
||||||
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
|
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
|
||||||
pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine))
|
pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, gachaEntries, gachaHandler))
|
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore))
|
pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore))
|
||||||
pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer())
|
pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer())
|
||||||
pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, gimmickCatalog))
|
pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, questEngine))
|
pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterNotificationServiceServer(srv, service.NewNotificationServiceServer(userStore, userStore))
|
pb.RegisterNotificationServiceServer(srv, service.NewNotificationServiceServer(userStore, userStore))
|
||||||
pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, cageOrnamentCatalog, questEngine.Granter))
|
pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterDeckServiceServer(srv, service.NewDeckServiceServer(userStore, userStore))
|
pb.RegisterDeckServiceServer(srv, service.NewDeckServiceServer(userStore, userStore))
|
||||||
pb.RegisterFriendServiceServer(srv, service.NewFriendServiceServer(userStore, userStore))
|
pb.RegisterFriendServiceServer(srv, service.NewFriendServiceServer(userStore, userStore))
|
||||||
pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, loginBonusCatalog))
|
pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterNaviCutInServiceServer(srv, service.NewNaviCutInServiceServer(userStore, userStore))
|
pb.RegisterNaviCutInServiceServer(srv, service.NewNaviCutInServiceServer(userStore, userStore))
|
||||||
pb.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore))
|
pb.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore))
|
||||||
pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(userStore, userStore))
|
pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(userStore, userStore))
|
||||||
pb.RegisterPortalCageServiceServer(srv, service.NewPortalCageServiceServer(userStore, userStore))
|
pb.RegisterPortalCageServiceServer(srv, service.NewPortalCageServiceServer(userStore, userStore))
|
||||||
pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, characterViewerCatalog))
|
pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterMissionServiceServer(srv, service.NewMissionServiceServer(userStore, userStore))
|
pb.RegisterMissionServiceServer(srv, service.NewMissionServiceServer(userStore, userStore))
|
||||||
pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, shopCatalog, questEngine.Granter))
|
pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, costumeCatalog, gameConfig))
|
pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore))
|
pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore))
|
||||||
pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, omikujiCatalog))
|
pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, weaponCatalog, gameConfig))
|
pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, exploreCatalog))
|
pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, characterBoardCatalog))
|
pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, partsCatalog, gameConfig))
|
pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, characterRebirthCatalog, gameConfig))
|
pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, companionCatalog, gameConfig))
|
pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, materialCatalog, gameConfig))
|
pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, consumableItemCatalog, gameConfig))
|
pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, sideStoryCatalog))
|
pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine))
|
pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, holder))
|
||||||
pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter))
|
pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, holder))
|
||||||
|
pb.RegisterLabyrinthServiceServer(srv, service.NewLabyrinthServiceServer(userStore, userStore, holder))
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-156
@@ -9,30 +9,31 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"lunar-tear/server/internal/database"
|
"lunar-tear/server/internal/database"
|
||||||
"lunar-tear/server/internal/gacha"
|
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/masterdata/memorydb"
|
|
||||||
"lunar-tear/server/internal/questflow"
|
|
||||||
"lunar-tear/server/internal/store/sqlite"
|
"lunar-tear/server/internal/store/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const masterDataPath = "assets/release/20240404193219.bin.e"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
listen := flag.String("listen", "0.0.0.0:443", "gRPC listen address (host:port)")
|
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")
|
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")
|
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)")
|
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)")
|
authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)")
|
||||||
|
adminListen := flag.String("admin-listen", "127.0.0.1:8082", "admin webhook listen address (host:port). Loopback by default; only binds when LUNAR_ADMIN_TOKEN is set.")
|
||||||
|
noRegister := flag.Bool("no-register", false, "Disallow new account registrations for clients, when present. Default = false")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *octoURL == "" {
|
if *octoURL == "" {
|
||||||
log.Fatalf("--octo-url is required (e.g. http://10.0.2.2:8080)")
|
log.Fatalf("--octo-url is required (e.g. http://10.0.2.2:8080)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := memorydb.Init("assets/release/20240404193219.bin.e"); err != nil {
|
holder, err := runtime.NewHolder(masterDataPath)
|
||||||
log.Fatalf("load master data: %v", err)
|
if err != nil {
|
||||||
|
log.Fatalf("init master data: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("master data loaded (%d tables)", memorydb.TableCount())
|
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
@@ -44,158 +45,11 @@ func main() {
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
log.Printf("database opened: %s", *dbPath)
|
log.Printf("database opened: %s", *dbPath)
|
||||||
|
|
||||||
gameConfig, err := masterdata.LoadGameConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load game config: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)",
|
|
||||||
gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold)
|
|
||||||
|
|
||||||
partsCatalog, err := masterdata.LoadPartsCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load parts catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType))
|
|
||||||
|
|
||||||
questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load quest catalog: %v", err)
|
|
||||||
}
|
|
||||||
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
|
|
||||||
userStore := sqlite.New(db, gametime.Now)
|
userStore := sqlite.New(db, gametime.Now)
|
||||||
|
|
||||||
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
grpcServer := startGRPC(*listen, *publicAddr, *octoURL, *authURL, userStore, holder, *noRegister)
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load gacha catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("gacha catalog loaded: %d entries", len(gachaEntries))
|
|
||||||
|
|
||||||
gachaPool, err := masterdata.LoadGachaPool()
|
startAdmin(*adminListen, holder)
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load gacha pool: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d",
|
|
||||||
len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials))
|
|
||||||
|
|
||||||
shopCatalog, err := masterdata.LoadShopCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load shop catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops",
|
|
||||||
len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells))
|
|
||||||
|
|
||||||
gachaPool.BuildShopFeatured(shopCatalog)
|
|
||||||
gachaPool.PruneUnpairedCostumes()
|
|
||||||
gachaPool.BuildFeaturedMapping(gachaEntries)
|
|
||||||
gachaPool.BuildBannerPools(gachaEntries)
|
|
||||||
masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool)
|
|
||||||
|
|
||||||
dupExchange, err := masterdata.LoadDupExchange()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load dup exchange: %v", err)
|
|
||||||
}
|
|
||||||
dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("enrich dup exchange: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded)
|
|
||||||
|
|
||||||
gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange)
|
|
||||||
|
|
||||||
conditionResolver, err := masterdata.LoadConditionResolver()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load condition resolver: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog()
|
|
||||||
loginBonusCatalog := masterdata.LoadLoginBonusCatalog()
|
|
||||||
characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver)
|
|
||||||
omikujiCatalog := masterdata.LoadOmikujiCatalog()
|
|
||||||
|
|
||||||
materialCatalog, err := masterdata.LoadMaterialCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load material catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("material catalog loaded: %d materials", len(materialCatalog.All))
|
|
||||||
|
|
||||||
consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load consumable item catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All))
|
|
||||||
|
|
||||||
costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load costume catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity))
|
|
||||||
|
|
||||||
weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load weapon catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId))
|
|
||||||
|
|
||||||
exploreCatalog, err := masterdata.LoadExploreCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load explore catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
|
|
||||||
|
|
||||||
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load gimmick catalog: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load character board catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
|
|
||||||
|
|
||||||
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load character rebirth catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
|
|
||||||
|
|
||||||
companionCatalog, err := masterdata.LoadCompanionCatalog()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("load companion catalog: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
|
|
||||||
|
|
||||||
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
|
||||||
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
|
|
||||||
|
|
||||||
grpcServer := startGRPC(
|
|
||||||
*listen,
|
|
||||||
*publicAddr,
|
|
||||||
*octoURL,
|
|
||||||
*authURL,
|
|
||||||
userStore,
|
|
||||||
questHandler,
|
|
||||||
gachaHandler,
|
|
||||||
gachaEntries,
|
|
||||||
cageOrnamentCatalog,
|
|
||||||
loginBonusCatalog,
|
|
||||||
characterViewerCatalog,
|
|
||||||
shopCatalog,
|
|
||||||
costumeCatalog,
|
|
||||||
omikujiCatalog,
|
|
||||||
weaponCatalog,
|
|
||||||
exploreCatalog,
|
|
||||||
gimmickCatalog,
|
|
||||||
characterBoardCatalog,
|
|
||||||
partsCatalog,
|
|
||||||
characterRebirthCatalog,
|
|
||||||
companionCatalog,
|
|
||||||
materialCatalog,
|
|
||||||
consumableItemCatalog,
|
|
||||||
gameConfig,
|
|
||||||
sideStoryCatalog,
|
|
||||||
bigHuntCatalog,
|
|
||||||
)
|
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
log.Println("shutting down...")
|
log.Println("shutting down...")
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/auth"
|
||||||
|
"lunar-tear/server/internal/database"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/store/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dbPath := flag.String("db", "db/game.db", "SQLite database path")
|
||||||
|
authdbPath := flag.String("auth-db", "db/auth.db", "SQLite auth server database path")
|
||||||
|
|
||||||
|
name := flag.String("name", "", "Nickname of the new account to-be")
|
||||||
|
password := flag.String("password", "", "Password of the new account to-be")
|
||||||
|
platform := flag.String("platform", "android", "Platform of the user. Can be: \"android\", \"ios\"")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *name == "" {
|
||||||
|
log.Fatal("--name flag is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *password == "" {
|
||||||
|
log.Fatal("--password flag is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*platform != "android") && (*platform != "ios") {
|
||||||
|
log.Fatal("--platform can be either \"android\" or \"ios\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := database.Open(*dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("open database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
userStore := sqlite.New(db, nil)
|
||||||
|
|
||||||
|
authDb, err := database.Open(*authdbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("open auth database: %v", err)
|
||||||
|
}
|
||||||
|
defer authDb.Close()
|
||||||
|
|
||||||
|
authStore, err := auth.NewAuthStore(authDb)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("init auth store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userExists := authStore.UserExists(*name)
|
||||||
|
if userExists {
|
||||||
|
log.Fatal("Username is already taken")
|
||||||
|
}
|
||||||
|
|
||||||
|
var userPlatform model.ClientPlatform
|
||||||
|
|
||||||
|
if *platform == "android" {
|
||||||
|
userPlatform.OsType = model.OsTypeAndroid
|
||||||
|
userPlatform.PlatformType = model.PlatformTypeGooglePlayStore
|
||||||
|
} else {
|
||||||
|
userPlatform.OsType = model.OsTypeIOS
|
||||||
|
userPlatform.PlatformType = model.PlatformTypeAppStore
|
||||||
|
}
|
||||||
|
|
||||||
|
userUuid := uuid.New().String()
|
||||||
|
id, err := userStore.CreateUser(userUuid, userPlatform)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Registered user %d in database successfully", id)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Register user in database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authUser, err := authStore.CreateUser(*name, *password)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Register auth account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = userStore.SetFacebookId(id, authUser.ID)
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Bound user %d with facebook account %v", id, authUser.Username)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("failed to bind user with facebook account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Account %v created successfully.", *name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/huh/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
gameDBPath = "db/game.db"
|
||||||
|
backupDir = "db/backups"
|
||||||
|
backupSuffix = ".bak"
|
||||||
|
)
|
||||||
|
|
||||||
|
const banner = `
|
||||||
|
_ _____
|
||||||
|
| | _ _ _ _ __ _ _ _ |_ _|___ __ _ _ _
|
||||||
|
| |_| || | ' \/ _` + "`" + ` | '_| | |/ -_)/ _` + "`" + ` | '_|
|
||||||
|
|____\_,_|_||_\__,_|_| |_|\___|\__,_|_|
|
||||||
|
|
||||||
|
╭──────────────────────────────╮
|
||||||
|
│ RESTORE │
|
||||||
|
╰──────────────────────────────╯
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
lipgloss.EnableLegacyWindowsANSI(os.Stdout)
|
||||||
|
lipgloss.EnableLegacyWindowsANSI(os.Stderr)
|
||||||
|
fmt.Print(banner)
|
||||||
|
|
||||||
|
chosen, ok := pickBackup()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !confirmOverwrite(chosen) {
|
||||||
|
fmt.Println(" cancelled — nothing changed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := doRestore(chosen); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " restore failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf(" restored %s from %s\n", gameDBPath, chosen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickBackup() (string, bool) {
|
||||||
|
entries, err := os.ReadDir(backupDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, " no backups found in", backupDir)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
var backups []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() && strings.HasPrefix(e.Name(), "game.db.") && strings.HasSuffix(e.Name(), backupSuffix) {
|
||||||
|
backups = append(backups, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(backups) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, " no backups found in", backupDir)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
sort.Slice(backups, func(i, j int) bool { return backups[i] > backups[j] })
|
||||||
|
|
||||||
|
options := make([]huh.Option[string], 0, len(backups)+1)
|
||||||
|
for _, name := range backups {
|
||||||
|
options = append(options, huh.NewOption(name, name))
|
||||||
|
}
|
||||||
|
options = append(options, huh.NewOption("Cancel", ""))
|
||||||
|
|
||||||
|
var chosen string
|
||||||
|
if err := huh.NewSelect[string]().
|
||||||
|
Title("Pick a backup to restore").
|
||||||
|
Description("db/game.db will be replaced by the chosen file.").
|
||||||
|
Options(options...).
|
||||||
|
Value(&chosen).
|
||||||
|
Run(); err != nil || chosen == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return chosen, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmOverwrite(chosen string) bool {
|
||||||
|
confirm := false
|
||||||
|
if err := huh.NewConfirm().
|
||||||
|
Title("Overwrite db/game.db?").
|
||||||
|
Description(fmt.Sprintf(
|
||||||
|
"This will REPLACE db/game.db with %s.\n"+
|
||||||
|
"Any progress since that backup will be lost.\n"+
|
||||||
|
"(A fresh backup will be taken on the next ./wizard launch.)",
|
||||||
|
chosen)).
|
||||||
|
Affirmative("Yes, restore").
|
||||||
|
Negative("Cancel").
|
||||||
|
Value(&confirm).
|
||||||
|
Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return confirm
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRestore(chosen string) error {
|
||||||
|
src := filepath.Join(backupDir, chosen)
|
||||||
|
if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("%s no longer exists", src)
|
||||||
|
}
|
||||||
|
if err := copyFile(src, gameDBPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = os.Remove(gameDBPath + "-wal")
|
||||||
|
_ = os.Remove(gameDBPath + "-shm")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return out.Sync()
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"charm.land/huh/v2/spinner"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
gameDBPath = "db/game.db"
|
||||||
|
backupDir = "db/backups"
|
||||||
|
backupSuffix = ".bak"
|
||||||
|
backupRetainN = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
func backupGameDB() {
|
||||||
|
if _, err := os.Stat(gameDBPath); errors.Is(err, os.ErrNotExist) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sourceMode {
|
||||||
|
fmt.Println(" Backing up db/game.db...")
|
||||||
|
doBackupGameDB()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = spinner.New().Title(" Backing up db/game.db...").Action(doBackupGameDB).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func doBackupGameDB() {
|
||||||
|
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " failed to create %s: %v\n", backupDir, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := time.Now().UTC().Format("20060102T150405Z")
|
||||||
|
dest := filepath.Join(backupDir, fmt.Sprintf("game.db.%s%s", ts, backupSuffix))
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " failed to open %s: %v\n", gameDBPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
escaped := strings.ReplaceAll(dest, "'", "''")
|
||||||
|
if _, err := db.Exec(fmt.Sprintf("VACUUM INTO '%s'", escaped)); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " VACUUM INTO failed: %v\n", err)
|
||||||
|
_ = os.Remove(dest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneOldBackups()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneOldBackups() {
|
||||||
|
entries, err := os.ReadDir(backupDir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var backups []os.DirEntry
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() && strings.HasPrefix(e.Name(), "game.db.") && strings.HasSuffix(e.Name(), backupSuffix) {
|
||||||
|
backups = append(backups, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(backups) <= backupRetainN {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(backups, func(i, j int) bool { return backups[i].Name() < backups[j].Name() })
|
||||||
|
for _, old := range backups[:len(backups)-backupRetainN] {
|
||||||
|
_ = os.Remove(filepath.Join(backupDir, old.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
-40
@@ -32,13 +32,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Device string `json:"device"`
|
Device string `json:"device"`
|
||||||
Detail string `json:"detail"`
|
Detail string `json:"detail"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
GRPCPort int `json:"grpc_port,omitempty"`
|
GRPCPort int `json:"grpc_port,omitempty"`
|
||||||
CDNPort int `json:"cdn_port,omitempty"`
|
CDNPort int `json:"cdn_port,omitempty"`
|
||||||
AuthPort int `json:"auth_port,omitempty"`
|
AuthPort int `json:"auth_port,omitempty"`
|
||||||
|
AdminPort int `json:"admin_port,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -47,10 +48,13 @@ const (
|
|||||||
defaultAuthPort = 3000
|
defaultAuthPort = 3000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ports.Admin is opt-in: 0 means the admin webhook is not configured by the
|
||||||
|
// wizard at all. Other ports always get a default if unset.
|
||||||
type ports struct {
|
type ports struct {
|
||||||
GRPC int
|
GRPC int
|
||||||
CDN int
|
CDN int
|
||||||
Auth int
|
Auth int
|
||||||
|
Admin int
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -59,6 +63,7 @@ func main() {
|
|||||||
grpcPort := flag.Int("grpc-port", defaultGRPCPort, "gRPC server port")
|
grpcPort := flag.Int("grpc-port", defaultGRPCPort, "gRPC server port")
|
||||||
cdnPort := flag.Int("cdn-port", defaultCDNPort, "CDN server port")
|
cdnPort := flag.Int("cdn-port", defaultCDNPort, "CDN server port")
|
||||||
authPort := flag.Int("auth-port", defaultAuthPort, "auth server port")
|
authPort := flag.Int("auth-port", defaultAuthPort, "auth server port")
|
||||||
|
adminPort := flag.Int("admin-port", 0, "admin webhook port (0 = disabled). Bound on 127.0.0.1; only takes effect when LUNAR_ADMIN_TOKEN is set.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
flagSet := map[string]bool{}
|
flagSet := map[string]bool{}
|
||||||
@@ -69,21 +74,29 @@ func main() {
|
|||||||
|
|
||||||
fmt.Print(banner)
|
fmt.Print(banner)
|
||||||
|
|
||||||
|
sourceMode = isSourceCheckout()
|
||||||
|
|
||||||
if !*setupOnly {
|
if !*setupOnly {
|
||||||
validateAssets()
|
validateAssets()
|
||||||
validateTools()
|
if sourceMode {
|
||||||
validateProtocIncludes()
|
validateTools()
|
||||||
runProtoc()
|
validateProtocIncludes()
|
||||||
runMigrate()
|
runProtoc()
|
||||||
downloadDeps()
|
backupGameDB()
|
||||||
|
runMigrate()
|
||||||
|
downloadDeps()
|
||||||
|
} else {
|
||||||
|
backupGameDB()
|
||||||
|
runMigrateEmbedded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, cfg, firstRun := resolveIP(*preferSaved)
|
ip, cfg, firstRun := resolveIP(*preferSaved)
|
||||||
|
|
||||||
p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, cfg)
|
p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, *adminPort, cfg)
|
||||||
savedPorts := portsFromConfig(cfg)
|
savedPorts := portsFromConfig(cfg)
|
||||||
|
|
||||||
if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth) {
|
if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth || p.Admin != savedPorts.Admin) {
|
||||||
if !warnPortChange(savedPorts, p) {
|
if !warnPortChange(savedPorts, p) {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
@@ -92,6 +105,7 @@ func main() {
|
|||||||
cfg.GRPCPort = p.GRPC
|
cfg.GRPCPort = p.GRPC
|
||||||
cfg.CDNPort = p.CDN
|
cfg.CDNPort = p.CDN
|
||||||
cfg.AuthPort = p.Auth
|
cfg.AuthPort = p.Auth
|
||||||
|
cfg.AdminPort = p.Admin
|
||||||
saveConfig(cfg)
|
saveConfig(cfg)
|
||||||
|
|
||||||
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14)
|
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14)
|
||||||
@@ -101,6 +115,9 @@ func main() {
|
|||||||
fmt.Printf(" %s %s\n", labelStyle.Render("Game server:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.GRPC)))
|
fmt.Printf(" %s %s\n", labelStyle.Render("Game server:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.GRPC)))
|
||||||
fmt.Printf(" %s %s\n", labelStyle.Render("CDN:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.CDN)))
|
fmt.Printf(" %s %s\n", labelStyle.Render("CDN:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.CDN)))
|
||||||
fmt.Printf(" %s %s\n", labelStyle.Render("Auth:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.Auth)))
|
fmt.Printf(" %s %s\n", labelStyle.Render("Auth:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.Auth)))
|
||||||
|
if p.Admin > 0 {
|
||||||
|
fmt.Printf(" %s %s\n", labelStyle.Render("Admin webhook:"), addrStyle.Render(fmt.Sprintf("127.0.0.1:%d", p.Admin)))
|
||||||
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if firstRun || *setupOnly {
|
if firstRun || *setupOnly {
|
||||||
@@ -122,23 +139,68 @@ type assetCheck struct {
|
|||||||
var requiredAssets = []assetCheck{
|
var requiredAssets = []assetCheck{
|
||||||
{"assets", true},
|
{"assets", true},
|
||||||
{"assets/release/20240404193219.bin.e", false},
|
{"assets/release/20240404193219.bin.e", false},
|
||||||
{"assets/revisions/0/list.bin", false},
|
}
|
||||||
{"assets/revisions/0/assetbundle", true},
|
|
||||||
{"assets/revisions/0/resources", true},
|
// assetTrees lists every rev-0 layout the CDN accepts. At least one full set
|
||||||
|
// must be present; users may have only the un-split shared tree, only Android,
|
||||||
|
// only iOS, or any combination.
|
||||||
|
var assetTrees = [][]assetCheck{
|
||||||
|
{
|
||||||
|
{"assets/revisions/0/list.bin", false},
|
||||||
|
{"assets/revisions/0/assetbundle", true},
|
||||||
|
{"assets/revisions/0/resources", true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{"assets/revisions/0/android/list.bin", false},
|
||||||
|
{"assets/revisions/0/android/assetbundle", true},
|
||||||
|
{"assets/revisions/0/android/resources", true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{"assets/revisions/0/ios/list.bin", false},
|
||||||
|
{"assets/revisions/0/ios/assetbundle", true},
|
||||||
|
{"assets/revisions/0/ios/resources", true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAsset(a assetCheck) (missing string, ok bool) {
|
||||||
|
info, err := os.Stat(a.path)
|
||||||
|
if err != nil {
|
||||||
|
return a.path, false
|
||||||
|
}
|
||||||
|
if a.dir && !info.IsDir() {
|
||||||
|
return a.path + string(filepath.Separator), false
|
||||||
|
}
|
||||||
|
if !a.dir && info.IsDir() {
|
||||||
|
return a.path, false
|
||||||
|
}
|
||||||
|
return "", true
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAssets() {
|
func validateAssets() {
|
||||||
var missing []string
|
var missing []string
|
||||||
for _, a := range requiredAssets {
|
for _, a := range requiredAssets {
|
||||||
info, err := os.Stat(a.path)
|
if m, ok := checkAsset(a); !ok {
|
||||||
if err != nil {
|
missing = append(missing, m)
|
||||||
missing = append(missing, a.path)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if a.dir && !info.IsDir() {
|
}
|
||||||
missing = append(missing, a.path+string(filepath.Separator))
|
|
||||||
} else if !a.dir && info.IsDir() {
|
var treeMissing [][]string
|
||||||
missing = append(missing, a.path)
|
anyTreeOK := false
|
||||||
|
for _, group := range assetTrees {
|
||||||
|
var groupMissing []string
|
||||||
|
for _, a := range group {
|
||||||
|
if m, ok := checkAsset(a); !ok {
|
||||||
|
groupMissing = append(groupMissing, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(groupMissing) == 0 {
|
||||||
|
anyTreeOK = true
|
||||||
|
}
|
||||||
|
treeMissing = append(treeMissing, groupMissing)
|
||||||
|
}
|
||||||
|
if !anyTreeOK {
|
||||||
|
for _, gm := range treeMissing {
|
||||||
|
missing = append(missing, gm...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +223,8 @@ func validateAssets() {
|
|||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(dimStyle.Render(" Place the extracted game assets under server/assets/ and try again."))
|
b.WriteString(dimStyle.Render(" Place the extracted game assets under server/assets/ and try again."))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
b.WriteString(dimStyle.Render(" At least one of assets/revisions/0/, assets/revisions/0/android/, or assets/revisions/0/ios/ must be fully present."))
|
||||||
|
b.WriteString("\n")
|
||||||
b.WriteString(dimStyle.Render(" Get them from ") + hlStyle.Render("#resources") + dimStyle.Render(" on Discord: ") + hlStyle.Hyperlink("https://discord.com/invite/MZAf5aVkJG").Render("https://discord.com/invite/MZAf5aVkJG"))
|
b.WriteString(dimStyle.Render(" Get them from ") + hlStyle.Render("#resources") + dimStyle.Render(" on Discord: ") + hlStyle.Hyperlink("https://discord.com/invite/MZAf5aVkJG").Render("https://discord.com/invite/MZAf5aVkJG"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
@@ -430,6 +494,22 @@ func warnPortChange(old, new ports) bool {
|
|||||||
}
|
}
|
||||||
return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", label+":", oldP, newP))
|
return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", label+":", oldP, newP))
|
||||||
}
|
}
|
||||||
|
// Admin formatting handles the disabled (0) state since the port is
|
||||||
|
// opt-in and we don't want to display "0" to the user.
|
||||||
|
adminLine := func(oldP, newP int) (string, bool) {
|
||||||
|
switch {
|
||||||
|
case oldP == 0 && newP == 0:
|
||||||
|
return "", false
|
||||||
|
case oldP == 0 && newP != 0:
|
||||||
|
return hlStyle.Render(fmt.Sprintf(" %-7s disabled → %d", "Admin:", newP)), true
|
||||||
|
case oldP != 0 && newP == 0:
|
||||||
|
return hlStyle.Render(fmt.Sprintf(" %-7s %d → disabled", "Admin:", oldP)), true
|
||||||
|
case oldP == newP:
|
||||||
|
return dimStyle.Render(fmt.Sprintf(" %-7s %d (unchanged)", "Admin:", oldP)), true
|
||||||
|
default:
|
||||||
|
return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", "Admin:", oldP, newP)), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -440,7 +520,12 @@ func warnPortChange(old, new ports) bool {
|
|||||||
b.WriteString(portLine("CDN", old.CDN, new.CDN))
|
b.WriteString(portLine("CDN", old.CDN, new.CDN))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(portLine("Auth", old.Auth, new.Auth))
|
b.WriteString(portLine("Auth", old.Auth, new.Auth))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n")
|
||||||
|
if line, show := adminLine(old.Admin, new.Admin); show {
|
||||||
|
b.WriteString(line)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
b.WriteString(dimStyle.Render(" Your APK was patched for the old ports. You may need to re-patch."))
|
b.WriteString(dimStyle.Render(" Your APK was patched for the old ports. You may need to re-patch."))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
fmt.Print(b.String())
|
fmt.Print(b.String())
|
||||||
@@ -774,7 +859,7 @@ func loadConfig() (config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func portsFromConfig(cfg config) ports {
|
func portsFromConfig(cfg config) ports {
|
||||||
p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort}
|
p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort, Admin: cfg.AdminPort}
|
||||||
if p.GRPC == 0 {
|
if p.GRPC == 0 {
|
||||||
p.GRPC = defaultGRPCPort
|
p.GRPC = defaultGRPCPort
|
||||||
}
|
}
|
||||||
@@ -784,10 +869,11 @@ func portsFromConfig(cfg config) ports {
|
|||||||
if p.Auth == 0 {
|
if p.Auth == 0 {
|
||||||
p.Auth = defaultAuthPort
|
p.Auth = defaultAuthPort
|
||||||
}
|
}
|
||||||
|
// Admin is opt-in: leave 0 = disabled.
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, saved config) ports {
|
func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag, adminFlag int, saved config) ports {
|
||||||
resolve := func(name string, flagVal, savedVal, defaultVal int) int {
|
resolve := func(name string, flagVal, savedVal, defaultVal int) int {
|
||||||
if flagSet[name] {
|
if flagSet[name] {
|
||||||
return flagVal
|
return flagVal
|
||||||
@@ -801,6 +887,9 @@ func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, save
|
|||||||
GRPC: resolve("grpc-port", grpcFlag, saved.GRPCPort, defaultGRPCPort),
|
GRPC: resolve("grpc-port", grpcFlag, saved.GRPCPort, defaultGRPCPort),
|
||||||
CDN: resolve("cdn-port", cdnFlag, saved.CDNPort, defaultCDNPort),
|
CDN: resolve("cdn-port", cdnFlag, saved.CDNPort, defaultCDNPort),
|
||||||
Auth: resolve("auth-port", authFlag, saved.AuthPort, defaultAuthPort),
|
Auth: resolve("auth-port", authFlag, saved.AuthPort, defaultAuthPort),
|
||||||
|
// defaultVal=0 keeps admin opt-in: never enabled unless --admin-port
|
||||||
|
// is passed or a non-zero value was previously saved.
|
||||||
|
Admin: resolve("admin-port", adminFlag, saved.AdminPort, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,21 +908,30 @@ func launchDev(ip string, p ports) {
|
|||||||
}
|
}
|
||||||
devBin := filepath.Join("bin", "dev"+ext)
|
devBin := filepath.Join("bin", "dev"+ext)
|
||||||
|
|
||||||
_ = spinner.New().Title(" Building services...").Action(func() {
|
if sourceMode {
|
||||||
if err := os.MkdirAll("bin", 0755); err != nil {
|
_ = spinner.New().Title(" Building services...").Action(func() {
|
||||||
fmt.Fprintf(os.Stderr, " Failed to create bin/: %v\n", err)
|
if err := os.MkdirAll("bin", 0755); err != nil {
|
||||||
os.Exit(1)
|
fmt.Fprintf(os.Stderr, " Failed to create bin/: %v\n", err)
|
||||||
}
|
os.Exit(1)
|
||||||
runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev")
|
}
|
||||||
}).Run()
|
runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev")
|
||||||
|
}).Run()
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.Command(devBin,
|
devArgs := []string{
|
||||||
"--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC),
|
"--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC),
|
||||||
"--grpc.public-addr", fmt.Sprintf("%s:%d", ip, p.GRPC),
|
"--grpc.public-addr", fmt.Sprintf("%s:%d", ip, p.GRPC),
|
||||||
"--cdn.listen", fmt.Sprintf("0.0.0.0:%d", p.CDN),
|
"--cdn.listen", fmt.Sprintf("0.0.0.0:%d", p.CDN),
|
||||||
"--cdn.public-addr", fmt.Sprintf("%s:%d", ip, p.CDN),
|
"--cdn.public-addr", fmt.Sprintf("%s:%d", ip, p.CDN),
|
||||||
"--auth.listen", fmt.Sprintf("0.0.0.0:%d", p.Auth),
|
"--auth.listen", fmt.Sprintf("0.0.0.0:%d", p.Auth),
|
||||||
)
|
}
|
||||||
|
// Bind admin on loopback only — the wizard is for local dev, and the
|
||||||
|
// webhook should never be exposed to the LAN by accident. Operators who
|
||||||
|
// want a different bind can run cmd/dev directly with --admin.listen.
|
||||||
|
if p.Admin > 0 {
|
||||||
|
devArgs = append(devArgs, "--admin.listen", fmt.Sprintf("127.0.0.1:%d", p.Admin))
|
||||||
|
}
|
||||||
|
cmd := exec.Command(devBin, devArgs...)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"lunar-tear/server/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sourceMode bool
|
||||||
|
|
||||||
|
func isSourceCheckout() bool {
|
||||||
|
if _, err := os.Stat("go.mod"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("proto"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrateEmbedded() {
|
||||||
|
fmt.Println(" Running migrations...")
|
||||||
|
if err := os.MkdirAll("db", 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " Failed to create db/: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " open db: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
if err := migrations.Up(context.Background(), db); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " migration failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,14 @@ services:
|
|||||||
LUNAR_PUBLIC_ADDR: 127.0.0.1:8003
|
LUNAR_PUBLIC_ADDR: 127.0.0.1:8003
|
||||||
LUNAR_OCTO_URL: http://cdn:8080
|
LUNAR_OCTO_URL: http://cdn:8080
|
||||||
LUNAR_AUTH_URL: http://auth:3000
|
LUNAR_AUTH_URL: http://auth:3000
|
||||||
|
LUNAR_ADMIN_LISTEN: 0.0.0.0:8082
|
||||||
|
LUNAR_ADMIN_TOKEN: ${LUNAR_ADMIN_TOKEN:-}
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/opt/lunar-tear/db
|
- ./db:/opt/lunar-tear/db
|
||||||
- ./assets:/opt/lunar-tear/assets
|
- ./assets:/opt/lunar-tear/assets
|
||||||
ports:
|
ports:
|
||||||
- 8003:8003
|
- 8003:8003
|
||||||
|
- 127.0.0.1:8082:8082
|
||||||
depends_on:
|
depends_on:
|
||||||
- cdn
|
- cdn
|
||||||
- auth
|
- auth
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ if [ -n "${LUNAR_AUTH_URL}" ]; then
|
|||||||
AUTH_FLAG="--auth-url ${LUNAR_AUTH_URL}"
|
AUTH_FLAG="--auth-url ${LUNAR_AUTH_URL}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ADMIN_FLAG=""
|
||||||
|
if [ -n "${LUNAR_ADMIN_LISTEN}" ]; then
|
||||||
|
ADMIN_FLAG="--admin-listen ${LUNAR_ADMIN_LISTEN}"
|
||||||
|
fi
|
||||||
|
|
||||||
exec ./lunar-tear \
|
exec ./lunar-tear \
|
||||||
--listen "${LUNAR_LISTEN:-0.0.0.0:443}" \
|
--listen "${LUNAR_LISTEN:-0.0.0.0:443}" \
|
||||||
--public-addr "${LUNAR_PUBLIC_ADDR}" \
|
--public-addr "${LUNAR_PUBLIC_ADDR}" \
|
||||||
--octo-url "${LUNAR_OCTO_URL}" \
|
--octo-url "${LUNAR_OCTO_URL}" \
|
||||||
${AUTH_FLAG}
|
${AUTH_FLAG} \
|
||||||
|
${ADMIN_FLAG}
|
||||||
|
|||||||
+11
-7
@@ -7,12 +7,12 @@ require (
|
|||||||
github.com/pierrec/lz4/v4 v4.1.26
|
github.com/pierrec/lz4/v4 v4.1.26
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/net v0.52.0
|
golang.org/x/net v0.53.0
|
||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.43.0
|
||||||
golang.org/x/term v0.42.0
|
golang.org/x/term v0.42.0
|
||||||
google.golang.org/grpc v1.79.1
|
google.golang.org/grpc v1.80.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
modernc.org/sqlite v1.48.2
|
modernc.org/sqlite v1.49.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -34,21 +34,25 @@ require (
|
|||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pressly/goose/v3 v3.27.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.72.1 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,8 +54,12 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
|
|||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||||
|
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
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/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
@@ -66,10 +70,14 @@ 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
|
||||||
|
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
@@ -82,20 +90,28 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
|||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
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 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
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/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
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/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 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
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 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
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/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -109,18 +125,25 @@ 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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/cc/v4 v4.28.1 h1:XpLbkYVQ24E8tX5u8+yWGvaxerxkR/S4zqxI8ZoSBuc=
|
||||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
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/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||||
|
modernc.org/ccgo/v4 v4.33.0 h1:dspBCm75jsj8Y/ufwAMVfe375L2iYdMyQ2QG/v3hL54=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
@@ -131,16 +154,21 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
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 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
|
modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0=
|
||||||
|
modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
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 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
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 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
|
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||||
|
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
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 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tokenTTL = 24 * time.Hour
|
const TokenTTL = 24 * time.Hour
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTokenInvalid = errors.New("invalid token")
|
ErrTokenInvalid = errors.New("invalid token")
|
||||||
@@ -38,7 +38,7 @@ func (t *TokenService) Generate(user AuthUser) (string, error) {
|
|||||||
Sub: user.ID,
|
Sub: user.ID,
|
||||||
Name: user.Username,
|
Name: user.Username,
|
||||||
Iat: now,
|
Iat: now,
|
||||||
Exp: now + int64(tokenTTL.Seconds()),
|
Exp: now + int64(TokenTTL.Seconds()),
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(claims)
|
payload, err := json.Marshal(claims)
|
||||||
@@ -49,22 +49,17 @@ type RewardItem struct {
|
|||||||
Count int32
|
Count int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type BigHuntWeeklyRewardKey struct {
|
|
||||||
ScheduleId int32
|
|
||||||
AttributeType int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type BigHuntCatalog struct {
|
type BigHuntCatalog struct {
|
||||||
BossQuestById map[int32]BigHuntBossQuestRow
|
BossQuestById map[int32]BigHuntBossQuestRow
|
||||||
QuestById map[int32]BigHuntQuestRow
|
QuestById map[int32]BigHuntQuestRow
|
||||||
ScoreCoefficients map[int32]int32
|
ScoreCoefficients map[int32]int32
|
||||||
BossByBossId map[int32]BigHuntBossRow
|
BossByBossId map[int32]BigHuntBossRow
|
||||||
GradeThresholds map[int32][]GradeThreshold
|
GradeThresholds map[int32][]GradeThreshold
|
||||||
ActiveScheduleId int32
|
ActiveScheduleId int32
|
||||||
ScoreRewardSchedules map[int32][]ScoreRewardScheduleEntry
|
ScoreRewardSchedules map[int32][]ScoreRewardScheduleEntry
|
||||||
ScoreRewardThresholds map[int32][]ScoreRewardThreshold
|
ScoreRewardThresholds map[int32][]ScoreRewardThreshold
|
||||||
RewardItems map[int32][]RewardItem
|
RewardItems map[int32][]RewardItem
|
||||||
WeeklyRewardSchedules map[BigHuntWeeklyRewardKey][]ScoreRewardScheduleEntry
|
WeeklyRewardSchedulesByAttr map[int32][]ScoreRewardScheduleEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BigHuntCatalog) ResolveActiveScoreRewardGroupId(scheduleId int32, nowMillis int64) int32 {
|
func (c *BigHuntCatalog) ResolveActiveScoreRewardGroupId(scheduleId int32, nowMillis int64) int32 {
|
||||||
@@ -80,8 +75,8 @@ func (c *BigHuntCatalog) ResolveActiveScoreRewardGroupId(scheduleId int32, nowMi
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BigHuntCatalog) ResolveActiveWeeklyRewardGroupId(key BigHuntWeeklyRewardKey, nowMillis int64) int32 {
|
func (c *BigHuntCatalog) ResolveActiveWeeklyRewardGroupIdByAttr(attributeType int32, nowMillis int64) int32 {
|
||||||
entries := c.WeeklyRewardSchedules[key]
|
entries := c.WeeklyRewardSchedulesByAttr[attributeType]
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if nowMillis >= e.StartDatetime {
|
if nowMillis >= e.StartDatetime {
|
||||||
return e.BigHuntScoreRewardGroupId
|
return e.BigHuntScoreRewardGroupId
|
||||||
@@ -264,20 +259,16 @@ func LoadBigHuntCatalog() *BigHuntCatalog {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("load big hunt weekly attribute score reward group schedule table: %v", err)
|
log.Fatalf("load big hunt weekly attribute score reward group schedule table: %v", err)
|
||||||
}
|
}
|
||||||
weeklyRewardSchedules := make(map[BigHuntWeeklyRewardKey][]ScoreRewardScheduleEntry)
|
weeklyRewardSchedulesByAttr := make(map[int32][]ScoreRewardScheduleEntry)
|
||||||
for _, r := range weeklySchedRows {
|
for _, r := range weeklySchedRows {
|
||||||
key := BigHuntWeeklyRewardKey{
|
weeklyRewardSchedulesByAttr[r.AttributeType] = append(weeklyRewardSchedulesByAttr[r.AttributeType], ScoreRewardScheduleEntry{
|
||||||
ScheduleId: r.BigHuntWeeklyAttributeScoreRewardGroupScheduleId,
|
|
||||||
AttributeType: r.AttributeType,
|
|
||||||
}
|
|
||||||
weeklyRewardSchedules[key] = append(weeklyRewardSchedules[key], ScoreRewardScheduleEntry{
|
|
||||||
BigHuntScoreRewardGroupId: r.BigHuntScoreRewardGroupId,
|
BigHuntScoreRewardGroupId: r.BigHuntScoreRewardGroupId,
|
||||||
StartDatetime: r.StartDatetime,
|
StartDatetime: r.StartDatetime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for k := range weeklyRewardSchedules {
|
for k := range weeklyRewardSchedulesByAttr {
|
||||||
sort.Slice(weeklyRewardSchedules[k], func(i, j int) bool {
|
sort.Slice(weeklyRewardSchedulesByAttr[k], func(i, j int) bool {
|
||||||
return weeklyRewardSchedules[k][i].StartDatetime > weeklyRewardSchedules[k][j].StartDatetime
|
return weeklyRewardSchedulesByAttr[k][i].StartDatetime > weeklyRewardSchedulesByAttr[k][j].StartDatetime
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,15 +276,15 @@ func LoadBigHuntCatalog() *BigHuntCatalog {
|
|||||||
len(bossQuestById), len(questById), len(bossByBossId), len(scoreCoefficients), len(rewardItems), activeScheduleId)
|
len(bossQuestById), len(questById), len(bossByBossId), len(scoreCoefficients), len(rewardItems), activeScheduleId)
|
||||||
|
|
||||||
return &BigHuntCatalog{
|
return &BigHuntCatalog{
|
||||||
BossQuestById: bossQuestById,
|
BossQuestById: bossQuestById,
|
||||||
QuestById: questById,
|
QuestById: questById,
|
||||||
ScoreCoefficients: scoreCoefficients,
|
ScoreCoefficients: scoreCoefficients,
|
||||||
BossByBossId: bossByBossId,
|
BossByBossId: bossByBossId,
|
||||||
GradeThresholds: gradeThresholds,
|
GradeThresholds: gradeThresholds,
|
||||||
ActiveScheduleId: activeScheduleId,
|
ActiveScheduleId: activeScheduleId,
|
||||||
ScoreRewardSchedules: scoreRewardSchedules,
|
ScoreRewardSchedules: scoreRewardSchedules,
|
||||||
ScoreRewardThresholds: scoreRewardThresholds,
|
ScoreRewardThresholds: scoreRewardThresholds,
|
||||||
RewardItems: rewardItems,
|
RewardItems: rewardItems,
|
||||||
WeeklyRewardSchedules: weeklyRewardSchedules,
|
WeeklyRewardSchedulesByAttr: weeklyRewardSchedulesByAttr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ConsumableItemCatalog struct {
|
type ConsumableItemCatalog struct {
|
||||||
All map[int32]EntityMConsumableItem
|
All map[int32]EntityMConsumableItem
|
||||||
|
Effects map[int32][]EntityMConsumableItemEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConsumableItemCatalog() (*ConsumableItemCatalog, error) {
|
func LoadConsumableItemCatalog() (*ConsumableItemCatalog, error) {
|
||||||
@@ -15,12 +16,20 @@ func LoadConsumableItemCatalog() (*ConsumableItemCatalog, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load consumable item table: %w", err)
|
return nil, fmt.Errorf("load consumable item table: %w", err)
|
||||||
}
|
}
|
||||||
|
effects, err := utils.ReadTable[EntityMConsumableItemEffect]("m_consumable_item_effect")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load consumable item effect table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
catalog := &ConsumableItemCatalog{
|
catalog := &ConsumableItemCatalog{
|
||||||
All: make(map[int32]EntityMConsumableItem, len(rows)),
|
All: make(map[int32]EntityMConsumableItem, len(rows)),
|
||||||
|
Effects: make(map[int32][]EntityMConsumableItemEffect, len(effects)),
|
||||||
}
|
}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
catalog.All[row.ConsumableItemId] = row
|
catalog.All[row.ConsumableItemId] = row
|
||||||
}
|
}
|
||||||
|
for _, e := range effects {
|
||||||
|
catalog.Effects[e.ConsumableItemId] = append(catalog.Effects[e.ConsumableItemId], e)
|
||||||
|
}
|
||||||
return catalog, nil
|
return catalog, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
package masterdata
|
||||||
|
|
||||||
|
// Source: based on public_costumes_link_export_2026-05-21_142314.csv by @Keziah,
|
||||||
|
// with each weapon resolved to its m_weapon_evolution_group EvolutionOrder=1 root
|
||||||
|
var costumeWeaponPairings = map[int32]int32{
|
||||||
|
10100: 101001,
|
||||||
|
10101: 101011,
|
||||||
|
10102: 101021,
|
||||||
|
10103: 101031,
|
||||||
|
10104: 101041,
|
||||||
|
10105: 101051,
|
||||||
|
10106: 101061,
|
||||||
|
10107: 101071,
|
||||||
|
10108: 101081,
|
||||||
|
10109: 101091,
|
||||||
|
10110: 101101,
|
||||||
|
10111: 101121,
|
||||||
|
10112: 101111,
|
||||||
|
10113: 101131,
|
||||||
|
10114: 101141,
|
||||||
|
10115: 101151,
|
||||||
|
10116: 101161,
|
||||||
|
10117: 101171,
|
||||||
|
10118: 101181,
|
||||||
|
10119: 101191,
|
||||||
|
10120: 101201,
|
||||||
|
10121: 101211,
|
||||||
|
21000: 210161,
|
||||||
|
21001: 210031,
|
||||||
|
21002: 210171,
|
||||||
|
21003: 210181,
|
||||||
|
21004: 210191,
|
||||||
|
21005: 210271,
|
||||||
|
22000: 220081,
|
||||||
|
22001: 220021,
|
||||||
|
22002: 220051,
|
||||||
|
22003: 220061,
|
||||||
|
22004: 220141,
|
||||||
|
22005: 220161,
|
||||||
|
22006: 220181,
|
||||||
|
22007: 220191,
|
||||||
|
22008: 220211,
|
||||||
|
22009: 220231,
|
||||||
|
22010: 220241,
|
||||||
|
23000: 230001,
|
||||||
|
23001: 230021,
|
||||||
|
23004: 230051,
|
||||||
|
23005: 230151,
|
||||||
|
23006: 230261,
|
||||||
|
23007: 230271,
|
||||||
|
23008: 230281,
|
||||||
|
24000: 240091,
|
||||||
|
24001: 240121,
|
||||||
|
24002: 240131,
|
||||||
|
24003: 240011,
|
||||||
|
24004: 240081,
|
||||||
|
24005: 240201,
|
||||||
|
24006: 240221,
|
||||||
|
24007: 240241,
|
||||||
|
24008: 240271,
|
||||||
|
24009: 240311,
|
||||||
|
25000: 250121,
|
||||||
|
25001: 250071,
|
||||||
|
25002: 250011,
|
||||||
|
25003: 250151,
|
||||||
|
25005: 250021,
|
||||||
|
25006: 250171,
|
||||||
|
25007: 250221,
|
||||||
|
25008: 250231,
|
||||||
|
25009: 250261,
|
||||||
|
31000: 310081,
|
||||||
|
31001: 310061,
|
||||||
|
31002: 310021,
|
||||||
|
31004: 310191,
|
||||||
|
31005: 310211,
|
||||||
|
31008: 310221,
|
||||||
|
31009: 310241,
|
||||||
|
31010: 310261,
|
||||||
|
31011: 310291,
|
||||||
|
31013: 310321,
|
||||||
|
31014: 310331,
|
||||||
|
31015: 310371,
|
||||||
|
31016: 310401,
|
||||||
|
31017: 310411,
|
||||||
|
31018: 310421,
|
||||||
|
31019: 310431,
|
||||||
|
31020: 310461,
|
||||||
|
31021: 310471,
|
||||||
|
31022: 310481,
|
||||||
|
31023: 310511,
|
||||||
|
31024: 310531,
|
||||||
|
31025: 310541,
|
||||||
|
31026: 310551,
|
||||||
|
31027: 310571,
|
||||||
|
31028: 310591,
|
||||||
|
31029: 310621,
|
||||||
|
31030: 310641,
|
||||||
|
31031: 310661,
|
||||||
|
31032: 310691,
|
||||||
|
31033: 310701,
|
||||||
|
31034: 310711,
|
||||||
|
32000: 320081,
|
||||||
|
32001: 320041,
|
||||||
|
32002: 320011,
|
||||||
|
32003: 320111,
|
||||||
|
32004: 320051,
|
||||||
|
32005: 320141,
|
||||||
|
32006: 320151,
|
||||||
|
32007: 320171,
|
||||||
|
32008: 320181,
|
||||||
|
32009: 320201,
|
||||||
|
32011: 320231,
|
||||||
|
32012: 320241,
|
||||||
|
32013: 320271,
|
||||||
|
32014: 320281,
|
||||||
|
32015: 320301,
|
||||||
|
32016: 320331,
|
||||||
|
32017: 320351,
|
||||||
|
32018: 320371,
|
||||||
|
32019: 320381,
|
||||||
|
32020: 320391,
|
||||||
|
32021: 320421,
|
||||||
|
32022: 320431,
|
||||||
|
32023: 320441,
|
||||||
|
32024: 320451,
|
||||||
|
32025: 320461,
|
||||||
|
32026: 320471,
|
||||||
|
32027: 320501,
|
||||||
|
32028: 320531,
|
||||||
|
32029: 320541,
|
||||||
|
32030: 320551,
|
||||||
|
32031: 320561,
|
||||||
|
32032: 320581,
|
||||||
|
32033: 320601,
|
||||||
|
32034: 320611,
|
||||||
|
32035: 320621,
|
||||||
|
32036: 320641,
|
||||||
|
33000: 330001,
|
||||||
|
33001: 330121,
|
||||||
|
33002: 330011,
|
||||||
|
33003: 330021,
|
||||||
|
33005: 330161,
|
||||||
|
33006: 330171,
|
||||||
|
33007: 330191,
|
||||||
|
33009: 330211,
|
||||||
|
33010: 330231,
|
||||||
|
33011: 330261,
|
||||||
|
33012: 330281,
|
||||||
|
33013: 330321,
|
||||||
|
33014: 330341,
|
||||||
|
33015: 330381,
|
||||||
|
33016: 330401,
|
||||||
|
33017: 330421,
|
||||||
|
33018: 330451,
|
||||||
|
33019: 330471,
|
||||||
|
33020: 330501,
|
||||||
|
33021: 330521,
|
||||||
|
33022: 330541,
|
||||||
|
33023: 330551,
|
||||||
|
33024: 330561,
|
||||||
|
33025: 330571,
|
||||||
|
33026: 330581,
|
||||||
|
33027: 330591,
|
||||||
|
33028: 330601,
|
||||||
|
33029: 330631,
|
||||||
|
33030: 330641,
|
||||||
|
33031: 330671,
|
||||||
|
33032: 330691,
|
||||||
|
33033: 330701,
|
||||||
|
34000: 340011,
|
||||||
|
34001: 340121,
|
||||||
|
34002: 340151,
|
||||||
|
34003: 340161,
|
||||||
|
34004: 340131,
|
||||||
|
34005: 340071,
|
||||||
|
34009: 340231,
|
||||||
|
34010: 340241,
|
||||||
|
34011: 340251,
|
||||||
|
34012: 340261,
|
||||||
|
34013: 340281,
|
||||||
|
34014: 340291,
|
||||||
|
34015: 340301,
|
||||||
|
34016: 340321,
|
||||||
|
34017: 340341,
|
||||||
|
34018: 340351,
|
||||||
|
34019: 340361,
|
||||||
|
34020: 340381,
|
||||||
|
34021: 340391,
|
||||||
|
34022: 340411,
|
||||||
|
34023: 340421,
|
||||||
|
34024: 340441,
|
||||||
|
34025: 340451,
|
||||||
|
34026: 340461,
|
||||||
|
34027: 340491,
|
||||||
|
34028: 340501,
|
||||||
|
34029: 340521,
|
||||||
|
34030: 340531,
|
||||||
|
34031: 340541,
|
||||||
|
34032: 340571,
|
||||||
|
34033: 340601,
|
||||||
|
34034: 340611,
|
||||||
|
34035: 340621,
|
||||||
|
34036: 340631,
|
||||||
|
34037: 340651,
|
||||||
|
34038: 340681,
|
||||||
|
34039: 340701,
|
||||||
|
34040: 340721,
|
||||||
|
34041: 340731,
|
||||||
|
34042: 340751,
|
||||||
|
34043: 340761,
|
||||||
|
34044: 340781,
|
||||||
|
34045: 340801,
|
||||||
|
34046: 340831,
|
||||||
|
34047: 340861,
|
||||||
|
34048: 340871,
|
||||||
|
35000: 350011,
|
||||||
|
35001: 350161,
|
||||||
|
35002: 350141,
|
||||||
|
35003: 350061,
|
||||||
|
35005: 350081,
|
||||||
|
35006: 350121,
|
||||||
|
35008: 350181,
|
||||||
|
35009: 350191,
|
||||||
|
35010: 350221,
|
||||||
|
35011: 350231,
|
||||||
|
35012: 350261,
|
||||||
|
35013: 350271,
|
||||||
|
35014: 350301,
|
||||||
|
35015: 350321,
|
||||||
|
35016: 350341,
|
||||||
|
35017: 350361,
|
||||||
|
35018: 350391,
|
||||||
|
35019: 350401,
|
||||||
|
35020: 350411,
|
||||||
|
35021: 350431,
|
||||||
|
35022: 350441,
|
||||||
|
35023: 350451,
|
||||||
|
35024: 350461,
|
||||||
|
35025: 350491,
|
||||||
|
35026: 350501,
|
||||||
|
35027: 350511,
|
||||||
|
35028: 350531,
|
||||||
|
35029: 350551,
|
||||||
|
35030: 350601,
|
||||||
|
35031: 350621,
|
||||||
|
35032: 350631,
|
||||||
|
35033: 350641,
|
||||||
|
35034: 350661,
|
||||||
|
35035: 350681,
|
||||||
|
35036: 350691,
|
||||||
|
35037: 350701,
|
||||||
|
35038: 350711,
|
||||||
|
41000: 410031,
|
||||||
|
41001: 410071,
|
||||||
|
41002: 410111,
|
||||||
|
41003: 410151,
|
||||||
|
42000: 420031,
|
||||||
|
42001: 420071,
|
||||||
|
42002: 420111,
|
||||||
|
42003: 420151,
|
||||||
|
43000: 430031,
|
||||||
|
43001: 430071,
|
||||||
|
43002: 430111,
|
||||||
|
43003: 430151,
|
||||||
|
44000: 440031,
|
||||||
|
44001: 440071,
|
||||||
|
44002: 440111,
|
||||||
|
44003: 440151,
|
||||||
|
44004: 440191,
|
||||||
|
45000: 450031,
|
||||||
|
45001: 450071,
|
||||||
|
45002: 450111,
|
||||||
|
45003: 450151,
|
||||||
|
51001: 510011,
|
||||||
|
51002: 510021,
|
||||||
|
51003: 510031,
|
||||||
|
52001: 520011,
|
||||||
|
52002: 520021,
|
||||||
|
53001: 530011,
|
||||||
|
53002: 530021,
|
||||||
|
54001: 540011,
|
||||||
|
54002: 540021,
|
||||||
|
55001: 550011,
|
||||||
|
55002: 550021,
|
||||||
|
55003: 550031,
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -124,9 +124,9 @@ func LoadGachaCatalog() ([]store.GachaCatalogEntry, map[int32]GachaMedalInfo, er
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for groupId, steps := range stepupSteps {
|
for _, steps := range stepupSteps {
|
||||||
first := steps[0]
|
first := steps[0]
|
||||||
gachaId := groupId
|
gachaId := first.DestinationDomainId
|
||||||
|
|
||||||
medal := gachaToMedal[first.DestinationDomainId]
|
medal := gachaToMedal[first.DestinationDomainId]
|
||||||
medalId := medal.GachaMedalId
|
medalId := medal.GachaMedalId
|
||||||
@@ -154,7 +154,7 @@ func LoadGachaCatalog() ([]store.GachaCatalogEntry, map[int32]GachaMedalInfo, er
|
|||||||
GachaDecorationType: model.GachaDecorationFestival,
|
GachaDecorationType: model.GachaDecorationFestival,
|
||||||
SortOrder: first.SortOrderDesc,
|
SortOrder: first.SortOrderDesc,
|
||||||
BannerAssetName: first.BannerAssetName,
|
BannerAssetName: first.BannerAssetName,
|
||||||
GroupId: groupId,
|
GroupId: gachaId,
|
||||||
CeilingCount: model.PityCeilingCount,
|
CeilingCount: model.PityCeilingCount,
|
||||||
PricePhases: pricePhases,
|
PricePhases: pricePhases,
|
||||||
MaxStepNumber: maxStep,
|
MaxStepNumber: maxStep,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package masterdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
@@ -34,16 +33,31 @@ type ShopFeaturedEntry struct {
|
|||||||
WeaponId int32
|
WeaponId int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CatalogTerm struct {
|
||||||
|
TermId int32
|
||||||
|
StartDatetime int64
|
||||||
|
Costumes []GachaPoolItem
|
||||||
|
Weapons []GachaPoolItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// StandardPoolTermId is the catalog term whose items form the cross-banner
|
||||||
|
// standard pool (term 1 holds the launch starter set).
|
||||||
|
const StandardPoolTermId int32 = 1
|
||||||
|
|
||||||
type GachaCatalog struct {
|
type GachaCatalog struct {
|
||||||
CostumesByRarity map[int32][]GachaPoolItem
|
CostumesByRarity map[int32][]GachaPoolItem
|
||||||
WeaponsByRarity map[int32][]GachaPoolItem
|
WeaponsByRarity map[int32][]GachaPoolItem
|
||||||
Materials []GachaPoolItem
|
StandardCostumesByRarity map[int32][]GachaPoolItem
|
||||||
CostumeById map[int32]GachaPoolItem
|
StandardWeaponsByRarity map[int32][]GachaPoolItem
|
||||||
WeaponById map[int32]GachaPoolItem
|
Materials []GachaPoolItem
|
||||||
CostumeWeaponMap map[int32]int32 // costumeId -> paired weaponId
|
CostumeById map[int32]GachaPoolItem
|
||||||
FeaturedByGacha map[int32]FeaturedSet
|
WeaponById map[int32]GachaPoolItem
|
||||||
BannerPools map[int32]*BannerPool
|
CostumeWeaponMap map[int32]int32 // costumeId -> paired weaponId
|
||||||
ShopFeaturedByMedal map[int32][]ShopFeaturedEntry // consumableId -> paired entries
|
FeaturedByGacha map[int32]FeaturedSet
|
||||||
|
BannerPools map[int32]*BannerPool
|
||||||
|
ShopFeaturedByMedal map[int32][]ShopFeaturedEntry // consumableId -> paired entries
|
||||||
|
TermById map[int32]*CatalogTerm
|
||||||
|
TermsByStartDatetime map[int64][]*CatalogTerm
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadGachaPool() (*GachaCatalog, error) {
|
func LoadGachaPool() (*GachaCatalog, error) {
|
||||||
@@ -73,42 +87,76 @@ func LoadGachaPool() (*GachaCatalog, error) {
|
|||||||
}
|
}
|
||||||
evolvedWeapons := buildEvolvedWeaponSet(evoGroupRows)
|
evolvedWeapons := buildEvolvedWeaponSet(evoGroupRows)
|
||||||
|
|
||||||
|
terms, err := utils.ReadTable[EntityMCatalogTerm]("m_catalog_term")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load catalog term table: %w", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
sceneGrants, err := utils.ReadTable[EntityMUserQuestSceneGrantPossession]("m_user_quest_scene_grant_possession")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load user quest scene grant possession table: %w", err)
|
||||||
|
}
|
||||||
|
missionRewardRows, err := utils.ReadTable[EntityMMissionReward]("m_mission_reward")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load mission reward table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
questGrantedCostumes := make(map[int32]bool)
|
||||||
|
questGrantedWeapons := make(map[int32]bool)
|
||||||
|
collectGrant := func(possType, possId int32) {
|
||||||
|
switch possType {
|
||||||
|
case int32(model.PossessionTypeCostume):
|
||||||
|
questGrantedCostumes[possId] = true
|
||||||
|
case int32(model.PossessionTypeWeapon):
|
||||||
|
questGrantedWeapons[possId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range firstClearRewards {
|
||||||
|
collectGrant(r.PossessionType, r.PossessionId)
|
||||||
|
}
|
||||||
|
for _, r := range sceneGrants {
|
||||||
|
collectGrant(r.PossessionType, r.PossessionId)
|
||||||
|
}
|
||||||
|
for _, r := range missionRewardRows {
|
||||||
|
collectGrant(r.PossessionType, r.PossessionId)
|
||||||
|
}
|
||||||
|
|
||||||
catalogCostumeSet := make(map[int32]bool, len(catalogCostumes))
|
catalogCostumeSet := make(map[int32]bool, len(catalogCostumes))
|
||||||
costumeTermId := make(map[int32]int32, len(catalogCostumes))
|
|
||||||
for _, c := range catalogCostumes {
|
for _, c := range catalogCostumes {
|
||||||
catalogCostumeSet[c.CostumeId] = true
|
catalogCostumeSet[c.CostumeId] = true
|
||||||
costumeTermId[c.CostumeId] = c.CatalogTermId
|
|
||||||
}
|
}
|
||||||
catalogWeaponSet := make(map[int32]bool, len(catalogWeapons))
|
catalogWeaponSet := make(map[int32]bool, len(catalogWeapons))
|
||||||
for _, w := range catalogWeapons {
|
for _, w := range catalogWeapons {
|
||||||
catalogWeaponSet[w.WeaponId] = true
|
catalogWeaponSet[w.WeaponId] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
costumeWeaponType := make(map[int32]int32, len(costumes))
|
|
||||||
for _, c := range costumes {
|
|
||||||
costumeWeaponType[c.CostumeId] = c.SkillfulWeaponType
|
|
||||||
}
|
|
||||||
|
|
||||||
weaponTypeById := make(map[int32]int32, len(weapons))
|
|
||||||
weaponRarityById := make(map[int32]int32, len(weapons))
|
|
||||||
restrictedWeapons := make(map[int32]bool)
|
restrictedWeapons := make(map[int32]bool)
|
||||||
for _, w := range weapons {
|
for _, w := range weapons {
|
||||||
weaponTypeById[w.WeaponId] = w.WeaponType
|
|
||||||
weaponRarityById[w.WeaponId] = w.RarityType
|
|
||||||
if w.IsRestrictDiscard {
|
if w.IsRestrictDiscard {
|
||||||
restrictedWeapons[w.WeaponId] = true
|
restrictedWeapons[w.WeaponId] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pool := &GachaCatalog{
|
pool := &GachaCatalog{
|
||||||
CostumesByRarity: make(map[int32][]GachaPoolItem),
|
CostumesByRarity: make(map[int32][]GachaPoolItem),
|
||||||
WeaponsByRarity: make(map[int32][]GachaPoolItem),
|
WeaponsByRarity: make(map[int32][]GachaPoolItem),
|
||||||
CostumeById: make(map[int32]GachaPoolItem),
|
CostumeById: make(map[int32]GachaPoolItem),
|
||||||
WeaponById: make(map[int32]GachaPoolItem),
|
WeaponById: make(map[int32]GachaPoolItem),
|
||||||
CostumeWeaponMap: make(map[int32]int32),
|
CostumeWeaponMap: make(map[int32]int32),
|
||||||
FeaturedByGacha: make(map[int32]FeaturedSet),
|
FeaturedByGacha: make(map[int32]FeaturedSet),
|
||||||
|
TermById: make(map[int32]*CatalogTerm),
|
||||||
|
TermsByStartDatetime: make(map[int64][]*CatalogTerm),
|
||||||
|
}
|
||||||
|
for _, t := range terms {
|
||||||
|
ct := &CatalogTerm{TermId: t.CatalogTermId, StartDatetime: t.StartDatetime}
|
||||||
|
pool.TermById[t.CatalogTermId] = ct
|
||||||
|
pool.TermsByStartDatetime[t.StartDatetime] = append(pool.TermsByStartDatetime[t.StartDatetime], ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
questGrantedCostumeCount := 0
|
||||||
for _, c := range costumes {
|
for _, c := range costumes {
|
||||||
if !catalogCostumeSet[c.CostumeId] {
|
if !catalogCostumeSet[c.CostumeId] {
|
||||||
continue
|
continue
|
||||||
@@ -116,6 +164,10 @@ func LoadGachaPool() (*GachaCatalog, error) {
|
|||||||
if c.RarityType < model.RaritySRare {
|
if c.RarityType < model.RaritySRare {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if questGrantedCostumes[c.CostumeId] {
|
||||||
|
questGrantedCostumeCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
item := GachaPoolItem{
|
item := GachaPoolItem{
|
||||||
PossessionType: int32(model.PossessionTypeCostume),
|
PossessionType: int32(model.PossessionTypeCostume),
|
||||||
PossessionId: c.CostumeId,
|
PossessionId: c.CostumeId,
|
||||||
@@ -127,11 +179,18 @@ func LoadGachaPool() (*GachaCatalog, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restrictedCount := 0
|
restrictedCount := 0
|
||||||
|
questGrantedWeaponCount := 0
|
||||||
|
evolvedFilteredCount := 0
|
||||||
for _, w := range weapons {
|
for _, w := range weapons {
|
||||||
if !catalogWeaponSet[w.WeaponId] {
|
if !catalogWeaponSet[w.WeaponId] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if evolvedWeapons[w.WeaponId] {
|
if evolvedWeapons[w.WeaponId] {
|
||||||
|
evolvedFilteredCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if questGrantedWeapons[w.WeaponId] {
|
||||||
|
questGrantedWeaponCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item := GachaPoolItem{
|
item := GachaPoolItem{
|
||||||
@@ -147,62 +206,56 @@ func LoadGachaPool() (*GachaCatalog, error) {
|
|||||||
pool.WeaponsByRarity[w.RarityType] = append(pool.WeaponsByRarity[w.RarityType], item)
|
pool.WeaponsByRarity[w.RarityType] = append(pool.WeaponsByRarity[w.RarityType], item)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[GachaPool] excluded %d evolved weapons, %d restricted weapons from pool", len(evolvedWeapons), restrictedCount)
|
// Bucket catalog items into their terms (uses the post-filter CostumeById/WeaponById).
|
||||||
|
for _, cc := range catalogCostumes {
|
||||||
type weaponKey struct {
|
ct := pool.TermById[cc.CatalogTermId]
|
||||||
TermId int32
|
if ct == nil {
|
||||||
WeaponType int32
|
continue
|
||||||
Rarity int32
|
}
|
||||||
|
if item, ok := pool.CostumeById[cc.CostumeId]; ok {
|
||||||
|
ct.Costumes = append(ct.Costumes, item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
weaponsByKey := make(map[weaponKey][]int32)
|
|
||||||
for _, cw := range catalogWeapons {
|
for _, cw := range catalogWeapons {
|
||||||
if evolvedWeapons[cw.WeaponId] || restrictedWeapons[cw.WeaponId] {
|
ct := pool.TermById[cw.CatalogTermId]
|
||||||
|
if ct == nil || restrictedWeapons[cw.WeaponId] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wt := weaponTypeById[cw.WeaponId]
|
if item, ok := pool.WeaponById[cw.WeaponId]; ok {
|
||||||
r := weaponRarityById[cw.WeaponId]
|
ct.Weapons = append(ct.Weapons, item)
|
||||||
if wt == 0 || r < model.RaritySRare {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
k := weaponKey{TermId: cw.CatalogTermId, WeaponType: wt, Rarity: r}
|
|
||||||
weaponsByKey[k] = append(weaponsByKey[k], cw.WeaponId)
|
|
||||||
}
|
|
||||||
for k, ids := range weaponsByKey {
|
|
||||||
slices.Sort(ids)
|
|
||||||
weaponsByKey[k] = ids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exact, pattern, bestGuess := 0, 0, 0
|
// Standard pool: items in term 1 (the launch starter set, same on every banner).
|
||||||
for costumeId, item := range pool.CostumeById {
|
pool.StandardCostumesByRarity = make(map[int32][]GachaPoolItem)
|
||||||
tid := costumeTermId[costumeId]
|
pool.StandardWeaponsByRarity = make(map[int32][]GachaPoolItem)
|
||||||
wt := costumeWeaponType[costumeId]
|
if std := pool.TermById[StandardPoolTermId]; std != nil {
|
||||||
k := weaponKey{TermId: tid, WeaponType: wt, Rarity: item.RarityType}
|
for _, c := range std.Costumes {
|
||||||
candidates := weaponsByKey[k]
|
pool.StandardCostumesByRarity[c.RarityType] = append(pool.StandardCostumesByRarity[c.RarityType], c)
|
||||||
if len(candidates) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if len(candidates) == 1 {
|
for _, w := range std.Weapons {
|
||||||
pool.CostumeWeaponMap[costumeId] = candidates[0]
|
pool.StandardWeaponsByRarity[w.RarityType] = append(pool.StandardWeaponsByRarity[w.RarityType], w)
|
||||||
exact++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
idPattern := costumeId*10 + 1
|
|
||||||
found := false
|
|
||||||
for _, wid := range candidates {
|
|
||||||
if wid == idPattern {
|
|
||||||
pool.CostumeWeaponMap[costumeId] = wid
|
|
||||||
pattern++
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
pool.CostumeWeaponMap[costumeId] = candidates[0]
|
|
||||||
bestGuess++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[GachaPool] costume-weapon pairing: %d exact, %d id-pattern, %d best-guess, %d total",
|
stdCos, stdWea := 0, 0
|
||||||
exact, pattern, bestGuess, len(pool.CostumeWeaponMap))
|
for _, items := range pool.StandardCostumesByRarity {
|
||||||
|
stdCos += len(items)
|
||||||
|
}
|
||||||
|
for _, items := range pool.StandardWeaponsByRarity {
|
||||||
|
stdWea += len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[GachaPool] catalog terms: %d, standard pool: %d costumes + %d weapons (term %d)",
|
||||||
|
len(pool.TermById), stdCos, stdWea, StandardPoolTermId)
|
||||||
|
log.Printf("[GachaPool] pool excludes %d evolved, %d quest-granted costumes, %d quest-granted weapons, %d restricted weapons",
|
||||||
|
evolvedFilteredCount, questGrantedCostumeCount, questGrantedWeaponCount, restrictedCount)
|
||||||
|
|
||||||
|
for costumeId := range pool.CostumeById {
|
||||||
|
if wid, ok := costumeWeaponPairings[costumeId]; ok {
|
||||||
|
pool.CostumeWeaponMap[costumeId] = wid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("[GachaPool] costume-weapon pairing: %d entries from lookup table", len(pool.CostumeWeaponMap))
|
||||||
|
|
||||||
for _, m := range materials {
|
for _, m := range materials {
|
||||||
pool.Materials = append(pool.Materials, GachaPoolItem{
|
pool.Materials = append(pool.Materials, GachaPoolItem{
|
||||||
@@ -217,7 +270,6 @@ func LoadGachaPool() (*GachaCatalog, error) {
|
|||||||
|
|
||||||
func (pool *GachaCatalog) BuildShopFeatured(shop *ShopCatalog) {
|
func (pool *GachaCatalog) BuildShopFeatured(shop *ShopCatalog) {
|
||||||
pool.ShopFeaturedByMedal = make(map[int32][]ShopFeaturedEntry)
|
pool.ShopFeaturedByMedal = make(map[int32][]ShopFeaturedEntry)
|
||||||
shopPairs := 0
|
|
||||||
for _, cells := range shop.ExchangeShopCells {
|
for _, cells := range shop.ExchangeShopCells {
|
||||||
consumableId := shop.Items[cells[0].ShopItemId].PriceId
|
consumableId := shop.Items[cells[0].ShopItemId].PriceId
|
||||||
|
|
||||||
@@ -237,16 +289,12 @@ func (pool *GachaCatalog) BuildShopFeatured(shop *ShopCatalog) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entries = append(entries, ShopFeaturedEntry{CostumeId: costumeId, WeaponId: weaponId})
|
entries = append(entries, ShopFeaturedEntry{CostumeId: costumeId, WeaponId: weaponId})
|
||||||
if costumeId != 0 && weaponId != 0 {
|
|
||||||
pool.CostumeWeaponMap[costumeId] = weaponId
|
|
||||||
shopPairs++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(entries) > 0 {
|
if len(entries) > 0 {
|
||||||
pool.ShopFeaturedByMedal[consumableId] = entries
|
pool.ShopFeaturedByMedal[consumableId] = entries
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[GachaPool] shop featured: %d consumables, %d costume-weapon pairs overridden", len(pool.ShopFeaturedByMedal), shopPairs)
|
log.Printf("[GachaPool] shop featured: %d consumables", len(pool.ShopFeaturedByMedal))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pool *GachaCatalog) PruneUnpairedCostumes() {
|
func (pool *GachaCatalog) PruneUnpairedCostumes() {
|
||||||
@@ -269,119 +317,138 @@ func (pool *GachaCatalog) PruneUnpairedCostumes() {
|
|||||||
log.Printf("[GachaPool] pruned %d unpaired costumes", pruned)
|
log.Printf("[GachaPool] pruned %d unpaired costumes", pruned)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pool *GachaCatalog) BuildFeaturedMapping(entries []store.GachaCatalogEntry) {
|
// BuildFeaturedFromTerms derives a featured set for each non-chapter banner by
|
||||||
|
// unioning items from catalog terms that started on the banner's StartDatetime
|
||||||
|
// (excluding term 1 — the standard pool). Falls back to medal-exchange shop
|
||||||
|
// contents for banners whose StartDatetime doesn't line up with a term.
|
||||||
|
func (pool *GachaCatalog) BuildFeaturedFromTerms(entries []store.GachaCatalogEntry) {
|
||||||
matched := 0
|
matched := 0
|
||||||
|
fromShop := 0
|
||||||
|
gachaEligible := 0
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.MedalConsumableItemId == 0 {
|
if entry.GachaLabelType == model.GachaLabelChapter {
|
||||||
continue
|
|
||||||
}
|
|
||||||
shopEntries, ok := pool.ShopFeaturedByMedal[entry.MedalConsumableItemId]
|
|
||||||
if !ok || len(shopEntries) == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
gachaEligible++
|
||||||
|
|
||||||
seenCostume := make(map[int32]bool)
|
costumes, weapons := pool.unionTermFeatured(entry.StartDatetime)
|
||||||
linkedWeapons := make(map[int32]bool)
|
|
||||||
var costumes []GachaPoolItem
|
|
||||||
for _, se := range shopEntries {
|
|
||||||
if se.CostumeId != 0 && !seenCostume[se.CostumeId] {
|
|
||||||
costumes = append(costumes, pool.CostumeById[se.CostumeId])
|
|
||||||
seenCostume[se.CostumeId] = true
|
|
||||||
linkedWeapons[se.WeaponId] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seenWeapon := make(map[int32]bool)
|
if len(costumes) == 0 && len(weapons) == 0 && entry.MedalConsumableItemId != 0 {
|
||||||
var weapons []GachaPoolItem
|
if shopEntries, ok := pool.ShopFeaturedByMedal[entry.MedalConsumableItemId]; ok {
|
||||||
for _, se := range shopEntries {
|
costumes, weapons = pool.featuredFromShop(shopEntries)
|
||||||
if se.WeaponId != 0 && !linkedWeapons[se.WeaponId] && !seenWeapon[se.WeaponId] {
|
if len(costumes) > 0 || len(weapons) > 0 {
|
||||||
if item, ok := pool.WeaponById[se.WeaponId]; ok {
|
fromShop++
|
||||||
weapons = append(weapons, item)
|
|
||||||
seenWeapon[se.WeaponId] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(costumes) == 0 && len(weapons) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sort.Slice(costumes, func(i, j int) bool { return costumes[i].PossessionId < costumes[j].PossessionId })
|
||||||
|
sort.Slice(weapons, func(i, j int) bool { return weapons[i].PossessionId < weapons[j].PossessionId })
|
||||||
|
|
||||||
pool.FeaturedByGacha[entry.GachaId] = FeaturedSet{Costumes: costumes, Weapons: weapons}
|
pool.FeaturedByGacha[entry.GachaId] = FeaturedSet{Costumes: costumes, Weapons: weapons}
|
||||||
matched++
|
matched++
|
||||||
}
|
}
|
||||||
log.Printf("[GachaPool] featured mapping: %d/%d banners matched via shop", matched, len(entries))
|
log.Printf("[GachaPool] featured per banner: %d/%d (term-match + %d from shop-fallback)",
|
||||||
|
matched, gachaEligible, fromShop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pool *GachaCatalog) unionTermFeatured(startDatetime int64) (costumes, weapons []GachaPoolItem) {
|
||||||
|
coTerms := pool.TermsByStartDatetime[startDatetime]
|
||||||
|
if len(coTerms) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
seenCostume := make(map[int32]bool)
|
||||||
|
seenWeapon := make(map[int32]bool)
|
||||||
|
for _, t := range coTerms {
|
||||||
|
if t.TermId == StandardPoolTermId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, c := range t.Costumes {
|
||||||
|
if c.RarityType < model.RaritySRare || seenCostume[c.PossessionId] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
costumes = append(costumes, c)
|
||||||
|
seenCostume[c.PossessionId] = true
|
||||||
|
}
|
||||||
|
for _, w := range t.Weapons {
|
||||||
|
if w.RarityType < model.RaritySRare || seenWeapon[w.PossessionId] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
weapons = append(weapons, w)
|
||||||
|
seenWeapon[w.PossessionId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return costumes, weapons
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pool *GachaCatalog) featuredFromShop(shopEntries []ShopFeaturedEntry) (costumes, weapons []GachaPoolItem) {
|
||||||
|
seenCostume := make(map[int32]bool)
|
||||||
|
seenWeapon := make(map[int32]bool)
|
||||||
|
linkedWeapons := make(map[int32]bool)
|
||||||
|
for _, se := range shopEntries {
|
||||||
|
if se.CostumeId == 0 || seenCostume[se.CostumeId] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item, ok := pool.CostumeById[se.CostumeId]; ok && item.RarityType >= model.RaritySRare {
|
||||||
|
costumes = append(costumes, item)
|
||||||
|
seenCostume[se.CostumeId] = true
|
||||||
|
linkedWeapons[se.WeaponId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, se := range shopEntries {
|
||||||
|
if se.WeaponId == 0 || linkedWeapons[se.WeaponId] || seenWeapon[se.WeaponId] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item, ok := pool.WeaponById[se.WeaponId]; ok && item.RarityType >= model.RaritySRare {
|
||||||
|
weapons = append(weapons, item)
|
||||||
|
seenWeapon[se.WeaponId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return costumes, weapons
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pool *GachaCatalog) BuildBannerPools(entries []store.GachaCatalogEntry) {
|
func (pool *GachaCatalog) BuildBannerPools(entries []store.GachaCatalogEntry) {
|
||||||
allFeaturedCostumes := make(map[int32]bool)
|
|
||||||
allFeaturedWeapons := make(map[int32]bool)
|
|
||||||
for _, fs := range pool.FeaturedByGacha {
|
|
||||||
for _, c := range fs.Costumes {
|
|
||||||
allFeaturedCostumes[c.PossessionId] = true
|
|
||||||
allFeaturedWeapons[pool.CostumeWeaponMap[c.PossessionId]] = true
|
|
||||||
}
|
|
||||||
for _, w := range fs.Weapons {
|
|
||||||
allFeaturedWeapons[w.PossessionId] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commonCostumes := make(map[int32][]GachaPoolItem)
|
|
||||||
for rarity, items := range pool.CostumesByRarity {
|
|
||||||
for _, item := range items {
|
|
||||||
if !allFeaturedCostumes[item.PossessionId] {
|
|
||||||
commonCostumes[rarity] = append(commonCostumes[rarity], item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commonWeapons := make(map[int32][]GachaPoolItem)
|
|
||||||
for rarity, items := range pool.WeaponsByRarity {
|
|
||||||
for _, item := range items {
|
|
||||||
if !allFeaturedWeapons[item.PossessionId] {
|
|
||||||
commonWeapons[rarity] = append(commonWeapons[rarity], item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commonPool := &BannerPool{
|
|
||||||
CostumesByRarity: commonCostumes,
|
|
||||||
WeaponsByRarity: commonWeapons,
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.BannerPools = make(map[int32]*BannerPool)
|
pool.BannerPools = make(map[int32]*BannerPool)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
fs, hasFeatured := pool.FeaturedByGacha[entry.GachaId]
|
fs, hasFeatured := pool.FeaturedByGacha[entry.GachaId]
|
||||||
if !hasFeatured {
|
|
||||||
pool.BannerPools[entry.GachaId] = commonPool
|
bannerCostumes := cloneRarityMap(pool.StandardCostumesByRarity)
|
||||||
continue
|
bannerWeapons := cloneRarityMap(pool.StandardWeaponsByRarity)
|
||||||
}
|
|
||||||
|
|
||||||
var allFeatured []GachaPoolItem
|
var allFeatured []GachaPoolItem
|
||||||
bannerCostumes := make(map[int32][]GachaPoolItem)
|
if hasFeatured {
|
||||||
for rarity, items := range commonCostumes {
|
for _, c := range fs.Costumes {
|
||||||
bannerCostumes[rarity] = append(bannerCostumes[rarity], items...)
|
bannerCostumes[c.RarityType] = append(bannerCostumes[c.RarityType], c)
|
||||||
|
allFeatured = append(allFeatured, c)
|
||||||
|
if wid, ok := pool.CostumeWeaponMap[c.PossessionId]; ok {
|
||||||
|
if w, ok := pool.WeaponById[wid]; ok {
|
||||||
|
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
|
||||||
|
allFeatured = append(allFeatured, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, w := range fs.Weapons {
|
||||||
|
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
|
||||||
|
allFeatured = append(allFeatured, w)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bannerWeapons := make(map[int32][]GachaPoolItem)
|
|
||||||
for rarity, items := range commonWeapons {
|
|
||||||
bannerWeapons[rarity] = append(bannerWeapons[rarity], items...)
|
|
||||||
}
|
|
||||||
for _, c := range fs.Costumes {
|
|
||||||
bannerCostumes[c.RarityType] = append(bannerCostumes[c.RarityType], c)
|
|
||||||
allFeatured = append(allFeatured, c)
|
|
||||||
wid := pool.CostumeWeaponMap[c.PossessionId]
|
|
||||||
w := pool.WeaponById[wid]
|
|
||||||
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
|
|
||||||
allFeatured = append(allFeatured, w)
|
|
||||||
}
|
|
||||||
for _, w := range fs.Weapons {
|
|
||||||
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
|
|
||||||
allFeatured = append(allFeatured, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.BannerPools[entry.GachaId] = &BannerPool{
|
pool.BannerPools[entry.GachaId] = &BannerPool{
|
||||||
CostumesByRarity: bannerCostumes,
|
CostumesByRarity: bannerCostumes,
|
||||||
WeaponsByRarity: bannerWeapons,
|
WeaponsByRarity: bannerWeapons,
|
||||||
Featured: allFeatured,
|
Featured: allFeatured,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.Printf("[GachaPool] banner pools: %d banners built from standard pool + per-banner featured", len(pool.BannerPools))
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("[GachaPool] banner pools: %d banners, %d featured costumes stripped, %d featured weapons stripped",
|
func cloneRarityMap(src map[int32][]GachaPoolItem) map[int32][]GachaPoolItem {
|
||||||
len(pool.BannerPools), len(allFeaturedCostumes), len(allFeaturedWeapons))
|
dst := make(map[int32][]GachaPoolItem, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
dst[k] = append([]GachaPoolItem(nil), v...)
|
||||||
|
}
|
||||||
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildEvolvedWeaponSet(rows []EntityMWeaponEvolutionGroup) map[int32]bool {
|
func buildEvolvedWeaponSet(rows []EntityMWeaponEvolutionGroup) map[int32]bool {
|
||||||
|
|||||||
@@ -3,57 +3,463 @@ package masterdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MaxUserGimmickRows is the server's per-table cap for gimmick projections and the
|
||||||
|
// in-memory user.Gimmick.Sequences map. The client hard-caps each of its
|
||||||
|
// IUserGimmick* tables at 1024 rows; we project up to this many with a small
|
||||||
|
// safety margin so we never overflow client buffers. Shared by InitSequenceSchedule
|
||||||
|
// (limits user.Gimmick.Sequences map size) and the IUserGimmick / Ornament /
|
||||||
|
// Sequence projections.
|
||||||
|
const MaxUserGimmickRows = 1000
|
||||||
|
|
||||||
type gimmickScheduleEntry struct {
|
type gimmickScheduleEntry struct {
|
||||||
ScheduleId int32
|
ScheduleId int32
|
||||||
StartDatetime int64
|
StartDatetime int64
|
||||||
EndDatetime int64
|
EndDatetime int64
|
||||||
FirstSequenceId int32
|
FirstSequenceId int32
|
||||||
RequiredQuestId int32 // 0 = always active
|
RequiredQuestId int32 // 0 = always active
|
||||||
|
IsHidden bool // hidden-story or cage-memory gimmick: bypasses the quest gate
|
||||||
|
Rank int // trim priority — see gimmickTypeRank
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGimmickTable[T any](name, what string) ([]T, bool) {
|
||||||
|
rows, err := utils.ReadTable[T](name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[gimmick] %s unavailable, %s empty: %v", name, what, err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return rows, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func gimmickTypeRank(t model.GimmickType) int {
|
||||||
|
switch t {
|
||||||
|
case model.GimmickTypeReport: // hidden missions / stories
|
||||||
|
return 0
|
||||||
|
case model.GimmickTypeCageMemory: // lost archives
|
||||||
|
return 1
|
||||||
|
case model.GimmickTypeCageTreasureHunt: // treasure
|
||||||
|
return 2
|
||||||
|
case model.GimmickTypeBrokenObelisk, model.GimmickTypeFirstBrokenObelisk:
|
||||||
|
return 3
|
||||||
|
case model.GimmickTypeIronGrill:
|
||||||
|
return 4
|
||||||
|
case model.GimmickTypeRadioMessage:
|
||||||
|
return 5
|
||||||
|
case model.GimmickTypeMapOnlyCageTreasureHunt, model.GimmickTypeMapOnlyHideObelisk:
|
||||||
|
return 6
|
||||||
|
case model.GimmickTypeCageIntervalDropItem, model.GimmickTypeMapOnlyCageIntervalDrop:
|
||||||
|
return 7 // birds — bottom
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
|
||||||
|
type gimmickTypeTables struct {
|
||||||
|
byGimmick map[int32]model.GimmickType
|
||||||
|
bySequence map[int32]model.GimmickType
|
||||||
|
}
|
||||||
|
|
||||||
|
var gimmickTypes = sync.OnceValue(loadGimmickTypes)
|
||||||
|
|
||||||
|
func loadGimmickTypes() gimmickTypeTables {
|
||||||
|
empty := gimmickTypeTables{
|
||||||
|
byGimmick: map[int32]model.GimmickType{},
|
||||||
|
bySequence: map[int32]model.GimmickType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicks, ok := readGimmickTable[EntityMGimmick]("m_gimmick", "type tables")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
groups, ok := readGimmickTable[EntityMGimmickGroup]("m_gimmick_group", "type tables")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "type tables")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
byGimmick := make(map[int32]model.GimmickType, len(gimmicks))
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
byGimmick[g.GimmickId] = model.GimmickType(g.GimmickType)
|
||||||
|
}
|
||||||
|
typeByGroup := make(map[int32]model.GimmickType, len(groups))
|
||||||
|
for _, grp := range groups {
|
||||||
|
if _, seen := typeByGroup[grp.GimmickGroupId]; seen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t, ok := byGimmick[grp.GimmickId]; ok {
|
||||||
|
typeByGroup[grp.GimmickGroupId] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bySequence := make(map[int32]model.GimmickType, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
if t, ok := typeByGroup[seq.GimmickGroupId]; ok {
|
||||||
|
bySequence[seq.GimmickSequenceId] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gimmickTypeTables{byGimmick: byGimmick, bySequence: bySequence}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gimmickSequenceTypes() map[int32]model.GimmickType {
|
||||||
|
return gimmickTypes().bySequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadGimmickSequenceRanks() map[int32]int {
|
||||||
|
types := gimmickSequenceTypes()
|
||||||
|
out := make(map[int32]int, len(types))
|
||||||
|
for sid, t := range types {
|
||||||
|
out[sid] = gimmickTypeRank(t)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type SequenceReward struct {
|
||||||
|
PossessionType int32
|
||||||
|
PossessionId int32
|
||||||
|
Count int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type GimmickCatalog struct {
|
type GimmickCatalog struct {
|
||||||
schedules []gimmickScheduleEntry
|
schedules []gimmickScheduleEntry
|
||||||
|
hiddenSequences map[int32]bool // GimmickSequenceId -> report/cage-memory
|
||||||
|
sequenceRewards map[int32][]SequenceReward // GimmickSequenceId -> clear rewards
|
||||||
|
gimmickTypes map[int32]model.GimmickType
|
||||||
|
cageMemoryItems map[int32]int32 // CageMemory GimmickId -> ImportantItemId (type 4)
|
||||||
|
hiddenBirdRewards map[GimmickOrnamentRef]SequenceReward
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadGimmickCatalog(resolver *ConditionResolver) (*GimmickCatalog, error) {
|
func LoadGimmickCatalog(resolver *ConditionResolver, cageOrnaments *CageOrnamentCatalog) (*GimmickCatalog, error) {
|
||||||
rows, err := utils.ReadTable[EntityMGimmickSequenceSchedule]("m_gimmick_sequence_schedule")
|
rows, err := utils.ReadTable[EntityMGimmickSequenceSchedule]("m_gimmick_sequence_schedule")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load gimmick sequence schedule table: %w", err)
|
return nil, fmt.Errorf("load gimmick sequence schedule table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries := make([]gimmickScheduleEntry, 0, len(rows))
|
seqTypes := gimmickSequenceTypes()
|
||||||
|
hiddenSeq := make(map[int32]bool, len(seqTypes))
|
||||||
|
for sid, t := range seqTypes {
|
||||||
|
if t == model.GimmickTypeReport || t == model.GimmickTypeCageMemory {
|
||||||
|
hiddenSeq[sid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick rule: prefer schedules with EndDatetime in the future (so the client's
|
||||||
|
// in-cage filter doesn't drop them), then by lowest StartDatetime (longest
|
||||||
|
// historical validity, tends to carry the simplest RequiredQuestId), tie-broken
|
||||||
|
// by lowest ScheduleId for determinism. The future-end preference matters for
|
||||||
|
// "Fickle Black Birds" (type 1) where real expiry dates vary; other types have
|
||||||
|
// EndDatetime = 9999-03-31 so the preference is a no-op.
|
||||||
|
now := gametime.NowMillis()
|
||||||
|
bestBySeq := make(map[int32]gimmickScheduleEntry, len(rows))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
entry := gimmickScheduleEntry{
|
entry := gimmickScheduleEntry{
|
||||||
ScheduleId: r.GimmickSequenceScheduleId,
|
ScheduleId: r.GimmickSequenceScheduleId,
|
||||||
StartDatetime: r.StartDatetime,
|
StartDatetime: r.StartDatetime,
|
||||||
EndDatetime: r.EndDatetime,
|
EndDatetime: r.EndDatetime,
|
||||||
FirstSequenceId: r.FirstGimmickSequenceId,
|
FirstSequenceId: r.FirstGimmickSequenceId,
|
||||||
|
IsHidden: hiddenSeq[r.FirstGimmickSequenceId],
|
||||||
|
Rank: gimmickTypeRank(seqTypes[r.FirstGimmickSequenceId]),
|
||||||
}
|
}
|
||||||
if r.ReleaseEvaluateConditionId != 0 {
|
if r.ReleaseEvaluateConditionId != 0 {
|
||||||
if qid, ok := resolver.RequiredQuestId(r.ReleaseEvaluateConditionId); ok {
|
if qid, ok := resolver.RequiredQuestId(r.ReleaseEvaluateConditionId); ok {
|
||||||
entry.RequiredQuestId = qid
|
entry.RequiredQuestId = qid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries = append(entries, entry)
|
if existing, ok := bestBySeq[entry.FirstSequenceId]; ok {
|
||||||
|
existingFuture := existing.EndDatetime > now
|
||||||
|
entryFuture := entry.EndDatetime > now
|
||||||
|
if existingFuture != entryFuture {
|
||||||
|
// Future-end schedule wins over expired one.
|
||||||
|
if existingFuture {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if existing.StartDatetime < entry.StartDatetime ||
|
||||||
|
(existing.StartDatetime == entry.StartDatetime && existing.ScheduleId <= entry.ScheduleId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bestBySeq[entry.FirstSequenceId] = entry
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("gimmick catalog loaded: %d schedules", len(entries))
|
entries := make([]gimmickScheduleEntry, 0, len(bestBySeq))
|
||||||
return &GimmickCatalog{schedules: entries}, nil
|
hiddenCount := 0
|
||||||
|
for _, entry := range bestBySeq {
|
||||||
|
if entry.IsHidden {
|
||||||
|
hiddenCount++
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
dedupedCount := len(rows) - len(entries)
|
||||||
|
|
||||||
|
// Sort by (Rank, ScheduleId) so ActiveScheduleKeys returns the priority order
|
||||||
|
// directly: treasure/lost-archives/hidden-missions first, birds last. This is
|
||||||
|
// what lets InitSequenceSchedule's 1000-row cap trim from the bottom.
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
if entries[i].Rank != entries[j].Rank {
|
||||||
|
return entries[i].Rank < entries[j].Rank
|
||||||
|
}
|
||||||
|
return entries[i].ScheduleId < entries[j].ScheduleId
|
||||||
|
})
|
||||||
|
|
||||||
|
sequenceRewards := loadGimmickSequenceRewards()
|
||||||
|
cageMemoryItems := loadCageMemoryImportantItems(gimmickTypes().byGimmick)
|
||||||
|
hiddenBirdRewards := loadHiddenBirdRewards(cageOrnaments)
|
||||||
|
|
||||||
|
log.Printf("gimmick catalog loaded: %d schedules (%d hidden-content, %d duplicates dropped), %d reward sequences, %d cage-memory items, %d hidden-bird rewards",
|
||||||
|
len(entries), hiddenCount, dedupedCount, len(sequenceRewards), len(cageMemoryItems), len(hiddenBirdRewards))
|
||||||
|
return &GimmickCatalog{
|
||||||
|
schedules: entries,
|
||||||
|
hiddenSequences: hiddenSeq,
|
||||||
|
sequenceRewards: sequenceRewards,
|
||||||
|
gimmickTypes: gimmickTypes().byGimmick,
|
||||||
|
cageMemoryItems: cageMemoryItems,
|
||||||
|
hiddenBirdRewards: hiddenBirdRewards,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiddenBirdReward returns the per-tap reward for a MAP_ONLY_CAGE_TREASURE_HUNT
|
||||||
|
// ("Hidden Black Birds", type 7) ornament. Returns false if there's no mapping
|
||||||
|
// (e.g. the ornament view has no corresponding cage-ornament-reward entry).
|
||||||
|
func (c *GimmickCatalog) HiddenBirdReward(gimmickId, ornamentIndex int32) (SequenceReward, bool) {
|
||||||
|
r, ok := c.hiddenBirdRewards[GimmickOrnamentRef{GimmickId: gimmickId, OrnamentIndex: ornamentIndex}]
|
||||||
|
return r, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadHiddenBirdRewards resolves (GimmickId, OrnamentIndex) -> CageOrnamentReward for
|
||||||
|
// every type-7 ("Hidden Black Birds") gimmick. The mapping is structural:
|
||||||
|
//
|
||||||
|
// m_gimmick (GimmickType == 7) -> GimmickOrnamentGroupId
|
||||||
|
// m_gimmick_ornament (matching group) -> GimmickOrnamentViewId
|
||||||
|
// m_cage_ornament (CageOrnamentId == ViewId) -> CageOrnamentRewardId
|
||||||
|
// m_cage_ornament_reward (matching id) -> PossessionType / PossessionId / Count
|
||||||
|
//
|
||||||
|
// 110 of 114 type-7 ornaments have a matching m_cage_ornament row in the current
|
||||||
|
// data; the rest log a warning and are silently skipped so the player just gets
|
||||||
|
// no reward on those (no crash).
|
||||||
|
func loadHiddenBirdRewards(cageOrnaments *CageOrnamentCatalog) map[GimmickOrnamentRef]SequenceReward {
|
||||||
|
empty := map[GimmickOrnamentRef]SequenceReward{}
|
||||||
|
if cageOrnaments == nil {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicks, ok := readGimmickTable[EntityMGimmick]("m_gimmick", "hidden-bird rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
ornaments, ok := readGimmickTable[EntityMGimmickOrnament]("m_gimmick_ornament", "hidden-bird rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicksByGroup := make(map[int32][]int32)
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
if model.GimmickType(g.GimmickType) == model.GimmickTypeMapOnlyCageTreasureHunt {
|
||||||
|
gimmicksByGroup[g.GimmickOrnamentGroupId] = append(gimmicksByGroup[g.GimmickOrnamentGroupId], g.GimmickId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[GimmickOrnamentRef]SequenceReward)
|
||||||
|
missing := 0
|
||||||
|
for _, o := range ornaments {
|
||||||
|
gids, ok := gimmicksByGroup[o.GimmickOrnamentGroupId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reward, ok := cageOrnaments.LookupReward(o.GimmickOrnamentViewId)
|
||||||
|
if !ok {
|
||||||
|
missing++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := SequenceReward{
|
||||||
|
PossessionType: reward.PossessionType,
|
||||||
|
PossessionId: reward.PossessionId,
|
||||||
|
Count: reward.Count,
|
||||||
|
}
|
||||||
|
for _, gid := range gids {
|
||||||
|
out[GimmickOrnamentRef{GimmickId: gid, OrnamentIndex: o.GimmickOrnamentIndex}] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missing > 0 {
|
||||||
|
log.Printf("[gimmick] %d hidden-bird ornaments had no m_cage_ornament_reward row", missing)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GimmickCatalog) GimmickType(gimmickId int32) model.GimmickType {
|
||||||
|
return c.gimmickTypes[gimmickId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CageMemoryImportantItem returns the ImportantItemId (type 4) that the library uses
|
||||||
|
// to mark a tapped cage memory as collected, given the world-gimmick id. The mapping
|
||||||
|
// is derived from m_gimmick_additional_asset texture suffixes — see
|
||||||
|
// loadCageMemoryImportantItems.
|
||||||
|
func (c *GimmickCatalog) CageMemoryImportantItem(gimmickId int32) (int32, bool) {
|
||||||
|
id, ok := c.cageMemoryItems[gimmickId]
|
||||||
|
return id, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// importantItemTypeCageMemory mirrors EntityMImportantItem.ImportantItemType==4 — the
|
||||||
|
// CageMemory entry that the library's HasCageMemory check resolves to.
|
||||||
|
const importantItemTypeCageMemory int32 = 4
|
||||||
|
|
||||||
|
func loadCageMemoryImportantItems(typeByGimmick map[int32]model.GimmickType) map[int32]int32 {
|
||||||
|
empty := map[int32]int32{}
|
||||||
|
|
||||||
|
ornaments, ok := readGimmickTable[EntityMGimmickOrnament]("m_gimmick_ornament", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
chapters, ok := readGimmickTable[EntityMMainQuestChapter]("m_main_quest_chapter", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
routes, ok := readGimmickTable[EntityMMainQuestRoute]("m_main_quest_route", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
cageMemories, ok := readGimmickTable[EntityMCageMemory]("m_cage_memory", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
items, ok := readGimmickTable[EntityMImportantItem]("m_important_item", "cage-memory items")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
chapterByOrnamentGroup := make(map[int32]int32, len(ornaments))
|
||||||
|
for _, o := range ornaments {
|
||||||
|
if _, seen := chapterByOrnamentGroup[o.GimmickOrnamentGroupId]; seen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
chapterByOrnamentGroup[o.GimmickOrnamentGroupId] = o.ChapterId
|
||||||
|
}
|
||||||
|
routeByChapter := make(map[int32]int32, len(chapters))
|
||||||
|
for _, c := range chapters {
|
||||||
|
routeByChapter[c.MainQuestChapterId] = c.MainQuestRouteId
|
||||||
|
}
|
||||||
|
seasonByRoute := make(map[int32]int32, len(routes))
|
||||||
|
for _, r := range routes {
|
||||||
|
seasonByRoute[r.MainQuestRouteId] = r.MainQuestSeasonId
|
||||||
|
}
|
||||||
|
cmsBySeason := make(map[int32][]int32)
|
||||||
|
for _, c := range cageMemories {
|
||||||
|
cmsBySeason[c.MainQuestSeasonId] = append(cmsBySeason[c.MainQuestSeasonId], c.CageMemoryId)
|
||||||
|
}
|
||||||
|
for s := range cmsBySeason {
|
||||||
|
sort.Slice(cmsBySeason[s], func(i, j int) bool { return cmsBySeason[s][i] < cmsBySeason[s][j] })
|
||||||
|
}
|
||||||
|
itemByCageMemory := make(map[int32]int32)
|
||||||
|
for _, it := range items {
|
||||||
|
if it.ImportantItemType == importantItemTypeCageMemory && it.CageMemoryId != 0 {
|
||||||
|
itemByCageMemory[it.CageMemoryId] = it.ImportantItemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gimmicksByRoute := make(map[int32][]int32)
|
||||||
|
for gid, t := range typeByGimmick {
|
||||||
|
if t != model.GimmickTypeCageMemory {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
chapter, ok := chapterByOrnamentGroup[gid]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] cage-memory %d has no ornament row, skipping mapping", gid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
route, ok := routeByChapter[chapter]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] cage-memory %d chapter %d has no route, skipping mapping", gid, chapter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gimmicksByRoute[route] = append(gimmicksByRoute[route], gid)
|
||||||
|
}
|
||||||
|
for r := range gimmicksByRoute {
|
||||||
|
sort.Slice(gimmicksByRoute[r], func(i, j int) bool { return gimmicksByRoute[r][i] < gimmicksByRoute[r][j] })
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[int32]int32)
|
||||||
|
for route, gids := range gimmicksByRoute {
|
||||||
|
season, ok := seasonByRoute[route]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] route %d has no season, skipping %d cage-memory gimmicks", route, len(gids))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seasonCms := cmsBySeason[season]
|
||||||
|
for i, gid := range gids {
|
||||||
|
if i >= len(seasonCms) {
|
||||||
|
log.Printf("[gimmick] route %d (season %d) has %d cage-memory gimmicks but only %d cage memories; gimmick %d skipped",
|
||||||
|
route, season, len(gids), len(seasonCms), gid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cageMemoryId := seasonCms[i]
|
||||||
|
itemId, ok := itemByCageMemory[cageMemoryId]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[gimmick] cage memory %d (gimmick %d) has no m_important_item row (type 4), skipping",
|
||||||
|
cageMemoryId, gid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[gid] = itemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadGimmickSequenceRewards() map[int32][]SequenceReward {
|
||||||
|
empty := map[int32][]SequenceReward{}
|
||||||
|
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "sequence rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
rewardGroups, ok := readGimmickTable[EntityMGimmickSequenceRewardGroup]("m_gimmick_sequence_reward_group", "sequence rewards")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardsByGroup := make(map[int32][]SequenceReward)
|
||||||
|
for _, rg := range rewardGroups {
|
||||||
|
if rg.PossessionType == 0 || rg.PossessionId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rewardsByGroup[rg.GimmickSequenceRewardGroupId] = append(
|
||||||
|
rewardsByGroup[rg.GimmickSequenceRewardGroupId], SequenceReward{
|
||||||
|
PossessionType: rg.PossessionType,
|
||||||
|
PossessionId: rg.PossessionId,
|
||||||
|
Count: rg.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardsBySequence := make(map[int32][]SequenceReward, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
if rewards := rewardsByGroup[seq.GimmickSequenceRewardGroupId]; len(rewards) > 0 {
|
||||||
|
rewardsBySequence[seq.GimmickSequenceId] = rewards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rewardsBySequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GimmickCatalog) IsHiddenSequence(sequenceId int32) bool {
|
||||||
|
return c.hiddenSequences[sequenceId]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GimmickCatalog) SequenceRewards(sequenceId int32) []SequenceReward {
|
||||||
|
return c.sequenceRewards[sequenceId]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int64) []store.GimmickSequenceKey {
|
func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int64) []store.GimmickSequenceKey {
|
||||||
var keys []store.GimmickSequenceKey
|
keys := make([]store.GimmickSequenceKey, 0, len(c.schedules))
|
||||||
for _, s := range c.schedules {
|
for _, s := range c.schedules {
|
||||||
if nowMillis < s.StartDatetime || nowMillis > s.EndDatetime {
|
if nowMillis < s.StartDatetime {
|
||||||
continue
|
continue // future schedules still skipped
|
||||||
}
|
}
|
||||||
if s.RequiredQuestId != 0 {
|
if !s.IsHidden && s.RequiredQuestId != 0 {
|
||||||
q, ok := user.Quests[s.RequiredQuestId]
|
q, ok := user.Quests[s.RequiredQuestId]
|
||||||
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
|
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
|
||||||
continue
|
continue
|
||||||
@@ -66,3 +472,126 @@ func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int6
|
|||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GimmickOrnamentRef struct {
|
||||||
|
GimmickId int32
|
||||||
|
OrnamentIndex int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadGimmickOrnamentRefs() map[int32][]GimmickOrnamentRef {
|
||||||
|
empty := map[int32][]GimmickOrnamentRef{}
|
||||||
|
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
groups, ok := readGimmickTable[EntityMGimmickGroup]("m_gimmick_group", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
gimmicks, ok := readGimmickTable[EntityMGimmick]("m_gimmick", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
ornaments, ok := readGimmickTable[EntityMGimmickOrnament]("m_gimmick_ornament", "ornament refs")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
indicesByOrnamentGroup := make(map[int32][]int32)
|
||||||
|
for _, o := range ornaments {
|
||||||
|
indicesByOrnamentGroup[o.GimmickOrnamentGroupId] = append(
|
||||||
|
indicesByOrnamentGroup[o.GimmickOrnamentGroupId], o.GimmickOrnamentIndex)
|
||||||
|
}
|
||||||
|
ornamentGroupByGimmick := make(map[int32]int32, len(gimmicks))
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
ornamentGroupByGimmick[g.GimmickId] = g.GimmickOrnamentGroupId
|
||||||
|
}
|
||||||
|
gimmicksByGroup := make(map[int32][]int32)
|
||||||
|
for _, grp := range groups {
|
||||||
|
gimmicksByGroup[grp.GimmickGroupId] = append(gimmicksByGroup[grp.GimmickGroupId], grp.GimmickId)
|
||||||
|
}
|
||||||
|
|
||||||
|
refsBySequence := make(map[int32][]GimmickOrnamentRef, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
var refs []GimmickOrnamentRef
|
||||||
|
for _, gimmickId := range gimmicksByGroup[seq.GimmickGroupId] {
|
||||||
|
for _, ornamentIndex := range indicesByOrnamentGroup[ornamentGroupByGimmick[gimmickId]] {
|
||||||
|
refs = append(refs, GimmickOrnamentRef{GimmickId: gimmickId, OrnamentIndex: ornamentIndex})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(refs) > 0 {
|
||||||
|
refsBySequence[seq.GimmickSequenceId] = refs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("gimmick ornament refs loaded: %d sequences", len(refsBySequence))
|
||||||
|
return refsBySequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadHiddenGimmickSequenceIDs() map[int32]bool {
|
||||||
|
types := gimmickSequenceTypes()
|
||||||
|
out := make(map[int32]bool, len(types))
|
||||||
|
for sid, t := range types {
|
||||||
|
if t == model.GimmickTypeReport || t == model.GimmickTypeCageMemory || t == model.GimmickTypeMapOnlyCageTreasureHunt {
|
||||||
|
out[sid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadBirdGimmickIDs() map[int32]bool {
|
||||||
|
byGimmick := gimmickTypes().byGimmick
|
||||||
|
out := make(map[int32]bool, len(byGimmick))
|
||||||
|
for gid, t := range byGimmick {
|
||||||
|
if t == model.GimmickTypeCageIntervalDropItem || t == model.GimmickTypeMapOnlyCageIntervalDrop {
|
||||||
|
out[gid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadGimmickSequenceChains() map[int32][]int32 {
|
||||||
|
empty := map[int32][]int32{}
|
||||||
|
|
||||||
|
sequences, ok := readGimmickTable[EntityMGimmickSequence]("m_gimmick_sequence", "sequence chains")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
groups, ok := readGimmickTable[EntityMGimmickSequenceGroup]("m_gimmick_sequence_group", "sequence chains")
|
||||||
|
if !ok {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
membersByGroup := make(map[int32][]int32)
|
||||||
|
for _, g := range groups {
|
||||||
|
membersByGroup[g.GimmickSequenceGroupId] = append(membersByGroup[g.GimmickSequenceGroupId], g.GimmickSequenceId)
|
||||||
|
}
|
||||||
|
nextGroupBySequence := make(map[int32]int32, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
nextGroupBySequence[seq.GimmickSequenceId] = seq.NextGimmickSequenceGroupId
|
||||||
|
}
|
||||||
|
|
||||||
|
chains := make(map[int32][]int32, len(sequences))
|
||||||
|
for _, seq := range sequences {
|
||||||
|
start := seq.GimmickSequenceId
|
||||||
|
seen := map[int32]bool{start: true}
|
||||||
|
chain := []int32{start}
|
||||||
|
for queue := []int32{start}; len(queue) > 0; {
|
||||||
|
cur := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
nextGroup := nextGroupBySequence[cur]
|
||||||
|
if nextGroup == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, member := range membersByGroup[nextGroup] {
|
||||||
|
if !seen[member] {
|
||||||
|
seen[member] = true
|
||||||
|
chain = append(chain, member)
|
||||||
|
queue = append(queue, member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chains[start] = chain
|
||||||
|
}
|
||||||
|
return chains
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package masterdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/store"
|
||||||
|
"lunar-tear/server/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HiddenStoryRequirements struct {
|
||||||
|
MissionIds []int32
|
||||||
|
QuestMissions []store.QuestMissionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadHiddenStoryRequirements() HiddenStoryRequirements {
|
||||||
|
var empty HiddenStoryRequirements
|
||||||
|
|
||||||
|
gimmicks, err := utils.ReadTable[EntityMGimmick]("m_gimmick")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[hiddenstory] m_gimmick unavailable: %v", err)
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
conditions, err := utils.ReadTable[EntityMEvaluateCondition]("m_evaluate_condition")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[hiddenstory] m_evaluate_condition unavailable: %v", err)
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
valueGroups, err := utils.ReadTable[EntityMEvaluateConditionValueGroup]("m_evaluate_condition_value_group")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[hiddenstory] m_evaluate_condition_value_group unavailable: %v", err)
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
condById := make(map[int32]EntityMEvaluateCondition, len(conditions))
|
||||||
|
for _, c := range conditions {
|
||||||
|
condById[c.EvaluateConditionId] = c
|
||||||
|
}
|
||||||
|
valuesByGroup := make(map[int32]map[int32]int64)
|
||||||
|
for _, vg := range valueGroups {
|
||||||
|
g := valuesByGroup[vg.EvaluateConditionValueGroupId]
|
||||||
|
if g == nil {
|
||||||
|
g = make(map[int32]int64)
|
||||||
|
valuesByGroup[vg.EvaluateConditionValueGroupId] = g
|
||||||
|
}
|
||||||
|
g[vg.GroupIndex] = vg.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
missionSet := make(map[int32]struct{})
|
||||||
|
questMissionSet := make(map[store.QuestMissionKey]struct{})
|
||||||
|
seen := make(map[int32]bool)
|
||||||
|
|
||||||
|
var resolve func(conditionId int32, depth int)
|
||||||
|
resolve = func(conditionId int32, depth int) {
|
||||||
|
if conditionId == 0 || depth > 16 || seen[conditionId] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[conditionId] = true
|
||||||
|
c, ok := condById[conditionId]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
group := valuesByGroup[c.EvaluateConditionValueGroupId]
|
||||||
|
switch model.EvaluateConditionFunctionType(c.EvaluateConditionFunctionType) {
|
||||||
|
case model.EvaluateConditionFunctionTypeRecursion:
|
||||||
|
// Value-group entries are sub-condition ids; satisfying all leaves makes
|
||||||
|
// both AND and OR recursion conditions evaluate true.
|
||||||
|
for _, sub := range group {
|
||||||
|
resolve(int32(sub), depth+1)
|
||||||
|
}
|
||||||
|
case model.EvaluateConditionFunctionTypeMissionClear:
|
||||||
|
if v, ok := group[defaultGroupIndex]; ok {
|
||||||
|
missionSet[int32(v)] = struct{}{}
|
||||||
|
}
|
||||||
|
case model.EvaluateConditionFunctionTypeQuestMissionClear:
|
||||||
|
questId, ok1 := group[1]
|
||||||
|
questMissionId, ok2 := group[2]
|
||||||
|
if ok1 && ok2 {
|
||||||
|
questMissionSet[store.QuestMissionKey{
|
||||||
|
QuestId: int32(questId),
|
||||||
|
QuestMissionId: int32(questMissionId),
|
||||||
|
}] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range gimmicks {
|
||||||
|
switch model.GimmickType(g.GimmickType) {
|
||||||
|
case model.GimmickTypeReport, model.GimmickTypeCageMemory:
|
||||||
|
resolve(g.ClearEvaluateConditionId, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req := HiddenStoryRequirements{}
|
||||||
|
for id := range missionSet {
|
||||||
|
req.MissionIds = append(req.MissionIds, id)
|
||||||
|
}
|
||||||
|
for key := range questMissionSet {
|
||||||
|
req.QuestMissions = append(req.QuestMissions, key)
|
||||||
|
}
|
||||||
|
log.Printf("hidden-story requirements: %d missions, %d quest-missions", len(req.MissionIds), len(req.QuestMissions))
|
||||||
|
return req
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package masterdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LabyrinthChapter struct {
|
||||||
|
EventQuestChapterId int32
|
||||||
|
LatestSeasonNumber int32
|
||||||
|
StageOrders []int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabyrinthStageTier struct {
|
||||||
|
QuestMissionClearCount int32
|
||||||
|
Rewards []RewardItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabyrinthSeasonMilestone struct {
|
||||||
|
HeadQuestId int32
|
||||||
|
HeadStageOrder int32
|
||||||
|
Rewards []RewardItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type labyrinthStageKey struct {
|
||||||
|
ChapterId int32
|
||||||
|
StageOrder int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabyrinthCatalog struct {
|
||||||
|
ChaptersByOrder []LabyrinthChapter
|
||||||
|
ClearRewardsByStage map[labyrinthStageKey][]RewardItem
|
||||||
|
AccumTiersByStage map[labyrinthStageKey][]LabyrinthStageTier
|
||||||
|
SeasonMilestonesByChapter map[int32][]LabyrinthSeasonMilestone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LabyrinthCatalog) StageClearReward(chapterId, stageOrder int32) []RewardItem {
|
||||||
|
return c.ClearRewardsByStage[labyrinthStageKey{chapterId, stageOrder}]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LabyrinthCatalog) CollectAccumulationRewards(chapterId, stageOrder, oldCount, targetCount int32) ([]RewardItem, int32) {
|
||||||
|
var items []RewardItem
|
||||||
|
highest := int32(0)
|
||||||
|
for _, t := range c.AccumTiersByStage[labyrinthStageKey{chapterId, stageOrder}] {
|
||||||
|
if t.QuestMissionClearCount > oldCount && t.QuestMissionClearCount <= targetCount {
|
||||||
|
items = append(items, t.Rewards...)
|
||||||
|
if t.QuestMissionClearCount > highest {
|
||||||
|
highest = t.QuestMissionClearCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items, highest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LabyrinthCatalog) SeasonMilestones(chapterId int32) []LabyrinthSeasonMilestone {
|
||||||
|
return c.SeasonMilestonesByChapter[chapterId]
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadLabyrinthCatalog() *LabyrinthCatalog {
|
||||||
|
seasonRows, err := utils.ReadTable[EntityMEventQuestLabyrinthSeason]("m_event_quest_labyrinth_season")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[labyrinth] m_event_quest_labyrinth_season unavailable, labyrinth disabled: %v", err)
|
||||||
|
return &LabyrinthCatalog{}
|
||||||
|
}
|
||||||
|
stageRows, err := utils.ReadTable[EntityMEventQuestLabyrinthStage]("m_event_quest_labyrinth_stage")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[labyrinth] m_event_quest_labyrinth_stage unavailable, labyrinth disabled: %v", err)
|
||||||
|
return &LabyrinthCatalog{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// chapterId -> highest SeasonNumber
|
||||||
|
latestSeason := make(map[int32]int32)
|
||||||
|
for _, r := range seasonRows {
|
||||||
|
if r.SeasonNumber > latestSeason[r.EventQuestChapterId] {
|
||||||
|
latestSeason[r.EventQuestChapterId] = r.SeasonNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// chapterId -> stage orders
|
||||||
|
stagesByChapter := make(map[int32][]int32)
|
||||||
|
for _, r := range stageRows {
|
||||||
|
stagesByChapter[r.EventQuestChapterId] = append(stagesByChapter[r.EventQuestChapterId], r.StageOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters := make([]LabyrinthChapter, 0, len(latestSeason))
|
||||||
|
for chapterId, season := range latestSeason {
|
||||||
|
stages := stagesByChapter[chapterId]
|
||||||
|
sort.Slice(stages, func(i, j int) bool { return stages[i] < stages[j] })
|
||||||
|
chapters = append(chapters, LabyrinthChapter{
|
||||||
|
EventQuestChapterId: chapterId,
|
||||||
|
LatestSeasonNumber: season,
|
||||||
|
StageOrders: stages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(chapters, func(i, j int) bool {
|
||||||
|
return chapters[i].EventQuestChapterId < chapters[j].EventQuestChapterId
|
||||||
|
})
|
||||||
|
|
||||||
|
clearRewards, accumTiers, seasonMilestones := loadLabyrinthRewards(seasonRows, stageRows)
|
||||||
|
|
||||||
|
log.Printf("labyrinth catalog loaded: %d chapters, %d stages with clear rewards, %d with accumulation rewards, %d chapters with season rewards",
|
||||||
|
len(chapters), len(clearRewards), len(accumTiers), len(seasonMilestones))
|
||||||
|
return &LabyrinthCatalog{
|
||||||
|
ChaptersByOrder: chapters,
|
||||||
|
ClearRewardsByStage: clearRewards,
|
||||||
|
AccumTiersByStage: accumTiers,
|
||||||
|
SeasonMilestonesByChapter: seasonMilestones,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadLabyrinthRewards(seasonRows []EntityMEventQuestLabyrinthSeason, stageRows []EntityMEventQuestLabyrinthStage) (
|
||||||
|
clearRewards map[labyrinthStageKey][]RewardItem,
|
||||||
|
accumTiers map[labyrinthStageKey][]LabyrinthStageTier,
|
||||||
|
seasonMilestones map[int32][]LabyrinthSeasonMilestone,
|
||||||
|
) {
|
||||||
|
rewardGroupRows, err := utils.ReadTable[EntityMEventQuestLabyrinthRewardGroup]("m_event_quest_labyrinth_reward_group")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[labyrinth] m_event_quest_labyrinth_reward_group unavailable, rewards disabled: %v", err)
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reward group id -> reward items
|
||||||
|
itemsByRewardGroup := make(map[int32][]RewardItem)
|
||||||
|
for _, r := range rewardGroupRows {
|
||||||
|
itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId] = append(itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId], RewardItem{
|
||||||
|
PossessionType: r.PossessionType,
|
||||||
|
PossessionId: r.PossessionId,
|
||||||
|
Count: r.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// per-stage one-time clear reward
|
||||||
|
clearRewards = make(map[labyrinthStageKey][]RewardItem)
|
||||||
|
for _, r := range stageRows {
|
||||||
|
if r.StageClearRewardGroupId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if items := itemsByRewardGroup[r.StageClearRewardGroupId]; len(items) > 0 {
|
||||||
|
clearRewards[labyrinthStageKey{r.EventQuestChapterId, r.StageOrder}] = items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if accumGroupRows, err := utils.ReadTable[EntityMEventQuestLabyrinthStageAccumulationRewardGroup]("m_event_quest_labyrinth_stage_accumulation_reward_group"); err != nil {
|
||||||
|
log.Printf("[labyrinth] m_event_quest_labyrinth_stage_accumulation_reward_group unavailable, accumulation rewards disabled: %v", err)
|
||||||
|
} else {
|
||||||
|
// accumulation group id -> tiers (threshold + resolved reward items)
|
||||||
|
tiersByGroup := make(map[int32][]LabyrinthStageTier)
|
||||||
|
for _, r := range accumGroupRows {
|
||||||
|
tiersByGroup[r.EventQuestLabyrinthStageAccumulationRewardGroupId] = append(tiersByGroup[r.EventQuestLabyrinthStageAccumulationRewardGroupId], LabyrinthStageTier{
|
||||||
|
QuestMissionClearCount: r.QuestMissionClearCount,
|
||||||
|
Rewards: itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
accumTiers = make(map[labyrinthStageKey][]LabyrinthStageTier)
|
||||||
|
for _, r := range stageRows {
|
||||||
|
if r.StageAccumulationRewardGroupId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tiers := tiersByGroup[r.StageAccumulationRewardGroupId]
|
||||||
|
sort.Slice(tiers, func(i, j int) bool {
|
||||||
|
return tiers[i].QuestMissionClearCount < tiers[j].QuestMissionClearCount
|
||||||
|
})
|
||||||
|
accumTiers[labyrinthStageKey{r.EventQuestChapterId, r.StageOrder}] = tiers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// per-chapter season-reward milestones
|
||||||
|
if seasonRewardRows, err := utils.ReadTable[EntityMEventQuestLabyrinthSeasonRewardGroup]("m_event_quest_labyrinth_season_reward_group"); err != nil {
|
||||||
|
log.Printf("[labyrinth] m_event_quest_labyrinth_season_reward_group unavailable, season rewards disabled: %v", err)
|
||||||
|
} else {
|
||||||
|
seasonMilestones = buildLabyrinthSeasonMilestones(seasonRows, seasonRewardRows, itemsByRewardGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clearRewards, accumTiers, seasonMilestones
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLabyrinthSeasonMilestones(
|
||||||
|
seasonRows []EntityMEventQuestLabyrinthSeason,
|
||||||
|
seasonRewardRows []EntityMEventQuestLabyrinthSeasonRewardGroup,
|
||||||
|
itemsByRewardGroup map[int32][]RewardItem,
|
||||||
|
) map[int32][]LabyrinthSeasonMilestone {
|
||||||
|
// chapter -> SeasonRewardGroupId (all seasons of a chapter share one)
|
||||||
|
groupByChapter := make(map[int32]int32)
|
||||||
|
for _, r := range seasonRows {
|
||||||
|
groupByChapter[r.EventQuestChapterId] = r.SeasonRewardGroupId
|
||||||
|
}
|
||||||
|
// SeasonRewardGroupId -> its rows, in table order
|
||||||
|
rowsByGroup := make(map[int32][]EntityMEventQuestLabyrinthSeasonRewardGroup)
|
||||||
|
for _, r := range seasonRewardRows {
|
||||||
|
rowsByGroup[r.EventQuestLabyrinthSeasonRewardGroupId] = append(rowsByGroup[r.EventQuestLabyrinthSeasonRewardGroupId], r)
|
||||||
|
}
|
||||||
|
|
||||||
|
milestones := make(map[int32][]LabyrinthSeasonMilestone)
|
||||||
|
for chapterId, seasonGroupId := range groupByChapter {
|
||||||
|
rows := rowsByGroup[seasonGroupId]
|
||||||
|
if len(rows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// rank distinct reward-group ids ascending -> 1-based head stage order
|
||||||
|
stageByRewardGroup := make(map[int32]int32)
|
||||||
|
var distinct []int32
|
||||||
|
for _, r := range rows {
|
||||||
|
if _, seen := stageByRewardGroup[r.EventQuestLabyrinthRewardGroupId]; !seen {
|
||||||
|
stageByRewardGroup[r.EventQuestLabyrinthRewardGroupId] = 0
|
||||||
|
distinct = append(distinct, r.EventQuestLabyrinthRewardGroupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(distinct, func(i, j int) bool { return distinct[i] < distinct[j] })
|
||||||
|
for i, gid := range distinct {
|
||||||
|
stageByRewardGroup[gid] = int32(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
list := make([]LabyrinthSeasonMilestone, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
list = append(list, LabyrinthSeasonMilestone{
|
||||||
|
HeadQuestId: r.HeadQuestId,
|
||||||
|
HeadStageOrder: stageByRewardGroup[r.EventQuestLabyrinthRewardGroupId],
|
||||||
|
Rewards: itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
milestones[chapterId] = list
|
||||||
|
}
|
||||||
|
return milestones
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package masterdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,29 +20,72 @@ type LoginBonusReward struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoginBonusCatalog struct {
|
type LoginBonusCatalog struct {
|
||||||
stamps map[loginBonusStampKey]LoginBonusReward
|
stamps map[loginBonusStampKey]LoginBonusReward
|
||||||
|
bonusPages map[int32][]int32
|
||||||
|
totalPages map[int32]int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LoginBonusCatalog) LookupStampReward(loginBonusId, pageNumber, stampNumber int32) (LoginBonusReward, bool) {
|
func (c *LoginBonusCatalog) LookupStampReward(loginBonusId, pageNumber, stampNumber int32) (LoginBonusReward, bool) {
|
||||||
entry, ok := c.stamps[loginBonusStampKey{loginBonusId, pageNumber, stampNumber}]
|
pages := c.bonusPages[loginBonusId]
|
||||||
|
lower := int32(-1)
|
||||||
|
for _, p := range pages {
|
||||||
|
if p <= pageNumber {
|
||||||
|
lower = p
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lower < 0 {
|
||||||
|
return LoginBonusReward{}, false
|
||||||
|
}
|
||||||
|
entry, ok := c.stamps[loginBonusStampKey{loginBonusId, lower, stampNumber}]
|
||||||
return entry, ok
|
return entry, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *LoginBonusCatalog) TotalPageCount(loginBonusId int32) int32 {
|
||||||
|
return c.totalPages[loginBonusId]
|
||||||
|
}
|
||||||
|
|
||||||
func LoadLoginBonusCatalog() *LoginBonusCatalog {
|
func LoadLoginBonusCatalog() *LoginBonusCatalog {
|
||||||
stamps, err := utils.ReadTable[EntityMLoginBonusStamp]("m_login_bonus_stamp")
|
stamps, err := utils.ReadTable[EntityMLoginBonusStamp]("m_login_bonus_stamp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("load login bonus stamp table: %v", err)
|
log.Fatalf("load login bonus stamp table: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cat := &LoginBonusCatalog{
|
bonuses, err := utils.ReadTable[EntityMLoginBonus]("m_login_bonus")
|
||||||
stamps: make(map[loginBonusStampKey]LoginBonusReward, len(stamps)),
|
if err != nil {
|
||||||
|
log.Fatalf("load login bonus table: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cat := &LoginBonusCatalog{
|
||||||
|
stamps: make(map[loginBonusStampKey]LoginBonusReward, len(stamps)),
|
||||||
|
bonusPages: make(map[int32][]int32),
|
||||||
|
totalPages: make(map[int32]int32, len(bonuses)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, b := range bonuses {
|
||||||
|
cat.totalPages[b.LoginBonusId] = b.TotalPageCount
|
||||||
|
}
|
||||||
|
|
||||||
|
seenPages := make(map[loginBonusStampKey]struct{})
|
||||||
for _, s := range stamps {
|
for _, s := range stamps {
|
||||||
cat.stamps[loginBonusStampKey{s.LoginBonusId, s.LowerPageNumber, s.StampNumber}] = LoginBonusReward{
|
cat.stamps[loginBonusStampKey{s.LoginBonusId, s.LowerPageNumber, s.StampNumber}] = LoginBonusReward{
|
||||||
PossessionType: s.RewardPossessionType,
|
PossessionType: s.RewardPossessionType,
|
||||||
PossessionId: s.RewardPossessionId,
|
PossessionId: s.RewardPossessionId,
|
||||||
Count: s.RewardCount,
|
Count: s.RewardCount,
|
||||||
}
|
}
|
||||||
|
dedup := loginBonusStampKey{LoginBonusId: s.LoginBonusId, LowerPageNumber: s.LowerPageNumber}
|
||||||
|
if _, exists := seenPages[dedup]; !exists {
|
||||||
|
seenPages[dedup] = struct{}{}
|
||||||
|
cat.bonusPages[s.LoginBonusId] = append(cat.bonusPages[s.LoginBonusId], s.LowerPageNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for id := range cat.bonusPages {
|
||||||
|
sort.Slice(cat.bonusPages[id], func(i, j int) bool {
|
||||||
|
return cat.bonusPages[id][i] < cat.bonusPages[id][j]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return cat
|
return cat
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package masterdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
@@ -32,8 +33,9 @@ func BuildExpThresholds(paramMapRows []EntityMNumericalParameterMap, mapId int32
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MaterialCatalog struct {
|
type MaterialCatalog struct {
|
||||||
All map[int32]EntityMMaterial
|
All map[int32]EntityMMaterial
|
||||||
ByType map[model.MaterialType]map[int32]EntityMMaterial
|
ByType map[model.MaterialType]map[int32]EntityMMaterial
|
||||||
|
SaleObtain map[int32][]EntityMMaterialSaleObtainPossession
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMaterialCatalog() (*MaterialCatalog, error) {
|
func LoadMaterialCatalog() (*MaterialCatalog, error) {
|
||||||
@@ -43,8 +45,9 @@ func LoadMaterialCatalog() (*MaterialCatalog, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
catalog := &MaterialCatalog{
|
catalog := &MaterialCatalog{
|
||||||
All: make(map[int32]EntityMMaterial, len(rows)),
|
All: make(map[int32]EntityMMaterial, len(rows)),
|
||||||
ByType: make(map[model.MaterialType]map[int32]EntityMMaterial),
|
ByType: make(map[model.MaterialType]map[int32]EntityMMaterial),
|
||||||
|
SaleObtain: make(map[int32][]EntityMMaterialSaleObtainPossession),
|
||||||
}
|
}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
catalog.All[row.MaterialId] = row
|
catalog.All[row.MaterialId] = row
|
||||||
@@ -54,5 +57,15 @@ func LoadMaterialCatalog() (*MaterialCatalog, error) {
|
|||||||
}
|
}
|
||||||
catalog.ByType[mt][row.MaterialId] = row
|
catalog.ByType[mt][row.MaterialId] = row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saleRows, err := utils.ReadTable[EntityMMaterialSaleObtainPossession]("m_material_sale_obtain_possession")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("material catalog: sale-obtain table unavailable, side rewards on sell will be skipped: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, row := range saleRows {
|
||||||
|
catalog.SaleObtain[row.MaterialSaleObtainPossessionId] = append(catalog.SaleObtain[row.MaterialSaleObtainPossessionId], row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return catalog, nil
|
return catalog, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ type QuestCatalog struct {
|
|||||||
TutorialUnlockConditions []EntityMTutorialUnlockCondition
|
TutorialUnlockConditions []EntityMTutorialUnlockCondition
|
||||||
ChapterLastSceneByQuestId map[int32]int32
|
ChapterLastSceneByQuestId map[int32]int32
|
||||||
SeasonIdByRouteId map[int32]int32
|
SeasonIdByRouteId map[int32]int32
|
||||||
|
RoutesBySeason map[int32][]int32
|
||||||
|
RouteCompletionQuestId map[int32]int32
|
||||||
|
BattleOnlyTargetSceneByQuestId map[int32]int32
|
||||||
|
|
||||||
UserExpThresholds []int32
|
UserExpThresholds []int32
|
||||||
CharacterExpThresholds []int32
|
CharacterExpThresholds []int32
|
||||||
@@ -113,8 +116,53 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
return nil, fmt.Errorf("load main quest route table: %w", err)
|
return nil, fmt.Errorf("load main quest route table: %w", err)
|
||||||
}
|
}
|
||||||
seasonIdByRouteId := make(map[int32]int32, len(routes))
|
seasonIdByRouteId := make(map[int32]int32, len(routes))
|
||||||
|
routesBySeason := make(map[int32][]int32, len(routes))
|
||||||
|
sortOrderByRoute := make(map[int32]int32, len(routes))
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
|
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
|
||||||
|
routesBySeason[r.MainQuestSeasonId] = append(routesBySeason[r.MainQuestSeasonId], r.MainQuestRouteId)
|
||||||
|
sortOrderByRoute[r.MainQuestRouteId] = r.SortOrder
|
||||||
|
}
|
||||||
|
for seasonId, ids := range routesBySeason {
|
||||||
|
s := ids
|
||||||
|
sort.Slice(s, func(i, j int) bool { return sortOrderByRoute[s[i]] > sortOrderByRoute[s[j]] })
|
||||||
|
routesBySeason[seasonId] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
anotherReplayConds, err := utils.ReadTable[EntityMMainQuestRouteAnotherReplayFlowUnlockCondition]("m_main_quest_route_another_replay_flow_unlock_condition")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load main quest route another replay flow unlock condition table: %w", err)
|
||||||
|
}
|
||||||
|
evaluateConds, err := utils.ReadTable[EntityMEvaluateCondition]("m_evaluate_condition")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load evaluate condition table: %w", err)
|
||||||
|
}
|
||||||
|
valueGroupByConditionId := make(map[int32]int32, len(evaluateConds))
|
||||||
|
for _, c := range evaluateConds {
|
||||||
|
valueGroupByConditionId[c.EvaluateConditionId] = c.EvaluateConditionValueGroupId
|
||||||
|
}
|
||||||
|
evaluateValueGroups, err := utils.ReadTable[EntityMEvaluateConditionValueGroup]("m_evaluate_condition_value_group")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load evaluate condition value group table: %w", err)
|
||||||
|
}
|
||||||
|
valueByGroupId := make(map[int32]int32, len(evaluateValueGroups))
|
||||||
|
for _, vg := range evaluateValueGroups {
|
||||||
|
if _, exists := valueByGroupId[vg.EvaluateConditionValueGroupId]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
valueByGroupId[vg.EvaluateConditionValueGroupId] = int32(vg.Value)
|
||||||
|
}
|
||||||
|
routeCompletionQuestId := make(map[int32]int32, len(anotherReplayConds))
|
||||||
|
for _, c := range anotherReplayConds {
|
||||||
|
valueGroupId, ok := valueGroupByConditionId[c.UnlockEvaluateConditionId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
questId, ok := valueByGroupId[valueGroupId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
routeCompletionQuestId[c.MainQuestRouteId] = questId
|
||||||
}
|
}
|
||||||
|
|
||||||
firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch")
|
firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch")
|
||||||
@@ -238,6 +286,15 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
return nil, fmt.Errorf("load tutorial unlock condition table: %w", err)
|
return nil, fmt.Errorf("load tutorial unlock condition table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
battleOnlyTargetSceneByQuestId := make(map[int32]int32)
|
||||||
|
for _, scene := range scenes {
|
||||||
|
if scene.IsBattleOnlyTarget {
|
||||||
|
if _, exists := battleOnlyTargetSceneByQuestId[scene.QuestId]; !exists {
|
||||||
|
battleOnlyTargetSceneByQuestId[scene.QuestId] = scene.QuestSceneId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
paramMapRows, err := LoadParameterMap()
|
paramMapRows, err := LoadParameterMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -529,6 +586,9 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
TutorialUnlockConditions: tutorialUnlockConds,
|
TutorialUnlockConditions: tutorialUnlockConds,
|
||||||
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
||||||
SeasonIdByRouteId: seasonIdByRouteId,
|
SeasonIdByRouteId: seasonIdByRouteId,
|
||||||
|
RoutesBySeason: routesBySeason,
|
||||||
|
RouteCompletionQuestId: routeCompletionQuestId,
|
||||||
|
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
||||||
|
|
||||||
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
||||||
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
||||||
@@ -545,3 +605,8 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
PartsCatalog: partsCatalog,
|
PartsCatalog: partsCatalog,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *QuestCatalog) BattleOnlyTargetSceneIdFor(questId int32) (int32, bool) {
|
||||||
|
v, ok := q.BattleOnlyTargetSceneByQuestId[questId]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,35 @@ package masterdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SideStorySceneInfo struct {
|
||||||
|
SceneId int32
|
||||||
|
Type model.SideStorySceneIdType
|
||||||
|
}
|
||||||
|
|
||||||
|
type SideStoryQuestInfo struct {
|
||||||
|
SideStoryQuestId int32
|
||||||
|
Scenes []SideStorySceneInfo // the 7 scenes, one per type
|
||||||
|
Quests []int32 // ordered event quests (the chapter+difficulty sequence)
|
||||||
|
}
|
||||||
|
|
||||||
type SideStoryCatalog struct {
|
type SideStoryCatalog struct {
|
||||||
FirstSceneByQuestId map[int32]int32
|
QuestById map[int32]*SideStoryQuestInfo
|
||||||
|
ChapterByEventQuestId map[int32]int32 // event quest id -> side story chapter id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *SideStoryQuestInfo) SceneIdByType(t model.SideStorySceneIdType) (int32, bool) {
|
||||||
|
for _, s := range q.Scenes {
|
||||||
|
if s.Type == t {
|
||||||
|
return s.SceneId, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadSideStoryCatalog() *SideStoryCatalog {
|
func LoadSideStoryCatalog() *SideStoryCatalog {
|
||||||
@@ -14,14 +38,90 @@ func LoadSideStoryCatalog() *SideStoryCatalog {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("load side story quest scene table: %v", err)
|
log.Fatalf("load side story quest scene table: %v", err)
|
||||||
}
|
}
|
||||||
|
limitContents, err := utils.ReadTable[EntityMSideStoryQuestLimitContent]("m_side_story_quest_limit_content")
|
||||||
firstScene := make(map[int32]int32, len(scenes)/7)
|
if err != nil {
|
||||||
for _, s := range scenes {
|
log.Fatalf("load side story quest limit content table: %v", err)
|
||||||
if s.SortOrder == 1 {
|
}
|
||||||
firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId
|
seqGroups, err := utils.ReadTable[EntityMEventQuestSequenceGroup]("m_event_quest_sequence_group")
|
||||||
}
|
if err != nil {
|
||||||
|
log.Fatalf("load event quest sequence group table: %v", err)
|
||||||
|
}
|
||||||
|
sequences, err := utils.ReadTable[EntityMEventQuestSequence]("m_event_quest_sequence")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("load event quest sequence table: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("side story catalog loaded: %d quests", len(firstScene))
|
seqRows := make(map[int32][]EntityMEventQuestSequence)
|
||||||
return &SideStoryCatalog{FirstSceneByQuestId: firstScene}
|
for _, s := range sequences {
|
||||||
|
seqRows[s.EventQuestSequenceId] = append(seqRows[s.EventQuestSequenceId], s)
|
||||||
|
}
|
||||||
|
orderedQuestIds := make(map[int32][]int32, len(seqRows))
|
||||||
|
for seqId, rows := range seqRows {
|
||||||
|
sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder })
|
||||||
|
ids := make([]int32, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
ids[i] = r.QuestId
|
||||||
|
}
|
||||||
|
orderedQuestIds[seqId] = ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// (chapterId, difficulty) -> sequenceId. Sequence group id == chapter id.
|
||||||
|
type chapDiff struct{ chapter, difficulty int32 }
|
||||||
|
sequenceByChapterDiff := make(map[chapDiff]int32, len(seqGroups))
|
||||||
|
for _, g := range seqGroups {
|
||||||
|
sequenceByChapterDiff[chapDiff{g.EventQuestSequenceGroupId, g.DifficultyType}] = g.EventQuestSequenceId
|
||||||
|
}
|
||||||
|
|
||||||
|
// sideStoryQuestId -> limit content row. Limit content id == side story quest id.
|
||||||
|
limitByQuest := make(map[int32]EntityMSideStoryQuestLimitContent, len(limitContents))
|
||||||
|
for _, lc := range limitContents {
|
||||||
|
limitByQuest[lc.SideStoryQuestLimitContentId] = lc
|
||||||
|
}
|
||||||
|
|
||||||
|
// sideStoryQuestId -> scene rows
|
||||||
|
scenesByQuest := make(map[int32][]EntityMSideStoryQuestScene)
|
||||||
|
for _, sc := range scenes {
|
||||||
|
scenesByQuest[sc.SideStoryQuestId] = append(scenesByQuest[sc.SideStoryQuestId], sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
questById := make(map[int32]*SideStoryQuestInfo, len(scenesByQuest))
|
||||||
|
chapterByEventQuest := make(map[int32]int32)
|
||||||
|
|
||||||
|
for ssqId, rows := range scenesByQuest {
|
||||||
|
sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder })
|
||||||
|
|
||||||
|
var orderedQuests []int32
|
||||||
|
var chapterId, difficulty int32
|
||||||
|
if lc, ok := limitByQuest[ssqId]; ok {
|
||||||
|
chapterId = lc.EventQuestChapterId
|
||||||
|
difficulty = lc.DifficultyType
|
||||||
|
if seqId, ok := sequenceByChapterDiff[chapDiff{chapterId, difficulty}]; ok {
|
||||||
|
orderedQuests = orderedQuestIds[seqId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chapterId != 0 {
|
||||||
|
for _, questId := range orderedQuests {
|
||||||
|
chapterByEventQuest[questId] = chapterId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &SideStoryQuestInfo{
|
||||||
|
SideStoryQuestId: ssqId,
|
||||||
|
Scenes: make([]SideStorySceneInfo, 0, len(rows)),
|
||||||
|
Quests: orderedQuests,
|
||||||
|
}
|
||||||
|
for _, sc := range rows {
|
||||||
|
info.Scenes = append(info.Scenes, SideStorySceneInfo{
|
||||||
|
SceneId: sc.SideStoryQuestSceneId,
|
||||||
|
Type: model.SideStorySceneIdType(sc.SortOrder),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
questById[ssqId] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("side story catalog loaded: %d quests, %d scenes", len(questById), len(scenes))
|
||||||
|
return &SideStoryCatalog{
|
||||||
|
QuestById: questById,
|
||||||
|
ChapterByEventQuestId: chapterByEventQuest,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package masterdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TowerTier struct {
|
||||||
|
QuestMissionClearCount int32
|
||||||
|
Rewards []RewardItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type TowerCatalog struct {
|
||||||
|
TiersByChapter map[int32][]TowerTier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TowerCatalog) CollectRewards(chapterId, oldCount, targetCount int32) ([]RewardItem, int32) {
|
||||||
|
var items []RewardItem
|
||||||
|
highest := int32(0)
|
||||||
|
for _, t := range c.TiersByChapter[chapterId] {
|
||||||
|
if t.QuestMissionClearCount > oldCount && t.QuestMissionClearCount <= targetCount {
|
||||||
|
items = append(items, t.Rewards...)
|
||||||
|
if t.QuestMissionClearCount > highest {
|
||||||
|
highest = t.QuestMissionClearCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items, highest
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadTowerCatalog() *TowerCatalog {
|
||||||
|
// chapterId -> accumulation reward group id
|
||||||
|
accumRewardRows, err := utils.ReadTable[EntityMEventQuestTowerAccumulationReward]("m_event_quest_tower_accumulation_reward")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("load event quest tower accumulation reward table: %v", err)
|
||||||
|
}
|
||||||
|
groupByChapter := make(map[int32]int32, len(accumRewardRows))
|
||||||
|
for _, r := range accumRewardRows {
|
||||||
|
groupByChapter[r.EventQuestChapterId] = r.EventQuestTowerAccumulationRewardGroupId
|
||||||
|
}
|
||||||
|
|
||||||
|
// reward group id -> reward items
|
||||||
|
rewardGroupRows, err := utils.ReadTable[EntityMEventQuestTowerRewardGroup]("m_event_quest_tower_reward_group")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("load event quest tower reward group table: %v", err)
|
||||||
|
}
|
||||||
|
itemsByRewardGroup := make(map[int32][]RewardItem)
|
||||||
|
for _, r := range rewardGroupRows {
|
||||||
|
itemsByRewardGroup[r.EventQuestTowerRewardGroupId] = append(itemsByRewardGroup[r.EventQuestTowerRewardGroupId], RewardItem{
|
||||||
|
PossessionType: r.PossessionType,
|
||||||
|
PossessionId: r.PossessionId,
|
||||||
|
Count: r.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// accumulation group id -> tiers (threshold + resolved reward items)
|
||||||
|
accumGroupRows, err := utils.ReadTable[EntityMEventQuestTowerAccumulationRewardGroup]("m_event_quest_tower_accumulation_reward_group")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("load event quest tower accumulation reward group table: %v", err)
|
||||||
|
}
|
||||||
|
tiersByGroup := make(map[int32][]TowerTier)
|
||||||
|
for _, r := range accumGroupRows {
|
||||||
|
tiersByGroup[r.EventQuestTowerAccumulationRewardGroupId] = append(tiersByGroup[r.EventQuestTowerAccumulationRewardGroupId], TowerTier{
|
||||||
|
QuestMissionClearCount: r.QuestMissionClearCount,
|
||||||
|
Rewards: itemsByRewardGroup[r.EventQuestTowerRewardGroupId],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve per-chapter, sorted ascending by threshold
|
||||||
|
tiersByChapter := make(map[int32][]TowerTier, len(groupByChapter))
|
||||||
|
for chapterId, groupId := range groupByChapter {
|
||||||
|
tiers := tiersByGroup[groupId]
|
||||||
|
sort.Slice(tiers, func(i, j int) bool {
|
||||||
|
return tiers[i].QuestMissionClearCount < tiers[j].QuestMissionClearCount
|
||||||
|
})
|
||||||
|
tiersByChapter[chapterId] = tiers
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("tower catalog loaded: %d chapters", len(tiersByChapter))
|
||||||
|
|
||||||
|
return &TowerCatalog{TiersByChapter: tiersByChapter}
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ type WeaponCatalog struct {
|
|||||||
BaseExpByEnhanceId map[int32]int32
|
BaseExpByEnhanceId map[int32]int32
|
||||||
ReleaseConditionsByGroupId map[int32][]EntityMWeaponStoryReleaseConditionGroup
|
ReleaseConditionsByGroupId map[int32][]EntityMWeaponStoryReleaseConditionGroup
|
||||||
|
|
||||||
|
LevelingEnhanceIdByWeaponId map[int32]int32
|
||||||
|
|
||||||
AwakenByWeaponId map[int32]EntityMWeaponAwaken
|
AwakenByWeaponId map[int32]EntityMWeaponAwaken
|
||||||
AwakenMaterialsByGroupId map[int32][]EntityMWeaponAwakenMaterialGroup
|
AwakenMaterialsByGroupId map[int32][]EntityMWeaponAwakenMaterialGroup
|
||||||
}
|
}
|
||||||
@@ -142,6 +144,8 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) {
|
|||||||
BaseExpByEnhanceId: make(map[int32]int32, len(enhanceRows)),
|
BaseExpByEnhanceId: make(map[int32]int32, len(enhanceRows)),
|
||||||
ReleaseConditionsByGroupId: make(map[int32][]EntityMWeaponStoryReleaseConditionGroup),
|
ReleaseConditionsByGroupId: make(map[int32][]EntityMWeaponStoryReleaseConditionGroup),
|
||||||
|
|
||||||
|
LevelingEnhanceIdByWeaponId: make(map[int32]int32, len(weapons)),
|
||||||
|
|
||||||
AwakenByWeaponId: make(map[int32]EntityMWeaponAwaken, len(awakenRows)),
|
AwakenByWeaponId: make(map[int32]EntityMWeaponAwaken, len(awakenRows)),
|
||||||
AwakenMaterialsByGroupId: make(map[int32][]EntityMWeaponAwakenMaterialGroup),
|
AwakenMaterialsByGroupId: make(map[int32][]EntityMWeaponAwakenMaterialGroup),
|
||||||
}
|
}
|
||||||
@@ -342,5 +346,28 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) {
|
|||||||
}
|
}
|
||||||
log.Printf("[WeaponCatalog] rarity fallback: assigned synthetic enhance IDs to %d weapons", fallbackCount)
|
log.Printf("[WeaponCatalog] rarity fallback: assigned synthetic enhance IDs to %d weapons", fallbackCount)
|
||||||
|
|
||||||
|
// Build LevelingEnhanceIdByWeaponId: every member of an evolution chain
|
||||||
|
// inherits the chain root's (post-rarity-fallback) WeaponSpecificEnhanceId
|
||||||
|
// so cumulative exp stays consistent across Evolve. Weapons not in any
|
||||||
|
// chain map to their own enhance id.
|
||||||
|
for _, rows := range grouped {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rootMaster, ok := catalog.Weapons[rows[0].WeaponId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rootEnhanceId := rootMaster.WeaponSpecificEnhanceId
|
||||||
|
for _, row := range rows {
|
||||||
|
catalog.LevelingEnhanceIdByWeaponId[row.WeaponId] = rootEnhanceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for wid, w := range catalog.Weapons {
|
||||||
|
if _, exists := catalog.LevelingEnhanceIdByWeaponId[wid]; !exists {
|
||||||
|
catalog.LevelingEnhanceIdByWeaponId[wid] = w.WeaponSpecificEnhanceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return catalog, nil
|
return catalog, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package masterdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebviewPanelMissionCatalog struct {
|
||||||
|
PageIds []int32 // every WebviewPanelMissionPageId, sorted ascending
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadWebviewPanelMissionCatalog() *WebviewPanelMissionCatalog {
|
||||||
|
rows, err := utils.ReadTable[EntityMWebviewPanelMissionPage]("m_webview_panel_mission_page")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("load webview panel mission page table: %v", err)
|
||||||
|
return &WebviewPanelMissionCatalog{}
|
||||||
|
}
|
||||||
|
ids := make([]int32, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
ids = append(ids, r.WebviewPanelMissionPageId)
|
||||||
|
}
|
||||||
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||||
|
return &WebviewPanelMissionCatalog{PageIds: ids}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type GimmickType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
GimmickTypeUnknown GimmickType = 0
|
||||||
|
GimmickTypeCageTreasureHunt GimmickType = 1 // "Fickle Black Birds" — in-cage flick-and-tap birds
|
||||||
|
GimmickTypeCageIntervalDropItem GimmickType = 2 // "Lost Items" — in-cage 3-dot drops that respawn on interval
|
||||||
|
GimmickTypeBrokenObelisk GimmickType = 3 // "Broken Scarecrow" (per tip text); zero rows in m_gimmick, unused
|
||||||
|
GimmickTypeIronGrill GimmickType = 4 // unused (zero rows in m_gimmick), in-game name unknown
|
||||||
|
GimmickTypeRadioMessage GimmickType = 5 // unused (zero rows in m_gimmick); client has GimmickRadioMessage class but no data
|
||||||
|
GimmickTypeFirstBrokenObelisk GimmickType = 6 // variant of Broken Scarecrow; zero rows in m_gimmick, unused
|
||||||
|
GimmickTypeMapOnlyCageTreasureHunt GimmickType = 7 // "Hidden Black Birds" — world-map birds; per-tap reward from m_cage_ornament_reward
|
||||||
|
GimmickTypeMapOnlyCageIntervalDrop GimmickType = 8 // map-side variant of Lost Items
|
||||||
|
GimmickTypeReport GimmickType = 9 // "Hidden Stories" — hidden mission markers
|
||||||
|
GimmickTypeCageMemory GimmickType = 10 // "Lost Archives" — collectible library entries (one-shot ImportantItem type-4)
|
||||||
|
GimmickTypeMapOnlyHideObelisk GimmickType = 11 // "Stray Scarecrow" — world-map scarecrows (not yet implemented)
|
||||||
|
)
|
||||||
@@ -12,6 +12,11 @@ const (
|
|||||||
QuestFlowTypeAnotherRouteReplayFlow QuestFlowType = 4
|
QuestFlowTypeAnotherRouteReplayFlow QuestFlowType = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func IsReplayQuestFlowType(t int32) bool {
|
||||||
|
return t == int32(QuestFlowTypeReplayFlow) ||
|
||||||
|
t == int32(QuestFlowTypeAnotherRouteReplayFlow)
|
||||||
|
}
|
||||||
|
|
||||||
func (t QuestFlowType) String() string {
|
func (t QuestFlowType) String() string {
|
||||||
switch t {
|
switch t {
|
||||||
case QuestFlowTypeUnknown:
|
case QuestFlowTypeUnknown:
|
||||||
@@ -38,6 +43,15 @@ const (
|
|||||||
QuestResultTypeFullResult QuestResultType = 3
|
QuestResultTypeFullResult QuestResultType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MissionProgressStatusType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
MissionProgressStatusTypeUnknown MissionProgressStatusType = 0
|
||||||
|
MissionProgressStatusTypeInProgress MissionProgressStatusType = 1
|
||||||
|
MissionProgressStatusTypeClear MissionProgressStatusType = 2
|
||||||
|
MissionProgressStatusTypeRewardReceived MissionProgressStatusType = 9
|
||||||
|
)
|
||||||
|
|
||||||
type QuestSceneType int32
|
type QuestSceneType int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -155,6 +169,19 @@ type SideStoryQuestStateType int32
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
SideStoryQuestStateUnknown SideStoryQuestStateType = 0
|
SideStoryQuestStateUnknown SideStoryQuestStateType = 0
|
||||||
SideStoryQuestStateActive SideStoryQuestStateType = 1
|
SideStoryQuestStateActive SideStoryQuestStateType = 2
|
||||||
SideStoryQuestStateCleared SideStoryQuestStateType = 2
|
SideStoryQuestStateCleared SideStoryQuestStateType = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type SideStorySceneIdType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
SideStorySceneInvalid SideStorySceneIdType = 0
|
||||||
|
SideStorySceneIntroduction SideStorySceneIdType = 1
|
||||||
|
SideStoryScenePlayGeneralQuest SideStorySceneIdType = 2
|
||||||
|
SideStorySceneUnlockLastQuest SideStorySceneIdType = 3
|
||||||
|
SideStoryScenePlayLastQuest SideStorySceneIdType = 4
|
||||||
|
SideStorySceneOutroduction SideStorySceneIdType = 5
|
||||||
|
SideStorySceneShowCostumeAcquisition SideStorySceneIdType = 6
|
||||||
|
SideStoryScenePlayFreeQuest SideStorySceneIdType = 7
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
|
|||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
|||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
|
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||||
@@ -53,15 +54,31 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
|||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreClearedAfterRetire(user, questId, isRetired)
|
||||||
|
|
||||||
|
user.EventQuest.CurrentEventQuestChapterId = 0
|
||||||
user.EventQuest.CurrentQuestId = 0
|
user.EventQuest.CurrentQuestId = 0
|
||||||
user.EventQuest.CurrentQuestSceneId = 0
|
user.EventQuest.CurrentQuestSceneId = 0
|
||||||
user.EventQuest.HeadQuestSceneId = 0
|
user.EventQuest.HeadQuestSceneId = 0
|
||||||
|
user.EventQuest.LatestVersion = nowMillis
|
||||||
|
|
||||||
h.clearQuestMissions(user, questId, nowMillis)
|
h.clearQuestMissions(user, questId, nowMillis)
|
||||||
|
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) recordSideStoryLimitContentStatus(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
|
chapterId, ok := h.SideStoryChapterByEventQuestId[questId]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st := user.QuestLimitContentStatus[questId]
|
||||||
|
st.LimitContentQuestStatusType = 1
|
||||||
|
st.EventQuestChapterId = chapterId
|
||||||
|
st.LatestVersion = nowMillis
|
||||||
|
user.QuestLimitContentStatus[questId] = st
|
||||||
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) {
|
||||||
h.HandleQuestRestart(user, questId, nowMillis)
|
h.HandleQuestRestart(user, questId, nowMillis)
|
||||||
|
|
||||||
@@ -70,8 +87,7 @@ func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuest
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
scene, ok := h.SceneById[questSceneId]
|
if _, ok := h.SceneById[questSceneId]; !ok {
|
||||||
if !ok {
|
|
||||||
log.Printf("[HandleEventQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
log.Printf("[HandleEventQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -82,8 +98,4 @@ func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, ques
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
h.applySceneGrants(user, questSceneId, nowMillis)
|
||||||
|
|
||||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
|
||||||
h.clearQuestMissions(user, scene.QuestId, nowMillis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
|
|||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||||
@@ -51,6 +51,8 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
|
|||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreClearedAfterRetire(user, questId, isRetired)
|
||||||
|
|
||||||
user.ExtraQuest.CurrentQuestId = 0
|
user.ExtraQuest.CurrentQuestId = 0
|
||||||
user.ExtraQuest.CurrentQuestSceneId = 0
|
user.ExtraQuest.CurrentQuestSceneId = 0
|
||||||
user.ExtraQuest.HeadQuestSceneId = 0
|
user.ExtraQuest.HeadQuestSceneId = 0
|
||||||
@@ -67,8 +69,7 @@ func (h *QuestHandler) HandleExtraQuestRestart(user *store.UserState, questId in
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
scene, ok := h.SceneById[questSceneId]
|
if _, ok := h.SceneById[questSceneId]; !ok {
|
||||||
if !ok {
|
|
||||||
log.Printf("[HandleExtraQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
log.Printf("[HandleExtraQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,8 +80,4 @@ func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, ques
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
h.applySceneGrants(user, questSceneId, nowMillis)
|
||||||
|
|
||||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
|
||||||
h.clearQuestMissions(user, scene.QuestId, nowMillis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,23 @@ type FinishOutcome struct {
|
|||||||
|
|
||||||
type QuestHandler struct {
|
type QuestHandler struct {
|
||||||
*masterdata.QuestCatalog
|
*masterdata.QuestCatalog
|
||||||
Config *masterdata.GameConfig
|
Config *masterdata.GameConfig
|
||||||
Granter *store.PossessionGranter
|
Granter *store.PossessionGranter
|
||||||
|
SideStoryChapterByEventQuestId map[int32]int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *QuestHandler {
|
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler {
|
||||||
granter := BuildGranter(catalog)
|
granter := BuildGranter(catalog)
|
||||||
return &QuestHandler{QuestCatalog: catalog, Config: config, Granter: granter}
|
var sideStoryChapters map[int32]int32
|
||||||
|
if sideStory != nil {
|
||||||
|
sideStoryChapters = sideStory.ChapterByEventQuestId
|
||||||
|
}
|
||||||
|
return &QuestHandler{
|
||||||
|
QuestCatalog: catalog,
|
||||||
|
Config: config,
|
||||||
|
Granter: granter,
|
||||||
|
SideStoryChapterByEventQuestId: sideStoryChapters,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ func (h *QuestHandler) initQuestState(user *store.UserState, questId int32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isMainQuestPlayable(quest masterdata.EntityMQuest) bool {
|
func isMainQuestPlayable(quest masterdata.EntityMQuest) bool {
|
||||||
return !quest.IsRunInTheBackground && quest.IsCountedAsQuest
|
if quest.IsRunInTheBackground {
|
||||||
|
// A background quest is still actively played — and must NOT be
|
||||||
|
// auto-cleared on start — when it carries battle content (a non-zero
|
||||||
|
// recommended deck power, e.g. quests 500/515/30515). Pure cutscene
|
||||||
|
// background quests have RecommendedDeckPower == 0.
|
||||||
|
return quest.RecommendedDeckPower > 0
|
||||||
|
}
|
||||||
|
return quest.IsCountedAsQuest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) clearQuestMissions(user *store.UserState, questId int32, nowMillis int64) {
|
func (h *QuestHandler) clearQuestMissions(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
@@ -38,49 +45,63 @@ func (h *QuestHandler) clearQuestMissions(user *store.UserState, questId int32,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleQuestStart(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
func (h *QuestHandler) HandleQuestStart(user *store.UserState, questId int32, isBattleOnly, isMainFlow bool, userDeckNumber int32, nowMillis int64) {
|
||||||
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, false, nowMillis)
|
h.handleQuestStartInternal(user, questId, isBattleOnly, isMainFlow, userDeckNumber, false, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleQuestStartReplay(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
func (h *QuestHandler) HandleQuestStartReplay(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
||||||
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, true, nowMillis)
|
h.handleQuestStartInternal(user, questId, isBattleOnly, false, userDeckNumber, true, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, isReplayFlow bool, nowMillis int64) {
|
func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId int32, isBattleOnly, isMainFlow bool, userDeckNumber int32, isReplayFlow bool, nowMillis int64) {
|
||||||
quest, ok := h.QuestById[questId]
|
quest, ok := h.QuestById[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestStart", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleQuestStart", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
h.initQuestState(user, questId)
|
h.initQuestState(user, questId)
|
||||||
|
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
store.ConsumeStamina(user, quest.Stamina, h.MaxStaminaByLevel[user.Status.Level]*1000, nowMillis)
|
||||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
if questState.QuestStateType == model.UserQuestStateTypeCleared {
|
questState.IsBattleOnly = isBattleOnly
|
||||||
if isReplayFlow {
|
questState.UserDeckNumber = userDeckNumber
|
||||||
user.MainQuest.SavedCurrentQuestSceneId = user.MainQuest.CurrentQuestSceneId
|
|
||||||
user.MainQuest.SavedHeadQuestSceneId = user.MainQuest.HeadQuestSceneId
|
isCleared := questState.QuestStateType == model.UserQuestStateTypeCleared
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeReplayFlow)
|
isMenuPick := !isReplayFlow && !isMainFlow
|
||||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
|
||||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
switch {
|
||||||
user.MainQuest.LatestVersion = nowMillis
|
case isMenuPick:
|
||||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
snapshotMainQuestIfNeeded(user)
|
||||||
|
sceneId := h.menuPickSceneId(questId, isBattleOnly)
|
||||||
|
user.MainQuest.ProgressQuestSceneId = sceneId
|
||||||
|
user.MainQuest.ProgressHeadQuestSceneId = sceneId
|
||||||
|
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
|
user.PortalCageStatus.IsCurrentProgress = false
|
||||||
|
user.PortalCageStatus.LatestVersion = nowMillis
|
||||||
|
user.SideStoryActiveProgress = store.SideStoryActiveProgress{LatestVersion: nowMillis}
|
||||||
|
user.MainQuest.LatestVersion = nowMillis
|
||||||
|
log.Printf("[HandleQuestStart] QuestMenuPick quest=%d isBattleOnly=%v scene=%d cleared=%v",
|
||||||
|
questId, isBattleOnly, sceneId, isCleared)
|
||||||
|
if isCleared {
|
||||||
questState.LatestStartDatetime = nowMillis
|
questState.LatestStartDatetime = nowMillis
|
||||||
questState.IsBattleOnly = isBattleOnly
|
|
||||||
questState.UserDeckNumber = userDeckNumber
|
|
||||||
user.Quests[questId] = questState
|
user.Quests[questId] = questState
|
||||||
log.Printf("[HandleQuestStart] replay flow started for quest %d, saved scene=%d head=%d",
|
return
|
||||||
questId, user.MainQuest.SavedCurrentQuestSceneId, user.MainQuest.SavedHeadQuestSceneId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case isReplayFlow:
|
||||||
|
h.applyReplayStart(user, quest, questId, isBattleOnly, nowMillis)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCleared {
|
||||||
|
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||||
|
questState.LatestStartDatetime = nowMillis
|
||||||
|
user.Quests[questId] = questState
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
questState.IsBattleOnly = isBattleOnly
|
|
||||||
questState.UserDeckNumber = userDeckNumber
|
|
||||||
if isMainQuestPlayable(quest) {
|
if isMainQuestPlayable(quest) {
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||||
@@ -90,7 +111,6 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
|
|||||||
questState.ClearCount = 1
|
questState.ClearCount = 1
|
||||||
questState.DailyClearCount = 1
|
questState.DailyClearCount = 1
|
||||||
questState.LastClearDatetime = nowMillis
|
questState.LastClearDatetime = nowMillis
|
||||||
|
|
||||||
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
||||||
firstSceneId := sceneIds[0]
|
firstSceneId := sceneIds[0]
|
||||||
prevSceneId := user.MainQuest.CurrentQuestSceneId
|
prevSceneId := user.MainQuest.CurrentQuestSceneId
|
||||||
@@ -102,14 +122,85 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
|
|||||||
user.Quests[questId] = questState
|
user.Quests[questId] = questState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, outcome *FinishOutcome, nowMillis int64) {
|
func snapshotMainQuestIfNeeded(user *store.UserState) {
|
||||||
|
if user.MainQuest.SavedContext.Active {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.MainQuest.SavedContext = store.SavedQuestContext{
|
||||||
|
Active: true,
|
||||||
|
CurrentQuestSceneId: user.MainQuest.CurrentQuestSceneId,
|
||||||
|
HeadQuestSceneId: user.MainQuest.HeadQuestSceneId,
|
||||||
|
CurrentMainQuestRouteId: user.MainQuest.CurrentMainQuestRouteId,
|
||||||
|
MainQuestSeasonId: user.MainQuest.MainQuestSeasonId,
|
||||||
|
IsReachedLastQuestScene: user.MainQuest.IsReachedLastQuestScene,
|
||||||
|
PortalCageInProgress: user.PortalCageStatus.IsCurrentProgress,
|
||||||
|
CurrentQuestFlowType: user.MainQuest.CurrentQuestFlowType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) applyReplayStart(user *store.UserState, quest masterdata.EntityMQuest, questId int32, isBattleOnly bool, nowMillis int64) {
|
||||||
|
flowType := h.replayFlowTypeFromQuestId(user, questId)
|
||||||
|
if model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType) {
|
||||||
|
flowType = model.QuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||||
|
}
|
||||||
|
user.MainQuest.CurrentQuestFlowType = int32(flowType)
|
||||||
|
user.MainQuest.LatestVersion = nowMillis
|
||||||
|
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
|
questState.LatestStartDatetime = nowMillis
|
||||||
|
|
||||||
|
if isMainQuestPlayable(quest) {
|
||||||
|
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||||
|
user.Quests[questId] = questState
|
||||||
|
} else {
|
||||||
|
if questState.QuestStateType != model.UserQuestStateTypeCleared {
|
||||||
|
questState.QuestStateType = model.UserQuestStateTypeCleared
|
||||||
|
questState.ClearCount++
|
||||||
|
questState.DailyClearCount++
|
||||||
|
questState.LastClearDatetime = nowMillis
|
||||||
|
}
|
||||||
|
user.Quests[questId] = questState
|
||||||
|
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
||||||
|
h.advanceReplayFlowScene(user, sceneIds[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[HandleQuestStart] replay quest=%d flowType=%s isBattleOnly=%v playable=%v current=%d head=%d",
|
||||||
|
questId, flowType, isBattleOnly, isMainQuestPlayable(quest),
|
||||||
|
user.MainQuest.ReplayFlowCurrentQuestSceneId,
|
||||||
|
user.MainQuest.ReplayFlowHeadQuestSceneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) menuPickSceneId(questId int32, isBattleOnly bool) int32 {
|
||||||
|
if isBattleOnly {
|
||||||
|
if v, ok := h.BattleOnlyTargetSceneIdFor(questId); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scenes := h.SceneIdsByQuestId[questId]; len(scenes) > 0 {
|
||||||
|
return scenes[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, outcome *FinishOutcome, nowMillis int64, wasReplay bool) {
|
||||||
|
questState := user.Quests[questId]
|
||||||
|
h.applyExpAndGoldRewards(user, questId, nowMillis)
|
||||||
if !questState.IsRewardGranted {
|
if !questState.IsRewardGranted {
|
||||||
h.applyQuestRewards(user, questId, nowMillis)
|
if !wasReplay {
|
||||||
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
h.applyFirstClearItemRewards(user, questId, nowMillis)
|
||||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeHalfResult, nowMillis)...)
|
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
||||||
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeHalfResult, nowMillis)...)
|
||||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeFullResult, nowMillis)...)
|
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
||||||
|
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeFullResult, nowMillis)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range outcome.MissionClearRewards {
|
||||||
|
h.applyRewardPossession(user, r.PossessionType, r.PossessionId, r.Count, nowMillis)
|
||||||
|
}
|
||||||
|
for _, r := range outcome.MissionClearCompleteRewards {
|
||||||
|
h.applyRewardPossession(user, r.PossessionType, r.PossessionId, r.Count, nowMillis)
|
||||||
|
}
|
||||||
questState.IsRewardGranted = true
|
questState.IsRewardGranted = true
|
||||||
}
|
}
|
||||||
for _, drop := range outcome.DropRewards {
|
for _, drop := range outcome.DropRewards {
|
||||||
@@ -122,22 +213,65 @@ func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, o
|
|||||||
questState.ClearCount++
|
questState.ClearCount++
|
||||||
questState.DailyClearCount++
|
questState.DailyClearCount++
|
||||||
questState.LastClearDatetime = nowMillis
|
questState.LastClearDatetime = nowMillis
|
||||||
|
questState.IsBattleOnly = false
|
||||||
user.Quests[questId] = questState
|
user.Quests[questId] = questState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) finalizeChainPreviousQuest(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
|
if _, ok := h.QuestById[questId]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.initQuestState(user, questId)
|
||||||
|
questState := user.Quests[questId]
|
||||||
|
if questState.QuestStateType == model.UserQuestStateTypeCleared {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !questState.IsRewardGranted {
|
||||||
|
h.applyQuestRewards(user, questId, nowMillis)
|
||||||
|
questState.IsRewardGranted = true
|
||||||
|
}
|
||||||
|
questState.QuestStateType = model.UserQuestStateTypeCleared
|
||||||
|
questState.ClearCount++
|
||||||
|
questState.DailyClearCount++
|
||||||
|
questState.LastClearDatetime = nowMillis
|
||||||
|
questState.IsBattleOnly = false
|
||||||
|
user.Quests[questId] = questState
|
||||||
|
h.clearQuestMissions(user, questId, nowMillis)
|
||||||
|
log.Printf("[HandleMainQuestSceneProgress] finalized chain-previous quest %d (cleared)", questId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreClearedAfterRetire(user *store.UserState, questId int32, isRetired bool) {
|
||||||
|
if !isRetired {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qs := user.Quests[questId]
|
||||||
|
if qs.ClearCount > 0 && qs.QuestStateType == model.UserQuestStateTypeActive {
|
||||||
|
qs.QuestStateType = model.UserQuestStateTypeCleared
|
||||||
|
user.Quests[questId] = qs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
||||||
quest, ok := h.QuestById[questId]
|
quest, ok := h.QuestById[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.initQuestState(user, questId)
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
wasReplay := user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow)
|
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||||
|
wasMenuReplay := user.MainQuest.SavedContext.Active
|
||||||
|
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, wasReplay)
|
||||||
|
|
||||||
if isMainQuestPlayable(quest) && !wasReplay {
|
// A replay-flow finish must NOT move the MainFlow scene pointer: the
|
||||||
|
// finished quest is a replay-variant (30000+) with no chapter, so a
|
||||||
|
// replay scene left in CurrentQuestSceneId makes the client world map's
|
||||||
|
// CalculatorWorldMap.GetCurrentSeasonId resolve chapter 0 and NRE. The
|
||||||
|
// replay's own position is tracked in ReplayFlowCurrentQuestSceneId.
|
||||||
|
if isMainQuestPlayable(quest) && !wasMenuReplay && !wasReplay {
|
||||||
lastSceneId := h.getLastMainFlowSceneId(questId)
|
lastSceneId := h.getLastMainFlowSceneId(questId)
|
||||||
h.advanceMainFlowScene(user, questId, lastSceneId)
|
h.advanceMainFlowScene(user, questId, lastSceneId)
|
||||||
}
|
}
|
||||||
@@ -149,24 +283,32 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreClearedAfterRetire(user, questId, isRetired)
|
||||||
|
|
||||||
user.MainQuest.ProgressQuestSceneId = 0
|
user.MainQuest.ProgressQuestSceneId = 0
|
||||||
user.MainQuest.ProgressHeadQuestSceneId = 0
|
user.MainQuest.ProgressHeadQuestSceneId = 0
|
||||||
user.MainQuest.ProgressQuestFlowType = 0
|
if !wasReplay {
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeUnknown)
|
// Keep replay flow types on replay finish so the client's
|
||||||
|
// Story.ApplyNewestPlayingScene keeps _isReplayed=true (popup result UI).
|
||||||
|
user.MainQuest.ProgressQuestFlowType = 0
|
||||||
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeUnknown)
|
||||||
|
}
|
||||||
|
|
||||||
if wasReplay {
|
if wasMenuReplay {
|
||||||
if user.MainQuest.SavedCurrentQuestSceneId > 0 {
|
ctx := user.MainQuest.SavedContext
|
||||||
user.MainQuest.CurrentQuestSceneId = user.MainQuest.SavedCurrentQuestSceneId
|
user.MainQuest.CurrentQuestSceneId = ctx.CurrentQuestSceneId
|
||||||
}
|
user.MainQuest.HeadQuestSceneId = ctx.HeadQuestSceneId
|
||||||
if user.MainQuest.SavedHeadQuestSceneId > 0 {
|
user.MainQuest.CurrentMainQuestRouteId = ctx.CurrentMainQuestRouteId
|
||||||
user.MainQuest.HeadQuestSceneId = user.MainQuest.SavedHeadQuestSceneId
|
user.MainQuest.MainQuestSeasonId = ctx.MainQuestSeasonId
|
||||||
}
|
user.MainQuest.IsReachedLastQuestScene = ctx.IsReachedLastQuestScene
|
||||||
user.MainQuest.SavedCurrentQuestSceneId = 0
|
user.MainQuest.CurrentQuestFlowType = ctx.CurrentQuestFlowType
|
||||||
user.MainQuest.SavedHeadQuestSceneId = 0
|
user.PortalCageStatus.IsCurrentProgress = ctx.PortalCageInProgress
|
||||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
user.PortalCageStatus.LatestVersion = nowMillis
|
||||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
user.MainQuest.SavedContext = store.SavedQuestContext{}
|
||||||
log.Printf("[HandleQuestFinish] replay flow ended for quest %d, restored scene=%d head=%d",
|
user.MainQuest.LatestVersion = nowMillis
|
||||||
questId, user.MainQuest.CurrentQuestSceneId, user.MainQuest.HeadQuestSceneId)
|
log.Printf("[HandleQuestFinish] restored snapshot for quest %d (route=%d season=%d scene=%d head=%d cage=%v flow=%d)",
|
||||||
|
questId, ctx.CurrentMainQuestRouteId, ctx.MainQuestSeasonId,
|
||||||
|
ctx.CurrentQuestSceneId, ctx.HeadQuestSceneId, ctx.PortalCageInProgress, ctx.CurrentQuestFlowType)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.clearQuestMissions(user, questId, nowMillis)
|
h.clearQuestMissions(user, questId, nowMillis)
|
||||||
@@ -215,14 +357,16 @@ func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount
|
|||||||
|
|
||||||
func (h *QuestHandler) HandleQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
questDef, ok := h.QuestById[questId]
|
questDef, ok := h.QuestById[questId]
|
||||||
if ok && isMainQuestPlayable(questDef) {
|
// Only seed CurrentQuestFlowType when it's not already set (initial
|
||||||
|
// natural progression). Don't clobber an in-flight ReplayFlow (Map Play
|
||||||
|
// resume).
|
||||||
|
if ok && isMainQuestPlayable(questDef) && user.MainQuest.CurrentQuestFlowType == 0 {
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
quest := user.Quests[questId]
|
quest := user.Quests[questId]
|
||||||
quest.QuestId = questId
|
quest.QuestId = questId
|
||||||
quest.QuestStateType = model.UserQuestStateTypeActive
|
quest.QuestStateType = model.UserQuestStateTypeActive
|
||||||
quest.IsBattleOnly = false
|
|
||||||
quest.LatestStartDatetime = nowMillis
|
quest.LatestStartDatetime = nowMillis
|
||||||
user.Quests[questId] = quest
|
user.Quests[questId] = quest
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func (h *QuestHandler) isQuestCleared(user *store.UserState, questId int32) bool {
|
func (h *QuestHandler) isQuestCleared(user *store.UserState, questId int32) bool {
|
||||||
quest, ok := user.Quests[questId]
|
quest, ok := user.Quests[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("unknown questId=%d for isQuestCleared", questId))
|
return false
|
||||||
}
|
}
|
||||||
return quest.QuestStateType == model.UserQuestStateTypeCleared
|
return quest.QuestStateType == model.UserQuestStateTypeCleared
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,9 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
|
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !questState.IsRewardGranted {
|
isReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||||
|
|
||||||
|
if !questState.IsRewardGranted && !isReplay {
|
||||||
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||||
outcome.FirstClearRewards = append(outcome.FirstClearRewards, RewardGrant{
|
outcome.FirstClearRewards = append(outcome.FirstClearRewards, RewardGrant{
|
||||||
@@ -62,7 +64,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow) && questDef.QuestReplayFlowRewardGroupId > 0 {
|
if isReplay && questDef.QuestReplayFlowRewardGroupId > 0 {
|
||||||
for _, reward := range h.ReplayFlowRewardsByGroupId[questDef.QuestReplayFlowRewardGroupId] {
|
for _, reward := range h.ReplayFlowRewardsByGroupId[questDef.QuestReplayFlowRewardGroupId] {
|
||||||
outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{
|
outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{
|
||||||
PossessionType: model.PossessionType(reward.PossessionType),
|
PossessionType: model.PossessionType(reward.PossessionType),
|
||||||
@@ -72,48 +74,53 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingClearCount := 0
|
// Mission rewards / BigWin are first-clear concepts. Reference
|
||||||
regularMissionCount := 0
|
// IUserQuestMissionTable has no rows for replay-variant ids (30000+):
|
||||||
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
// the popup is empty on replay in the original game.
|
||||||
missionDef, ok := h.MissionById[questMissionId]
|
if !isReplay {
|
||||||
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) == model.QuestMissionConditionTypeComplete {
|
pendingClearCount := 0
|
||||||
continue
|
regularMissionCount := 0
|
||||||
}
|
|
||||||
regularMissionCount++
|
|
||||||
|
|
||||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
|
||||||
mission := user.QuestMissions[key]
|
|
||||||
|
|
||||||
if !mission.IsClear {
|
|
||||||
pendingClearCount++
|
|
||||||
outcome.MissionClearRewards = appendMissionRewards(
|
|
||||||
outcome.MissionClearRewards,
|
|
||||||
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
priorClearCount := regularMissionCount - pendingClearCount
|
|
||||||
// On our server every mission auto-clears, so priorClearCount + pendingClearCount
|
|
||||||
// always equals regularMissionCount. The two-variable form is kept to mirror the
|
|
||||||
// original game's intent where individual missions could fail their conditions.
|
|
||||||
allRegularWillClear := regularMissionCount > 0 && (priorClearCount+pendingClearCount) == regularMissionCount
|
|
||||||
if allRegularWillClear {
|
|
||||||
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
||||||
missionDef, ok := h.MissionById[questMissionId]
|
missionDef, ok := h.MissionById[questMissionId]
|
||||||
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) != model.QuestMissionConditionTypeComplete {
|
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) == model.QuestMissionConditionTypeComplete {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
regularMissionCount++
|
||||||
|
|
||||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
||||||
if !user.QuestMissions[key].IsClear {
|
mission := user.QuestMissions[key]
|
||||||
outcome.MissionClearCompleteRewards = appendMissionRewards(
|
|
||||||
outcome.MissionClearCompleteRewards,
|
if !mission.IsClear {
|
||||||
|
pendingClearCount++
|
||||||
|
outcome.MissionClearRewards = appendMissionRewards(
|
||||||
|
outcome.MissionClearRewards,
|
||||||
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
||||||
)
|
)
|
||||||
outcome.BigWinClearedQuestMissionIds = append(outcome.BigWinClearedQuestMissionIds, questMissionId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outcome.IsBigWin = len(outcome.BigWinClearedQuestMissionIds) > 0
|
|
||||||
|
priorClearCount := regularMissionCount - pendingClearCount
|
||||||
|
// On our server every mission auto-clears, so priorClearCount + pendingClearCount
|
||||||
|
// always equals regularMissionCount. The two-variable form is kept to mirror the
|
||||||
|
// original game's intent where individual missions could fail their conditions.
|
||||||
|
allRegularWillClear := regularMissionCount > 0 && (priorClearCount+pendingClearCount) == regularMissionCount
|
||||||
|
if allRegularWillClear {
|
||||||
|
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
||||||
|
missionDef, ok := h.MissionById[questMissionId]
|
||||||
|
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) != model.QuestMissionConditionTypeComplete {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
||||||
|
if !user.QuestMissions[key].IsClear {
|
||||||
|
outcome.MissionClearCompleteRewards = appendMissionRewards(
|
||||||
|
outcome.MissionClearCompleteRewards,
|
||||||
|
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
||||||
|
)
|
||||||
|
outcome.BigWinClearedQuestMissionIds = append(outcome.BigWinClearedQuestMissionIds, questMissionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outcome.IsBigWin = len(outcome.BigWinClearedQuestMissionIds) > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome.DropRewards = h.computeDropRewards(questDef)
|
outcome.DropRewards = h.computeDropRewards(questDef)
|
||||||
@@ -159,6 +166,10 @@ func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, now
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if questDef.CharacterExp == 0 && questDef.CostumeExp == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
deckCostumeUuids, deckCharacterIds := h.resolveDeckUnits(user, questId)
|
deckCostumeUuids, deckCharacterIds := h.resolveDeckUnits(user, questId)
|
||||||
if deckCostumeUuids == nil {
|
if deckCostumeUuids == nil {
|
||||||
log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (deck not resolved)", questId)
|
log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (deck not resolved)", questId)
|
||||||
@@ -240,7 +251,7 @@ func (h *QuestHandler) resolveDeckUnits(user *store.UserState, questId int32) (c
|
|||||||
return costumeUuids, characterIds
|
return costumeUuids, characterIds
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, nowMillis int64) {
|
func (h *QuestHandler) applyExpAndGoldRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
questDef, ok := h.QuestById[questId]
|
questDef, ok := h.QuestById[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -252,13 +263,24 @@ func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, n
|
|||||||
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
|
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
|
||||||
log.Printf("[applyQuestRewards] questId=%d gold: +%d -> total=%d", questId, questDef.Gold, user.ConsumableItems[h.Config.ConsumableItemIdForGold])
|
log.Printf("[applyQuestRewards] questId=%d gold: +%d -> total=%d", questId, questDef.Gold, user.ConsumableItems[h.Config.ConsumableItemIdForGold])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) applyFirstClearItemRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
|
questDef, ok := h.QuestById[questId]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||||
h.applyRewardPossession(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
h.applyRewardPossession(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
|
h.applyExpAndGoldRewards(user, questId, nowMillis)
|
||||||
|
h.applyFirstClearItemRewards(user, questId, nowMillis)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) applyRewardPossession(user *store.UserState, possType model.PossessionType, possId, count int32, nowMillis int64) {
|
func (h *QuestHandler) applyRewardPossession(user *store.UserState, possType model.PossessionType, possId, count int32, nowMillis int64) {
|
||||||
h.Granter.GrantFull(user, possType, possId, count, nowMillis)
|
h.Granter.GrantFull(user, possType, possId, count, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package questflow
|
package questflow
|
||||||
|
|
||||||
|
// MainQuest scene-field families mirror three client entity tables:
|
||||||
|
//
|
||||||
|
// MainFlow* — EntityIUserMainQuestMainFlowStatus (#11443)
|
||||||
|
// Progress* — EntityIUserMainQuestProgressStatus (#11444)
|
||||||
|
// ReplayFlow* — EntityIUserMainQuestReplayFlowStatus (#11445)
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -27,10 +33,11 @@ func (h *QuestHandler) isSceneAhead(newSceneId, currentHeadId int32) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, sceneId int32) {
|
func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, sceneId int32) {
|
||||||
user.MainQuest.CurrentQuestSceneId = sceneId
|
if !h.isSceneAhead(sceneId, user.MainQuest.HeadQuestSceneId) {
|
||||||
if h.isSceneAhead(sceneId, user.MainQuest.HeadQuestSceneId) {
|
return
|
||||||
user.MainQuest.HeadQuestSceneId = sceneId
|
|
||||||
}
|
}
|
||||||
|
user.MainQuest.CurrentQuestSceneId = sceneId
|
||||||
|
user.MainQuest.HeadQuestSceneId = sceneId
|
||||||
|
|
||||||
lastSceneId := h.getChapterLastSceneId(questId)
|
lastSceneId := h.getChapterLastSceneId(questId)
|
||||||
user.MainQuest.IsReachedLastQuestScene = sceneId == lastSceneId
|
user.MainQuest.IsReachedLastQuestScene = sceneId == lastSceneId
|
||||||
@@ -43,6 +50,37 @@ func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, scen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) advanceReplayFlowScene(user *store.UserState, sceneId int32) {
|
||||||
|
if !h.isSceneAhead(sceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.MainQuest.ReplayFlowCurrentQuestSceneId = sceneId
|
||||||
|
user.MainQuest.ReplayFlowHeadQuestSceneId = sceneId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) SeasonRoutesFor(user *store.UserState) map[int32]int32 {
|
||||||
|
out := make(map[int32]int32)
|
||||||
|
for seasonId, routes := range h.RoutesBySeason {
|
||||||
|
if seasonId <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, routeId := range routes {
|
||||||
|
finalQuestId, ok := h.RouteCompletionQuestId[routeId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if q, ok := user.Quests[finalQuestId]; ok && q.ClearCount > 0 {
|
||||||
|
out[seasonId] = routeId
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cur := user.MainQuest.MainQuestSeasonId; cur >= 2 && user.MainQuest.CurrentMainQuestRouteId > 0 {
|
||||||
|
out[cur] = user.MainQuest.CurrentMainQuestRouteId
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
scene, ok := h.SceneById[questSceneId]
|
scene, ok := h.SceneById[questSceneId]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -109,11 +147,53 @@ func (h *QuestHandler) getChapterLastSceneId(questId int32) int32 {
|
|||||||
|
|
||||||
func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
||||||
if user.MainQuest.ReplayFlowHeadQuestSceneId == 0 || h.isSceneAhead(questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
|
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
|
||||||
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
|
|
||||||
|
user.PortalCageStatus.IsCurrentProgress = false
|
||||||
|
user.PortalCageStatus.LatestVersion = nowMillis
|
||||||
|
|
||||||
|
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
|
||||||
|
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flowType := h.replayFlowType(user, questSceneId)
|
||||||
|
user.MainQuest.CurrentQuestFlowType = int32(flowType)
|
||||||
user.MainQuest.LatestVersion = nowMillis
|
user.MainQuest.LatestVersion = nowMillis
|
||||||
log.Printf("[HandleReplayFlowSceneProgress] sceneId=%d replayHead=%d", questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId)
|
log.Printf("[HandleReplayFlowSceneProgress] sceneId=%d flowType=%s", questSceneId, flowType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) replayFlowType(user *store.UserState, questSceneId int32) model.QuestFlowType {
|
||||||
|
scene, ok := h.SceneById[questSceneId]
|
||||||
|
if !ok {
|
||||||
|
return model.QuestFlowTypeReplayFlow
|
||||||
|
}
|
||||||
|
routeId, ok := h.RouteIdByQuestId[scene.QuestId]
|
||||||
|
if !ok {
|
||||||
|
return model.QuestFlowTypeReplayFlow
|
||||||
|
}
|
||||||
|
return h.replayFlowTypeForRoute(user, routeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) replayFlowTypeForRoute(user *store.UserState, routeId int32) model.QuestFlowType {
|
||||||
|
seasonId, ok := h.SeasonIdByRouteId[routeId]
|
||||||
|
if !ok {
|
||||||
|
return model.QuestFlowTypeReplayFlow
|
||||||
|
}
|
||||||
|
pairs := h.SeasonRoutesFor(user)
|
||||||
|
if recorded, ok := pairs[seasonId]; ok && recorded != routeId {
|
||||||
|
return model.QuestFlowTypeAnotherRouteReplayFlow
|
||||||
|
}
|
||||||
|
return model.QuestFlowTypeReplayFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) replayFlowTypeFromQuestId(user *store.UserState, questId int32) model.QuestFlowType {
|
||||||
|
routeId, ok := h.RouteIdByQuestId[questId]
|
||||||
|
if !ok {
|
||||||
|
return model.QuestFlowTypeReplayFlow
|
||||||
|
}
|
||||||
|
return h.replayFlowTypeForRoute(user, routeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, questSceneId int32) {
|
func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, questSceneId int32) {
|
||||||
@@ -127,19 +207,33 @@ func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, quest
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleMainQuestSceneProgress", questSceneId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleMainQuestSceneProgress", questSceneId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if isMainQuestPlayable(quest) {
|
if prevSceneId := user.MainQuest.ProgressQuestSceneId; prevSceneId != 0 {
|
||||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
if prevScene, ok := h.SceneById[prevSceneId]; ok && prevScene.QuestId != quest.QuestId {
|
||||||
nowMillis := gametime.NowMillis()
|
// Skip if the previous quest is playable — it has its own FinishMainQuest;
|
||||||
h.clearQuestMissions(user, quest.QuestId, nowMillis)
|
// chain-finalizing here would double-increment ClearCount.
|
||||||
|
if prevQuest, ok := h.QuestById[prevScene.QuestId]; ok && !isMainQuestPlayable(prevQuest) {
|
||||||
|
h.finalizeChainPreviousQuest(user, prevScene.QuestId, gametime.NowMillis())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||||
|
|
||||||
|
if isMainQuestPlayable(quest) {
|
||||||
user.MainQuest.ProgressQuestSceneId = questSceneId
|
user.MainQuest.ProgressQuestSceneId = questSceneId
|
||||||
if h.isSceneAhead(questSceneId, user.MainQuest.ProgressHeadQuestSceneId) {
|
if h.isSceneAhead(questSceneId, user.MainQuest.ProgressHeadQuestSceneId) {
|
||||||
user.MainQuest.ProgressHeadQuestSceneId = questSceneId
|
user.MainQuest.ProgressHeadQuestSceneId = questSceneId
|
||||||
}
|
}
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
if isReplay {
|
||||||
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
user.MainQuest.ProgressQuestFlowType = user.MainQuest.CurrentQuestFlowType
|
||||||
} else {
|
} else {
|
||||||
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||||
|
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||||
|
}
|
||||||
|
} else if !isReplay {
|
||||||
|
// Background/non-playable quest: advance the MainFlow pointer — but not
|
||||||
|
// during a replay, where the isReplay block below tracks the ReplayFlow
|
||||||
|
// scene and the MainFlow pointer must stay on real main-story progress.
|
||||||
user.MainQuest.CurrentQuestSceneId = questSceneId
|
user.MainQuest.CurrentQuestSceneId = questSceneId
|
||||||
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
|
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
|
||||||
user.MainQuest.HeadQuestSceneId = questSceneId
|
user.MainQuest.HeadQuestSceneId = questSceneId
|
||||||
@@ -147,4 +241,12 @@ func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, quest
|
|||||||
lastSceneId := h.getChapterLastSceneId(quest.QuestId)
|
lastSceneId := h.getChapterLastSceneId(quest.QuestId)
|
||||||
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
|
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isReplay {
|
||||||
|
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
||||||
|
if h.isSceneAhead(questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
|
||||||
|
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
|
||||||
|
}
|
||||||
|
user.MainQuest.LatestVersion = gametime.NowMillis()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/gacha"
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/masterdata/memorydb"
|
||||||
|
"lunar-tear/server/internal/questflow"
|
||||||
|
"lunar-tear/server/internal/userdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever
|
||||||
|
// memorydb currently holds and returns a fully populated *Catalogs. Called
|
||||||
|
// once at startup and again on every reload.
|
||||||
|
func buildCatalogs() (*Catalogs, error) {
|
||||||
|
log.Printf("master data loaded (%d tables)", memorydb.TableCount())
|
||||||
|
|
||||||
|
gameConfig, err := masterdata.LoadGameConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load game config: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)",
|
||||||
|
gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold)
|
||||||
|
|
||||||
|
partsCatalog, err := masterdata.LoadPartsCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load parts catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType))
|
||||||
|
|
||||||
|
questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load quest catalog: %w", err)
|
||||||
|
}
|
||||||
|
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
||||||
|
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog)
|
||||||
|
userdata.SetQuestHandler(questHandler)
|
||||||
|
|
||||||
|
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load gacha catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("gacha catalog loaded: %d entries", len(gachaEntries))
|
||||||
|
|
||||||
|
gachaPool, err := masterdata.LoadGachaPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load gacha pool: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d",
|
||||||
|
len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials))
|
||||||
|
|
||||||
|
shopCatalog, err := masterdata.LoadShopCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load shop catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops",
|
||||||
|
len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells))
|
||||||
|
|
||||||
|
gachaPool.BuildShopFeatured(shopCatalog)
|
||||||
|
gachaPool.PruneUnpairedCostumes()
|
||||||
|
gachaPool.BuildFeaturedFromTerms(gachaEntries)
|
||||||
|
gachaPool.BuildBannerPools(gachaEntries)
|
||||||
|
masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool)
|
||||||
|
|
||||||
|
dupExchange, err := masterdata.LoadDupExchange()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load dup exchange: %w", err)
|
||||||
|
}
|
||||||
|
dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("enrich dup exchange: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded)
|
||||||
|
|
||||||
|
gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange)
|
||||||
|
|
||||||
|
conditionResolver, err := masterdata.LoadConditionResolver()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load condition resolver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog()
|
||||||
|
loginBonusCatalog := masterdata.LoadLoginBonusCatalog()
|
||||||
|
characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver)
|
||||||
|
omikujiCatalog := masterdata.LoadOmikujiCatalog()
|
||||||
|
|
||||||
|
materialCatalog, err := masterdata.LoadMaterialCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load material catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("material catalog loaded: %d materials", len(materialCatalog.All))
|
||||||
|
|
||||||
|
consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load consumable item catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All))
|
||||||
|
|
||||||
|
costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load costume catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity))
|
||||||
|
|
||||||
|
weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load weapon catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId))
|
||||||
|
|
||||||
|
exploreCatalog, err := masterdata.LoadExploreCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load explore catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
|
||||||
|
|
||||||
|
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver, cageOrnamentCatalog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load gimmick catalog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load character board catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
|
||||||
|
|
||||||
|
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load character rebirth catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
|
||||||
|
|
||||||
|
companionCatalog, err := masterdata.LoadCompanionCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load companion catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
|
||||||
|
|
||||||
|
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
|
||||||
|
|
||||||
|
towerCatalog := masterdata.LoadTowerCatalog()
|
||||||
|
|
||||||
|
labyrinthCatalog := masterdata.LoadLabyrinthCatalog()
|
||||||
|
|
||||||
|
return &Catalogs{
|
||||||
|
GameConfig: gameConfig,
|
||||||
|
Parts: partsCatalog,
|
||||||
|
Quest: questCatalog,
|
||||||
|
GachaEntries: gachaEntries,
|
||||||
|
GachaMedals: medalInfo,
|
||||||
|
GachaPool: gachaPool,
|
||||||
|
Shop: shopCatalog,
|
||||||
|
DupExchange: dupExchange,
|
||||||
|
ConditionResolver: conditionResolver,
|
||||||
|
CageOrnament: cageOrnamentCatalog,
|
||||||
|
LoginBonus: loginBonusCatalog,
|
||||||
|
CharacterViewer: characterViewerCatalog,
|
||||||
|
Omikuji: omikujiCatalog,
|
||||||
|
Material: materialCatalog,
|
||||||
|
ConsumableItem: consumableItemCatalog,
|
||||||
|
Costume: costumeCatalog,
|
||||||
|
Weapon: weaponCatalog,
|
||||||
|
Explore: exploreCatalog,
|
||||||
|
Gimmick: gimmickCatalog,
|
||||||
|
CharacterBoard: characterBoardCatalog,
|
||||||
|
CharacterRebirth: characterRebirthCatalog,
|
||||||
|
Companion: companionCatalog,
|
||||||
|
SideStory: sideStoryCatalog,
|
||||||
|
BigHunt: bigHuntCatalog,
|
||||||
|
Tower: towerCatalog,
|
||||||
|
Labyrinth: labyrinthCatalog,
|
||||||
|
QuestHandler: questHandler,
|
||||||
|
GachaHandler: gachaHandler,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Package runtime owns the live, hot-swappable view of master data.
|
||||||
|
//
|
||||||
|
// The Holder atomically swaps a *Catalogs aggregate every time the operator
|
||||||
|
// asks the server to re-read assets/release/20240404193219.bin.e (typically via
|
||||||
|
// the admin webhook in cmd/lunar-tear/admin.go). gRPC services hold a *Holder
|
||||||
|
// and call Get() at the start of each RPC, so they always see a consistent
|
||||||
|
// snapshot.
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/gacha"
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/masterdata/memorydb"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/questflow"
|
||||||
|
"lunar-tear/server/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Catalogs is an immutable snapshot of every catalog and catalog-derived
|
||||||
|
// handler the server needs at runtime. A new *Catalogs is built from scratch
|
||||||
|
// on every reload and atomically published via Holder.
|
||||||
|
type Catalogs struct {
|
||||||
|
GameConfig *masterdata.GameConfig
|
||||||
|
Parts *masterdata.PartsCatalog
|
||||||
|
Quest *masterdata.QuestCatalog
|
||||||
|
GachaEntries []store.GachaCatalogEntry
|
||||||
|
GachaMedals map[int32]masterdata.GachaMedalInfo
|
||||||
|
GachaPool *masterdata.GachaCatalog
|
||||||
|
Shop *masterdata.ShopCatalog
|
||||||
|
DupExchange map[int32][]model.DupExchangeEntry
|
||||||
|
ConditionResolver *masterdata.ConditionResolver
|
||||||
|
CageOrnament *masterdata.CageOrnamentCatalog
|
||||||
|
LoginBonus *masterdata.LoginBonusCatalog
|
||||||
|
CharacterViewer *masterdata.CharacterViewerCatalog
|
||||||
|
Omikuji *masterdata.OmikujiCatalog
|
||||||
|
Material *masterdata.MaterialCatalog
|
||||||
|
ConsumableItem *masterdata.ConsumableItemCatalog
|
||||||
|
Costume *masterdata.CostumeCatalog
|
||||||
|
Weapon *masterdata.WeaponCatalog
|
||||||
|
Explore *masterdata.ExploreCatalog
|
||||||
|
Gimmick *masterdata.GimmickCatalog
|
||||||
|
CharacterBoard *masterdata.CharacterBoardCatalog
|
||||||
|
CharacterRebirth *masterdata.CharacterRebirthCatalog
|
||||||
|
Companion *masterdata.CompanionCatalog
|
||||||
|
SideStory *masterdata.SideStoryCatalog
|
||||||
|
BigHunt *masterdata.BigHuntCatalog
|
||||||
|
Tower *masterdata.TowerCatalog
|
||||||
|
Labyrinth *masterdata.LabyrinthCatalog
|
||||||
|
|
||||||
|
// Catalog-derived handlers must rebuild on every reload because they
|
||||||
|
// embed/cache pointers to specific catalog instances.
|
||||||
|
QuestHandler *questflow.QuestHandler
|
||||||
|
GachaHandler *gacha.GachaHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holder owns the current *Catalogs and the bin.e path. Concurrent readers
|
||||||
|
// call Get(); the single-writer Reload() rebuilds and atomically publishes.
|
||||||
|
type Holder struct {
|
||||||
|
binPath string
|
||||||
|
cur atomic.Pointer[Catalogs]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHolder reads the binary at binPath, builds the initial catalogs, and
|
||||||
|
// returns a ready-to-use Holder. Subsequent calls to Reload() re-read the
|
||||||
|
// same path.
|
||||||
|
func NewHolder(binPath string) (*Holder, error) {
|
||||||
|
h := &Holder{binPath: binPath}
|
||||||
|
if err := h.Reload(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload re-reads the bin.e from disk, rebuilds every catalog and handler,
|
||||||
|
// atomically publishes the new snapshot, and bumps the bin.e mtime so client
|
||||||
|
// caches invalidate (see service/data.go GetLatestMasterDataVersion).
|
||||||
|
func (h *Holder) Reload() error {
|
||||||
|
if err := memorydb.Init(h.binPath); err != nil {
|
||||||
|
return fmt.Errorf("memorydb.Init: %w", err)
|
||||||
|
}
|
||||||
|
c, err := buildCatalogs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("buildCatalogs: %w", err)
|
||||||
|
}
|
||||||
|
h.cur.Store(c)
|
||||||
|
now := time.Now()
|
||||||
|
if err := os.Chtimes(h.binPath, now, now); err != nil {
|
||||||
|
// Non-fatal: the catalogs swapped fine in-memory; clients may take
|
||||||
|
// longer to invalidate their cached download but server-side state is
|
||||||
|
// already coherent.
|
||||||
|
log.Printf("[runtime] os.Chtimes(%s) failed (clients may not invalidate cache): %v", h.binPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the current snapshot. Safe for concurrent callers; the returned
|
||||||
|
// pointer is stable for the duration of the caller's use.
|
||||||
|
func (h *Holder) Get() *Catalogs {
|
||||||
|
return h.cur.Load()
|
||||||
|
}
|
||||||
@@ -70,32 +70,32 @@ func (t *revisionTracker) Active(clientAddr string) string {
|
|||||||
return revision
|
return revision
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *assetResolver) Resolve(objectId, assetType, activeRevision string) (assetResolution, bool) {
|
func (r *assetResolver) Resolve(objectId, assetType, activeRevision, platform string) (assetResolution, bool) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resolution := assetResolution{ActiveRevision: activeRevision}
|
resolution := assetResolution{ActiveRevision: activeRevision}
|
||||||
revision := activeRevision
|
revision := activeRevision
|
||||||
|
|
||||||
candidates, listSize, ok := objectIdToFilePathCandidates(r.baseDir, revision, assetType, objectId)
|
candidates, listSize, ok := objectIdToFilePathCandidates(r.baseDir, revision, platform, assetType, objectId)
|
||||||
if ok && len(candidates) > 0 {
|
if ok && len(candidates) > 0 {
|
||||||
resolution.ListRevision = revision
|
resolution.ListRevision = revision
|
||||||
resolution.ListSize = listSize
|
resolution.ListSize = listSize
|
||||||
resolution.Candidates = candidates
|
resolution.Candidates = candidates
|
||||||
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
||||||
log.Printf("[HTTP] Asset resolve slow: object_id=%s type=%s active_revision=%s list_revision=%s elapsed=%s", objectId, assetType, activeRevision, revision, elapsed)
|
log.Printf("[HTTP] Asset resolve slow: object_id=%s type=%s platform=%s active_revision=%s list_revision=%s elapsed=%s", objectId, assetType, platform, activeRevision, revision, elapsed)
|
||||||
}
|
}
|
||||||
return resolution, true
|
return resolution, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
||||||
log.Printf("[HTTP] Asset resolve miss: object_id=%s type=%s active_revision=%s elapsed=%s", objectId, assetType, activeRevision, elapsed)
|
log.Printf("[HTTP] Asset resolve miss: object_id=%s type=%s platform=%s active_revision=%s elapsed=%s", objectId, assetType, platform, activeRevision, elapsed)
|
||||||
}
|
}
|
||||||
return resolution, false
|
return resolution, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *assetResolver) Prewarm(activeRevision string) {
|
func (r *assetResolver) Prewarm(activeRevision, platform string) {
|
||||||
if activeRevision == "" {
|
if activeRevision == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = loadListBinIndex(r.baseDir, activeRevision)
|
_, _ = loadListBinIndex(r.baseDir, activeRevision, platform)
|
||||||
_ = loadInfoIndex(r.baseDir, activeRevision)
|
_ = loadInfoIndex(r.baseDir, activeRevision, platform)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,29 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BannerServiceServer struct {
|
type BannerServiceServer struct {
|
||||||
pb.UnimplementedBannerServiceServer
|
pb.UnimplementedBannerServiceServer
|
||||||
catalog []store.GachaCatalogEntry
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBannerServiceServer(catalog []store.GachaCatalogEntry) *BannerServiceServer {
|
func NewBannerServiceServer(holder *runtime.Holder) *BannerServiceServer {
|
||||||
return &BannerServiceServer{catalog: catalog}
|
return &BannerServiceServer{holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) {
|
func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) {
|
||||||
catalog := s.catalog
|
catalog := s.holder.Get().GachaEntries
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
var termLimited []*pb.GachaBanner
|
var termLimited []*pb.GachaBanner
|
||||||
var latestChapter *pb.GachaBanner
|
var latestChapter *pb.GachaBanner
|
||||||
for _, entry := range catalog {
|
for _, entry := range catalog {
|
||||||
|
if !gachaActiveAt(entry, nowMillis) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle {
|
if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,21 +15,18 @@ type CageOrnamentServiceServer struct {
|
|||||||
pb.UnimplementedCageOrnamentServiceServer
|
pb.UnimplementedCageOrnamentServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.CageOrnamentCatalog
|
holder *runtime.Holder
|
||||||
granter *store.PossessionGranter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CageOrnamentCatalog, granter *store.PossessionGranter) *CageOrnamentServiceServer {
|
func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CageOrnamentServiceServer {
|
||||||
return &CageOrnamentServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter}
|
return &CageOrnamentServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.ReceiveRewardRequest) (*pb.ReceiveRewardResponse, error) {
|
func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.ReceiveRewardRequest) (*pb.ReceiveRewardResponse, error) {
|
||||||
log.Printf("[CageOrnamentService] ReceiveReward: cageOrnamentId=%d", req.CageOrnamentId)
|
log.Printf("[CageOrnamentService] ReceiveReward: cageOrnamentId=%d", req.CageOrnamentId)
|
||||||
|
|
||||||
reward, ok := s.catalog.LookupReward(req.CageOrnamentId)
|
cat := s.holder.Get()
|
||||||
if !ok {
|
reward, ok := cat.CageOrnament.LookupReward(req.CageOrnamentId)
|
||||||
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()
|
nowMillis := gametime.NowMillis()
|
||||||
@@ -39,9 +36,21 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R
|
|||||||
AcquisitionDatetime: nowMillis,
|
AcquisitionDatetime: nowMillis,
|
||||||
LatestVersion: nowMillis,
|
LatestVersion: nowMillis,
|
||||||
}
|
}
|
||||||
s.granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
if ok {
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
// "Fickle Black Birds" (type-1 gimmicks) tap into this RPC with CageOrnamentIds
|
||||||
|
// not present in m_cage_ornament_reward (their GimmickOrnamentViewIds are 101/103,
|
||||||
|
// not the 1002xxx-style ids the table uses). Record the access and return an empty
|
||||||
|
// reward so the client doesn't hang and the server doesn't crash.
|
||||||
|
log.Printf("[CageOrnamentService] ReceiveReward: no reward mapping for cageOrnamentId=%d, returning empty",
|
||||||
|
req.CageOrnamentId)
|
||||||
|
return &pb.ReceiveRewardResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
return &pb.ReceiveRewardResponse{
|
return &pb.ReceiveRewardResponse{
|
||||||
CageOrnamentReward: []*pb.CageOrnamentReward{
|
CageOrnamentReward: []*pb.CageOrnamentReward{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,21 +15,23 @@ type CharacterServiceServer struct {
|
|||||||
pb.UnimplementedCharacterServiceServer
|
pb.UnimplementedCharacterServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.CharacterRebirthCatalog
|
holder *runtime.Holder
|
||||||
config *masterdata.GameConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterRebirthCatalog, config *masterdata.GameConfig) *CharacterServiceServer {
|
func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterServiceServer {
|
||||||
return &CharacterServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
return &CharacterServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthRequest) (*pb.RebirthResponse, error) {
|
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)
|
log.Printf("[CharacterService] Rebirth: characterId=%d rebirthCount=%d", req.CharacterId, req.RebirthCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.CharacterRebirth
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
stepGroupId, ok := s.catalog.StepGroupByCharacterId[req.CharacterId]
|
stepGroupId, ok := catalog.StepGroupByCharacterId[req.CharacterId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CharacterService] Rebirth: no step group for characterId=%d", req.CharacterId)
|
log.Printf("[CharacterService] Rebirth: no step group for characterId=%d", req.CharacterId)
|
||||||
return &pb.RebirthResponse{}, nil
|
return &pb.RebirthResponse{}, nil
|
||||||
@@ -40,17 +43,17 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq
|
|||||||
targetCount := currentCount + req.RebirthCount
|
targetCount := currentCount + req.RebirthCount
|
||||||
|
|
||||||
for count := currentCount; count < targetCount; count++ {
|
for count := currentCount; count < targetCount; count++ {
|
||||||
step, ok := s.catalog.StepByGroupAndCount[masterdata.StepKey{GroupId: stepGroupId, BeforeRebirthCount: count}]
|
step, ok := catalog.StepByGroupAndCount[masterdata.StepKey{GroupId: stepGroupId, BeforeRebirthCount: count}]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CharacterService] Rebirth: no step row for groupId=%d beforeCount=%d", stepGroupId, count)
|
log.Printf("[CharacterService] Rebirth: no step row for groupId=%d beforeCount=%d", stepGroupId, count)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
goldId := s.config.ConsumableItemIdForGold
|
goldId := config.ConsumableItemIdForGold
|
||||||
user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-s.config.CharacterRebirthConsumeGold, 0)
|
user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-config.CharacterRebirthConsumeGold, 0)
|
||||||
log.Printf("[CharacterService] Rebirth: consumed gold=%d", s.config.CharacterRebirthConsumeGold)
|
log.Printf("[CharacterService] Rebirth: consumed gold=%d", config.CharacterRebirthConsumeGold)
|
||||||
|
|
||||||
materials := s.catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId]
|
materials := catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId]
|
||||||
for _, mat := range materials {
|
for _, mat := range materials {
|
||||||
user.Materials[mat.MaterialId] -= mat.Count
|
user.Materials[mat.MaterialId] -= mat.Count
|
||||||
if user.Materials[mat.MaterialId] <= 0 {
|
if user.Materials[mat.MaterialId] <= 0 {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,43 +15,44 @@ type CharacterBoardServiceServer struct {
|
|||||||
pb.UnimplementedCharacterBoardServiceServer
|
pb.UnimplementedCharacterBoardServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.CharacterBoardCatalog
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterBoardCatalog) *CharacterBoardServiceServer {
|
func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterBoardServiceServer {
|
||||||
return &CharacterBoardServiceServer{users: users, sessions: sessions, catalog: catalog}
|
return &CharacterBoardServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) {
|
func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) {
|
||||||
log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId)
|
log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId)
|
||||||
|
|
||||||
|
catalog := s.holder.Get().CharacterBoard
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
for _, panelId := range req.CharacterBoardPanelId {
|
for _, panelId := range req.CharacterBoardPanelId {
|
||||||
panel, ok := s.catalog.PanelById[panelId]
|
panel, ok := catalog.PanelById[panelId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CharacterBoardService] unknown panelId=%d, skipping", panelId)
|
log.Printf("[CharacterBoardService] unknown panelId=%d, skipping", panelId)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
s.consumeCosts(user, panel)
|
consumeBoardCosts(catalog, user, panel)
|
||||||
s.setReleaseBit(user, panel)
|
setBoardReleaseBit(user, panel)
|
||||||
s.applyEffects(user, panel)
|
applyBoardEffects(catalog, user, panel)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.ReleasePanelResponse{}, nil
|
return &pb.ReleasePanelResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterBoardServiceServer) consumeCosts(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
|
func consumeBoardCosts(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
|
||||||
costs := s.catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId]
|
costs := catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId]
|
||||||
for _, cost := range costs {
|
for _, cost := range costs {
|
||||||
store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count)
|
store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
|
func setBoardReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
|
||||||
boardId := panel.CharacterBoardId
|
boardId := panel.CharacterBoardId
|
||||||
board := user.CharacterBoards[boardId]
|
board := user.CharacterBoards[boardId]
|
||||||
board.CharacterBoardId = boardId
|
board.CharacterBoardId = boardId
|
||||||
@@ -73,26 +75,26 @@ func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel
|
|||||||
user.CharacterBoards[boardId] = board
|
user.CharacterBoards[boardId] = board
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
|
func applyBoardEffects(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
|
||||||
effects := s.catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId]
|
effects := catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId]
|
||||||
for _, eff := range effects {
|
for _, eff := range effects {
|
||||||
switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) {
|
switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) {
|
||||||
case model.CharacterBoardEffectTypeAbility:
|
case model.CharacterBoardEffectTypeAbility:
|
||||||
s.applyAbilityEffect(user, eff)
|
applyBoardAbilityEffect(catalog, user, eff)
|
||||||
case model.CharacterBoardEffectTypeStatusUp:
|
case model.CharacterBoardEffectTypeStatusUp:
|
||||||
s.applyStatusUpEffect(user, eff)
|
applyBoardStatusUpEffect(catalog, user, eff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) {
|
func applyBoardAbilityEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) {
|
||||||
ability, ok := s.catalog.AbilityById[eff.CharacterBoardEffectId]
|
ability, ok := catalog.AbilityById[eff.CharacterBoardEffectId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId)
|
log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
characterId := s.resolveCharacterId(ability.CharacterBoardEffectTargetGroupId)
|
characterId := resolveBoardCharacterId(catalog, ability.CharacterBoardEffectTargetGroupId)
|
||||||
if characterId == 0 {
|
if characterId == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -103,21 +105,21 @@ func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState,
|
|||||||
state.AbilityId = ability.AbilityId
|
state.AbilityId = ability.AbilityId
|
||||||
state.Level += eff.EffectValue
|
state.Level += eff.EffectValue
|
||||||
|
|
||||||
if maxLvl, ok := s.catalog.AbilityMaxLevel[key]; ok && state.Level > maxLvl {
|
if maxLvl, ok := catalog.AbilityMaxLevel[key]; ok && state.Level > maxLvl {
|
||||||
state.Level = maxLvl
|
state.Level = maxLvl
|
||||||
}
|
}
|
||||||
|
|
||||||
user.CharacterBoardAbilities[key] = state
|
user.CharacterBoardAbilities[key] = state
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) {
|
func applyBoardStatusUpEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) {
|
||||||
statusUp, ok := s.catalog.StatusUpById[eff.CharacterBoardEffectId]
|
statusUp, ok := catalog.StatusUpById[eff.CharacterBoardEffectId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId)
|
log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
characterId := s.resolveCharacterId(statusUp.CharacterBoardEffectTargetGroupId)
|
characterId := resolveBoardCharacterId(catalog, statusUp.CharacterBoardEffectTargetGroupId)
|
||||||
if characterId == 0 {
|
if characterId == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -151,8 +153,8 @@ func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState,
|
|||||||
user.CharacterBoardStatusUps[key] = state
|
user.CharacterBoardStatusUps[key] = state
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterBoardServiceServer) resolveCharacterId(targetGroupId int32) int32 {
|
func resolveBoardCharacterId(catalog *masterdata.CharacterBoardCatalog, targetGroupId int32) int32 {
|
||||||
targets := s.catalog.EffectTargetsByGroupId[targetGroupId]
|
targets := catalog.EffectTargetsByGroupId[targetGroupId]
|
||||||
for _, t := range targets {
|
for _, t := range targets {
|
||||||
if t.TargetValue != 0 {
|
if t.TargetValue != 0 {
|
||||||
return t.TargetValue
|
return t.TargetValue
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -16,11 +16,11 @@ type CharacterViewerServiceServer struct {
|
|||||||
pb.UnimplementedCharacterViewerServiceServer
|
pb.UnimplementedCharacterViewerServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.CharacterViewerCatalog
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterViewerCatalog) *CharacterViewerServiceServer {
|
func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterViewerServiceServer {
|
||||||
return &CharacterViewerServiceServer{users: users, sessions: sessions, catalog: catalog}
|
return &CharacterViewerServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ *emptypb.Empty) (*pb.CharacterViewerTopResponse, error) {
|
func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ *emptypb.Empty) (*pb.CharacterViewerTopResponse, error) {
|
||||||
@@ -32,7 +32,7 @@ func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _
|
|||||||
panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err))
|
panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
released := s.catalog.ReleasedFieldIds(user)
|
released := s.holder.Get().CharacterViewer.ReleasedFieldIds(user)
|
||||||
log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId)
|
log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId)
|
||||||
|
|
||||||
return &pb.CharacterViewerTopResponse{
|
return &pb.CharacterViewerTopResponse{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,17 +18,19 @@ type CompanionServiceServer struct {
|
|||||||
pb.UnimplementedCompanionServiceServer
|
pb.UnimplementedCompanionServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.CompanionCatalog
|
holder *runtime.Holder
|
||||||
config *masterdata.GameConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CompanionCatalog, config *masterdata.GameConfig) *CompanionServiceServer {
|
func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CompanionServiceServer {
|
||||||
return &CompanionServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
return &CompanionServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionEnhanceRequest) (*pb.CompanionEnhanceResponse, error) {
|
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)
|
log.Printf("[CompanionService] Enhance: uuid=%s addLevel=%d", req.UserCompanionUuid, req.AddLevelCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Companion
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -38,7 +41,7 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
compDef, ok := s.catalog.CompanionById[companion.CompanionId]
|
compDef, ok := catalog.CompanionById[companion.CompanionId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CompanionService] Enhance: companion master id=%d not found", companion.CompanionId)
|
log.Printf("[CompanionService] Enhance: companion master id=%d not found", companion.CompanionId)
|
||||||
return
|
return
|
||||||
@@ -50,13 +53,13 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE
|
|||||||
}
|
}
|
||||||
|
|
||||||
for lvl := companion.Level; lvl < targetLevel; lvl++ {
|
for lvl := companion.Level; lvl < targetLevel; lvl++ {
|
||||||
if costFunc, ok := s.catalog.GoldCostByCategory[compDef.CompanionCategoryType]; ok {
|
if costFunc, ok := catalog.GoldCostByCategory[compDef.CompanionCategoryType]; ok {
|
||||||
goldCost := costFunc.Evaluate(lvl)
|
goldCost := costFunc.Evaluate(lvl)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
}
|
}
|
||||||
|
|
||||||
matKey := masterdata.CompanionLevelKey{CategoryType: compDef.CompanionCategoryType, Level: lvl}
|
matKey := masterdata.CompanionLevelKey{CategoryType: compDef.CompanionCategoryType, Level: lvl}
|
||||||
if mat, ok := s.catalog.MaterialsByKey[matKey]; ok {
|
if mat, ok := catalog.MaterialsByKey[matKey]; ok {
|
||||||
user.Materials[mat.MaterialId] -= mat.Count
|
user.Materials[mat.MaterialId] -= mat.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/gametime"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,23 +16,68 @@ type ConsumableItemServiceServer struct {
|
|||||||
pb.UnimplementedConsumableItemServiceServer
|
pb.UnimplementedConsumableItemServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.ConsumableItemCatalog
|
holder *runtime.Holder
|
||||||
config *masterdata.GameConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ConsumableItemCatalog, config *masterdata.GameConfig) *ConsumableItemServiceServer {
|
func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ConsumableItemServiceServer {
|
||||||
return &ConsumableItemServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
return &ConsumableItemServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConsumableItemServiceServer) UseEffectItem(ctx context.Context, req *pb.ConsumableItemUseEffectItemRequest) (*pb.ConsumableItemUseEffectItemResponse, error) {
|
||||||
|
log.Printf("[ConsumableItemService] UseEffectItem: consumableItemId=%d count=%d", req.ConsumableItemId, req.Count)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.ConsumableItem
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
if _, ok := catalog.All[req.ConsumableItemId]; !ok {
|
||||||
|
log.Printf("[ConsumableItemService] UseEffectItem: unknown consumableItemId=%d", req.ConsumableItemId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cur := user.ConsumableItems[req.ConsumableItemId]
|
||||||
|
if cur < req.Count {
|
||||||
|
log.Printf("[ConsumableItemService] UseEffectItem: insufficient consumableItemId=%d have=%d need=%d", req.ConsumableItemId, cur, req.Count)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.ConsumableItems[req.ConsumableItemId] -= req.Count
|
||||||
|
if user.ConsumableItems[req.ConsumableItemId] <= 0 {
|
||||||
|
delete(user.ConsumableItems, req.ConsumableItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
maxStaminaMillis := cat.Shop.MaxStaminaMillis[user.Status.Level]
|
||||||
|
for _, effect := range catalog.Effects[req.ConsumableItemId] {
|
||||||
|
switch effect.EffectTargetType {
|
||||||
|
case model.EffectTargetStaminaRecovery:
|
||||||
|
millis := store.ResolveStaminaEffectMillis(effect.EffectValueType, effect.EffectValue, maxStaminaMillis)
|
||||||
|
store.RecoverStamina(user, millis*req.Count, maxStaminaMillis, nowMillis)
|
||||||
|
default:
|
||||||
|
log.Printf("[ConsumableItemService] UseEffectItem: unhandled effect targetType=%d valueType=%d value=%d itemId=%d",
|
||||||
|
effect.EffectTargetType, effect.EffectValueType, effect.EffectValue, req.ConsumableItemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("consumable item use effect item: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ConsumableItemUseEffectItemResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) {
|
func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) {
|
||||||
log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession))
|
log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession))
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.ConsumableItem
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|
||||||
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
totalGold := int32(0)
|
totalGold := int32(0)
|
||||||
for _, item := range req.ConsumableItemPossession {
|
for _, item := range req.ConsumableItemPossession {
|
||||||
row, ok := s.catalog.All[item.ConsumableItemId]
|
row, ok := catalog.All[item.ConsumableItemId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId)
|
log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId)
|
||||||
continue
|
continue
|
||||||
@@ -53,7 +100,7 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab
|
|||||||
}
|
}
|
||||||
|
|
||||||
if totalGold > 0 {
|
if totalGold > 0 {
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
|
user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
|
||||||
log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold)
|
log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,17 +21,19 @@ type CostumeServiceServer struct {
|
|||||||
pb.UnimplementedCostumeServiceServer
|
pb.UnimplementedCostumeServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.CostumeCatalog
|
holder *runtime.Holder
|
||||||
config *masterdata.GameConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CostumeCatalog, config *masterdata.GameConfig) *CostumeServiceServer {
|
func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CostumeServiceServer {
|
||||||
return &CostumeServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
return &CostumeServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceRequest) (*pb.EnhanceResponse, error) {
|
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)
|
log.Printf("[CostumeService] Enhance: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Costume
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cm, ok := s.catalog.Costumes[costume.CostumeId]
|
cm, ok := catalog.Costumes[costume.CostumeId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] Enhance: costume master id=%d not found", costume.CostumeId)
|
log.Printf("[CostumeService] Enhance: costume master id=%d not found", costume.CostumeId)
|
||||||
return
|
return
|
||||||
@@ -50,7 +53,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
totalExp := int32(0)
|
totalExp := int32(0)
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
for materialId, count := range req.Materials {
|
for materialId, count := range req.Materials {
|
||||||
mat, ok := s.catalog.Materials[materialId]
|
mat, ok := catalog.Materials[materialId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] Enhance: material id=%d not found, skipping", materialId)
|
log.Printf("[CostumeService] Enhance: material id=%d not found, skipping", materialId)
|
||||||
continue
|
continue
|
||||||
@@ -66,20 +69,20 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
|
|
||||||
expPerUnit := mat.EffectValue
|
expPerUnit := mat.EffectValue
|
||||||
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
|
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
|
||||||
expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000
|
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += expPerUnit * count
|
totalExp += expPerUnit * count
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
||||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
log.Printf("[CostumeService] Enhance: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
|
log.Printf("[CostumeService] Enhance: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
costume.Exp += totalExp
|
costume.Exp += totalExp
|
||||||
|
|
||||||
if thresholds, ok := s.catalog.ExpByRarity[cm.RarityType]; ok {
|
if thresholds, ok := catalog.ExpByRarity[cm.RarityType]; ok {
|
||||||
costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds)
|
costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +103,9 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest) (*pb.AwakenResponse, error) {
|
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)
|
log.Printf("[CostumeService] Awaken: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Costume
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -110,7 +116,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
awakenRow, ok := s.catalog.AwakenByCostumeId[costume.CostumeId]
|
awakenRow, ok := catalog.AwakenByCostumeId[costume.CostumeId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] Awaken: no awaken data for costumeId=%d", costume.CostumeId)
|
log.Printf("[CostumeService] Awaken: no awaken data for costumeId=%d", costume.CostumeId)
|
||||||
return
|
return
|
||||||
@@ -118,8 +124,8 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
|
|||||||
|
|
||||||
nextStep := costume.AwakenCount + 1
|
nextStep := costume.AwakenCount + 1
|
||||||
|
|
||||||
if gold, ok := s.catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok {
|
if gold, ok := catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok {
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= gold
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= gold
|
||||||
log.Printf("[CostumeService] Awaken: gold cost=%d", gold)
|
log.Printf("[CostumeService] Awaken: gold cost=%d", gold)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +143,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
|
|||||||
user.Costumes[req.UserCostumeUuid] = costume
|
user.Costumes[req.UserCostumeUuid] = costume
|
||||||
log.Printf("[CostumeService] Awaken: costumeId=%d awakenCount=%d", costume.CostumeId, nextStep)
|
log.Printf("[CostumeService] Awaken: costumeId=%d awakenCount=%d", costume.CostumeId, nextStep)
|
||||||
|
|
||||||
effectSteps, ok := s.catalog.AwakenEffectsByGroupAndStep[awakenRow.CostumeAwakenEffectGroupId]
|
effectSteps, ok := catalog.AwakenEffectsByGroupAndStep[awakenRow.CostumeAwakenEffectGroupId]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -148,11 +154,11 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
|
|||||||
|
|
||||||
switch model.CostumeAwakenEffectType(effect.CostumeAwakenEffectType) {
|
switch model.CostumeAwakenEffectType(effect.CostumeAwakenEffectType) {
|
||||||
case model.CostumeAwakenEffectTypeStatusUp:
|
case model.CostumeAwakenEffectTypeStatusUp:
|
||||||
s.applyAwakenStatusUp(user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis)
|
applyCostumeAwakenStatusUp(catalog, user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis)
|
||||||
case model.CostumeAwakenEffectTypeAbility:
|
case model.CostumeAwakenEffectTypeAbility:
|
||||||
log.Printf("[CostumeService] Awaken: ability effect id=%d (client-resolved)", effect.CostumeAwakenEffectId)
|
log.Printf("[CostumeService] Awaken: ability effect id=%d (client-resolved)", effect.CostumeAwakenEffectId)
|
||||||
case model.CostumeAwakenEffectTypeItemAcquire:
|
case model.CostumeAwakenEffectTypeItemAcquire:
|
||||||
s.applyAwakenItemAcquire(user, effect.CostumeAwakenEffectId, nowMillis)
|
applyCostumeAwakenItemAcquire(catalog, user, effect.CostumeAwakenEffectId, nowMillis)
|
||||||
default:
|
default:
|
||||||
log.Printf("[CostumeService] Awaken: unknown effect type=%d", effect.CostumeAwakenEffectType)
|
log.Printf("[CostumeService] Awaken: unknown effect type=%d", effect.CostumeAwakenEffectType)
|
||||||
}
|
}
|
||||||
@@ -164,8 +170,8 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
|
|||||||
return &pb.AwakenResponse{}, nil
|
return &pb.AwakenResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) {
|
func applyCostumeAwakenStatusUp(catalog *masterdata.CostumeCatalog, user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) {
|
||||||
rows, ok := s.catalog.AwakenStatusUpByGroup[statusUpGroupId]
|
rows, ok := catalog.AwakenStatusUpByGroup[statusUpGroupId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] Awaken: status up group %d not found", statusUpGroupId)
|
log.Printf("[CostumeService] Awaken: status up group %d not found", statusUpGroupId)
|
||||||
return
|
return
|
||||||
@@ -201,8 +207,8 @@ func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, itemAcquireId int32, nowMillis int64) {
|
func applyCostumeAwakenItemAcquire(catalog *masterdata.CostumeCatalog, user *store.UserState, itemAcquireId int32, nowMillis int64) {
|
||||||
acq, ok := s.catalog.AwakenItemAcquireById[itemAcquireId]
|
acq, ok := catalog.AwakenItemAcquireById[itemAcquireId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] Awaken: item acquire id=%d not found", itemAcquireId)
|
log.Printf("[CostumeService] Awaken: item acquire id=%d not found", itemAcquireId)
|
||||||
return
|
return
|
||||||
@@ -226,6 +232,9 @@ func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, ite
|
|||||||
func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.EnhanceActiveSkillRequest) (*pb.EnhanceActiveSkillResponse, error) {
|
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)
|
log.Printf("[CostumeService] EnhanceActiveSkill: uuid=%s addLevel=%d", req.UserCostumeUuid, req.AddLevelCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Costume
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -236,13 +245,13 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cm, ok := s.catalog.Costumes[costume.CostumeId]
|
cm, ok := catalog.Costumes[costume.CostumeId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] EnhanceActiveSkill: costume master id=%d not found", costume.CostumeId)
|
log.Printf("[CostumeService] EnhanceActiveSkill: costume master id=%d not found", costume.CostumeId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
groupRows := s.catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId]
|
groupRows := catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId]
|
||||||
enhanceMatId := int32(-1)
|
enhanceMatId := int32(-1)
|
||||||
for _, g := range groupRows {
|
for _, g := range groupRows {
|
||||||
if g.CostumeLimitBreakCountLowerLimit <= costume.LimitBreakCount {
|
if g.CostumeLimitBreakCountLowerLimit <= costume.LimitBreakCount {
|
||||||
@@ -259,7 +268,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
|
|||||||
skill := user.CostumeActiveSkills[req.UserCostumeUuid]
|
skill := user.CostumeActiveSkills[req.UserCostumeUuid]
|
||||||
currentLevel := skill.Level
|
currentLevel := skill.Level
|
||||||
|
|
||||||
maxLevelFunc, ok := s.catalog.ActiveSkillMaxLevelByRarity[cm.RarityType]
|
maxLevelFunc, ok := catalog.ActiveSkillMaxLevelByRarity[cm.RarityType]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] EnhanceActiveSkill: no max level func for rarity=%d", cm.RarityType)
|
log.Printf("[CostumeService] EnhanceActiveSkill: no max level func for rarity=%d", cm.RarityType)
|
||||||
return
|
return
|
||||||
@@ -277,7 +286,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
|
|||||||
|
|
||||||
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
||||||
key := [2]int32{enhanceMatId, lvl}
|
key := [2]int32{enhanceMatId, lvl}
|
||||||
mats := s.catalog.ActiveSkillEnhanceMats[key]
|
mats := catalog.ActiveSkillEnhanceMats[key]
|
||||||
for _, mat := range mats {
|
for _, mat := range mats {
|
||||||
cur := user.Materials[mat.MaterialId]
|
cur := user.Materials[mat.MaterialId]
|
||||||
cost := mat.Count
|
cost := mat.Count
|
||||||
@@ -288,9 +297,9 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
|
|||||||
user.Materials[mat.MaterialId] = cur - cost
|
user.Materials[mat.MaterialId] = cur - cost
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.ActiveSkillCostByRarity[cm.RarityType]; ok {
|
if costFunc, ok := catalog.ActiveSkillCostByRarity[cm.RarityType]; ok {
|
||||||
goldCost := costFunc.Evaluate(lvl + 1)
|
goldCost := costFunc.Evaluate(lvl + 1)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +319,9 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
|
|||||||
func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBreakRequest) (*pb.LimitBreakResponse, error) {
|
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)
|
log.Printf("[CostumeService] LimitBreak: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Costume
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -320,12 +332,12 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if costume.LimitBreakCount >= s.config.CostumeLimitBreakAvailableCount {
|
if costume.LimitBreakCount >= config.CostumeLimitBreakAvailableCount {
|
||||||
log.Printf("[CostumeService] LimitBreak: already at max limit break %d", costume.LimitBreakCount)
|
log.Printf("[CostumeService] LimitBreak: already at max limit break %d", costume.LimitBreakCount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cm, ok := s.catalog.Costumes[costume.CostumeId]
|
cm, ok := catalog.Costumes[costume.CostumeId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] LimitBreak: costume master id=%d not found", costume.CostumeId)
|
log.Printf("[CostumeService] LimitBreak: costume master id=%d not found", costume.CostumeId)
|
||||||
return
|
return
|
||||||
@@ -342,9 +354,9 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
|
|||||||
totalMaterialCount += count
|
totalMaterialCount += count
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.LimitBreakCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.LimitBreakCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
||||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
log.Printf("[CostumeService] LimitBreak: gold cost=%d", goldCost)
|
log.Printf("[CostumeService] LimitBreak: gold cost=%d", goldCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +375,9 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
|
|||||||
func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req *pb.UnlockLotteryEffectSlotRequest) (*pb.UnlockLotteryEffectSlotResponse, error) {
|
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)
|
log.Printf("[CostumeService] UnlockLotteryEffectSlot: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Costume
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -373,15 +388,15 @@ func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
|
effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
|
log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectUnlockSlotConsumeGold
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectUnlockSlotConsumeGold
|
||||||
|
|
||||||
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId]
|
mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId]
|
||||||
for _, mat := range mats {
|
for _, mat := range mats {
|
||||||
cur := user.Materials[mat.MaterialId]
|
cur := user.Materials[mat.MaterialId]
|
||||||
cost := mat.Count
|
cost := mat.Count
|
||||||
@@ -418,6 +433,9 @@ func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req
|
|||||||
func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.DrawLotteryEffectRequest) (*pb.DrawLotteryEffectResponse, error) {
|
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)
|
log.Printf("[CostumeService] DrawLotteryEffect: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Costume
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -428,21 +446,21 @@ func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.Dr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
|
effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
|
log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oddsPool := s.catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId]
|
oddsPool := catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId]
|
||||||
if len(oddsPool) == 0 {
|
if len(oddsPool) == 0 {
|
||||||
log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId)
|
log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectDrawSlotConsumeGold
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectDrawSlotConsumeGold
|
||||||
|
|
||||||
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId]
|
mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId]
|
||||||
for _, mat := range mats {
|
for _, mat := range mats {
|
||||||
cur := user.Materials[mat.MaterialId]
|
cur := user.Materials[mat.MaterialId]
|
||||||
cost := mat.Count
|
cost := mat.Count
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
@@ -12,6 +13,16 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// masterDataBinPath is the canonical location of the encrypted master data
|
||||||
|
// file. The mtime of this file is folded into the version string so the
|
||||||
|
// client invalidates its cache as soon as an admin reload swaps it in.
|
||||||
|
const masterDataBinPath = "assets/release/20240404193219.bin.e"
|
||||||
|
|
||||||
|
// masterDataBaseVersion preserves the historical "yyyymmddHHMMSS" value the
|
||||||
|
// client has always seen; we suffix it with the file mtime to force a
|
||||||
|
// re-download when content changes.
|
||||||
|
const masterDataBaseVersion = "20240404193219"
|
||||||
|
|
||||||
type DataServiceServer struct {
|
type DataServiceServer struct {
|
||||||
pb.UnimplementedDataServiceServer
|
pb.UnimplementedDataServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
@@ -23,9 +34,15 @@ func NewDataServiceServer(users store.UserRepository, sessions store.SessionRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DataServiceServer) GetLatestMasterDataVersion(ctx context.Context, _ *emptypb.Empty) (*pb.MasterDataGetLatestVersionResponse, error) {
|
func (s *DataServiceServer) GetLatestMasterDataVersion(ctx context.Context, _ *emptypb.Empty) (*pb.MasterDataGetLatestVersionResponse, error) {
|
||||||
log.Printf("[DataService] GetLatestMasterDataVersion")
|
version := masterDataBaseVersion
|
||||||
|
if info, err := os.Stat(masterDataBinPath); err == nil {
|
||||||
|
version = fmt.Sprintf("%s_%d", masterDataBaseVersion, info.ModTime().UnixMilli())
|
||||||
|
} else {
|
||||||
|
log.Printf("[DataService] stat %s: %v (falling back to base version)", masterDataBinPath, err)
|
||||||
|
}
|
||||||
|
log.Printf("[DataService] GetLatestMasterDataVersion -> %s", version)
|
||||||
return &pb.MasterDataGetLatestVersionResponse{
|
return &pb.MasterDataGetLatestVersionResponse{
|
||||||
LatestMasterDataVersion: "20240404193219",
|
LatestMasterDataVersion: version,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,11 +191,45 @@ func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.Repla
|
|||||||
}
|
}
|
||||||
store.ApplyDeckReplacement(user, model.DeckType(detail.DeckType), detail.UserDeckNumber, deckSlotsFromProto(detail.Deck), nowMillis)
|
store.ApplyDeckReplacement(user, model.DeckType(detail.DeckType), detail.UserDeckNumber, deckSlotsFromProto(detail.Deck), nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
key := store.DeckKey{DeckType: model.DeckType(req.DeckType), UserDeckNumber: req.UserDeckNumber}
|
||||||
|
td := user.TripleDecks[key]
|
||||||
|
td.DeckType = model.DeckType(req.DeckType)
|
||||||
|
td.UserDeckNumber = req.UserDeckNumber
|
||||||
|
td.DeckNumber01 = innerDeckNumber(req.DeckDetail01)
|
||||||
|
td.DeckNumber02 = innerDeckNumber(req.DeckDetail02)
|
||||||
|
td.DeckNumber03 = innerDeckNumber(req.DeckDetail03)
|
||||||
|
td.LatestVersion = nowMillis
|
||||||
|
user.TripleDecks[key] = td
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.ReplaceTripleDeckResponse{}, nil
|
return &pb.ReplaceTripleDeckResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func innerDeckNumber(d *pb.DeckDetail) int32 {
|
||||||
|
if d == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return d.UserDeckNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DeckServiceServer) UpdateTripleDeckName(ctx context.Context, req *pb.UpdateTripleDeckNameRequest) (*pb.UpdateTripleDeckNameResponse, error) {
|
||||||
|
log.Printf("[DeckService] UpdateTripleDeckName: deckType=%d deckNumber=%d name=%q", req.DeckType, req.UserDeckNumber, req.Name)
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|
||||||
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
key := store.DeckKey{DeckType: model.DeckType(req.DeckType), UserDeckNumber: req.UserDeckNumber}
|
||||||
|
td := user.TripleDecks[key]
|
||||||
|
td.DeckType = model.DeckType(req.DeckType)
|
||||||
|
td.UserDeckNumber = req.UserDeckNumber
|
||||||
|
td.Name = req.Name
|
||||||
|
td.LatestVersion = gametime.NowMillis()
|
||||||
|
user.TripleDecks[key] = td
|
||||||
|
})
|
||||||
|
|
||||||
|
return &pb.UpdateTripleDeckNameResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DeckServiceServer) ReplaceMultiDeck(ctx context.Context, req *pb.ReplaceMultiDeckRequest) (*pb.ReplaceMultiDeckResponse, error) {
|
func (s *DeckServiceServer) ReplaceMultiDeck(ctx context.Context, req *pb.ReplaceMultiDeckRequest) (*pb.ReplaceMultiDeckResponse, error) {
|
||||||
log.Printf("[DeckService] ReplaceMultiDeck: %d entries", len(req.DeckDetail))
|
log.Printf("[DeckService] ReplaceMultiDeck: %d entries", len(req.DeckDetail))
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,17 +22,18 @@ type ExploreServiceServer struct {
|
|||||||
pb.UnimplementedExploreServiceServer
|
pb.UnimplementedExploreServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.ExploreCatalog
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ExploreCatalog) *ExploreServiceServer {
|
func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ExploreServiceServer {
|
||||||
return &ExploreServiceServer{users: users, sessions: sessions, catalog: catalog}
|
return &ExploreServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartExploreRequest) (*pb.StartExploreResponse, error) {
|
func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartExploreRequest) (*pb.StartExploreResponse, error) {
|
||||||
log.Printf("[ExploreService] StartExplore: exploreId=%d useConsumableItemId=%d", req.ExploreId, req.UseConsumableItemId)
|
log.Printf("[ExploreService] StartExplore: exploreId=%d useConsumableItemId=%d", req.ExploreId, req.UseConsumableItemId)
|
||||||
|
|
||||||
if _, ok := s.catalog.Explores[req.ExploreId]; !ok {
|
catalog := s.holder.Get().Explore
|
||||||
|
if _, ok := catalog.Explores[req.ExploreId]; !ok {
|
||||||
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
|
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx
|
|||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
explore := s.catalog.Explores[req.ExploreId]
|
explore := catalog.Explores[req.ExploreId]
|
||||||
if req.UseConsumableItemId > 0 && explore.ConsumeItemCount > 0 {
|
if req.UseConsumableItemId > 0 && explore.ConsumeItemCount > 0 {
|
||||||
cur := user.ConsumableItems[req.UseConsumableItemId]
|
cur := user.ConsumableItems[req.UseConsumableItemId]
|
||||||
user.ConsumableItems[req.UseConsumableItemId] = cur - explore.ConsumeItemCount
|
user.ConsumableItems[req.UseConsumableItemId] = cur - explore.ConsumeItemCount
|
||||||
@@ -64,12 +65,13 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx
|
|||||||
func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.FinishExploreRequest) (*pb.FinishExploreResponse, error) {
|
func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.FinishExploreRequest) (*pb.FinishExploreResponse, error) {
|
||||||
log.Printf("[ExploreService] FinishExplore: exploreId=%d score=%d", req.ExploreId, req.Score)
|
log.Printf("[ExploreService] FinishExplore: exploreId=%d score=%d", req.ExploreId, req.Score)
|
||||||
|
|
||||||
explore, ok := s.catalog.Explores[req.ExploreId]
|
catalog := s.holder.Get().Explore
|
||||||
|
explore, ok := catalog.Explores[req.ExploreId]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
|
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
|
||||||
}
|
}
|
||||||
|
|
||||||
assetGradeIconId := s.catalog.GradeForScore(req.ExploreId, req.Score)
|
assetGradeIconId := catalog.GradeForScore(req.ExploreId, req.Score)
|
||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gacha"
|
"lunar-tear/server/internal/gacha"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -20,34 +21,33 @@ type GachaServiceServer struct {
|
|||||||
pb.UnimplementedGachaServiceServer
|
pb.UnimplementedGachaServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog []store.GachaCatalogEntry
|
holder *runtime.Holder
|
||||||
handler *gacha.GachaHandler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGachaServiceServer(
|
func NewGachaServiceServer(
|
||||||
users store.UserRepository,
|
users store.UserRepository,
|
||||||
sessions store.SessionRepository,
|
sessions store.SessionRepository,
|
||||||
catalog []store.GachaCatalogEntry,
|
holder *runtime.Holder,
|
||||||
handler *gacha.GachaHandler,
|
|
||||||
) *GachaServiceServer {
|
) *GachaServiceServer {
|
||||||
return &GachaServiceServer{
|
return &GachaServiceServer{
|
||||||
users: users,
|
users: users,
|
||||||
sessions: sessions,
|
sessions: sessions,
|
||||||
catalog: catalog,
|
holder: holder,
|
||||||
handler: handler,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) {
|
func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) {
|
||||||
log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType)
|
log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType)
|
||||||
|
|
||||||
catalog := s.catalog
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.GachaEntries
|
||||||
|
handler := cat.GachaHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
user, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
user, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
user.EnsureMaps()
|
user.EnsureMaps()
|
||||||
s.autoConvertExpiredMedals(user, catalog, nowMillis)
|
autoConvertExpiredMedals(user, catalog, handler, nowMillis)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("update user: %w", err)
|
return nil, fmt.Errorf("update user: %w", err)
|
||||||
@@ -55,6 +55,9 @@ func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaL
|
|||||||
|
|
||||||
gachaList := make([]*pb.Gacha, 0, len(catalog))
|
gachaList := make([]*pb.Gacha, 0, len(catalog))
|
||||||
for _, entry := range catalog {
|
for _, entry := range catalog {
|
||||||
|
if !gachaActiveAt(entry, nowMillis) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !matchesGachaLabel(req.GachaLabelType, entry.GachaLabelType) {
|
if !matchesGachaLabel(req.GachaLabelType, entry.GachaLabelType) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -71,7 +74,7 @@ func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaL
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, catalog []store.GachaCatalogEntry, nowMillis int64) {
|
func autoConvertExpiredMedals(user *store.UserState, catalog []store.GachaCatalogEntry, handler *gacha.GachaHandler, nowMillis int64) {
|
||||||
for _, entry := range catalog {
|
for _, entry := range catalog {
|
||||||
if entry.GachaMedalId == 0 || entry.EndDatetime == 0 {
|
if entry.GachaMedalId == 0 || entry.EndDatetime == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -84,7 +87,7 @@ func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, cat
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
medalInfo, ok := s.handler.MedalInfo[entry.GachaId]
|
medalInfo, ok := handler.MedalInfo[entry.GachaId]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -117,7 +120,8 @@ func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, cat
|
|||||||
func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) {
|
func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) {
|
||||||
log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId)
|
log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId)
|
||||||
|
|
||||||
catalog := s.catalog
|
catalog := s.holder.Get().GachaEntries
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
user, err := s.users.LoadUser(userId)
|
user, err := s.users.LoadUser(userId)
|
||||||
@@ -128,11 +132,15 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque
|
|||||||
byId := make(map[int32]*pb.Gacha, len(req.GachaId))
|
byId := make(map[int32]*pb.Gacha, len(req.GachaId))
|
||||||
for _, wantedId := range req.GachaId {
|
for _, wantedId := range req.GachaId {
|
||||||
for _, entry := range catalog {
|
for _, entry := range catalog {
|
||||||
if entry.GachaId == wantedId {
|
if entry.GachaId != wantedId {
|
||||||
bs := user.Gacha.BannerStates[entry.GachaId]
|
continue
|
||||||
byId[wantedId] = toProtoGacha(entry, &bs)
|
}
|
||||||
|
if !gachaActiveAt(entry, nowMillis) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
bs := user.Gacha.BannerStates[entry.GachaId]
|
||||||
|
byId[wantedId] = toProtoGacha(entry, &bs)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,10 +152,12 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque
|
|||||||
func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) {
|
func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) {
|
||||||
log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount)
|
log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount)
|
||||||
|
|
||||||
entry := findCatalogEntry(s.catalog, req.GachaId)
|
cat := s.holder.Get()
|
||||||
|
entry := findCatalogEntry(cat.GachaEntries, req.GachaId)
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
|
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
|
||||||
}
|
}
|
||||||
|
handler := cat.GachaHandler
|
||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
execCount := req.ExecCount
|
execCount := req.ExecCount
|
||||||
@@ -156,9 +166,17 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
|
|||||||
}
|
}
|
||||||
|
|
||||||
var drawResult *gacha.DrawResult
|
var drawResult *gacha.DrawResult
|
||||||
|
ownedCostumes := map[int32]bool{}
|
||||||
|
ownedWeapons := map[int32]bool{}
|
||||||
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
for _, c := range user.Costumes {
|
||||||
|
ownedCostumes[c.CostumeId] = true
|
||||||
|
}
|
||||||
|
for _, w := range user.Weapons {
|
||||||
|
ownedWeapons[w.WeaponId] = true
|
||||||
|
}
|
||||||
var drawErr error
|
var drawErr error
|
||||||
drawResult, drawErr = s.handler.HandleDraw(user, *entry, req.GachaPricePhaseId, execCount)
|
drawResult, drawErr = handler.HandleDraw(user, *entry, req.GachaPricePhaseId, execCount)
|
||||||
if drawErr != nil {
|
if drawErr != nil {
|
||||||
log.Printf("[GachaService] Draw error: %v", drawErr)
|
log.Printf("[GachaService] Draw error: %v", drawErr)
|
||||||
drawResult = &gacha.DrawResult{}
|
drawResult = &gacha.DrawResult{}
|
||||||
@@ -193,15 +211,6 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
|
|||||||
weaponPT := int32(model.PossessionTypeWeapon)
|
weaponPT := int32(model.PossessionTypeWeapon)
|
||||||
isMaterialDraw := model.IsMaterialBanner(entry.GachaLabelType)
|
isMaterialDraw := model.IsMaterialBanner(entry.GachaLabelType)
|
||||||
|
|
||||||
ownedCostumes := make(map[int32]bool, len(updatedUser.Costumes))
|
|
||||||
for _, c := range updatedUser.Costumes {
|
|
||||||
ownedCostumes[c.CostumeId] = true
|
|
||||||
}
|
|
||||||
ownedWeapons := make(map[int32]bool, len(updatedUser.Weapons))
|
|
||||||
for _, w := range updatedUser.Weapons {
|
|
||||||
ownedWeapons[w.WeaponId] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, item := range drawResult.Items {
|
for i, item := range drawResult.Items {
|
||||||
isNew := !isOwnedByType(item, ownedCostumes, ownedWeapons, updatedUser)
|
isNew := !isOwnedByType(item, ownedCostumes, ownedWeapons, updatedUser)
|
||||||
|
|
||||||
@@ -285,14 +294,16 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
|
|||||||
func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) {
|
func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) {
|
||||||
log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId)
|
log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId)
|
||||||
|
|
||||||
entry := findCatalogEntry(s.catalog, req.GachaId)
|
cat := s.holder.Get()
|
||||||
|
entry := findCatalogEntry(cat.GachaEntries, req.GachaId)
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
|
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
|
||||||
}
|
}
|
||||||
|
handler := cat.GachaHandler
|
||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
if resetErr := s.handler.HandleResetBox(user, *entry); resetErr != nil {
|
if resetErr := handler.HandleResetBox(user, *entry); resetErr != nil {
|
||||||
log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr)
|
log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -315,7 +326,7 @@ func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Em
|
|||||||
return nil, fmt.Errorf("snapshot user: %w", err)
|
return nil, fmt.Errorf("snapshot user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxCount := s.handler.Config.RewardGachaDailyMaxCount
|
maxCount := s.holder.Get().GachaHandler.Config.RewardGachaDailyMaxCount
|
||||||
if maxCount <= 0 {
|
if maxCount <= 0 {
|
||||||
maxCount = model.DefaultDailyDrawLimit
|
maxCount = model.DefaultDailyDrawLimit
|
||||||
}
|
}
|
||||||
@@ -337,11 +348,20 @@ func (s *GachaServiceServer) RewardDraw(ctx context.Context, req *pb.RewardDrawR
|
|||||||
log.Printf("[GachaService] RewardDraw: placement=%q reward=%q amount=%q", req.PlacementName, req.RewardName, req.RewardAmount)
|
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)
|
||||||
|
handler := s.holder.Get().GachaHandler
|
||||||
|
|
||||||
var items []gacha.DrawnItem
|
var items []gacha.DrawnItem
|
||||||
|
ownedCostumes := map[int32]bool{}
|
||||||
|
ownedWeapons := map[int32]bool{}
|
||||||
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
for _, c := range user.Costumes {
|
||||||
|
ownedCostumes[c.CostumeId] = true
|
||||||
|
}
|
||||||
|
for _, w := range user.Weapons {
|
||||||
|
ownedWeapons[w.WeaponId] = true
|
||||||
|
}
|
||||||
var drawErr error
|
var drawErr error
|
||||||
items, drawErr = s.handler.HandleRewardDraw(user, 1)
|
items, drawErr = handler.HandleRewardDraw(user, 1)
|
||||||
if drawErr != nil {
|
if drawErr != nil {
|
||||||
log.Printf("[GachaService] RewardDraw error: %v", drawErr)
|
log.Printf("[GachaService] RewardDraw error: %v", drawErr)
|
||||||
}
|
}
|
||||||
@@ -350,15 +370,6 @@ func (s *GachaServiceServer) RewardDraw(ctx context.Context, req *pb.RewardDrawR
|
|||||||
return nil, fmt.Errorf("update user: %w", err)
|
return nil, fmt.Errorf("update user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ownedCostumes := make(map[int32]bool, len(updatedUser.Costumes))
|
|
||||||
for _, c := range updatedUser.Costumes {
|
|
||||||
ownedCostumes[c.CostumeId] = true
|
|
||||||
}
|
|
||||||
ownedWeapons := make(map[int32]bool, len(updatedUser.Weapons))
|
|
||||||
for _, w := range updatedUser.Weapons {
|
|
||||||
ownedWeapons[w.WeaponId] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]*pb.RewardGachaItem, 0, len(items))
|
results := make([]*pb.RewardGachaItem, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
results = append(results, &pb.RewardGachaItem{
|
results = append(results, &pb.RewardGachaItem{
|
||||||
@@ -395,6 +406,16 @@ func matchesGachaLabel(labels []int32, label int32) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gachaActiveAt(entry store.GachaCatalogEntry, nowMillis int64) bool {
|
||||||
|
if entry.StartDatetime != 0 && nowMillis < entry.StartDatetime {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if entry.EndDatetime != 0 && nowMillis >= entry.EndDatetime {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func toProtoGacha(entry store.GachaCatalogEntry, bs *store.GachaBannerState) *pb.Gacha {
|
func toProtoGacha(entry store.GachaCatalogEntry, bs *store.GachaBannerState) *pb.Gacha {
|
||||||
g := &pb.Gacha{
|
g := &pb.Gacha{
|
||||||
GachaId: entry.GachaId,
|
GachaId: entry.GachaId,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -14,13 +16,13 @@ import (
|
|||||||
|
|
||||||
type GimmickServiceServer struct {
|
type GimmickServiceServer struct {
|
||||||
pb.UnimplementedGimmickServiceServer
|
pb.UnimplementedGimmickServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
gimmickCatalog *masterdata.GimmickCatalog
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, gimmickCatalog *masterdata.GimmickCatalog) *GimmickServiceServer {
|
func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *GimmickServiceServer {
|
||||||
return &GimmickServiceServer{users: users, sessions: sessions, gimmickCatalog: gimmickCatalog}
|
return &GimmickServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) {
|
func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) {
|
||||||
@@ -43,6 +45,10 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
|
|||||||
log.Printf("[GimmickService] UpdateGimmickProgress: scheduleId=%d sequenceId=%d gimmickId=%d ornamentIndex=%d progressValueBit=%d flowType=%d",
|
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)
|
req.GimmickSequenceScheduleId, req.GimmickSequenceId, req.GimmickId, req.GimmickOrnamentIndex, req.ProgressValueBit, req.FlowType)
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
cat := s.holder.Get()
|
||||||
|
|
||||||
|
var ornamentRewards []*pb.GimmickReward
|
||||||
|
var sequenceCleared bool
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
progressKey := store.GimmickKey{
|
progressKey := store.GimmickKey{
|
||||||
@@ -53,7 +59,6 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
|
|||||||
progress := user.Gimmick.Progress[progressKey]
|
progress := user.Gimmick.Progress[progressKey]
|
||||||
progress.Key = progressKey
|
progress.Key = progressKey
|
||||||
progress.StartDatetime = nowMillis
|
progress.StartDatetime = nowMillis
|
||||||
user.Gimmick.Progress[progressKey] = progress
|
|
||||||
|
|
||||||
ornamentKey := store.GimmickOrnamentKey{
|
ornamentKey := store.GimmickOrnamentKey{
|
||||||
GimmickSequenceScheduleId: req.GimmickSequenceScheduleId,
|
GimmickSequenceScheduleId: req.GimmickSequenceScheduleId,
|
||||||
@@ -66,28 +71,151 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
|
|||||||
ornament.ProgressValueBit = req.ProgressValueBit
|
ornament.ProgressValueBit = req.ProgressValueBit
|
||||||
ornament.BaseDatetime = nowMillis
|
ornament.BaseDatetime = nowMillis
|
||||||
user.Gimmick.OrnamentProgress[ornamentKey] = ornament
|
user.Gimmick.OrnamentProgress[ornamentKey] = ornament
|
||||||
|
|
||||||
|
// Per-type branches:
|
||||||
|
// * Report (type 9, "Hidden Stories") — mark gimmick + sequence
|
||||||
|
// cleared, grant SequenceRewards (ImportantItem type 3, library reads it).
|
||||||
|
// * MapOnlyCageTreasureHunt (type 7, "Hidden Black Birds") — same as Report
|
||||||
|
// but the per-tap reward also comes back from m_cage_ornament_reward via
|
||||||
|
// GimmickOrnamentViewId.
|
||||||
|
// * CageMemory (type 10, "Lost Archives") — resolve an ImportantItem
|
||||||
|
// (type 4) from the gimmick's monitor texture and grant it. IsGimmickCleared
|
||||||
|
// stays false (matches original userdata; only ornament progress flips).
|
||||||
|
// * CageTreasureHunt / CageIntervalDropItem* — stub per-tap material so
|
||||||
|
// the client's reward popup fires; real reward source still unmapped.
|
||||||
|
switch cat.Gimmick.GimmickType(req.GimmickId) {
|
||||||
|
case model.GimmickTypeReport:
|
||||||
|
progress.IsGimmickCleared = true
|
||||||
|
sequenceCleared = markSequenceClearedOnce(user, cat, req.GimmickSequenceScheduleId, req.GimmickSequenceId, nowMillis)
|
||||||
|
|
||||||
|
case model.GimmickTypeMapOnlyCageTreasureHunt:
|
||||||
|
r, ok := cat.Gimmick.HiddenBirdReward(req.GimmickId, req.GimmickOrnamentIndex)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[GimmickService] UpdateGimmickProgress: hidden-bird %d ornament %d has no reward mapping, skipping",
|
||||||
|
req.GimmickId, req.GimmickOrnamentIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionType(r.PossessionType), r.PossessionId, r.Count, nowMillis)
|
||||||
|
ornamentRewards = append(ornamentRewards, &pb.GimmickReward{
|
||||||
|
PossessionType: r.PossessionType,
|
||||||
|
PossessionId: r.PossessionId,
|
||||||
|
Count: r.Count,
|
||||||
|
})
|
||||||
|
progress.IsGimmickCleared = true
|
||||||
|
sequenceCleared = markSequenceClearedOnce(user, cat, req.GimmickSequenceScheduleId, req.GimmickSequenceId, nowMillis)
|
||||||
|
|
||||||
|
case model.GimmickTypeCageMemory:
|
||||||
|
itemId, ok := cat.Gimmick.CageMemoryImportantItem(req.GimmickId)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[GimmickService] UpdateGimmickProgress: cage memory %d has no important-item mapping, skipping grant",
|
||||||
|
req.GimmickId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, owned := user.ImportantItems[itemId]; owned {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionTypeImportantItem, itemId, 1, nowMillis)
|
||||||
|
ornamentRewards = append(ornamentRewards, &pb.GimmickReward{
|
||||||
|
PossessionType: int32(model.PossessionTypeImportantItem),
|
||||||
|
PossessionId: itemId,
|
||||||
|
Count: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
case model.GimmickTypeCageTreasureHunt,
|
||||||
|
model.GimmickTypeCageIntervalDropItem,
|
||||||
|
model.GimmickTypeMapOnlyCageIntervalDrop:
|
||||||
|
// Per-tap drops with no per-gimmick reward in master data:
|
||||||
|
// * type 1 — "Fickle Black Birds" in the cage
|
||||||
|
// * type 2 — "Lost Items" in the cage
|
||||||
|
// * type 8 — Lost Items (map variant)
|
||||||
|
// Stub: grant 1 of Material 100004 (the most-common reward across
|
||||||
|
// m_cage_ornament_reward — 15 occurrences — likely a low-tier shard) per
|
||||||
|
// tap so the client's reward-popup path fires and the player accumulates
|
||||||
|
// something. Replace once a real per-gimmick mapping surfaces.
|
||||||
|
const stubMaterialId = int32(100004)
|
||||||
|
const stubMaterialCount = int32(1)
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionTypeMaterial, stubMaterialId, stubMaterialCount, nowMillis)
|
||||||
|
ornamentRewards = append(ornamentRewards, &pb.GimmickReward{
|
||||||
|
PossessionType: int32(model.PossessionTypeMaterial),
|
||||||
|
PossessionId: stubMaterialId,
|
||||||
|
Count: stubMaterialCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
user.Gimmick.Progress[progressKey] = progress
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var clearReward []*pb.GimmickReward
|
||||||
|
if sequenceCleared {
|
||||||
|
for _, r := range cat.Gimmick.SequenceRewards(req.GimmickSequenceId) {
|
||||||
|
clearReward = append(clearReward, &pb.GimmickReward{
|
||||||
|
PossessionType: r.PossessionType,
|
||||||
|
PossessionId: r.PossessionId,
|
||||||
|
Count: r.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return &pb.UpdateGimmickProgressResponse{
|
return &pb.UpdateGimmickProgressResponse{
|
||||||
GimmickOrnamentReward: []*pb.GimmickReward{},
|
GimmickOrnamentReward: ornamentRewards,
|
||||||
IsSequenceCleared: false,
|
IsSequenceCleared: sequenceCleared,
|
||||||
GimmickSequenceClearReward: []*pb.GimmickReward{},
|
GimmickSequenceClearReward: clearReward,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func markSequenceClearedOnce(user *store.UserState, cat *runtime.Catalogs, scheduleId, sequenceId int32, nowMillis int64) bool {
|
||||||
|
seqKey := store.GimmickSequenceKey{
|
||||||
|
GimmickSequenceScheduleId: scheduleId,
|
||||||
|
GimmickSequenceId: sequenceId,
|
||||||
|
}
|
||||||
|
sequence := user.Gimmick.Sequences[seqKey]
|
||||||
|
sequence.Key = seqKey
|
||||||
|
defer func() { user.Gimmick.Sequences[seqKey] = sequence }()
|
||||||
|
|
||||||
|
if sequence.IsGimmickSequenceCleared {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sequence.IsGimmickSequenceCleared = true
|
||||||
|
sequence.ClearDatetime = nowMillis
|
||||||
|
for _, r := range cat.Gimmick.SequenceRewards(sequenceId) {
|
||||||
|
cat.QuestHandler.Granter.GrantFull(user, model.PossessionType(r.PossessionType), r.PossessionId, r.Count, nowMillis)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *emptypb.Empty) (*pb.InitSequenceScheduleResponse, error) {
|
func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *emptypb.Empty) (*pb.InitSequenceScheduleResponse, error) {
|
||||||
log.Printf("[GimmickService] InitSequenceSchedule")
|
log.Printf("[GimmickService] InitSequenceSchedule")
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
eligible := s.holder.Get().Gimmick.ActiveScheduleKeys(*user, now)
|
||||||
|
eligibleSet := make(map[store.GimmickSequenceKey]struct{}, len(eligible))
|
||||||
|
for _, key := range eligible {
|
||||||
|
eligibleSet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
pruned := 0
|
||||||
|
for key, entry := range user.Gimmick.Sequences {
|
||||||
|
if _, ok := eligibleSet[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.IsGimmickSequenceCleared {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(user.Gimmick.Sequences, key)
|
||||||
|
pruned++
|
||||||
|
}
|
||||||
|
|
||||||
added := 0
|
added := 0
|
||||||
for _, key := range s.gimmickCatalog.ActiveScheduleKeys(*user, now) {
|
for _, key := range eligible {
|
||||||
|
if len(user.Gimmick.Sequences) >= masterdata.MaxUserGimmickRows {
|
||||||
|
break
|
||||||
|
}
|
||||||
if _, exists := user.Gimmick.Sequences[key]; !exists {
|
if _, exists := user.Gimmick.Sequences[key]; !exists {
|
||||||
user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key}
|
user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key}
|
||||||
added++
|
added++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if added > 0 {
|
if pruned > 0 || added > 0 {
|
||||||
log.Printf("[GimmickService] InitSequenceSchedule: added %d sequences (total %d)", added, len(user.Gimmick.Sequences))
|
log.Printf("[GimmickService] InitSequenceSchedule: pruned %d stale, added %d sequences (total %d, eligible %d, cap %d)",
|
||||||
|
pruned, added, len(user.Gimmick.Sequences), len(eligible), masterdata.MaxUserGimmickRows)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return &pb.InitSequenceScheduleResponse{}, nil
|
return &pb.InitSequenceScheduleResponse{}, nil
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/gametime"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
|
"lunar-tear/server/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LabyrinthServiceServer struct {
|
||||||
|
pb.UnimplementedLabyrinthServiceServer
|
||||||
|
users store.UserRepository
|
||||||
|
sessions store.SessionRepository
|
||||||
|
holder *runtime.Holder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLabyrinthServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *LabyrinthServiceServer {
|
||||||
|
if holder == nil {
|
||||||
|
panic("runtime holder is required")
|
||||||
|
}
|
||||||
|
return &LabyrinthServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LabyrinthServiceServer) ReceiveStageAccumulationReward(ctx context.Context, req *pb.ReceiveStageAccumulationRewardRequest) (*pb.ReceiveStageAccumulationRewardResponse, error) {
|
||||||
|
log.Printf("[LabyrinthService] ReceiveStageAccumulationReward: chapter=%d stage=%d questMissionClearCount=%d",
|
||||||
|
req.EventQuestChapterId, req.StageOrder, req.QuestMissionClearCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
laby := cat.Labyrinth
|
||||||
|
granter := cat.QuestHandler.Granter
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
key := store.LabyrinthStageKey{
|
||||||
|
EventQuestChapterId: req.EventQuestChapterId,
|
||||||
|
StageOrder: req.StageOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
rec := user.LabyrinthStages[key]
|
||||||
|
old := rec.AccumulationRewardReceivedQuestMissionCount
|
||||||
|
|
||||||
|
items, highest := laby.CollectAccumulationRewards(req.EventQuestChapterId, req.StageOrder, old, req.QuestMissionClearCount)
|
||||||
|
if highest <= old {
|
||||||
|
log.Printf("[LabyrinthService] ReceiveStageAccumulationReward: nothing to grant for chapter=%d stage=%d (claimed=%d, target=%d)",
|
||||||
|
req.EventQuestChapterId, req.StageOrder, old, req.QuestMissionClearCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
granter.GrantFull(user, model.PossessionType(it.PossessionType), it.PossessionId, it.Count, nowMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec.EventQuestChapterId = req.EventQuestChapterId
|
||||||
|
rec.StageOrder = req.StageOrder
|
||||||
|
rec.AccumulationRewardReceivedQuestMissionCount = highest
|
||||||
|
rec.LatestVersion = nowMillis
|
||||||
|
user.LabyrinthStages[key] = rec
|
||||||
|
|
||||||
|
log.Printf("[LabyrinthService] ReceiveStageAccumulationReward: chapter=%d stage=%d granted %d item(s), claimed %d -> %d",
|
||||||
|
req.EventQuestChapterId, req.StageOrder, len(items), old, highest)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &pb.ReceiveStageAccumulationRewardResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LabyrinthServiceServer) ReceiveStageClearReward(ctx context.Context, req *pb.ReceiveStageClearRewardRequest) (*pb.ReceiveStageClearRewardResponse, error) {
|
||||||
|
log.Printf("[LabyrinthService] ReceiveStageClearReward: chapter=%d stage=%d",
|
||||||
|
req.EventQuestChapterId, req.StageOrder)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
laby := cat.Labyrinth
|
||||||
|
granter := cat.QuestHandler.Granter
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
key := store.LabyrinthStageKey{
|
||||||
|
EventQuestChapterId: req.EventQuestChapterId,
|
||||||
|
StageOrder: req.StageOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
rec := user.LabyrinthStages[key]
|
||||||
|
if rec.IsReceivedStageClearReward {
|
||||||
|
log.Printf("[LabyrinthService] ReceiveStageClearReward: already claimed chapter=%d stage=%d",
|
||||||
|
req.EventQuestChapterId, req.StageOrder)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := laby.StageClearReward(req.EventQuestChapterId, req.StageOrder)
|
||||||
|
for _, it := range items {
|
||||||
|
granter.GrantFull(user, model.PossessionType(it.PossessionType), it.PossessionId, it.Count, nowMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec.EventQuestChapterId = req.EventQuestChapterId
|
||||||
|
rec.StageOrder = req.StageOrder
|
||||||
|
rec.IsReceivedStageClearReward = true
|
||||||
|
rec.LatestVersion = nowMillis
|
||||||
|
user.LabyrinthStages[key] = rec
|
||||||
|
|
||||||
|
log.Printf("[LabyrinthService] ReceiveStageClearReward: chapter=%d stage=%d granted %d item(s)",
|
||||||
|
req.EventQuestChapterId, req.StageOrder, len(items))
|
||||||
|
})
|
||||||
|
|
||||||
|
return &pb.ReceiveStageClearRewardResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LabyrinthServiceServer) UpdateSeasonData(ctx context.Context, req *pb.UpdateSeasonDataRequest) (*pb.UpdateSeasonDataResponse, error) {
|
||||||
|
laby := s.holder.Get().Labyrinth
|
||||||
|
|
||||||
|
var seasonResult []*pb.LabyrinthSeasonResult
|
||||||
|
for _, m := range laby.SeasonMilestones(req.EventQuestChapterId) {
|
||||||
|
rewards := make([]*pb.LabyrinthReward, 0, len(m.Rewards))
|
||||||
|
for _, it := range m.Rewards {
|
||||||
|
rewards = append(rewards, &pb.LabyrinthReward{
|
||||||
|
PossessionType: it.PossessionType,
|
||||||
|
PossessionId: it.PossessionId,
|
||||||
|
Count: it.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
seasonResult = append(seasonResult, &pb.LabyrinthSeasonResult{
|
||||||
|
EventQuestChapterId: req.EventQuestChapterId,
|
||||||
|
HeadQuestId: m.HeadQuestId,
|
||||||
|
SeasonReward: rewards,
|
||||||
|
HeadStageOrder: m.HeadStageOrder,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[LabyrinthService] UpdateSeasonData: chapter=%d -> %d milestone(s)",
|
||||||
|
req.EventQuestChapterId, len(seasonResult))
|
||||||
|
return &pb.UpdateSeasonDataResponse{SeasonResult: seasonResult}, nil
|
||||||
|
}
|
||||||
@@ -45,14 +45,21 @@ type infoLoad struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
listBinCache = make(map[string]listBinIndex) // revision → index
|
listBinCache = make(map[string]listBinIndex) // revision/platform → index
|
||||||
listBinInflight = make(map[string]*listBinLoad)
|
listBinInflight = make(map[string]*listBinLoad)
|
||||||
listBinCacheMu sync.RWMutex
|
listBinCacheMu sync.RWMutex
|
||||||
infoCache = make(map[string]map[string]infoAlias) // revision → from-name → duplicate target
|
infoCache = make(map[string]map[string]infoAlias) // revision/platform → from-name → duplicate target
|
||||||
infoInflight = make(map[string]*infoLoad)
|
infoInflight = make(map[string]*infoLoad)
|
||||||
infoCacheMu sync.RWMutex
|
infoCacheMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func cacheKey(revision, platform string) string {
|
||||||
|
if platform == "" {
|
||||||
|
return revision + "/_shared"
|
||||||
|
}
|
||||||
|
return revision + "/" + platform
|
||||||
|
}
|
||||||
|
|
||||||
// infoJSONEntry is one entry from assets/revisions/{rev}/info.json (duplicate files: serve to-name when asked for from-name).
|
// infoJSONEntry is one entry from assets/revisions/{rev}/info.json (duplicate files: serve to-name when asked for from-name).
|
||||||
type infoJSONEntry struct {
|
type infoJSONEntry struct {
|
||||||
FromName string `json:"from-name"`
|
FromName string `json:"from-name"`
|
||||||
@@ -208,33 +215,34 @@ func parseListBin(data []byte) listBinIndex {
|
|||||||
return idx
|
return idx
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadListBinIndex(baseDir, revision string) (listBinIndex, bool) {
|
func loadListBinIndex(baseDir, revision, platform string) (listBinIndex, bool) {
|
||||||
|
key := cacheKey(revision, platform)
|
||||||
listBinCacheMu.RLock()
|
listBinCacheMu.RLock()
|
||||||
idx, ok := listBinCache[revision]
|
idx, ok := listBinCache[key]
|
||||||
listBinCacheMu.RUnlock()
|
listBinCacheMu.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
return idx, true
|
return idx, true
|
||||||
}
|
}
|
||||||
|
|
||||||
listBinCacheMu.Lock()
|
listBinCacheMu.Lock()
|
||||||
if idx, ok := listBinCache[revision]; ok {
|
if idx, ok := listBinCache[key]; ok {
|
||||||
listBinCacheMu.Unlock()
|
listBinCacheMu.Unlock()
|
||||||
return idx, true
|
return idx, true
|
||||||
}
|
}
|
||||||
if load := listBinInflight[revision]; load != nil {
|
if load := listBinInflight[key]; load != nil {
|
||||||
listBinCacheMu.Unlock()
|
listBinCacheMu.Unlock()
|
||||||
<-load.done
|
<-load.done
|
||||||
return load.idx, load.ok
|
return load.idx, load.ok
|
||||||
}
|
}
|
||||||
load := &listBinLoad{done: make(chan struct{})}
|
load := &listBinLoad{done: make(chan struct{})}
|
||||||
listBinInflight[revision] = load
|
listBinInflight[key] = load
|
||||||
listBinCacheMu.Unlock()
|
listBinCacheMu.Unlock()
|
||||||
|
|
||||||
filePath := filepath.Join(baseDir, "assets", "revisions", revision, "list.bin")
|
filePath := filepath.Join(baseDir, "assets", "revisions", revision, platform, "list.bin")
|
||||||
data, err := os.ReadFile(filePath)
|
data, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
listBinCacheMu.Lock()
|
listBinCacheMu.Lock()
|
||||||
delete(listBinInflight, revision)
|
delete(listBinInflight, key)
|
||||||
close(load.done)
|
close(load.done)
|
||||||
listBinCacheMu.Unlock()
|
listBinCacheMu.Unlock()
|
||||||
return nil, false
|
return nil, false
|
||||||
@@ -243,41 +251,42 @@ func loadListBinIndex(baseDir, revision string) (listBinIndex, bool) {
|
|||||||
load.idx = idx
|
load.idx = idx
|
||||||
load.ok = true
|
load.ok = true
|
||||||
listBinCacheMu.Lock()
|
listBinCacheMu.Lock()
|
||||||
listBinCache[revision] = idx
|
listBinCache[key] = idx
|
||||||
delete(listBinInflight, revision)
|
delete(listBinInflight, key)
|
||||||
close(load.done)
|
close(load.done)
|
||||||
listBinCacheMu.Unlock()
|
listBinCacheMu.Unlock()
|
||||||
return idx, true
|
return idx, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadInfoIndex(baseDir, revision string) map[string]infoAlias {
|
func loadInfoIndex(baseDir, revision, platform string) map[string]infoAlias {
|
||||||
|
key := cacheKey(revision, platform)
|
||||||
infoCacheMu.RLock()
|
infoCacheMu.RLock()
|
||||||
m, ok := infoCache[revision]
|
m, ok := infoCache[key]
|
||||||
infoCacheMu.RUnlock()
|
infoCacheMu.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
infoCacheMu.Lock()
|
infoCacheMu.Lock()
|
||||||
if m, ok := infoCache[revision]; ok {
|
if m, ok := infoCache[key]; ok {
|
||||||
infoCacheMu.Unlock()
|
infoCacheMu.Unlock()
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
if load := infoInflight[revision]; load != nil {
|
if load := infoInflight[key]; load != nil {
|
||||||
infoCacheMu.Unlock()
|
infoCacheMu.Unlock()
|
||||||
<-load.done
|
<-load.done
|
||||||
return load.m
|
return load.m
|
||||||
}
|
}
|
||||||
load := &infoLoad{done: make(chan struct{})}
|
load := &infoLoad{done: make(chan struct{})}
|
||||||
infoInflight[revision] = load
|
infoInflight[key] = load
|
||||||
infoCacheMu.Unlock()
|
infoCacheMu.Unlock()
|
||||||
|
|
||||||
filePath := filepath.Join(baseDir, "assets", "revisions", revision, "info.json")
|
filePath := filepath.Join(baseDir, "assets", "revisions", revision, platform, "info.json")
|
||||||
data, err := os.ReadFile(filePath)
|
data, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
infoCacheMu.Lock()
|
infoCacheMu.Lock()
|
||||||
infoCache[revision] = nil
|
infoCache[key] = nil
|
||||||
delete(infoInflight, revision)
|
delete(infoInflight, key)
|
||||||
close(load.done)
|
close(load.done)
|
||||||
infoCacheMu.Unlock()
|
infoCacheMu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
@@ -285,8 +294,8 @@ func loadInfoIndex(baseDir, revision string) map[string]infoAlias {
|
|||||||
var entries []infoJSONEntry
|
var entries []infoJSONEntry
|
||||||
if err := json.Unmarshal(data, &entries); err != nil {
|
if err := json.Unmarshal(data, &entries); err != nil {
|
||||||
infoCacheMu.Lock()
|
infoCacheMu.Lock()
|
||||||
infoCache[revision] = nil
|
infoCache[key] = nil
|
||||||
delete(infoInflight, revision)
|
delete(infoInflight, key)
|
||||||
close(load.done)
|
close(load.done)
|
||||||
infoCacheMu.Unlock()
|
infoCacheMu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
@@ -307,8 +316,8 @@ func loadInfoIndex(baseDir, revision string) map[string]infoAlias {
|
|||||||
}
|
}
|
||||||
load.m = m
|
load.m = m
|
||||||
infoCacheMu.Lock()
|
infoCacheMu.Lock()
|
||||||
infoCache[revision] = m
|
infoCache[key] = m
|
||||||
delete(infoInflight, revision)
|
delete(infoInflight, key)
|
||||||
close(load.done)
|
close(load.done)
|
||||||
infoCacheMu.Unlock()
|
infoCacheMu.Unlock()
|
||||||
return m
|
return m
|
||||||
@@ -378,7 +387,7 @@ func hasNonASCII(s string) bool {
|
|||||||
// an en locale fallback is appended (marked IsLocaleFallback so callers can skip MD5 validation).
|
// 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
|
// For paths with non-ASCII characters, mojibake (double-encoded) and fullwidth-to-ASCII
|
||||||
// variants are also tried.
|
// variants are also tried.
|
||||||
func pathStrToFullPaths(baseDir, revision, assetType, pathStr string) []pathCandidate {
|
func pathStrToFullPaths(baseDir, revision, platform, assetType, pathStr string) []pathCandidate {
|
||||||
fsPath := strings.ReplaceAll(pathStr, ")", "/")
|
fsPath := strings.ReplaceAll(pathStr, ")", "/")
|
||||||
if strings.Contains(fsPath, "..") || filepath.IsAbs(fsPath) || strings.HasPrefix(fsPath, "/") {
|
if strings.Contains(fsPath, "..") || filepath.IsAbs(fsPath) || strings.HasPrefix(fsPath, "/") {
|
||||||
return nil
|
return nil
|
||||||
@@ -402,7 +411,7 @@ func pathStrToFullPaths(baseDir, revision, assetType, pathStr string) []pathCand
|
|||||||
if strings.Contains(pathStr, ")ko)") {
|
if strings.Contains(pathStr, ")ko)") {
|
||||||
entries = append(entries, tagged{strings.ReplaceAll(pathStr, ")ko)", ")en)"), true})
|
entries = append(entries, tagged{strings.ReplaceAll(pathStr, ")ko)", ")en)"), true})
|
||||||
}
|
}
|
||||||
base := filepath.Join(baseDir, "assets", "revisions", revision)
|
base := filepath.Join(baseDir, "assets", "revisions", revision, platform)
|
||||||
var out []pathCandidate
|
var out []pathCandidate
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
@@ -434,64 +443,86 @@ func appendUniqueCandidate(candidates []assetCandidate, seen map[string]bool, ca
|
|||||||
return append(candidates, candidate)
|
return append(candidates, candidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateCandidatePath(baseDir string, candidate assetCandidate, assetType, targetRevision, targetBaseName string) string {
|
func duplicateCandidatePath(baseDir string, candidate assetCandidate, platform, assetType, targetRevision, targetBaseName string) string {
|
||||||
root := filepath.Join(baseDir, "assets", "revisions", candidate.Revision, assetType)
|
root := filepath.Join(baseDir, "assets", "revisions", candidate.Revision, platform, assetType)
|
||||||
rel, err := filepath.Rel(root, candidate.Path)
|
rel, err := filepath.Rel(root, candidate.Path)
|
||||||
if err != nil || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
if err != nil || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return filepath.Join(baseDir, "assets", "revisions", targetRevision, assetType, filepath.Dir(rel), targetBaseName)
|
return filepath.Join(baseDir, "assets", "revisions", targetRevision, platform, assetType, filepath.Dir(rel), targetBaseName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// objectIdToFilePathCandidates returns candidate file paths for the object: list.bin path, locale fallbacks
|
// objectIdToFilePathCandidates returns candidate file paths for the object: list.bin path, locale fallbacks
|
||||||
// (ja/ko -> en), then info.json duplicate mappings (from-name -> to-name, possibly in a different revision).
|
// (ja/ko -> en), then info.json duplicate mappings (from-name -> to-name, possibly in a different revision).
|
||||||
// The original locale path is tried first (with MD5 validation); locale fallbacks are tried after
|
// 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).
|
// (without MD5 validation, since the hash in list.bin refers to the original locale's content).
|
||||||
|
//
|
||||||
|
// Two tiers are searched in order: the requested platform tree (e.g. revisions/0/ios/...) and then
|
||||||
|
// the un-split shared tree (revisions/0/...) which acts as a fallback for operators who deploy a
|
||||||
|
// single unified asset dump. Each tier carries its own list.bin md5 so corruption is still detected.
|
||||||
// Callers should try each path until one exists on disk.
|
// Callers should try each path until one exists on disk.
|
||||||
func objectIdToFilePathCandidates(baseDir, revision, assetType, objectId string) (candidates []assetCandidate, size int64, ok bool) {
|
func objectIdToFilePathCandidates(baseDir, revision, platform, assetType, objectId string) (candidates []assetCandidate, size int64, ok bool) {
|
||||||
idx, ok := loadListBinIndex(baseDir, revision)
|
|
||||||
if !ok || idx == nil {
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
entry, ok := idx[objectId]
|
|
||||||
if !ok || entry.Path == "" {
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
paths := pathStrToFullPaths(baseDir, revision, assetType, entry.Path)
|
|
||||||
if len(paths) == 0 {
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, pc := range paths {
|
var firstSize int64
|
||||||
md5 := entry.MD5
|
var anyHit bool
|
||||||
if pc.IsLocaleFallback {
|
|
||||||
md5 = ""
|
appendForPlatform := func(p, label string) {
|
||||||
|
idx, idxOk := loadListBinIndex(baseDir, revision, p)
|
||||||
|
if !idxOk || idx == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
candidates = appendUniqueCandidate(candidates, seen, assetCandidate{
|
entry, entryOk := idx[objectId]
|
||||||
Path: pc.Path,
|
if !entryOk || entry.Path == "" {
|
||||||
Revision: revision,
|
return
|
||||||
Source: "list.bin",
|
}
|
||||||
ExpectedMD5: md5,
|
paths := pathStrToFullPaths(baseDir, revision, p, assetType, entry.Path)
|
||||||
})
|
if len(paths) == 0 {
|
||||||
}
|
return
|
||||||
infoIndex := loadInfoIndex(baseDir, revision)
|
}
|
||||||
if len(infoIndex) > 0 {
|
tierStart := len(candidates)
|
||||||
for _, c := range candidates {
|
for _, pc := range paths {
|
||||||
alias, ok := infoIndex[filepath.Base(c.Path)]
|
md5 := entry.MD5
|
||||||
if !ok || alias.ToName == "" {
|
if pc.IsLocaleFallback {
|
||||||
continue
|
md5 = ""
|
||||||
}
|
|
||||||
alt := duplicateCandidatePath(baseDir, c, assetType, alias.ToRevision, alias.ToName)
|
|
||||||
if alt == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
candidates = appendUniqueCandidate(candidates, seen, assetCandidate{
|
candidates = appendUniqueCandidate(candidates, seen, assetCandidate{
|
||||||
Path: alt,
|
Path: pc.Path,
|
||||||
Revision: alias.ToRevision,
|
Revision: revision,
|
||||||
Source: "info.json redirect",
|
Source: "list.bin (" + label + ")",
|
||||||
ExpectedMD5: alias.MD5,
|
ExpectedMD5: md5,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
infoIndex := loadInfoIndex(baseDir, revision, p)
|
||||||
|
if len(infoIndex) > 0 {
|
||||||
|
tierCandidates := candidates[tierStart:]
|
||||||
|
for _, c := range tierCandidates {
|
||||||
|
alias, aliasOk := infoIndex[filepath.Base(c.Path)]
|
||||||
|
if !aliasOk || alias.ToName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alt := duplicateCandidatePath(baseDir, c, p, assetType, alias.ToRevision, alias.ToName)
|
||||||
|
if alt == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidates = appendUniqueCandidate(candidates, seen, assetCandidate{
|
||||||
|
Path: alt,
|
||||||
|
Revision: alias.ToRevision,
|
||||||
|
Source: "info.json redirect (" + label + ")",
|
||||||
|
ExpectedMD5: alias.MD5,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !anyHit {
|
||||||
|
firstSize = entry.Size
|
||||||
|
anyHit = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return candidates, entry.Size, true
|
|
||||||
|
appendForPlatform(platform, platform)
|
||||||
|
appendForPlatform("", "shared")
|
||||||
|
|
||||||
|
if !anyHit {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
return candidates, firstSize, true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -19,35 +23,37 @@ type LoginBonusServiceServer struct {
|
|||||||
pb.UnimplementedLoginBonusServiceServer
|
pb.UnimplementedLoginBonusServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.LoginBonusCatalog
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.LoginBonusCatalog) *LoginBonusServiceServer {
|
func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *LoginBonusServiceServer {
|
||||||
return &LoginBonusServiceServer{users: users, sessions: sessions, catalog: catalog}
|
return &LoginBonusServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) {
|
func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) {
|
||||||
log.Printf("[LoginBonusService] ReceiveStamp")
|
log.Printf("[LoginBonusService] ReceiveStamp")
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
catalog := s.holder.Get().LoginBonus
|
||||||
|
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
user, err := s.users.LoadUser(userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage, nextStamp, reward, err := resolveNextStamp(catalog, user.LoginBonus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[LoginBonusService] bonusId=%d page %d->%d stamp %d->%d possType=%d possId=%d count=%d (-> gift box)",
|
||||||
|
user.LoginBonus.LoginBonusId,
|
||||||
|
user.LoginBonus.CurrentPageNumber, nextPage,
|
||||||
|
user.LoginBonus.CurrentStampNumber, nextStamp,
|
||||||
|
reward.PossessionType, reward.PossessionId, reward.Count)
|
||||||
|
|
||||||
|
s.users.UpdateUser(userId, func(u *store.UserState) {
|
||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
nextStamp := user.LoginBonus.CurrentStampNumber + 1
|
u.Gifts.NotReceived = append(u.Gifts.NotReceived, store.NotReceivedGiftState{
|
||||||
|
|
||||||
reward, ok := s.catalog.LookupStampReward(
|
|
||||||
user.LoginBonus.LoginBonusId,
|
|
||||||
user.LoginBonus.CurrentPageNumber,
|
|
||||||
nextStamp,
|
|
||||||
)
|
|
||||||
if !ok {
|
|
||||||
log.Fatalf("[LoginBonusService] no reward found for bonusId=%d page=%d stamp=%d",
|
|
||||||
user.LoginBonus.LoginBonusId, user.LoginBonus.CurrentPageNumber, nextStamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[LoginBonusService] stamp %d -> possType=%d possId=%d count=%d (-> gift box)",
|
|
||||||
nextStamp, reward.PossessionType, reward.PossessionId, reward.Count)
|
|
||||||
|
|
||||||
user.Gifts.NotReceived = append(user.Gifts.NotReceived, store.NotReceivedGiftState{
|
|
||||||
GiftCommon: store.GiftCommonState{
|
GiftCommon: store.GiftCommonState{
|
||||||
PossessionType: reward.PossessionType,
|
PossessionType: reward.PossessionType,
|
||||||
PossessionId: reward.PossessionId,
|
PossessionId: reward.PossessionId,
|
||||||
@@ -57,11 +63,42 @@ func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb
|
|||||||
ExpirationDatetime: now + int64(30*24*time.Hour/time.Millisecond),
|
ExpirationDatetime: now + int64(30*24*time.Hour/time.Millisecond),
|
||||||
UserGiftUuid: uuid.New().String(),
|
UserGiftUuid: uuid.New().String(),
|
||||||
})
|
})
|
||||||
user.Notifications.GiftNotReceiveCount = int32(len(user.Gifts.NotReceived))
|
u.Notifications.GiftNotReceiveCount = int32(len(u.Gifts.NotReceived))
|
||||||
user.LoginBonus.CurrentStampNumber = nextStamp
|
u.LoginBonus.CurrentPageNumber = nextPage
|
||||||
user.LoginBonus.LatestRewardReceiveDatetime = now
|
u.LoginBonus.CurrentStampNumber = nextStamp
|
||||||
user.LoginBonus.LatestVersion = now
|
u.LoginBonus.LatestRewardReceiveDatetime = now
|
||||||
|
u.LoginBonus.LatestVersion = now
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.ReceiveStampResponse{}, nil
|
return &pb.ReceiveStampResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveNextStamp(catalog *masterdata.LoginBonusCatalog, lb store.UserLoginBonusState) (nextPage, nextStamp int32, reward masterdata.LoginBonusReward, err error) {
|
||||||
|
bonusId := lb.LoginBonusId
|
||||||
|
curPage := lb.CurrentPageNumber
|
||||||
|
curStamp := lb.CurrentStampNumber
|
||||||
|
|
||||||
|
nextPage = curPage
|
||||||
|
nextStamp = curStamp + 1
|
||||||
|
var ok bool
|
||||||
|
reward, ok = catalog.LookupStampReward(bonusId, nextPage, nextStamp)
|
||||||
|
if !ok {
|
||||||
|
nextPage = curPage + 1
|
||||||
|
nextStamp = 1
|
||||||
|
total := catalog.TotalPageCount(bonusId)
|
||||||
|
if total > 0 && nextPage > total {
|
||||||
|
err = status.Errorf(codes.FailedPrecondition,
|
||||||
|
"login bonus %d exhausted (page %d stamp %d is the last)",
|
||||||
|
bonusId, curPage, curStamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reward, ok = catalog.LookupStampReward(bonusId, nextPage, nextStamp)
|
||||||
|
if !ok {
|
||||||
|
err = status.Errorf(codes.FailedPrecondition,
|
||||||
|
"no reward found for login bonus %d page %d stamp %d",
|
||||||
|
bonusId, nextPage, nextStamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,23 +15,25 @@ type MaterialServiceServer struct {
|
|||||||
pb.UnimplementedMaterialServiceServer
|
pb.UnimplementedMaterialServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.MaterialCatalog
|
holder *runtime.Holder
|
||||||
config *masterdata.GameConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.MaterialCatalog, config *masterdata.GameConfig) *MaterialServiceServer {
|
func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *MaterialServiceServer {
|
||||||
return &MaterialServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
return &MaterialServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) {
|
func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) {
|
||||||
log.Printf("[MaterialService] Sell: %d item(s)", len(req.MaterialPossession))
|
log.Printf("[MaterialService] Sell: %d item(s)", len(req.MaterialPossession))
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Material
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|
||||||
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
totalGold := int32(0)
|
totalGold := int32(0)
|
||||||
for _, item := range req.MaterialPossession {
|
for _, item := range req.MaterialPossession {
|
||||||
mat, ok := s.catalog.All[item.MaterialId]
|
mat, ok := catalog.All[item.MaterialId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[MaterialService] Sell: unknown materialId=%d, skipping", item.MaterialId)
|
log.Printf("[MaterialService] Sell: unknown materialId=%d, skipping", item.MaterialId)
|
||||||
continue
|
continue
|
||||||
@@ -50,10 +53,18 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe
|
|||||||
gold := mat.SellPrice * item.Count
|
gold := mat.SellPrice * item.Count
|
||||||
totalGold += gold
|
totalGold += gold
|
||||||
log.Printf("[MaterialService] Sell: materialId=%d x%d -> %d gold", item.MaterialId, item.Count, gold)
|
log.Printf("[MaterialService] Sell: materialId=%d x%d -> %d gold", item.MaterialId, item.Count, gold)
|
||||||
|
|
||||||
|
if mat.MaterialSaleObtainPossessionId != 0 {
|
||||||
|
for _, row := range catalog.SaleObtain[mat.MaterialSaleObtainPossessionId] {
|
||||||
|
grantCount := row.Count * item.Count
|
||||||
|
store.GrantPossession(user, model.PossessionType(row.PossessionType), row.PossessionId, grantCount)
|
||||||
|
log.Printf("[MaterialService] Sell: materialId=%d x%d -> SaleObtain type=%d id=%d +%d", item.MaterialId, item.Count, row.PossessionType, row.PossessionId, grantCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalGold > 0 {
|
if totalGold > 0 {
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
|
user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
|
||||||
log.Printf("[MaterialService] Sell: total gold +%d", totalGold)
|
log.Printf("[MaterialService] Sell: total gold +%d", totalGold)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,6 +44,30 @@ const informationPage = `<!DOCTYPE html>
|
|||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
|
const panelMissionPage = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Panel Missions</title>
|
||||||
|
<style>
|
||||||
|
body { margin:0; padding:48px 20px; font-family:"Noto Sans",sans-serif;
|
||||||
|
background:#0a0a0f; color:#d4cfc6; text-align:center; }
|
||||||
|
h1 { font-size:1.3em; letter-spacing:.15em; color:#e8e0d0; margin-bottom:6px; }
|
||||||
|
.sub { font-size:.75em; color:#888; margin-bottom:28px; }
|
||||||
|
.sep { width:60px; border:none; border-top:1px solid #333; margin:24px auto; }
|
||||||
|
p { font-size:.85em; line-height:1.6; color:#999; max-width:340px; margin:0 auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>PANEL MISSIONS</h1>
|
||||||
|
<div class="sub">Card Stories</div>
|
||||||
|
<hr class="sep">
|
||||||
|
<p>All panel missions are cleared.</p>
|
||||||
|
<p>Their Card Stories are available in Library › Extra Stories.</p>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
// resourcesURLOriginal is the base URL embedded in list.bin; must be replaced with same-length (43 bytes) when rewriting.
|
// resourcesURLOriginal is the base URL embedded in list.bin; must be replaced with same-length (43 bytes) when rewriting.
|
||||||
const resourcesURLOriginal = "https://resources.app.nierreincarnation.com"
|
const resourcesURLOriginal = "https://resources.app.nierreincarnation.com"
|
||||||
|
|
||||||
@@ -133,7 +157,9 @@ func NewOctoHTTPServer(resourcesBaseURL, baseDir string) *OctoHTTPServer {
|
|||||||
revisions: newRevisionTracker(),
|
revisions: newRevisionTracker(),
|
||||||
resolver: newAssetResolver(baseDir),
|
resolver: newAssetResolver(baseDir),
|
||||||
}
|
}
|
||||||
s.resolver.Prewarm("0")
|
s.resolver.Prewarm("0", platformAndroid)
|
||||||
|
s.resolver.Prewarm("0", platformIOS)
|
||||||
|
s.resolver.Prewarm("0", "")
|
||||||
s.mux.HandleFunc("/", s.handleAll)
|
s.mux.HandleFunc("/", s.handleAll)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@@ -142,12 +168,24 @@ func (s *OctoHTTPServer) Handler() http.Handler {
|
|||||||
return s.mux
|
return s.mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listBinPath prefers the platform-split list.bin and falls back to the un-split shared tree
|
||||||
|
// when the platform-specific file is missing, so operators with a single unified asset dump
|
||||||
|
// keep working.
|
||||||
|
func (s *OctoHTTPServer) listBinPath(revision, platform string) string {
|
||||||
|
p := filepath.Join(s.BaseDir, "assets", "revisions", revision, platform, "list.bin")
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return filepath.Join(s.BaseDir, "assets", "revisions", revision, "list.bin")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OctoHTTPServer) handleAll(w http.ResponseWriter, r *http.Request) {
|
func (s *OctoHTTPServer) handleAll(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
|
platform := platformFromUserAgent(r)
|
||||||
isAssetRequest := strings.Contains(path, "/unso-")
|
isAssetRequest := strings.Contains(path, "/unso-")
|
||||||
isMasterDataRequest := strings.Contains(path, "/assets/release/") && strings.Contains(path, "database.bin")
|
isMasterDataRequest := strings.Contains(path, "/assets/release/") && strings.Contains(path, "database.bin")
|
||||||
if !isAssetRequest && !isMasterDataRequest {
|
if !isAssetRequest && !isMasterDataRequest {
|
||||||
log.Printf("[HTTP] %s %s (Host: %s)", r.Method, r.URL.String(), r.Host)
|
log.Printf("[HTTP] %s %s (Host: %s, platform: %s)", r.Method, r.URL.String(), r.Host, platform)
|
||||||
for k, v := range r.Header {
|
for k, v := range r.Header {
|
||||||
log.Printf("[HTTP] %s: %s", k, v)
|
log.Printf("[HTTP] %s: %s", k, v)
|
||||||
}
|
}
|
||||||
@@ -155,13 +193,13 @@ func (s *OctoHTTPServer) handleAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Octo v2 API — asset bundle management
|
// Octo v2 API — asset bundle management
|
||||||
if strings.HasPrefix(path, "/v2/") {
|
if strings.HasPrefix(path, "/v2/") {
|
||||||
s.handleOctoV2(w, r, path)
|
s.handleOctoV2(w, r, path, platform)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Octo v1 list: /v1/list/{version}/{revision} — same list.bin as v2, keyed by revision
|
// Octo v1 list: /v1/list/{version}/{revision} — same list.bin as v2, keyed by revision
|
||||||
if strings.HasPrefix(path, "/v1/list/") {
|
if strings.HasPrefix(path, "/v1/list/") {
|
||||||
s.serveOctoV1List(w, r, path)
|
s.serveOctoV1List(w, r, path, platform)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +226,7 @@ func (s *OctoHTTPServer) handleAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Asset bundle requests (from list.bin URLs: .../unso-{v}-{type}/{o}?generation=...&alt=media)
|
// Asset bundle requests (from list.bin URLs: .../unso-{v}-{type}/{o}?generation=...&alt=media)
|
||||||
if strings.Contains(path, "/unso-") {
|
if strings.Contains(path, "/unso-") {
|
||||||
s.serveUnsoAsset(w, r, path)
|
s.serveUnsoAsset(w, r, path, platform)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,8 +256,8 @@ func (s *OctoHTTPServer) handleAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte{})
|
w.Write([]byte{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OctoHTTPServer) handleOctoV2(w http.ResponseWriter, r *http.Request, path string) {
|
func (s *OctoHTTPServer) handleOctoV2(w http.ResponseWriter, r *http.Request, path, platform string) {
|
||||||
log.Printf("[OctoV2] %s %s", r.Method, path)
|
log.Printf("[OctoV2] %s %s (platform=%s)", r.Method, path, platform)
|
||||||
|
|
||||||
// /v2/pub/a/{appId}/v/{version}/list/{offset} — resource listing
|
// /v2/pub/a/{appId}/v/{version}/list/{offset} — resource listing
|
||||||
if strings.Contains(path, "/list/") {
|
if strings.Contains(path, "/list/") {
|
||||||
@@ -228,13 +266,13 @@ func (s *OctoHTTPServer) handleOctoV2(w http.ResponseWriter, r *http.Request, pa
|
|||||||
requestedRevision := parts[len(parts)-1]
|
requestedRevision := parts[len(parts)-1]
|
||||||
if requestedRevision != "" {
|
if requestedRevision != "" {
|
||||||
revision := "0"
|
revision := "0"
|
||||||
filePath := filepath.Join(s.BaseDir, "assets", "revisions", "0", "list.bin")
|
filePath := s.listBinPath(revision, platform)
|
||||||
if requestedRevision != revision {
|
if requestedRevision != revision {
|
||||||
log.Printf("[OctoV2] Resource list request revision=%s canonicalized to revision=%s", requestedRevision, revision)
|
log.Printf("[OctoV2] Resource list request revision=%s canonicalized to revision=%s", requestedRevision, revision)
|
||||||
}
|
}
|
||||||
log.Printf("[OctoV2] Resource list request — serving %s (requested_revision=%s canonical_revision=%s)", filePath, requestedRevision, revision)
|
log.Printf("[OctoV2] Resource list request — serving %s (requested_revision=%s canonical_revision=%s platform=%s)", filePath, requestedRevision, revision, platform)
|
||||||
s.revisions.Remember(r.RemoteAddr, revision)
|
s.revisions.Remember(r.RemoteAddr, revision)
|
||||||
go s.resolver.Prewarm(revision)
|
go s.resolver.Prewarm(revision, platform)
|
||||||
s.serveListBin(w, filePath)
|
s.serveListBin(w, filePath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -259,8 +297,8 @@ func (s *OctoHTTPServer) handleOctoV2(w http.ResponseWriter, r *http.Request, pa
|
|||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveOctoV1List handles GET /v1/list/{version}/{revision} — serves assets/revisions/{revision}/list.bin.
|
// serveOctoV1List handles GET /v1/list/{version}/{revision} — serves assets/revisions/{revision}/{platform}/list.bin.
|
||||||
func (s *OctoHTTPServer) serveOctoV1List(w http.ResponseWriter, r *http.Request, path string) {
|
func (s *OctoHTTPServer) serveOctoV1List(w http.ResponseWriter, r *http.Request, path, platform string) {
|
||||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
// ["v1", "list", "300116832", "0"] -> revision = last segment
|
// ["v1", "list", "300116832", "0"] -> revision = last segment
|
||||||
requestedRevision := "0"
|
requestedRevision := "0"
|
||||||
@@ -268,18 +306,18 @@ func (s *OctoHTTPServer) serveOctoV1List(w http.ResponseWriter, r *http.Request,
|
|||||||
requestedRevision = parts[len(parts)-1]
|
requestedRevision = parts[len(parts)-1]
|
||||||
}
|
}
|
||||||
revision := "0"
|
revision := "0"
|
||||||
filePath := filepath.Join(s.BaseDir, "assets", "revisions", "0", "list.bin")
|
filePath := s.listBinPath(revision, platform)
|
||||||
if requestedRevision != revision {
|
if requestedRevision != revision {
|
||||||
log.Printf("[OctoV1] list request revision=%s canonicalized to revision=%s", requestedRevision, revision)
|
log.Printf("[OctoV1] list request revision=%s canonicalized to revision=%s", requestedRevision, revision)
|
||||||
}
|
}
|
||||||
log.Printf("[OctoV1] %s %s — serving %s (requested_revision=%s canonical_revision=%s)", r.Method, path, filePath, requestedRevision, revision)
|
log.Printf("[OctoV1] %s %s — serving %s (requested_revision=%s canonical_revision=%s platform=%s)", r.Method, path, filePath, requestedRevision, revision, platform)
|
||||||
s.revisions.Remember(r.RemoteAddr, revision)
|
s.revisions.Remember(r.RemoteAddr, revision)
|
||||||
go s.resolver.Prewarm(revision)
|
go s.resolver.Prewarm(revision, platform)
|
||||||
s.serveListBin(w, filePath)
|
s.serveListBin(w, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveUnsoAsset serves asset bundle or resource for URLs like /resource-bundle-server/unso-{version}-{type}/{object_id}.
|
// serveUnsoAsset serves asset bundle or resource for URLs like /resource-bundle-server/unso-{version}-{type}/{object_id}.
|
||||||
func (s *OctoHTTPServer) serveUnsoAsset(w http.ResponseWriter, r *http.Request, path string) {
|
func (s *OctoHTTPServer) serveUnsoAsset(w http.ResponseWriter, r *http.Request, path, platform string) {
|
||||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
var segment, objectId string
|
var segment, objectId string
|
||||||
for i, p := range parts {
|
for i, p := range parts {
|
||||||
@@ -311,9 +349,9 @@ func (s *OctoHTTPServer) serveUnsoAsset(w http.ResponseWriter, r *http.Request,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
activeRevision := s.revisions.Active(r.RemoteAddr)
|
activeRevision := s.revisions.Active(r.RemoteAddr)
|
||||||
resolution, ok := s.resolver.Resolve(objectId, assetType, activeRevision)
|
resolution, ok := s.resolver.Resolve(objectId, assetType, activeRevision, platform)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[HTTP] Asset not found: %s (object_id=%s type=%s active_revision=%s) no candidates", path, objectId, assetType, activeRevision)
|
log.Printf("[HTTP] Asset not found: %s (object_id=%s type=%s platform=%s active_revision=%s) no candidates", path, objectId, assetType, platform, activeRevision)
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@@ -354,7 +392,7 @@ func (s *OctoHTTPServer) serveUnsoAsset(w http.ResponseWriter, r *http.Request,
|
|||||||
}
|
}
|
||||||
if !strings.EqualFold(actualMD5, candidate.ExpectedMD5) {
|
if !strings.EqualFold(actualMD5, candidate.ExpectedMD5) {
|
||||||
md5Mismatches = append(md5Mismatches, candidate.Revision+":"+candidate.Path+" ["+candidate.Source+"] expected="+candidate.ExpectedMD5+" actual="+actualMD5)
|
md5Mismatches = append(md5Mismatches, candidate.Revision+":"+candidate.Path+" ["+candidate.Source+"] expected="+candidate.ExpectedMD5+" actual="+actualMD5)
|
||||||
log.Printf("[HTTP] Asset md5 mismatch: object_id=%s type=%s path=%s expected=%s actual=%s active_revision=%s list_revision=%s resolved_revision=%s source=%s", objectId, assetType, candidate.Path, candidate.ExpectedMD5, actualMD5, resolution.ActiveRevision, resolution.ListRevision, candidate.Revision, candidate.Source)
|
log.Printf("[HTTP] Asset md5 mismatch: object_id=%s type=%s platform=%s path=%s expected=%s actual=%s active_revision=%s list_revision=%s resolved_revision=%s source=%s", objectId, assetType, platform, candidate.Path, candidate.ExpectedMD5, actualMD5, resolution.ActiveRevision, resolution.ListRevision, candidate.Revision, candidate.Source)
|
||||||
f.Close()
|
f.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -366,9 +404,9 @@ func (s *OctoHTTPServer) serveUnsoAsset(w http.ResponseWriter, r *http.Request,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(md5Mismatches) > 0 {
|
if len(md5Mismatches) > 0 {
|
||||||
log.Printf("[HTTP] Asset md5 mismatches: object_id=%s type=%s active_revision=%s list_revision=%s mismatches=%v", objectId, assetType, resolution.ActiveRevision, resolution.ListRevision, md5Mismatches)
|
log.Printf("[HTTP] Asset md5 mismatches: object_id=%s type=%s platform=%s active_revision=%s list_revision=%s mismatches=%v", objectId, assetType, platform, resolution.ActiveRevision, resolution.ListRevision, md5Mismatches)
|
||||||
}
|
}
|
||||||
log.Printf("[HTTP] Asset not found: %s (object_id=%s type=%s active_revision=%s list_revision=%s) tried paths: %v", path, objectId, assetType, resolution.ActiveRevision, resolution.ListRevision, triedPaths)
|
log.Printf("[HTTP] Asset not found: %s (object_id=%s type=%s platform=%s active_revision=%s list_revision=%s) tried paths: %v", path, objectId, assetType, platform, resolution.ActiveRevision, resolution.ListRevision, triedPaths)
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
@@ -400,24 +438,12 @@ func (s *OctoHTTPServer) serveListBin(w http.ResponseWriter, filePath string) {
|
|||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveDatabaseBinE serves MasterMemory database: /assets/release/{version}/database.bin.e
|
// serveDatabaseBinE serves the master data binary. The URL's {version} segment
|
||||||
// -> assets/release/{version}.bin.e (or assets/release/database.bin.e fallback).
|
// is a cache key (it changes whenever the file's mtime changes, see
|
||||||
func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, path string) {
|
// DataService.GetLatestMasterDataVersion) but does not select a different file —
|
||||||
parts := strings.Split(path, "/")
|
// there's only ever one bin.e on disk.
|
||||||
var version string
|
func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, _ string) {
|
||||||
for i, p := range parts {
|
filePath := filepath.Join(s.BaseDir, "assets", "release", "20240404193219.bin.e")
|
||||||
if p == "release" && i+1 < len(parts) {
|
|
||||||
version = parts[i+1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filePath := filepath.Join(s.BaseDir, "assets", "release", "database.bin.e")
|
|
||||||
if version != "" {
|
|
||||||
vPath := filepath.Join(s.BaseDir, "assets", "release", version+".bin.e")
|
|
||||||
if _, err := os.Stat(vPath); err == nil {
|
|
||||||
filePath = vPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
http.ServeFile(w, r, filePath)
|
http.ServeFile(w, r, filePath)
|
||||||
}
|
}
|
||||||
@@ -454,6 +480,13 @@ func (s *OctoHTTPServer) handleWebAPI(w http.ResponseWriter, r *http.Request, pa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(path, "panelmission") {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(panelMissionPage))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><body></body></html>`))
|
w.Write([]byte(`<!DOCTYPE html><html><body></body></html>`))
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,11 +15,11 @@ type OmikujiServiceServer struct {
|
|||||||
pb.UnimplementedOmikujiServiceServer
|
pb.UnimplementedOmikujiServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.OmikujiCatalog
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.OmikujiCatalog) *OmikujiServiceServer {
|
func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *OmikujiServiceServer {
|
||||||
return &OmikujiServiceServer{users: users, sessions: sessions, catalog: catalog}
|
return &OmikujiServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiDrawRequest) (*pb.OmikujiDrawResponse, error) {
|
func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiDrawRequest) (*pb.OmikujiDrawResponse, error) {
|
||||||
@@ -36,7 +36,7 @@ func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiD
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &pb.OmikujiDrawResponse{
|
return &pb.OmikujiDrawResponse{
|
||||||
OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId),
|
OmikujiResultAssetId: s.holder.Get().Omikuji.LookupAssetId(req.OmikujiId),
|
||||||
OmikujiItem: []*pb.OmikujiItem{},
|
OmikujiItem: []*pb.OmikujiItem{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,17 +19,63 @@ type PartsServiceServer struct {
|
|||||||
pb.UnimplementedPartsServiceServer
|
pb.UnimplementedPartsServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.PartsCatalog
|
holder *runtime.Holder
|
||||||
config *masterdata.GameConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.PartsCatalog, config *masterdata.GameConfig) *PartsServiceServer {
|
func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *PartsServiceServer {
|
||||||
return &PartsServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
return &PartsServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PartsServiceServer) Protect(ctx context.Context, req *pb.PartsProtectRequest) (*pb.PartsProtectResponse, error) {
|
||||||
|
log.Printf("[PartsService] Protect: uuids=%v", req.UserPartsUuid)
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
for _, uuid := range req.UserPartsUuid {
|
||||||
|
part, ok := user.Parts[uuid]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[PartsService] Protect: part uuid=%s not found", uuid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
part.IsProtected = true
|
||||||
|
part.LatestVersion = nowMillis
|
||||||
|
user.Parts[uuid] = part
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return &pb.PartsProtectResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PartsServiceServer) Unprotect(ctx context.Context, req *pb.PartsUnprotectRequest) (*pb.PartsUnprotectResponse, error) {
|
||||||
|
log.Printf("[PartsService] Unprotect: uuids=%v", req.UserPartsUuid)
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
for _, uuid := range req.UserPartsUuid {
|
||||||
|
part, ok := user.Parts[uuid]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[PartsService] Unprotect: part uuid=%s not found", uuid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
part.IsProtected = false
|
||||||
|
part.LatestVersion = nowMillis
|
||||||
|
user.Parts[uuid] = part
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return &pb.PartsUnprotectResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) {
|
func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) {
|
||||||
log.Printf("[PartsService] Sell: %d part(s)", len(req.UserPartsUuid))
|
log.Printf("[PartsService] Sell: %d part(s)", len(req.UserPartsUuid))
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Parts
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|
||||||
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
@@ -44,13 +91,13 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
partDef, ok := s.catalog.PartsById[part.PartsId]
|
partDef, ok := catalog.PartsById[part.PartsId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[PartsService] Sell: partsId=%d not in catalog, skipping", part.PartsId)
|
log.Printf("[PartsService] Sell: partsId=%d not in catalog, skipping", part.PartsId)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sellFunc, ok := s.catalog.SellPriceByRarity[partDef.RarityType]
|
sellFunc, ok := catalog.SellPriceByRarity[partDef.RarityType]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[PartsService] Sell: no sell price func for rarity=%d, skipping", partDef.RarityType)
|
log.Printf("[PartsService] Sell: no sell price func for rarity=%d, skipping", partDef.RarityType)
|
||||||
continue
|
continue
|
||||||
@@ -68,7 +115,7 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if totalGold > 0 {
|
if totalGold > 0 {
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
|
user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
|
||||||
log.Printf("[PartsService] Sell: total gold +%d", totalGold)
|
log.Printf("[PartsService] Sell: total gold +%d", totalGold)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -82,6 +129,9 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
|
|||||||
func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) {
|
func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) {
|
||||||
log.Printf("[PartsService] Enhance: uuid=%s", req.UserPartsUuid)
|
log.Printf("[PartsService] Enhance: uuid=%s", req.UserPartsUuid)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Parts
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -99,33 +149,33 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
partDef, ok := s.catalog.PartsById[part.PartsId]
|
partDef, ok := catalog.PartsById[part.PartsId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[PartsService] Enhance: part master id=%d not found", part.PartsId)
|
log.Printf("[PartsService] Enhance: part master id=%d not found", part.PartsId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rarity, ok := s.catalog.RarityByRarityType[partDef.RarityType]
|
rarity, ok := catalog.RarityByRarityType[partDef.RarityType]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[PartsService] Enhance: rarity type=%d not found", partDef.RarityType)
|
log.Printf("[PartsService] Enhance: rarity type=%d not found", partDef.RarityType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
goldCost := int32(0)
|
goldCost := int32(0)
|
||||||
if prices, ok := s.catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok {
|
if prices, ok := catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok {
|
||||||
goldCost = prices[part.Level]
|
goldCost = prices[part.Level]
|
||||||
}
|
}
|
||||||
|
|
||||||
currentGold := user.ConsumableItems[s.config.ConsumableItemIdForGold]
|
currentGold := user.ConsumableItems[config.ConsumableItemIdForGold]
|
||||||
if currentGold < goldCost {
|
if currentGold < goldCost {
|
||||||
log.Printf("[PartsService] Enhance: insufficient gold have=%d need=%d", currentGold, goldCost)
|
log.Printf("[PartsService] Enhance: insufficient gold have=%d need=%d", currentGold, goldCost)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
|
|
||||||
successRate := int32(1000)
|
successRate := int32(1000)
|
||||||
if rates, ok := s.catalog.RateByGroupAndLevel[rarity.PartsLevelUpRateGroupId]; ok {
|
if rates, ok := catalog.RateByGroupAndLevel[rarity.PartsLevelUpRateGroupId]; ok {
|
||||||
if r, ok := rates[part.Level]; ok {
|
if r, ok := rates[part.Level]; ok {
|
||||||
successRate = r
|
successRate = r
|
||||||
}
|
}
|
||||||
@@ -137,7 +187,7 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
|
|||||||
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)",
|
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)",
|
||||||
part.PartsId, part.Level-1, part.Level, successRate, goldCost)
|
part.PartsId, part.Level-1, part.Level, successRate, goldCost)
|
||||||
|
|
||||||
s.grantSubStatuses(user, req.UserPartsUuid, part, partDef, nowMillis)
|
grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)",
|
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)",
|
||||||
part.PartsId, part.Level, successRate, goldCost)
|
part.PartsId, part.Level, successRate, goldCost)
|
||||||
@@ -155,9 +205,9 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) {
|
func grantPartsSubStatuses(catalog *masterdata.PartsCatalog, user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) {
|
||||||
unlockLevels := s.catalog.SubStatusUnlockLvls[partDef.RarityType]
|
unlockLevels := catalog.SubStatusUnlockLvls[partDef.RarityType]
|
||||||
pool := s.catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId]
|
pool := catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId]
|
||||||
if len(pool) == 0 {
|
if len(pool) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,13 +223,13 @@ func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string
|
|||||||
}
|
}
|
||||||
|
|
||||||
pick := pool[rand.Intn(len(pool))]
|
pick := pool[rand.Intn(len(pool))]
|
||||||
def, ok := s.catalog.PartsStatusMainById[pick]
|
def, ok := catalog.PartsStatusMainById[pick]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
statusValue := def.StatusChangeInitialValue
|
statusValue := def.StatusChangeInitialValue
|
||||||
if f, ok := s.catalog.FuncResolver.Resolve(def.StatusNumericalFunctionId); ok {
|
if f, ok := catalog.FuncResolver.Resolve(def.StatusNumericalFunctionId); ok {
|
||||||
statusValue = f.Evaluate(part.Level)
|
statusValue = f.Evaluate(part.Level)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,3 +270,102 @@ func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsRep
|
|||||||
|
|
||||||
return &pb.PartsReplacePresetResponse{}, nil
|
return &pb.PartsReplacePresetResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PartsServiceServer) UpdatePresetName(ctx context.Context, req *pb.PartsUpdatePresetNameRequest) (*pb.PartsUpdatePresetNameResponse, error) {
|
||||||
|
log.Printf("[PartsService] UpdatePresetName: preset=%d name=%q", req.UserPartsPresetNumber, req.Name)
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
preset := user.PartsPresets[req.UserPartsPresetNumber]
|
||||||
|
preset.UserPartsPresetNumber = req.UserPartsPresetNumber
|
||||||
|
preset.Name = req.Name
|
||||||
|
preset.LatestVersion = nowMillis
|
||||||
|
user.PartsPresets[req.UserPartsPresetNumber] = preset
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parts update preset name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.PartsUpdatePresetNameResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PartsServiceServer) UpdatePresetTagNumber(ctx context.Context, req *pb.PartsUpdatePresetTagNumberRequest) (*pb.PartsUpdatePresetTagNumberResponse, error) {
|
||||||
|
log.Printf("[PartsService] UpdatePresetTagNumber: preset=%d tag=%d", req.UserPartsPresetNumber, req.UserPartsPresetTagNumber)
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
preset := user.PartsPresets[req.UserPartsPresetNumber]
|
||||||
|
preset.UserPartsPresetNumber = req.UserPartsPresetNumber
|
||||||
|
preset.UserPartsPresetTagNumber = req.UserPartsPresetTagNumber
|
||||||
|
preset.LatestVersion = nowMillis
|
||||||
|
user.PartsPresets[req.UserPartsPresetNumber] = preset
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parts update preset tag number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.PartsUpdatePresetTagNumberResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PartsServiceServer) UpdatePresetTagName(ctx context.Context, req *pb.PartsUpdatePresetTagNameRequest) (*pb.PartsUpdatePresetTagNameResponse, error) {
|
||||||
|
log.Printf("[PartsService] UpdatePresetTagName: tag=%d name=%q", req.UserPartsPresetTagNumber, req.Name)
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
tag := user.PartsPresetTags[req.UserPartsPresetTagNumber]
|
||||||
|
tag.UserPartsPresetTagNumber = req.UserPartsPresetTagNumber
|
||||||
|
tag.Name = req.Name
|
||||||
|
tag.LatestVersion = nowMillis
|
||||||
|
user.PartsPresetTags[req.UserPartsPresetTagNumber] = tag
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parts update preset tag name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.PartsUpdatePresetTagNameResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PartsServiceServer) CopyPreset(ctx context.Context, req *pb.PartsCopyPresetRequest) (*pb.PartsCopyPresetResponse, error) {
|
||||||
|
log.Printf("[PartsService] CopyPreset: from=%d to=%d", req.FromUserPartsPresetNumber, req.ToUserPartsPresetNumber)
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
from, ok := user.PartsPresets[req.FromUserPartsPresetNumber]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("[PartsService] CopyPreset: source preset=%d not found, skipping", req.FromUserPartsPresetNumber)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
to := from
|
||||||
|
to.UserPartsPresetNumber = req.ToUserPartsPresetNumber
|
||||||
|
to.LatestVersion = nowMillis
|
||||||
|
user.PartsPresets[req.ToUserPartsPresetNumber] = to
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parts copy preset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.PartsCopyPresetResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PartsServiceServer) RemovePreset(ctx context.Context, req *pb.PartsRemovePresetRequest) (*pb.PartsRemovePresetResponse, error) {
|
||||||
|
log.Printf("[PartsService] RemovePreset: preset=%d", req.UserPartsPresetNumber)
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|
||||||
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
delete(user.PartsPresets, req.UserPartsPresetNumber)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parts remove preset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.PartsRemovePresetResponse{}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
platformAndroid = "android"
|
||||||
|
platformIOS = "ios"
|
||||||
|
)
|
||||||
|
|
||||||
|
// platformFromUserAgent classifies an HTTP request as iOS vs Android based on
|
||||||
|
// the User-Agent header. Unity's UnityWebRequest does not set a UA on iOS, so
|
||||||
|
// CFNetwork's default ("<bundle>/<build> CFNetwork/x Darwin/x") is what arrives;
|
||||||
|
// on Android Unity sets "UnityPlayer/... (UnityWebRequest/...)". Any other UA
|
||||||
|
// (or none) is treated as Android, matching model.DefaultPlatform.
|
||||||
|
func platformFromUserAgent(r *http.Request) string {
|
||||||
|
ua := r.Header.Get("User-Agent")
|
||||||
|
if strings.Contains(ua, "Darwin/") || strings.Contains(ua, "CFNetwork/") {
|
||||||
|
return platformIOS
|
||||||
|
}
|
||||||
|
return platformAndroid
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +28,18 @@ func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Cont
|
|||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
user.PortalCageStatus.IsCurrentProgress = true
|
user.PortalCageStatus.IsCurrentProgress = true
|
||||||
user.PortalCageStatus.LatestVersion = now
|
user.PortalCageStatus.LatestVersion = now
|
||||||
|
// Mama's Room ends any active replay — flip flow type to MainFlow;
|
||||||
|
// ReplayFlow* stay sticky (matches original userdata snapshot).
|
||||||
|
if user.MainQuest.CurrentQuestFlowType != int32(model.QuestFlowTypeMainFlow) {
|
||||||
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
|
user.MainQuest.LatestVersion = now
|
||||||
|
}
|
||||||
|
// Returning to Mama's Room also ends any active side story.
|
||||||
|
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
|
||||||
|
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||||
|
LatestVersion: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return &pb.UpdatePortalCageSceneProgressResponse{}, nil
|
return &pb.UpdatePortalCageSceneProgressResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/questflow"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -18,34 +18,37 @@ type BigHuntServiceServer struct {
|
|||||||
pb.UnimplementedBigHuntServiceServer
|
pb.UnimplementedBigHuntServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.BigHuntCatalog
|
holder *runtime.Holder
|
||||||
engine *questflow.QuestHandler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBigHuntServiceServer(
|
func NewBigHuntServiceServer(
|
||||||
users store.UserRepository,
|
users store.UserRepository,
|
||||||
sessions store.SessionRepository,
|
sessions store.SessionRepository,
|
||||||
catalog *masterdata.BigHuntCatalog,
|
holder *runtime.Holder,
|
||||||
engine *questflow.QuestHandler,
|
|
||||||
) *BigHuntServiceServer {
|
) *BigHuntServiceServer {
|
||||||
return &BigHuntServiceServer{users: users, sessions: sessions, catalog: catalog, engine: engine}
|
return &BigHuntServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.StartBigHuntQuestRequest) (*pb.StartBigHuntQuestResponse, error) {
|
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",
|
log.Printf("[BigHuntService] StartBigHuntQuest: bossQuestId=%d questId=%d deckNumber=%d isDryRun=%v",
|
||||||
req.BigHuntBossQuestId, req.BigHuntQuestId, req.UserDeckNumber, req.IsDryRun)
|
req.BigHuntBossQuestId, req.BigHuntQuestId, req.UserDeckNumber, req.IsDryRun)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.BigHunt
|
||||||
|
engine := cat.QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
bhQuest, ok := s.catalog.QuestById[req.BigHuntQuestId]
|
bhQuest, ok := catalog.QuestById[req.BigHuntQuestId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId)
|
log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
today := gametime.StartOfDayMillis()
|
||||||
|
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
if ok {
|
if ok {
|
||||||
s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis)
|
engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.BigHuntProgress = store.BigHuntProgress{
|
user.BigHuntProgress = store.BigHuntProgress{
|
||||||
@@ -59,6 +62,9 @@ func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.St
|
|||||||
user.BigHuntDeckNumber = req.UserDeckNumber
|
user.BigHuntDeckNumber = req.UserDeckNumber
|
||||||
|
|
||||||
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
||||||
|
if st.LatestChallengeDatetime < today {
|
||||||
|
st.DailyChallengeCount = 0
|
||||||
|
}
|
||||||
st.DailyChallengeCount++
|
st.DailyChallengeCount++
|
||||||
st.LatestChallengeDatetime = nowMillis
|
st.LatestChallengeDatetime = nowMillis
|
||||||
st.LatestVersion = nowMillis
|
st.LatestVersion = nowMillis
|
||||||
@@ -85,21 +91,27 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v",
|
log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v",
|
||||||
req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired)
|
req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.BigHunt
|
||||||
|
engine := cat.QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
bhQuest := s.catalog.QuestById[req.BigHuntQuestId]
|
bhQuest := catalog.QuestById[req.BigHuntQuestId]
|
||||||
bossQuest := s.catalog.BossQuestById[req.BigHuntBossQuestId]
|
bossQuest := catalog.BossQuestById[req.BigHuntBossQuestId]
|
||||||
boss := s.catalog.BossByBossId[bossQuest.BigHuntBossId]
|
boss := catalog.BossByBossId[bossQuest.BigHuntBossId]
|
||||||
|
|
||||||
var scoreInfo *pb.BigHuntScoreInfo
|
var scoreInfo *pb.BigHuntScoreInfo
|
||||||
var scoreRewards []*pb.BigHuntReward
|
var scoreRewards []*pb.BigHuntReward
|
||||||
|
var battleReportWaves []*pb.BigHuntBattleReportWave
|
||||||
|
|
||||||
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)
|
engine.HandleBigHuntQuestFinish(user, bhQuest.QuestId, req.IsRetired, false, nowMillis)
|
||||||
|
|
||||||
if req.IsRetired || user.BigHuntProgress.IsDryRun {
|
if req.IsRetired || user.BigHuntProgress.IsDryRun {
|
||||||
user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis}
|
user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis}
|
||||||
|
user.BigHuntBattleBinary = nil
|
||||||
|
user.BigHuntBattleDetail = store.BigHuntBattleDetail{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +120,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
baseScore := totalDamage
|
baseScore := totalDamage
|
||||||
|
|
||||||
difficultyBonusPermil := int32(0)
|
difficultyBonusPermil := int32(0)
|
||||||
if coeff, ok := s.catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok {
|
if coeff, ok := catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok {
|
||||||
difficultyBonusPermil = coeff
|
difficultyBonusPermil = coeff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +137,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
|
|
||||||
userScore := baseScore * int64(1000+difficultyBonusPermil+aliveBonusPermil+maxComboBonusPermil) / 1000
|
userScore := baseScore * int64(1000+difficultyBonusPermil+aliveBonusPermil+maxComboBonusPermil) / 1000
|
||||||
|
|
||||||
isHighScore := false
|
if userScore > user.BigHuntMaxScores[bossQuest.BigHuntBossId].MaxScore {
|
||||||
oldMaxBoss := user.BigHuntMaxScores[bossQuest.BigHuntBossId]
|
|
||||||
oldMax := oldMaxBoss.MaxScore
|
|
||||||
if userScore > oldMax {
|
|
||||||
isHighScore = true
|
|
||||||
user.BigHuntMaxScores[bossQuest.BigHuntBossId] = store.BigHuntMaxScore{
|
user.BigHuntMaxScores[bossQuest.BigHuntBossId] = store.BigHuntMaxScore{
|
||||||
MaxScore: userScore,
|
MaxScore: userScore,
|
||||||
MaxScoreUpdateDatetime: nowMillis,
|
MaxScoreUpdateDatetime: nowMillis,
|
||||||
@@ -138,11 +146,12 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
schedKey := store.BigHuntScheduleScoreKey{
|
schedKey := store.BigHuntScheduleScoreKey{
|
||||||
BigHuntScheduleId: s.catalog.ActiveScheduleId,
|
BigHuntScheduleId: catalog.ActiveScheduleId,
|
||||||
BigHuntBossId: bossQuest.BigHuntBossId,
|
BigHuntBossId: bossQuest.BigHuntBossId,
|
||||||
}
|
}
|
||||||
oldSchedMax := user.BigHuntScheduleMaxScores[schedKey].MaxScore
|
oldSchedMax := user.BigHuntScheduleMaxScores[schedKey].MaxScore
|
||||||
if userScore > oldSchedMax {
|
isHighScore := userScore > oldSchedMax
|
||||||
|
if isHighScore {
|
||||||
user.BigHuntScheduleMaxScores[schedKey] = store.BigHuntScheduleMaxScore{
|
user.BigHuntScheduleMaxScores[schedKey] = store.BigHuntScheduleMaxScore{
|
||||||
MaxScore: userScore,
|
MaxScore: userScore,
|
||||||
MaxScoreUpdateDatetime: nowMillis,
|
MaxScoreUpdateDatetime: nowMillis,
|
||||||
@@ -163,7 +172,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assetGradeIconId := s.catalog.ResolveGradeIconId(bossQuest.BigHuntBossId, userScore)
|
assetGradeIconId := catalog.ResolveGradeIconId(bossQuest.BigHuntBossId, userScore)
|
||||||
|
|
||||||
scoreInfo = &pb.BigHuntScoreInfo{
|
scoreInfo = &pb.BigHuntScoreInfo{
|
||||||
UserScore: userScore,
|
UserScore: userScore,
|
||||||
@@ -177,12 +186,12 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isHighScore {
|
if isHighScore {
|
||||||
rewardGroupId := s.catalog.ResolveActiveScoreRewardGroupId(
|
rewardGroupId := catalog.ResolveActiveScoreRewardGroupId(
|
||||||
bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis)
|
bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis)
|
||||||
if rewardGroupId > 0 {
|
if rewardGroupId > 0 {
|
||||||
newItems := s.catalog.CollectNewRewards(rewardGroupId, oldMax, userScore)
|
newItems := catalog.CollectNewRewards(rewardGroupId, oldSchedMax, userScore)
|
||||||
for _, item := range newItems {
|
for _, item := range newItems {
|
||||||
s.engine.Granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
engine.Granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
||||||
scoreRewards = append(scoreRewards, &pb.BigHuntReward{
|
scoreRewards = append(scoreRewards, &pb.BigHuntReward{
|
||||||
PossessionType: item.PossessionType,
|
PossessionType: item.PossessionType,
|
||||||
PossessionId: item.PossessionId,
|
PossessionId: item.PossessionId,
|
||||||
@@ -192,6 +201,31 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(detail.CostumeBattleInfo) > 0 {
|
||||||
|
wavesByIndex := map[int32]*pb.BigHuntBattleReportWave{}
|
||||||
|
var waveOrder []int32
|
||||||
|
for _, ci := range detail.CostumeBattleInfo {
|
||||||
|
wave, ok := wavesByIndex[ci.WaveIndex]
|
||||||
|
if !ok {
|
||||||
|
wave = &pb.BigHuntBattleReportWave{}
|
||||||
|
wavesByIndex[ci.WaveIndex] = wave
|
||||||
|
waveOrder = append(waveOrder, ci.WaveIndex)
|
||||||
|
}
|
||||||
|
wave.BattleReportCostume = append(wave.BattleReportCostume, &pb.BigHuntBattleReportCostume{
|
||||||
|
CostumeId: ci.CostumeId,
|
||||||
|
TotalDamage: ci.TotalDamage,
|
||||||
|
HitCount: ci.HitCount,
|
||||||
|
BattleReportRandomDisplay: &pb.BattleReportRandomDisplay{
|
||||||
|
RandomDisplayValueType: ci.RandomDisplayValueType,
|
||||||
|
RandomDisplayValue: ci.RandomDisplayValue,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, idx := range waveOrder {
|
||||||
|
battleReportWaves = append(battleReportWaves, wavesByIndex[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis}
|
user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis}
|
||||||
user.BigHuntBattleBinary = nil
|
user.BigHuntBattleBinary = nil
|
||||||
user.BigHuntBattleDetail = store.BigHuntBattleDetail{}
|
user.BigHuntBattleDetail = store.BigHuntBattleDetail{}
|
||||||
@@ -204,33 +238,46 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
|
|||||||
scoreRewards = []*pb.BigHuntReward{}
|
scoreRewards = []*pb.BigHuntReward{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if battleReportWaves == nil {
|
||||||
|
battleReportWaves = []*pb.BigHuntBattleReportWave{}
|
||||||
|
}
|
||||||
|
battleReport := &pb.BigHuntBattleReport{
|
||||||
|
BattleReportWave: battleReportWaves,
|
||||||
|
}
|
||||||
|
|
||||||
return &pb.FinishBigHuntQuestResponse{
|
return &pb.FinishBigHuntQuestResponse{
|
||||||
ScoreInfo: scoreInfo,
|
ScoreInfo: scoreInfo,
|
||||||
ScoreReward: scoreRewards,
|
ScoreReward: scoreRewards,
|
||||||
BattleReport: &pb.BigHuntBattleReport{
|
BattleReport: battleReport,
|
||||||
BattleReportWave: []*pb.BigHuntBattleReportWave{},
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb.RestartBigHuntQuestRequest) (*pb.RestartBigHuntQuestResponse, error) {
|
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)
|
log.Printf("[BigHuntService] RestartBigHuntQuest: bossQuestId=%d questId=%d", req.BigHuntBossQuestId, req.BigHuntQuestId)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.BigHunt
|
||||||
|
engine := cat.QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
bhQuest := s.catalog.QuestById[req.BigHuntQuestId]
|
bhQuest := catalog.QuestById[req.BigHuntQuestId]
|
||||||
|
|
||||||
var battleBinary []byte
|
var battleBinary []byte
|
||||||
var deckNumber int32
|
var deckNumber int32
|
||||||
|
|
||||||
|
today := gametime.StartOfDayMillis()
|
||||||
|
|
||||||
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)
|
engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, user.BigHuntDeckNumber, nowMillis)
|
||||||
|
|
||||||
user.BigHuntProgress.CurrentQuestSceneId = 0
|
user.BigHuntProgress.CurrentQuestSceneId = 0
|
||||||
user.BigHuntProgress.LatestVersion = nowMillis
|
user.BigHuntProgress.LatestVersion = nowMillis
|
||||||
|
|
||||||
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
||||||
|
if st.LatestChallengeDatetime < today {
|
||||||
|
st.DailyChallengeCount = 0
|
||||||
|
}
|
||||||
st.DailyChallengeCount++
|
st.DailyChallengeCount++
|
||||||
st.LatestChallengeDatetime = nowMillis
|
st.LatestChallengeDatetime = nowMillis
|
||||||
st.LatestVersion = nowMillis
|
st.LatestVersion = nowMillis
|
||||||
@@ -249,19 +296,58 @@ func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb.
|
|||||||
func (s *BigHuntServiceServer) SkipBigHuntQuest(ctx context.Context, req *pb.SkipBigHuntQuestRequest) (*pb.SkipBigHuntQuestResponse, error) {
|
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)
|
log.Printf("[BigHuntService] SkipBigHuntQuest: bossQuestId=%d skipCount=%d", req.BigHuntBossQuestId, req.SkipCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.BigHunt
|
||||||
|
granter := cat.QuestHandler.Granter
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
today := gametime.StartOfDayMillis()
|
||||||
|
|
||||||
|
bossQuest, hasBossQuest := catalog.BossQuestById[req.BigHuntBossQuestId]
|
||||||
|
var scoreRewards []*pb.BigHuntReward
|
||||||
|
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
||||||
|
if st.LatestChallengeDatetime < today {
|
||||||
|
st.DailyChallengeCount = 0
|
||||||
|
}
|
||||||
st.DailyChallengeCount += req.SkipCount
|
st.DailyChallengeCount += req.SkipCount
|
||||||
st.LatestChallengeDatetime = nowMillis
|
st.LatestChallengeDatetime = nowMillis
|
||||||
st.LatestVersion = nowMillis
|
st.LatestVersion = nowMillis
|
||||||
user.BigHuntStatuses[req.BigHuntBossQuestId] = st
|
user.BigHuntStatuses[req.BigHuntBossQuestId] = st
|
||||||
|
|
||||||
|
if !hasBossQuest || req.SkipCount <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rewardGroupId := catalog.ResolveActiveScoreRewardGroupId(bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis)
|
||||||
|
if rewardGroupId == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
maxScore := user.BigHuntScheduleMaxScores[store.BigHuntScheduleScoreKey{
|
||||||
|
BigHuntScheduleId: catalog.ActiveScheduleId,
|
||||||
|
BigHuntBossId: bossQuest.BigHuntBossId,
|
||||||
|
}].MaxScore
|
||||||
|
if maxScore <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := catalog.CollectNewRewards(rewardGroupId, 0, maxScore)
|
||||||
|
for n := int32(0); n < req.SkipCount; n++ {
|
||||||
|
for _, item := range items {
|
||||||
|
granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
||||||
|
scoreRewards = append(scoreRewards, &pb.BigHuntReward{
|
||||||
|
PossessionType: item.PossessionType,
|
||||||
|
PossessionId: item.PossessionId,
|
||||||
|
Count: item.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if scoreRewards == nil {
|
||||||
|
scoreRewards = []*pb.BigHuntReward{}
|
||||||
|
}
|
||||||
return &pb.SkipBigHuntQuestResponse{
|
return &pb.SkipBigHuntQuestResponse{
|
||||||
ScoreReward: []*pb.BigHuntReward{},
|
ScoreReward: scoreRewards,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,12 +370,35 @@ func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *p
|
|||||||
user.BigHuntBattleBinary = req.BattleBinary
|
user.BigHuntBattleBinary = req.BattleBinary
|
||||||
|
|
||||||
if req.BigHuntBattleDetail != nil {
|
if req.BigHuntBattleDetail != nil {
|
||||||
|
existingCostumes := user.BigHuntBattleDetail.CostumeBattleInfo
|
||||||
|
nextWaveIndex := int32(bigHuntWaveCount(existingCostumes))
|
||||||
|
newCostumes := make([]store.BigHuntCostumeBattleInfo, 0, len(req.BigHuntBattleDetail.CostumeBattleInfo))
|
||||||
|
for _, ci := range req.BigHuntBattleDetail.CostumeBattleInfo {
|
||||||
|
if ci == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var rdType int32
|
||||||
|
var rdValue int64
|
||||||
|
if rd := ci.BattleReportRandomDisplay; rd != nil {
|
||||||
|
rdType = rd.RandomDisplayValueType
|
||||||
|
rdValue = rd.RandomDisplayValue
|
||||||
|
}
|
||||||
|
newCostumes = append(newCostumes, store.BigHuntCostumeBattleInfo{
|
||||||
|
WaveIndex: nextWaveIndex,
|
||||||
|
CostumeId: resolveBigHuntCostumeId(user, ci.UserDeckNumber, ci.DeckCharacterNumber),
|
||||||
|
TotalDamage: ci.TotalDamage,
|
||||||
|
HitCount: ci.HitCount,
|
||||||
|
RandomDisplayValueType: rdType,
|
||||||
|
RandomDisplayValue: rdValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
user.BigHuntBattleDetail = store.BigHuntBattleDetail{
|
user.BigHuntBattleDetail = store.BigHuntBattleDetail{
|
||||||
DeckType: req.BigHuntBattleDetail.DeckType,
|
DeckType: req.BigHuntBattleDetail.DeckType,
|
||||||
UserTripleDeckNumber: req.BigHuntBattleDetail.UserTripleDeckNumber,
|
UserTripleDeckNumber: req.BigHuntBattleDetail.UserTripleDeckNumber,
|
||||||
BossKnockDownCount: req.BigHuntBattleDetail.BossKnockDownCount,
|
BossKnockDownCount: req.BigHuntBattleDetail.BossKnockDownCount,
|
||||||
MaxComboCount: req.BigHuntBattleDetail.MaxComboCount,
|
MaxComboCount: req.BigHuntBattleDetail.MaxComboCount,
|
||||||
TotalDamage: totalDamage,
|
TotalDamage: totalDamage,
|
||||||
|
CostumeBattleInfo: append(existingCostumes, newCostumes...),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +411,7 @@ func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *p
|
|||||||
func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) {
|
func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) {
|
||||||
log.Printf("[BigHuntService] GetBigHuntTopData")
|
log.Printf("[BigHuntService] GetBigHuntTopData")
|
||||||
|
|
||||||
|
catalog := s.holder.Get().BigHunt
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
user, _ := s.users.LoadUser(userId)
|
user, _ := s.users.LoadUser(userId)
|
||||||
|
|
||||||
@@ -309,13 +419,13 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
|
|||||||
weeklyVersion := gametime.WeeklyVersion(nowMillis)
|
weeklyVersion := gametime.WeeklyVersion(nowMillis)
|
||||||
|
|
||||||
var weeklyScoreResults []*pb.WeeklyScoreResult
|
var weeklyScoreResults []*pb.WeeklyScoreResult
|
||||||
for _, boss := range s.catalog.BossByBossId {
|
for _, boss := range catalog.BossByBossId {
|
||||||
key := store.BigHuntWeeklyScoreKey{
|
key := store.BigHuntWeeklyScoreKey{
|
||||||
BigHuntWeeklyVersion: weeklyVersion,
|
BigHuntWeeklyVersion: weeklyVersion,
|
||||||
AttributeType: boss.AttributeType,
|
AttributeType: boss.AttributeType,
|
||||||
}
|
}
|
||||||
ws := user.BigHuntWeeklyMaxScores[key]
|
ws := user.BigHuntWeeklyMaxScores[key]
|
||||||
gradeIconId := s.catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore)
|
gradeIconId := catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore)
|
||||||
|
|
||||||
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
|
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
|
||||||
AttributeType: boss.AttributeType,
|
AttributeType: boss.AttributeType,
|
||||||
@@ -330,10 +440,10 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
|
|||||||
|
|
||||||
ws := user.BigHuntWeeklyStatuses[weeklyVersion]
|
ws := user.BigHuntWeeklyStatuses[weeklyVersion]
|
||||||
|
|
||||||
weeklyRewards := s.resolveWeeklyRewards(user, weeklyVersion, nowMillis)
|
weeklyRewards := resolveBigHuntWeeklyRewards(catalog, user, weeklyVersion, nowMillis)
|
||||||
|
|
||||||
lastWeekVersion := weeklyVersion - 7*24*60*60*1000
|
lastWeekVersion := weeklyVersion - 7*24*60*60*1000
|
||||||
lastWeekRewards := s.resolveWeeklyRewards(user, lastWeekVersion, nowMillis)
|
lastWeekRewards := resolveBigHuntWeeklyRewards(catalog, user, lastWeekVersion, nowMillis)
|
||||||
|
|
||||||
return &pb.GetBigHuntTopDataResponse{
|
return &pb.GetBigHuntTopDataResponse{
|
||||||
WeeklyScoreResult: weeklyScoreResults,
|
WeeklyScoreResult: weeklyScoreResults,
|
||||||
@@ -343,14 +453,49 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward {
|
func bigHuntWaveCount(infos []store.BigHuntCostumeBattleInfo) int {
|
||||||
var rewards []*pb.BigHuntReward
|
if len(infos) == 0 {
|
||||||
for _, boss := range s.catalog.BossByBossId {
|
return 0
|
||||||
rewardKey := masterdata.BigHuntWeeklyRewardKey{
|
}
|
||||||
ScheduleId: 1,
|
return int(infos[len(infos)-1].WaveIndex) + 1
|
||||||
AttributeType: boss.AttributeType,
|
}
|
||||||
|
|
||||||
|
func resolveBigHuntCostumeId(user *store.UserState, userDeckNumber, deckCharacterNumber int32) int32 {
|
||||||
|
if userDeckNumber == 0 {
|
||||||
|
userDeckNumber = user.BigHuntDeckNumber
|
||||||
|
}
|
||||||
|
for _, dt := range []model.DeckType{model.DeckTypeBigHunt, model.DeckTypeQuest} {
|
||||||
|
deck, ok := user.Decks[store.DeckKey{DeckType: dt, UserDeckNumber: userDeckNumber}]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
rewardGroupId := s.catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis)
|
var dcUuid string
|
||||||
|
switch deckCharacterNumber {
|
||||||
|
case 1:
|
||||||
|
dcUuid = deck.UserDeckCharacterUuid01
|
||||||
|
case 2:
|
||||||
|
dcUuid = deck.UserDeckCharacterUuid02
|
||||||
|
case 3:
|
||||||
|
dcUuid = deck.UserDeckCharacterUuid03
|
||||||
|
}
|
||||||
|
if dcUuid == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dc, ok := user.DeckCharacters[dcUuid]
|
||||||
|
if !ok || dc.UserCostumeUuid == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if costume, ok := user.Costumes[dc.UserCostumeUuid]; ok {
|
||||||
|
return costume.CostumeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveBigHuntWeeklyRewards(catalog *masterdata.BigHuntCatalog, user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward {
|
||||||
|
var rewards []*pb.BigHuntReward
|
||||||
|
for _, boss := range catalog.BossByBossId {
|
||||||
|
rewardGroupId := catalog.ResolveActiveWeeklyRewardGroupIdByAttr(boss.AttributeType, nowMillis)
|
||||||
if rewardGroupId == 0 {
|
if rewardGroupId == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -359,7 +504,7 @@ func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weekly
|
|||||||
AttributeType: boss.AttributeType,
|
AttributeType: boss.AttributeType,
|
||||||
}
|
}
|
||||||
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
|
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
|
||||||
for _, item := range s.catalog.CollectNewRewards(rewardGroupId, 0, maxScore) {
|
for _, item := range catalog.CollectNewRewards(rewardGroupId, 0, maxScore) {
|
||||||
rewards = append(rewards, &pb.BigHuntReward{
|
rewards = append(rewards, &pb.BigHuntReward{
|
||||||
PossessionType: item.PossessionType,
|
PossessionType: item.PossessionType,
|
||||||
PossessionId: item.PossessionId,
|
PossessionId: item.PossessionId,
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ import (
|
|||||||
func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) {
|
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)
|
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
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)
|
engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
drops := engine.BattleDropRewards(req.QuestId)
|
||||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||||
for i, d := range drops {
|
for i, d := range drops {
|
||||||
pbDrops[i] = &pb.BattleDropReward{
|
pbDrops[i] = &pb.BattleDropReward{
|
||||||
@@ -40,10 +41,11 @@ 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)
|
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated)
|
||||||
|
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var outcome questflow.FinishOutcome
|
var outcome questflow.FinishOutcome
|
||||||
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)
|
outcome = engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.FinishEventQuestResponse{
|
return &pb.FinishEventQuestResponse{
|
||||||
@@ -61,9 +63,10 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis
|
|||||||
func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.RestartEventQuestRequest) (*pb.RestartEventQuestResponse, error) {
|
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)
|
log.Printf("[QuestService] RestartEventQuest: chapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis())
|
engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis())
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.RestartEventQuestResponse{
|
return &pb.RestartEventQuestResponse{
|
||||||
@@ -74,9 +77,10 @@ func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.Rest
|
|||||||
func (s *QuestServiceServer) UpdateEventQuestSceneProgress(ctx context.Context, req *pb.UpdateEventQuestSceneProgressRequest) (*pb.UpdateEventQuestSceneProgressResponse, error) {
|
func (s *QuestServiceServer) UpdateEventQuestSceneProgress(ctx context.Context, req *pb.UpdateEventQuestSceneProgressRequest) (*pb.UpdateEventQuestSceneProgressResponse, error) {
|
||||||
log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.UpdateEventQuestSceneProgressResponse{}, nil
|
return &pb.UpdateEventQuestSceneProgressResponse{}, nil
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import (
|
|||||||
func (s *QuestServiceServer) StartExtraQuest(ctx context.Context, req *pb.StartExtraQuestRequest) (*pb.StartExtraQuestResponse, error) {
|
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)
|
log.Printf("[QuestService] StartExtraQuest: questId=%d deckNumber=%d", req.QuestId, req.UserDeckNumber)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
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)
|
engine.HandleExtraQuestStart(user, req.QuestId, req.UserDeckNumber, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
drops := engine.BattleDropRewards(req.QuestId)
|
||||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||||
for i, d := range drops {
|
for i, d := range drops {
|
||||||
pbDrops[i] = &pb.BattleDropReward{
|
pbDrops[i] = &pb.BattleDropReward{
|
||||||
@@ -38,10 +39,11 @@ 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)
|
log.Printf("[QuestService] FinishExtraQuest: questId=%d isRetired=%v isAnnihilated=%v", req.QuestId, req.IsRetired, req.IsAnnihilated)
|
||||||
|
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var outcome questflow.FinishOutcome
|
var outcome questflow.FinishOutcome
|
||||||
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)
|
outcome = engine.HandleExtraQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.FinishExtraQuestResponse{
|
return &pb.FinishExtraQuestResponse{
|
||||||
@@ -58,14 +60,15 @@ func (s *QuestServiceServer) FinishExtraQuest(ctx context.Context, req *pb.Finis
|
|||||||
func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.RestartExtraQuestRequest) (*pb.RestartExtraQuestResponse, error) {
|
func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.RestartExtraQuestRequest) (*pb.RestartExtraQuestResponse, error) {
|
||||||
log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId)
|
log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var deckNumber int32
|
var deckNumber int32
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis())
|
engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis())
|
||||||
deckNumber = user.Quests[req.QuestId].UserDeckNumber
|
deckNumber = user.Quests[req.QuestId].UserDeckNumber
|
||||||
})
|
})
|
||||||
|
|
||||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
drops := engine.BattleDropRewards(req.QuestId)
|
||||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||||
for i, d := range drops {
|
for i, d := range drops {
|
||||||
pbDrops[i] = &pb.BattleDropReward{
|
pbDrops[i] = &pb.BattleDropReward{
|
||||||
@@ -84,9 +87,10 @@ func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.Rest
|
|||||||
func (s *QuestServiceServer) UpdateExtraQuestSceneProgress(ctx context.Context, req *pb.UpdateExtraQuestSceneProgressRequest) (*pb.UpdateExtraQuestSceneProgressResponse, error) {
|
func (s *QuestServiceServer) UpdateExtraQuestSceneProgress(ctx context.Context, req *pb.UpdateExtraQuestSceneProgressRequest) (*pb.UpdateExtraQuestSceneProgressResponse, error) {
|
||||||
log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.UpdateExtraQuestSceneProgressResponse{}, nil
|
return &pb.UpdateExtraQuestSceneProgressResponse{}, nil
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/questflow"
|
"lunar-tear/server/internal/questflow"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -17,22 +18,23 @@ type QuestServiceServer struct {
|
|||||||
pb.UnimplementedQuestServiceServer
|
pb.UnimplementedQuestServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
engine *questflow.QuestHandler
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *QuestServiceServer {
|
func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *QuestServiceServer {
|
||||||
if engine == nil {
|
if holder == nil {
|
||||||
panic("quest handler is required")
|
panic("runtime holder is required")
|
||||||
}
|
}
|
||||||
return &QuestServiceServer{users: users, sessions: sessions, engine: engine}
|
return &QuestServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, req *pb.UpdateMainFlowSceneProgressRequest) (*pb.UpdateMainFlowSceneProgressResponse, error) {
|
func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, req *pb.UpdateMainFlowSceneProgressRequest) (*pb.UpdateMainFlowSceneProgressResponse, error) {
|
||||||
log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
|
log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.UpdateMainFlowSceneProgressResponse{}, nil
|
return &pb.UpdateMainFlowSceneProgressResponse{}, nil
|
||||||
@@ -41,9 +43,10 @@ func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, re
|
|||||||
func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, req *pb.UpdateReplayFlowSceneProgressRequest) (*pb.UpdateReplayFlowSceneProgressResponse, error) {
|
func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, req *pb.UpdateReplayFlowSceneProgressRequest) (*pb.UpdateReplayFlowSceneProgressResponse, error) {
|
||||||
log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
|
log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.UpdateReplayFlowSceneProgressResponse{}, nil
|
return &pb.UpdateReplayFlowSceneProgressResponse{}, nil
|
||||||
@@ -52,9 +55,10 @@ func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context,
|
|||||||
func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) {
|
func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) {
|
||||||
log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleMainQuestSceneProgress(user, req.QuestSceneId)
|
engine.HandleMainQuestSceneProgress(user, req.QuestSceneId)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.UpdateMainQuestSceneProgressResponse{}, nil
|
return &pb.UpdateMainQuestSceneProgressResponse{}, nil
|
||||||
@@ -63,17 +67,18 @@ func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, r
|
|||||||
func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) {
|
func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) {
|
||||||
log.Printf("[QuestService] StartMainQuest: %+v", req)
|
log.Printf("[QuestService] StartMainQuest: %+v", req)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
if req.IsReplayFlow {
|
if req.IsReplayFlow {
|
||||||
s.engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
||||||
} else {
|
} else {
|
||||||
s.engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.IsMainFlow, req.UserDeckNumber, nowMillis)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
drops := engine.BattleDropRewards(req.QuestId)
|
||||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||||
for i, d := range drops {
|
for i, d := range drops {
|
||||||
pbDrops[i] = &pb.BattleDropReward{
|
pbDrops[i] = &pb.BattleDropReward{
|
||||||
@@ -108,10 +113,11 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish
|
|||||||
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType)
|
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType)
|
||||||
|
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var outcome questflow.FinishOutcome
|
var outcome questflow.FinishOutcome
|
||||||
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)
|
outcome = engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.FinishMainQuestResponse{
|
return &pb.FinishMainQuestResponse{
|
||||||
@@ -130,14 +136,15 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish
|
|||||||
func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.RestartMainQuestRequest) (*pb.RestartMainQuestResponse, error) {
|
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)
|
log.Printf("[QuestService] RestartMainQuest: questId=%d isMainFlow=%v", req.QuestId, req.IsMainFlow)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var deckNumber int32
|
var deckNumber int32
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
s.engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis())
|
engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis())
|
||||||
deckNumber = user.Quests[req.QuestId].UserDeckNumber
|
deckNumber = user.Quests[req.QuestId].UserDeckNumber
|
||||||
})
|
})
|
||||||
|
|
||||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
drops := engine.BattleDropRewards(req.QuestId)
|
||||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||||
for i, d := range drops {
|
for i, d := range drops {
|
||||||
pbDrops[i] = &pb.BattleDropReward{
|
pbDrops[i] = &pb.BattleDropReward{
|
||||||
@@ -162,6 +169,7 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq
|
|||||||
log.Printf("[QuestService] SkipQuest: questId=%d skipCount=%d useEffectItems=%d", req.QuestId, req.SkipCount, len(req.UseEffectItem))
|
log.Printf("[QuestService] SkipQuest: questId=%d skipCount=%d useEffectItems=%d", req.QuestId, req.SkipCount, len(req.UseEffectItem))
|
||||||
|
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var outcome questflow.FinishOutcome
|
var outcome questflow.FinishOutcome
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
@@ -172,7 +180,7 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq
|
|||||||
user.ConsumableItems[item.ConsumableItemId] = 0
|
user.ConsumableItems[item.ConsumableItemId] = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outcome = s.engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis)
|
outcome = engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.SkipQuestResponse{
|
return &pb.SkipQuestResponse{
|
||||||
@@ -184,15 +192,21 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq
|
|||||||
func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) {
|
func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) {
|
||||||
log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId)
|
log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId)
|
||||||
|
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
|
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
|
||||||
if seasonId, ok := s.engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
|
if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
|
||||||
user.MainQuest.MainQuestSeasonId = seasonId
|
user.MainQuest.MainQuestSeasonId = seasonId
|
||||||
}
|
}
|
||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
user.PortalCageStatus.IsCurrentProgress = false
|
user.PortalCageStatus.IsCurrentProgress = false
|
||||||
user.PortalCageStatus.LatestVersion = now
|
user.PortalCageStatus.LatestVersion = now
|
||||||
|
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
|
||||||
|
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||||
|
LatestVersion: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.SetRouteResponse{}, nil
|
return &pb.SetRouteResponse{}, nil
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,11 +16,68 @@ type SideStoryQuestServiceServer struct {
|
|||||||
pb.UnimplementedSideStoryQuestServiceServer
|
pb.UnimplementedSideStoryQuestServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.SideStoryCatalog
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.SideStoryCatalog) *SideStoryQuestServiceServer {
|
func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *SideStoryQuestServiceServer {
|
||||||
return &SideStoryQuestServiceServer{users: users, sessions: sessions, catalog: catalog}
|
return &SideStoryQuestServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sideStoryClearedCount(info *masterdata.SideStoryQuestInfo, user *store.UserState) int {
|
||||||
|
cleared := 0
|
||||||
|
for _, questId := range info.Quests {
|
||||||
|
if user.QuestLimitContentStatus[questId].LimitContentQuestStatusType == 1 {
|
||||||
|
cleared++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleared
|
||||||
|
}
|
||||||
|
|
||||||
|
func sideStoryQuestCleared(info *masterdata.SideStoryQuestInfo, user *store.UserState) bool {
|
||||||
|
return info != nil && len(info.Quests) > 0 && sideStoryClearedCount(info, user) == len(info.Quests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sideStoryNextSceneAfterBattle(info *masterdata.SideStoryQuestInfo, user *store.UserState) (int32, bool) {
|
||||||
|
cleared := sideStoryClearedCount(info, user)
|
||||||
|
if cleared == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
total := len(info.Quests)
|
||||||
|
var sceneType model.SideStorySceneIdType
|
||||||
|
switch {
|
||||||
|
case cleared >= total:
|
||||||
|
sceneType = model.SideStorySceneOutroduction
|
||||||
|
case cleared == total-1:
|
||||||
|
sceneType = model.SideStorySceneUnlockLastQuest
|
||||||
|
default:
|
||||||
|
sceneType = model.SideStoryScenePlayLastQuest
|
||||||
|
}
|
||||||
|
return info.SceneIdByType(sceneType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySideStoryProgressState(progress *store.SideStoryQuestProgress, info *masterdata.SideStoryQuestInfo, user *store.UserState) {
|
||||||
|
if sideStoryQuestCleared(info, user) {
|
||||||
|
progress.SideStoryQuestStateType = model.SideStoryQuestStateCleared
|
||||||
|
} else if progress.SideStoryQuestStateType == model.SideStoryQuestStateUnknown {
|
||||||
|
progress.SideStoryQuestStateType = model.SideStoryQuestStateActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSideStoryActive(user *store.UserState, questId, sceneId int32, nowMillis int64) {
|
||||||
|
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||||
|
CurrentSideStoryQuestId: questId,
|
||||||
|
CurrentSideStoryQuestSceneId: sceneId,
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSideStoryScene(user *store.UserState, info *masterdata.SideStoryQuestInfo, questId, sceneId int32, nowMillis int64) {
|
||||||
|
progress := user.SideStoryQuests[questId]
|
||||||
|
progress.HeadSideStoryQuestSceneId = sceneId
|
||||||
|
applySideStoryProgressState(&progress, info, user)
|
||||||
|
progress.LatestVersion = nowMillis
|
||||||
|
user.SideStoryQuests[questId] = progress
|
||||||
|
setSideStoryActive(user, questId, sceneId, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) {
|
func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) {
|
||||||
@@ -27,29 +85,30 @@ func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Con
|
|||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
firstSceneId := s.catalog.FirstSceneByQuestId[req.SideStoryQuestId]
|
info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId]
|
||||||
|
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
if info == nil || len(info.Quests) == 0 {
|
||||||
|
log.Printf("[SideStoryQuestService] unknown sideStoryQuestId=%d, skipping", req.SideStoryQuestId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
|
existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
|
||||||
|
|
||||||
var sceneId int32
|
var scene int32
|
||||||
if exists && existing.HeadSideStoryQuestSceneId > 0 {
|
var ok bool
|
||||||
sceneId = existing.HeadSideStoryQuestSceneId
|
if !exists || existing.HeadSideStoryQuestSceneId == 0 {
|
||||||
|
scene, ok = info.SceneIdByType(model.SideStorySceneIntroduction)
|
||||||
} else {
|
} else {
|
||||||
sceneId = firstSceneId
|
scene, ok = sideStoryNextSceneAfterBattle(info, user)
|
||||||
}
|
if !ok {
|
||||||
|
scene, ok = existing.HeadSideStoryQuestSceneId, true
|
||||||
user.SideStoryActiveProgress.CurrentSideStoryQuestId = req.SideStoryQuestId
|
|
||||||
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = sceneId
|
|
||||||
user.SideStoryActiveProgress.LatestVersion = nowMillis
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
user.SideStoryQuests[req.SideStoryQuestId] = store.SideStoryQuestProgress{
|
|
||||||
HeadSideStoryQuestSceneId: firstSceneId,
|
|
||||||
SideStoryQuestStateType: model.SideStoryQuestStateActive,
|
|
||||||
LatestVersion: nowMillis,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSideStoryScene(user, info, req.SideStoryQuestId, scene, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.MoveSideStoryQuestResponse{}, nil
|
return &pb.MoveSideStoryQuestResponse{}, nil
|
||||||
@@ -61,16 +120,10 @@ func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx cont
|
|||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId]
|
||||||
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = req.SideStoryQuestSceneId
|
|
||||||
user.SideStoryActiveProgress.LatestVersion = nowMillis
|
|
||||||
|
|
||||||
progress := user.SideStoryQuests[req.SideStoryQuestId]
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
if req.SideStoryQuestSceneId > progress.HeadSideStoryQuestSceneId {
|
setSideStoryScene(user, info, req.SideStoryQuestId, req.SideStoryQuestSceneId, nowMillis)
|
||||||
progress.HeadSideStoryQuestSceneId = req.SideStoryQuestSceneId
|
|
||||||
}
|
|
||||||
progress.LatestVersion = nowMillis
|
|
||||||
user.SideStoryQuests[req.SideStoryQuestId] = progress
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil
|
return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/gametime"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *QuestServiceServer) ReceiveTowerAccumulationReward(ctx context.Context, req *pb.ReceiveTowerAccumulationRewardRequest) (*pb.ReceiveTowerAccumulationRewardResponse, error) {
|
||||||
|
log.Printf("[QuestService] ReceiveTowerAccumulationReward: eventQuestChapterId=%d targetMissionClearCount=%d",
|
||||||
|
req.EventQuestChapterId, req.TargetMissionClearCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
tower := cat.Tower
|
||||||
|
granter := cat.QuestHandler.Granter
|
||||||
|
|
||||||
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
rec := user.TowerAccumulationRewards[req.EventQuestChapterId]
|
||||||
|
old := rec.LatestRewardReceiveQuestMissionClearCount
|
||||||
|
|
||||||
|
items, highest := tower.CollectRewards(req.EventQuestChapterId, old, req.TargetMissionClearCount)
|
||||||
|
if highest <= old {
|
||||||
|
log.Printf("[QuestService] ReceiveTowerAccumulationReward: nothing to grant for chapter=%d (claimed=%d, target=%d)",
|
||||||
|
req.EventQuestChapterId, old, req.TargetMissionClearCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
granter.GrantFull(user, model.PossessionType(it.PossessionType), it.PossessionId, it.Count, nowMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec.EventQuestChapterId = req.EventQuestChapterId
|
||||||
|
rec.LatestRewardReceiveQuestMissionClearCount = highest
|
||||||
|
rec.LatestVersion = nowMillis
|
||||||
|
user.TowerAccumulationRewards[req.EventQuestChapterId] = rec
|
||||||
|
|
||||||
|
log.Printf("[QuestService] ReceiveTowerAccumulationReward: chapter=%d granted %d item(s), claimed %d -> %d",
|
||||||
|
req.EventQuestChapterId, len(items), old, highest)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &pb.ReceiveTowerAccumulationRewardResponse{}, nil
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -15,43 +15,74 @@ import (
|
|||||||
|
|
||||||
type RewardServiceServer struct {
|
type RewardServiceServer struct {
|
||||||
pb.UnimplementedRewardServiceServer
|
pb.UnimplementedRewardServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
bhCatalog *masterdata.BigHuntCatalog
|
holder *runtime.Holder
|
||||||
granter *store.PossessionGranter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRewardServiceServer(
|
func NewRewardServiceServer(
|
||||||
users store.UserRepository,
|
users store.UserRepository,
|
||||||
sessions store.SessionRepository,
|
sessions store.SessionRepository,
|
||||||
bhCatalog *masterdata.BigHuntCatalog,
|
holder *runtime.Holder,
|
||||||
granter *store.PossessionGranter,
|
|
||||||
) *RewardServiceServer {
|
) *RewardServiceServer {
|
||||||
return &RewardServiceServer{users: users, sessions: sessions, bhCatalog: bhCatalog, granter: granter}
|
return &RewardServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveBigHuntRewardResponse, error) {
|
func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveBigHuntRewardResponse, error) {
|
||||||
log.Printf("[RewardService] ReceiveBigHuntReward")
|
log.Printf("[RewardService] ReceiveBigHuntReward")
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
bhCatalog := cat.BigHunt
|
||||||
|
granter := cat.QuestHandler.Granter
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
weeklyVersion := gametime.WeeklyVersion(nowMillis)
|
weeklyVersion := gametime.WeeklyVersion(nowMillis)
|
||||||
|
today := gametime.StartOfDayMillis()
|
||||||
|
|
||||||
var weeklyScoreResults []*pb.WeeklyScoreResult
|
var weeklyScoreResults []*pb.WeeklyScoreResult
|
||||||
var weeklyRewards []*pb.BigHuntReward
|
var weeklyRewards []*pb.BigHuntReward
|
||||||
isReceived := false
|
isReceived := false
|
||||||
|
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
for bossQuestId, bossQuest := range bhCatalog.BossQuestById {
|
||||||
|
st := user.BigHuntStatuses[bossQuestId]
|
||||||
|
if st.LastDailyRewardReceivedDayVersion >= today {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rewardGroupId := bhCatalog.ResolveActiveScoreRewardGroupId(bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis)
|
||||||
|
if rewardGroupId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
maxScore := user.BigHuntScheduleMaxScores[store.BigHuntScheduleScoreKey{
|
||||||
|
BigHuntScheduleId: bhCatalog.ActiveScheduleId,
|
||||||
|
BigHuntBossId: bossQuest.BigHuntBossId,
|
||||||
|
}].MaxScore
|
||||||
|
if maxScore <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items := bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore)
|
||||||
|
for _, item := range items {
|
||||||
|
granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
||||||
|
}
|
||||||
|
if len(items) > 0 {
|
||||||
|
log.Printf("[RewardService] ReceiveBigHuntReward: bossQuestId=%d granted %d daily rewards (maxScore=%d, group=%d)",
|
||||||
|
bossQuestId, len(items), maxScore, rewardGroupId)
|
||||||
|
}
|
||||||
|
st.LastDailyRewardReceivedDayVersion = today
|
||||||
|
st.LatestVersion = nowMillis
|
||||||
|
user.BigHuntStatuses[bossQuestId] = st
|
||||||
|
}
|
||||||
|
|
||||||
ws := user.BigHuntWeeklyStatuses[weeklyVersion]
|
ws := user.BigHuntWeeklyStatuses[weeklyVersion]
|
||||||
isReceived = ws.IsReceivedWeeklyReward
|
isReceived = ws.IsReceivedWeeklyReward
|
||||||
|
|
||||||
for _, boss := range s.bhCatalog.BossByBossId {
|
for _, boss := range bhCatalog.BossByBossId {
|
||||||
key := store.BigHuntWeeklyScoreKey{
|
key := store.BigHuntWeeklyScoreKey{
|
||||||
BigHuntWeeklyVersion: weeklyVersion,
|
BigHuntWeeklyVersion: weeklyVersion,
|
||||||
AttributeType: boss.AttributeType,
|
AttributeType: boss.AttributeType,
|
||||||
}
|
}
|
||||||
wms := user.BigHuntWeeklyMaxScores[key]
|
wms := user.BigHuntWeeklyMaxScores[key]
|
||||||
gradeIcon := s.bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore)
|
gradeIcon := bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore)
|
||||||
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
|
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
|
||||||
AttributeType: boss.AttributeType,
|
AttributeType: boss.AttributeType,
|
||||||
BeforeMaxScore: wms.MaxScore,
|
BeforeMaxScore: wms.MaxScore,
|
||||||
@@ -64,12 +95,8 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isReceived {
|
if !isReceived {
|
||||||
for _, boss := range s.bhCatalog.BossByBossId {
|
for _, boss := range bhCatalog.BossByBossId {
|
||||||
rewardKey := masterdata.BigHuntWeeklyRewardKey{
|
rewardGroupId := bhCatalog.ResolveActiveWeeklyRewardGroupIdByAttr(boss.AttributeType, nowMillis)
|
||||||
ScheduleId: 1,
|
|
||||||
AttributeType: boss.AttributeType,
|
|
||||||
}
|
|
||||||
rewardGroupId := s.bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis)
|
|
||||||
if rewardGroupId == 0 {
|
if rewardGroupId == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -80,9 +107,9 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty
|
|||||||
}
|
}
|
||||||
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
|
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
|
||||||
|
|
||||||
items := s.bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore)
|
items := bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
s.granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
||||||
weeklyRewards = append(weeklyRewards, &pb.BigHuntReward{
|
weeklyRewards = append(weeklyRewards, &pb.BigHuntReward{
|
||||||
PossessionType: item.PossessionType,
|
PossessionType: item.PossessionType,
|
||||||
PossessionId: item.PossessionId,
|
PossessionId: item.PossessionId,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
@@ -18,23 +19,25 @@ type ShopServiceServer struct {
|
|||||||
pb.UnimplementedShopServiceServer
|
pb.UnimplementedShopServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.ShopCatalog
|
holder *runtime.Holder
|
||||||
granter *store.PossessionGranter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ShopCatalog, granter *store.PossessionGranter) *ShopServiceServer {
|
func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ShopServiceServer {
|
||||||
return &ShopServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter}
|
return &ShopServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) {
|
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)
|
log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Shop
|
||||||
|
granter := cat.QuestHandler.Granter
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
for shopItemId, qty := range req.ShopItems {
|
for shopItemId, qty := range req.ShopItems {
|
||||||
item, ok := s.catalog.Items[shopItemId]
|
item, ok := catalog.Items[shopItemId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId)
|
log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId)
|
||||||
continue
|
continue
|
||||||
@@ -46,8 +49,8 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, content := range s.catalog.Contents[shopItemId] {
|
for _, content := range catalog.Contents[shopItemId] {
|
||||||
s.granter.GrantFull(user,
|
granter.GrantFull(user,
|
||||||
model.PossessionType(content.PossessionType),
|
model.PossessionType(content.PossessionType),
|
||||||
content.PossessionId,
|
content.PossessionId,
|
||||||
content.Count*qty,
|
content.Count*qty,
|
||||||
@@ -55,7 +58,7 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.applyContentEffects(user, shopItemId, qty, nowMillis)
|
applyShopContentEffects(catalog, user, shopItemId, qty, nowMillis)
|
||||||
|
|
||||||
si := user.ShopItems[shopItemId]
|
si := user.ShopItems[shopItemId]
|
||||||
si.ShopItemId = shopItemId
|
si.ShopItemId = shopItemId
|
||||||
@@ -76,12 +79,13 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
|
|||||||
func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
|
func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
|
||||||
log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed)
|
log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed)
|
||||||
|
|
||||||
|
catalog := s.holder.Get().Shop
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
_, 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 {
|
if len(user.ShopReplaceableLineup) == 0 && len(catalog.ItemShopPool) > 0 {
|
||||||
for i, itemId := range s.catalog.ItemShopPool {
|
for i, itemId := range catalog.ItemShopPool {
|
||||||
slot := int32(i + 1)
|
slot := int32(i + 1)
|
||||||
user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{
|
user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{
|
||||||
SlotNumber: slot,
|
SlotNumber: slot,
|
||||||
@@ -93,7 +97,7 @@ func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.Refresh
|
|||||||
if req.IsGemUsed {
|
if req.IsGemUsed {
|
||||||
user.ShopReplaceable.LineupUpdateCount++
|
user.ShopReplaceable.LineupUpdateCount++
|
||||||
user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis
|
user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis
|
||||||
for _, itemId := range s.catalog.ItemShopPool {
|
for _, itemId := range catalog.ItemShopPool {
|
||||||
if si, ok := user.ShopItems[itemId]; ok {
|
if si, ok := user.ShopItems[itemId]; ok {
|
||||||
si.BoughtCount = 0
|
si.BoughtCount = 0
|
||||||
si.LatestVersion = nowMillis
|
si.LatestVersion = nowMillis
|
||||||
@@ -120,11 +124,14 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
|
|||||||
log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s",
|
log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s",
|
||||||
req.ShopId, req.ShopItemId, req.ProductId)
|
req.ShopId, req.ShopItemId, req.ProductId)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Shop
|
||||||
|
granter := cat.QuestHandler.Granter
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
_, 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]
|
item, ok := catalog.Items[req.ShopItemId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId)
|
log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId)
|
||||||
return
|
return
|
||||||
@@ -134,8 +141,8 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
|
|||||||
log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err)
|
log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, content := range s.catalog.Contents[req.ShopItemId] {
|
for _, content := range catalog.Contents[req.ShopItemId] {
|
||||||
s.granter.GrantFull(user,
|
granter.GrantFull(user,
|
||||||
model.PossessionType(content.PossessionType),
|
model.PossessionType(content.PossessionType),
|
||||||
content.PossessionId,
|
content.PossessionId,
|
||||||
content.Count,
|
content.Count,
|
||||||
@@ -143,13 +150,13 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.applyContentEffects(user, req.ShopItemId, 1, nowMillis)
|
applyShopContentEffects(catalog, user, req.ShopItemId, 1, nowMillis)
|
||||||
|
|
||||||
si := user.ShopItems[req.ShopItemId]
|
si := user.ShopItems[req.ShopItemId]
|
||||||
si.ShopItemId = req.ShopItemId
|
si.ShopItemId = req.ShopItemId
|
||||||
si.BoughtCount++
|
si.BoughtCount++
|
||||||
if item.ShopItemLimitedStockId > 0 {
|
if item.ShopItemLimitedStockId > 0 {
|
||||||
if maxCount, ok := s.catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount {
|
if maxCount, ok := catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount {
|
||||||
si.BoughtCount = 0
|
si.BoughtCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,27 +189,15 @@ func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemId, qty int32, nowMillis int64) {
|
func applyShopContentEffects(catalog *masterdata.ShopCatalog, user *store.UserState, shopItemId, qty int32, nowMillis int64) {
|
||||||
for _, effect := range s.catalog.Effects[shopItemId] {
|
for _, effect := range catalog.Effects[shopItemId] {
|
||||||
switch effect.EffectTargetType {
|
switch effect.EffectTargetType {
|
||||||
case model.EffectTargetStaminaRecovery:
|
case model.EffectTargetStaminaRecovery:
|
||||||
maxMillis := s.catalog.MaxStaminaMillis[user.Status.Level]
|
maxMillis := catalog.MaxStaminaMillis[user.Status.Level]
|
||||||
millis := s.resolveEffectMillis(effect.EffectValueType, effect.EffectValue, user.Status.Level)
|
millis := store.ResolveStaminaEffectMillis(effect.EffectValueType, effect.EffectValue, maxMillis)
|
||||||
store.RecoverStamina(user, millis*qty, maxMillis, nowMillis)
|
store.RecoverStamina(user, millis*qty, maxMillis, nowMillis)
|
||||||
default:
|
default:
|
||||||
log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType)
|
log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShopServiceServer) resolveEffectMillis(effectValueType, effectValue, userLevel int32) int32 {
|
|
||||||
switch effectValueType {
|
|
||||||
case model.EffectValueFixed:
|
|
||||||
return effectValue
|
|
||||||
case model.EffectValuePermil:
|
|
||||||
maxMillis := s.catalog.MaxStaminaMillis[userLevel]
|
|
||||||
return effectValue * maxMillis / 1000
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/questflow"
|
"lunar-tear/server/internal/questflow"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,17 +16,18 @@ type TutorialServiceServer struct {
|
|||||||
pb.UnimplementedTutorialServiceServer
|
pb.UnimplementedTutorialServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
engine *questflow.QuestHandler
|
holder *runtime.Holder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *TutorialServiceServer {
|
func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *TutorialServiceServer {
|
||||||
return &TutorialServiceServer{users: users, sessions: sessions, engine: engine}
|
return &TutorialServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb.SetTutorialProgressRequest) (*pb.SetTutorialProgressResponse, error) {
|
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)
|
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()
|
nowMillis := gametime.NowMillis()
|
||||||
|
engine := s.holder.Get().QuestHandler
|
||||||
var grants []questflow.RewardGrant
|
var grants []questflow.RewardGrant
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
existing, exists := user.Tutorials[req.TutorialType]
|
existing, exists := user.Tutorials[req.TutorialType]
|
||||||
@@ -36,7 +38,7 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb
|
|||||||
ChoiceId: req.ChoiceId,
|
ChoiceId: req.ChoiceId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
|
grants = engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
|
||||||
if req.TutorialType == int32(model.TutorialTypeMenuFirst) && req.ProgressPhase == 20 {
|
if req.TutorialType == int32(model.TutorialTypeMenuFirst) && req.ProgressPhase == 20 {
|
||||||
store.EnsureDefaultDeck(user, nowMillis)
|
store.EnsureDefaultDeck(user, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,30 +12,44 @@ import (
|
|||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/peer"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserServiceServer struct {
|
type UserServiceServer struct {
|
||||||
pb.UnimplementedUserServiceServer
|
pb.UnimplementedUserServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
authURL string
|
holder *runtime.Holder
|
||||||
|
authURL string
|
||||||
|
noRegister bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository, authURL string) *UserServiceServer {
|
func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder, authURL string, noRegister bool) *UserServiceServer {
|
||||||
if authURL != "" && !strings.Contains(authURL, "://") {
|
if authURL != "" && !strings.Contains(authURL, "://") {
|
||||||
authURL = "http://" + authURL
|
authURL = "http://" + authURL
|
||||||
}
|
}
|
||||||
return &UserServiceServer{users: users, sessions: sessions, authURL: authURL}
|
return &UserServiceServer{users: users, sessions: sessions, holder: holder, authURL: authURL, noRegister: noRegister}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
|
func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
|
||||||
|
if s.noRegister {
|
||||||
|
ip := "invalid"
|
||||||
|
|
||||||
|
if p, ok := peer.FromContext(ctx); ok {
|
||||||
|
ip = p.Addr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Denied user registration: ip=%s uuid=%s", ip, req.Uuid)
|
||||||
|
}
|
||||||
|
|
||||||
platform := model.ClientPlatformFromContext(ctx)
|
platform := model.ClientPlatformFromContext(ctx)
|
||||||
userId, err := s.users.CreateUser(req.Uuid, platform)
|
userId, err := s.users.CreateUser(req.Uuid, platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,11 +103,14 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
|
|||||||
|
|
||||||
func (s *UserServiceServer) TransferUser(ctx context.Context, req *pb.TransferUserRequest) (*pb.TransferUserResponse, error) {
|
func (s *UserServiceServer) TransferUser(ctx context.Context, req *pb.TransferUserRequest) (*pb.TransferUserResponse, error) {
|
||||||
platform := model.ClientPlatformFromContext(ctx)
|
platform := model.ClientPlatformFromContext(ctx)
|
||||||
|
|
||||||
log.Printf("[UserService] TransferUser: platform=%s", platform)
|
log.Printf("[UserService] TransferUser: platform=%s", platform)
|
||||||
userId, err := s.users.CreateUser(req.Uuid, platform)
|
|
||||||
|
userId, err := s.users.GetUserByUUID(req.Uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create user: %w", err)
|
return nil, fmt.Errorf("create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pb.TransferUserResponse{
|
return &pb.TransferUserResponse{
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
Signature: "transferred-sig",
|
Signature: "transferred-sig",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,12 +18,11 @@ type WeaponServiceServer struct {
|
|||||||
pb.UnimplementedWeaponServiceServer
|
pb.UnimplementedWeaponServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
catalog *masterdata.WeaponCatalog
|
holder *runtime.Holder
|
||||||
config *masterdata.GameConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.WeaponCatalog, config *masterdata.GameConfig) *WeaponServiceServer {
|
func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *WeaponServiceServer {
|
||||||
return &WeaponServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
return &WeaponServiceServer{users: users, sessions: sessions, holder: holder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectRequest) (*pb.ProtectResponse, error) {
|
func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectRequest) (*pb.ProtectResponse, error) {
|
||||||
@@ -72,6 +72,9 @@ func (s *WeaponServiceServer) Unprotect(ctx context.Context, req *pb.UnprotectRe
|
|||||||
func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.EnhanceByMaterialRequest) (*pb.EnhanceByMaterialResponse, error) {
|
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)
|
log.Printf("[WeaponService] EnhanceByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceByMaterial: weapon master id=%d not found", weapon.WeaponId)
|
log.Printf("[WeaponService] EnhanceByMaterial: weapon master id=%d not found", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
@@ -91,7 +94,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
totalExp := int32(0)
|
totalExp := int32(0)
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
for materialId, count := range req.Materials {
|
for materialId, count := range req.Materials {
|
||||||
mat, ok := s.catalog.Materials[materialId]
|
mat, ok := catalog.Materials[materialId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceByMaterial: material id=%d not found, skipping", materialId)
|
log.Printf("[WeaponService] EnhanceByMaterial: material id=%d not found, skipping", materialId)
|
||||||
continue
|
continue
|
||||||
@@ -107,27 +110,45 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
|
|
||||||
expPerUnit := mat.EffectValue
|
expPerUnit := mat.EffectValue
|
||||||
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
|
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
|
||||||
expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000
|
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += expPerUnit * count
|
totalExp += expPerUnit * count
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
log.Printf("[WeaponService] EnhanceByMaterial: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
|
log.Printf("[WeaponService] EnhanceByMaterial: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
weapon.Exp += totalExp
|
weapon.Exp += totalExp
|
||||||
if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
||||||
|
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||||
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
|
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||||
|
if weapon.Level > cap {
|
||||||
|
weapon.Level = cap
|
||||||
|
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
||||||
|
weapon.Exp = thresholds[cap]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
note := user.WeaponNotes[weapon.WeaponId]
|
||||||
|
if note.MaxLevel < weapon.Level {
|
||||||
|
note.WeaponId = weapon.WeaponId
|
||||||
|
note.MaxLevel = weapon.Level
|
||||||
|
note.LatestVersion = nowMillis
|
||||||
|
user.WeaponNotes[weapon.WeaponId] = note
|
||||||
}
|
}
|
||||||
|
|
||||||
weapon.LatestVersion = nowMillis
|
weapon.LatestVersion = nowMillis
|
||||||
user.Weapons[req.UserWeaponUuid] = weapon
|
user.Weapons[req.UserWeaponUuid] = weapon
|
||||||
log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
|
log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
|
||||||
|
|
||||||
s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis)
|
checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("weapon enhance by material: %w", err)
|
return nil, fmt.Errorf("weapon enhance by material: %w", err)
|
||||||
@@ -142,6 +163,9 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) {
|
func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) {
|
||||||
log.Printf("[WeaponService] Sell: uuids=%v", req.UserWeaponUuid)
|
log.Printf("[WeaponService] Sell: uuids=%v", req.UserWeaponUuid)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
|
||||||
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
@@ -153,17 +177,17 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] Sell: weapon master id=%d not found, skipping", weapon.WeaponId)
|
log.Printf("[WeaponService] Sell: weapon master id=%d not found, skipping", weapon.WeaponId)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if sellFunc, ok := s.catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if sellFunc, ok := catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
totalGold += sellFunc.Evaluate(weapon.Level)
|
totalGold += sellFunc.Evaluate(weapon.Level)
|
||||||
}
|
}
|
||||||
|
|
||||||
if medals, ok := s.catalog.MedalsByWeaponId[weapon.WeaponId]; ok {
|
if medals, ok := catalog.MedalsByWeaponId[weapon.WeaponId]; ok {
|
||||||
for itemId, count := range medals {
|
for itemId, count := range medals {
|
||||||
user.ConsumableItems[itemId] += count
|
user.ConsumableItems[itemId] += count
|
||||||
}
|
}
|
||||||
@@ -176,7 +200,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
|
|||||||
}
|
}
|
||||||
|
|
||||||
if totalGold > 0 {
|
if totalGold > 0 {
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
|
user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
|
||||||
log.Printf("[WeaponService] Sell: granted %d gold", totalGold)
|
log.Printf("[WeaponService] Sell: granted %d gold", totalGold)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -190,6 +214,9 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
|
|||||||
func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) (*pb.EvolveResponse, error) {
|
func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) (*pb.EvolveResponse, error) {
|
||||||
log.Printf("[WeaponService] Evolve: uuid=%s", req.UserWeaponUuid)
|
log.Printf("[WeaponService] Evolve: uuid=%s", req.UserWeaponUuid)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -200,20 +227,20 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] Evolve: weapon master id=%d not found", weapon.WeaponId)
|
log.Printf("[WeaponService] Evolve: weapon master id=%d not found", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
evolvedId, ok := s.catalog.EvolutionNextWeaponId[weapon.WeaponId]
|
evolvedId, ok := catalog.EvolutionNextWeaponId[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] Evolve: no evolution for weaponId=%d", weapon.WeaponId)
|
log.Printf("[WeaponService] Evolve: no evolution for weaponId=%d", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
mats := s.catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId]
|
mats := catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId]
|
||||||
for _, mat := range mats {
|
for _, mat := range mats {
|
||||||
cur := user.Materials[mat.MaterialId]
|
cur := user.Materials[mat.MaterialId]
|
||||||
cost := mat.Count
|
cost := mat.Count
|
||||||
@@ -225,19 +252,61 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
|
|||||||
totalMaterialCount += cost
|
totalMaterialCount += cost
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.EvolutionCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.EvolutionCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost)
|
log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldWeaponId := wm.WeaponId
|
||||||
weapon.WeaponId = evolvedId
|
weapon.WeaponId = evolvedId
|
||||||
weapon.LatestVersion = nowMillis
|
weapon.LatestVersion = nowMillis
|
||||||
user.Weapons[req.UserWeaponUuid] = weapon
|
user.Weapons[req.UserWeaponUuid] = weapon
|
||||||
|
|
||||||
evolvedMaster, ok := s.catalog.Weapons[evolvedId]
|
note, hasNote := user.WeaponNotes[evolvedId]
|
||||||
|
if !hasNote {
|
||||||
|
note = store.WeaponNoteState{
|
||||||
|
WeaponId: evolvedId,
|
||||||
|
MaxLevel: weapon.Level,
|
||||||
|
MaxLimitBreakCount: weapon.LimitBreakCount,
|
||||||
|
FirstAcquisitionDatetime: nowMillis,
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
|
user.WeaponNotes[evolvedId] = note
|
||||||
|
} else {
|
||||||
|
changed := false
|
||||||
|
if note.MaxLevel < weapon.Level {
|
||||||
|
note.MaxLevel = weapon.Level
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if note.MaxLimitBreakCount < weapon.LimitBreakCount {
|
||||||
|
note.MaxLimitBreakCount = weapon.LimitBreakCount
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
note.WeaponId = evolvedId
|
||||||
|
note.LatestVersion = nowMillis
|
||||||
|
user.WeaponNotes[evolvedId] = note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldStory, hasOldStory := user.WeaponStories[oldWeaponId]; hasOldStory {
|
||||||
|
newStory, hasNewStory := user.WeaponStories[evolvedId]
|
||||||
|
if !hasNewStory || newStory.ReleasedMaxStoryIndex < oldStory.ReleasedMaxStoryIndex {
|
||||||
|
if user.WeaponStories == nil {
|
||||||
|
user.WeaponStories = make(map[int32]store.WeaponStoryState)
|
||||||
|
}
|
||||||
|
user.WeaponStories[evolvedId] = store.WeaponStoryState{
|
||||||
|
WeaponId: evolvedId,
|
||||||
|
ReleasedMaxStoryIndex: oldStory.ReleasedMaxStoryIndex,
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evolvedMaster, ok := catalog.Weapons[evolvedId]
|
||||||
if ok {
|
if ok {
|
||||||
if slots, ok := s.catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok {
|
if slots, ok := catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok {
|
||||||
abilities := make([]store.WeaponAbilityState, len(slots))
|
abilities := make([]store.WeaponAbilityState, len(slots))
|
||||||
for i, slot := range slots {
|
for i, slot := range slots {
|
||||||
abilities[i] = store.WeaponAbilityState{
|
abilities[i] = store.WeaponAbilityState{
|
||||||
@@ -250,9 +319,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId)
|
log.Printf("[WeaponService] Evolve: weaponId %d -> %d", oldWeaponId, evolvedId)
|
||||||
|
|
||||||
s.checkWeaponStoryUnlocks(user, evolvedId, weapon.Level, nowMillis)
|
checkWeaponStoryUnlocks(catalog, user, evolvedId, weapon.Level, nowMillis)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("weapon evolve: %w", err)
|
return nil, fmt.Errorf("weapon evolve: %w", err)
|
||||||
@@ -264,6 +333,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
|
|||||||
func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceSkillRequest) (*pb.EnhanceSkillResponse, error) {
|
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)
|
log.Printf("[WeaponService] EnhanceSkill: uuid=%s skillId=%d addLevel=%d", req.UserWeaponUuid, req.SkillId, req.AddLevelCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -274,13 +346,13 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceSkill: weapon master id=%d not found", weapon.WeaponId)
|
log.Printf("[WeaponService] EnhanceSkill: weapon master id=%d not found", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
groupRows := s.catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId]
|
groupRows := catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId]
|
||||||
var skillGroup *masterdata.EntityMWeaponSkillGroup
|
var skillGroup *masterdata.EntityMWeaponSkillGroup
|
||||||
for i := range groupRows {
|
for i := range groupRows {
|
||||||
if groupRows[i].SkillId == req.SkillId {
|
if groupRows[i].SkillId == req.SkillId {
|
||||||
@@ -306,7 +378,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
maxLevelFunc, ok := s.catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
|
maxLevelFunc, ok := catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceSkill: no max skill level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
|
log.Printf("[WeaponService] EnhanceSkill: no max skill level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
|
||||||
return
|
return
|
||||||
@@ -326,7 +398,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
|
|||||||
enhanceMatId := skillGroup.WeaponSkillEnhancementMaterialId
|
enhanceMatId := skillGroup.WeaponSkillEnhancementMaterialId
|
||||||
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
||||||
key := [2]int32{enhanceMatId, lvl}
|
key := [2]int32{enhanceMatId, lvl}
|
||||||
mats := s.catalog.SkillEnhanceMats[key]
|
mats := catalog.SkillEnhanceMats[key]
|
||||||
for _, mat := range mats {
|
for _, mat := range mats {
|
||||||
cur := user.Materials[mat.MaterialId]
|
cur := user.Materials[mat.MaterialId]
|
||||||
cost := mat.Count
|
cost := mat.Count
|
||||||
@@ -337,9 +409,9 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
|
|||||||
user.Materials[mat.MaterialId] = cur - cost
|
user.Materials[mat.MaterialId] = cur - cost
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.SkillCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if costFunc, ok := catalog.SkillCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
goldCost := costFunc.Evaluate(lvl + 1)
|
goldCost := costFunc.Evaluate(lvl + 1)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,6 +432,9 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
|
|||||||
func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.EnhanceAbilityRequest) (*pb.EnhanceAbilityResponse, error) {
|
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)
|
log.Printf("[WeaponService] EnhanceAbility: uuid=%s abilityId=%d addLevel=%d", req.UserWeaponUuid, req.AbilityId, req.AddLevelCount)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -370,13 +445,13 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceAbility: weapon master id=%d not found", weapon.WeaponId)
|
log.Printf("[WeaponService] EnhanceAbility: weapon master id=%d not found", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
groupRows := s.catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId]
|
groupRows := catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId]
|
||||||
var abilityGroup *masterdata.EntityMWeaponAbilityGroup
|
var abilityGroup *masterdata.EntityMWeaponAbilityGroup
|
||||||
for i := range groupRows {
|
for i := range groupRows {
|
||||||
if groupRows[i].AbilityId == req.AbilityId {
|
if groupRows[i].AbilityId == req.AbilityId {
|
||||||
@@ -402,7 +477,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
maxLevelFunc, ok := s.catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
|
maxLevelFunc, ok := catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceAbility: no max ability level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
|
log.Printf("[WeaponService] EnhanceAbility: no max ability level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
|
||||||
return
|
return
|
||||||
@@ -422,7 +497,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
|
|||||||
enhanceMatId := abilityGroup.WeaponAbilityEnhancementMaterialId
|
enhanceMatId := abilityGroup.WeaponAbilityEnhancementMaterialId
|
||||||
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
||||||
key := [2]int32{enhanceMatId, lvl}
|
key := [2]int32{enhanceMatId, lvl}
|
||||||
mats := s.catalog.AbilityEnhanceMats[key]
|
mats := catalog.AbilityEnhanceMats[key]
|
||||||
for _, mat := range mats {
|
for _, mat := range mats {
|
||||||
cur := user.Materials[mat.MaterialId]
|
cur := user.Materials[mat.MaterialId]
|
||||||
cost := mat.Count
|
cost := mat.Count
|
||||||
@@ -433,9 +508,9 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
|
|||||||
user.Materials[mat.MaterialId] = cur - cost
|
user.Materials[mat.MaterialId] = cur - cost
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.AbilityCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if costFunc, ok := catalog.AbilityCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
goldCost := costFunc.Evaluate(lvl + 1)
|
goldCost := costFunc.Evaluate(lvl + 1)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +531,9 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
|
|||||||
func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.LimitBreakByMaterialRequest) (*pb.LimitBreakByMaterialResponse, error) {
|
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)
|
log.Printf("[WeaponService] LimitBreakByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -466,18 +544,18 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount {
|
if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount {
|
||||||
log.Printf("[WeaponService] LimitBreakByMaterial: already at max limit break %d", weapon.LimitBreakCount)
|
log.Printf("[WeaponService] LimitBreakByMaterial: already at max limit break %d", weapon.LimitBreakCount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] LimitBreakByMaterial: weapon master id=%d not found", weapon.WeaponId)
|
log.Printf("[WeaponService] LimitBreakByMaterial: weapon master id=%d not found", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
|
remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
|
||||||
|
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
for materialId, count := range req.Materials {
|
for materialId, count := range req.Materials {
|
||||||
@@ -496,9 +574,9 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
|
|||||||
totalMaterialCount += count
|
totalMaterialCount += count
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.LimitBreakCostByMaterialByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.LimitBreakCostByMaterialByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
log.Printf("[WeaponService] LimitBreakByMaterial: gold cost=%d", goldCost)
|
log.Printf("[WeaponService] LimitBreakByMaterial: gold cost=%d", goldCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,6 +603,9 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
|
|||||||
func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.LimitBreakByWeaponRequest) (*pb.LimitBreakByWeaponResponse, error) {
|
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)
|
log.Printf("[WeaponService] LimitBreakByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -535,18 +616,18 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount {
|
if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount {
|
||||||
log.Printf("[WeaponService] LimitBreakByWeapon: already at max limit break %d", weapon.LimitBreakCount)
|
log.Printf("[WeaponService] LimitBreakByWeapon: already at max limit break %d", weapon.LimitBreakCount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] LimitBreakByWeapon: weapon master id=%d not found", weapon.WeaponId)
|
log.Printf("[WeaponService] LimitBreakByWeapon: weapon master id=%d not found", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
|
remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
|
||||||
|
|
||||||
consumedCount := int32(0)
|
consumedCount := int32(0)
|
||||||
for _, uuid := range req.MaterialUserWeaponUuids {
|
for _, uuid := range req.MaterialUserWeaponUuids {
|
||||||
@@ -560,7 +641,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
||||||
for itemId, count := range medals {
|
for itemId, count := range medals {
|
||||||
user.ConsumableItems[itemId] += count
|
user.ConsumableItems[itemId] += count
|
||||||
}
|
}
|
||||||
@@ -573,9 +654,9 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
|
|||||||
consumedCount++
|
consumedCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.LimitBreakCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
|
if costFunc, ok := catalog.LimitBreakCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
|
||||||
goldCost := costFunc.Evaluate(consumedCount)
|
goldCost := costFunc.Evaluate(consumedCount)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
log.Printf("[WeaponService] LimitBreakByWeapon: gold cost=%d", goldCost)
|
log.Printf("[WeaponService] LimitBreakByWeapon: gold cost=%d", goldCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,6 +683,9 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
|
|||||||
func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.EnhanceByWeaponRequest) (*pb.EnhanceByWeaponResponse, error) {
|
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)
|
log.Printf("[WeaponService] EnhanceByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -612,7 +696,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
wm, ok := catalog.Weapons[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceByWeapon: weapon master id=%d not found", weapon.WeaponId)
|
log.Printf("[WeaponService] EnhanceByWeapon: weapon master id=%d not found", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
@@ -627,19 +711,20 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
matMaster, ok := s.catalog.Weapons[matWeapon.WeaponId]
|
matMaster, ok := catalog.Weapons[matWeapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] EnhanceByWeapon: material weapon master id=%d not found, skipping", matWeapon.WeaponId)
|
log.Printf("[WeaponService] EnhanceByWeapon: material weapon master id=%d not found, skipping", matWeapon.WeaponId)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
baseExp := s.catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId]
|
matLevelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[matWeapon.WeaponId]
|
||||||
|
baseExp := catalog.BaseExpByEnhanceId[matLevelingEnhanceId]
|
||||||
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
||||||
baseExp = baseExp * s.config.MaterialSameWeaponExpCoefficientPermil / 1000
|
baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += baseExp
|
totalExp += baseExp
|
||||||
|
|
||||||
if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
||||||
for itemId, count := range medals {
|
for itemId, count := range medals {
|
||||||
user.ConsumableItems[itemId] += count
|
user.ConsumableItems[itemId] += count
|
||||||
}
|
}
|
||||||
@@ -652,22 +737,40 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
consumedCount++
|
consumedCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := s.catalog.EnhanceCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
|
if costFunc, ok := catalog.EnhanceCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
|
||||||
goldCost := costFunc.Evaluate(consumedCount)
|
goldCost := costFunc.Evaluate(consumedCount)
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
|
||||||
log.Printf("[WeaponService] EnhanceByWeapon: gold cost=%d (weapons=%d)", goldCost, consumedCount)
|
log.Printf("[WeaponService] EnhanceByWeapon: gold cost=%d (weapons=%d)", goldCost, consumedCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
weapon.Exp += totalExp
|
weapon.Exp += totalExp
|
||||||
if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
||||||
|
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||||
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
|
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||||
|
if weapon.Level > cap {
|
||||||
|
weapon.Level = cap
|
||||||
|
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
||||||
|
weapon.Exp = thresholds[cap]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
note := user.WeaponNotes[weapon.WeaponId]
|
||||||
|
if note.MaxLevel < weapon.Level {
|
||||||
|
note.WeaponId = weapon.WeaponId
|
||||||
|
note.MaxLevel = weapon.Level
|
||||||
|
note.LatestVersion = nowMillis
|
||||||
|
user.WeaponNotes[weapon.WeaponId] = note
|
||||||
}
|
}
|
||||||
|
|
||||||
weapon.LatestVersion = nowMillis
|
weapon.LatestVersion = nowMillis
|
||||||
user.Weapons[req.UserWeaponUuid] = weapon
|
user.Weapons[req.UserWeaponUuid] = weapon
|
||||||
log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
|
log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
|
||||||
|
|
||||||
s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis)
|
checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("weapon enhance by weapon: %w", err)
|
return nil, fmt.Errorf("weapon enhance by weapon: %w", err)
|
||||||
@@ -679,13 +782,13 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, weaponId, level int32, nowMillis int64) {
|
func checkWeaponStoryUnlocks(catalog *masterdata.WeaponCatalog, user *store.UserState, weaponId, level int32, nowMillis int64) {
|
||||||
wm, ok := s.catalog.Weapons[weaponId]
|
wm, ok := catalog.Weapons[weaponId]
|
||||||
if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 {
|
if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
evoOrder, hasEvo := s.catalog.EvolutionOrder[weaponId]
|
evoOrder, hasEvo := catalog.EvolutionOrder[weaponId]
|
||||||
conditions := s.catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId]
|
conditions := catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId]
|
||||||
|
|
||||||
for _, cond := range conditions {
|
for _, cond := range conditions {
|
||||||
switch model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) {
|
switch model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) {
|
||||||
@@ -696,14 +799,14 @@ func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, wea
|
|||||||
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||||
}
|
}
|
||||||
case model.WeaponStoryReleaseConditionTypeReachInitialMaxLevel:
|
case model.WeaponStoryReleaseConditionTypeReachInitialMaxLevel:
|
||||||
if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
if level >= maxFunc.Evaluate(0) {
|
if level >= maxFunc.Evaluate(0) {
|
||||||
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case model.WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel:
|
case model.WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel:
|
||||||
if hasEvo && evoOrder >= 1 {
|
if hasEvo && evoOrder >= 1 {
|
||||||
if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
if level >= maxFunc.Evaluate(0) {
|
if level >= maxFunc.Evaluate(0) {
|
||||||
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||||
}
|
}
|
||||||
@@ -720,6 +823,9 @@ func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, wea
|
|||||||
func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) {
|
func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) {
|
||||||
log.Printf("[WeaponService] Awaken: uuid=%s", req.UserWeaponUuid)
|
log.Printf("[WeaponService] Awaken: uuid=%s", req.UserWeaponUuid)
|
||||||
|
|
||||||
|
cat := s.holder.Get()
|
||||||
|
catalog := cat.Weapon
|
||||||
|
config := cat.GameConfig
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|
||||||
@@ -730,7 +836,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
awakenRow, ok := s.catalog.AwakenByWeaponId[weapon.WeaponId]
|
awakenRow, ok := catalog.AwakenByWeaponId[weapon.WeaponId]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[WeaponService] Awaken: no awaken data for weaponId=%d", weapon.WeaponId)
|
log.Printf("[WeaponService] Awaken: no awaken data for weaponId=%d", weapon.WeaponId)
|
||||||
return
|
return
|
||||||
@@ -741,7 +847,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mats := s.catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId]
|
mats := catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId]
|
||||||
for _, mat := range mats {
|
for _, mat := range mats {
|
||||||
cur := user.Materials[mat.MaterialId]
|
cur := user.Materials[mat.MaterialId]
|
||||||
cost := mat.Count
|
cost := mat.Count
|
||||||
@@ -753,7 +859,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if awakenRow.ConsumeGold > 0 {
|
if awakenRow.ConsumeGold > 0 {
|
||||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= awakenRow.ConsumeGold
|
user.ConsumableItems[config.ConsumableItemIdForGold] -= awakenRow.ConsumeGold
|
||||||
log.Printf("[WeaponService] Awaken: gold cost=%d", awakenRow.ConsumeGold)
|
log.Printf("[WeaponService] Awaken: gold cost=%d", awakenRow.ConsumeGold)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,3 +878,14 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
|
|||||||
|
|
||||||
return &pb.WeaponAwakenResponse{}, nil
|
return &pb.WeaponAwakenResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func awakenedLevelCap(catalog *masterdata.WeaponCatalog, user *store.UserState, weapon store.WeaponState, weaponUuid string, baseCap int32) int32 {
|
||||||
|
if _, awoken := user.WeaponAwakens[weaponUuid]; !awoken {
|
||||||
|
return baseCap
|
||||||
|
}
|
||||||
|
row, ok := catalog.AwakenByWeaponId[weapon.WeaponId]
|
||||||
|
if !ok {
|
||||||
|
return baseCap
|
||||||
|
}
|
||||||
|
return baseCap + row.LevelLimitUp
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func CloneUserState(u UserState) UserState {
|
|||||||
out.DeckSubWeapons = cloneSliceMap(u.DeckSubWeapons)
|
out.DeckSubWeapons = cloneSliceMap(u.DeckSubWeapons)
|
||||||
out.DeckParts = cloneSliceMap(u.DeckParts)
|
out.DeckParts = cloneSliceMap(u.DeckParts)
|
||||||
out.Decks = maps.Clone(u.Decks)
|
out.Decks = maps.Clone(u.Decks)
|
||||||
|
out.TripleDecks = maps.Clone(u.TripleDecks)
|
||||||
out.Quests = maps.Clone(u.Quests)
|
out.Quests = maps.Clone(u.Quests)
|
||||||
out.QuestMissions = maps.Clone(u.QuestMissions)
|
out.QuestMissions = maps.Clone(u.QuestMissions)
|
||||||
out.WeaponStories = maps.Clone(u.WeaponStories)
|
out.WeaponStories = maps.Clone(u.WeaponStories)
|
||||||
@@ -25,11 +26,15 @@ func CloneUserState(u UserState) UserState {
|
|||||||
Unlocks: maps.Clone(u.Gimmick.Unlocks),
|
Unlocks: maps.Clone(u.Gimmick.Unlocks),
|
||||||
}
|
}
|
||||||
out.CageOrnamentRewards = maps.Clone(u.CageOrnamentRewards)
|
out.CageOrnamentRewards = maps.Clone(u.CageOrnamentRewards)
|
||||||
|
out.TowerAccumulationRewards = maps.Clone(u.TowerAccumulationRewards)
|
||||||
|
out.LabyrinthSeasons = maps.Clone(u.LabyrinthSeasons)
|
||||||
|
out.LabyrinthStages = maps.Clone(u.LabyrinthStages)
|
||||||
out.ConsumableItems = maps.Clone(u.ConsumableItems)
|
out.ConsumableItems = maps.Clone(u.ConsumableItems)
|
||||||
out.Materials = maps.Clone(u.Materials)
|
out.Materials = maps.Clone(u.Materials)
|
||||||
out.Parts = maps.Clone(u.Parts)
|
out.Parts = maps.Clone(u.Parts)
|
||||||
out.PartsGroupNotes = maps.Clone(u.PartsGroupNotes)
|
out.PartsGroupNotes = maps.Clone(u.PartsGroupNotes)
|
||||||
out.PartsPresets = maps.Clone(u.PartsPresets)
|
out.PartsPresets = maps.Clone(u.PartsPresets)
|
||||||
|
out.PartsPresetTags = maps.Clone(u.PartsPresetTags)
|
||||||
out.PartsStatusSubs = maps.Clone(u.PartsStatusSubs)
|
out.PartsStatusSubs = maps.Clone(u.PartsStatusSubs)
|
||||||
out.ImportantItems = maps.Clone(u.ImportantItems)
|
out.ImportantItems = maps.Clone(u.ImportantItems)
|
||||||
out.CostumeActiveSkills = maps.Clone(u.CostumeActiveSkills)
|
out.CostumeActiveSkills = maps.Clone(u.CostumeActiveSkills)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const (
|
|||||||
starterMissionId = int32(1)
|
starterMissionId = int32(1)
|
||||||
starterMainQuestRouteId = int32(1)
|
starterMainQuestRouteId = int32(1)
|
||||||
starterMainQuestSeasonId = int32(1)
|
starterMainQuestSeasonId = int32(1)
|
||||||
missionInProgress = int32(1)
|
|
||||||
|
|
||||||
defaultBirthYear = int32(2000)
|
defaultBirthYear = int32(2000)
|
||||||
defaultBirthMonth = int32(1)
|
defaultBirthMonth = int32(1)
|
||||||
@@ -97,6 +96,7 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
|
|||||||
Companions: make(map[string]CompanionState),
|
Companions: make(map[string]CompanionState),
|
||||||
DeckCharacters: make(map[string]DeckCharacterState),
|
DeckCharacters: make(map[string]DeckCharacterState),
|
||||||
Decks: make(map[DeckKey]DeckState),
|
Decks: make(map[DeckKey]DeckState),
|
||||||
|
TripleDecks: make(map[DeckKey]TripleDeckState),
|
||||||
DeckSubWeapons: make(map[string][]string),
|
DeckSubWeapons: make(map[string][]string),
|
||||||
DeckParts: make(map[string][]string),
|
DeckParts: make(map[string][]string),
|
||||||
Quests: make(map[int32]UserQuestState),
|
Quests: make(map[int32]UserQuestState),
|
||||||
@@ -113,7 +113,7 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
|
|||||||
starterMissionId: {
|
starterMissionId: {
|
||||||
MissionId: starterMissionId,
|
MissionId: starterMissionId,
|
||||||
StartDatetime: nowMillis,
|
StartDatetime: nowMillis,
|
||||||
MissionProgressStatusType: missionInProgress,
|
MissionProgressStatusType: int32(model.MissionProgressStatusTypeInProgress),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Gimmick: GimmickState{
|
Gimmick: GimmickState{
|
||||||
@@ -122,29 +122,33 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
|
|||||||
Sequences: make(map[GimmickSequenceKey]GimmickSequenceState),
|
Sequences: make(map[GimmickSequenceKey]GimmickSequenceState),
|
||||||
Unlocks: make(map[GimmickKey]GimmickUnlockState),
|
Unlocks: make(map[GimmickKey]GimmickUnlockState),
|
||||||
},
|
},
|
||||||
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
|
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
|
||||||
ConsumableItems: make(map[int32]int32),
|
TowerAccumulationRewards: make(map[int32]TowerAccumulationRewardState),
|
||||||
Materials: make(map[int32]int32),
|
LabyrinthSeasons: make(map[int32]LabyrinthSeasonState),
|
||||||
Thoughts: make(map[string]ThoughtState),
|
LabyrinthStages: make(map[LabyrinthStageKey]LabyrinthStageState),
|
||||||
Parts: make(map[string]PartsState),
|
ConsumableItems: make(map[int32]int32),
|
||||||
PartsGroupNotes: make(map[int32]PartsGroupNoteState),
|
Materials: make(map[int32]int32),
|
||||||
PartsPresets: make(map[int32]PartsPresetState),
|
Thoughts: make(map[string]ThoughtState),
|
||||||
PartsStatusSubs: make(map[PartsStatusSubKey]PartsStatusSubState),
|
Parts: make(map[string]PartsState),
|
||||||
ImportantItems: make(map[int32]int32),
|
PartsGroupNotes: make(map[int32]PartsGroupNoteState),
|
||||||
CostumeActiveSkills: make(map[string]CostumeActiveSkillState),
|
PartsPresets: make(map[int32]PartsPresetState),
|
||||||
WeaponSkills: make(map[string][]WeaponSkillState),
|
PartsPresetTags: make(map[int32]PartsPresetTagState),
|
||||||
WeaponAbilities: make(map[string][]WeaponAbilityState),
|
PartsStatusSubs: make(map[PartsStatusSubKey]PartsStatusSubState),
|
||||||
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
|
ImportantItems: make(map[int32]int32),
|
||||||
WeaponNotes: make(map[int32]WeaponNoteState),
|
CostumeActiveSkills: make(map[string]CostumeActiveSkillState),
|
||||||
NaviCutInPlayed: make(map[int32]bool),
|
WeaponSkills: make(map[string][]WeaponSkillState),
|
||||||
ViewedMovies: make(map[int32]int64),
|
WeaponAbilities: make(map[string][]WeaponAbilityState),
|
||||||
ContentsStories: make(map[int32]int64),
|
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
|
||||||
DrawnOmikuji: make(map[int32]int64),
|
WeaponNotes: make(map[int32]WeaponNoteState),
|
||||||
PremiumItems: make(map[int32]int64),
|
NaviCutInPlayed: make(map[int32]bool),
|
||||||
DokanConfirmed: make(map[int32]bool),
|
ViewedMovies: make(map[int32]int64),
|
||||||
ShopItems: make(map[int32]UserShopItemState),
|
ContentsStories: make(map[int32]int64),
|
||||||
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
|
DrawnOmikuji: make(map[int32]int64),
|
||||||
ExploreScores: make(map[int32]ExploreScoreState),
|
PremiumItems: make(map[int32]int64),
|
||||||
|
DokanConfirmed: make(map[int32]bool),
|
||||||
|
ShopItems: make(map[int32]UserShopItemState),
|
||||||
|
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
|
||||||
|
ExploreScores: make(map[int32]ExploreScoreState),
|
||||||
|
|
||||||
CharacterBoards: make(map[int32]CharacterBoardState),
|
CharacterBoards: make(map[int32]CharacterBoardState),
|
||||||
CharacterBoardAbilities: make(map[CharacterBoardAbilityKey]CharacterBoardAbilityState),
|
CharacterBoardAbilities: make(map[CharacterBoardAbilityKey]CharacterBoardAbilityState),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func initMaps(u *store.UserState) {
|
|||||||
u.Thoughts = make(map[string]store.ThoughtState)
|
u.Thoughts = make(map[string]store.ThoughtState)
|
||||||
u.DeckCharacters = make(map[string]store.DeckCharacterState)
|
u.DeckCharacters = make(map[string]store.DeckCharacterState)
|
||||||
u.Decks = make(map[store.DeckKey]store.DeckState)
|
u.Decks = make(map[store.DeckKey]store.DeckState)
|
||||||
|
u.TripleDecks = make(map[store.DeckKey]store.TripleDeckState)
|
||||||
u.DeckSubWeapons = make(map[string][]string)
|
u.DeckSubWeapons = make(map[string][]string)
|
||||||
u.DeckParts = make(map[string][]string)
|
u.DeckParts = make(map[string][]string)
|
||||||
u.Quests = make(map[int32]store.UserQuestState)
|
u.Quests = make(map[int32]store.UserQuestState)
|
||||||
@@ -60,6 +61,7 @@ func initMaps(u *store.UserState) {
|
|||||||
u.Parts = make(map[string]store.PartsState)
|
u.Parts = make(map[string]store.PartsState)
|
||||||
u.PartsGroupNotes = make(map[int32]store.PartsGroupNoteState)
|
u.PartsGroupNotes = make(map[int32]store.PartsGroupNoteState)
|
||||||
u.PartsPresets = make(map[int32]store.PartsPresetState)
|
u.PartsPresets = make(map[int32]store.PartsPresetState)
|
||||||
|
u.PartsPresetTags = make(map[int32]store.PartsPresetTagState)
|
||||||
u.PartsStatusSubs = make(map[store.PartsStatusSubKey]store.PartsStatusSubState)
|
u.PartsStatusSubs = make(map[store.PartsStatusSubKey]store.PartsStatusSubState)
|
||||||
u.DeckTypeNotes = make(map[model.DeckType]store.DeckTypeNoteState)
|
u.DeckTypeNotes = make(map[model.DeckType]store.DeckTypeNoteState)
|
||||||
u.ConsumableItems = make(map[int32]int32)
|
u.ConsumableItems = make(map[int32]int32)
|
||||||
@@ -75,6 +77,9 @@ func initMaps(u *store.UserState) {
|
|||||||
u.ShopReplaceableLineup = make(map[int32]store.UserShopReplaceableLineupState)
|
u.ShopReplaceableLineup = make(map[int32]store.UserShopReplaceableLineupState)
|
||||||
u.ExploreScores = make(map[int32]store.ExploreScoreState)
|
u.ExploreScores = make(map[int32]store.ExploreScoreState)
|
||||||
u.CageOrnamentRewards = make(map[int32]store.CageOrnamentRewardState)
|
u.CageOrnamentRewards = make(map[int32]store.CageOrnamentRewardState)
|
||||||
|
u.TowerAccumulationRewards = make(map[int32]store.TowerAccumulationRewardState)
|
||||||
|
u.LabyrinthSeasons = make(map[int32]store.LabyrinthSeasonState)
|
||||||
|
u.LabyrinthStages = make(map[store.LabyrinthStageKey]store.LabyrinthStageState)
|
||||||
u.CharacterBoards = make(map[int32]store.CharacterBoardState)
|
u.CharacterBoards = make(map[int32]store.CharacterBoardState)
|
||||||
u.CharacterBoardAbilities = make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState)
|
u.CharacterBoardAbilities = make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState)
|
||||||
u.CharacterBoardStatusUps = make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState)
|
u.CharacterBoardStatusUps = make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState)
|
||||||
@@ -124,17 +129,26 @@ func load1to1(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
Scan(&u.LoginBonus.LoginBonusId, &u.LoginBonus.CurrentPageNumber, &u.LoginBonus.CurrentStampNumber,
|
Scan(&u.LoginBonus.LoginBonusId, &u.LoginBonus.CurrentPageNumber, &u.LoginBonus.CurrentStampNumber,
|
||||||
&u.LoginBonus.LatestRewardReceiveDatetime, &u.LoginBonus.LatestVersion)
|
&u.LoginBonus.LatestRewardReceiveDatetime, &u.LoginBonus.LatestVersion)
|
||||||
|
|
||||||
|
var ctxActive, ctxIsLast, ctxCage int
|
||||||
_ = db.QueryRow(`SELECT current_quest_flow_type, current_main_quest_route_id, current_quest_scene_id,
|
_ = db.QueryRow(`SELECT current_quest_flow_type, current_main_quest_route_id, current_quest_scene_id,
|
||||||
head_quest_scene_id, is_reached_last_quest_scene, progress_quest_scene_id, progress_head_quest_scene_id,
|
head_quest_scene_id, is_reached_last_quest_scene, progress_quest_scene_id, progress_head_quest_scene_id,
|
||||||
progress_quest_flow_type, main_quest_season_id, latest_version, saved_current_quest_scene_id,
|
progress_quest_flow_type, main_quest_season_id, latest_version,
|
||||||
saved_head_quest_scene_id, replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id
|
saved_ctx_active, saved_ctx_current_quest_scene_id, saved_ctx_head_quest_scene_id,
|
||||||
|
saved_ctx_current_main_quest_route_id, saved_ctx_main_quest_season_id,
|
||||||
|
saved_ctx_is_reached_last_quest_scene, saved_ctx_portal_cage_in_progress, saved_ctx_current_quest_flow_type,
|
||||||
|
replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id
|
||||||
FROM user_main_quest WHERE user_id=?`, uid).
|
FROM user_main_quest WHERE user_id=?`, uid).
|
||||||
Scan(&u.MainQuest.CurrentQuestFlowType, &u.MainQuest.CurrentMainQuestRouteId, &u.MainQuest.CurrentQuestSceneId,
|
Scan(&u.MainQuest.CurrentQuestFlowType, &u.MainQuest.CurrentMainQuestRouteId, &u.MainQuest.CurrentQuestSceneId,
|
||||||
&u.MainQuest.HeadQuestSceneId, &b, &u.MainQuest.ProgressQuestSceneId, &u.MainQuest.ProgressHeadQuestSceneId,
|
&u.MainQuest.HeadQuestSceneId, &b, &u.MainQuest.ProgressQuestSceneId, &u.MainQuest.ProgressHeadQuestSceneId,
|
||||||
&u.MainQuest.ProgressQuestFlowType, &u.MainQuest.MainQuestSeasonId, &u.MainQuest.LatestVersion,
|
&u.MainQuest.ProgressQuestFlowType, &u.MainQuest.MainQuestSeasonId, &u.MainQuest.LatestVersion,
|
||||||
&u.MainQuest.SavedCurrentQuestSceneId, &u.MainQuest.SavedHeadQuestSceneId,
|
&ctxActive, &u.MainQuest.SavedContext.CurrentQuestSceneId, &u.MainQuest.SavedContext.HeadQuestSceneId,
|
||||||
|
&u.MainQuest.SavedContext.CurrentMainQuestRouteId, &u.MainQuest.SavedContext.MainQuestSeasonId,
|
||||||
|
&ctxIsLast, &ctxCage, &u.MainQuest.SavedContext.CurrentQuestFlowType,
|
||||||
&u.MainQuest.ReplayFlowCurrentQuestSceneId, &u.MainQuest.ReplayFlowHeadQuestSceneId)
|
&u.MainQuest.ReplayFlowCurrentQuestSceneId, &u.MainQuest.ReplayFlowHeadQuestSceneId)
|
||||||
u.MainQuest.IsReachedLastQuestScene = b != 0
|
u.MainQuest.IsReachedLastQuestScene = b != 0
|
||||||
|
u.MainQuest.SavedContext.Active = ctxActive != 0
|
||||||
|
u.MainQuest.SavedContext.IsReachedLastQuestScene = ctxIsLast != 0
|
||||||
|
u.MainQuest.SavedContext.PortalCageInProgress = ctxCage != 0
|
||||||
|
|
||||||
_ = db.QueryRow(`SELECT current_event_quest_chapter_id, current_quest_id, current_quest_scene_id,
|
_ = db.QueryRow(`SELECT current_event_quest_chapter_id, current_quest_id, current_quest_scene_id,
|
||||||
head_quest_scene_id, latest_version FROM user_event_quest WHERE user_id=?`, uid).
|
head_quest_scene_id, latest_version FROM user_event_quest WHERE user_id=?`, uid).
|
||||||
@@ -163,6 +177,13 @@ func load1to1(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
&u.BigHuntBattleDetail.TotalDamage, &u.BigHuntDeckNumber, &u.BigHuntBattleBinary)
|
&u.BigHuntBattleDetail.TotalDamage, &u.BigHuntDeckNumber, &u.BigHuntBattleBinary)
|
||||||
u.BigHuntProgress.IsDryRun = isDryRun != 0
|
u.BigHuntProgress.IsDryRun = isDryRun != 0
|
||||||
|
|
||||||
|
queryRows(db, `SELECT wave_index, costume_id, total_damage, hit_count, random_display_value_type, random_display_value
|
||||||
|
FROM user_big_hunt_costume_battle_infos WHERE user_id=? ORDER BY wave_index, sort_order`, uid, func(rows *sql.Rows) {
|
||||||
|
var ci store.BigHuntCostumeBattleInfo
|
||||||
|
rows.Scan(&ci.WaveIndex, &ci.CostumeId, &ci.TotalDamage, &ci.HitCount, &ci.RandomDisplayValueType, &ci.RandomDisplayValue)
|
||||||
|
u.BigHuntBattleDetail.CostumeBattleInfo = append(u.BigHuntBattleDetail.CostumeBattleInfo, ci)
|
||||||
|
})
|
||||||
|
|
||||||
var isActive, isUnread int
|
var isActive, isUnread int
|
||||||
_ = db.QueryRow(`SELECT is_active, start_count, finish_count, last_started_at, last_finished_at,
|
_ = db.QueryRow(`SELECT is_active, start_count, finish_count, last_started_at, last_finished_at,
|
||||||
last_user_party_count, last_npc_party_count, last_battle_binary_size, last_elapsed_frame_count
|
last_user_party_count, last_npc_party_count, last_battle_binary_size, last_elapsed_frame_count
|
||||||
@@ -282,6 +303,16 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.Decks[store.DeckKey{DeckType: v.DeckType, UserDeckNumber: v.UserDeckNumber}] = v
|
u.Decks[store.DeckKey{DeckType: v.DeckType, UserDeckNumber: v.UserDeckNumber}] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryRows(db, `SELECT deck_type, user_deck_number, name, deck_number01, deck_number02, deck_number03, latest_version
|
||||||
|
FROM user_triple_decks WHERE user_id=?`, uid,
|
||||||
|
func(rows *sql.Rows) {
|
||||||
|
var v store.TripleDeckState
|
||||||
|
var dt int32
|
||||||
|
rows.Scan(&dt, &v.UserDeckNumber, &v.Name, &v.DeckNumber01, &v.DeckNumber02, &v.DeckNumber03, &v.LatestVersion)
|
||||||
|
v.DeckType = model.DeckType(dt)
|
||||||
|
u.TripleDecks[store.DeckKey{DeckType: v.DeckType, UserDeckNumber: v.UserDeckNumber}] = v
|
||||||
|
})
|
||||||
|
|
||||||
queryRows(db, `SELECT user_deck_character_uuid, ordinal, user_weapon_uuid
|
queryRows(db, `SELECT user_deck_character_uuid, ordinal, user_weapon_uuid
|
||||||
FROM user_deck_sub_weapons WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid,
|
FROM user_deck_sub_weapons WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid,
|
||||||
func(rows *sql.Rows) {
|
func(rows *sql.Rows) {
|
||||||
@@ -454,6 +485,14 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.PartsPresets[v.UserPartsPresetNumber] = v
|
u.PartsPresets[v.UserPartsPresetNumber] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryRows(db, `SELECT user_parts_preset_tag_number, name, latest_version
|
||||||
|
FROM user_parts_preset_tags WHERE user_id=?`, uid,
|
||||||
|
func(rows *sql.Rows) {
|
||||||
|
var v store.PartsPresetTagState
|
||||||
|
rows.Scan(&v.UserPartsPresetTagNumber, &v.Name, &v.LatestVersion)
|
||||||
|
u.PartsPresetTags[v.UserPartsPresetTagNumber] = v
|
||||||
|
})
|
||||||
|
|
||||||
queryRows(db, `SELECT user_parts_uuid, status_index, parts_status_sub_lottery_id, level,
|
queryRows(db, `SELECT user_parts_uuid, status_index, parts_status_sub_lottery_id, level,
|
||||||
status_kind_type, status_calculation_type, status_change_value, latest_version
|
status_kind_type, status_calculation_type, status_change_value, latest_version
|
||||||
FROM user_parts_status_subs WHERE user_id=?`, uid,
|
FROM user_parts_status_subs WHERE user_id=?`, uid,
|
||||||
@@ -517,7 +556,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.DokanConfirmed[id] = true
|
u.DokanConfirmed[id] = true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Gifts
|
|
||||||
queryRows(db, `SELECT user_gift_uuid, is_received, possession_type, possession_id, count, grant_datetime,
|
queryRows(db, `SELECT user_gift_uuid, is_received, possession_type, possession_id, count, grant_datetime,
|
||||||
description_gift_text_id, equipment_data, expiration_datetime, received_datetime
|
description_gift_text_id, equipment_data, expiration_datetime, received_datetime
|
||||||
FROM user_gifts WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
FROM user_gifts WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
@@ -540,7 +578,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Gacha converted medals
|
|
||||||
queryRows(db, `SELECT consumable_item_id, count FROM user_gacha_converted_medals WHERE user_id=? ORDER BY ordinal`, uid,
|
queryRows(db, `SELECT consumable_item_id, count FROM user_gacha_converted_medals WHERE user_id=? ORDER BY ordinal`, uid,
|
||||||
func(rows *sql.Rows) {
|
func(rows *sql.Rows) {
|
||||||
var v store.ConsumableItemState
|
var v store.ConsumableItemState
|
||||||
@@ -548,7 +585,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession = append(u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession, v)
|
u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession = append(u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession, v)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Gacha banners
|
|
||||||
queryRows(db, `SELECT gacha_id, medal_count, step_number, loop_count, draw_count, box_number
|
queryRows(db, `SELECT gacha_id, medal_count, step_number, loop_count, draw_count, box_number
|
||||||
FROM user_gacha_banners WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
FROM user_gacha_banners WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var v store.GachaBannerState
|
var v store.GachaBannerState
|
||||||
@@ -566,7 +602,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Character boards
|
|
||||||
queryRows(db, `SELECT character_board_id, panel_release_bit1, panel_release_bit2, panel_release_bit3,
|
queryRows(db, `SELECT character_board_id, panel_release_bit1, panel_release_bit2, panel_release_bit3,
|
||||||
panel_release_bit4, latest_version FROM user_character_boards WHERE user_id=?`, uid,
|
panel_release_bit4, latest_version FROM user_character_boards WHERE user_id=?`, uid,
|
||||||
func(rows *sql.Rows) {
|
func(rows *sql.Rows) {
|
||||||
@@ -608,6 +643,29 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.CageOrnamentRewards[v.CageOrnamentId] = v
|
u.CageOrnamentRewards[v.CageOrnamentId] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryRows(db, `SELECT event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version
|
||||||
|
FROM user_event_quest_tower_accumulation_rewards WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
|
var v store.TowerAccumulationRewardState
|
||||||
|
rows.Scan(&v.EventQuestChapterId, &v.LatestRewardReceiveQuestMissionClearCount, &v.LatestVersion)
|
||||||
|
u.TowerAccumulationRewards[v.EventQuestChapterId] = v
|
||||||
|
})
|
||||||
|
|
||||||
|
queryRows(db, `SELECT event_quest_chapter_id, last_join_season_number, last_season_reward_received_season_number, latest_version
|
||||||
|
FROM user_event_quest_labyrinth_seasons WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
|
var v store.LabyrinthSeasonState
|
||||||
|
rows.Scan(&v.EventQuestChapterId, &v.LastJoinSeasonNumber, &v.LastSeasonRewardReceivedSeasonNumber, &v.LatestVersion)
|
||||||
|
u.LabyrinthSeasons[v.EventQuestChapterId] = v
|
||||||
|
})
|
||||||
|
|
||||||
|
queryRows(db, `SELECT event_quest_chapter_id, stage_order, is_received_stage_clear_reward, accumulation_reward_received_quest_mission_count, latest_version
|
||||||
|
FROM user_event_quest_labyrinth_stages WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
|
var v store.LabyrinthStageState
|
||||||
|
var rcvd int
|
||||||
|
rows.Scan(&v.EventQuestChapterId, &v.StageOrder, &rcvd, &v.AccumulationRewardReceivedQuestMissionCount, &v.LatestVersion)
|
||||||
|
v.IsReceivedStageClearReward = rcvd != 0
|
||||||
|
u.LabyrinthStages[store.LabyrinthStageKey{EventQuestChapterId: v.EventQuestChapterId, StageOrder: v.StageOrder}] = v
|
||||||
|
})
|
||||||
|
|
||||||
queryRows(db, `SELECT shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version
|
queryRows(db, `SELECT shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version
|
||||||
FROM user_shop_items WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
FROM user_shop_items WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var v store.UserShopItemState
|
var v store.UserShopItemState
|
||||||
@@ -622,7 +680,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.ShopReplaceableLineup[v.SlotNumber] = v
|
u.ShopReplaceableLineup[v.SlotNumber] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
// Gimmick tables
|
|
||||||
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
|
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
|
||||||
is_gimmick_cleared, start_datetime, latest_version FROM user_gimmick_progress WHERE user_id=?`, uid,
|
is_gimmick_cleared, start_datetime, latest_version FROM user_gimmick_progress WHERE user_id=?`, uid,
|
||||||
func(rows *sql.Rows) {
|
func(rows *sql.Rows) {
|
||||||
@@ -665,7 +722,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.Gimmick.Unlocks[v.Key] = v
|
u.Gimmick.Unlocks[v.Key] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
// Big hunt maps
|
|
||||||
queryRows(db, `SELECT big_hunt_boss_id, max_score, max_score_update_datetime, latest_version
|
queryRows(db, `SELECT big_hunt_boss_id, max_score, max_score_update_datetime, latest_version
|
||||||
FROM user_big_hunt_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
FROM user_big_hunt_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var id int32
|
var id int32
|
||||||
@@ -674,11 +730,11 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.BigHuntMaxScores[id] = v
|
u.BigHuntMaxScores[id] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
queryRows(db, `SELECT big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, latest_version
|
queryRows(db, `SELECT big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, last_daily_reward_received_day_version, latest_version
|
||||||
FROM user_big_hunt_statuses WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
FROM user_big_hunt_statuses WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var id int32
|
var id int32
|
||||||
var v store.BigHuntStatus
|
var v store.BigHuntStatus
|
||||||
rows.Scan(&id, &v.DailyChallengeCount, &v.LatestChallengeDatetime, &v.LatestVersion)
|
rows.Scan(&id, &v.DailyChallengeCount, &v.LatestChallengeDatetime, &v.LastDailyRewardReceivedDayVersion, &v.LatestVersion)
|
||||||
u.BigHuntStatuses[id] = v
|
u.BigHuntStatuses[id] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ func boolToInt(b bool) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeUserState inserts all child table rows for a newly created user.
|
// Precondition: the users row must already exist.
|
||||||
// The users row must already exist.
|
|
||||||
func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
||||||
exec := func(query string, args ...any) error {
|
exec := func(query string, args ...any) error {
|
||||||
_, err := tx.Exec(query, args...)
|
_, err := tx.Exec(query, args...)
|
||||||
@@ -50,11 +49,17 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
u.LoginBonus.LatestRewardReceiveDatetime, u.LoginBonus.LatestVersion); err != nil {
|
u.LoginBonus.LatestRewardReceiveDatetime, u.LoginBonus.LatestVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := exec(`INSERT INTO user_main_quest (user_id, current_quest_flow_type, current_main_quest_route_id, current_quest_scene_id, head_quest_scene_id, is_reached_last_quest_scene, progress_quest_scene_id, progress_head_quest_scene_id, progress_quest_flow_type, main_quest_season_id, latest_version, saved_current_quest_scene_id, saved_head_quest_scene_id, replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_main_quest (user_id, current_quest_flow_type, current_main_quest_route_id, current_quest_scene_id, head_quest_scene_id, is_reached_last_quest_scene, progress_quest_scene_id, progress_head_quest_scene_id, progress_quest_flow_type, main_quest_season_id, latest_version, saved_ctx_active, saved_ctx_current_quest_scene_id, saved_ctx_head_quest_scene_id, saved_ctx_current_main_quest_route_id, saved_ctx_main_quest_season_id, saved_ctx_is_reached_last_quest_scene, saved_ctx_portal_cage_in_progress, saved_ctx_current_quest_flow_type, replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
uid, u.MainQuest.CurrentQuestFlowType, u.MainQuest.CurrentMainQuestRouteId, u.MainQuest.CurrentQuestSceneId,
|
uid, u.MainQuest.CurrentQuestFlowType, u.MainQuest.CurrentMainQuestRouteId, u.MainQuest.CurrentQuestSceneId,
|
||||||
u.MainQuest.HeadQuestSceneId, boolToInt(u.MainQuest.IsReachedLastQuestScene), u.MainQuest.ProgressQuestSceneId,
|
u.MainQuest.HeadQuestSceneId, boolToInt(u.MainQuest.IsReachedLastQuestScene), u.MainQuest.ProgressQuestSceneId,
|
||||||
u.MainQuest.ProgressHeadQuestSceneId, u.MainQuest.ProgressQuestFlowType, u.MainQuest.MainQuestSeasonId,
|
u.MainQuest.ProgressHeadQuestSceneId, u.MainQuest.ProgressQuestFlowType, u.MainQuest.MainQuestSeasonId,
|
||||||
u.MainQuest.LatestVersion, u.MainQuest.SavedCurrentQuestSceneId, u.MainQuest.SavedHeadQuestSceneId,
|
u.MainQuest.LatestVersion,
|
||||||
|
boolToInt(u.MainQuest.SavedContext.Active),
|
||||||
|
u.MainQuest.SavedContext.CurrentQuestSceneId, u.MainQuest.SavedContext.HeadQuestSceneId,
|
||||||
|
u.MainQuest.SavedContext.CurrentMainQuestRouteId, u.MainQuest.SavedContext.MainQuestSeasonId,
|
||||||
|
boolToInt(u.MainQuest.SavedContext.IsReachedLastQuestScene),
|
||||||
|
boolToInt(u.MainQuest.SavedContext.PortalCageInProgress),
|
||||||
|
u.MainQuest.SavedContext.CurrentQuestFlowType,
|
||||||
u.MainQuest.ReplayFlowCurrentQuestSceneId, u.MainQuest.ReplayFlowHeadQuestSceneId); err != nil {
|
u.MainQuest.ReplayFlowCurrentQuestSceneId, u.MainQuest.ReplayFlowHeadQuestSceneId); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -78,6 +83,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
u.BigHuntBattleDetail.MaxComboCount, u.BigHuntBattleDetail.TotalDamage, u.BigHuntDeckNumber, u.BigHuntBattleBinary); err != nil {
|
u.BigHuntBattleDetail.MaxComboCount, u.BigHuntBattleDetail.TotalDamage, u.BigHuntDeckNumber, u.BigHuntBattleBinary); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for i, ci := range u.BigHuntBattleDetail.CostumeBattleInfo {
|
||||||
|
if err := exec(`INSERT INTO user_big_hunt_costume_battle_infos (user_id, wave_index, sort_order, costume_id, total_damage, hit_count, random_display_value_type, random_display_value) VALUES (?,?,?,?,?,?,?,?)`,
|
||||||
|
uid, ci.WaveIndex, i, ci.CostumeId, ci.TotalDamage, ci.HitCount, ci.RandomDisplayValueType, ci.RandomDisplayValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := exec(`INSERT INTO user_battle (user_id, is_active, start_count, finish_count, last_started_at, last_finished_at, last_user_party_count, last_npc_party_count, last_battle_binary_size, last_elapsed_frame_count) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_battle (user_id, is_active, start_count, finish_count, last_started_at, last_finished_at, last_user_party_count, last_npc_party_count, last_battle_binary_size, last_elapsed_frame_count) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||||
uid, boolToInt(u.Battle.IsActive), u.Battle.StartCount, u.Battle.FinishCount, u.Battle.LastStartedAt,
|
uid, boolToInt(u.Battle.IsActive), u.Battle.StartCount, u.Battle.FinishCount, u.Battle.LastStartedAt,
|
||||||
u.Battle.LastFinishedAt, u.Battle.LastUserPartyCount, u.Battle.LastNpcPartyCount,
|
u.Battle.LastFinishedAt, u.Battle.LastUserPartyCount, u.Battle.LastNpcPartyCount,
|
||||||
@@ -118,7 +129,6 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map tables
|
|
||||||
for _, v := range u.Characters {
|
for _, v := range u.Characters {
|
||||||
if err := exec(`INSERT INTO user_characters (user_id, character_id, level, exp, latest_version) VALUES (?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_characters (user_id, character_id, level, exp, latest_version) VALUES (?,?,?,?,?)`,
|
||||||
uid, v.CharacterId, v.Level, v.Exp, v.LatestVersion); err != nil {
|
uid, v.CharacterId, v.Level, v.Exp, v.LatestVersion); err != nil {
|
||||||
@@ -161,6 +171,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for k, v := range u.TripleDecks {
|
||||||
|
if err := exec(`INSERT INTO user_triple_decks (user_id, deck_type, user_deck_number, name, deck_number01, deck_number02, deck_number03, latest_version) VALUES (?,?,?,?,?,?,?,?)`,
|
||||||
|
uid, int32(k.DeckType), k.UserDeckNumber, v.Name, v.DeckNumber01, v.DeckNumber02, v.DeckNumber03, v.LatestVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for key, uuids := range u.DeckSubWeapons {
|
for key, uuids := range u.DeckSubWeapons {
|
||||||
for i, uuid := range uuids {
|
for i, uuid := range uuids {
|
||||||
if err := exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`,
|
if err := exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`,
|
||||||
@@ -290,6 +306,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, v := range u.PartsPresetTags {
|
||||||
|
if err := exec(`INSERT INTO user_parts_preset_tags (user_id, user_parts_preset_tag_number, name, latest_version) VALUES (?,?,?,?)`,
|
||||||
|
uid, v.UserPartsPresetTagNumber, v.Name, v.LatestVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, v := range u.PartsStatusSubs {
|
for _, v := range u.PartsStatusSubs {
|
||||||
if err := exec(`INSERT INTO user_parts_status_subs (user_id, user_parts_uuid, status_index, parts_status_sub_lottery_id, level, status_kind_type, status_calculation_type, status_change_value, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_parts_status_subs (user_id, user_parts_uuid, status_index, parts_status_sub_lottery_id, level, status_kind_type, status_calculation_type, status_change_value, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`,
|
||||||
uid, v.UserPartsUuid, v.StatusIndex, v.PartsStatusSubLotteryId, v.Level, v.StatusKindType, v.StatusCalculationType, v.StatusChangeValue, v.LatestVersion); err != nil {
|
uid, v.UserPartsUuid, v.StatusIndex, v.PartsStatusSubLotteryId, v.Level, v.StatusKindType, v.StatusCalculationType, v.StatusChangeValue, v.LatestVersion); err != nil {
|
||||||
@@ -426,6 +448,24 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, v := range u.TowerAccumulationRewards {
|
||||||
|
if err := exec(`INSERT INTO user_event_quest_tower_accumulation_rewards (user_id, event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version) VALUES (?,?,?,?)`,
|
||||||
|
uid, v.EventQuestChapterId, v.LatestRewardReceiveQuestMissionClearCount, v.LatestVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range u.LabyrinthSeasons {
|
||||||
|
if err := exec(`INSERT INTO user_event_quest_labyrinth_seasons (user_id, event_quest_chapter_id, last_join_season_number, last_season_reward_received_season_number, latest_version) VALUES (?,?,?,?,?)`,
|
||||||
|
uid, v.EventQuestChapterId, v.LastJoinSeasonNumber, v.LastSeasonRewardReceivedSeasonNumber, v.LatestVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range u.LabyrinthStages {
|
||||||
|
if err := exec(`INSERT INTO user_event_quest_labyrinth_stages (user_id, event_quest_chapter_id, stage_order, is_received_stage_clear_reward, accumulation_reward_received_quest_mission_count, latest_version) VALUES (?,?,?,?,?,?)`,
|
||||||
|
uid, k.EventQuestChapterId, k.StageOrder, boolToInt(v.IsReceivedStageClearReward), v.AccumulationRewardReceivedQuestMissionCount, v.LatestVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, v := range u.ShopItems {
|
for _, v := range u.ShopItems {
|
||||||
if err := exec(`INSERT INTO user_shop_items (user_id, shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version) VALUES (?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_shop_items (user_id, shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version) VALUES (?,?,?,?,?)`,
|
||||||
uid, v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion); err != nil {
|
uid, v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion); err != nil {
|
||||||
@@ -469,8 +509,8 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id, v := range u.BigHuntStatuses {
|
for id, v := range u.BigHuntStatuses {
|
||||||
if err := exec(`INSERT INTO user_big_hunt_statuses (user_id, big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, latest_version) VALUES (?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_big_hunt_statuses (user_id, big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, last_daily_reward_received_day_version, latest_version) VALUES (?,?,?,?,?,?)`,
|
||||||
uid, id, v.DailyChallengeCount, v.LatestChallengeDatetime, v.LatestVersion); err != nil {
|
uid, id, v.DailyChallengeCount, v.LatestChallengeDatetime, v.LastDailyRewardReceivedDayVersion, v.LatestVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,18 +536,13 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// diffAndSave compares before/after UserState and writes only changed rows.
|
// 1:1 tables update on field-change; maps INSERT OR REPLACE + DELETE; slice tables DELETE-all then INSERT-all.
|
||||||
// For 1:1 tables, it UPDATEs if any field changed.
|
|
||||||
// For map tables, it uses INSERT OR REPLACE for added/modified entries and DELETE for removed ones.
|
|
||||||
// For slice-based data (gifts, medals, deck sub-weapons/parts, weapon skills/abilities),
|
|
||||||
// it does DELETE-all then INSERT-all for simplicity.
|
|
||||||
func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||||
exec := func(query string, args ...any) error {
|
exec := func(query string, args ...any) error {
|
||||||
_, err := tx.Exec(query, args...)
|
_, err := tx.Exec(query, args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// users table
|
|
||||||
if before.PlayerId != after.PlayerId || before.OsType != after.OsType || before.PlatformType != after.PlatformType ||
|
if before.PlayerId != after.PlayerId || before.OsType != after.OsType || before.PlatformType != after.PlatformType ||
|
||||||
before.UserRestrictionType != after.UserRestrictionType || before.RegisterDatetime != after.RegisterDatetime ||
|
before.UserRestrictionType != after.UserRestrictionType || before.RegisterDatetime != after.RegisterDatetime ||
|
||||||
before.GameStartDatetime != after.GameStartDatetime || before.LatestVersion != after.LatestVersion ||
|
before.GameStartDatetime != after.GameStartDatetime || before.LatestVersion != after.LatestVersion ||
|
||||||
@@ -562,11 +597,17 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if before.MainQuest != after.MainQuest {
|
if before.MainQuest != after.MainQuest {
|
||||||
if err := exec(`UPDATE user_main_quest SET current_quest_flow_type=?, current_main_quest_route_id=?, current_quest_scene_id=?, head_quest_scene_id=?, is_reached_last_quest_scene=?, progress_quest_scene_id=?, progress_head_quest_scene_id=?, progress_quest_flow_type=?, main_quest_season_id=?, latest_version=?, saved_current_quest_scene_id=?, saved_head_quest_scene_id=?, replay_flow_current_quest_scene_id=?, replay_flow_head_quest_scene_id=? WHERE user_id=?`,
|
if err := exec(`UPDATE user_main_quest SET current_quest_flow_type=?, current_main_quest_route_id=?, current_quest_scene_id=?, head_quest_scene_id=?, is_reached_last_quest_scene=?, progress_quest_scene_id=?, progress_head_quest_scene_id=?, progress_quest_flow_type=?, main_quest_season_id=?, latest_version=?, saved_ctx_active=?, saved_ctx_current_quest_scene_id=?, saved_ctx_head_quest_scene_id=?, saved_ctx_current_main_quest_route_id=?, saved_ctx_main_quest_season_id=?, saved_ctx_is_reached_last_quest_scene=?, saved_ctx_portal_cage_in_progress=?, saved_ctx_current_quest_flow_type=?, replay_flow_current_quest_scene_id=?, replay_flow_head_quest_scene_id=? WHERE user_id=?`,
|
||||||
after.MainQuest.CurrentQuestFlowType, after.MainQuest.CurrentMainQuestRouteId, after.MainQuest.CurrentQuestSceneId,
|
after.MainQuest.CurrentQuestFlowType, after.MainQuest.CurrentMainQuestRouteId, after.MainQuest.CurrentQuestSceneId,
|
||||||
after.MainQuest.HeadQuestSceneId, boolToInt(after.MainQuest.IsReachedLastQuestScene), after.MainQuest.ProgressQuestSceneId,
|
after.MainQuest.HeadQuestSceneId, boolToInt(after.MainQuest.IsReachedLastQuestScene), after.MainQuest.ProgressQuestSceneId,
|
||||||
after.MainQuest.ProgressHeadQuestSceneId, after.MainQuest.ProgressQuestFlowType, after.MainQuest.MainQuestSeasonId,
|
after.MainQuest.ProgressHeadQuestSceneId, after.MainQuest.ProgressQuestFlowType, after.MainQuest.MainQuestSeasonId,
|
||||||
after.MainQuest.LatestVersion, after.MainQuest.SavedCurrentQuestSceneId, after.MainQuest.SavedHeadQuestSceneId,
|
after.MainQuest.LatestVersion,
|
||||||
|
boolToInt(after.MainQuest.SavedContext.Active),
|
||||||
|
after.MainQuest.SavedContext.CurrentQuestSceneId, after.MainQuest.SavedContext.HeadQuestSceneId,
|
||||||
|
after.MainQuest.SavedContext.CurrentMainQuestRouteId, after.MainQuest.SavedContext.MainQuestSeasonId,
|
||||||
|
boolToInt(after.MainQuest.SavedContext.IsReachedLastQuestScene),
|
||||||
|
boolToInt(after.MainQuest.SavedContext.PortalCageInProgress),
|
||||||
|
after.MainQuest.SavedContext.CurrentQuestFlowType,
|
||||||
after.MainQuest.ReplayFlowCurrentQuestSceneId, after.MainQuest.ReplayFlowHeadQuestSceneId, uid); err != nil {
|
after.MainQuest.ReplayFlowCurrentQuestSceneId, after.MainQuest.ReplayFlowHeadQuestSceneId, uid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -589,7 +630,7 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if before.BigHuntProgress != after.BigHuntProgress || before.BigHuntBattleDetail != after.BigHuntBattleDetail || before.BigHuntDeckNumber != after.BigHuntDeckNumber {
|
if before.BigHuntProgress != after.BigHuntProgress || !bigHuntBattleDetailEqual(before.BigHuntBattleDetail, after.BigHuntBattleDetail) || before.BigHuntDeckNumber != after.BigHuntDeckNumber {
|
||||||
if err := exec(`UPDATE user_big_hunt_state SET current_big_hunt_boss_quest_id=?, current_big_hunt_quest_id=?, current_quest_scene_id=?, is_dry_run=?, latest_version=?, deck_type=?, user_triple_deck_number=?, boss_knock_down_count=?, max_combo_count=?, total_damage=?, deck_number=?, battle_binary=? WHERE user_id=?`,
|
if err := exec(`UPDATE user_big_hunt_state SET current_big_hunt_boss_quest_id=?, current_big_hunt_quest_id=?, current_quest_scene_id=?, is_dry_run=?, latest_version=?, deck_type=?, user_triple_deck_number=?, boss_knock_down_count=?, max_combo_count=?, total_damage=?, deck_number=?, battle_binary=? WHERE user_id=?`,
|
||||||
after.BigHuntProgress.CurrentBigHuntBossQuestId, after.BigHuntProgress.CurrentBigHuntQuestId,
|
after.BigHuntProgress.CurrentBigHuntBossQuestId, after.BigHuntProgress.CurrentBigHuntQuestId,
|
||||||
after.BigHuntProgress.CurrentQuestSceneId, boolToInt(after.BigHuntProgress.IsDryRun), after.BigHuntProgress.LatestVersion,
|
after.BigHuntProgress.CurrentQuestSceneId, boolToInt(after.BigHuntProgress.IsDryRun), after.BigHuntProgress.LatestVersion,
|
||||||
@@ -597,6 +638,15 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
after.BigHuntBattleDetail.MaxComboCount, after.BigHuntBattleDetail.TotalDamage, after.BigHuntDeckNumber, after.BigHuntBattleBinary, uid); err != nil {
|
after.BigHuntBattleDetail.MaxComboCount, after.BigHuntBattleDetail.TotalDamage, after.BigHuntDeckNumber, after.BigHuntBattleBinary, uid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := exec(`DELETE FROM user_big_hunt_costume_battle_infos WHERE user_id=?`, uid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, ci := range after.BigHuntBattleDetail.CostumeBattleInfo {
|
||||||
|
if err := exec(`INSERT INTO user_big_hunt_costume_battle_infos (user_id, wave_index, sort_order, costume_id, total_damage, hit_count, random_display_value_type, random_display_value) VALUES (?,?,?,?,?,?,?,?)`,
|
||||||
|
uid, ci.WaveIndex, i, ci.CostumeId, ci.TotalDamage, ci.HitCount, ci.RandomDisplayValueType, ci.RandomDisplayValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if before.Battle != after.Battle {
|
if before.Battle != after.Battle {
|
||||||
if err := exec(`UPDATE user_battle SET is_active=?, start_count=?, finish_count=?, last_started_at=?, last_finished_at=?, last_user_party_count=?, last_npc_party_count=?, last_battle_binary_size=?, last_elapsed_frame_count=? WHERE user_id=?`,
|
if err := exec(`UPDATE user_battle SET is_active=?, start_count=?, finish_count=?, last_started_at=?, last_finished_at=?, last_user_party_count=?, last_npc_party_count=?, last_battle_binary_size=?, last_elapsed_frame_count=? WHERE user_id=?`,
|
||||||
@@ -637,7 +687,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gacha scalar
|
|
||||||
if before.Gacha.RewardAvailable != after.Gacha.RewardAvailable || before.Gacha.TodaysCurrentDrawCount != after.Gacha.TodaysCurrentDrawCount ||
|
if before.Gacha.RewardAvailable != after.Gacha.RewardAvailable || before.Gacha.TodaysCurrentDrawCount != after.Gacha.TodaysCurrentDrawCount ||
|
||||||
before.Gacha.DailyMaxCount != after.Gacha.DailyMaxCount || before.Gacha.LastRewardDrawDate != after.Gacha.LastRewardDrawDate {
|
before.Gacha.DailyMaxCount != after.Gacha.DailyMaxCount || before.Gacha.LastRewardDrawDate != after.Gacha.LastRewardDrawDate {
|
||||||
var obtainItemId, obtainCount sql.NullInt64
|
var obtainItemId, obtainCount sql.NullInt64
|
||||||
@@ -652,7 +701,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map tables — use generic diff helpers
|
|
||||||
diffMapInt32(tx, uid, before.Characters, after.Characters, "user_characters", "character_id",
|
diffMapInt32(tx, uid, before.Characters, after.Characters, "user_characters", "character_id",
|
||||||
func(v store.CharacterState) []any { return []any{v.CharacterId, v.Level, v.Exp, v.LatestVersion} },
|
func(v store.CharacterState) []any { return []any{v.CharacterId, v.Level, v.Exp, v.LatestVersion} },
|
||||||
"character_id, level, exp, latest_version")
|
"character_id, level, exp, latest_version")
|
||||||
@@ -677,7 +725,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return []any{v.UserDeckCharacterUuid, v.UserCostumeUuid, v.MainUserWeaponUuid, v.UserCompanionUuid, v.Power, v.UserThoughtUuid, v.DressupCostumeId, v.LatestVersion}
|
return []any{v.UserDeckCharacterUuid, v.UserCostumeUuid, v.MainUserWeaponUuid, v.UserCompanionUuid, v.Power, v.UserThoughtUuid, v.DressupCostumeId, v.LatestVersion}
|
||||||
}, "user_deck_character_uuid, user_costume_uuid, main_user_weapon_uuid, user_companion_uuid, power, user_thought_uuid, dressup_costume_id, latest_version")
|
}, "user_deck_character_uuid, user_costume_uuid, main_user_weapon_uuid, user_companion_uuid, power, user_thought_uuid, dressup_costume_id, latest_version")
|
||||||
|
|
||||||
// Decks (composite key)
|
|
||||||
for k, v := range after.Decks {
|
for k, v := range after.Decks {
|
||||||
if old, ok := before.Decks[k]; !ok || old != v {
|
if old, ok := before.Decks[k]; !ok || old != v {
|
||||||
exec(fmt.Sprintf(`INSERT OR REPLACE INTO user_decks (user_id, deck_type, user_deck_number, user_deck_character_uuid01, user_deck_character_uuid02, user_deck_character_uuid03, name, power, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`),
|
exec(fmt.Sprintf(`INSERT OR REPLACE INTO user_decks (user_id, deck_type, user_deck_number, user_deck_character_uuid01, user_deck_character_uuid02, user_deck_character_uuid03, name, power, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`),
|
||||||
@@ -690,7 +737,18 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slice-based tables: delete all + reinsert
|
for k, v := range after.TripleDecks {
|
||||||
|
if old, ok := before.TripleDecks[k]; !ok || old != v {
|
||||||
|
exec(`INSERT OR REPLACE INTO user_triple_decks (user_id, deck_type, user_deck_number, name, deck_number01, deck_number02, deck_number03, latest_version) VALUES (?,?,?,?,?,?,?,?)`,
|
||||||
|
uid, int32(k.DeckType), k.UserDeckNumber, v.Name, v.DeckNumber01, v.DeckNumber02, v.DeckNumber03, v.LatestVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k := range before.TripleDecks {
|
||||||
|
if _, ok := after.TripleDecks[k]; !ok {
|
||||||
|
exec(`DELETE FROM user_triple_decks WHERE user_id=? AND deck_type=? AND user_deck_number=?`, uid, int32(k.DeckType), k.UserDeckNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
replaceSliceTable(tx, uid, "user_deck_sub_weapons", after.DeckSubWeapons, func(key string, uuids []string) {
|
replaceSliceTable(tx, uid, "user_deck_sub_weapons", after.DeckSubWeapons, func(key string, uuids []string) {
|
||||||
for i, uuid := range uuids {
|
for i, uuid := range uuids {
|
||||||
exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`, uid, key, i, uuid)
|
exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`, uid, key, i, uuid)
|
||||||
@@ -707,7 +765,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return []any{v.QuestId, int32(v.QuestStateType), boolToInt(v.IsBattleOnly), v.UserDeckNumber, v.LatestStartDatetime, v.ClearCount, v.DailyClearCount, v.LastClearDatetime, v.ShortestClearFrames, boolToInt(v.IsRewardGranted), v.LatestVersion}
|
return []any{v.QuestId, int32(v.QuestStateType), boolToInt(v.IsBattleOnly), v.UserDeckNumber, v.LatestStartDatetime, v.ClearCount, v.DailyClearCount, v.LastClearDatetime, v.ShortestClearFrames, boolToInt(v.IsRewardGranted), v.LatestVersion}
|
||||||
}, "quest_id, quest_state_type, is_battle_only, user_deck_number, latest_start_datetime, clear_count, daily_clear_count, last_clear_datetime, shortest_clear_frames, is_reward_granted, latest_version")
|
}, "quest_id, quest_state_type, is_battle_only, user_deck_number, latest_start_datetime, clear_count, daily_clear_count, last_clear_datetime, shortest_clear_frames, is_reward_granted, latest_version")
|
||||||
|
|
||||||
// Quest missions (composite key)
|
|
||||||
for k, v := range after.QuestMissions {
|
for k, v := range after.QuestMissions {
|
||||||
if old, ok := before.QuestMissions[k]; !ok || old != v {
|
if old, ok := before.QuestMissions[k]; !ok || old != v {
|
||||||
exec(`INSERT OR REPLACE INTO user_quest_missions (user_id, quest_id, quest_mission_id, progress_value, is_clear, latest_clear_datetime, latest_version) VALUES (?,?,?,?,?,?,?)`,
|
exec(`INSERT OR REPLACE INTO user_quest_missions (user_id, quest_id, quest_mission_id, progress_value, is_clear, latest_clear_datetime, latest_version) VALUES (?,?,?,?,?,?,?)`,
|
||||||
@@ -734,6 +791,7 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
func(v store.SideStoryQuestProgress) []any {
|
func(v store.SideStoryQuestProgress) []any {
|
||||||
return []any{0, v.HeadSideStoryQuestSceneId, int32(v.SideStoryQuestStateType), v.LatestVersion}
|
return []any{0, v.HeadSideStoryQuestSceneId, int32(v.SideStoryQuestStateType), v.LatestVersion}
|
||||||
}, "side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version")
|
}, "side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version")
|
||||||
|
|
||||||
diffMapInt32(tx, uid, before.QuestLimitContentStatus, after.QuestLimitContentStatus, "user_quest_limit_content_status", "limit_content_id",
|
diffMapInt32(tx, uid, before.QuestLimitContentStatus, after.QuestLimitContentStatus, "user_quest_limit_content_status", "limit_content_id",
|
||||||
func(v store.QuestLimitContentStatus) []any {
|
func(v store.QuestLimitContentStatus) []any {
|
||||||
return []any{0, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion}
|
return []any{0, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion}
|
||||||
@@ -748,7 +806,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return []any{v.WeaponId, v.MaxLevel, v.MaxLimitBreakCount, v.FirstAcquisitionDatetime, v.LatestVersion}
|
return []any{v.WeaponId, v.MaxLevel, v.MaxLimitBreakCount, v.FirstAcquisitionDatetime, v.LatestVersion}
|
||||||
}, "weapon_id, max_level, max_limit_break_count, first_acquisition_datetime, latest_version")
|
}, "weapon_id, max_level, max_limit_break_count, first_acquisition_datetime, latest_version")
|
||||||
|
|
||||||
// Weapon skills/abilities: slice-based, delete+reinsert
|
|
||||||
exec(`DELETE FROM user_weapon_skills WHERE user_id=?`, uid)
|
exec(`DELETE FROM user_weapon_skills WHERE user_id=?`, uid)
|
||||||
for _, skills := range after.WeaponSkills {
|
for _, skills := range after.WeaponSkills {
|
||||||
for _, v := range skills {
|
for _, v := range skills {
|
||||||
@@ -770,7 +827,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return []any{v.UserCostumeUuid, v.Level, v.AcquisitionDatetime, v.LatestVersion}
|
return []any{v.UserCostumeUuid, v.Level, v.AcquisitionDatetime, v.LatestVersion}
|
||||||
}, "user_costume_uuid, level, acquisition_datetime, latest_version")
|
}, "user_costume_uuid, level, acquisition_datetime, latest_version")
|
||||||
|
|
||||||
// Costume awaken status ups (composite key)
|
|
||||||
for k, v := range after.CostumeAwakenStatusUps {
|
for k, v := range after.CostumeAwakenStatusUps {
|
||||||
if old, ok := before.CostumeAwakenStatusUps[k]; !ok || old != v {
|
if old, ok := before.CostumeAwakenStatusUps[k]; !ok || old != v {
|
||||||
exec(`INSERT OR REPLACE INTO user_costume_awaken_status_ups (user_id, user_costume_uuid, status_calculation_type, hp, attack, vitality, agility, critical_ratio, critical_attack, latest_version) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
exec(`INSERT OR REPLACE INTO user_costume_awaken_status_ups (user_id, user_costume_uuid, status_calculation_type, hp, attack, vitality, agility, critical_ratio, critical_attack, latest_version) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||||
@@ -813,6 +869,10 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
func(v store.PartsPresetState) []any {
|
func(v store.PartsPresetState) []any {
|
||||||
return []any{v.UserPartsPresetNumber, v.UserPartsUuid01, v.UserPartsUuid02, v.UserPartsUuid03, v.Name, v.UserPartsPresetTagNumber, v.LatestVersion}
|
return []any{v.UserPartsPresetNumber, v.UserPartsUuid01, v.UserPartsUuid02, v.UserPartsUuid03, v.Name, v.UserPartsPresetTagNumber, v.LatestVersion}
|
||||||
}, "user_parts_preset_number, user_parts_uuid01, user_parts_uuid02, user_parts_uuid03, name, user_parts_preset_tag_number, latest_version")
|
}, "user_parts_preset_number, user_parts_uuid01, user_parts_uuid02, user_parts_uuid03, name, user_parts_preset_tag_number, latest_version")
|
||||||
|
diffMapInt32(tx, uid, before.PartsPresetTags, after.PartsPresetTags, "user_parts_preset_tags", "user_parts_preset_tag_number",
|
||||||
|
func(v store.PartsPresetTagState) []any {
|
||||||
|
return []any{v.UserPartsPresetTagNumber, v.Name, v.LatestVersion}
|
||||||
|
}, "user_parts_preset_tag_number, name, latest_version")
|
||||||
|
|
||||||
for k, v := range after.PartsStatusSubs {
|
for k, v := range after.PartsStatusSubs {
|
||||||
if old, ok := before.PartsStatusSubs[k]; !ok || old != v {
|
if old, ok := before.PartsStatusSubs[k]; !ok || old != v {
|
||||||
@@ -826,7 +886,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deck type notes (key is model.DeckType which is int32-based)
|
|
||||||
for k, v := range after.DeckTypeNotes {
|
for k, v := range after.DeckTypeNotes {
|
||||||
if old, ok := before.DeckTypeNotes[k]; !ok || old != v {
|
if old, ok := before.DeckTypeNotes[k]; !ok || old != v {
|
||||||
exec(`INSERT OR REPLACE INTO user_deck_type_notes (user_id, deck_type, max_deck_power, latest_version) VALUES (?,?,?,?)`,
|
exec(`INSERT OR REPLACE INTO user_deck_type_notes (user_id, deck_type, max_deck_power, latest_version) VALUES (?,?,?,?)`,
|
||||||
@@ -860,7 +919,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
diffTimestampMap(tx, uid, before.DrawnOmikuji, after.DrawnOmikuji, "user_drawn_omikuji", "omikuji_id")
|
diffTimestampMap(tx, uid, before.DrawnOmikuji, after.DrawnOmikuji, "user_drawn_omikuji", "omikuji_id")
|
||||||
diffBoolMap(tx, uid, before.DokanConfirmed, after.DokanConfirmed, "user_dokan_confirmed", "dokan_id")
|
diffBoolMap(tx, uid, before.DokanConfirmed, after.DokanConfirmed, "user_dokan_confirmed", "dokan_id")
|
||||||
|
|
||||||
// Gifts: delete all + reinsert
|
|
||||||
exec(`DELETE FROM user_gifts WHERE user_id=?`, uid)
|
exec(`DELETE FROM user_gifts WHERE user_id=?`, uid)
|
||||||
for _, g := range after.Gifts.NotReceived {
|
for _, g := range after.Gifts.NotReceived {
|
||||||
var expDt sql.NullInt64
|
var expDt sql.NullInt64
|
||||||
@@ -876,19 +934,16 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
uid, uuid, g.GiftCommon.PossessionType, g.GiftCommon.PossessionId, g.GiftCommon.Count, g.GiftCommon.GrantDatetime, g.GiftCommon.DescriptionGiftTextId, g.GiftCommon.EquipmentData, g.ReceivedDatetime)
|
uid, uuid, g.GiftCommon.PossessionType, g.GiftCommon.PossessionId, g.GiftCommon.Count, g.GiftCommon.GrantDatetime, g.GiftCommon.DescriptionGiftTextId, g.GiftCommon.EquipmentData, g.ReceivedDatetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gacha converted medals: delete+reinsert
|
|
||||||
exec(`DELETE FROM user_gacha_converted_medals WHERE user_id=?`, uid)
|
exec(`DELETE FROM user_gacha_converted_medals WHERE user_id=?`, uid)
|
||||||
for i, v := range after.Gacha.ConvertedGachaMedal.ConvertedMedalPossession {
|
for i, v := range after.Gacha.ConvertedGachaMedal.ConvertedMedalPossession {
|
||||||
exec(`INSERT INTO user_gacha_converted_medals (user_id, ordinal, consumable_item_id, count) VALUES (?,?,?,?)`, uid, i, v.ConsumableItemId, v.Count)
|
exec(`INSERT INTO user_gacha_converted_medals (user_id, ordinal, consumable_item_id, count) VALUES (?,?,?,?)`, uid, i, v.ConsumableItemId, v.Count)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gacha banners
|
|
||||||
for id, v := range after.Gacha.BannerStates {
|
for id, v := range after.Gacha.BannerStates {
|
||||||
if old, ok := before.Gacha.BannerStates[id]; !ok || old.MedalCount != v.MedalCount || old.StepNumber != v.StepNumber || old.LoopCount != v.LoopCount || old.DrawCount != v.DrawCount || old.BoxNumber != v.BoxNumber {
|
if old, ok := before.Gacha.BannerStates[id]; !ok || old.MedalCount != v.MedalCount || old.StepNumber != v.StepNumber || old.LoopCount != v.LoopCount || old.DrawCount != v.DrawCount || old.BoxNumber != v.BoxNumber {
|
||||||
exec(`INSERT OR REPLACE INTO user_gacha_banners (user_id, gacha_id, medal_count, step_number, loop_count, draw_count, box_number) VALUES (?,?,?,?,?,?,?)`,
|
exec(`INSERT OR REPLACE INTO user_gacha_banners (user_id, gacha_id, medal_count, step_number, loop_count, draw_count, box_number) VALUES (?,?,?,?,?,?,?)`,
|
||||||
uid, v.GachaId, v.MedalCount, v.StepNumber, v.LoopCount, v.DrawCount, v.BoxNumber)
|
uid, v.GachaId, v.MedalCount, v.StepNumber, v.LoopCount, v.DrawCount, v.BoxNumber)
|
||||||
}
|
}
|
||||||
// Box drew counts: always delete+reinsert for this gacha
|
|
||||||
exec(`DELETE FROM user_gacha_banner_box_drew_counts WHERE user_id=? AND gacha_id=?`, uid, id)
|
exec(`DELETE FROM user_gacha_banner_box_drew_counts WHERE user_id=? AND gacha_id=?`, uid, id)
|
||||||
for itemId, count := range v.BoxDrewCounts {
|
for itemId, count := range v.BoxDrewCounts {
|
||||||
exec(`INSERT INTO user_gacha_banner_box_drew_counts (user_id, gacha_id, box_item_id, count) VALUES (?,?,?,?)`, uid, id, itemId, count)
|
exec(`INSERT INTO user_gacha_banner_box_drew_counts (user_id, gacha_id, box_item_id, count) VALUES (?,?,?,?)`, uid, id, itemId, count)
|
||||||
@@ -906,7 +961,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return []any{v.CharacterBoardId, v.PanelReleaseBit1, v.PanelReleaseBit2, v.PanelReleaseBit3, v.PanelReleaseBit4, v.LatestVersion}
|
return []any{v.CharacterBoardId, v.PanelReleaseBit1, v.PanelReleaseBit2, v.PanelReleaseBit3, v.PanelReleaseBit4, v.LatestVersion}
|
||||||
}, "character_board_id, panel_release_bit1, panel_release_bit2, panel_release_bit3, panel_release_bit4, latest_version")
|
}, "character_board_id, panel_release_bit1, panel_release_bit2, panel_release_bit3, panel_release_bit4, latest_version")
|
||||||
|
|
||||||
// Character board abilities (composite key)
|
|
||||||
for k, v := range after.CharacterBoardAbilities {
|
for k, v := range after.CharacterBoardAbilities {
|
||||||
if old, ok := before.CharacterBoardAbilities[k]; !ok || old != v {
|
if old, ok := before.CharacterBoardAbilities[k]; !ok || old != v {
|
||||||
exec(`INSERT OR REPLACE INTO user_character_board_abilities (user_id, character_id, ability_id, level, latest_version) VALUES (?,?,?,?,?)`,
|
exec(`INSERT OR REPLACE INTO user_character_board_abilities (user_id, character_id, ability_id, level, latest_version) VALUES (?,?,?,?,?)`,
|
||||||
@@ -919,7 +973,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character board status ups (composite key)
|
|
||||||
for k, v := range after.CharacterBoardStatusUps {
|
for k, v := range after.CharacterBoardStatusUps {
|
||||||
if old, ok := before.CharacterBoardStatusUps[k]; !ok || old != v {
|
if old, ok := before.CharacterBoardStatusUps[k]; !ok || old != v {
|
||||||
exec(`INSERT OR REPLACE INTO user_character_board_status_ups (user_id, character_id, status_calculation_type, hp, attack, vitality, agility, critical_ratio, critical_attack, latest_version) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
exec(`INSERT OR REPLACE INTO user_character_board_status_ups (user_id, character_id, status_calculation_type, hp, attack, vitality, agility, critical_ratio, critical_attack, latest_version) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||||
@@ -942,6 +995,27 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return []any{v.CageOrnamentId, v.AcquisitionDatetime, v.LatestVersion}
|
return []any{v.CageOrnamentId, v.AcquisitionDatetime, v.LatestVersion}
|
||||||
},
|
},
|
||||||
"cage_ornament_id, acquisition_datetime, latest_version")
|
"cage_ornament_id, acquisition_datetime, latest_version")
|
||||||
|
diffMapInt32(tx, uid, before.TowerAccumulationRewards, after.TowerAccumulationRewards, "user_event_quest_tower_accumulation_rewards", "event_quest_chapter_id",
|
||||||
|
func(v store.TowerAccumulationRewardState) []any {
|
||||||
|
return []any{v.EventQuestChapterId, v.LatestRewardReceiveQuestMissionClearCount, v.LatestVersion}
|
||||||
|
},
|
||||||
|
"event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version")
|
||||||
|
diffMapInt32(tx, uid, before.LabyrinthSeasons, after.LabyrinthSeasons, "user_event_quest_labyrinth_seasons", "event_quest_chapter_id",
|
||||||
|
func(v store.LabyrinthSeasonState) []any {
|
||||||
|
return []any{v.EventQuestChapterId, v.LastJoinSeasonNumber, v.LastSeasonRewardReceivedSeasonNumber, v.LatestVersion}
|
||||||
|
},
|
||||||
|
"event_quest_chapter_id, last_join_season_number, last_season_reward_received_season_number, latest_version")
|
||||||
|
for k, v := range after.LabyrinthStages {
|
||||||
|
if old, ok := before.LabyrinthStages[k]; !ok || old != v {
|
||||||
|
exec(`INSERT OR REPLACE INTO user_event_quest_labyrinth_stages (user_id, event_quest_chapter_id, stage_order, is_received_stage_clear_reward, accumulation_reward_received_quest_mission_count, latest_version) VALUES (?,?,?,?,?,?)`,
|
||||||
|
uid, k.EventQuestChapterId, k.StageOrder, boolToInt(v.IsReceivedStageClearReward), v.AccumulationRewardReceivedQuestMissionCount, v.LatestVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k := range before.LabyrinthStages {
|
||||||
|
if _, ok := after.LabyrinthStages[k]; !ok {
|
||||||
|
exec(`DELETE FROM user_event_quest_labyrinth_stages WHERE user_id=? AND event_quest_chapter_id=? AND stage_order=?`, uid, k.EventQuestChapterId, k.StageOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
diffMapInt32(tx, uid, before.ShopItems, after.ShopItems, "user_shop_items", "shop_item_id",
|
diffMapInt32(tx, uid, before.ShopItems, after.ShopItems, "user_shop_items", "shop_item_id",
|
||||||
func(v store.UserShopItemState) []any {
|
func(v store.UserShopItemState) []any {
|
||||||
return []any{v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion}
|
return []any{v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion}
|
||||||
@@ -952,7 +1026,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
},
|
},
|
||||||
"slot_number, shop_item_id, latest_version")
|
"slot_number, shop_item_id, latest_version")
|
||||||
|
|
||||||
// Gimmick tables (composite keys)
|
|
||||||
for k, v := range after.Gimmick.Progress {
|
for k, v := range after.Gimmick.Progress {
|
||||||
if old, ok := before.Gimmick.Progress[k]; !ok || old != v {
|
if old, ok := before.Gimmick.Progress[k]; !ok || old != v {
|
||||||
exec(`INSERT OR REPLACE INTO user_gimmick_progress (user_id, gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id, is_gimmick_cleared, start_datetime, latest_version) VALUES (?,?,?,?,?,?,?)`,
|
exec(`INSERT OR REPLACE INTO user_gimmick_progress (user_id, gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id, is_gimmick_cleared, start_datetime, latest_version) VALUES (?,?,?,?,?,?,?)`,
|
||||||
@@ -1002,7 +1075,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Big hunt maps
|
|
||||||
diffMapInt32(tx, uid, before.BigHuntMaxScores, after.BigHuntMaxScores, "user_big_hunt_max_scores", "big_hunt_boss_id",
|
diffMapInt32(tx, uid, before.BigHuntMaxScores, after.BigHuntMaxScores, "user_big_hunt_max_scores", "big_hunt_boss_id",
|
||||||
func(v store.BigHuntMaxScore) []any {
|
func(v store.BigHuntMaxScore) []any {
|
||||||
return []any{0, v.MaxScore, v.MaxScoreUpdateDatetime, v.LatestVersion}
|
return []any{0, v.MaxScore, v.MaxScoreUpdateDatetime, v.LatestVersion}
|
||||||
@@ -1010,9 +1082,9 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
"big_hunt_boss_id, max_score, max_score_update_datetime, latest_version")
|
"big_hunt_boss_id, max_score, max_score_update_datetime, latest_version")
|
||||||
diffMapInt32(tx, uid, before.BigHuntStatuses, after.BigHuntStatuses, "user_big_hunt_statuses", "big_hunt_boss_id",
|
diffMapInt32(tx, uid, before.BigHuntStatuses, after.BigHuntStatuses, "user_big_hunt_statuses", "big_hunt_boss_id",
|
||||||
func(v store.BigHuntStatus) []any {
|
func(v store.BigHuntStatus) []any {
|
||||||
return []any{0, v.DailyChallengeCount, v.LatestChallengeDatetime, v.LatestVersion}
|
return []any{0, v.DailyChallengeCount, v.LatestChallengeDatetime, v.LastDailyRewardReceivedDayVersion, v.LatestVersion}
|
||||||
},
|
},
|
||||||
"big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, latest_version")
|
"big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, last_daily_reward_received_day_version, latest_version")
|
||||||
|
|
||||||
for k, v := range after.BigHuntScheduleMaxScores {
|
for k, v := range after.BigHuntScheduleMaxScores {
|
||||||
if old, ok := before.BigHuntScheduleMaxScores[k]; !ok || old != v {
|
if old, ok := before.BigHuntScheduleMaxScores[k]; !ok || old != v {
|
||||||
@@ -1051,7 +1123,23 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic diff helpers for map tables with int32 keys
|
func bigHuntBattleDetailEqual(a, b store.BigHuntBattleDetail) bool {
|
||||||
|
if a.DeckType != b.DeckType || a.UserTripleDeckNumber != b.UserTripleDeckNumber ||
|
||||||
|
a.BossKnockDownCount != b.BossKnockDownCount || a.MaxComboCount != b.MaxComboCount ||
|
||||||
|
a.TotalDamage != b.TotalDamage {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(a.CostumeBattleInfo) != len(b.CostumeBattleInfo) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a.CostumeBattleInfo {
|
||||||
|
if a.CostumeBattleInfo[i] != b.CostumeBattleInfo[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func diffMapInt32[V comparable](tx *sql.Tx, uid int64, before, after map[int32]V, table, keyCol string, vals func(V) []any, cols string) {
|
func diffMapInt32[V comparable](tx *sql.Tx, uid int64, before, after map[int32]V, table, keyCol string, vals func(V) []any, cols string) {
|
||||||
for k, v := range after {
|
for k, v := range after {
|
||||||
if old, ok := before[k]; !ok || old != v {
|
if old, ok := before[k]; !ok || old != v {
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ func (s *SQLiteStore) CreateUser(uuid string, platform model.ClientPlatform) (in
|
|||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
var existingId int64
|
|
||||||
err = tx.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&existingId)
|
|
||||||
if err == nil {
|
|
||||||
return existingId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nowMillis := s.clock().UnixMilli()
|
nowMillis := s.clock().UnixMilli()
|
||||||
|
|
||||||
res, err := tx.Exec(`INSERT INTO users (uuid, player_id, os_type, platform_type, user_restriction_type,
|
res, err := tx.Exec(`INSERT INTO users (uuid, player_id, os_type, platform_type, user_restriction_type,
|
||||||
@@ -85,6 +79,9 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
|
|||||||
|
|
||||||
// Child tables in reverse-dependency order (matches schema's goose Down).
|
// Child tables in reverse-dependency order (matches schema's goose Down).
|
||||||
childTables := []string{
|
childTables := []string{
|
||||||
|
"user_event_quest_labyrinth_stages",
|
||||||
|
"user_event_quest_labyrinth_seasons",
|
||||||
|
"user_event_quest_tower_accumulation_rewards",
|
||||||
"user_cage_ornament_rewards",
|
"user_cage_ornament_rewards",
|
||||||
"user_shop_replaceable_lineup",
|
"user_shop_replaceable_lineup",
|
||||||
"user_shop_items",
|
"user_shop_items",
|
||||||
@@ -115,6 +112,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
|
|||||||
"user_big_hunt_max_scores",
|
"user_big_hunt_max_scores",
|
||||||
"user_quest_limit_content_status",
|
"user_quest_limit_content_status",
|
||||||
"user_side_story_quests",
|
"user_side_story_quests",
|
||||||
|
"user_main_quest_season_routes",
|
||||||
"user_missions",
|
"user_missions",
|
||||||
"user_quest_missions",
|
"user_quest_missions",
|
||||||
"user_quests",
|
"user_quests",
|
||||||
@@ -124,6 +122,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
|
|||||||
"user_decks",
|
"user_decks",
|
||||||
"user_deck_characters",
|
"user_deck_characters",
|
||||||
"user_parts_status_subs",
|
"user_parts_status_subs",
|
||||||
|
"user_parts_preset_tags",
|
||||||
"user_parts_presets",
|
"user_parts_presets",
|
||||||
"user_parts_group_notes",
|
"user_parts_group_notes",
|
||||||
"user_parts",
|
"user_parts",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import "log"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
const StaminaRecoveryDivisor int64 = 180
|
const StaminaRecoveryDivisor int64 = 180
|
||||||
|
|
||||||
@@ -39,3 +43,14 @@ func ReplenishStamina(user *UserState, maxStaminaMillis int32, nowMillis int64)
|
|||||||
user.Status.StaminaUpdateDatetime = nowMillis
|
user.Status.StaminaUpdateDatetime = nowMillis
|
||||||
log.Printf("[ReplenishStamina] set to %d", maxStaminaMillis)
|
log.Printf("[ReplenishStamina] set to %d", maxStaminaMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ResolveStaminaEffectMillis(effectValueType, effectValue, maxStaminaMillis int32) int32 {
|
||||||
|
switch effectValueType {
|
||||||
|
case model.EffectValueFixed:
|
||||||
|
return effectValue * 1000
|
||||||
|
case model.EffectValuePermil:
|
||||||
|
return effectValue * maxStaminaMillis / 1000
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+131
-44
@@ -62,45 +62,50 @@ type UserState struct {
|
|||||||
Gacha GachaState
|
Gacha GachaState
|
||||||
Notifications NotificationState
|
Notifications NotificationState
|
||||||
|
|
||||||
Characters map[int32]CharacterState
|
Characters map[int32]CharacterState
|
||||||
Costumes map[string]CostumeState
|
Costumes map[string]CostumeState
|
||||||
Weapons map[string]WeaponState
|
Weapons map[string]WeaponState
|
||||||
Companions map[string]CompanionState
|
Companions map[string]CompanionState
|
||||||
Thoughts map[string]ThoughtState
|
Thoughts map[string]ThoughtState
|
||||||
DeckCharacters map[string]DeckCharacterState
|
DeckCharacters map[string]DeckCharacterState
|
||||||
Decks map[DeckKey]DeckState
|
Decks map[DeckKey]DeckState
|
||||||
Quests map[int32]UserQuestState
|
TripleDecks map[DeckKey]TripleDeckState
|
||||||
QuestMissions map[QuestMissionKey]UserQuestMissionState
|
Quests map[int32]UserQuestState
|
||||||
Missions map[int32]UserMissionState
|
QuestMissions map[QuestMissionKey]UserQuestMissionState
|
||||||
WeaponStories map[int32]WeaponStoryState
|
Missions map[int32]UserMissionState
|
||||||
Gimmick GimmickState
|
WeaponStories map[int32]WeaponStoryState
|
||||||
CageOrnamentRewards map[int32]CageOrnamentRewardState
|
Gimmick GimmickState
|
||||||
ConsumableItems map[int32]int32
|
CageOrnamentRewards map[int32]CageOrnamentRewardState
|
||||||
Materials map[int32]int32
|
TowerAccumulationRewards map[int32]TowerAccumulationRewardState
|
||||||
Parts map[string]PartsState
|
LabyrinthSeasons map[int32]LabyrinthSeasonState
|
||||||
PartsGroupNotes map[int32]PartsGroupNoteState
|
LabyrinthStages map[LabyrinthStageKey]LabyrinthStageState
|
||||||
PartsPresets map[int32]PartsPresetState
|
ConsumableItems map[int32]int32
|
||||||
PartsStatusSubs map[PartsStatusSubKey]PartsStatusSubState
|
Materials map[int32]int32
|
||||||
ImportantItems map[int32]int32
|
Parts map[string]PartsState
|
||||||
CostumeActiveSkills map[string]CostumeActiveSkillState
|
PartsGroupNotes map[int32]PartsGroupNoteState
|
||||||
WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid
|
PartsPresets map[int32]PartsPresetState
|
||||||
WeaponAbilities map[string][]WeaponAbilityState // key: userWeaponUuid
|
PartsPresetTags map[int32]PartsPresetTagState
|
||||||
WeaponAwakens map[string]WeaponAwakenState // key: userWeaponUuid
|
PartsStatusSubs map[PartsStatusSubKey]PartsStatusSubState
|
||||||
DeckTypeNotes map[model.DeckType]DeckTypeNoteState
|
ImportantItems map[int32]int32
|
||||||
WeaponNotes map[int32]WeaponNoteState
|
CostumeActiveSkills map[string]CostumeActiveSkillState
|
||||||
DeckSubWeapons map[string][]string
|
WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid
|
||||||
DeckParts map[string][]string
|
WeaponAbilities map[string][]WeaponAbilityState // key: userWeaponUuid
|
||||||
NaviCutInPlayed map[int32]bool
|
WeaponAwakens map[string]WeaponAwakenState // key: userWeaponUuid
|
||||||
ViewedMovies map[int32]int64
|
DeckTypeNotes map[model.DeckType]DeckTypeNoteState
|
||||||
ContentsStories map[int32]int64
|
WeaponNotes map[int32]WeaponNoteState
|
||||||
DrawnOmikuji map[int32]int64
|
DeckSubWeapons map[string][]string
|
||||||
PremiumItems map[int32]int64
|
DeckParts map[string][]string
|
||||||
DokanConfirmed map[int32]bool
|
NaviCutInPlayed map[int32]bool
|
||||||
PortalCageStatus PortalCageStatusState
|
ViewedMovies map[int32]int64
|
||||||
GuerrillaFreeOpen GuerrillaFreeOpenState
|
ContentsStories map[int32]int64
|
||||||
ShopItems map[int32]UserShopItemState
|
DrawnOmikuji map[int32]int64
|
||||||
ShopReplaceable UserShopReplaceableState
|
PremiumItems map[int32]int64
|
||||||
ShopReplaceableLineup map[int32]UserShopReplaceableLineupState
|
DokanConfirmed map[int32]bool
|
||||||
|
PortalCageStatus PortalCageStatusState
|
||||||
|
GuerrillaFreeOpen GuerrillaFreeOpenState
|
||||||
|
ShopItems map[int32]UserShopItemState
|
||||||
|
ShopReplaceable UserShopReplaceableState
|
||||||
|
ShopReplaceableLineup map[int32]UserShopReplaceableLineupState
|
||||||
|
|
||||||
Explore ExploreState
|
Explore ExploreState
|
||||||
ExploreScores map[int32]ExploreScoreState
|
ExploreScores map[int32]ExploreScoreState
|
||||||
@@ -141,6 +146,9 @@ func (u *UserState) EnsureMaps() {
|
|||||||
if u.Decks == nil {
|
if u.Decks == nil {
|
||||||
u.Decks = make(map[DeckKey]DeckState)
|
u.Decks = make(map[DeckKey]DeckState)
|
||||||
}
|
}
|
||||||
|
if u.TripleDecks == nil {
|
||||||
|
u.TripleDecks = make(map[DeckKey]TripleDeckState)
|
||||||
|
}
|
||||||
if u.DeckSubWeapons == nil {
|
if u.DeckSubWeapons == nil {
|
||||||
u.DeckSubWeapons = make(map[string][]string)
|
u.DeckSubWeapons = make(map[string][]string)
|
||||||
}
|
}
|
||||||
@@ -183,6 +191,15 @@ func (u *UserState) EnsureMaps() {
|
|||||||
if u.CageOrnamentRewards == nil {
|
if u.CageOrnamentRewards == nil {
|
||||||
u.CageOrnamentRewards = make(map[int32]CageOrnamentRewardState)
|
u.CageOrnamentRewards = make(map[int32]CageOrnamentRewardState)
|
||||||
}
|
}
|
||||||
|
if u.TowerAccumulationRewards == nil {
|
||||||
|
u.TowerAccumulationRewards = make(map[int32]TowerAccumulationRewardState)
|
||||||
|
}
|
||||||
|
if u.LabyrinthSeasons == nil {
|
||||||
|
u.LabyrinthSeasons = make(map[int32]LabyrinthSeasonState)
|
||||||
|
}
|
||||||
|
if u.LabyrinthStages == nil {
|
||||||
|
u.LabyrinthStages = make(map[LabyrinthStageKey]LabyrinthStageState)
|
||||||
|
}
|
||||||
if u.ConsumableItems == nil {
|
if u.ConsumableItems == nil {
|
||||||
u.ConsumableItems = make(map[int32]int32)
|
u.ConsumableItems = make(map[int32]int32)
|
||||||
}
|
}
|
||||||
@@ -198,6 +215,9 @@ func (u *UserState) EnsureMaps() {
|
|||||||
if u.PartsPresets == nil {
|
if u.PartsPresets == nil {
|
||||||
u.PartsPresets = make(map[int32]PartsPresetState)
|
u.PartsPresets = make(map[int32]PartsPresetState)
|
||||||
}
|
}
|
||||||
|
if u.PartsPresetTags == nil {
|
||||||
|
u.PartsPresetTags = make(map[int32]PartsPresetTagState)
|
||||||
|
}
|
||||||
if u.PartsStatusSubs == nil {
|
if u.PartsStatusSubs == nil {
|
||||||
u.PartsStatusSubs = make(map[PartsStatusSubKey]PartsStatusSubState)
|
u.PartsStatusSubs = make(map[PartsStatusSubKey]PartsStatusSubState)
|
||||||
}
|
}
|
||||||
@@ -451,6 +471,16 @@ type DeckState struct {
|
|||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TripleDeckState struct {
|
||||||
|
DeckType model.DeckType
|
||||||
|
UserDeckNumber int32
|
||||||
|
Name string
|
||||||
|
DeckNumber01 int32
|
||||||
|
DeckNumber02 int32
|
||||||
|
DeckNumber03 int32
|
||||||
|
LatestVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
type DeckCharacterInput struct {
|
type DeckCharacterInput struct {
|
||||||
UserCostumeUuid string
|
UserCostumeUuid string
|
||||||
MainUserWeaponUuid string
|
MainUserWeaponUuid string
|
||||||
@@ -510,12 +540,25 @@ type MainQuestState struct {
|
|||||||
MainQuestSeasonId int32
|
MainQuestSeasonId int32
|
||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
|
|
||||||
SavedCurrentQuestSceneId int32
|
SavedContext SavedQuestContext
|
||||||
SavedHeadQuestSceneId int32
|
|
||||||
ReplayFlowCurrentQuestSceneId int32
|
ReplayFlowCurrentQuestSceneId int32
|
||||||
ReplayFlowHeadQuestSceneId int32
|
ReplayFlowHeadQuestSceneId int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavedQuestContext snapshots player state when entering a menu-replay (cleared
|
||||||
|
// quest started from the Main Quest List menu). On finish, every field is
|
||||||
|
// restored atomically so the player returns to the exact pre-replay state.
|
||||||
|
type SavedQuestContext struct {
|
||||||
|
Active bool
|
||||||
|
CurrentQuestSceneId int32
|
||||||
|
HeadQuestSceneId int32
|
||||||
|
CurrentMainQuestRouteId int32
|
||||||
|
MainQuestSeasonId int32
|
||||||
|
IsReachedLastQuestScene bool
|
||||||
|
PortalCageInProgress bool
|
||||||
|
CurrentQuestFlowType int32
|
||||||
|
}
|
||||||
|
|
||||||
type EventQuestState struct {
|
type EventQuestState struct {
|
||||||
CurrentEventQuestChapterId int32
|
CurrentEventQuestChapterId int32
|
||||||
CurrentQuestId int32
|
CurrentQuestId int32
|
||||||
@@ -564,9 +607,10 @@ type BigHuntMaxScore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BigHuntStatus struct {
|
type BigHuntStatus struct {
|
||||||
DailyChallengeCount int32
|
DailyChallengeCount int32
|
||||||
LatestChallengeDatetime int64
|
LatestChallengeDatetime int64
|
||||||
LatestVersion int64
|
LastDailyRewardReceivedDayVersion int64
|
||||||
|
LatestVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type BigHuntScheduleScoreKey struct {
|
type BigHuntScheduleScoreKey struct {
|
||||||
@@ -629,6 +673,16 @@ type BigHuntBattleDetail struct {
|
|||||||
BossKnockDownCount int32
|
BossKnockDownCount int32
|
||||||
MaxComboCount int32
|
MaxComboCount int32
|
||||||
TotalDamage int64
|
TotalDamage int64
|
||||||
|
CostumeBattleInfo []BigHuntCostumeBattleInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type BigHuntCostumeBattleInfo struct {
|
||||||
|
WaveIndex int32
|
||||||
|
CostumeId int32
|
||||||
|
TotalDamage int64
|
||||||
|
HitCount int32
|
||||||
|
RandomDisplayValueType int32
|
||||||
|
RandomDisplayValue int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type BattleState struct {
|
type BattleState struct {
|
||||||
@@ -811,6 +865,33 @@ type CageOrnamentRewardState struct {
|
|||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TowerAccumulationRewardState struct {
|
||||||
|
EventQuestChapterId int32
|
||||||
|
LatestRewardReceiveQuestMissionClearCount int32
|
||||||
|
LatestVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabyrinthSeasonState struct {
|
||||||
|
EventQuestChapterId int32
|
||||||
|
LastJoinSeasonNumber int32
|
||||||
|
LastSeasonRewardReceivedSeasonNumber int32
|
||||||
|
LatestVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabyrinthStageKey is the composite key for UserState.LabyrinthStages.
|
||||||
|
type LabyrinthStageKey struct {
|
||||||
|
EventQuestChapterId int32
|
||||||
|
StageOrder int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabyrinthStageState struct {
|
||||||
|
EventQuestChapterId int32
|
||||||
|
StageOrder int32
|
||||||
|
IsReceivedStageClearReward bool
|
||||||
|
AccumulationRewardReceivedQuestMissionCount int32
|
||||||
|
LatestVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
type PartsState struct {
|
type PartsState struct {
|
||||||
UserPartsUuid string
|
UserPartsUuid string
|
||||||
PartsId int32
|
PartsId int32
|
||||||
@@ -837,6 +918,12 @@ type PartsPresetState struct {
|
|||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PartsPresetTagState struct {
|
||||||
|
UserPartsPresetTagNumber int32
|
||||||
|
Name string
|
||||||
|
LatestVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
type PartsStatusSubKey struct {
|
type PartsStatusSubKey struct {
|
||||||
UserPartsUuid string
|
UserPartsUuid string
|
||||||
StatusIndex int32
|
StatusIndex int32
|
||||||
|
|||||||
@@ -100,8 +100,11 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
add("IUserMainQuestFlowStatus")
|
add("IUserMainQuestFlowStatus")
|
||||||
add("IUserMainQuestMainFlowStatus")
|
add("IUserMainQuestMainFlowStatus")
|
||||||
add("IUserMainQuestProgressStatus")
|
add("IUserMainQuestProgressStatus")
|
||||||
add("IUserMainQuestSeasonRoute")
|
|
||||||
add("IUserMainQuestReplayFlowStatus")
|
add("IUserMainQuestReplayFlowStatus")
|
||||||
|
// IUserMainQuestSeasonRoute is derived from MainQuest + Quests at projection
|
||||||
|
// time (see proj_quest.go / questflow.QuestHandler.SeasonRoutesFor) — flag it
|
||||||
|
// whenever either of those upstream inputs changes.
|
||||||
|
add("IUserMainQuestSeasonRoute")
|
||||||
}
|
}
|
||||||
if before.EventQuest != after.EventQuest {
|
if before.EventQuest != after.EventQuest {
|
||||||
add("IUserEventQuestProgressStatus")
|
add("IUserEventQuestProgressStatus")
|
||||||
@@ -161,6 +164,9 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
if !mapsEqualStruct(before.PartsPresets, after.PartsPresets) {
|
if !mapsEqualStruct(before.PartsPresets, after.PartsPresets) {
|
||||||
add("IUserPartsPreset")
|
add("IUserPartsPreset")
|
||||||
}
|
}
|
||||||
|
if !mapsEqualStruct(before.PartsPresetTags, after.PartsPresetTags) {
|
||||||
|
add("IUserPartsPresetTag")
|
||||||
|
}
|
||||||
if !mapsEqualStruct(before.PartsStatusSubs, after.PartsStatusSubs) {
|
if !mapsEqualStruct(before.PartsStatusSubs, after.PartsStatusSubs) {
|
||||||
add("IUserPartsStatusSub")
|
add("IUserPartsStatusSub")
|
||||||
}
|
}
|
||||||
@@ -186,6 +192,9 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
if !mapsEqualStruct(before.Decks, after.Decks) {
|
if !mapsEqualStruct(before.Decks, after.Decks) {
|
||||||
add("IUserDeck")
|
add("IUserDeck")
|
||||||
}
|
}
|
||||||
|
if !mapsEqualStruct(before.TripleDecks, after.TripleDecks) {
|
||||||
|
add("IUserTripleDeck")
|
||||||
|
}
|
||||||
if !mapsEqualSliceValues(before.DeckSubWeapons, after.DeckSubWeapons) {
|
if !mapsEqualSliceValues(before.DeckSubWeapons, after.DeckSubWeapons) {
|
||||||
add("IUserDeckSubWeaponGroup")
|
add("IUserDeckSubWeaponGroup")
|
||||||
}
|
}
|
||||||
@@ -194,6 +203,7 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.Quests, after.Quests) {
|
if !mapsEqualStruct(before.Quests, after.Quests) {
|
||||||
add("IUserQuest")
|
add("IUserQuest")
|
||||||
|
add("IUserMainQuestSeasonRoute")
|
||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) {
|
if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) {
|
||||||
add("IUserQuestMission")
|
add("IUserQuestMission")
|
||||||
@@ -255,6 +265,12 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
if !mapsEqualStruct(before.CageOrnamentRewards, after.CageOrnamentRewards) {
|
if !mapsEqualStruct(before.CageOrnamentRewards, after.CageOrnamentRewards) {
|
||||||
add("IUserCageOrnamentReward")
|
add("IUserCageOrnamentReward")
|
||||||
}
|
}
|
||||||
|
if !mapsEqualStruct(before.TowerAccumulationRewards, after.TowerAccumulationRewards) {
|
||||||
|
add("IUserEventQuestTowerAccumulationReward")
|
||||||
|
}
|
||||||
|
if !mapsEqualStruct(before.LabyrinthStages, after.LabyrinthStages) {
|
||||||
|
add("IUserEventQuestLabyrinthStage")
|
||||||
|
}
|
||||||
|
|
||||||
if !mapsEqualStruct(before.BigHuntMaxScores, after.BigHuntMaxScores) {
|
if !mapsEqualStruct(before.BigHuntMaxScores, after.BigHuntMaxScores) {
|
||||||
add("IUserBigHuntMaxScore")
|
add("IUserBigHuntMaxScore")
|
||||||
@@ -273,10 +289,12 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !gimmickStateEqual(before.Gimmick, after.Gimmick) {
|
if !gimmickStateEqual(before.Gimmick, after.Gimmick) {
|
||||||
if !mapsEqualStruct(before.Gimmick.Progress, after.Gimmick.Progress) {
|
if !mapsEqualStruct(before.Gimmick.Progress, after.Gimmick.Progress) ||
|
||||||
|
!mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
||||||
add("IUserGimmick")
|
add("IUserGimmick")
|
||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.Gimmick.OrnamentProgress, after.Gimmick.OrnamentProgress) {
|
if !mapsEqualStruct(before.Gimmick.OrnamentProgress, after.Gimmick.OrnamentProgress) ||
|
||||||
|
!mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
||||||
add("IUserGimmickOrnamentProgress")
|
add("IUserGimmickOrnamentProgress")
|
||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
if !mapsEqualStruct(before.Gimmick.Sequences, after.Gimmick.Sequences) {
|
||||||
@@ -294,17 +312,16 @@ func ComputeDelta(before, after *store.UserState, changedTables []string) map[st
|
|||||||
diff := make(map[string]*pb.DiffData, len(changedTables))
|
diff := make(map[string]*pb.DiffData, len(changedTables))
|
||||||
for _, table := range changedTables {
|
for _, table := range changedTables {
|
||||||
afterJSON := projectTable(table, *after)
|
afterJSON := projectTable(table, *after)
|
||||||
|
updates := afterJSON
|
||||||
deleteKeys := "[]"
|
deleteKeys := "[]"
|
||||||
if kf := keyFieldsForTable(table); len(kf) > 0 {
|
if kf := keyFieldsForTable(table); len(kf) > 0 {
|
||||||
beforeJSON := projectTable(table, *before)
|
beforeRecs := parseJSONRecords(projectTable(table, *before))
|
||||||
deleteKeys = ComputeDeleteKeys(
|
afterRecs := parseJSONRecords(afterJSON)
|
||||||
parseJSONRecords(beforeJSON),
|
updates = ComputeUpdateRecords(beforeRecs, afterRecs, kf)
|
||||||
parseJSONRecords(afterJSON),
|
deleteKeys = ComputeDeleteKeys(beforeRecs, afterRecs, kf)
|
||||||
kf,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
diff[table] = &pb.DiffData{
|
diff[table] = &pb.DiffData{
|
||||||
UpdateRecordsJson: afterJSON,
|
UpdateRecordsJson: updates,
|
||||||
DeleteKeysJson: deleteKeys,
|
DeleteKeysJson: deleteKeys,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,6 +374,8 @@ func keyFieldsForTable(table string) []string {
|
|||||||
return []string{"userId", "userDeckCharacterUuid"}
|
return []string{"userId", "userDeckCharacterUuid"}
|
||||||
case "IUserDeck":
|
case "IUserDeck":
|
||||||
return []string{"userId", "deckType", "userDeckNumber"}
|
return []string{"userId", "deckType", "userDeckNumber"}
|
||||||
|
case "IUserTripleDeck":
|
||||||
|
return []string{"userId", "deckType", "userDeckNumber"}
|
||||||
case "IUserDeckSubWeaponGroup":
|
case "IUserDeckSubWeaponGroup":
|
||||||
return []string{"userId", "userDeckCharacterUuid", "sortOrder"}
|
return []string{"userId", "userDeckCharacterUuid", "sortOrder"}
|
||||||
case "IUserDeckPartsGroup":
|
case "IUserDeckPartsGroup":
|
||||||
@@ -413,8 +432,14 @@ func keyFieldsForTable(table string) []string {
|
|||||||
return []string{"userId", "partsGroupId"}
|
return []string{"userId", "partsGroupId"}
|
||||||
case "IUserPartsPreset":
|
case "IUserPartsPreset":
|
||||||
return []string{"userId", "userPartsPresetNumber"}
|
return []string{"userId", "userPartsPresetNumber"}
|
||||||
|
case "IUserPartsPresetTag":
|
||||||
|
return []string{"userId", "userPartsPresetTagNumber"}
|
||||||
case "IUserCageOrnamentReward":
|
case "IUserCageOrnamentReward":
|
||||||
return []string{"userId", "cageOrnamentId"}
|
return []string{"userId", "cageOrnamentId"}
|
||||||
|
case "IUserEventQuestTowerAccumulationReward":
|
||||||
|
return []string{"userId", "eventQuestChapterId"}
|
||||||
|
case "IUserEventQuestLabyrinthStage":
|
||||||
|
return []string{"userId", "eventQuestChapterId", "stageOrder"}
|
||||||
case "IUserAutoSaleSettingDetail":
|
case "IUserAutoSaleSettingDetail":
|
||||||
return []string{"userId", "possessionAutoSaleItemType"}
|
return []string{"userId", "possessionAutoSaleItemType"}
|
||||||
case "IUserCharacterRebirth":
|
case "IUserCharacterRebirth":
|
||||||
@@ -435,6 +460,8 @@ func keyFieldsForTable(table string) []string {
|
|||||||
return []string{"userId", "dokanId"}
|
return []string{"userId", "dokanId"}
|
||||||
case "IUserSideStoryQuest":
|
case "IUserSideStoryQuest":
|
||||||
return []string{"userId", "sideStoryQuestId"}
|
return []string{"userId", "sideStoryQuestId"}
|
||||||
|
case "IUserMainQuestSeasonRoute":
|
||||||
|
return []string{"userId", "mainQuestSeasonId", "mainQuestRouteId"}
|
||||||
case "IUserQuestLimitContentStatus":
|
case "IUserQuestLimitContentStatus":
|
||||||
return []string{"userId", "questId"}
|
return []string{"userId", "questId"}
|
||||||
case "IUserBigHuntMaxScore":
|
case "IUserBigHuntMaxScore":
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package userdata
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
@@ -120,6 +121,34 @@ func ComputeDeleteKeys(oldRecords, newRecords []map[string]any, keyFields []stri
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ComputeUpdateRecords(oldRecords, newRecords []map[string]any, keyFields []string) string {
|
||||||
|
if len(newRecords) == 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
|
||||||
|
oldByKey := make(map[string]map[string]any, len(oldRecords))
|
||||||
|
for _, r := range oldRecords {
|
||||||
|
oldByKey[compositeKey(r, keyFields)] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed []map[string]any
|
||||||
|
for _, r := range newRecords {
|
||||||
|
prev, exists := oldByKey[compositeKey(r, keyFields)]
|
||||||
|
if !exists || !reflect.DeepEqual(prev, r) {
|
||||||
|
changed = append(changed, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(changed) == 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(changed)
|
||||||
|
if err != nil {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
func compositeKey(record map[string]any, fields []string) string {
|
func compositeKey(record map[string]any, fields []string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, f := range fields {
|
for i, f := range fields {
|
||||||
|
|||||||
@@ -57,11 +57,12 @@ func init() {
|
|||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
st := user.BigHuntStatuses[int32(id)]
|
st := user.BigHuntStatuses[int32(id)]
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"bigHuntBossQuestId": int32(id),
|
"bigHuntBossQuestId": int32(id),
|
||||||
"dailyChallengeCount": st.DailyChallengeCount,
|
"dailyChallengeCount": st.DailyChallengeCount,
|
||||||
"latestChallengeDatetime": st.LatestChallengeDatetime,
|
"latestChallengeDatetime": st.LatestChallengeDatetime,
|
||||||
"latestVersion": st.LatestVersion,
|
"lastDailyRewardReceivedDayVersion": st.LastDailyRewardReceivedDayVersion,
|
||||||
|
"latestVersion": st.LatestVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
s, _ := utils.EncodeJSONMaps(records...)
|
s, _ := utils.EncodeJSONMaps(records...)
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ func init() {
|
|||||||
s, _ := utils.EncodeJSONMaps(sortedDeckRecords(user)...)
|
s, _ := utils.EncodeJSONMaps(sortedDeckRecords(user)...)
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
register("IUserTripleDeck", func(user store.UserState) string {
|
||||||
|
s, _ := utils.EncodeJSONMaps(sortedTripleDeckRecords(user)...)
|
||||||
|
return s
|
||||||
|
})
|
||||||
register("IUserDeckCharacter", func(user store.UserState) string {
|
register("IUserDeckCharacter", func(user store.UserState) string {
|
||||||
s, _ := utils.EncodeJSONMaps(sortedDeckCharacterRecords(user)...)
|
s, _ := utils.EncodeJSONMaps(sortedDeckCharacterRecords(user)...)
|
||||||
return s
|
return s
|
||||||
@@ -68,6 +72,35 @@ func sortedDeckRecords(user store.UserState) []map[string]any {
|
|||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortedTripleDeckRecords(user store.UserState) []map[string]any {
|
||||||
|
keys := make([]store.DeckKey, 0, len(user.TripleDecks))
|
||||||
|
for key := range user.TripleDecks {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
if keys[i].DeckType != keys[j].DeckType {
|
||||||
|
return keys[i].DeckType < keys[j].DeckType
|
||||||
|
}
|
||||||
|
return keys[i].UserDeckNumber < keys[j].UserDeckNumber
|
||||||
|
})
|
||||||
|
|
||||||
|
records := make([]map[string]any, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
row := user.TripleDecks[key]
|
||||||
|
records = append(records, map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"deckType": int32(row.DeckType),
|
||||||
|
"userDeckNumber": row.UserDeckNumber,
|
||||||
|
"name": row.Name,
|
||||||
|
"deckNumber01": row.DeckNumber01,
|
||||||
|
"deckNumber02": row.DeckNumber02,
|
||||||
|
"deckNumber03": row.DeckNumber03,
|
||||||
|
"latestVersion": row.LatestVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
func sortedDeckCharacterRecords(user store.UserState) []map[string]any {
|
func sortedDeckCharacterRecords(user store.UserState) []map[string]any {
|
||||||
keys := sortedStringKeys(user.DeckCharacters)
|
keys := sortedStringKeys(user.DeckCharacters)
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
|
|||||||
@@ -2,11 +2,21 @@ package userdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var gimmickOrnamentRefs = sync.OnceValue(masterdata.LoadGimmickOrnamentRefs)
|
||||||
|
var gimmickSequenceChains = sync.OnceValue(masterdata.LoadGimmickSequenceChains)
|
||||||
|
var hiddenSequenceSet = sync.OnceValue(masterdata.LoadHiddenGimmickSequenceIDs)
|
||||||
|
var gimmickSequenceRanks = sync.OnceValue(masterdata.LoadGimmickSequenceRanks)
|
||||||
|
var birdGimmicks = sync.OnceValue(masterdata.LoadBirdGimmickIDs)
|
||||||
|
|
||||||
|
const birdDefaultBaseDatetime int64 = 1577836800000 // 2020-01-01 00:00:00 UTC in ms
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
register("IUserGimmick", func(user store.UserState) string {
|
register("IUserGimmick", func(user store.UserState) string {
|
||||||
s, _ := utils.EncodeJSONMaps(sortedGimmickRecords(user)...)
|
s, _ := utils.EncodeJSONMaps(sortedGimmickRecords(user)...)
|
||||||
@@ -26,9 +36,65 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func projectActiveChainOrnaments(
|
||||||
|
user store.UserState,
|
||||||
|
addKey func(seqKey store.GimmickSequenceKey, seqId int32, ref masterdata.GimmickOrnamentRef),
|
||||||
|
sizeFn func() int,
|
||||||
|
cap int,
|
||||||
|
) {
|
||||||
|
refs := gimmickOrnamentRefs()
|
||||||
|
chains := gimmickSequenceChains()
|
||||||
|
hiddenSeq := hiddenSequenceSet()
|
||||||
|
|
||||||
|
walkChain := func(seqKey store.GimmickSequenceKey) {
|
||||||
|
chain := chains[seqKey.GimmickSequenceId]
|
||||||
|
if len(chain) == 0 {
|
||||||
|
chain = []int32{seqKey.GimmickSequenceId}
|
||||||
|
}
|
||||||
|
for _, seqId := range chain {
|
||||||
|
for _, ref := range refs[seqId] {
|
||||||
|
addKey(seqKey, seqId, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonHidden []store.GimmickSequenceKey
|
||||||
|
for seqKey := range user.Gimmick.Sequences {
|
||||||
|
if hiddenSeq[seqKey.GimmickSequenceId] {
|
||||||
|
walkChain(seqKey)
|
||||||
|
} else {
|
||||||
|
nonHidden = append(nonHidden, seqKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, seqKey := range nonHidden {
|
||||||
|
if sizeFn() >= cap {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
walkChain(seqKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sortedGimmickRecords(user store.UserState) []map[string]any {
|
func sortedGimmickRecords(user store.UserState) []map[string]any {
|
||||||
keys := make([]store.GimmickKey, 0, len(user.Gimmick.Progress))
|
|
||||||
|
keySet := make(map[store.GimmickKey]struct{})
|
||||||
|
// Real progress rows (genuine user data) — always kept.
|
||||||
for key := range user.Gimmick.Progress {
|
for key := range user.Gimmick.Progress {
|
||||||
|
keySet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
projectActiveChainOrnaments(user,
|
||||||
|
func(seqKey store.GimmickSequenceKey, seqId int32, ref masterdata.GimmickOrnamentRef) {
|
||||||
|
keySet[store.GimmickKey{
|
||||||
|
GimmickSequenceScheduleId: seqKey.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId: seqId,
|
||||||
|
GimmickId: ref.GimmickId,
|
||||||
|
}] = struct{}{}
|
||||||
|
},
|
||||||
|
func() int { return len(keySet) },
|
||||||
|
masterdata.MaxUserGimmickRows,
|
||||||
|
)
|
||||||
|
|
||||||
|
keys := make([]store.GimmickKey, 0, len(keySet))
|
||||||
|
for key := range keySet {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
@@ -37,57 +103,103 @@ func sortedGimmickRecords(user store.UserState) []map[string]any {
|
|||||||
|
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
row := user.Gimmick.Progress[key]
|
isGimmickCleared := false
|
||||||
|
startDatetime := user.GameStartDatetime
|
||||||
|
latestVersion := user.GameStartDatetime
|
||||||
|
if row, ok := user.Gimmick.Progress[key]; ok {
|
||||||
|
isGimmickCleared = row.IsGimmickCleared
|
||||||
|
startDatetime = row.StartDatetime
|
||||||
|
latestVersion = row.LatestVersion
|
||||||
|
}
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
|
"gimmickSequenceScheduleId": key.GimmickSequenceScheduleId,
|
||||||
"gimmickSequenceId": row.Key.GimmickSequenceId,
|
"gimmickSequenceId": key.GimmickSequenceId,
|
||||||
"gimmickId": row.Key.GimmickId,
|
"gimmickId": key.GimmickId,
|
||||||
"isGimmickCleared": row.IsGimmickCleared,
|
"isGimmickCleared": isGimmickCleared,
|
||||||
"startDatetime": row.StartDatetime,
|
"startDatetime": startDatetime,
|
||||||
"latestVersion": row.LatestVersion,
|
"latestVersion": latestVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedGimmickOrnamentProgressRecords(user store.UserState) []map[string]any {
|
func sortedGimmickOrnamentProgressRecords(user store.UserState) []map[string]any {
|
||||||
keys := make([]store.GimmickOrnamentKey, 0, len(user.Gimmick.OrnamentProgress))
|
|
||||||
|
keySet := make(map[store.GimmickOrnamentKey]struct{})
|
||||||
|
// Real progress rows (genuine user data) — always kept.
|
||||||
for key := range user.Gimmick.OrnamentProgress {
|
for key := range user.Gimmick.OrnamentProgress {
|
||||||
|
keySet[key] = struct{}{}
|
||||||
|
}
|
||||||
|
projectActiveChainOrnaments(user,
|
||||||
|
func(seqKey store.GimmickSequenceKey, seqId int32, ref masterdata.GimmickOrnamentRef) {
|
||||||
|
keySet[store.GimmickOrnamentKey{
|
||||||
|
GimmickSequenceScheduleId: seqKey.GimmickSequenceScheduleId,
|
||||||
|
GimmickSequenceId: seqId,
|
||||||
|
GimmickId: ref.GimmickId,
|
||||||
|
GimmickOrnamentIndex: ref.OrnamentIndex,
|
||||||
|
}] = struct{}{}
|
||||||
|
},
|
||||||
|
func() int { return len(keySet) },
|
||||||
|
masterdata.MaxUserGimmickRows,
|
||||||
|
)
|
||||||
|
|
||||||
|
keys := make([]store.GimmickOrnamentKey, 0, len(keySet))
|
||||||
|
for key := range keySet {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
return compareGimmickOrnamentKey(keys[i], keys[j]) < 0
|
return compareGimmickOrnamentKey(keys[i], keys[j]) < 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
birdG := birdGimmicks()
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
row := user.Gimmick.OrnamentProgress[key]
|
progressValueBit := int32(0)
|
||||||
|
baseDatetime := user.GameStartDatetime
|
||||||
|
latestVersion := user.GameStartDatetime
|
||||||
|
if row, ok := user.Gimmick.OrnamentProgress[key]; ok {
|
||||||
|
progressValueBit = row.ProgressValueBit
|
||||||
|
baseDatetime = row.BaseDatetime
|
||||||
|
latestVersion = row.LatestVersion
|
||||||
|
} else if birdG[key.GimmickId] {
|
||||||
|
baseDatetime = birdDefaultBaseDatetime
|
||||||
|
}
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
|
"gimmickSequenceScheduleId": key.GimmickSequenceScheduleId,
|
||||||
"gimmickSequenceId": row.Key.GimmickSequenceId,
|
"gimmickSequenceId": key.GimmickSequenceId,
|
||||||
"gimmickId": row.Key.GimmickId,
|
"gimmickId": key.GimmickId,
|
||||||
"gimmickOrnamentIndex": row.Key.GimmickOrnamentIndex,
|
"gimmickOrnamentIndex": key.GimmickOrnamentIndex,
|
||||||
"progressValueBit": row.ProgressValueBit,
|
"progressValueBit": progressValueBit,
|
||||||
"baseDatetime": row.BaseDatetime,
|
"baseDatetime": baseDatetime,
|
||||||
"latestVersion": row.LatestVersion,
|
"latestVersion": latestVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedGimmickSequenceRecords(user store.UserState) []map[string]any {
|
func sortedGimmickSequenceRecords(user store.UserState) []map[string]any {
|
||||||
|
|
||||||
|
ranks := gimmickSequenceRanks()
|
||||||
|
|
||||||
keys := make([]store.GimmickSequenceKey, 0, len(user.Gimmick.Sequences))
|
keys := make([]store.GimmickSequenceKey, 0, len(user.Gimmick.Sequences))
|
||||||
for key := range user.Gimmick.Sequences {
|
for key := range user.Gimmick.Sequences {
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
ri, rj := ranks[keys[i].GimmickSequenceId], ranks[keys[j].GimmickSequenceId]
|
||||||
|
if ri != rj {
|
||||||
|
return ri < rj
|
||||||
|
}
|
||||||
if keys[i].GimmickSequenceScheduleId != keys[j].GimmickSequenceScheduleId {
|
if keys[i].GimmickSequenceScheduleId != keys[j].GimmickSequenceScheduleId {
|
||||||
return keys[i].GimmickSequenceScheduleId < keys[j].GimmickSequenceScheduleId
|
return keys[i].GimmickSequenceScheduleId < keys[j].GimmickSequenceScheduleId
|
||||||
}
|
}
|
||||||
return keys[i].GimmickSequenceId < keys[j].GimmickSequenceId
|
return keys[i].GimmickSequenceId < keys[j].GimmickSequenceId
|
||||||
})
|
})
|
||||||
|
if len(keys) > masterdata.MaxUserGimmickRows {
|
||||||
|
keys = keys[:masterdata.MaxUserGimmickRows]
|
||||||
|
}
|
||||||
|
|
||||||
records := make([]map[string]any, 0, len(keys))
|
records := make([]map[string]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ func init() {
|
|||||||
s, _ := utils.EncodeJSONMaps(sortedPartsPresetRecords(user)...)
|
s, _ := utils.EncodeJSONMaps(sortedPartsPresetRecords(user)...)
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
register("IUserPartsPresetTag", func(user store.UserState) string {
|
||||||
|
s, _ := utils.EncodeJSONMaps(sortedPartsPresetTagRecords(user)...)
|
||||||
|
return s
|
||||||
|
})
|
||||||
register("IUserCostumeAwakenStatusUp", func(user store.UserState) string {
|
register("IUserCostumeAwakenStatusUp", func(user store.UserState) string {
|
||||||
s, _ := utils.EncodeJSONMaps(sortedCostumeAwakenStatusUpRecords(user)...)
|
s, _ := utils.EncodeJSONMaps(sortedCostumeAwakenStatusUpRecords(user)...)
|
||||||
return s
|
return s
|
||||||
@@ -122,7 +126,6 @@ func init() {
|
|||||||
"IUserCostumeLevelBonusReleaseStatus",
|
"IUserCostumeLevelBonusReleaseStatus",
|
||||||
"IUserCostumeLotteryEffectAbility",
|
"IUserCostumeLotteryEffectAbility",
|
||||||
"IUserCostumeLotteryEffectStatusUp",
|
"IUserCostumeLotteryEffectStatusUp",
|
||||||
"IUserPartsPresetTag",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +499,25 @@ func sortedPartsPresetRecords(user store.UserState) []map[string]any {
|
|||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortedPartsPresetTagRecords(user store.UserState) []map[string]any {
|
||||||
|
ids := make([]int, 0, len(user.PartsPresetTags))
|
||||||
|
for id := range user.PartsPresetTags {
|
||||||
|
ids = append(ids, int(id))
|
||||||
|
}
|
||||||
|
sort.Ints(ids)
|
||||||
|
records := make([]map[string]any, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
row := user.PartsPresetTags[int32(id)]
|
||||||
|
records = append(records, map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"userPartsPresetTagNumber": row.UserPartsPresetTagNumber,
|
||||||
|
"name": row.Name,
|
||||||
|
"latestVersion": row.LatestVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
func sortedPartsStatusSubRecords(user store.UserState) []map[string]any {
|
func sortedPartsStatusSubRecords(user store.UserState) []map[string]any {
|
||||||
keys := make([]store.PartsStatusSubKey, 0, len(user.PartsStatusSubs))
|
keys := make([]store.PartsStatusSubKey, 0, len(user.PartsStatusSubs))
|
||||||
for k := range user.PartsStatusSubs {
|
for k := range user.PartsStatusSubs {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package userdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/store"
|
||||||
|
"lunar-tear/server/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var labyrinthCatalog = sync.OnceValue(masterdata.LoadLabyrinthCatalog)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("IUserEventQuestLabyrinthSeason", func(user store.UserState) string {
|
||||||
|
chapters := labyrinthCatalog().ChaptersByOrder
|
||||||
|
records := make([]map[string]any, 0, len(chapters))
|
||||||
|
for _, ch := range chapters {
|
||||||
|
if st, ok := user.LabyrinthSeasons[ch.EventQuestChapterId]; ok {
|
||||||
|
records = append(records, map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"eventQuestChapterId": st.EventQuestChapterId,
|
||||||
|
"lastJoinSeasonNumber": st.LastJoinSeasonNumber,
|
||||||
|
"lastSeasonRewardReceivedSeasonNumber": st.LastSeasonRewardReceivedSeasonNumber,
|
||||||
|
"latestVersion": st.LatestVersion,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
records = append(records, map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"eventQuestChapterId": ch.EventQuestChapterId,
|
||||||
|
"lastJoinSeasonNumber": ch.LatestSeasonNumber,
|
||||||
|
"lastSeasonRewardReceivedSeasonNumber": 0,
|
||||||
|
"latestVersion": user.GameStartDatetime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
s, _ := utils.EncodeJSONMaps(records...)
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
|
||||||
|
register("IUserEventQuestLabyrinthStage", func(user store.UserState) string {
|
||||||
|
records := make([]map[string]any, 0)
|
||||||
|
for _, ch := range labyrinthCatalog().ChaptersByOrder {
|
||||||
|
for _, stageOrder := range ch.StageOrders {
|
||||||
|
key := store.LabyrinthStageKey{
|
||||||
|
EventQuestChapterId: ch.EventQuestChapterId,
|
||||||
|
StageOrder: stageOrder,
|
||||||
|
}
|
||||||
|
if st, ok := user.LabyrinthStages[key]; ok {
|
||||||
|
records = append(records, map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"eventQuestChapterId": st.EventQuestChapterId,
|
||||||
|
"stageOrder": st.StageOrder,
|
||||||
|
"isReceivedStageClearReward": st.IsReceivedStageClearReward,
|
||||||
|
"accumulationRewardReceivedQuestMissionCount": st.AccumulationRewardReceivedQuestMissionCount,
|
||||||
|
"latestVersion": st.LatestVersion,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
records = append(records, map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"eventQuestChapterId": ch.EventQuestChapterId,
|
||||||
|
"stageOrder": stageOrder,
|
||||||
|
"isReceivedStageClearReward": false,
|
||||||
|
"accumulationRewardReceivedQuestMissionCount": 0,
|
||||||
|
"latestVersion": user.GameStartDatetime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s, _ := utils.EncodeJSONMaps(records...)
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user