From 3fe564cb1dd0786a3f2a42eb61e832ae3baeb8ec Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Tue, 28 Apr 2026 21:22:28 +0300 Subject: [PATCH] Add admin API for content reload --- README.md | 54 +++++-- server/cmd/dev/main.go | 27 +++- server/cmd/lunar-tear/admin.go | 56 +++++++ server/cmd/lunar-tear/grpc.go | 120 ++++----------- server/cmd/lunar-tear/main.go | 165 ++------------------ server/cmd/wizard/main.go | 75 ++++++--- server/docker-compose.yaml | 3 + server/entrypoint.sh | 8 +- server/internal/runtime/build.go | 170 +++++++++++++++++++++ server/internal/runtime/holder.go | 104 +++++++++++++ server/internal/service/banner.go | 15 +- server/internal/service/cageornament.go | 15 +- server/internal/service/character.go | 23 +-- server/internal/service/characterboard.go | 48 +++--- server/internal/service/characterviewer.go | 10 +- server/internal/service/companion.go | 19 ++- server/internal/service/consumableitem.go | 16 +- server/internal/service/costume.go | 92 ++++++----- server/internal/service/data.go | 21 ++- server/internal/service/explore.go | 18 ++- server/internal/service/gacha.go | 63 +++++--- server/internal/service/gimmick.go | 14 +- server/internal/service/loginbonus.go | 11 +- server/internal/service/material.go | 16 +- server/internal/service/octo.go | 24 +-- server/internal/service/omikuji.go | 10 +- server/internal/service/parts.go | 44 +++--- server/internal/service/quest_bighunt.go | 64 ++++---- server/internal/service/quest_event.go | 14 +- server/internal/service/quest_extra.go | 16 +- server/internal/service/quest_main.go | 41 +++-- server/internal/service/quest_sidestory.go | 10 +- server/internal/service/reward.go | 28 ++-- server/internal/service/shop.go | 51 ++++--- server/internal/service/tutorial.go | 10 +- server/internal/service/weapon.go | 155 +++++++++++-------- 36 files changed, 992 insertions(+), 638 deletions(-) create mode 100644 server/cmd/lunar-tear/admin.go create mode 100644 server/internal/runtime/build.go create mode 100644 server/internal/runtime/holder.go diff --git a/README.md b/README.md index 972a562..c4eaf73 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ go run ./cmd/wizard --grpc-port 9003 --cdn-port 9080 | `--grpc-port` | `8003` | gRPC server port | | `--cdn-port` | `8080` | CDN 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. @@ -172,6 +173,7 @@ make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000" | `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr | | `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear | | `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear | +| `--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 @@ -180,16 +182,39 @@ make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000" | -------- | ---- | ------------- | ----------------------------------------------------------- | | gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) | | 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`) -| Flag | Default | Description | -| --------------- | ----------------- | ---------------------------------------------------- | -| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | -| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | -| `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | -| `--db` | `db/game.db` | SQLite database path | -| `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | +| Flag | Default | Description | +| ---------------- | ----------------- | ---------------------------------------------------- | +| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | +| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | +| `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | +| `--db` | `db/game.db` | SQLite database path | +| `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | +| `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. | + +### 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`) @@ -214,11 +239,22 @@ Each service has its own image and can be deployed independently: | Service | Image | Default Port | Notes | | -------- | --------------------------- | ------------ | ------------------------------ | -| `server` | `kretts/lunar-tear:latest` | 8003 | gRPC game server | +| `server` | `kretts/lunar-tear:latest` | 8003, 8082 | gRPC game server + admin webhook | | `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN | | `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login | -The game server is configured via environment variables in the compose file: `LUNAR_LISTEN` (bind address), `LUNAR_PUBLIC_ADDR` (client-facing address), `LUNAR_OCTO_URL`, and `LUNAR_AUTH_URL`. Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. +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 diff --git a/server/cmd/dev/main.go b/server/cmd/dev/main.go index 01781be..e928cd4 100644 --- a/server/cmd/dev/main.go +++ b/server/cmd/dev/main.go @@ -93,6 +93,10 @@ func main() { 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)") + // 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.") + noColor := flag.Bool("no-color", false, "disable colored output") flag.Parse() @@ -139,11 +143,7 @@ func main() { label: "grpc", color: colorYellow, cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext), - "--listen", *grpcListen, - "--public-addr", *grpcPublicAddr, - "--db", *grpcDB, - "--octo-url", *grpcOctoURL, - "--auth-url", *grpcAuthURL, + grpcArgs(*grpcListen, *grpcPublicAddr, *grpcDB, *grpcOctoURL, *grpcAuthURL, *adminListen)..., ), }, } @@ -200,3 +200,20 @@ func prefixLines(wg *sync.WaitGroup, prefix string, r io.Reader) { 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) []string { + args := []string{ + "--listen", listen, + "--public-addr", publicAddr, + "--db", db, + "--octo-url", octoURL, + "--auth-url", authURL, + } + if adminListen != "" { + args = append(args, "--admin-listen", adminListen) + } + return args +} diff --git a/server/cmd/lunar-tear/admin.go b/server/cmd/lunar-tear/admin.go new file mode 100644 index 0000000..0608350 --- /dev/null +++ b/server/cmd/lunar-tear/admin.go @@ -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) + } + }() +} diff --git a/server/cmd/lunar-tear/grpc.go b/server/cmd/lunar-tear/grpc.go index 876f2a9..890f005 100644 --- a/server/cmd/lunar-tear/grpc.go +++ b/server/cmd/lunar-tear/grpc.go @@ -6,10 +6,8 @@ import ( "strconv" pb "lunar-tear/server/gen/proto" - "lunar-tear/server/internal/gacha" "lunar-tear/server/internal/interceptor" - "lunar-tear/server/internal/masterdata" - "lunar-tear/server/internal/questflow" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/service" "lunar-tear/server/internal/store" @@ -40,27 +38,7 @@ func startGRPC( store.UserRepository store.SessionRepository }, - questEngine *questflow.QuestHandler, - gachaHandler *gacha.GachaHandler, - 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, + holder *runtime.Holder, ) *grpc.Server { lis, err := net.Listen("tcp", listenAddr) if err != nil { @@ -74,33 +52,7 @@ func startGRPC( grpc.UnknownServiceHandler(interceptor.UnknownService), ) - registerServices(grpcServer, - 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, - ) + registerServices(grpcServer, publicAddr, octoURL, authURL, userStore, holder) reflection.Register(grpcServer) @@ -124,66 +76,46 @@ func registerServices( store.UserRepository store.SessionRepository }, - questEngine *questflow.QuestHandler, - gachaHandler *gacha.GachaHandler, - 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, + holder *runtime.Holder, ) { pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr) 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.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore)) pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL)) pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore)) - pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine)) - pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, gachaEntries, gachaHandler)) + pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, holder)) + pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, holder)) pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore)) pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer()) - pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, gimmickCatalog)) - pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, questEngine)) + pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, holder)) + pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, holder)) 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.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.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore)) pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(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.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, shopCatalog, questEngine.Granter)) - pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, costumeCatalog, gameConfig)) + pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, holder)) + pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, holder)) pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore)) - pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, omikujiCatalog)) - pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, weaponCatalog, gameConfig)) - pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, exploreCatalog)) - pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, characterBoardCatalog)) - pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, partsCatalog, gameConfig)) - pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, characterRebirthCatalog, gameConfig)) - pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, companionCatalog, gameConfig)) - pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, materialCatalog, gameConfig)) - pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, consumableItemCatalog, gameConfig)) - pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, sideStoryCatalog)) - pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine)) - pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter)) + pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, holder)) + pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, holder)) + pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, holder)) + pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, holder)) + pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, holder)) + pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, holder)) + pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, holder)) + pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, holder)) + pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, holder)) + pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, holder)) + pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, holder)) + pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, holder)) } diff --git a/server/cmd/lunar-tear/main.go b/server/cmd/lunar-tear/main.go index 69f1953..14c19f8 100644 --- a/server/cmd/lunar-tear/main.go +++ b/server/cmd/lunar-tear/main.go @@ -9,30 +9,30 @@ import ( "syscall" "lunar-tear/server/internal/database" - "lunar-tear/server/internal/gacha" "lunar-tear/server/internal/gametime" - "lunar-tear/server/internal/masterdata" - "lunar-tear/server/internal/masterdata/memorydb" - "lunar-tear/server/internal/questflow" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store/sqlite" ) +const masterDataPath = "assets/release/20240404193219.bin.e" + func main() { listen := flag.String("listen", "0.0.0.0:443", "gRPC listen address (host:port)") publicAddr := flag.String("public-addr", "127.0.0.1:443", "externally-reachable host:port advertised to clients") dbPath := flag.String("db", "db/game.db", "SQLite database path") octoURL := flag.String("octo-url", "", "Octo CDN base URL the client will use for assets (e.g. http://10.0.2.2:8080)") authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)") + 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.") flag.Parse() if *octoURL == "" { 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 { - log.Fatalf("load master data: %v", err) + holder, err := runtime.NewHolder(masterDataPath) + 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) defer stop() @@ -44,158 +44,11 @@ func main() { defer db.Close() 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) - gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog() - if err != nil { - log.Fatalf("load gacha catalog: %v", err) - } - log.Printf("gacha catalog loaded: %d entries", len(gachaEntries)) + grpcServer := startGRPC(*listen, *publicAddr, *octoURL, *authURL, userStore, holder) - gachaPool, err := masterdata.LoadGachaPool() - 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, - ) + startAdmin(*adminListen, holder) <-ctx.Done() log.Println("shutting down...") diff --git a/server/cmd/wizard/main.go b/server/cmd/wizard/main.go index 19c0f74..f21b37b 100644 --- a/server/cmd/wizard/main.go +++ b/server/cmd/wizard/main.go @@ -32,13 +32,14 @@ const ( ) type config struct { - IP string `json:"ip"` - Device string `json:"device"` - Detail string `json:"detail"` - Summary string `json:"summary"` - GRPCPort int `json:"grpc_port,omitempty"` - CDNPort int `json:"cdn_port,omitempty"` - AuthPort int `json:"auth_port,omitempty"` + IP string `json:"ip"` + Device string `json:"device"` + Detail string `json:"detail"` + Summary string `json:"summary"` + GRPCPort int `json:"grpc_port,omitempty"` + CDNPort int `json:"cdn_port,omitempty"` + AuthPort int `json:"auth_port,omitempty"` + AdminPort int `json:"admin_port,omitempty"` } const ( @@ -47,10 +48,13 @@ const ( 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 { - GRPC int - CDN int - Auth int + GRPC int + CDN int + Auth int + Admin int } func main() { @@ -59,6 +63,7 @@ func main() { grpcPort := flag.Int("grpc-port", defaultGRPCPort, "gRPC server port") cdnPort := flag.Int("cdn-port", defaultCDNPort, "CDN 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() flagSet := map[string]bool{} @@ -80,10 +85,10 @@ func main() { ip, cfg, firstRun := resolveIP(*preferSaved) - p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, cfg) + p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, *adminPort, 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) { os.Exit(0) } @@ -92,6 +97,7 @@ func main() { cfg.GRPCPort = p.GRPC cfg.CDNPort = p.CDN cfg.AuthPort = p.Auth + cfg.AdminPort = p.Admin saveConfig(cfg) labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14) @@ -101,6 +107,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("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))) + 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() if firstRun || *setupOnly { @@ -477,6 +486,22 @@ func warnPortChange(old, new ports) bool { } 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 b.WriteString("\n") @@ -487,7 +512,12 @@ func warnPortChange(old, new ports) bool { b.WriteString(portLine("CDN", old.CDN, new.CDN)) b.WriteString("\n") 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("\n\n") fmt.Print(b.String()) @@ -821,7 +851,7 @@ func loadConfig() (config, error) { } 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 { p.GRPC = defaultGRPCPort } @@ -831,10 +861,11 @@ func portsFromConfig(cfg config) ports { if p.Auth == 0 { p.Auth = defaultAuthPort } + // Admin is opt-in: leave 0 = disabled. 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 { if flagSet[name] { return flagVal @@ -848,6 +879,9 @@ func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, save GRPC: resolve("grpc-port", grpcFlag, saved.GRPCPort, defaultGRPCPort), CDN: resolve("cdn-port", cdnFlag, saved.CDNPort, defaultCDNPort), 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), } } @@ -874,13 +908,20 @@ func launchDev(ip string, p ports) { 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.public-addr", fmt.Sprintf("%s:%d", ip, p.GRPC), "--cdn.listen", fmt.Sprintf("0.0.0.0:%d", p.CDN), "--cdn.public-addr", fmt.Sprintf("%s:%d", ip, p.CDN), "--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.Stderr = os.Stderr cmd.Stdin = os.Stdin diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml index f83ac0c..faf1613 100644 --- a/server/docker-compose.yaml +++ b/server/docker-compose.yaml @@ -10,11 +10,14 @@ services: LUNAR_PUBLIC_ADDR: 127.0.0.1:8003 LUNAR_OCTO_URL: http://cdn:8080 LUNAR_AUTH_URL: http://auth:3000 + LUNAR_ADMIN_LISTEN: 0.0.0.0:8082 + LUNAR_ADMIN_TOKEN: ${LUNAR_ADMIN_TOKEN:-} volumes: - ./db:/opt/lunar-tear/db - ./assets:/opt/lunar-tear/assets ports: - 8003:8003 + - 127.0.0.1:8082:8082 depends_on: - cdn - auth diff --git a/server/entrypoint.sh b/server/entrypoint.sh index df7ee90..a3972bc 100755 --- a/server/entrypoint.sh +++ b/server/entrypoint.sh @@ -9,8 +9,14 @@ if [ -n "${LUNAR_AUTH_URL}" ]; then AUTH_FLAG="--auth-url ${LUNAR_AUTH_URL}" fi +ADMIN_FLAG="" +if [ -n "${LUNAR_ADMIN_LISTEN}" ]; then + ADMIN_FLAG="--admin-listen ${LUNAR_ADMIN_LISTEN}" +fi + exec ./lunar-tear \ --listen "${LUNAR_LISTEN:-0.0.0.0:443}" \ --public-addr "${LUNAR_PUBLIC_ADDR}" \ --octo-url "${LUNAR_OCTO_URL}" \ - ${AUTH_FLAG} + ${AUTH_FLAG} \ + ${ADMIN_FLAG} diff --git a/server/internal/runtime/build.go b/server/internal/runtime/build.go new file mode 100644 index 0000000..d19a239 --- /dev/null +++ b/server/internal/runtime/build.go @@ -0,0 +1,170 @@ +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" +) + +// 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) + } + questHandler := questflow.NewQuestHandler(questCatalog, gameConfig) + + 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.BuildFeaturedMapping(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) + 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)) + + sideStoryCatalog := masterdata.LoadSideStoryCatalog() + bigHuntCatalog := masterdata.LoadBigHuntCatalog() + + 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, + QuestHandler: questHandler, + GachaHandler: gachaHandler, + }, nil +} diff --git a/server/internal/runtime/holder.go b/server/internal/runtime/holder.go new file mode 100644 index 0000000..ef22bf2 --- /dev/null +++ b/server/internal/runtime/holder.go @@ -0,0 +1,104 @@ +// 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 + + // 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() +} diff --git a/server/internal/service/banner.go b/server/internal/service/banner.go index 408d90e..4e3328d 100644 --- a/server/internal/service/banner.go +++ b/server/internal/service/banner.go @@ -4,24 +4,29 @@ import ( "context" pb "lunar-tear/server/gen/proto" + "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/model" - "lunar-tear/server/internal/store" + "lunar-tear/server/internal/runtime" ) type BannerServiceServer struct { pb.UnimplementedBannerServiceServer - catalog []store.GachaCatalogEntry + holder *runtime.Holder } -func NewBannerServiceServer(catalog []store.GachaCatalogEntry) *BannerServiceServer { - return &BannerServiceServer{catalog: catalog} +func NewBannerServiceServer(holder *runtime.Holder) *BannerServiceServer { + return &BannerServiceServer{holder: holder} } 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 latestChapter *pb.GachaBanner for _, entry := range catalog { + if !gachaActiveAt(entry, nowMillis) { + continue + } if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle { continue } diff --git a/server/internal/service/cageornament.go b/server/internal/service/cageornament.go index 920857b..1801ea1 100644 --- a/server/internal/service/cageornament.go +++ b/server/internal/service/cageornament.go @@ -6,8 +6,8 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" - "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -15,21 +15,22 @@ type CageOrnamentServiceServer struct { pb.UnimplementedCageOrnamentServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.CageOrnamentCatalog - granter *store.PossessionGranter + holder *runtime.Holder } -func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CageOrnamentCatalog, granter *store.PossessionGranter) *CageOrnamentServiceServer { - return &CageOrnamentServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter} +func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CageOrnamentServiceServer { + return &CageOrnamentServiceServer{users: users, sessions: sessions, holder: holder} } func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.ReceiveRewardRequest) (*pb.ReceiveRewardResponse, error) { log.Printf("[CageOrnamentService] ReceiveReward: cageOrnamentId=%d", req.CageOrnamentId) - reward, ok := s.catalog.LookupReward(req.CageOrnamentId) + cat := s.holder.Get() + reward, ok := cat.CageOrnament.LookupReward(req.CageOrnamentId) if !ok { log.Fatalf("[CageOrnamentService] ReceiveReward: no reward for cageOrnamentId=%d", req.CageOrnamentId) } + granter := cat.QuestHandler.Granter userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -39,7 +40,7 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R AcquisitionDatetime: nowMillis, LatestVersion: nowMillis, } - s.granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis) + granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis) }) return &pb.ReceiveRewardResponse{ diff --git a/server/internal/service/character.go b/server/internal/service/character.go index 5fb6ad1..9e2d6a9 100644 --- a/server/internal/service/character.go +++ b/server/internal/service/character.go @@ -7,6 +7,7 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -14,21 +15,23 @@ type CharacterServiceServer struct { pb.UnimplementedCharacterServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.CharacterRebirthCatalog - config *masterdata.GameConfig + holder *runtime.Holder } -func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterRebirthCatalog, config *masterdata.GameConfig) *CharacterServiceServer { - return &CharacterServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterServiceServer { + return &CharacterServiceServer{users: users, sessions: sessions, holder: holder} } 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) + cat := s.holder.Get() + catalog := cat.CharacterRebirth + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - stepGroupId, ok := s.catalog.StepGroupByCharacterId[req.CharacterId] + stepGroupId, ok := catalog.StepGroupByCharacterId[req.CharacterId] if !ok { log.Printf("[CharacterService] Rebirth: no step group for characterId=%d", req.CharacterId) return &pb.RebirthResponse{}, nil @@ -40,17 +43,17 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq targetCount := currentCount + req.RebirthCount 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 { log.Printf("[CharacterService] Rebirth: no step row for groupId=%d beforeCount=%d", stepGroupId, count) return } - goldId := s.config.ConsumableItemIdForGold - user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-s.config.CharacterRebirthConsumeGold, 0) - log.Printf("[CharacterService] Rebirth: consumed gold=%d", s.config.CharacterRebirthConsumeGold) + goldId := config.ConsumableItemIdForGold + user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-config.CharacterRebirthConsumeGold, 0) + log.Printf("[CharacterService] Rebirth: consumed gold=%d", config.CharacterRebirthConsumeGold) - materials := s.catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId] + materials := catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId] for _, mat := range materials { user.Materials[mat.MaterialId] -= mat.Count if user.Materials[mat.MaterialId] <= 0 { diff --git a/server/internal/service/characterboard.go b/server/internal/service/characterboard.go index ebd7abc..4df09bc 100644 --- a/server/internal/service/characterboard.go +++ b/server/internal/service/characterboard.go @@ -7,6 +7,7 @@ import ( 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" ) @@ -14,43 +15,44 @@ type CharacterBoardServiceServer struct { pb.UnimplementedCharacterBoardServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.CharacterBoardCatalog + holder *runtime.Holder } -func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterBoardCatalog) *CharacterBoardServiceServer { - return &CharacterBoardServiceServer{users: users, sessions: sessions, catalog: catalog} +func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterBoardServiceServer { + return &CharacterBoardServiceServer{users: users, sessions: sessions, holder: holder} } func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) { log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId) + catalog := s.holder.Get().CharacterBoard userId := CurrentUserId(ctx, s.users, s.sessions) s.users.UpdateUser(userId, func(user *store.UserState) { for _, panelId := range req.CharacterBoardPanelId { - panel, ok := s.catalog.PanelById[panelId] + panel, ok := catalog.PanelById[panelId] if !ok { log.Printf("[CharacterBoardService] unknown panelId=%d, skipping", panelId) continue } - s.consumeCosts(user, panel) - s.setReleaseBit(user, panel) - s.applyEffects(user, panel) + consumeBoardCosts(catalog, user, panel) + setBoardReleaseBit(user, panel) + applyBoardEffects(catalog, user, panel) } }) return &pb.ReleasePanelResponse{}, nil } -func (s *CharacterBoardServiceServer) consumeCosts(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { - costs := s.catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId] +func consumeBoardCosts(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { + costs := catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId] for _, cost := range costs { store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count) } } -func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { +func setBoardReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { boardId := panel.CharacterBoardId board := user.CharacterBoards[boardId] board.CharacterBoardId = boardId @@ -73,26 +75,26 @@ func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel user.CharacterBoards[boardId] = board } -func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { - effects := s.catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId] +func applyBoardEffects(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { + effects := catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId] for _, eff := range effects { switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) { case model.CharacterBoardEffectTypeAbility: - s.applyAbilityEffect(user, eff) + applyBoardAbilityEffect(catalog, user, eff) case model.CharacterBoardEffectTypeStatusUp: - s.applyStatusUpEffect(user, eff) + applyBoardStatusUpEffect(catalog, user, eff) } } } -func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { - ability, ok := s.catalog.AbilityById[eff.CharacterBoardEffectId] +func applyBoardAbilityEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { + ability, ok := catalog.AbilityById[eff.CharacterBoardEffectId] if !ok { log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId) return } - characterId := s.resolveCharacterId(ability.CharacterBoardEffectTargetGroupId) + characterId := resolveBoardCharacterId(catalog, ability.CharacterBoardEffectTargetGroupId) if characterId == 0 { return } @@ -103,21 +105,21 @@ func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, state.AbilityId = ability.AbilityId 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 } user.CharacterBoardAbilities[key] = state } -func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { - statusUp, ok := s.catalog.StatusUpById[eff.CharacterBoardEffectId] +func applyBoardStatusUpEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { + statusUp, ok := catalog.StatusUpById[eff.CharacterBoardEffectId] if !ok { log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId) return } - characterId := s.resolveCharacterId(statusUp.CharacterBoardEffectTargetGroupId) + characterId := resolveBoardCharacterId(catalog, statusUp.CharacterBoardEffectTargetGroupId) if characterId == 0 { return } @@ -151,8 +153,8 @@ func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, user.CharacterBoardStatusUps[key] = state } -func (s *CharacterBoardServiceServer) resolveCharacterId(targetGroupId int32) int32 { - targets := s.catalog.EffectTargetsByGroupId[targetGroupId] +func resolveBoardCharacterId(catalog *masterdata.CharacterBoardCatalog, targetGroupId int32) int32 { + targets := catalog.EffectTargetsByGroupId[targetGroupId] for _, t := range targets { if t.TargetValue != 0 { return t.TargetValue diff --git a/server/internal/service/characterviewer.go b/server/internal/service/characterviewer.go index 5aff808..e39f8f7 100644 --- a/server/internal/service/characterviewer.go +++ b/server/internal/service/characterviewer.go @@ -6,7 +6,7 @@ import ( "log" pb "lunar-tear/server/gen/proto" - "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" "google.golang.org/protobuf/types/known/emptypb" @@ -16,11 +16,11 @@ type CharacterViewerServiceServer struct { pb.UnimplementedCharacterViewerServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.CharacterViewerCatalog + holder *runtime.Holder } -func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterViewerCatalog) *CharacterViewerServiceServer { - return &CharacterViewerServiceServer{users: users, sessions: sessions, catalog: catalog} +func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterViewerServiceServer { + return &CharacterViewerServiceServer{users: users, sessions: sessions, holder: holder} } 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)) } - released := s.catalog.ReleasedFieldIds(user) + released := s.holder.Get().CharacterViewer.ReleasedFieldIds(user) log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId) return &pb.CharacterViewerTopResponse{ diff --git a/server/internal/service/companion.go b/server/internal/service/companion.go index 75e25a9..662e791 100644 --- a/server/internal/service/companion.go +++ b/server/internal/service/companion.go @@ -8,6 +8,7 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -17,17 +18,19 @@ type CompanionServiceServer struct { pb.UnimplementedCompanionServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.CompanionCatalog - config *masterdata.GameConfig + holder *runtime.Holder } -func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CompanionCatalog, config *masterdata.GameConfig) *CompanionServiceServer { - return &CompanionServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CompanionServiceServer { + return &CompanionServiceServer{users: users, sessions: sessions, holder: holder} } 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) + cat := s.holder.Get() + catalog := cat.Companion + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -38,7 +41,7 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE return } - compDef, ok := s.catalog.CompanionById[companion.CompanionId] + compDef, ok := catalog.CompanionById[companion.CompanionId] if !ok { log.Printf("[CompanionService] Enhance: companion master id=%d not found", companion.CompanionId) return @@ -50,13 +53,13 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE } 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost } 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 } } diff --git a/server/internal/service/consumableitem.go b/server/internal/service/consumableitem.go index ee0c00a..ba652f9 100644 --- a/server/internal/service/consumableitem.go +++ b/server/internal/service/consumableitem.go @@ -6,7 +6,7 @@ import ( "log" pb "lunar-tear/server/gen/proto" - "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -14,23 +14,25 @@ type ConsumableItemServiceServer struct { pb.UnimplementedConsumableItemServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.ConsumableItemCatalog - config *masterdata.GameConfig + holder *runtime.Holder } -func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ConsumableItemCatalog, config *masterdata.GameConfig) *ConsumableItemServiceServer { - return &ConsumableItemServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ConsumableItemServiceServer { + return &ConsumableItemServiceServer{users: users, sessions: sessions, holder: holder} } func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) { 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) _, err := s.users.UpdateUser(userId, func(user *store.UserState) { totalGold := int32(0) for _, item := range req.ConsumableItemPossession { - row, ok := s.catalog.All[item.ConsumableItemId] + row, ok := catalog.All[item.ConsumableItemId] if !ok { log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId) continue @@ -53,7 +55,7 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab } if totalGold > 0 { - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold) } }) diff --git a/server/internal/service/costume.go b/server/internal/service/costume.go index 600ed9d..b36dc68 100644 --- a/server/internal/service/costume.go +++ b/server/internal/service/costume.go @@ -13,6 +13,7 @@ import ( "lunar-tear/server/internal/gameutil" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -20,17 +21,19 @@ type CostumeServiceServer struct { pb.UnimplementedCostumeServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.CostumeCatalog - config *masterdata.GameConfig + holder *runtime.Holder } -func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CostumeCatalog, config *masterdata.GameConfig) *CostumeServiceServer { - return &CostumeServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CostumeServiceServer { + return &CostumeServiceServer{users: users, sessions: sessions, holder: holder} } 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) + cat := s.holder.Get() + catalog := cat.Costume + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -41,7 +44,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque return } - cm, ok := s.catalog.Costumes[costume.CostumeId] + cm, ok := catalog.Costumes[costume.CostumeId] if !ok { log.Printf("[CostumeService] Enhance: costume master id=%d not found", costume.CostumeId) return @@ -50,7 +53,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque totalExp := int32(0) totalMaterialCount := int32(0) for materialId, count := range req.Materials { - mat, ok := s.catalog.Materials[materialId] + mat, ok := catalog.Materials[materialId] if !ok { log.Printf("[CostumeService] Enhance: material id=%d not found, skipping", materialId) continue @@ -66,20 +69,20 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque expPerUnit := mat.EffectValue if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType { - expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 + expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000 } 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost log.Printf("[CostumeService] Enhance: gold cost=%d (materials=%d)", goldCost, totalMaterialCount) } 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) } @@ -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) { 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) nowMillis := gametime.NowMillis() @@ -110,7 +116,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest return } - awakenRow, ok := s.catalog.AwakenByCostumeId[costume.CostumeId] + awakenRow, ok := catalog.AwakenByCostumeId[costume.CostumeId] if !ok { log.Printf("[CostumeService] Awaken: no awaken data for costumeId=%d", costume.CostumeId) return @@ -118,8 +124,8 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest nextStep := costume.AwakenCount + 1 - if gold, ok := s.catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok { - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= gold + if gold, ok := catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok { + user.ConsumableItems[config.ConsumableItemIdForGold] -= 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 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 { return } @@ -148,11 +154,11 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest switch model.CostumeAwakenEffectType(effect.CostumeAwakenEffectType) { case model.CostumeAwakenEffectTypeStatusUp: - s.applyAwakenStatusUp(user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis) + applyCostumeAwakenStatusUp(catalog, user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis) case model.CostumeAwakenEffectTypeAbility: log.Printf("[CostumeService] Awaken: ability effect id=%d (client-resolved)", effect.CostumeAwakenEffectId) case model.CostumeAwakenEffectTypeItemAcquire: - s.applyAwakenItemAcquire(user, effect.CostumeAwakenEffectId, nowMillis) + applyCostumeAwakenItemAcquire(catalog, user, effect.CostumeAwakenEffectId, nowMillis) default: 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 } -func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) { - rows, ok := s.catalog.AwakenStatusUpByGroup[statusUpGroupId] +func applyCostumeAwakenStatusUp(catalog *masterdata.CostumeCatalog, user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) { + rows, ok := catalog.AwakenStatusUpByGroup[statusUpGroupId] if !ok { log.Printf("[CostumeService] Awaken: status up group %d not found", statusUpGroupId) return @@ -201,8 +207,8 @@ func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costum } } -func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, itemAcquireId int32, nowMillis int64) { - acq, ok := s.catalog.AwakenItemAcquireById[itemAcquireId] +func applyCostumeAwakenItemAcquire(catalog *masterdata.CostumeCatalog, user *store.UserState, itemAcquireId int32, nowMillis int64) { + acq, ok := catalog.AwakenItemAcquireById[itemAcquireId] if !ok { log.Printf("[CostumeService] Awaken: item acquire id=%d not found", itemAcquireId) 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) { 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) nowMillis := gametime.NowMillis() @@ -236,13 +245,13 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E return } - cm, ok := s.catalog.Costumes[costume.CostumeId] + cm, ok := catalog.Costumes[costume.CostumeId] if !ok { log.Printf("[CostumeService] EnhanceActiveSkill: costume master id=%d not found", costume.CostumeId) return } - groupRows := s.catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId] + groupRows := catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId] enhanceMatId := int32(-1) for _, g := range groupRows { if g.CostumeLimitBreakCountLowerLimit <= costume.LimitBreakCount { @@ -259,7 +268,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E skill := user.CostumeActiveSkills[req.UserCostumeUuid] currentLevel := skill.Level - maxLevelFunc, ok := s.catalog.ActiveSkillMaxLevelByRarity[cm.RarityType] + maxLevelFunc, ok := catalog.ActiveSkillMaxLevelByRarity[cm.RarityType] if !ok { log.Printf("[CostumeService] EnhanceActiveSkill: no max level func for rarity=%d", cm.RarityType) return @@ -277,7 +286,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { key := [2]int32{enhanceMatId, lvl} - mats := s.catalog.ActiveSkillEnhanceMats[key] + mats := catalog.ActiveSkillEnhanceMats[key] for _, mat := range mats { cur := user.Materials[mat.MaterialId] cost := mat.Count @@ -288,9 +297,9 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E 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) - 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) { 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) nowMillis := gametime.NowMillis() @@ -320,12 +332,12 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea return } - if costume.LimitBreakCount >= s.config.CostumeLimitBreakAvailableCount { + if costume.LimitBreakCount >= config.CostumeLimitBreakAvailableCount { log.Printf("[CostumeService] LimitBreak: already at max limit break %d", costume.LimitBreakCount) return } - cm, ok := s.catalog.Costumes[costume.CostumeId] + cm, ok := catalog.Costumes[costume.CostumeId] if !ok { log.Printf("[CostumeService] LimitBreak: costume master id=%d not found", costume.CostumeId) return @@ -342,9 +354,9 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= 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) { 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) nowMillis := gametime.NowMillis() @@ -373,15 +388,15 @@ func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req return } - effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] + effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] if !ok { log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber) return } - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectUnlockSlotConsumeGold + user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectUnlockSlotConsumeGold - mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId] + mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId] for _, mat := range mats { cur := user.Materials[mat.MaterialId] 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) { 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) nowMillis := gametime.NowMillis() @@ -428,21 +446,21 @@ func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.Dr return } - effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] + effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] if !ok { log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber) return } - oddsPool := s.catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId] + oddsPool := catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId] if len(oddsPool) == 0 { log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId) return } - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectDrawSlotConsumeGold + user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectDrawSlotConsumeGold - mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId] + mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId] for _, mat := range mats { cur := user.Materials[mat.MaterialId] cost := mat.Count diff --git a/server/internal/service/data.go b/server/internal/service/data.go index 52c7e1f..c99869b 100644 --- a/server/internal/service/data.go +++ b/server/internal/service/data.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/store" @@ -12,6 +13,16 @@ import ( "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 { pb.UnimplementedDataServiceServer 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) { - 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{ - LatestMasterDataVersion: "20240404193219", + LatestMasterDataVersion: version, }, nil } diff --git a/server/internal/service/explore.go b/server/internal/service/explore.go index 6ffc673..538f1e8 100644 --- a/server/internal/service/explore.go +++ b/server/internal/service/explore.go @@ -7,8 +7,8 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" - "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -22,17 +22,18 @@ type ExploreServiceServer struct { pb.UnimplementedExploreServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.ExploreCatalog + holder *runtime.Holder } -func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ExploreCatalog) *ExploreServiceServer { - return &ExploreServiceServer{users: users, sessions: sessions, catalog: catalog} +func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ExploreServiceServer { + return &ExploreServiceServer{users: users, sessions: sessions, holder: holder} } 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) - 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) } @@ -40,7 +41,7 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx nowMillis := gametime.NowMillis() _, 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 { cur := user.ConsumableItems[req.UseConsumableItemId] 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) { 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 { 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) nowMillis := gametime.NowMillis() diff --git a/server/internal/service/gacha.go b/server/internal/service/gacha.go index 499b4d5..cf38bc0 100644 --- a/server/internal/service/gacha.go +++ b/server/internal/service/gacha.go @@ -10,6 +10,7 @@ import ( "lunar-tear/server/internal/gacha" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -20,34 +21,33 @@ type GachaServiceServer struct { pb.UnimplementedGachaServiceServer users store.UserRepository sessions store.SessionRepository - catalog []store.GachaCatalogEntry - handler *gacha.GachaHandler + holder *runtime.Holder } func NewGachaServiceServer( users store.UserRepository, sessions store.SessionRepository, - catalog []store.GachaCatalogEntry, - handler *gacha.GachaHandler, + holder *runtime.Holder, ) *GachaServiceServer { return &GachaServiceServer{ users: users, sessions: sessions, - catalog: catalog, - handler: handler, + holder: holder, } } func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) { 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) nowMillis := gametime.NowMillis() user, err := s.users.UpdateUser(userId, func(user *store.UserState) { user.EnsureMaps() - s.autoConvertExpiredMedals(user, catalog, nowMillis) + autoConvertExpiredMedals(user, catalog, handler, nowMillis) }) if err != nil { 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)) for _, entry := range catalog { + if !gachaActiveAt(entry, nowMillis) { + continue + } if !matchesGachaLabel(req.GachaLabelType, entry.GachaLabelType) { continue } @@ -71,7 +74,7 @@ func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaL }, 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 { if entry.GachaMedalId == 0 || entry.EndDatetime == 0 { continue @@ -84,7 +87,7 @@ func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, cat continue } - medalInfo, ok := s.handler.MedalInfo[entry.GachaId] + medalInfo, ok := handler.MedalInfo[entry.GachaId] if !ok { 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) { 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) 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)) for _, wantedId := range req.GachaId { for _, entry := range catalog { - if entry.GachaId == wantedId { - bs := user.Gacha.BannerStates[entry.GachaId] - byId[wantedId] = toProtoGacha(entry, &bs) + if entry.GachaId != wantedId { + continue + } + if !gachaActiveAt(entry, nowMillis) { 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) { 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 { return nil, fmt.Errorf("gacha %d not found", req.GachaId) } + handler := cat.GachaHandler userId := CurrentUserId(ctx, s.users, s.sessions) execCount := req.ExecCount @@ -158,7 +168,7 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb var drawResult *gacha.DrawResult updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { 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 { log.Printf("[GachaService] Draw error: %v", drawErr) drawResult = &gacha.DrawResult{} @@ -285,14 +295,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) { 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 { return nil, fmt.Errorf("gacha %d not found", req.GachaId) } + handler := cat.GachaHandler userId := CurrentUserId(ctx, s.users, s.sessions) updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { - if resetErr := s.handler.HandleResetBox(user, *entry); resetErr != nil { + if resetErr := handler.HandleResetBox(user, *entry); resetErr != nil { log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr) } }) @@ -315,7 +327,7 @@ func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Em return nil, fmt.Errorf("snapshot user: %w", err) } - maxCount := s.handler.Config.RewardGachaDailyMaxCount + maxCount := s.holder.Get().GachaHandler.Config.RewardGachaDailyMaxCount if maxCount <= 0 { maxCount = model.DefaultDailyDrawLimit } @@ -337,11 +349,12 @@ 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) userId := CurrentUserId(ctx, s.users, s.sessions) + handler := s.holder.Get().GachaHandler var items []gacha.DrawnItem updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { var drawErr error - items, drawErr = s.handler.HandleRewardDraw(user, 1) + items, drawErr = handler.HandleRewardDraw(user, 1) if drawErr != nil { log.Printf("[GachaService] RewardDraw error: %v", drawErr) } @@ -395,6 +408,16 @@ func matchesGachaLabel(labels []int32, label int32) bool { 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 { g := &pb.Gacha{ GachaId: entry.GachaId, diff --git a/server/internal/service/gimmick.go b/server/internal/service/gimmick.go index 4d8c4d2..65b6639 100644 --- a/server/internal/service/gimmick.go +++ b/server/internal/service/gimmick.go @@ -6,7 +6,7 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" - "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -14,13 +14,13 @@ import ( type GimmickServiceServer struct { pb.UnimplementedGimmickServiceServer - users store.UserRepository - sessions store.SessionRepository - gimmickCatalog *masterdata.GimmickCatalog + users store.UserRepository + sessions store.SessionRepository + holder *runtime.Holder } -func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, gimmickCatalog *masterdata.GimmickCatalog) *GimmickServiceServer { - return &GimmickServiceServer{users: users, sessions: sessions, gimmickCatalog: gimmickCatalog} +func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *GimmickServiceServer { + return &GimmickServiceServer{users: users, sessions: sessions, holder: holder} } func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) { @@ -80,7 +80,7 @@ func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *empt now := gametime.NowMillis() s.users.UpdateUser(userId, func(user *store.UserState) { added := 0 - for _, key := range s.gimmickCatalog.ActiveScheduleKeys(*user, now) { + for _, key := range s.holder.Get().Gimmick.ActiveScheduleKeys(*user, now) { if _, exists := user.Gimmick.Sequences[key]; !exists { user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key} added++ diff --git a/server/internal/service/loginbonus.go b/server/internal/service/loginbonus.go index 0fc6775..f2dddba 100644 --- a/server/internal/service/loginbonus.go +++ b/server/internal/service/loginbonus.go @@ -9,7 +9,7 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" - "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -19,22 +19,23 @@ type LoginBonusServiceServer struct { pb.UnimplementedLoginBonusServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.LoginBonusCatalog + holder *runtime.Holder } -func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.LoginBonusCatalog) *LoginBonusServiceServer { - return &LoginBonusServiceServer{users: users, sessions: sessions, catalog: catalog} +func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *LoginBonusServiceServer { + return &LoginBonusServiceServer{users: users, sessions: sessions, holder: holder} } func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) { log.Printf("[LoginBonusService] ReceiveStamp") userId := CurrentUserId(ctx, s.users, s.sessions) + catalog := s.holder.Get().LoginBonus s.users.UpdateUser(userId, func(user *store.UserState) { now := gametime.NowMillis() nextStamp := user.LoginBonus.CurrentStampNumber + 1 - reward, ok := s.catalog.LookupStampReward( + reward, ok := catalog.LookupStampReward( user.LoginBonus.LoginBonusId, user.LoginBonus.CurrentPageNumber, nextStamp, diff --git a/server/internal/service/material.go b/server/internal/service/material.go index d9b0138..a790fa6 100644 --- a/server/internal/service/material.go +++ b/server/internal/service/material.go @@ -6,7 +6,7 @@ import ( "log" pb "lunar-tear/server/gen/proto" - "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -14,23 +14,25 @@ type MaterialServiceServer struct { pb.UnimplementedMaterialServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.MaterialCatalog - config *masterdata.GameConfig + holder *runtime.Holder } -func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.MaterialCatalog, config *masterdata.GameConfig) *MaterialServiceServer { - return &MaterialServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *MaterialServiceServer { + return &MaterialServiceServer{users: users, sessions: sessions, holder: holder} } func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) { 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) _, err := s.users.UpdateUser(userId, func(user *store.UserState) { totalGold := int32(0) for _, item := range req.MaterialPossession { - mat, ok := s.catalog.All[item.MaterialId] + mat, ok := catalog.All[item.MaterialId] if !ok { log.Printf("[MaterialService] Sell: unknown materialId=%d, skipping", item.MaterialId) continue @@ -53,7 +55,7 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe } if totalGold > 0 { - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold log.Printf("[MaterialService] Sell: total gold +%d", totalGold) } }) diff --git a/server/internal/service/octo.go b/server/internal/service/octo.go index e99fce9..8b8e43e 100644 --- a/server/internal/service/octo.go +++ b/server/internal/service/octo.go @@ -414,24 +414,12 @@ func (s *OctoHTTPServer) serveListBin(w http.ResponseWriter, filePath string) { w.Write(data) } -// serveDatabaseBinE serves MasterMemory database: /assets/release/{version}/database.bin.e -// -> assets/release/{version}.bin.e (or assets/release/database.bin.e fallback). -func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, path string) { - parts := strings.Split(path, "/") - var version string - for i, p := range parts { - 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 - } - } +// serveDatabaseBinE serves the master data binary. The URL's {version} segment +// is a cache key (it changes whenever the file's mtime changes, see +// DataService.GetLatestMasterDataVersion) but does not select a different file — +// there's only ever one bin.e on disk. +func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, _ string) { + filePath := filepath.Join(s.BaseDir, "assets", "release", "20240404193219.bin.e") w.Header().Set("Content-Type", "application/octet-stream") http.ServeFile(w, r, filePath) } diff --git a/server/internal/service/omikuji.go b/server/internal/service/omikuji.go index 816349d..169d27c 100644 --- a/server/internal/service/omikuji.go +++ b/server/internal/service/omikuji.go @@ -7,7 +7,7 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" - "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -15,11 +15,11 @@ type OmikujiServiceServer struct { pb.UnimplementedOmikujiServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.OmikujiCatalog + holder *runtime.Holder } -func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.OmikujiCatalog) *OmikujiServiceServer { - return &OmikujiServiceServer{users: users, sessions: sessions, catalog: catalog} +func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *OmikujiServiceServer { + return &OmikujiServiceServer{users: users, sessions: sessions, holder: holder} } 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{ - OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId), + OmikujiResultAssetId: s.holder.Get().Omikuji.LookupAssetId(req.OmikujiId), OmikujiItem: []*pb.OmikujiItem{}, }, nil } diff --git a/server/internal/service/parts.go b/server/internal/service/parts.go index 86177c7..b0eadfb 100644 --- a/server/internal/service/parts.go +++ b/server/internal/service/parts.go @@ -9,6 +9,7 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -18,17 +19,19 @@ type PartsServiceServer struct { pb.UnimplementedPartsServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.PartsCatalog - config *masterdata.GameConfig + holder *runtime.Holder } -func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.PartsCatalog, config *masterdata.GameConfig) *PartsServiceServer { - return &PartsServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *PartsServiceServer { + return &PartsServiceServer{users: users, sessions: sessions, holder: holder} } func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) { 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) _, err := s.users.UpdateUser(userId, func(user *store.UserState) { @@ -44,13 +47,13 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) continue } - partDef, ok := s.catalog.PartsById[part.PartsId] + partDef, ok := catalog.PartsById[part.PartsId] if !ok { log.Printf("[PartsService] Sell: partsId=%d not in catalog, skipping", part.PartsId) continue } - sellFunc, ok := s.catalog.SellPriceByRarity[partDef.RarityType] + sellFunc, ok := catalog.SellPriceByRarity[partDef.RarityType] if !ok { log.Printf("[PartsService] Sell: no sell price func for rarity=%d, skipping", partDef.RarityType) continue @@ -68,7 +71,7 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) } if totalGold > 0 { - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold log.Printf("[PartsService] Sell: total gold +%d", totalGold) } }) @@ -82,6 +85,9 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) { 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) nowMillis := gametime.NowMillis() @@ -99,33 +105,33 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe return } - partDef, ok := s.catalog.PartsById[part.PartsId] + partDef, ok := catalog.PartsById[part.PartsId] if !ok { log.Printf("[PartsService] Enhance: part master id=%d not found", part.PartsId) return } - rarity, ok := s.catalog.RarityByRarityType[partDef.RarityType] + rarity, ok := catalog.RarityByRarityType[partDef.RarityType] if !ok { log.Printf("[PartsService] Enhance: rarity type=%d not found", partDef.RarityType) return } goldCost := int32(0) - if prices, ok := s.catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok { + if prices, ok := catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok { goldCost = prices[part.Level] } - currentGold := user.ConsumableItems[s.config.ConsumableItemIdForGold] + currentGold := user.ConsumableItems[config.ConsumableItemIdForGold] if currentGold < goldCost { log.Printf("[PartsService] Enhance: insufficient gold have=%d need=%d", currentGold, goldCost) return } - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 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 { successRate = r } @@ -137,7 +143,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)", 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 { log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)", part.PartsId, part.Level, successRate, goldCost) @@ -155,9 +161,9 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe }, nil } -func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) { - unlockLevels := s.catalog.SubStatusUnlockLvls[partDef.RarityType] - pool := s.catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId] +func grantPartsSubStatuses(catalog *masterdata.PartsCatalog, user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) { + unlockLevels := catalog.SubStatusUnlockLvls[partDef.RarityType] + pool := catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId] if len(pool) == 0 { return } @@ -173,13 +179,13 @@ func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string } pick := pool[rand.Intn(len(pool))] - def, ok := s.catalog.PartsStatusMainById[pick] + def, ok := catalog.PartsStatusMainById[pick] if !ok { continue } 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) } diff --git a/server/internal/service/quest_bighunt.go b/server/internal/service/quest_bighunt.go index 0397e17..6f48d58 100644 --- a/server/internal/service/quest_bighunt.go +++ b/server/internal/service/quest_bighunt.go @@ -8,7 +8,7 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" - "lunar-tear/server/internal/questflow" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -18,34 +18,35 @@ type BigHuntServiceServer struct { pb.UnimplementedBigHuntServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.BigHuntCatalog - engine *questflow.QuestHandler + holder *runtime.Holder } func NewBigHuntServiceServer( users store.UserRepository, sessions store.SessionRepository, - catalog *masterdata.BigHuntCatalog, - engine *questflow.QuestHandler, + holder *runtime.Holder, ) *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) { log.Printf("[BigHuntService] StartBigHuntQuest: bossQuestId=%d questId=%d deckNumber=%d isDryRun=%v", 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) nowMillis := gametime.NowMillis() - bhQuest, ok := s.catalog.QuestById[req.BigHuntQuestId] + bhQuest, ok := catalog.QuestById[req.BigHuntQuestId] if !ok { log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId) } s.users.UpdateUser(userId, func(user *store.UserState) { if ok { - s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis) + engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis) } user.BigHuntProgress = store.BigHuntProgress{ @@ -85,18 +86,21 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v", req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired) + cat := s.holder.Get() + catalog := cat.BigHunt + engine := cat.QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - bhQuest := s.catalog.QuestById[req.BigHuntQuestId] - bossQuest := s.catalog.BossQuestById[req.BigHuntBossQuestId] - boss := s.catalog.BossByBossId[bossQuest.BigHuntBossId] + bhQuest := catalog.QuestById[req.BigHuntQuestId] + bossQuest := catalog.BossQuestById[req.BigHuntBossQuestId] + boss := catalog.BossByBossId[bossQuest.BigHuntBossId] var scoreInfo *pb.BigHuntScoreInfo var scoreRewards []*pb.BigHuntReward 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 { user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis} @@ -108,7 +112,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F baseScore := totalDamage difficultyBonusPermil := int32(0) - if coeff, ok := s.catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok { + if coeff, ok := catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok { difficultyBonusPermil = coeff } @@ -138,7 +142,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F } schedKey := store.BigHuntScheduleScoreKey{ - BigHuntScheduleId: s.catalog.ActiveScheduleId, + BigHuntScheduleId: catalog.ActiveScheduleId, BigHuntBossId: bossQuest.BigHuntBossId, } oldSchedMax := user.BigHuntScheduleMaxScores[schedKey].MaxScore @@ -163,7 +167,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{ UserScore: userScore, @@ -177,12 +181,12 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F } if isHighScore { - rewardGroupId := s.catalog.ResolveActiveScoreRewardGroupId( + rewardGroupId := catalog.ResolveActiveScoreRewardGroupId( bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis) if rewardGroupId > 0 { - newItems := s.catalog.CollectNewRewards(rewardGroupId, oldMax, userScore) + newItems := catalog.CollectNewRewards(rewardGroupId, oldMax, userScore) 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{ PossessionType: item.PossessionType, PossessionId: item.PossessionId, @@ -216,16 +220,19 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F 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) + cat := s.holder.Get() + catalog := cat.BigHunt + engine := cat.QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - bhQuest := s.catalog.QuestById[req.BigHuntQuestId] + bhQuest := catalog.QuestById[req.BigHuntQuestId] var battleBinary []byte var deckNumber int32 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.LatestVersion = nowMillis @@ -302,6 +309,7 @@ func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *p func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) { log.Printf("[BigHuntService] GetBigHuntTopData") + catalog := s.holder.Get().BigHunt userId := CurrentUserId(ctx, s.users, s.sessions) user, _ := s.users.LoadUser(userId) @@ -309,13 +317,13 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb weeklyVersion := gametime.WeeklyVersion(nowMillis) var weeklyScoreResults []*pb.WeeklyScoreResult - for _, boss := range s.catalog.BossByBossId { + for _, boss := range catalog.BossByBossId { key := store.BigHuntWeeklyScoreKey{ BigHuntWeeklyVersion: weeklyVersion, AttributeType: boss.AttributeType, } ws := user.BigHuntWeeklyMaxScores[key] - gradeIconId := s.catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore) + gradeIconId := catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore) weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{ AttributeType: boss.AttributeType, @@ -330,10 +338,10 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb ws := user.BigHuntWeeklyStatuses[weeklyVersion] - weeklyRewards := s.resolveWeeklyRewards(user, weeklyVersion, nowMillis) + weeklyRewards := resolveBigHuntWeeklyRewards(catalog, user, weeklyVersion, nowMillis) lastWeekVersion := weeklyVersion - 7*24*60*60*1000 - lastWeekRewards := s.resolveWeeklyRewards(user, lastWeekVersion, nowMillis) + lastWeekRewards := resolveBigHuntWeeklyRewards(catalog, user, lastWeekVersion, nowMillis) return &pb.GetBigHuntTopDataResponse{ WeeklyScoreResult: weeklyScoreResults, @@ -343,14 +351,14 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb }, nil } -func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward { +func resolveBigHuntWeeklyRewards(catalog *masterdata.BigHuntCatalog, user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward { var rewards []*pb.BigHuntReward - for _, boss := range s.catalog.BossByBossId { + for _, boss := range catalog.BossByBossId { rewardKey := masterdata.BigHuntWeeklyRewardKey{ ScheduleId: 1, AttributeType: boss.AttributeType, } - rewardGroupId := s.catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) + rewardGroupId := catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) if rewardGroupId == 0 { continue } @@ -359,7 +367,7 @@ func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weekly AttributeType: boss.AttributeType, } 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{ PossessionType: item.PossessionType, PossessionId: item.PossessionId, diff --git a/server/internal/service/quest_event.go b/server/internal/service/quest_event.go index 113e5ed..8355d29 100644 --- a/server/internal/service/quest_event.go +++ b/server/internal/service/quest_event.go @@ -15,13 +15,14 @@ import ( func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) { log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() 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)) for i, d := range drops { 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) nowMillis := gametime.NowMillis() + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome 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{ @@ -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) { log.Printf("[QuestService] RestartEventQuest: chapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) s.users.UpdateUser(userId, func(user *store.UserState) { - s.engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis()) + engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis()) }) 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) { log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) 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 diff --git a/server/internal/service/quest_extra.go b/server/internal/service/quest_extra.go index 8a4b0be..51222b3 100644 --- a/server/internal/service/quest_extra.go +++ b/server/internal/service/quest_extra.go @@ -13,13 +13,14 @@ import ( 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) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() 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)) for i, d := range drops { 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) nowMillis := gametime.NowMillis() + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome 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{ @@ -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) { log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) var deckNumber int32 s.users.UpdateUser(userId, func(user *store.UserState) { - s.engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis()) + engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis()) deckNumber = user.Quests[req.QuestId].UserDeckNumber }) - drops := s.engine.BattleDropRewards(req.QuestId) + drops := engine.BattleDropRewards(req.QuestId) pbDrops := make([]*pb.BattleDropReward, len(drops)) for i, d := range drops { 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) { log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) 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 diff --git a/server/internal/service/quest_main.go b/server/internal/service/quest_main.go index 0d5584d..10cba8d 100644 --- a/server/internal/service/quest_main.go +++ b/server/internal/service/quest_main.go @@ -8,6 +8,7 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/model" "lunar-tear/server/internal/questflow" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -17,22 +18,23 @@ type QuestServiceServer struct { pb.UnimplementedQuestServiceServer users store.UserRepository sessions store.SessionRepository - engine *questflow.QuestHandler + holder *runtime.Holder } -func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *QuestServiceServer { - if engine == nil { - panic("quest handler is required") +func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *QuestServiceServer { + if holder == nil { + 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) { log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) 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 @@ -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) { log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) 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 @@ -52,9 +55,10 @@ func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) { log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) s.users.UpdateUser(userId, func(user *store.UserState) { - s.engine.HandleMainQuestSceneProgress(user, req.QuestSceneId) + engine.HandleMainQuestSceneProgress(user, req.QuestSceneId) }) 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) { log.Printf("[QuestService] StartMainQuest: %+v", req) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() s.users.UpdateUser(userId, func(user *store.UserState) { 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 { - s.engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) + engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) } }) - drops := s.engine.BattleDropRewards(req.QuestId) + drops := engine.BattleDropRewards(req.QuestId) pbDrops := make([]*pb.BattleDropReward, len(drops)) for i, d := range drops { 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) nowMillis := gametime.NowMillis() + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome 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{ @@ -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) { log.Printf("[QuestService] RestartMainQuest: questId=%d isMainFlow=%v", req.QuestId, req.IsMainFlow) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) var deckNumber int32 s.users.UpdateUser(userId, func(user *store.UserState) { - s.engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis()) + engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis()) deckNumber = user.Quests[req.QuestId].UserDeckNumber }) - drops := s.engine.BattleDropRewards(req.QuestId) + drops := engine.BattleDropRewards(req.QuestId) pbDrops := make([]*pb.BattleDropReward, len(drops)) for i, d := range drops { 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)) nowMillis := gametime.NowMillis() + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) var outcome questflow.FinishOutcome 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 } } - outcome = s.engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis) + outcome = engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis) }) return &pb.SkipQuestResponse{ @@ -184,10 +192,11 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) { log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId) + engine := s.holder.Get().QuestHandler userId := CurrentUserId(ctx, s.users, s.sessions) s.users.UpdateUser(userId, func(user *store.UserState) { user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId - if seasonId, ok := s.engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok { + if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok { user.MainQuest.MainQuestSeasonId = seasonId } now := gametime.NowMillis() diff --git a/server/internal/service/quest_sidestory.go b/server/internal/service/quest_sidestory.go index f5c711e..d1b6c2b 100644 --- a/server/internal/service/quest_sidestory.go +++ b/server/internal/service/quest_sidestory.go @@ -6,8 +6,8 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" - "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -15,11 +15,11 @@ type SideStoryQuestServiceServer struct { pb.UnimplementedSideStoryQuestServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.SideStoryCatalog + holder *runtime.Holder } -func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.SideStoryCatalog) *SideStoryQuestServiceServer { - return &SideStoryQuestServiceServer{users: users, sessions: sessions, catalog: catalog} +func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *SideStoryQuestServiceServer { + return &SideStoryQuestServiceServer{users: users, sessions: sessions, holder: holder} } func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) { @@ -27,7 +27,7 @@ func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Con userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - firstSceneId := s.catalog.FirstSceneByQuestId[req.SideStoryQuestId] + firstSceneId := s.holder.Get().SideStory.FirstSceneByQuestId[req.SideStoryQuestId] s.users.UpdateUser(userId, func(user *store.UserState) { existing, exists := user.SideStoryQuests[req.SideStoryQuestId] diff --git a/server/internal/service/reward.go b/server/internal/service/reward.go index 9adc07e..da94a60 100644 --- a/server/internal/service/reward.go +++ b/server/internal/service/reward.go @@ -8,6 +8,7 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" emptypb "google.golang.org/protobuf/types/known/emptypb" @@ -15,24 +16,25 @@ import ( type RewardServiceServer struct { pb.UnimplementedRewardServiceServer - users store.UserRepository - sessions store.SessionRepository - bhCatalog *masterdata.BigHuntCatalog - granter *store.PossessionGranter + users store.UserRepository + sessions store.SessionRepository + holder *runtime.Holder } func NewRewardServiceServer( users store.UserRepository, sessions store.SessionRepository, - bhCatalog *masterdata.BigHuntCatalog, - granter *store.PossessionGranter, + holder *runtime.Holder, ) *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) { log.Printf("[RewardService] ReceiveBigHuntReward") + cat := s.holder.Get() + bhCatalog := cat.BigHunt + granter := cat.QuestHandler.Granter userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() weeklyVersion := gametime.WeeklyVersion(nowMillis) @@ -45,13 +47,13 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty ws := user.BigHuntWeeklyStatuses[weeklyVersion] isReceived = ws.IsReceivedWeeklyReward - for _, boss := range s.bhCatalog.BossByBossId { + for _, boss := range bhCatalog.BossByBossId { key := store.BigHuntWeeklyScoreKey{ BigHuntWeeklyVersion: weeklyVersion, AttributeType: boss.AttributeType, } wms := user.BigHuntWeeklyMaxScores[key] - gradeIcon := s.bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore) + gradeIcon := bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore) weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{ AttributeType: boss.AttributeType, BeforeMaxScore: wms.MaxScore, @@ -64,12 +66,12 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty } if !isReceived { - for _, boss := range s.bhCatalog.BossByBossId { + for _, boss := range bhCatalog.BossByBossId { rewardKey := masterdata.BigHuntWeeklyRewardKey{ ScheduleId: 1, AttributeType: boss.AttributeType, } - rewardGroupId := s.bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) + rewardGroupId := bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) if rewardGroupId == 0 { continue } @@ -80,9 +82,9 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty } maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore - items := s.bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore) + items := bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore) 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{ PossessionType: item.PossessionType, PossessionId: item.PossessionId, diff --git a/server/internal/service/shop.go b/server/internal/service/shop.go index 2ebfed3..0fad974 100644 --- a/server/internal/service/shop.go +++ b/server/internal/service/shop.go @@ -9,6 +9,7 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" "google.golang.org/protobuf/types/known/emptypb" @@ -18,23 +19,25 @@ type ShopServiceServer struct { pb.UnimplementedShopServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.ShopCatalog - granter *store.PossessionGranter + holder *runtime.Holder } -func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ShopCatalog, granter *store.PossessionGranter) *ShopServiceServer { - return &ShopServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter} +func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ShopServiceServer { + return &ShopServiceServer{users: users, sessions: sessions, holder: holder} } 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) + cat := s.holder.Get() + catalog := cat.Shop + granter := cat.QuestHandler.Granter userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() _, err := s.users.UpdateUser(userId, func(user *store.UserState) { for shopItemId, qty := range req.ShopItems { - item, ok := s.catalog.Items[shopItemId] + item, ok := catalog.Items[shopItemId] if !ok { log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId) continue @@ -46,8 +49,8 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu continue } - for _, content := range s.catalog.Contents[shopItemId] { - s.granter.GrantFull(user, + for _, content := range catalog.Contents[shopItemId] { + granter.GrantFull(user, model.PossessionType(content.PossessionType), content.PossessionId, 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.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) { log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed) + catalog := s.holder.Get().Shop userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() _, err := s.users.UpdateUser(userId, func(user *store.UserState) { - if len(user.ShopReplaceableLineup) == 0 && len(s.catalog.ItemShopPool) > 0 { - for i, itemId := range s.catalog.ItemShopPool { + if len(user.ShopReplaceableLineup) == 0 && len(catalog.ItemShopPool) > 0 { + for i, itemId := range catalog.ItemShopPool { slot := int32(i + 1) user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{ SlotNumber: slot, @@ -93,7 +97,7 @@ func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.Refresh if req.IsGemUsed { user.ShopReplaceable.LineupUpdateCount++ user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis - for _, itemId := range s.catalog.ItemShopPool { + for _, itemId := range catalog.ItemShopPool { if si, ok := user.ShopItems[itemId]; ok { si.BoughtCount = 0 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", req.ShopId, req.ShopItemId, req.ProductId) + cat := s.holder.Get() + catalog := cat.Shop + granter := cat.QuestHandler.Granter userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() _, err := s.users.UpdateUser(userId, func(user *store.UserState) { - item, ok := s.catalog.Items[req.ShopItemId] + item, ok := catalog.Items[req.ShopItemId] if !ok { log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId) return @@ -134,8 +141,8 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req * log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err) } - for _, content := range s.catalog.Contents[req.ShopItemId] { - s.granter.GrantFull(user, + for _, content := range catalog.Contents[req.ShopItemId] { + granter.GrantFull(user, model.PossessionType(content.PossessionType), content.PossessionId, 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.ShopItemId = req.ShopItemId si.BoughtCount++ 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 } } @@ -182,12 +189,12 @@ func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context, }, nil } -func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemId, qty int32, nowMillis int64) { - for _, effect := range s.catalog.Effects[shopItemId] { +func applyShopContentEffects(catalog *masterdata.ShopCatalog, user *store.UserState, shopItemId, qty int32, nowMillis int64) { + for _, effect := range catalog.Effects[shopItemId] { switch effect.EffectTargetType { case model.EffectTargetStaminaRecovery: - maxMillis := s.catalog.MaxStaminaMillis[user.Status.Level] - millis := s.resolveEffectMillis(effect.EffectValueType, effect.EffectValue, user.Status.Level) + maxMillis := catalog.MaxStaminaMillis[user.Status.Level] + millis := resolveShopEffectMillis(catalog, effect.EffectValueType, effect.EffectValue, user.Status.Level) store.RecoverStamina(user, millis*qty, maxMillis, nowMillis) default: log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType) @@ -195,12 +202,12 @@ func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemI } } -func (s *ShopServiceServer) resolveEffectMillis(effectValueType, effectValue, userLevel int32) int32 { +func resolveShopEffectMillis(catalog *masterdata.ShopCatalog, effectValueType, effectValue, userLevel int32) int32 { switch effectValueType { case model.EffectValueFixed: return effectValue case model.EffectValuePermil: - maxMillis := s.catalog.MaxStaminaMillis[userLevel] + maxMillis := catalog.MaxStaminaMillis[userLevel] return effectValue * maxMillis / 1000 default: return 0 diff --git a/server/internal/service/tutorial.go b/server/internal/service/tutorial.go index e0a0a61..f9d3df8 100644 --- a/server/internal/service/tutorial.go +++ b/server/internal/service/tutorial.go @@ -8,6 +8,7 @@ import ( "lunar-tear/server/internal/gametime" "lunar-tear/server/internal/model" "lunar-tear/server/internal/questflow" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -15,17 +16,18 @@ type TutorialServiceServer struct { pb.UnimplementedTutorialServiceServer users store.UserRepository sessions store.SessionRepository - engine *questflow.QuestHandler + holder *runtime.Holder } -func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *TutorialServiceServer { - return &TutorialServiceServer{users: users, sessions: sessions, engine: engine} +func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *TutorialServiceServer { + return &TutorialServiceServer{users: users, sessions: sessions, holder: holder} } func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb.SetTutorialProgressRequest) (*pb.SetTutorialProgressResponse, error) { log.Printf("[TutorialService] SetTutorialProgress: type=%d phase=%d choice=%d", req.TutorialType, req.ProgressPhase, req.ChoiceId) userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() + engine := s.holder.Get().QuestHandler var grants []questflow.RewardGrant s.users.UpdateUser(userId, func(user *store.UserState) { existing, exists := user.Tutorials[req.TutorialType] @@ -36,7 +38,7 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb 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 { store.EnsureDefaultDeck(user, nowMillis) } diff --git a/server/internal/service/weapon.go b/server/internal/service/weapon.go index 48427bc..1c78640 100644 --- a/server/internal/service/weapon.go +++ b/server/internal/service/weapon.go @@ -10,6 +10,7 @@ import ( "lunar-tear/server/internal/gameutil" "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" + "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" ) @@ -17,12 +18,11 @@ type WeaponServiceServer struct { pb.UnimplementedWeaponServiceServer users store.UserRepository sessions store.SessionRepository - catalog *masterdata.WeaponCatalog - config *masterdata.GameConfig + holder *runtime.Holder } -func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.WeaponCatalog, config *masterdata.GameConfig) *WeaponServiceServer { - return &WeaponServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} +func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *WeaponServiceServer { + return &WeaponServiceServer{users: users, sessions: sessions, holder: holder} } 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) { 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) nowMillis := gametime.NowMillis() @@ -82,7 +85,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh return } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] EnhanceByMaterial: weapon master id=%d not found", weapon.WeaponId) return @@ -91,7 +94,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh totalExp := int32(0) totalMaterialCount := int32(0) for materialId, count := range req.Materials { - mat, ok := s.catalog.Materials[materialId] + mat, ok := catalog.Materials[materialId] if !ok { log.Printf("[WeaponService] EnhanceByMaterial: material id=%d not found, skipping", materialId) continue @@ -107,19 +110,19 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh expPerUnit := mat.EffectValue if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType { - expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 + expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000 } 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost log.Printf("[WeaponService] EnhanceByMaterial: gold cost=%d (materials=%d)", goldCost, totalMaterialCount) } weapon.Exp += totalExp - if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { + if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) } @@ -127,7 +130,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh user.Weapons[req.UserWeaponUuid] = weapon log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) - s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) + checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis) }) if err != nil { return nil, fmt.Errorf("weapon enhance by material: %w", err) @@ -142,6 +145,9 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) { 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) _, err := s.users.UpdateUser(userId, func(user *store.UserState) { @@ -153,17 +159,17 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p continue } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] Sell: weapon master id=%d not found, skipping", weapon.WeaponId) continue } - if sellFunc, ok := s.catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { + if sellFunc, ok := catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 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 { user.ConsumableItems[itemId] += count } @@ -176,7 +182,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p } if totalGold > 0 { - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold log.Printf("[WeaponService] Sell: granted %d gold", totalGold) } }) @@ -190,6 +196,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) { 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) nowMillis := gametime.NowMillis() @@ -200,20 +209,20 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) return } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] Evolve: weapon master id=%d not found", weapon.WeaponId) return } - evolvedId, ok := s.catalog.EvolutionNextWeaponId[weapon.WeaponId] + evolvedId, ok := catalog.EvolutionNextWeaponId[weapon.WeaponId] if !ok { log.Printf("[WeaponService] Evolve: no evolution for weaponId=%d", weapon.WeaponId) return } totalMaterialCount := int32(0) - mats := s.catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId] + mats := catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId] for _, mat := range mats { cur := user.Materials[mat.MaterialId] cost := mat.Count @@ -225,9 +234,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost) } @@ -235,9 +244,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) weapon.LatestVersion = nowMillis user.Weapons[req.UserWeaponUuid] = weapon - evolvedMaster, ok := s.catalog.Weapons[evolvedId] + evolvedMaster, ok := catalog.Weapons[evolvedId] 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)) for i, slot := range slots { abilities[i] = store.WeaponAbilityState{ @@ -252,7 +261,7 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId) - s.checkWeaponStoryUnlocks(user, evolvedId, weapon.Level, nowMillis) + checkWeaponStoryUnlocks(catalog, user, evolvedId, weapon.Level, nowMillis) }) if err != nil { return nil, fmt.Errorf("weapon evolve: %w", err) @@ -264,6 +273,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) 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) + cat := s.holder.Get() + catalog := cat.Weapon + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -274,13 +286,13 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS return } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] EnhanceSkill: weapon master id=%d not found", weapon.WeaponId) return } - groupRows := s.catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId] + groupRows := catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId] var skillGroup *masterdata.EntityMWeaponSkillGroup for i := range groupRows { if groupRows[i].SkillId == req.SkillId { @@ -306,7 +318,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS return } - maxLevelFunc, ok := s.catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] + maxLevelFunc, ok := catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] if !ok { log.Printf("[WeaponService] EnhanceSkill: no max skill level func for enhanceId=%d", wm.WeaponSpecificEnhanceId) return @@ -326,7 +338,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS enhanceMatId := skillGroup.WeaponSkillEnhancementMaterialId for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { key := [2]int32{enhanceMatId, lvl} - mats := s.catalog.SkillEnhanceMats[key] + mats := catalog.SkillEnhanceMats[key] for _, mat := range mats { cur := user.Materials[mat.MaterialId] cost := mat.Count @@ -337,9 +349,9 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost } } @@ -360,6 +372,9 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS 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) + cat := s.holder.Get() + catalog := cat.Weapon + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -370,13 +385,13 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc return } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] EnhanceAbility: weapon master id=%d not found", weapon.WeaponId) return } - groupRows := s.catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId] + groupRows := catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId] var abilityGroup *masterdata.EntityMWeaponAbilityGroup for i := range groupRows { if groupRows[i].AbilityId == req.AbilityId { @@ -402,7 +417,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc return } - maxLevelFunc, ok := s.catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] + maxLevelFunc, ok := catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] if !ok { log.Printf("[WeaponService] EnhanceAbility: no max ability level func for enhanceId=%d", wm.WeaponSpecificEnhanceId) return @@ -422,7 +437,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc enhanceMatId := abilityGroup.WeaponAbilityEnhancementMaterialId for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { key := [2]int32{enhanceMatId, lvl} - mats := s.catalog.AbilityEnhanceMats[key] + mats := catalog.AbilityEnhanceMats[key] for _, mat := range mats { cur := user.Materials[mat.MaterialId] cost := mat.Count @@ -433,9 +448,9 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost } } @@ -456,6 +471,9 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc 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) + cat := s.holder.Get() + catalog := cat.Weapon + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -466,18 +484,18 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb. return } - if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount { + if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount { log.Printf("[WeaponService] LimitBreakByMaterial: already at max limit break %d", weapon.LimitBreakCount) return } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] LimitBreakByMaterial: weapon master id=%d not found", weapon.WeaponId) return } - remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount + remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount totalMaterialCount := int32(0) for materialId, count := range req.Materials { @@ -496,9 +514,9 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb. 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost log.Printf("[WeaponService] LimitBreakByMaterial: gold cost=%d", goldCost) } @@ -525,6 +543,9 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb. 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) + cat := s.holder.Get() + catalog := cat.Weapon + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -535,18 +556,18 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li return } - if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount { + if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount { log.Printf("[WeaponService] LimitBreakByWeapon: already at max limit break %d", weapon.LimitBreakCount) return } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] LimitBreakByWeapon: weapon master id=%d not found", weapon.WeaponId) return } - remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount + remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount consumedCount := int32(0) for _, uuid := range req.MaterialUserWeaponUuids { @@ -560,7 +581,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li continue } - if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { + if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { for itemId, count := range medals { user.ConsumableItems[itemId] += count } @@ -573,9 +594,9 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost log.Printf("[WeaponService] LimitBreakByWeapon: gold cost=%d", goldCost) } @@ -602,6 +623,9 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li 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) + cat := s.holder.Get() + catalog := cat.Weapon + config := cat.GameConfig userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() @@ -612,7 +636,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan return } - wm, ok := s.catalog.Weapons[weapon.WeaponId] + wm, ok := catalog.Weapons[weapon.WeaponId] if !ok { log.Printf("[WeaponService] EnhanceByWeapon: weapon master id=%d not found", weapon.WeaponId) return @@ -627,19 +651,19 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan continue } - matMaster, ok := s.catalog.Weapons[matWeapon.WeaponId] + matMaster, ok := catalog.Weapons[matWeapon.WeaponId] if !ok { log.Printf("[WeaponService] EnhanceByWeapon: material weapon master id=%d not found, skipping", matWeapon.WeaponId) continue } - baseExp := s.catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId] + baseExp := catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId] if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType { - baseExp = baseExp * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 + baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000 } totalExp += baseExp - if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { + if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { for itemId, count := range medals { user.ConsumableItems[itemId] += count } @@ -652,14 +676,14 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan 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) - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost log.Printf("[WeaponService] EnhanceByWeapon: gold cost=%d (weapons=%d)", goldCost, consumedCount) } weapon.Exp += totalExp - if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { + if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) } @@ -667,7 +691,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan user.Weapons[req.UserWeaponUuid] = weapon log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) - s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) + checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis) }) if err != nil { return nil, fmt.Errorf("weapon enhance by weapon: %w", err) @@ -679,13 +703,13 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan }, nil } -func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, weaponId, level int32, nowMillis int64) { - wm, ok := s.catalog.Weapons[weaponId] +func checkWeaponStoryUnlocks(catalog *masterdata.WeaponCatalog, user *store.UserState, weaponId, level int32, nowMillis int64) { + wm, ok := catalog.Weapons[weaponId] if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 { return } - evoOrder, hasEvo := s.catalog.EvolutionOrder[weaponId] - conditions := s.catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId] + evoOrder, hasEvo := catalog.EvolutionOrder[weaponId] + conditions := catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId] for _, cond := range conditions { switch model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) { @@ -696,14 +720,14 @@ func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, wea store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) } 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) { store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) } } case model.WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel: 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) { store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) } @@ -720,6 +744,9 @@ func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, wea func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) { 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) nowMillis := gametime.NowMillis() @@ -730,7 +757,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe return } - awakenRow, ok := s.catalog.AwakenByWeaponId[weapon.WeaponId] + awakenRow, ok := catalog.AwakenByWeaponId[weapon.WeaponId] if !ok { log.Printf("[WeaponService] Awaken: no awaken data for weaponId=%d", weapon.WeaponId) return @@ -741,7 +768,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe return } - mats := s.catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId] + mats := catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId] for _, mat := range mats { cur := user.Materials[mat.MaterialId] cost := mat.Count @@ -753,7 +780,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe } 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) }