mirror of Walter-Sparrow / lunar-tear
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add admin API for content reload

+992 -638
+45 -9
README.md
··· 46 46 | `--grpc-port` | `8003` | gRPC server port | 47 47 | `--cdn-port` | `8080` | CDN server port | 48 48 | `--auth-port` | `3000` | Auth server port | 49 + | `--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. | 49 50 50 51 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. 51 52 ··· 172 173 | `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr | 173 174 | `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear | 174 175 | `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear | 176 + | `--admin.listen` | *(empty)* | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. | 175 177 | `--no-color` | `false` | disable colored output | 176 178 177 179 ### Ports ··· 180 182 | -------- | ---- | ------------- | ----------------------------------------------------------- | 181 183 | gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) | 182 184 | HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages | 185 + | HTTP | 8082 | `lunar-tear` | admin webhook (`/api/admin/master-data/reload`); loopback by default, only binds when `LUNAR_ADMIN_TOKEN` is set | 186 + | HTTP | 3000 | `auth-server` | account registration and login | 183 187 184 188 ### Game Server Flags (`lunar-tear`) 185 189 186 - | Flag | Default | Description | 187 - | --------------- | ----------------- | ---------------------------------------------------- | 188 - | `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | 189 - | `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | 190 - | `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | 191 - | `--db` | `db/game.db` | SQLite database path | 192 - | `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | 190 + | Flag | Default | Description | 191 + | ---------------- | ----------------- | ---------------------------------------------------- | 192 + | `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | 193 + | `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | 194 + | `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | 195 + | `--db` | `db/game.db` | SQLite database path | 196 + | `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | 197 + | `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. | 198 + 199 + ### Live Master Data Reload 200 + 201 + The game server reads its master data from `assets/release/20240404193219.bin.e` at startup. To swap in updated content **without restarting** the server: 202 + 203 + 1. Replace `assets/release/20240404193219.bin.e` on disk with your edited copy. 204 + 2. POST to the admin webhook with a Bearer token matching `LUNAR_ADMIN_TOKEN`: 205 + 206 + ```bash 207 + curl -X POST -H "Authorization: Bearer ${LUNAR_ADMIN_TOKEN}" \ 208 + http://127.0.0.1:8082/api/admin/master-data/reload 209 + ``` 210 + 211 + 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. 212 + 213 + Security defaults are fail-closed: 214 + 215 + - `LUNAR_ADMIN_TOKEN` **must** be set in the environment, or the webhook listener never binds. 216 + - `--admin-listen` defaults to `127.0.0.1:8082` (loopback only). Bind to `0.0.0.0` only if you intend to expose it. 217 + - Authentication uses constant-time Bearer-token comparison. 193 218 194 219 ### CDN Flags (`octo-cdn`) 195 220 ··· 214 239 215 240 | Service | Image | Default Port | Notes | 216 241 | -------- | --------------------------- | ------------ | ------------------------------ | 217 - | `server` | `kretts/lunar-tear:latest` | 8003 | gRPC game server | 242 + | `server` | `kretts/lunar-tear:latest` | 8003, 8082 | gRPC game server + admin webhook | 218 243 | `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN | 219 244 | `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login | 220 245 221 - 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. 246 + The game server is configured via environment variables in the compose file: 247 + 248 + | Env var | Description | 249 + | --------------------- | -------------------------------------------------------------------------------------------- | 250 + | `LUNAR_LISTEN` | gRPC bind address | 251 + | `LUNAR_PUBLIC_ADDR` | Client-facing address advertised to the game | 252 + | `LUNAR_OCTO_URL` | CDN base URL the client uses for assets | 253 + | `LUNAR_AUTH_URL` | Auth server base URL (optional) | 254 + | `LUNAR_ADMIN_LISTEN` | Admin webhook bind address inside the container (compose default: `0.0.0.0:8082`) | 255 + | `LUNAR_ADMIN_TOKEN` | Bearer token for the admin webhook. **The webhook does not bind unless this is set.** | 256 + 257 + 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. 222 258 223 259 ### Makefile Targets 224 260
+22 -5
server/cmd/dev/main.go
··· 93 93 grpcOctoURL := flag.String("grpc.octo-url", "", "Octo CDN base URL passed to lunar-tear (default: derived from cdn.public-addr)") 94 94 grpcAuthURL := flag.String("grpc.auth-url", "", "auth server base URL passed to lunar-tear (default: derived from auth.listen)") 95 95 96 + // admin webhook is opt-in; empty leaves lunar-tear's own default in place 97 + // (the listener still only binds if LUNAR_ADMIN_TOKEN is set in the env). 98 + 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.") 99 + 96 100 noColor := flag.Bool("no-color", false, "disable colored output") 97 101 flag.Parse() 98 102 ··· 139 143 label: "grpc", 140 144 color: colorYellow, 141 145 cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext), 142 - "--listen", *grpcListen, 143 - "--public-addr", *grpcPublicAddr, 144 - "--db", *grpcDB, 145 - "--octo-url", *grpcOctoURL, 146 - "--auth-url", *grpcAuthURL, 146 + grpcArgs(*grpcListen, *grpcPublicAddr, *grpcDB, *grpcOctoURL, *grpcAuthURL, *adminListen)..., 147 147 ), 148 148 }, 149 149 } ··· 200 200 fmt.Printf("%s%s\n", prefix, scanner.Text()) 201 201 } 202 202 } 203 + 204 + // grpcArgs assembles the argv for the lunar-tear subprocess. The admin flag 205 + // is appended only when --admin.listen was supplied so we don't override 206 + // lunar-tear's own default when the operator hasn't opted in. 207 + func grpcArgs(listen, publicAddr, db, octoURL, authURL, adminListen string) []string { 208 + args := []string{ 209 + "--listen", listen, 210 + "--public-addr", publicAddr, 211 + "--db", db, 212 + "--octo-url", octoURL, 213 + "--auth-url", authURL, 214 + } 215 + if adminListen != "" { 216 + args = append(args, "--admin-listen", adminListen) 217 + } 218 + return args 219 + }
+56
server/cmd/lunar-tear/admin.go
··· 1 + package main 2 + 3 + import ( 4 + "crypto/subtle" 5 + "log" 6 + "net/http" 7 + "os" 8 + 9 + "lunar-tear/server/internal/runtime" 10 + ) 11 + 12 + // startAdmin spins up the admin webhook used by external content tools to 13 + // trigger an in-place re-read of assets/release/20240404193219.bin.e. 14 + // 15 + // Authentication: Bearer token via the LUNAR_ADMIN_TOKEN environment variable. 16 + // If LUNAR_ADMIN_TOKEN is unset or empty the listener does not bind at all 17 + // (fail closed), so a fresh deploy never exposes an unauthenticated endpoint. 18 + // 19 + // The default --admin-listen is 127.0.0.1:8082 so the webhook is only 20 + // reachable via loopback unless the operator opts in by binding to 0.0.0.0. 21 + func startAdmin(listen string, holder *runtime.Holder) { 22 + token := os.Getenv("LUNAR_ADMIN_TOKEN") 23 + if token == "" { 24 + log.Println("[admin] disabled (no LUNAR_ADMIN_TOKEN set)") 25 + return 26 + } 27 + expected := []byte("Bearer " + token) 28 + 29 + mux := http.NewServeMux() 30 + mux.HandleFunc("/api/admin/master-data/reload", func(w http.ResponseWriter, r *http.Request) { 31 + if r.Method != http.MethodPost { 32 + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) 33 + return 34 + } 35 + got := []byte(r.Header.Get("Authorization")) 36 + if len(got) != len(expected) || subtle.ConstantTimeCompare(got, expected) != 1 { 37 + http.Error(w, "unauthorized", http.StatusUnauthorized) 38 + return 39 + } 40 + if err := holder.Reload(); err != nil { 41 + log.Printf("[admin] master-data reload failed: %v", err) 42 + http.Error(w, "master-data reload failed", http.StatusInternalServerError) 43 + return 44 + } 45 + log.Printf("[admin] master-data reloaded by %s", r.RemoteAddr) 46 + w.Header().Set("Content-Type", "application/json") 47 + _, _ = w.Write([]byte(`{"ok":true}`)) 48 + }) 49 + 50 + log.Printf("[admin] webhook listener on %s (token-gated)", listen) 51 + go func() { 52 + if err := http.ListenAndServe(listen, mux); err != nil { 53 + log.Printf("[admin] webhook listener failed: %v", err) 54 + } 55 + }() 56 + }
+26 -94
server/cmd/lunar-tear/grpc.go
··· 6 6 "strconv" 7 7 8 8 pb "lunar-tear/server/gen/proto" 9 - "lunar-tear/server/internal/gacha" 10 9 "lunar-tear/server/internal/interceptor" 11 - "lunar-tear/server/internal/masterdata" 12 - "lunar-tear/server/internal/questflow" 10 + "lunar-tear/server/internal/runtime" 13 11 "lunar-tear/server/internal/service" 14 12 "lunar-tear/server/internal/store" 15 13 ··· 40 38 store.UserRepository 41 39 store.SessionRepository 42 40 }, 43 - questEngine *questflow.QuestHandler, 44 - gachaHandler *gacha.GachaHandler, 45 - gachaEntries []store.GachaCatalogEntry, 46 - cageOrnamentCatalog *masterdata.CageOrnamentCatalog, 47 - loginBonusCatalog *masterdata.LoginBonusCatalog, 48 - characterViewerCatalog *masterdata.CharacterViewerCatalog, 49 - shopCatalog *masterdata.ShopCatalog, 50 - costumeCatalog *masterdata.CostumeCatalog, 51 - omikujiCatalog *masterdata.OmikujiCatalog, 52 - weaponCatalog *masterdata.WeaponCatalog, 53 - exploreCatalog *masterdata.ExploreCatalog, 54 - gimmickCatalog *masterdata.GimmickCatalog, 55 - characterBoardCatalog *masterdata.CharacterBoardCatalog, 56 - partsCatalog *masterdata.PartsCatalog, 57 - characterRebirthCatalog *masterdata.CharacterRebirthCatalog, 58 - companionCatalog *masterdata.CompanionCatalog, 59 - materialCatalog *masterdata.MaterialCatalog, 60 - consumableItemCatalog *masterdata.ConsumableItemCatalog, 61 - gameConfig *masterdata.GameConfig, 62 - sideStoryCatalog *masterdata.SideStoryCatalog, 63 - bigHuntCatalog *masterdata.BigHuntCatalog, 41 + holder *runtime.Holder, 64 42 ) *grpc.Server { 65 43 lis, err := net.Listen("tcp", listenAddr) 66 44 if err != nil { ··· 74 52 grpc.UnknownServiceHandler(interceptor.UnknownService), 75 53 ) 76 54 77 - registerServices(grpcServer, 78 - publicAddr, 79 - octoURL, 80 - authURL, 81 - userStore, 82 - questEngine, 83 - gachaHandler, 84 - gachaEntries, 85 - cageOrnamentCatalog, 86 - loginBonusCatalog, 87 - characterViewerCatalog, 88 - shopCatalog, 89 - costumeCatalog, 90 - omikujiCatalog, 91 - weaponCatalog, 92 - exploreCatalog, 93 - gimmickCatalog, 94 - characterBoardCatalog, 95 - partsCatalog, 96 - characterRebirthCatalog, 97 - companionCatalog, 98 - materialCatalog, 99 - consumableItemCatalog, 100 - gameConfig, 101 - sideStoryCatalog, 102 - bigHuntCatalog, 103 - ) 55 + registerServices(grpcServer, publicAddr, octoURL, authURL, userStore, holder) 104 56 105 57 reflection.Register(grpcServer) 106 58 ··· 124 76 store.UserRepository 125 77 store.SessionRepository 126 78 }, 127 - questEngine *questflow.QuestHandler, 128 - gachaHandler *gacha.GachaHandler, 129 - gachaEntries []store.GachaCatalogEntry, 130 - cageOrnamentCatalog *masterdata.CageOrnamentCatalog, 131 - loginBonusCatalog *masterdata.LoginBonusCatalog, 132 - characterViewerCatalog *masterdata.CharacterViewerCatalog, 133 - shopCatalog *masterdata.ShopCatalog, 134 - costumeCatalog *masterdata.CostumeCatalog, 135 - omikujiCatalog *masterdata.OmikujiCatalog, 136 - weaponCatalog *masterdata.WeaponCatalog, 137 - exploreCatalog *masterdata.ExploreCatalog, 138 - gimmickCatalog *masterdata.GimmickCatalog, 139 - characterBoardCatalog *masterdata.CharacterBoardCatalog, 140 - partsCatalog *masterdata.PartsCatalog, 141 - characterRebirthCatalog *masterdata.CharacterRebirthCatalog, 142 - companionCatalog *masterdata.CompanionCatalog, 143 - materialCatalog *masterdata.MaterialCatalog, 144 - consumableItemCatalog *masterdata.ConsumableItemCatalog, 145 - gameConfig *masterdata.GameConfig, 146 - sideStoryCatalog *masterdata.SideStoryCatalog, 147 - bigHuntCatalog *masterdata.BigHuntCatalog, 79 + holder *runtime.Holder, 148 80 ) { 149 81 pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr) 150 82 pubPort, _ := strconv.Atoi(pubPortStr) 151 83 152 - pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(gachaEntries)) 84 + pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(holder)) 153 85 pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL)) 154 86 pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore)) 155 87 pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL)) 156 88 pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore)) 157 - pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine)) 158 - pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, gachaEntries, gachaHandler)) 89 + pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, holder)) 90 + pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, holder)) 159 91 pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore)) 160 92 pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer()) 161 - pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, gimmickCatalog)) 162 - pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, questEngine)) 93 + pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, holder)) 94 + pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, holder)) 163 95 pb.RegisterNotificationServiceServer(srv, service.NewNotificationServiceServer(userStore, userStore)) 164 - pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, cageOrnamentCatalog, questEngine.Granter)) 96 + pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, holder)) 165 97 pb.RegisterDeckServiceServer(srv, service.NewDeckServiceServer(userStore, userStore)) 166 98 pb.RegisterFriendServiceServer(srv, service.NewFriendServiceServer(userStore, userStore)) 167 - pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, loginBonusCatalog)) 99 + pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, holder)) 168 100 pb.RegisterNaviCutInServiceServer(srv, service.NewNaviCutInServiceServer(userStore, userStore)) 169 101 pb.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore)) 170 102 pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(userStore, userStore)) 171 103 pb.RegisterPortalCageServiceServer(srv, service.NewPortalCageServiceServer(userStore, userStore)) 172 - pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, characterViewerCatalog)) 104 + pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, holder)) 173 105 pb.RegisterMissionServiceServer(srv, service.NewMissionServiceServer(userStore, userStore)) 174 - pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, shopCatalog, questEngine.Granter)) 175 - pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, costumeCatalog, gameConfig)) 106 + pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, holder)) 107 + pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, holder)) 176 108 pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore)) 177 - pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, omikujiCatalog)) 178 - pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, weaponCatalog, gameConfig)) 179 - pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, exploreCatalog)) 180 - pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, characterBoardCatalog)) 181 - pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, partsCatalog, gameConfig)) 182 - pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, characterRebirthCatalog, gameConfig)) 183 - pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, companionCatalog, gameConfig)) 184 - pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, materialCatalog, gameConfig)) 185 - pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, consumableItemCatalog, gameConfig)) 186 - pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, sideStoryCatalog)) 187 - pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine)) 188 - pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter)) 109 + pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, holder)) 110 + pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, holder)) 111 + pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, holder)) 112 + pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, holder)) 113 + pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, holder)) 114 + pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, holder)) 115 + pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, holder)) 116 + pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, holder)) 117 + pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, holder)) 118 + pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, holder)) 119 + pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, holder)) 120 + pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, holder)) 189 121 }
+9 -156
server/cmd/lunar-tear/main.go
··· 9 9 "syscall" 10 10 11 11 "lunar-tear/server/internal/database" 12 - "lunar-tear/server/internal/gacha" 13 12 "lunar-tear/server/internal/gametime" 14 - "lunar-tear/server/internal/masterdata" 15 - "lunar-tear/server/internal/masterdata/memorydb" 16 - "lunar-tear/server/internal/questflow" 13 + "lunar-tear/server/internal/runtime" 17 14 "lunar-tear/server/internal/store/sqlite" 18 15 ) 19 16 17 + const masterDataPath = "assets/release/20240404193219.bin.e" 18 + 20 19 func main() { 21 20 listen := flag.String("listen", "0.0.0.0:443", "gRPC listen address (host:port)") 22 21 publicAddr := flag.String("public-addr", "127.0.0.1:443", "externally-reachable host:port advertised to clients") 23 22 dbPath := flag.String("db", "db/game.db", "SQLite database path") 24 23 octoURL := flag.String("octo-url", "", "Octo CDN base URL the client will use for assets (e.g. http://10.0.2.2:8080)") 25 24 authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)") 25 + 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.") 26 26 flag.Parse() 27 27 28 28 if *octoURL == "" { 29 29 log.Fatalf("--octo-url is required (e.g. http://10.0.2.2:8080)") 30 30 } 31 31 32 - if err := memorydb.Init("assets/release/20240404193219.bin.e"); err != nil { 33 - log.Fatalf("load master data: %v", err) 32 + holder, err := runtime.NewHolder(masterDataPath) 33 + if err != nil { 34 + log.Fatalf("init master data: %v", err) 34 35 } 35 - log.Printf("master data loaded (%d tables)", memorydb.TableCount()) 36 36 37 37 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 38 38 defer stop() ··· 44 44 defer db.Close() 45 45 log.Printf("database opened: %s", *dbPath) 46 46 47 - gameConfig, err := masterdata.LoadGameConfig() 48 - if err != nil { 49 - log.Fatalf("load game config: %v", err) 50 - } 51 - log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)", 52 - gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold) 53 - 54 - partsCatalog, err := masterdata.LoadPartsCatalog() 55 - if err != nil { 56 - log.Fatalf("load parts catalog: %v", err) 57 - } 58 - log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType)) 59 - 60 - questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog) 61 - if err != nil { 62 - log.Fatalf("load quest catalog: %v", err) 63 - } 64 - questHandler := questflow.NewQuestHandler(questCatalog, gameConfig) 65 47 userStore := sqlite.New(db, gametime.Now) 66 48 67 - gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog() 68 - if err != nil { 69 - log.Fatalf("load gacha catalog: %v", err) 70 - } 71 - log.Printf("gacha catalog loaded: %d entries", len(gachaEntries)) 49 + grpcServer := startGRPC(*listen, *publicAddr, *octoURL, *authURL, userStore, holder) 72 50 73 - gachaPool, err := masterdata.LoadGachaPool() 74 - if err != nil { 75 - log.Fatalf("load gacha pool: %v", err) 76 - } 77 - log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d", 78 - len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials)) 79 - 80 - shopCatalog, err := masterdata.LoadShopCatalog() 81 - if err != nil { 82 - log.Fatalf("load shop catalog: %v", err) 83 - } 84 - log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops", 85 - len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells)) 86 - 87 - gachaPool.BuildShopFeatured(shopCatalog) 88 - gachaPool.PruneUnpairedCostumes() 89 - gachaPool.BuildFeaturedMapping(gachaEntries) 90 - gachaPool.BuildBannerPools(gachaEntries) 91 - masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool) 92 - 93 - dupExchange, err := masterdata.LoadDupExchange() 94 - if err != nil { 95 - log.Fatalf("load dup exchange: %v", err) 96 - } 97 - dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool) 98 - if err != nil { 99 - log.Fatalf("enrich dup exchange: %v", err) 100 - } 101 - log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded) 102 - 103 - gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange) 104 - 105 - conditionResolver, err := masterdata.LoadConditionResolver() 106 - if err != nil { 107 - log.Fatalf("load condition resolver: %v", err) 108 - } 109 - 110 - cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog() 111 - loginBonusCatalog := masterdata.LoadLoginBonusCatalog() 112 - characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver) 113 - omikujiCatalog := masterdata.LoadOmikujiCatalog() 114 - 115 - materialCatalog, err := masterdata.LoadMaterialCatalog() 116 - if err != nil { 117 - log.Fatalf("load material catalog: %v", err) 118 - } 119 - log.Printf("material catalog loaded: %d materials", len(materialCatalog.All)) 120 - 121 - consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog() 122 - if err != nil { 123 - log.Fatalf("load consumable item catalog: %v", err) 124 - } 125 - log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All)) 126 - 127 - costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog) 128 - if err != nil { 129 - log.Fatalf("load costume catalog: %v", err) 130 - } 131 - log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity)) 132 - 133 - weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog) 134 - if err != nil { 135 - log.Fatalf("load weapon catalog: %v", err) 136 - } 137 - log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId)) 138 - 139 - exploreCatalog, err := masterdata.LoadExploreCatalog() 140 - if err != nil { 141 - log.Fatalf("load explore catalog: %v", err) 142 - } 143 - log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets)) 144 - 145 - gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver) 146 - if err != nil { 147 - log.Fatalf("load gimmick catalog: %v", err) 148 - } 149 - 150 - characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog() 151 - if err != nil { 152 - log.Fatalf("load character board catalog: %v", err) 153 - } 154 - log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById)) 155 - 156 - characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog() 157 - if err != nil { 158 - log.Fatalf("load character rebirth catalog: %v", err) 159 - } 160 - log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId)) 161 - 162 - companionCatalog, err := masterdata.LoadCompanionCatalog() 163 - if err != nil { 164 - log.Fatalf("load companion catalog: %v", err) 165 - } 166 - log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory)) 167 - 168 - sideStoryCatalog := masterdata.LoadSideStoryCatalog() 169 - bigHuntCatalog := masterdata.LoadBigHuntCatalog() 170 - 171 - grpcServer := startGRPC( 172 - *listen, 173 - *publicAddr, 174 - *octoURL, 175 - *authURL, 176 - userStore, 177 - questHandler, 178 - gachaHandler, 179 - gachaEntries, 180 - cageOrnamentCatalog, 181 - loginBonusCatalog, 182 - characterViewerCatalog, 183 - shopCatalog, 184 - costumeCatalog, 185 - omikujiCatalog, 186 - weaponCatalog, 187 - exploreCatalog, 188 - gimmickCatalog, 189 - characterBoardCatalog, 190 - partsCatalog, 191 - characterRebirthCatalog, 192 - companionCatalog, 193 - materialCatalog, 194 - consumableItemCatalog, 195 - gameConfig, 196 - sideStoryCatalog, 197 - bigHuntCatalog, 198 - ) 51 + startAdmin(*adminListen, holder) 199 52 200 53 <-ctx.Done() 201 54 log.Println("shutting down...")
+58 -17
server/cmd/wizard/main.go
··· 32 32 ) 33 33 34 34 type config struct { 35 - IP string `json:"ip"` 36 - Device string `json:"device"` 37 - Detail string `json:"detail"` 38 - Summary string `json:"summary"` 39 - GRPCPort int `json:"grpc_port,omitempty"` 40 - CDNPort int `json:"cdn_port,omitempty"` 41 - AuthPort int `json:"auth_port,omitempty"` 35 + IP string `json:"ip"` 36 + Device string `json:"device"` 37 + Detail string `json:"detail"` 38 + Summary string `json:"summary"` 39 + GRPCPort int `json:"grpc_port,omitempty"` 40 + CDNPort int `json:"cdn_port,omitempty"` 41 + AuthPort int `json:"auth_port,omitempty"` 42 + AdminPort int `json:"admin_port,omitempty"` 42 43 } 43 44 44 45 const ( ··· 47 48 defaultAuthPort = 3000 48 49 ) 49 50 51 + // ports.Admin is opt-in: 0 means the admin webhook is not configured by the 52 + // wizard at all. Other ports always get a default if unset. 50 53 type ports struct { 51 - GRPC int 52 - CDN int 53 - Auth int 54 + GRPC int 55 + CDN int 56 + Auth int 57 + Admin int 54 58 } 55 59 56 60 func main() { ··· 59 63 grpcPort := flag.Int("grpc-port", defaultGRPCPort, "gRPC server port") 60 64 cdnPort := flag.Int("cdn-port", defaultCDNPort, "CDN server port") 61 65 authPort := flag.Int("auth-port", defaultAuthPort, "auth server port") 66 + 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.") 62 67 flag.Parse() 63 68 64 69 flagSet := map[string]bool{} ··· 80 85 81 86 ip, cfg, firstRun := resolveIP(*preferSaved) 82 87 83 - p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, cfg) 88 + p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, *adminPort, cfg) 84 89 savedPorts := portsFromConfig(cfg) 85 90 86 - if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth) { 91 + if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth || p.Admin != savedPorts.Admin) { 87 92 if !warnPortChange(savedPorts, p) { 88 93 os.Exit(0) 89 94 } ··· 92 97 cfg.GRPCPort = p.GRPC 93 98 cfg.CDNPort = p.CDN 94 99 cfg.AuthPort = p.Auth 100 + cfg.AdminPort = p.Admin 95 101 saveConfig(cfg) 96 102 97 103 labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14) ··· 101 107 fmt.Printf(" %s %s\n", labelStyle.Render("Game server:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.GRPC))) 102 108 fmt.Printf(" %s %s\n", labelStyle.Render("CDN:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.CDN))) 103 109 fmt.Printf(" %s %s\n", labelStyle.Render("Auth:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.Auth))) 110 + if p.Admin > 0 { 111 + fmt.Printf(" %s %s\n", labelStyle.Render("Admin webhook:"), addrStyle.Render(fmt.Sprintf("127.0.0.1:%d", p.Admin))) 112 + } 104 113 fmt.Println() 105 114 106 115 if firstRun || *setupOnly { ··· 477 486 } 478 487 return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", label+":", oldP, newP)) 479 488 } 489 + // Admin formatting handles the disabled (0) state since the port is 490 + // opt-in and we don't want to display "0" to the user. 491 + adminLine := func(oldP, newP int) (string, bool) { 492 + switch { 493 + case oldP == 0 && newP == 0: 494 + return "", false 495 + case oldP == 0 && newP != 0: 496 + return hlStyle.Render(fmt.Sprintf(" %-7s disabled → %d", "Admin:", newP)), true 497 + case oldP != 0 && newP == 0: 498 + return hlStyle.Render(fmt.Sprintf(" %-7s %d → disabled", "Admin:", oldP)), true 499 + case oldP == newP: 500 + return dimStyle.Render(fmt.Sprintf(" %-7s %d (unchanged)", "Admin:", oldP)), true 501 + default: 502 + return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", "Admin:", oldP, newP)), true 503 + } 504 + } 480 505 481 506 var b strings.Builder 482 507 b.WriteString("\n") ··· 487 512 b.WriteString(portLine("CDN", old.CDN, new.CDN)) 488 513 b.WriteString("\n") 489 514 b.WriteString(portLine("Auth", old.Auth, new.Auth)) 490 - b.WriteString("\n\n") 515 + b.WriteString("\n") 516 + if line, show := adminLine(old.Admin, new.Admin); show { 517 + b.WriteString(line) 518 + b.WriteString("\n") 519 + } 520 + b.WriteString("\n") 491 521 b.WriteString(dimStyle.Render(" Your APK was patched for the old ports. You may need to re-patch.")) 492 522 b.WriteString("\n\n") 493 523 fmt.Print(b.String()) ··· 821 851 } 822 852 823 853 func portsFromConfig(cfg config) ports { 824 - p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort} 854 + p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort, Admin: cfg.AdminPort} 825 855 if p.GRPC == 0 { 826 856 p.GRPC = defaultGRPCPort 827 857 } ··· 831 861 if p.Auth == 0 { 832 862 p.Auth = defaultAuthPort 833 863 } 864 + // Admin is opt-in: leave 0 = disabled. 834 865 return p 835 866 } 836 867 837 - func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, saved config) ports { 868 + func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag, adminFlag int, saved config) ports { 838 869 resolve := func(name string, flagVal, savedVal, defaultVal int) int { 839 870 if flagSet[name] { 840 871 return flagVal ··· 848 879 GRPC: resolve("grpc-port", grpcFlag, saved.GRPCPort, defaultGRPCPort), 849 880 CDN: resolve("cdn-port", cdnFlag, saved.CDNPort, defaultCDNPort), 850 881 Auth: resolve("auth-port", authFlag, saved.AuthPort, defaultAuthPort), 882 + // defaultVal=0 keeps admin opt-in: never enabled unless --admin-port 883 + // is passed or a non-zero value was previously saved. 884 + Admin: resolve("admin-port", adminFlag, saved.AdminPort, 0), 851 885 } 852 886 } 853 887 ··· 874 908 runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev") 875 909 }).Run() 876 910 877 - cmd := exec.Command(devBin, 911 + devArgs := []string{ 878 912 "--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC), 879 913 "--grpc.public-addr", fmt.Sprintf("%s:%d", ip, p.GRPC), 880 914 "--cdn.listen", fmt.Sprintf("0.0.0.0:%d", p.CDN), 881 915 "--cdn.public-addr", fmt.Sprintf("%s:%d", ip, p.CDN), 882 916 "--auth.listen", fmt.Sprintf("0.0.0.0:%d", p.Auth), 883 - ) 917 + } 918 + // Bind admin on loopback only — the wizard is for local dev, and the 919 + // webhook should never be exposed to the LAN by accident. Operators who 920 + // want a different bind can run cmd/dev directly with --admin.listen. 921 + if p.Admin > 0 { 922 + devArgs = append(devArgs, "--admin.listen", fmt.Sprintf("127.0.0.1:%d", p.Admin)) 923 + } 924 + cmd := exec.Command(devBin, devArgs...) 884 925 cmd.Stdout = os.Stdout 885 926 cmd.Stderr = os.Stderr 886 927 cmd.Stdin = os.Stdin
+3
server/docker-compose.yaml
··· 10 10 LUNAR_PUBLIC_ADDR: 127.0.0.1:8003 11 11 LUNAR_OCTO_URL: http://cdn:8080 12 12 LUNAR_AUTH_URL: http://auth:3000 13 + LUNAR_ADMIN_LISTEN: 0.0.0.0:8082 14 + LUNAR_ADMIN_TOKEN: ${LUNAR_ADMIN_TOKEN:-} 13 15 volumes: 14 16 - ./db:/opt/lunar-tear/db 15 17 - ./assets:/opt/lunar-tear/assets 16 18 ports: 17 19 - 8003:8003 20 + - 127.0.0.1:8082:8082 18 21 depends_on: 19 22 - cdn 20 23 - auth
+7 -1
server/entrypoint.sh
··· 9 9 AUTH_FLAG="--auth-url ${LUNAR_AUTH_URL}" 10 10 fi 11 11 12 + ADMIN_FLAG="" 13 + if [ -n "${LUNAR_ADMIN_LISTEN}" ]; then 14 + ADMIN_FLAG="--admin-listen ${LUNAR_ADMIN_LISTEN}" 15 + fi 16 + 12 17 exec ./lunar-tear \ 13 18 --listen "${LUNAR_LISTEN:-0.0.0.0:443}" \ 14 19 --public-addr "${LUNAR_PUBLIC_ADDR}" \ 15 20 --octo-url "${LUNAR_OCTO_URL}" \ 16 - ${AUTH_FLAG} 21 + ${AUTH_FLAG} \ 22 + ${ADMIN_FLAG}
+170
server/internal/runtime/build.go
··· 1 + package runtime 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + 7 + "lunar-tear/server/internal/gacha" 8 + "lunar-tear/server/internal/masterdata" 9 + "lunar-tear/server/internal/masterdata/memorydb" 10 + "lunar-tear/server/internal/questflow" 11 + ) 12 + 13 + // buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever 14 + // memorydb currently holds and returns a fully populated *Catalogs. Called 15 + // once at startup and again on every reload. 16 + func buildCatalogs() (*Catalogs, error) { 17 + log.Printf("master data loaded (%d tables)", memorydb.TableCount()) 18 + 19 + gameConfig, err := masterdata.LoadGameConfig() 20 + if err != nil { 21 + return nil, fmt.Errorf("load game config: %w", err) 22 + } 23 + log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)", 24 + gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold) 25 + 26 + partsCatalog, err := masterdata.LoadPartsCatalog() 27 + if err != nil { 28 + return nil, fmt.Errorf("load parts catalog: %w", err) 29 + } 30 + log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType)) 31 + 32 + questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog) 33 + if err != nil { 34 + return nil, fmt.Errorf("load quest catalog: %w", err) 35 + } 36 + questHandler := questflow.NewQuestHandler(questCatalog, gameConfig) 37 + 38 + gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog() 39 + if err != nil { 40 + return nil, fmt.Errorf("load gacha catalog: %w", err) 41 + } 42 + log.Printf("gacha catalog loaded: %d entries", len(gachaEntries)) 43 + 44 + gachaPool, err := masterdata.LoadGachaPool() 45 + if err != nil { 46 + return nil, fmt.Errorf("load gacha pool: %w", err) 47 + } 48 + log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d", 49 + len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials)) 50 + 51 + shopCatalog, err := masterdata.LoadShopCatalog() 52 + if err != nil { 53 + return nil, fmt.Errorf("load shop catalog: %w", err) 54 + } 55 + log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops", 56 + len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells)) 57 + 58 + gachaPool.BuildShopFeatured(shopCatalog) 59 + gachaPool.PruneUnpairedCostumes() 60 + gachaPool.BuildFeaturedMapping(gachaEntries) 61 + gachaPool.BuildBannerPools(gachaEntries) 62 + masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool) 63 + 64 + dupExchange, err := masterdata.LoadDupExchange() 65 + if err != nil { 66 + return nil, fmt.Errorf("load dup exchange: %w", err) 67 + } 68 + dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool) 69 + if err != nil { 70 + return nil, fmt.Errorf("enrich dup exchange: %w", err) 71 + } 72 + log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded) 73 + 74 + gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange) 75 + 76 + conditionResolver, err := masterdata.LoadConditionResolver() 77 + if err != nil { 78 + return nil, fmt.Errorf("load condition resolver: %w", err) 79 + } 80 + 81 + cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog() 82 + loginBonusCatalog := masterdata.LoadLoginBonusCatalog() 83 + characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver) 84 + omikujiCatalog := masterdata.LoadOmikujiCatalog() 85 + 86 + materialCatalog, err := masterdata.LoadMaterialCatalog() 87 + if err != nil { 88 + return nil, fmt.Errorf("load material catalog: %w", err) 89 + } 90 + log.Printf("material catalog loaded: %d materials", len(materialCatalog.All)) 91 + 92 + consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog() 93 + if err != nil { 94 + return nil, fmt.Errorf("load consumable item catalog: %w", err) 95 + } 96 + log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All)) 97 + 98 + costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog) 99 + if err != nil { 100 + return nil, fmt.Errorf("load costume catalog: %w", err) 101 + } 102 + log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity)) 103 + 104 + weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog) 105 + if err != nil { 106 + return nil, fmt.Errorf("load weapon catalog: %w", err) 107 + } 108 + log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId)) 109 + 110 + exploreCatalog, err := masterdata.LoadExploreCatalog() 111 + if err != nil { 112 + return nil, fmt.Errorf("load explore catalog: %w", err) 113 + } 114 + log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets)) 115 + 116 + gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver) 117 + if err != nil { 118 + return nil, fmt.Errorf("load gimmick catalog: %w", err) 119 + } 120 + 121 + characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog() 122 + if err != nil { 123 + return nil, fmt.Errorf("load character board catalog: %w", err) 124 + } 125 + log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById)) 126 + 127 + characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog() 128 + if err != nil { 129 + return nil, fmt.Errorf("load character rebirth catalog: %w", err) 130 + } 131 + log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId)) 132 + 133 + companionCatalog, err := masterdata.LoadCompanionCatalog() 134 + if err != nil { 135 + return nil, fmt.Errorf("load companion catalog: %w", err) 136 + } 137 + log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory)) 138 + 139 + sideStoryCatalog := masterdata.LoadSideStoryCatalog() 140 + bigHuntCatalog := masterdata.LoadBigHuntCatalog() 141 + 142 + return &Catalogs{ 143 + GameConfig: gameConfig, 144 + Parts: partsCatalog, 145 + Quest: questCatalog, 146 + GachaEntries: gachaEntries, 147 + GachaMedals: medalInfo, 148 + GachaPool: gachaPool, 149 + Shop: shopCatalog, 150 + DupExchange: dupExchange, 151 + ConditionResolver: conditionResolver, 152 + CageOrnament: cageOrnamentCatalog, 153 + LoginBonus: loginBonusCatalog, 154 + CharacterViewer: characterViewerCatalog, 155 + Omikuji: omikujiCatalog, 156 + Material: materialCatalog, 157 + ConsumableItem: consumableItemCatalog, 158 + Costume: costumeCatalog, 159 + Weapon: weaponCatalog, 160 + Explore: exploreCatalog, 161 + Gimmick: gimmickCatalog, 162 + CharacterBoard: characterBoardCatalog, 163 + CharacterRebirth: characterRebirthCatalog, 164 + Companion: companionCatalog, 165 + SideStory: sideStoryCatalog, 166 + BigHunt: bigHuntCatalog, 167 + QuestHandler: questHandler, 168 + GachaHandler: gachaHandler, 169 + }, nil 170 + }
+104
server/internal/runtime/holder.go
··· 1 + // Package runtime owns the live, hot-swappable view of master data. 2 + // 3 + // The Holder atomically swaps a *Catalogs aggregate every time the operator 4 + // asks the server to re-read assets/release/20240404193219.bin.e (typically via 5 + // the admin webhook in cmd/lunar-tear/admin.go). gRPC services hold a *Holder 6 + // and call Get() at the start of each RPC, so they always see a consistent 7 + // snapshot. 8 + package runtime 9 + 10 + import ( 11 + "fmt" 12 + "log" 13 + "os" 14 + "sync/atomic" 15 + "time" 16 + 17 + "lunar-tear/server/internal/gacha" 18 + "lunar-tear/server/internal/masterdata" 19 + "lunar-tear/server/internal/masterdata/memorydb" 20 + "lunar-tear/server/internal/model" 21 + "lunar-tear/server/internal/questflow" 22 + "lunar-tear/server/internal/store" 23 + ) 24 + 25 + // Catalogs is an immutable snapshot of every catalog and catalog-derived 26 + // handler the server needs at runtime. A new *Catalogs is built from scratch 27 + // on every reload and atomically published via Holder. 28 + type Catalogs struct { 29 + GameConfig *masterdata.GameConfig 30 + Parts *masterdata.PartsCatalog 31 + Quest *masterdata.QuestCatalog 32 + GachaEntries []store.GachaCatalogEntry 33 + GachaMedals map[int32]masterdata.GachaMedalInfo 34 + GachaPool *masterdata.GachaCatalog 35 + Shop *masterdata.ShopCatalog 36 + DupExchange map[int32][]model.DupExchangeEntry 37 + ConditionResolver *masterdata.ConditionResolver 38 + CageOrnament *masterdata.CageOrnamentCatalog 39 + LoginBonus *masterdata.LoginBonusCatalog 40 + CharacterViewer *masterdata.CharacterViewerCatalog 41 + Omikuji *masterdata.OmikujiCatalog 42 + Material *masterdata.MaterialCatalog 43 + ConsumableItem *masterdata.ConsumableItemCatalog 44 + Costume *masterdata.CostumeCatalog 45 + Weapon *masterdata.WeaponCatalog 46 + Explore *masterdata.ExploreCatalog 47 + Gimmick *masterdata.GimmickCatalog 48 + CharacterBoard *masterdata.CharacterBoardCatalog 49 + CharacterRebirth *masterdata.CharacterRebirthCatalog 50 + Companion *masterdata.CompanionCatalog 51 + SideStory *masterdata.SideStoryCatalog 52 + BigHunt *masterdata.BigHuntCatalog 53 + 54 + // Catalog-derived handlers must rebuild on every reload because they 55 + // embed/cache pointers to specific catalog instances. 56 + QuestHandler *questflow.QuestHandler 57 + GachaHandler *gacha.GachaHandler 58 + } 59 + 60 + // Holder owns the current *Catalogs and the bin.e path. Concurrent readers 61 + // call Get(); the single-writer Reload() rebuilds and atomically publishes. 62 + type Holder struct { 63 + binPath string 64 + cur atomic.Pointer[Catalogs] 65 + } 66 + 67 + // NewHolder reads the binary at binPath, builds the initial catalogs, and 68 + // returns a ready-to-use Holder. Subsequent calls to Reload() re-read the 69 + // same path. 70 + func NewHolder(binPath string) (*Holder, error) { 71 + h := &Holder{binPath: binPath} 72 + if err := h.Reload(); err != nil { 73 + return nil, err 74 + } 75 + return h, nil 76 + } 77 + 78 + // Reload re-reads the bin.e from disk, rebuilds every catalog and handler, 79 + // atomically publishes the new snapshot, and bumps the bin.e mtime so client 80 + // caches invalidate (see service/data.go GetLatestMasterDataVersion). 81 + func (h *Holder) Reload() error { 82 + if err := memorydb.Init(h.binPath); err != nil { 83 + return fmt.Errorf("memorydb.Init: %w", err) 84 + } 85 + c, err := buildCatalogs() 86 + if err != nil { 87 + return fmt.Errorf("buildCatalogs: %w", err) 88 + } 89 + h.cur.Store(c) 90 + now := time.Now() 91 + if err := os.Chtimes(h.binPath, now, now); err != nil { 92 + // Non-fatal: the catalogs swapped fine in-memory; clients may take 93 + // longer to invalidate their cached download but server-side state is 94 + // already coherent. 95 + log.Printf("[runtime] os.Chtimes(%s) failed (clients may not invalidate cache): %v", h.binPath, err) 96 + } 97 + return nil 98 + } 99 + 100 + // Get returns the current snapshot. Safe for concurrent callers; the returned 101 + // pointer is stable for the duration of the caller's use. 102 + func (h *Holder) Get() *Catalogs { 103 + return h.cur.Load() 104 + }
+10 -5
server/internal/service/banner.go
··· 4 4 "context" 5 5 6 6 pb "lunar-tear/server/gen/proto" 7 + "lunar-tear/server/internal/gametime" 7 8 "lunar-tear/server/internal/model" 8 - "lunar-tear/server/internal/store" 9 + "lunar-tear/server/internal/runtime" 9 10 ) 10 11 11 12 type BannerServiceServer struct { 12 13 pb.UnimplementedBannerServiceServer 13 - catalog []store.GachaCatalogEntry 14 + holder *runtime.Holder 14 15 } 15 16 16 - func NewBannerServiceServer(catalog []store.GachaCatalogEntry) *BannerServiceServer { 17 - return &BannerServiceServer{catalog: catalog} 17 + func NewBannerServiceServer(holder *runtime.Holder) *BannerServiceServer { 18 + return &BannerServiceServer{holder: holder} 18 19 } 19 20 20 21 func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) { 21 - catalog := s.catalog 22 + catalog := s.holder.Get().GachaEntries 23 + nowMillis := gametime.NowMillis() 22 24 var termLimited []*pb.GachaBanner 23 25 var latestChapter *pb.GachaBanner 24 26 for _, entry := range catalog { 27 + if !gachaActiveAt(entry, nowMillis) { 28 + continue 29 + } 25 30 if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle { 26 31 continue 27 32 }
+8 -7
server/internal/service/cageornament.go
··· 6 6 7 7 pb "lunar-tear/server/gen/proto" 8 8 "lunar-tear/server/internal/gametime" 9 - "lunar-tear/server/internal/masterdata" 10 9 "lunar-tear/server/internal/model" 10 + "lunar-tear/server/internal/runtime" 11 11 "lunar-tear/server/internal/store" 12 12 ) 13 13 ··· 15 15 pb.UnimplementedCageOrnamentServiceServer 16 16 users store.UserRepository 17 17 sessions store.SessionRepository 18 - catalog *masterdata.CageOrnamentCatalog 19 - granter *store.PossessionGranter 18 + holder *runtime.Holder 20 19 } 21 20 22 - func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CageOrnamentCatalog, granter *store.PossessionGranter) *CageOrnamentServiceServer { 23 - return &CageOrnamentServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter} 21 + func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CageOrnamentServiceServer { 22 + return &CageOrnamentServiceServer{users: users, sessions: sessions, holder: holder} 24 23 } 25 24 26 25 func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.ReceiveRewardRequest) (*pb.ReceiveRewardResponse, error) { 27 26 log.Printf("[CageOrnamentService] ReceiveReward: cageOrnamentId=%d", req.CageOrnamentId) 28 27 29 - reward, ok := s.catalog.LookupReward(req.CageOrnamentId) 28 + cat := s.holder.Get() 29 + reward, ok := cat.CageOrnament.LookupReward(req.CageOrnamentId) 30 30 if !ok { 31 31 log.Fatalf("[CageOrnamentService] ReceiveReward: no reward for cageOrnamentId=%d", req.CageOrnamentId) 32 32 } 33 + granter := cat.QuestHandler.Granter 33 34 34 35 userId := CurrentUserId(ctx, s.users, s.sessions) 35 36 nowMillis := gametime.NowMillis() ··· 39 40 AcquisitionDatetime: nowMillis, 40 41 LatestVersion: nowMillis, 41 42 } 42 - s.granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis) 43 + granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis) 43 44 }) 44 45 45 46 return &pb.ReceiveRewardResponse{
+13 -10
server/internal/service/character.go
··· 7 7 pb "lunar-tear/server/gen/proto" 8 8 "lunar-tear/server/internal/gametime" 9 9 "lunar-tear/server/internal/masterdata" 10 + "lunar-tear/server/internal/runtime" 10 11 "lunar-tear/server/internal/store" 11 12 ) 12 13 ··· 14 15 pb.UnimplementedCharacterServiceServer 15 16 users store.UserRepository 16 17 sessions store.SessionRepository 17 - catalog *masterdata.CharacterRebirthCatalog 18 - config *masterdata.GameConfig 18 + holder *runtime.Holder 19 19 } 20 20 21 - func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterRebirthCatalog, config *masterdata.GameConfig) *CharacterServiceServer { 22 - return &CharacterServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} 21 + func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterServiceServer { 22 + return &CharacterServiceServer{users: users, sessions: sessions, holder: holder} 23 23 } 24 24 25 25 func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthRequest) (*pb.RebirthResponse, error) { 26 26 log.Printf("[CharacterService] Rebirth: characterId=%d rebirthCount=%d", req.CharacterId, req.RebirthCount) 27 27 28 + cat := s.holder.Get() 29 + catalog := cat.CharacterRebirth 30 + config := cat.GameConfig 28 31 userId := CurrentUserId(ctx, s.users, s.sessions) 29 32 nowMillis := gametime.NowMillis() 30 33 31 - stepGroupId, ok := s.catalog.StepGroupByCharacterId[req.CharacterId] 34 + stepGroupId, ok := catalog.StepGroupByCharacterId[req.CharacterId] 32 35 if !ok { 33 36 log.Printf("[CharacterService] Rebirth: no step group for characterId=%d", req.CharacterId) 34 37 return &pb.RebirthResponse{}, nil ··· 40 43 targetCount := currentCount + req.RebirthCount 41 44 42 45 for count := currentCount; count < targetCount; count++ { 43 - step, ok := s.catalog.StepByGroupAndCount[masterdata.StepKey{GroupId: stepGroupId, BeforeRebirthCount: count}] 46 + step, ok := catalog.StepByGroupAndCount[masterdata.StepKey{GroupId: stepGroupId, BeforeRebirthCount: count}] 44 47 if !ok { 45 48 log.Printf("[CharacterService] Rebirth: no step row for groupId=%d beforeCount=%d", stepGroupId, count) 46 49 return 47 50 } 48 51 49 - goldId := s.config.ConsumableItemIdForGold 50 - user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-s.config.CharacterRebirthConsumeGold, 0) 51 - log.Printf("[CharacterService] Rebirth: consumed gold=%d", s.config.CharacterRebirthConsumeGold) 52 + goldId := config.ConsumableItemIdForGold 53 + user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-config.CharacterRebirthConsumeGold, 0) 54 + log.Printf("[CharacterService] Rebirth: consumed gold=%d", config.CharacterRebirthConsumeGold) 52 55 53 - materials := s.catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId] 56 + materials := catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId] 54 57 for _, mat := range materials { 55 58 user.Materials[mat.MaterialId] -= mat.Count 56 59 if user.Materials[mat.MaterialId] <= 0 {
+25 -23
server/internal/service/characterboard.go
··· 7 7 pb "lunar-tear/server/gen/proto" 8 8 "lunar-tear/server/internal/masterdata" 9 9 "lunar-tear/server/internal/model" 10 + "lunar-tear/server/internal/runtime" 10 11 "lunar-tear/server/internal/store" 11 12 ) 12 13 ··· 14 15 pb.UnimplementedCharacterBoardServiceServer 15 16 users store.UserRepository 16 17 sessions store.SessionRepository 17 - catalog *masterdata.CharacterBoardCatalog 18 + holder *runtime.Holder 18 19 } 19 20 20 - func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterBoardCatalog) *CharacterBoardServiceServer { 21 - return &CharacterBoardServiceServer{users: users, sessions: sessions, catalog: catalog} 21 + func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterBoardServiceServer { 22 + return &CharacterBoardServiceServer{users: users, sessions: sessions, holder: holder} 22 23 } 23 24 24 25 func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) { 25 26 log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId) 26 27 28 + catalog := s.holder.Get().CharacterBoard 27 29 userId := CurrentUserId(ctx, s.users, s.sessions) 28 30 29 31 s.users.UpdateUser(userId, func(user *store.UserState) { 30 32 for _, panelId := range req.CharacterBoardPanelId { 31 - panel, ok := s.catalog.PanelById[panelId] 33 + panel, ok := catalog.PanelById[panelId] 32 34 if !ok { 33 35 log.Printf("[CharacterBoardService] unknown panelId=%d, skipping", panelId) 34 36 continue 35 37 } 36 38 37 - s.consumeCosts(user, panel) 38 - s.setReleaseBit(user, panel) 39 - s.applyEffects(user, panel) 39 + consumeBoardCosts(catalog, user, panel) 40 + setBoardReleaseBit(user, panel) 41 + applyBoardEffects(catalog, user, panel) 40 42 } 41 43 }) 42 44 43 45 return &pb.ReleasePanelResponse{}, nil 44 46 } 45 47 46 - func (s *CharacterBoardServiceServer) consumeCosts(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { 47 - costs := s.catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId] 48 + func consumeBoardCosts(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { 49 + costs := catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId] 48 50 for _, cost := range costs { 49 51 store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count) 50 52 } 51 53 } 52 54 53 - func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { 55 + func setBoardReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { 54 56 boardId := panel.CharacterBoardId 55 57 board := user.CharacterBoards[boardId] 56 58 board.CharacterBoardId = boardId ··· 73 75 user.CharacterBoards[boardId] = board 74 76 } 75 77 76 - func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { 77 - effects := s.catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId] 78 + func applyBoardEffects(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { 79 + effects := catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId] 78 80 for _, eff := range effects { 79 81 switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) { 80 82 case model.CharacterBoardEffectTypeAbility: 81 - s.applyAbilityEffect(user, eff) 83 + applyBoardAbilityEffect(catalog, user, eff) 82 84 case model.CharacterBoardEffectTypeStatusUp: 83 - s.applyStatusUpEffect(user, eff) 85 + applyBoardStatusUpEffect(catalog, user, eff) 84 86 } 85 87 } 86 88 } 87 89 88 - func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { 89 - ability, ok := s.catalog.AbilityById[eff.CharacterBoardEffectId] 90 + func applyBoardAbilityEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { 91 + ability, ok := catalog.AbilityById[eff.CharacterBoardEffectId] 90 92 if !ok { 91 93 log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId) 92 94 return 93 95 } 94 96 95 - characterId := s.resolveCharacterId(ability.CharacterBoardEffectTargetGroupId) 97 + characterId := resolveBoardCharacterId(catalog, ability.CharacterBoardEffectTargetGroupId) 96 98 if characterId == 0 { 97 99 return 98 100 } ··· 103 105 state.AbilityId = ability.AbilityId 104 106 state.Level += eff.EffectValue 105 107 106 - if maxLvl, ok := s.catalog.AbilityMaxLevel[key]; ok && state.Level > maxLvl { 108 + if maxLvl, ok := catalog.AbilityMaxLevel[key]; ok && state.Level > maxLvl { 107 109 state.Level = maxLvl 108 110 } 109 111 110 112 user.CharacterBoardAbilities[key] = state 111 113 } 112 114 113 - func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { 114 - statusUp, ok := s.catalog.StatusUpById[eff.CharacterBoardEffectId] 115 + func applyBoardStatusUpEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { 116 + statusUp, ok := catalog.StatusUpById[eff.CharacterBoardEffectId] 115 117 if !ok { 116 118 log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId) 117 119 return 118 120 } 119 121 120 - characterId := s.resolveCharacterId(statusUp.CharacterBoardEffectTargetGroupId) 122 + characterId := resolveBoardCharacterId(catalog, statusUp.CharacterBoardEffectTargetGroupId) 121 123 if characterId == 0 { 122 124 return 123 125 } ··· 151 153 user.CharacterBoardStatusUps[key] = state 152 154 } 153 155 154 - func (s *CharacterBoardServiceServer) resolveCharacterId(targetGroupId int32) int32 { 155 - targets := s.catalog.EffectTargetsByGroupId[targetGroupId] 156 + func resolveBoardCharacterId(catalog *masterdata.CharacterBoardCatalog, targetGroupId int32) int32 { 157 + targets := catalog.EffectTargetsByGroupId[targetGroupId] 156 158 for _, t := range targets { 157 159 if t.TargetValue != 0 { 158 160 return t.TargetValue
+5 -5
server/internal/service/characterviewer.go
··· 6 6 "log" 7 7 8 8 pb "lunar-tear/server/gen/proto" 9 - "lunar-tear/server/internal/masterdata" 9 + "lunar-tear/server/internal/runtime" 10 10 "lunar-tear/server/internal/store" 11 11 12 12 "google.golang.org/protobuf/types/known/emptypb" ··· 16 16 pb.UnimplementedCharacterViewerServiceServer 17 17 users store.UserRepository 18 18 sessions store.SessionRepository 19 - catalog *masterdata.CharacterViewerCatalog 19 + holder *runtime.Holder 20 20 } 21 21 22 - func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterViewerCatalog) *CharacterViewerServiceServer { 23 - return &CharacterViewerServiceServer{users: users, sessions: sessions, catalog: catalog} 22 + func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterViewerServiceServer { 23 + return &CharacterViewerServiceServer{users: users, sessions: sessions, holder: holder} 24 24 } 25 25 26 26 func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ *emptypb.Empty) (*pb.CharacterViewerTopResponse, error) { ··· 32 32 panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err)) 33 33 } 34 34 35 - released := s.catalog.ReleasedFieldIds(user) 35 + released := s.holder.Get().CharacterViewer.ReleasedFieldIds(user) 36 36 log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId) 37 37 38 38 return &pb.CharacterViewerTopResponse{
+11 -8
server/internal/service/companion.go
··· 8 8 pb "lunar-tear/server/gen/proto" 9 9 "lunar-tear/server/internal/gametime" 10 10 "lunar-tear/server/internal/masterdata" 11 + "lunar-tear/server/internal/runtime" 11 12 "lunar-tear/server/internal/store" 12 13 ) 13 14 ··· 17 18 pb.UnimplementedCompanionServiceServer 18 19 users store.UserRepository 19 20 sessions store.SessionRepository 20 - catalog *masterdata.CompanionCatalog 21 - config *masterdata.GameConfig 21 + holder *runtime.Holder 22 22 } 23 23 24 - func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CompanionCatalog, config *masterdata.GameConfig) *CompanionServiceServer { 25 - return &CompanionServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} 24 + func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CompanionServiceServer { 25 + return &CompanionServiceServer{users: users, sessions: sessions, holder: holder} 26 26 } 27 27 28 28 func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionEnhanceRequest) (*pb.CompanionEnhanceResponse, error) { 29 29 log.Printf("[CompanionService] Enhance: uuid=%s addLevel=%d", req.UserCompanionUuid, req.AddLevelCount) 30 30 31 + cat := s.holder.Get() 32 + catalog := cat.Companion 33 + config := cat.GameConfig 31 34 userId := CurrentUserId(ctx, s.users, s.sessions) 32 35 nowMillis := gametime.NowMillis() 33 36 ··· 38 41 return 39 42 } 40 43 41 - compDef, ok := s.catalog.CompanionById[companion.CompanionId] 44 + compDef, ok := catalog.CompanionById[companion.CompanionId] 42 45 if !ok { 43 46 log.Printf("[CompanionService] Enhance: companion master id=%d not found", companion.CompanionId) 44 47 return ··· 50 53 } 51 54 52 55 for lvl := companion.Level; lvl < targetLevel; lvl++ { 53 - if costFunc, ok := s.catalog.GoldCostByCategory[compDef.CompanionCategoryType]; ok { 56 + if costFunc, ok := catalog.GoldCostByCategory[compDef.CompanionCategoryType]; ok { 54 57 goldCost := costFunc.Evaluate(lvl) 55 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 58 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 56 59 } 57 60 58 61 matKey := masterdata.CompanionLevelKey{CategoryType: compDef.CompanionCategoryType, Level: lvl} 59 - if mat, ok := s.catalog.MaterialsByKey[matKey]; ok { 62 + if mat, ok := catalog.MaterialsByKey[matKey]; ok { 60 63 user.Materials[mat.MaterialId] -= mat.Count 61 64 } 62 65 }
+9 -7
server/internal/service/consumableitem.go
··· 6 6 "log" 7 7 8 8 pb "lunar-tear/server/gen/proto" 9 - "lunar-tear/server/internal/masterdata" 9 + "lunar-tear/server/internal/runtime" 10 10 "lunar-tear/server/internal/store" 11 11 ) 12 12 ··· 14 14 pb.UnimplementedConsumableItemServiceServer 15 15 users store.UserRepository 16 16 sessions store.SessionRepository 17 - catalog *masterdata.ConsumableItemCatalog 18 - config *masterdata.GameConfig 17 + holder *runtime.Holder 19 18 } 20 19 21 - func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ConsumableItemCatalog, config *masterdata.GameConfig) *ConsumableItemServiceServer { 22 - return &ConsumableItemServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} 20 + func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ConsumableItemServiceServer { 21 + return &ConsumableItemServiceServer{users: users, sessions: sessions, holder: holder} 23 22 } 24 23 25 24 func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) { 26 25 log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession)) 27 26 27 + cat := s.holder.Get() 28 + catalog := cat.ConsumableItem 29 + config := cat.GameConfig 28 30 userId := CurrentUserId(ctx, s.users, s.sessions) 29 31 30 32 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { 31 33 totalGold := int32(0) 32 34 for _, item := range req.ConsumableItemPossession { 33 - row, ok := s.catalog.All[item.ConsumableItemId] 35 + row, ok := catalog.All[item.ConsumableItemId] 34 36 if !ok { 35 37 log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId) 36 38 continue ··· 53 55 } 54 56 55 57 if totalGold > 0 { 56 - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold 58 + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold 57 59 log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold) 58 60 } 59 61 })
+55 -37
server/internal/service/costume.go
··· 13 13 "lunar-tear/server/internal/gameutil" 14 14 "lunar-tear/server/internal/masterdata" 15 15 "lunar-tear/server/internal/model" 16 + "lunar-tear/server/internal/runtime" 16 17 "lunar-tear/server/internal/store" 17 18 ) 18 19 ··· 20 21 pb.UnimplementedCostumeServiceServer 21 22 users store.UserRepository 22 23 sessions store.SessionRepository 23 - catalog *masterdata.CostumeCatalog 24 - config *masterdata.GameConfig 24 + holder *runtime.Holder 25 25 } 26 26 27 - func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CostumeCatalog, config *masterdata.GameConfig) *CostumeServiceServer { 28 - return &CostumeServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} 27 + func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CostumeServiceServer { 28 + return &CostumeServiceServer{users: users, sessions: sessions, holder: holder} 29 29 } 30 30 31 31 func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceRequest) (*pb.EnhanceResponse, error) { 32 32 log.Printf("[CostumeService] Enhance: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) 33 33 34 + cat := s.holder.Get() 35 + catalog := cat.Costume 36 + config := cat.GameConfig 34 37 userId := CurrentUserId(ctx, s.users, s.sessions) 35 38 nowMillis := gametime.NowMillis() 36 39 ··· 41 44 return 42 45 } 43 46 44 - cm, ok := s.catalog.Costumes[costume.CostumeId] 47 + cm, ok := catalog.Costumes[costume.CostumeId] 45 48 if !ok { 46 49 log.Printf("[CostumeService] Enhance: costume master id=%d not found", costume.CostumeId) 47 50 return ··· 50 53 totalExp := int32(0) 51 54 totalMaterialCount := int32(0) 52 55 for materialId, count := range req.Materials { 53 - mat, ok := s.catalog.Materials[materialId] 56 + mat, ok := catalog.Materials[materialId] 54 57 if !ok { 55 58 log.Printf("[CostumeService] Enhance: material id=%d not found, skipping", materialId) 56 59 continue ··· 66 69 67 70 expPerUnit := mat.EffectValue 68 71 if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType { 69 - expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 72 + expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000 70 73 } 71 74 totalExp += expPerUnit * count 72 75 } 73 76 74 - if costFunc, ok := s.catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 { 77 + if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 { 75 78 goldCost := costFunc.Evaluate(totalMaterialCount) 76 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 79 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 77 80 log.Printf("[CostumeService] Enhance: gold cost=%d (materials=%d)", goldCost, totalMaterialCount) 78 81 } 79 82 80 83 costume.Exp += totalExp 81 84 82 - if thresholds, ok := s.catalog.ExpByRarity[cm.RarityType]; ok { 85 + if thresholds, ok := catalog.ExpByRarity[cm.RarityType]; ok { 83 86 costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds) 84 87 } 85 88 ··· 100 103 func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest) (*pb.AwakenResponse, error) { 101 104 log.Printf("[CostumeService] Awaken: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) 102 105 106 + cat := s.holder.Get() 107 + catalog := cat.Costume 108 + config := cat.GameConfig 103 109 userId := CurrentUserId(ctx, s.users, s.sessions) 104 110 nowMillis := gametime.NowMillis() 105 111 ··· 110 116 return 111 117 } 112 118 113 - awakenRow, ok := s.catalog.AwakenByCostumeId[costume.CostumeId] 119 + awakenRow, ok := catalog.AwakenByCostumeId[costume.CostumeId] 114 120 if !ok { 115 121 log.Printf("[CostumeService] Awaken: no awaken data for costumeId=%d", costume.CostumeId) 116 122 return ··· 118 124 119 125 nextStep := costume.AwakenCount + 1 120 126 121 - if gold, ok := s.catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok { 122 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= gold 127 + if gold, ok := catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok { 128 + user.ConsumableItems[config.ConsumableItemIdForGold] -= gold 123 129 log.Printf("[CostumeService] Awaken: gold cost=%d", gold) 124 130 } 125 131 ··· 137 143 user.Costumes[req.UserCostumeUuid] = costume 138 144 log.Printf("[CostumeService] Awaken: costumeId=%d awakenCount=%d", costume.CostumeId, nextStep) 139 145 140 - effectSteps, ok := s.catalog.AwakenEffectsByGroupAndStep[awakenRow.CostumeAwakenEffectGroupId] 146 + effectSteps, ok := catalog.AwakenEffectsByGroupAndStep[awakenRow.CostumeAwakenEffectGroupId] 141 147 if !ok { 142 148 return 143 149 } ··· 148 154 149 155 switch model.CostumeAwakenEffectType(effect.CostumeAwakenEffectType) { 150 156 case model.CostumeAwakenEffectTypeStatusUp: 151 - s.applyAwakenStatusUp(user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis) 157 + applyCostumeAwakenStatusUp(catalog, user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis) 152 158 case model.CostumeAwakenEffectTypeAbility: 153 159 log.Printf("[CostumeService] Awaken: ability effect id=%d (client-resolved)", effect.CostumeAwakenEffectId) 154 160 case model.CostumeAwakenEffectTypeItemAcquire: 155 - s.applyAwakenItemAcquire(user, effect.CostumeAwakenEffectId, nowMillis) 161 + applyCostumeAwakenItemAcquire(catalog, user, effect.CostumeAwakenEffectId, nowMillis) 156 162 default: 157 163 log.Printf("[CostumeService] Awaken: unknown effect type=%d", effect.CostumeAwakenEffectType) 158 164 } ··· 164 170 return &pb.AwakenResponse{}, nil 165 171 } 166 172 167 - func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) { 168 - rows, ok := s.catalog.AwakenStatusUpByGroup[statusUpGroupId] 173 + func applyCostumeAwakenStatusUp(catalog *masterdata.CostumeCatalog, user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) { 174 + rows, ok := catalog.AwakenStatusUpByGroup[statusUpGroupId] 169 175 if !ok { 170 176 log.Printf("[CostumeService] Awaken: status up group %d not found", statusUpGroupId) 171 177 return ··· 201 207 } 202 208 } 203 209 204 - func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, itemAcquireId int32, nowMillis int64) { 205 - acq, ok := s.catalog.AwakenItemAcquireById[itemAcquireId] 210 + func applyCostumeAwakenItemAcquire(catalog *masterdata.CostumeCatalog, user *store.UserState, itemAcquireId int32, nowMillis int64) { 211 + acq, ok := catalog.AwakenItemAcquireById[itemAcquireId] 206 212 if !ok { 207 213 log.Printf("[CostumeService] Awaken: item acquire id=%d not found", itemAcquireId) 208 214 return ··· 226 232 func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.EnhanceActiveSkillRequest) (*pb.EnhanceActiveSkillResponse, error) { 227 233 log.Printf("[CostumeService] EnhanceActiveSkill: uuid=%s addLevel=%d", req.UserCostumeUuid, req.AddLevelCount) 228 234 235 + cat := s.holder.Get() 236 + catalog := cat.Costume 237 + config := cat.GameConfig 229 238 userId := CurrentUserId(ctx, s.users, s.sessions) 230 239 nowMillis := gametime.NowMillis() 231 240 ··· 236 245 return 237 246 } 238 247 239 - cm, ok := s.catalog.Costumes[costume.CostumeId] 248 + cm, ok := catalog.Costumes[costume.CostumeId] 240 249 if !ok { 241 250 log.Printf("[CostumeService] EnhanceActiveSkill: costume master id=%d not found", costume.CostumeId) 242 251 return 243 252 } 244 253 245 - groupRows := s.catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId] 254 + groupRows := catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId] 246 255 enhanceMatId := int32(-1) 247 256 for _, g := range groupRows { 248 257 if g.CostumeLimitBreakCountLowerLimit <= costume.LimitBreakCount { ··· 259 268 skill := user.CostumeActiveSkills[req.UserCostumeUuid] 260 269 currentLevel := skill.Level 261 270 262 - maxLevelFunc, ok := s.catalog.ActiveSkillMaxLevelByRarity[cm.RarityType] 271 + maxLevelFunc, ok := catalog.ActiveSkillMaxLevelByRarity[cm.RarityType] 263 272 if !ok { 264 273 log.Printf("[CostumeService] EnhanceActiveSkill: no max level func for rarity=%d", cm.RarityType) 265 274 return ··· 277 286 278 287 for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { 279 288 key := [2]int32{enhanceMatId, lvl} 280 - mats := s.catalog.ActiveSkillEnhanceMats[key] 289 + mats := catalog.ActiveSkillEnhanceMats[key] 281 290 for _, mat := range mats { 282 291 cur := user.Materials[mat.MaterialId] 283 292 cost := mat.Count ··· 288 297 user.Materials[mat.MaterialId] = cur - cost 289 298 } 290 299 291 - if costFunc, ok := s.catalog.ActiveSkillCostByRarity[cm.RarityType]; ok { 300 + if costFunc, ok := catalog.ActiveSkillCostByRarity[cm.RarityType]; ok { 292 301 goldCost := costFunc.Evaluate(lvl + 1) 293 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 302 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 294 303 } 295 304 } 296 305 ··· 310 319 func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBreakRequest) (*pb.LimitBreakResponse, error) { 311 320 log.Printf("[CostumeService] LimitBreak: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) 312 321 322 + cat := s.holder.Get() 323 + catalog := cat.Costume 324 + config := cat.GameConfig 313 325 userId := CurrentUserId(ctx, s.users, s.sessions) 314 326 nowMillis := gametime.NowMillis() 315 327 ··· 320 332 return 321 333 } 322 334 323 - if costume.LimitBreakCount >= s.config.CostumeLimitBreakAvailableCount { 335 + if costume.LimitBreakCount >= config.CostumeLimitBreakAvailableCount { 324 336 log.Printf("[CostumeService] LimitBreak: already at max limit break %d", costume.LimitBreakCount) 325 337 return 326 338 } 327 339 328 - cm, ok := s.catalog.Costumes[costume.CostumeId] 340 + cm, ok := catalog.Costumes[costume.CostumeId] 329 341 if !ok { 330 342 log.Printf("[CostumeService] LimitBreak: costume master id=%d not found", costume.CostumeId) 331 343 return ··· 342 354 totalMaterialCount += count 343 355 } 344 356 345 - if costFunc, ok := s.catalog.LimitBreakCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 { 357 + if costFunc, ok := catalog.LimitBreakCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 { 346 358 goldCost := costFunc.Evaluate(totalMaterialCount) 347 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 359 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 348 360 log.Printf("[CostumeService] LimitBreak: gold cost=%d", goldCost) 349 361 } 350 362 ··· 363 375 func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req *pb.UnlockLotteryEffectSlotRequest) (*pb.UnlockLotteryEffectSlotResponse, error) { 364 376 log.Printf("[CostumeService] UnlockLotteryEffectSlot: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber) 365 377 378 + cat := s.holder.Get() 379 + catalog := cat.Costume 380 + config := cat.GameConfig 366 381 userId := CurrentUserId(ctx, s.users, s.sessions) 367 382 nowMillis := gametime.NowMillis() 368 383 ··· 373 388 return 374 389 } 375 390 376 - effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] 391 + effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] 377 392 if !ok { 378 393 log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber) 379 394 return 380 395 } 381 396 382 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectUnlockSlotConsumeGold 397 + user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectUnlockSlotConsumeGold 383 398 384 - mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId] 399 + mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId] 385 400 for _, mat := range mats { 386 401 cur := user.Materials[mat.MaterialId] 387 402 cost := mat.Count ··· 418 433 func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.DrawLotteryEffectRequest) (*pb.DrawLotteryEffectResponse, error) { 419 434 log.Printf("[CostumeService] DrawLotteryEffect: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber) 420 435 436 + cat := s.holder.Get() 437 + catalog := cat.Costume 438 + config := cat.GameConfig 421 439 userId := CurrentUserId(ctx, s.users, s.sessions) 422 440 nowMillis := gametime.NowMillis() 423 441 ··· 428 446 return 429 447 } 430 448 431 - effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] 449 + effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] 432 450 if !ok { 433 451 log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber) 434 452 return 435 453 } 436 454 437 - oddsPool := s.catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId] 455 + oddsPool := catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId] 438 456 if len(oddsPool) == 0 { 439 457 log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId) 440 458 return 441 459 } 442 460 443 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectDrawSlotConsumeGold 461 + user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectDrawSlotConsumeGold 444 462 445 - mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId] 463 + mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId] 446 464 for _, mat := range mats { 447 465 cur := user.Materials[mat.MaterialId] 448 466 cost := mat.Count
+19 -2
server/internal/service/data.go
··· 4 4 "context" 5 5 "fmt" 6 6 "log" 7 + "os" 7 8 8 9 pb "lunar-tear/server/gen/proto" 9 10 "lunar-tear/server/internal/store" ··· 11 12 12 13 "google.golang.org/protobuf/types/known/emptypb" 13 14 ) 15 + 16 + // masterDataBinPath is the canonical location of the encrypted master data 17 + // file. The mtime of this file is folded into the version string so the 18 + // client invalidates its cache as soon as an admin reload swaps it in. 19 + const masterDataBinPath = "assets/release/20240404193219.bin.e" 20 + 21 + // masterDataBaseVersion preserves the historical "yyyymmddHHMMSS" value the 22 + // client has always seen; we suffix it with the file mtime to force a 23 + // re-download when content changes. 24 + const masterDataBaseVersion = "20240404193219" 14 25 15 26 type DataServiceServer struct { 16 27 pb.UnimplementedDataServiceServer ··· 23 34 } 24 35 25 36 func (s *DataServiceServer) GetLatestMasterDataVersion(ctx context.Context, _ *emptypb.Empty) (*pb.MasterDataGetLatestVersionResponse, error) { 26 - log.Printf("[DataService] GetLatestMasterDataVersion") 37 + version := masterDataBaseVersion 38 + if info, err := os.Stat(masterDataBinPath); err == nil { 39 + version = fmt.Sprintf("%s_%d", masterDataBaseVersion, info.ModTime().UnixMilli()) 40 + } else { 41 + log.Printf("[DataService] stat %s: %v (falling back to base version)", masterDataBinPath, err) 42 + } 43 + log.Printf("[DataService] GetLatestMasterDataVersion -> %s", version) 27 44 return &pb.MasterDataGetLatestVersionResponse{ 28 - LatestMasterDataVersion: "20240404193219", 45 + LatestMasterDataVersion: version, 29 46 }, nil 30 47 } 31 48
+10 -8
server/internal/service/explore.go
··· 7 7 8 8 pb "lunar-tear/server/gen/proto" 9 9 "lunar-tear/server/internal/gametime" 10 - "lunar-tear/server/internal/masterdata" 11 10 "lunar-tear/server/internal/model" 11 + "lunar-tear/server/internal/runtime" 12 12 "lunar-tear/server/internal/store" 13 13 ) 14 14 ··· 22 22 pb.UnimplementedExploreServiceServer 23 23 users store.UserRepository 24 24 sessions store.SessionRepository 25 - catalog *masterdata.ExploreCatalog 25 + holder *runtime.Holder 26 26 } 27 27 28 - func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ExploreCatalog) *ExploreServiceServer { 29 - return &ExploreServiceServer{users: users, sessions: sessions, catalog: catalog} 28 + func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ExploreServiceServer { 29 + return &ExploreServiceServer{users: users, sessions: sessions, holder: holder} 30 30 } 31 31 32 32 func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartExploreRequest) (*pb.StartExploreResponse, error) { 33 33 log.Printf("[ExploreService] StartExplore: exploreId=%d useConsumableItemId=%d", req.ExploreId, req.UseConsumableItemId) 34 34 35 - if _, ok := s.catalog.Explores[req.ExploreId]; !ok { 35 + catalog := s.holder.Get().Explore 36 + if _, ok := catalog.Explores[req.ExploreId]; !ok { 36 37 return nil, fmt.Errorf("explore id=%d not found", req.ExploreId) 37 38 } 38 39 ··· 40 41 nowMillis := gametime.NowMillis() 41 42 42 43 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { 43 - explore := s.catalog.Explores[req.ExploreId] 44 + explore := catalog.Explores[req.ExploreId] 44 45 if req.UseConsumableItemId > 0 && explore.ConsumeItemCount > 0 { 45 46 cur := user.ConsumableItems[req.UseConsumableItemId] 46 47 user.ConsumableItems[req.UseConsumableItemId] = cur - explore.ConsumeItemCount ··· 64 65 func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.FinishExploreRequest) (*pb.FinishExploreResponse, error) { 65 66 log.Printf("[ExploreService] FinishExplore: exploreId=%d score=%d", req.ExploreId, req.Score) 66 67 67 - explore, ok := s.catalog.Explores[req.ExploreId] 68 + catalog := s.holder.Get().Explore 69 + explore, ok := catalog.Explores[req.ExploreId] 68 70 if !ok { 69 71 return nil, fmt.Errorf("explore id=%d not found", req.ExploreId) 70 72 } 71 73 72 - assetGradeIconId := s.catalog.GradeForScore(req.ExploreId, req.Score) 74 + assetGradeIconId := catalog.GradeForScore(req.ExploreId, req.Score) 73 75 74 76 userId := CurrentUserId(ctx, s.users, s.sessions) 75 77 nowMillis := gametime.NowMillis()
+43 -20
server/internal/service/gacha.go
··· 10 10 "lunar-tear/server/internal/gacha" 11 11 "lunar-tear/server/internal/gametime" 12 12 "lunar-tear/server/internal/model" 13 + "lunar-tear/server/internal/runtime" 13 14 "lunar-tear/server/internal/store" 14 15 15 16 emptypb "google.golang.org/protobuf/types/known/emptypb" ··· 20 21 pb.UnimplementedGachaServiceServer 21 22 users store.UserRepository 22 23 sessions store.SessionRepository 23 - catalog []store.GachaCatalogEntry 24 - handler *gacha.GachaHandler 24 + holder *runtime.Holder 25 25 } 26 26 27 27 func NewGachaServiceServer( 28 28 users store.UserRepository, 29 29 sessions store.SessionRepository, 30 - catalog []store.GachaCatalogEntry, 31 - handler *gacha.GachaHandler, 30 + holder *runtime.Holder, 32 31 ) *GachaServiceServer { 33 32 return &GachaServiceServer{ 34 33 users: users, 35 34 sessions: sessions, 36 - catalog: catalog, 37 - handler: handler, 35 + holder: holder, 38 36 } 39 37 } 40 38 41 39 func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) { 42 40 log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType) 43 41 44 - catalog := s.catalog 42 + cat := s.holder.Get() 43 + catalog := cat.GachaEntries 44 + handler := cat.GachaHandler 45 45 userId := CurrentUserId(ctx, s.users, s.sessions) 46 46 nowMillis := gametime.NowMillis() 47 47 48 48 user, err := s.users.UpdateUser(userId, func(user *store.UserState) { 49 49 user.EnsureMaps() 50 - s.autoConvertExpiredMedals(user, catalog, nowMillis) 50 + autoConvertExpiredMedals(user, catalog, handler, nowMillis) 51 51 }) 52 52 if err != nil { 53 53 return nil, fmt.Errorf("update user: %w", err) ··· 55 55 56 56 gachaList := make([]*pb.Gacha, 0, len(catalog)) 57 57 for _, entry := range catalog { 58 + if !gachaActiveAt(entry, nowMillis) { 59 + continue 60 + } 58 61 if !matchesGachaLabel(req.GachaLabelType, entry.GachaLabelType) { 59 62 continue 60 63 } ··· 71 74 }, nil 72 75 } 73 76 74 - func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, catalog []store.GachaCatalogEntry, nowMillis int64) { 77 + func autoConvertExpiredMedals(user *store.UserState, catalog []store.GachaCatalogEntry, handler *gacha.GachaHandler, nowMillis int64) { 75 78 for _, entry := range catalog { 76 79 if entry.GachaMedalId == 0 || entry.EndDatetime == 0 { 77 80 continue ··· 84 87 continue 85 88 } 86 89 87 - medalInfo, ok := s.handler.MedalInfo[entry.GachaId] 90 + medalInfo, ok := handler.MedalInfo[entry.GachaId] 88 91 if !ok { 89 92 continue 90 93 } ··· 117 120 func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) { 118 121 log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId) 119 122 120 - catalog := s.catalog 123 + catalog := s.holder.Get().GachaEntries 124 + nowMillis := gametime.NowMillis() 121 125 122 126 userId := CurrentUserId(ctx, s.users, s.sessions) 123 127 user, err := s.users.LoadUser(userId) ··· 128 132 byId := make(map[int32]*pb.Gacha, len(req.GachaId)) 129 133 for _, wantedId := range req.GachaId { 130 134 for _, entry := range catalog { 131 - if entry.GachaId == wantedId { 132 - bs := user.Gacha.BannerStates[entry.GachaId] 133 - byId[wantedId] = toProtoGacha(entry, &bs) 135 + if entry.GachaId != wantedId { 136 + continue 137 + } 138 + if !gachaActiveAt(entry, nowMillis) { 134 139 break 135 140 } 141 + bs := user.Gacha.BannerStates[entry.GachaId] 142 + byId[wantedId] = toProtoGacha(entry, &bs) 143 + break 136 144 } 137 145 } 138 146 ··· 144 152 func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) { 145 153 log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount) 146 154 147 - entry := findCatalogEntry(s.catalog, req.GachaId) 155 + cat := s.holder.Get() 156 + entry := findCatalogEntry(cat.GachaEntries, req.GachaId) 148 157 if entry == nil { 149 158 return nil, fmt.Errorf("gacha %d not found", req.GachaId) 150 159 } 160 + handler := cat.GachaHandler 151 161 152 162 userId := CurrentUserId(ctx, s.users, s.sessions) 153 163 execCount := req.ExecCount ··· 158 168 var drawResult *gacha.DrawResult 159 169 updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { 160 170 var drawErr error 161 - drawResult, drawErr = s.handler.HandleDraw(user, *entry, req.GachaPricePhaseId, execCount) 171 + drawResult, drawErr = handler.HandleDraw(user, *entry, req.GachaPricePhaseId, execCount) 162 172 if drawErr != nil { 163 173 log.Printf("[GachaService] Draw error: %v", drawErr) 164 174 drawResult = &gacha.DrawResult{} ··· 285 295 func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) { 286 296 log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId) 287 297 288 - entry := findCatalogEntry(s.catalog, req.GachaId) 298 + cat := s.holder.Get() 299 + entry := findCatalogEntry(cat.GachaEntries, req.GachaId) 289 300 if entry == nil { 290 301 return nil, fmt.Errorf("gacha %d not found", req.GachaId) 291 302 } 303 + handler := cat.GachaHandler 292 304 293 305 userId := CurrentUserId(ctx, s.users, s.sessions) 294 306 updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { 295 - if resetErr := s.handler.HandleResetBox(user, *entry); resetErr != nil { 307 + if resetErr := handler.HandleResetBox(user, *entry); resetErr != nil { 296 308 log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr) 297 309 } 298 310 }) ··· 315 327 return nil, fmt.Errorf("snapshot user: %w", err) 316 328 } 317 329 318 - maxCount := s.handler.Config.RewardGachaDailyMaxCount 330 + maxCount := s.holder.Get().GachaHandler.Config.RewardGachaDailyMaxCount 319 331 if maxCount <= 0 { 320 332 maxCount = model.DefaultDailyDrawLimit 321 333 } ··· 337 349 log.Printf("[GachaService] RewardDraw: placement=%q reward=%q amount=%q", req.PlacementName, req.RewardName, req.RewardAmount) 338 350 339 351 userId := CurrentUserId(ctx, s.users, s.sessions) 352 + handler := s.holder.Get().GachaHandler 340 353 341 354 var items []gacha.DrawnItem 342 355 updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { 343 356 var drawErr error 344 - items, drawErr = s.handler.HandleRewardDraw(user, 1) 357 + items, drawErr = handler.HandleRewardDraw(user, 1) 345 358 if drawErr != nil { 346 359 log.Printf("[GachaService] RewardDraw error: %v", drawErr) 347 360 } ··· 393 406 } 394 407 } 395 408 return false 409 + } 410 + 411 + func gachaActiveAt(entry store.GachaCatalogEntry, nowMillis int64) bool { 412 + if entry.StartDatetime != 0 && nowMillis < entry.StartDatetime { 413 + return false 414 + } 415 + if entry.EndDatetime != 0 && nowMillis >= entry.EndDatetime { 416 + return false 417 + } 418 + return true 396 419 } 397 420 398 421 func toProtoGacha(entry store.GachaCatalogEntry, bs *store.GachaBannerState) *pb.Gacha {
+7 -7
server/internal/service/gimmick.go
··· 6 6 7 7 pb "lunar-tear/server/gen/proto" 8 8 "lunar-tear/server/internal/gametime" 9 - "lunar-tear/server/internal/masterdata" 9 + "lunar-tear/server/internal/runtime" 10 10 "lunar-tear/server/internal/store" 11 11 12 12 emptypb "google.golang.org/protobuf/types/known/emptypb" ··· 14 14 15 15 type GimmickServiceServer struct { 16 16 pb.UnimplementedGimmickServiceServer 17 - users store.UserRepository 18 - sessions store.SessionRepository 19 - gimmickCatalog *masterdata.GimmickCatalog 17 + users store.UserRepository 18 + sessions store.SessionRepository 19 + holder *runtime.Holder 20 20 } 21 21 22 - func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, gimmickCatalog *masterdata.GimmickCatalog) *GimmickServiceServer { 23 - return &GimmickServiceServer{users: users, sessions: sessions, gimmickCatalog: gimmickCatalog} 22 + func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *GimmickServiceServer { 23 + return &GimmickServiceServer{users: users, sessions: sessions, holder: holder} 24 24 } 25 25 26 26 func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) { ··· 80 80 now := gametime.NowMillis() 81 81 s.users.UpdateUser(userId, func(user *store.UserState) { 82 82 added := 0 83 - for _, key := range s.gimmickCatalog.ActiveScheduleKeys(*user, now) { 83 + for _, key := range s.holder.Get().Gimmick.ActiveScheduleKeys(*user, now) { 84 84 if _, exists := user.Gimmick.Sequences[key]; !exists { 85 85 user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key} 86 86 added++
+6 -5
server/internal/service/loginbonus.go
··· 9 9 10 10 pb "lunar-tear/server/gen/proto" 11 11 "lunar-tear/server/internal/gametime" 12 - "lunar-tear/server/internal/masterdata" 12 + "lunar-tear/server/internal/runtime" 13 13 "lunar-tear/server/internal/store" 14 14 15 15 emptypb "google.golang.org/protobuf/types/known/emptypb" ··· 19 19 pb.UnimplementedLoginBonusServiceServer 20 20 users store.UserRepository 21 21 sessions store.SessionRepository 22 - catalog *masterdata.LoginBonusCatalog 22 + holder *runtime.Holder 23 23 } 24 24 25 - func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.LoginBonusCatalog) *LoginBonusServiceServer { 26 - return &LoginBonusServiceServer{users: users, sessions: sessions, catalog: catalog} 25 + func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *LoginBonusServiceServer { 26 + return &LoginBonusServiceServer{users: users, sessions: sessions, holder: holder} 27 27 } 28 28 29 29 func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) { 30 30 log.Printf("[LoginBonusService] ReceiveStamp") 31 31 userId := CurrentUserId(ctx, s.users, s.sessions) 32 + catalog := s.holder.Get().LoginBonus 32 33 33 34 s.users.UpdateUser(userId, func(user *store.UserState) { 34 35 now := gametime.NowMillis() 35 36 nextStamp := user.LoginBonus.CurrentStampNumber + 1 36 37 37 - reward, ok := s.catalog.LookupStampReward( 38 + reward, ok := catalog.LookupStampReward( 38 39 user.LoginBonus.LoginBonusId, 39 40 user.LoginBonus.CurrentPageNumber, 40 41 nextStamp,
+9 -7
server/internal/service/material.go
··· 6 6 "log" 7 7 8 8 pb "lunar-tear/server/gen/proto" 9 - "lunar-tear/server/internal/masterdata" 9 + "lunar-tear/server/internal/runtime" 10 10 "lunar-tear/server/internal/store" 11 11 ) 12 12 ··· 14 14 pb.UnimplementedMaterialServiceServer 15 15 users store.UserRepository 16 16 sessions store.SessionRepository 17 - catalog *masterdata.MaterialCatalog 18 - config *masterdata.GameConfig 17 + holder *runtime.Holder 19 18 } 20 19 21 - func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.MaterialCatalog, config *masterdata.GameConfig) *MaterialServiceServer { 22 - return &MaterialServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} 20 + func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *MaterialServiceServer { 21 + return &MaterialServiceServer{users: users, sessions: sessions, holder: holder} 23 22 } 24 23 25 24 func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) { 26 25 log.Printf("[MaterialService] Sell: %d item(s)", len(req.MaterialPossession)) 27 26 27 + cat := s.holder.Get() 28 + catalog := cat.Material 29 + config := cat.GameConfig 28 30 userId := CurrentUserId(ctx, s.users, s.sessions) 29 31 30 32 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { 31 33 totalGold := int32(0) 32 34 for _, item := range req.MaterialPossession { 33 - mat, ok := s.catalog.All[item.MaterialId] 35 + mat, ok := catalog.All[item.MaterialId] 34 36 if !ok { 35 37 log.Printf("[MaterialService] Sell: unknown materialId=%d, skipping", item.MaterialId) 36 38 continue ··· 53 55 } 54 56 55 57 if totalGold > 0 { 56 - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold 58 + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold 57 59 log.Printf("[MaterialService] Sell: total gold +%d", totalGold) 58 60 } 59 61 })
+6 -18
server/internal/service/octo.go
··· 414 414 w.Write(data) 415 415 } 416 416 417 - // serveDatabaseBinE serves MasterMemory database: /assets/release/{version}/database.bin.e 418 - // -> assets/release/{version}.bin.e (or assets/release/database.bin.e fallback). 419 - func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, path string) { 420 - parts := strings.Split(path, "/") 421 - var version string 422 - for i, p := range parts { 423 - if p == "release" && i+1 < len(parts) { 424 - version = parts[i+1] 425 - break 426 - } 427 - } 428 - filePath := filepath.Join(s.BaseDir, "assets", "release", "database.bin.e") 429 - if version != "" { 430 - vPath := filepath.Join(s.BaseDir, "assets", "release", version+".bin.e") 431 - if _, err := os.Stat(vPath); err == nil { 432 - filePath = vPath 433 - } 434 - } 417 + // serveDatabaseBinE serves the master data binary. The URL's {version} segment 418 + // is a cache key (it changes whenever the file's mtime changes, see 419 + // DataService.GetLatestMasterDataVersion) but does not select a different file — 420 + // there's only ever one bin.e on disk. 421 + func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, _ string) { 422 + filePath := filepath.Join(s.BaseDir, "assets", "release", "20240404193219.bin.e") 435 423 w.Header().Set("Content-Type", "application/octet-stream") 436 424 http.ServeFile(w, r, filePath) 437 425 }
+5 -5
server/internal/service/omikuji.go
··· 7 7 8 8 pb "lunar-tear/server/gen/proto" 9 9 "lunar-tear/server/internal/gametime" 10 - "lunar-tear/server/internal/masterdata" 10 + "lunar-tear/server/internal/runtime" 11 11 "lunar-tear/server/internal/store" 12 12 ) 13 13 ··· 15 15 pb.UnimplementedOmikujiServiceServer 16 16 users store.UserRepository 17 17 sessions store.SessionRepository 18 - catalog *masterdata.OmikujiCatalog 18 + holder *runtime.Holder 19 19 } 20 20 21 - func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.OmikujiCatalog) *OmikujiServiceServer { 22 - return &OmikujiServiceServer{users: users, sessions: sessions, catalog: catalog} 21 + func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *OmikujiServiceServer { 22 + return &OmikujiServiceServer{users: users, sessions: sessions, holder: holder} 23 23 } 24 24 25 25 func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiDrawRequest) (*pb.OmikujiDrawResponse, error) { ··· 36 36 } 37 37 38 38 return &pb.OmikujiDrawResponse{ 39 - OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId), 39 + OmikujiResultAssetId: s.holder.Get().Omikuji.LookupAssetId(req.OmikujiId), 40 40 OmikujiItem: []*pb.OmikujiItem{}, 41 41 }, nil 42 42 }
+25 -19
server/internal/service/parts.go
··· 9 9 pb "lunar-tear/server/gen/proto" 10 10 "lunar-tear/server/internal/gametime" 11 11 "lunar-tear/server/internal/masterdata" 12 + "lunar-tear/server/internal/runtime" 12 13 "lunar-tear/server/internal/store" 13 14 ) 14 15 ··· 18 19 pb.UnimplementedPartsServiceServer 19 20 users store.UserRepository 20 21 sessions store.SessionRepository 21 - catalog *masterdata.PartsCatalog 22 - config *masterdata.GameConfig 22 + holder *runtime.Holder 23 23 } 24 24 25 - func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.PartsCatalog, config *masterdata.GameConfig) *PartsServiceServer { 26 - return &PartsServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} 25 + func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *PartsServiceServer { 26 + return &PartsServiceServer{users: users, sessions: sessions, holder: holder} 27 27 } 28 28 29 29 func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) { 30 30 log.Printf("[PartsService] Sell: %d part(s)", len(req.UserPartsUuid)) 31 31 32 + cat := s.holder.Get() 33 + catalog := cat.Parts 34 + config := cat.GameConfig 32 35 userId := CurrentUserId(ctx, s.users, s.sessions) 33 36 34 37 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { ··· 44 47 continue 45 48 } 46 49 47 - partDef, ok := s.catalog.PartsById[part.PartsId] 50 + partDef, ok := catalog.PartsById[part.PartsId] 48 51 if !ok { 49 52 log.Printf("[PartsService] Sell: partsId=%d not in catalog, skipping", part.PartsId) 50 53 continue 51 54 } 52 55 53 - sellFunc, ok := s.catalog.SellPriceByRarity[partDef.RarityType] 56 + sellFunc, ok := catalog.SellPriceByRarity[partDef.RarityType] 54 57 if !ok { 55 58 log.Printf("[PartsService] Sell: no sell price func for rarity=%d, skipping", partDef.RarityType) 56 59 continue ··· 68 71 } 69 72 70 73 if totalGold > 0 { 71 - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold 74 + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold 72 75 log.Printf("[PartsService] Sell: total gold +%d", totalGold) 73 76 } 74 77 }) ··· 82 85 func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) { 83 86 log.Printf("[PartsService] Enhance: uuid=%s", req.UserPartsUuid) 84 87 88 + cat := s.holder.Get() 89 + catalog := cat.Parts 90 + config := cat.GameConfig 85 91 userId := CurrentUserId(ctx, s.users, s.sessions) 86 92 nowMillis := gametime.NowMillis() 87 93 ··· 99 105 return 100 106 } 101 107 102 - partDef, ok := s.catalog.PartsById[part.PartsId] 108 + partDef, ok := catalog.PartsById[part.PartsId] 103 109 if !ok { 104 110 log.Printf("[PartsService] Enhance: part master id=%d not found", part.PartsId) 105 111 return 106 112 } 107 113 108 - rarity, ok := s.catalog.RarityByRarityType[partDef.RarityType] 114 + rarity, ok := catalog.RarityByRarityType[partDef.RarityType] 109 115 if !ok { 110 116 log.Printf("[PartsService] Enhance: rarity type=%d not found", partDef.RarityType) 111 117 return 112 118 } 113 119 114 120 goldCost := int32(0) 115 - if prices, ok := s.catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok { 121 + if prices, ok := catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok { 116 122 goldCost = prices[part.Level] 117 123 } 118 124 119 - currentGold := user.ConsumableItems[s.config.ConsumableItemIdForGold] 125 + currentGold := user.ConsumableItems[config.ConsumableItemIdForGold] 120 126 if currentGold < goldCost { 121 127 log.Printf("[PartsService] Enhance: insufficient gold have=%d need=%d", currentGold, goldCost) 122 128 return 123 129 } 124 130 125 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 131 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 126 132 127 133 successRate := int32(1000) 128 - if rates, ok := s.catalog.RateByGroupAndLevel[rarity.PartsLevelUpRateGroupId]; ok { 134 + if rates, ok := catalog.RateByGroupAndLevel[rarity.PartsLevelUpRateGroupId]; ok { 129 135 if r, ok := rates[part.Level]; ok { 130 136 successRate = r 131 137 } ··· 137 143 log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)", 138 144 part.PartsId, part.Level-1, part.Level, successRate, goldCost) 139 145 140 - s.grantSubStatuses(user, req.UserPartsUuid, part, partDef, nowMillis) 146 + grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis) 141 147 } else { 142 148 log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)", 143 149 part.PartsId, part.Level, successRate, goldCost) ··· 155 161 }, nil 156 162 } 157 163 158 - func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) { 159 - unlockLevels := s.catalog.SubStatusUnlockLvls[partDef.RarityType] 160 - pool := s.catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId] 164 + func grantPartsSubStatuses(catalog *masterdata.PartsCatalog, user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) { 165 + unlockLevels := catalog.SubStatusUnlockLvls[partDef.RarityType] 166 + pool := catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId] 161 167 if len(pool) == 0 { 162 168 return 163 169 } ··· 173 179 } 174 180 175 181 pick := pool[rand.Intn(len(pool))] 176 - def, ok := s.catalog.PartsStatusMainById[pick] 182 + def, ok := catalog.PartsStatusMainById[pick] 177 183 if !ok { 178 184 continue 179 185 } 180 186 181 187 statusValue := def.StatusChangeInitialValue 182 - if f, ok := s.catalog.FuncResolver.Resolve(def.StatusNumericalFunctionId); ok { 188 + if f, ok := catalog.FuncResolver.Resolve(def.StatusNumericalFunctionId); ok { 183 189 statusValue = f.Evaluate(part.Level) 184 190 } 185 191
+36 -28
server/internal/service/quest_bighunt.go
··· 8 8 "lunar-tear/server/internal/gametime" 9 9 "lunar-tear/server/internal/masterdata" 10 10 "lunar-tear/server/internal/model" 11 - "lunar-tear/server/internal/questflow" 11 + "lunar-tear/server/internal/runtime" 12 12 "lunar-tear/server/internal/store" 13 13 14 14 emptypb "google.golang.org/protobuf/types/known/emptypb" ··· 18 18 pb.UnimplementedBigHuntServiceServer 19 19 users store.UserRepository 20 20 sessions store.SessionRepository 21 - catalog *masterdata.BigHuntCatalog 22 - engine *questflow.QuestHandler 21 + holder *runtime.Holder 23 22 } 24 23 25 24 func NewBigHuntServiceServer( 26 25 users store.UserRepository, 27 26 sessions store.SessionRepository, 28 - catalog *masterdata.BigHuntCatalog, 29 - engine *questflow.QuestHandler, 27 + holder *runtime.Holder, 30 28 ) *BigHuntServiceServer { 31 - return &BigHuntServiceServer{users: users, sessions: sessions, catalog: catalog, engine: engine} 29 + return &BigHuntServiceServer{users: users, sessions: sessions, holder: holder} 32 30 } 33 31 34 32 func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.StartBigHuntQuestRequest) (*pb.StartBigHuntQuestResponse, error) { 35 33 log.Printf("[BigHuntService] StartBigHuntQuest: bossQuestId=%d questId=%d deckNumber=%d isDryRun=%v", 36 34 req.BigHuntBossQuestId, req.BigHuntQuestId, req.UserDeckNumber, req.IsDryRun) 37 35 36 + cat := s.holder.Get() 37 + catalog := cat.BigHunt 38 + engine := cat.QuestHandler 38 39 userId := CurrentUserId(ctx, s.users, s.sessions) 39 40 nowMillis := gametime.NowMillis() 40 41 41 - bhQuest, ok := s.catalog.QuestById[req.BigHuntQuestId] 42 + bhQuest, ok := catalog.QuestById[req.BigHuntQuestId] 42 43 if !ok { 43 44 log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId) 44 45 } 45 46 46 47 s.users.UpdateUser(userId, func(user *store.UserState) { 47 48 if ok { 48 - s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis) 49 + engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis) 49 50 } 50 51 51 52 user.BigHuntProgress = store.BigHuntProgress{ ··· 85 86 log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v", 86 87 req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired) 87 88 89 + cat := s.holder.Get() 90 + catalog := cat.BigHunt 91 + engine := cat.QuestHandler 88 92 userId := CurrentUserId(ctx, s.users, s.sessions) 89 93 nowMillis := gametime.NowMillis() 90 94 91 - bhQuest := s.catalog.QuestById[req.BigHuntQuestId] 92 - bossQuest := s.catalog.BossQuestById[req.BigHuntBossQuestId] 93 - boss := s.catalog.BossByBossId[bossQuest.BigHuntBossId] 95 + bhQuest := catalog.QuestById[req.BigHuntQuestId] 96 + bossQuest := catalog.BossQuestById[req.BigHuntBossQuestId] 97 + boss := catalog.BossByBossId[bossQuest.BigHuntBossId] 94 98 95 99 var scoreInfo *pb.BigHuntScoreInfo 96 100 var scoreRewards []*pb.BigHuntReward 97 101 98 102 s.users.UpdateUser(userId, func(user *store.UserState) { 99 - s.engine.HandleBigHuntQuestFinish(user, bhQuest.QuestId, req.IsRetired, false, nowMillis) 103 + engine.HandleBigHuntQuestFinish(user, bhQuest.QuestId, req.IsRetired, false, nowMillis) 100 104 101 105 if req.IsRetired || user.BigHuntProgress.IsDryRun { 102 106 user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis} ··· 108 112 baseScore := totalDamage 109 113 110 114 difficultyBonusPermil := int32(0) 111 - if coeff, ok := s.catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok { 115 + if coeff, ok := catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok { 112 116 difficultyBonusPermil = coeff 113 117 } 114 118 ··· 138 142 } 139 143 140 144 schedKey := store.BigHuntScheduleScoreKey{ 141 - BigHuntScheduleId: s.catalog.ActiveScheduleId, 145 + BigHuntScheduleId: catalog.ActiveScheduleId, 142 146 BigHuntBossId: bossQuest.BigHuntBossId, 143 147 } 144 148 oldSchedMax := user.BigHuntScheduleMaxScores[schedKey].MaxScore ··· 163 167 } 164 168 } 165 169 166 - assetGradeIconId := s.catalog.ResolveGradeIconId(bossQuest.BigHuntBossId, userScore) 170 + assetGradeIconId := catalog.ResolveGradeIconId(bossQuest.BigHuntBossId, userScore) 167 171 168 172 scoreInfo = &pb.BigHuntScoreInfo{ 169 173 UserScore: userScore, ··· 177 181 } 178 182 179 183 if isHighScore { 180 - rewardGroupId := s.catalog.ResolveActiveScoreRewardGroupId( 184 + rewardGroupId := catalog.ResolveActiveScoreRewardGroupId( 181 185 bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis) 182 186 if rewardGroupId > 0 { 183 - newItems := s.catalog.CollectNewRewards(rewardGroupId, oldMax, userScore) 187 + newItems := catalog.CollectNewRewards(rewardGroupId, oldMax, userScore) 184 188 for _, item := range newItems { 185 - s.engine.Granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis) 189 + engine.Granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis) 186 190 scoreRewards = append(scoreRewards, &pb.BigHuntReward{ 187 191 PossessionType: item.PossessionType, 188 192 PossessionId: item.PossessionId, ··· 216 220 func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb.RestartBigHuntQuestRequest) (*pb.RestartBigHuntQuestResponse, error) { 217 221 log.Printf("[BigHuntService] RestartBigHuntQuest: bossQuestId=%d questId=%d", req.BigHuntBossQuestId, req.BigHuntQuestId) 218 222 223 + cat := s.holder.Get() 224 + catalog := cat.BigHunt 225 + engine := cat.QuestHandler 219 226 userId := CurrentUserId(ctx, s.users, s.sessions) 220 227 nowMillis := gametime.NowMillis() 221 228 222 - bhQuest := s.catalog.QuestById[req.BigHuntQuestId] 229 + bhQuest := catalog.QuestById[req.BigHuntQuestId] 223 230 224 231 var battleBinary []byte 225 232 var deckNumber int32 226 233 227 234 s.users.UpdateUser(userId, func(user *store.UserState) { 228 - s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, user.BigHuntDeckNumber, nowMillis) 235 + engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, user.BigHuntDeckNumber, nowMillis) 229 236 230 237 user.BigHuntProgress.CurrentQuestSceneId = 0 231 238 user.BigHuntProgress.LatestVersion = nowMillis ··· 302 309 func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) { 303 310 log.Printf("[BigHuntService] GetBigHuntTopData") 304 311 312 + catalog := s.holder.Get().BigHunt 305 313 userId := CurrentUserId(ctx, s.users, s.sessions) 306 314 user, _ := s.users.LoadUser(userId) 307 315 ··· 309 317 weeklyVersion := gametime.WeeklyVersion(nowMillis) 310 318 311 319 var weeklyScoreResults []*pb.WeeklyScoreResult 312 - for _, boss := range s.catalog.BossByBossId { 320 + for _, boss := range catalog.BossByBossId { 313 321 key := store.BigHuntWeeklyScoreKey{ 314 322 BigHuntWeeklyVersion: weeklyVersion, 315 323 AttributeType: boss.AttributeType, 316 324 } 317 325 ws := user.BigHuntWeeklyMaxScores[key] 318 - gradeIconId := s.catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore) 326 + gradeIconId := catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore) 319 327 320 328 weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{ 321 329 AttributeType: boss.AttributeType, ··· 330 338 331 339 ws := user.BigHuntWeeklyStatuses[weeklyVersion] 332 340 333 - weeklyRewards := s.resolveWeeklyRewards(user, weeklyVersion, nowMillis) 341 + weeklyRewards := resolveBigHuntWeeklyRewards(catalog, user, weeklyVersion, nowMillis) 334 342 335 343 lastWeekVersion := weeklyVersion - 7*24*60*60*1000 336 - lastWeekRewards := s.resolveWeeklyRewards(user, lastWeekVersion, nowMillis) 344 + lastWeekRewards := resolveBigHuntWeeklyRewards(catalog, user, lastWeekVersion, nowMillis) 337 345 338 346 return &pb.GetBigHuntTopDataResponse{ 339 347 WeeklyScoreResult: weeklyScoreResults, ··· 343 351 }, nil 344 352 } 345 353 346 - func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward { 354 + func resolveBigHuntWeeklyRewards(catalog *masterdata.BigHuntCatalog, user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward { 347 355 var rewards []*pb.BigHuntReward 348 - for _, boss := range s.catalog.BossByBossId { 356 + for _, boss := range catalog.BossByBossId { 349 357 rewardKey := masterdata.BigHuntWeeklyRewardKey{ 350 358 ScheduleId: 1, 351 359 AttributeType: boss.AttributeType, 352 360 } 353 - rewardGroupId := s.catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) 361 + rewardGroupId := catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) 354 362 if rewardGroupId == 0 { 355 363 continue 356 364 } ··· 359 367 AttributeType: boss.AttributeType, 360 368 } 361 369 maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore 362 - for _, item := range s.catalog.CollectNewRewards(rewardGroupId, 0, maxScore) { 370 + for _, item := range catalog.CollectNewRewards(rewardGroupId, 0, maxScore) { 363 371 rewards = append(rewards, &pb.BigHuntReward{ 364 372 PossessionType: item.PossessionType, 365 373 PossessionId: item.PossessionId,
+9 -5
server/internal/service/quest_event.go
··· 15 15 func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) { 16 16 log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly) 17 17 18 + engine := s.holder.Get().QuestHandler 18 19 userId := CurrentUserId(ctx, s.users, s.sessions) 19 20 nowMillis := gametime.NowMillis() 20 21 s.users.UpdateUser(userId, func(user *store.UserState) { 21 - s.engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) 22 + engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) 22 23 }) 23 24 24 - drops := s.engine.BattleDropRewards(req.QuestId) 25 + drops := engine.BattleDropRewards(req.QuestId) 25 26 pbDrops := make([]*pb.BattleDropReward, len(drops)) 26 27 for i, d := range drops { 27 28 pbDrops[i] = &pb.BattleDropReward{ ··· 40 41 log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated) 41 42 42 43 nowMillis := gametime.NowMillis() 44 + engine := s.holder.Get().QuestHandler 43 45 userId := CurrentUserId(ctx, s.users, s.sessions) 44 46 var outcome questflow.FinishOutcome 45 47 s.users.UpdateUser(userId, func(user *store.UserState) { 46 - outcome = s.engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) 48 + outcome = engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) 47 49 }) 48 50 49 51 return &pb.FinishEventQuestResponse{ ··· 61 63 func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.RestartEventQuestRequest) (*pb.RestartEventQuestResponse, error) { 62 64 log.Printf("[QuestService] RestartEventQuest: chapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId) 63 65 66 + engine := s.holder.Get().QuestHandler 64 67 userId := CurrentUserId(ctx, s.users, s.sessions) 65 68 s.users.UpdateUser(userId, func(user *store.UserState) { 66 - s.engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis()) 69 + engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis()) 67 70 }) 68 71 69 72 return &pb.RestartEventQuestResponse{ ··· 74 77 func (s *QuestServiceServer) UpdateEventQuestSceneProgress(ctx context.Context, req *pb.UpdateEventQuestSceneProgressRequest) (*pb.UpdateEventQuestSceneProgressResponse, error) { 75 78 log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId) 76 79 80 + engine := s.holder.Get().QuestHandler 77 81 userId := CurrentUserId(ctx, s.users, s.sessions) 78 82 s.users.UpdateUser(userId, func(user *store.UserState) { 79 - s.engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 83 + engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 80 84 }) 81 85 82 86 return &pb.UpdateEventQuestSceneProgressResponse{}, nil
+10 -6
server/internal/service/quest_extra.go
··· 13 13 func (s *QuestServiceServer) StartExtraQuest(ctx context.Context, req *pb.StartExtraQuestRequest) (*pb.StartExtraQuestResponse, error) { 14 14 log.Printf("[QuestService] StartExtraQuest: questId=%d deckNumber=%d", req.QuestId, req.UserDeckNumber) 15 15 16 + engine := s.holder.Get().QuestHandler 16 17 userId := CurrentUserId(ctx, s.users, s.sessions) 17 18 nowMillis := gametime.NowMillis() 18 19 s.users.UpdateUser(userId, func(user *store.UserState) { 19 - s.engine.HandleExtraQuestStart(user, req.QuestId, req.UserDeckNumber, nowMillis) 20 + engine.HandleExtraQuestStart(user, req.QuestId, req.UserDeckNumber, nowMillis) 20 21 }) 21 22 22 - drops := s.engine.BattleDropRewards(req.QuestId) 23 + drops := engine.BattleDropRewards(req.QuestId) 23 24 pbDrops := make([]*pb.BattleDropReward, len(drops)) 24 25 for i, d := range drops { 25 26 pbDrops[i] = &pb.BattleDropReward{ ··· 38 39 log.Printf("[QuestService] FinishExtraQuest: questId=%d isRetired=%v isAnnihilated=%v", req.QuestId, req.IsRetired, req.IsAnnihilated) 39 40 40 41 nowMillis := gametime.NowMillis() 42 + engine := s.holder.Get().QuestHandler 41 43 userId := CurrentUserId(ctx, s.users, s.sessions) 42 44 var outcome questflow.FinishOutcome 43 45 s.users.UpdateUser(userId, func(user *store.UserState) { 44 - outcome = s.engine.HandleExtraQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) 46 + outcome = engine.HandleExtraQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) 45 47 }) 46 48 47 49 return &pb.FinishExtraQuestResponse{ ··· 58 60 func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.RestartExtraQuestRequest) (*pb.RestartExtraQuestResponse, error) { 59 61 log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId) 60 62 63 + engine := s.holder.Get().QuestHandler 61 64 userId := CurrentUserId(ctx, s.users, s.sessions) 62 65 var deckNumber int32 63 66 s.users.UpdateUser(userId, func(user *store.UserState) { 64 - s.engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis()) 67 + engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis()) 65 68 deckNumber = user.Quests[req.QuestId].UserDeckNumber 66 69 }) 67 70 68 - drops := s.engine.BattleDropRewards(req.QuestId) 71 + drops := engine.BattleDropRewards(req.QuestId) 69 72 pbDrops := make([]*pb.BattleDropReward, len(drops)) 70 73 for i, d := range drops { 71 74 pbDrops[i] = &pb.BattleDropReward{ ··· 84 87 func (s *QuestServiceServer) UpdateExtraQuestSceneProgress(ctx context.Context, req *pb.UpdateExtraQuestSceneProgressRequest) (*pb.UpdateExtraQuestSceneProgressResponse, error) { 85 88 log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId) 86 89 90 + engine := s.holder.Get().QuestHandler 87 91 userId := CurrentUserId(ctx, s.users, s.sessions) 88 92 s.users.UpdateUser(userId, func(user *store.UserState) { 89 - s.engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 93 + engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 90 94 }) 91 95 92 96 return &pb.UpdateExtraQuestSceneProgressResponse{}, nil
+25 -16
server/internal/service/quest_main.go
··· 8 8 "lunar-tear/server/internal/gametime" 9 9 "lunar-tear/server/internal/model" 10 10 "lunar-tear/server/internal/questflow" 11 + "lunar-tear/server/internal/runtime" 11 12 "lunar-tear/server/internal/store" 12 13 13 14 emptypb "google.golang.org/protobuf/types/known/emptypb" ··· 17 18 pb.UnimplementedQuestServiceServer 18 19 users store.UserRepository 19 20 sessions store.SessionRepository 20 - engine *questflow.QuestHandler 21 + holder *runtime.Holder 21 22 } 22 23 23 - func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *QuestServiceServer { 24 - if engine == nil { 25 - panic("quest handler is required") 24 + func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *QuestServiceServer { 25 + if holder == nil { 26 + panic("runtime holder is required") 26 27 } 27 - return &QuestServiceServer{users: users, sessions: sessions, engine: engine} 28 + return &QuestServiceServer{users: users, sessions: sessions, holder: holder} 28 29 } 29 30 30 31 func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, req *pb.UpdateMainFlowSceneProgressRequest) (*pb.UpdateMainFlowSceneProgressResponse, error) { 31 32 log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId) 32 33 34 + engine := s.holder.Get().QuestHandler 33 35 userId := CurrentUserId(ctx, s.users, s.sessions) 34 36 s.users.UpdateUser(userId, func(user *store.UserState) { 35 - s.engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 37 + engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 36 38 }) 37 39 38 40 return &pb.UpdateMainFlowSceneProgressResponse{}, nil ··· 41 43 func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, req *pb.UpdateReplayFlowSceneProgressRequest) (*pb.UpdateReplayFlowSceneProgressResponse, error) { 42 44 log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId) 43 45 46 + engine := s.holder.Get().QuestHandler 44 47 userId := CurrentUserId(ctx, s.users, s.sessions) 45 48 s.users.UpdateUser(userId, func(user *store.UserState) { 46 - s.engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 49 + engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) 47 50 }) 48 51 49 52 return &pb.UpdateReplayFlowSceneProgressResponse{}, nil ··· 52 55 func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) { 53 56 log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId) 54 57 58 + engine := s.holder.Get().QuestHandler 55 59 userId := CurrentUserId(ctx, s.users, s.sessions) 56 60 s.users.UpdateUser(userId, func(user *store.UserState) { 57 - s.engine.HandleMainQuestSceneProgress(user, req.QuestSceneId) 61 + engine.HandleMainQuestSceneProgress(user, req.QuestSceneId) 58 62 }) 59 63 60 64 return &pb.UpdateMainQuestSceneProgressResponse{}, nil ··· 63 67 func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) { 64 68 log.Printf("[QuestService] StartMainQuest: %+v", req) 65 69 70 + engine := s.holder.Get().QuestHandler 66 71 userId := CurrentUserId(ctx, s.users, s.sessions) 67 72 nowMillis := gametime.NowMillis() 68 73 s.users.UpdateUser(userId, func(user *store.UserState) { 69 74 if req.IsReplayFlow { 70 - s.engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) 75 + engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) 71 76 } else { 72 - s.engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) 77 + engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) 73 78 } 74 79 }) 75 80 76 - drops := s.engine.BattleDropRewards(req.QuestId) 81 + drops := engine.BattleDropRewards(req.QuestId) 77 82 pbDrops := make([]*pb.BattleDropReward, len(drops)) 78 83 for i, d := range drops { 79 84 pbDrops[i] = &pb.BattleDropReward{ ··· 108 113 req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType) 109 114 110 115 nowMillis := gametime.NowMillis() 116 + engine := s.holder.Get().QuestHandler 111 117 userId := CurrentUserId(ctx, s.users, s.sessions) 112 118 var outcome questflow.FinishOutcome 113 119 s.users.UpdateUser(userId, func(user *store.UserState) { 114 - outcome = s.engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) 120 + outcome = engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) 115 121 }) 116 122 117 123 return &pb.FinishMainQuestResponse{ ··· 130 136 func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.RestartMainQuestRequest) (*pb.RestartMainQuestResponse, error) { 131 137 log.Printf("[QuestService] RestartMainQuest: questId=%d isMainFlow=%v", req.QuestId, req.IsMainFlow) 132 138 139 + engine := s.holder.Get().QuestHandler 133 140 userId := CurrentUserId(ctx, s.users, s.sessions) 134 141 var deckNumber int32 135 142 s.users.UpdateUser(userId, func(user *store.UserState) { 136 - s.engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis()) 143 + engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis()) 137 144 deckNumber = user.Quests[req.QuestId].UserDeckNumber 138 145 }) 139 146 140 - drops := s.engine.BattleDropRewards(req.QuestId) 147 + drops := engine.BattleDropRewards(req.QuestId) 141 148 pbDrops := make([]*pb.BattleDropReward, len(drops)) 142 149 for i, d := range drops { 143 150 pbDrops[i] = &pb.BattleDropReward{ ··· 162 169 log.Printf("[QuestService] SkipQuest: questId=%d skipCount=%d useEffectItems=%d", req.QuestId, req.SkipCount, len(req.UseEffectItem)) 163 170 164 171 nowMillis := gametime.NowMillis() 172 + engine := s.holder.Get().QuestHandler 165 173 userId := CurrentUserId(ctx, s.users, s.sessions) 166 174 var outcome questflow.FinishOutcome 167 175 s.users.UpdateUser(userId, func(user *store.UserState) { ··· 172 180 user.ConsumableItems[item.ConsumableItemId] = 0 173 181 } 174 182 } 175 - outcome = s.engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis) 183 + outcome = engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis) 176 184 }) 177 185 178 186 return &pb.SkipQuestResponse{ ··· 184 192 func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) { 185 193 log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId) 186 194 195 + engine := s.holder.Get().QuestHandler 187 196 userId := CurrentUserId(ctx, s.users, s.sessions) 188 197 s.users.UpdateUser(userId, func(user *store.UserState) { 189 198 user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId 190 - if seasonId, ok := s.engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok { 199 + if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok { 191 200 user.MainQuest.MainQuestSeasonId = seasonId 192 201 } 193 202 now := gametime.NowMillis()
+5 -5
server/internal/service/quest_sidestory.go
··· 6 6 7 7 pb "lunar-tear/server/gen/proto" 8 8 "lunar-tear/server/internal/gametime" 9 - "lunar-tear/server/internal/masterdata" 10 9 "lunar-tear/server/internal/model" 10 + "lunar-tear/server/internal/runtime" 11 11 "lunar-tear/server/internal/store" 12 12 ) 13 13 ··· 15 15 pb.UnimplementedSideStoryQuestServiceServer 16 16 users store.UserRepository 17 17 sessions store.SessionRepository 18 - catalog *masterdata.SideStoryCatalog 18 + holder *runtime.Holder 19 19 } 20 20 21 - func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.SideStoryCatalog) *SideStoryQuestServiceServer { 22 - return &SideStoryQuestServiceServer{users: users, sessions: sessions, catalog: catalog} 21 + func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *SideStoryQuestServiceServer { 22 + return &SideStoryQuestServiceServer{users: users, sessions: sessions, holder: holder} 23 23 } 24 24 25 25 func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) { ··· 27 27 28 28 userId := CurrentUserId(ctx, s.users, s.sessions) 29 29 nowMillis := gametime.NowMillis() 30 - firstSceneId := s.catalog.FirstSceneByQuestId[req.SideStoryQuestId] 30 + firstSceneId := s.holder.Get().SideStory.FirstSceneByQuestId[req.SideStoryQuestId] 31 31 32 32 s.users.UpdateUser(userId, func(user *store.UserState) { 33 33 existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
+15 -13
server/internal/service/reward.go
··· 8 8 "lunar-tear/server/internal/gametime" 9 9 "lunar-tear/server/internal/masterdata" 10 10 "lunar-tear/server/internal/model" 11 + "lunar-tear/server/internal/runtime" 11 12 "lunar-tear/server/internal/store" 12 13 13 14 emptypb "google.golang.org/protobuf/types/known/emptypb" ··· 15 16 16 17 type RewardServiceServer struct { 17 18 pb.UnimplementedRewardServiceServer 18 - users store.UserRepository 19 - sessions store.SessionRepository 20 - bhCatalog *masterdata.BigHuntCatalog 21 - granter *store.PossessionGranter 19 + users store.UserRepository 20 + sessions store.SessionRepository 21 + holder *runtime.Holder 22 22 } 23 23 24 24 func NewRewardServiceServer( 25 25 users store.UserRepository, 26 26 sessions store.SessionRepository, 27 - bhCatalog *masterdata.BigHuntCatalog, 28 - granter *store.PossessionGranter, 27 + holder *runtime.Holder, 29 28 ) *RewardServiceServer { 30 - return &RewardServiceServer{users: users, sessions: sessions, bhCatalog: bhCatalog, granter: granter} 29 + return &RewardServiceServer{users: users, sessions: sessions, holder: holder} 31 30 } 32 31 33 32 func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveBigHuntRewardResponse, error) { 34 33 log.Printf("[RewardService] ReceiveBigHuntReward") 35 34 35 + cat := s.holder.Get() 36 + bhCatalog := cat.BigHunt 37 + granter := cat.QuestHandler.Granter 36 38 userId := CurrentUserId(ctx, s.users, s.sessions) 37 39 nowMillis := gametime.NowMillis() 38 40 weeklyVersion := gametime.WeeklyVersion(nowMillis) ··· 45 47 ws := user.BigHuntWeeklyStatuses[weeklyVersion] 46 48 isReceived = ws.IsReceivedWeeklyReward 47 49 48 - for _, boss := range s.bhCatalog.BossByBossId { 50 + for _, boss := range bhCatalog.BossByBossId { 49 51 key := store.BigHuntWeeklyScoreKey{ 50 52 BigHuntWeeklyVersion: weeklyVersion, 51 53 AttributeType: boss.AttributeType, 52 54 } 53 55 wms := user.BigHuntWeeklyMaxScores[key] 54 - gradeIcon := s.bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore) 56 + gradeIcon := bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore) 55 57 weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{ 56 58 AttributeType: boss.AttributeType, 57 59 BeforeMaxScore: wms.MaxScore, ··· 64 66 } 65 67 66 68 if !isReceived { 67 - for _, boss := range s.bhCatalog.BossByBossId { 69 + for _, boss := range bhCatalog.BossByBossId { 68 70 rewardKey := masterdata.BigHuntWeeklyRewardKey{ 69 71 ScheduleId: 1, 70 72 AttributeType: boss.AttributeType, 71 73 } 72 - rewardGroupId := s.bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) 74 + rewardGroupId := bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) 73 75 if rewardGroupId == 0 { 74 76 continue 75 77 } ··· 80 82 } 81 83 maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore 82 84 83 - items := s.bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore) 85 + items := bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore) 84 86 for _, item := range items { 85 - s.granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis) 87 + granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis) 86 88 weeklyRewards = append(weeklyRewards, &pb.BigHuntReward{ 87 89 PossessionType: item.PossessionType, 88 90 PossessionId: item.PossessionId,
+29 -22
server/internal/service/shop.go
··· 9 9 "lunar-tear/server/internal/gametime" 10 10 "lunar-tear/server/internal/masterdata" 11 11 "lunar-tear/server/internal/model" 12 + "lunar-tear/server/internal/runtime" 12 13 "lunar-tear/server/internal/store" 13 14 14 15 "google.golang.org/protobuf/types/known/emptypb" ··· 18 19 pb.UnimplementedShopServiceServer 19 20 users store.UserRepository 20 21 sessions store.SessionRepository 21 - catalog *masterdata.ShopCatalog 22 - granter *store.PossessionGranter 22 + holder *runtime.Holder 23 23 } 24 24 25 - func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ShopCatalog, granter *store.PossessionGranter) *ShopServiceServer { 26 - return &ShopServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter} 25 + func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ShopServiceServer { 26 + return &ShopServiceServer{users: users, sessions: sessions, holder: holder} 27 27 } 28 28 29 29 func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) { 30 30 log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems) 31 31 32 + cat := s.holder.Get() 33 + catalog := cat.Shop 34 + granter := cat.QuestHandler.Granter 32 35 userId := CurrentUserId(ctx, s.users, s.sessions) 33 36 nowMillis := gametime.NowMillis() 34 37 35 38 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { 36 39 for shopItemId, qty := range req.ShopItems { 37 - item, ok := s.catalog.Items[shopItemId] 40 + item, ok := catalog.Items[shopItemId] 38 41 if !ok { 39 42 log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId) 40 43 continue ··· 46 49 continue 47 50 } 48 51 49 - for _, content := range s.catalog.Contents[shopItemId] { 50 - s.granter.GrantFull(user, 52 + for _, content := range catalog.Contents[shopItemId] { 53 + granter.GrantFull(user, 51 54 model.PossessionType(content.PossessionType), 52 55 content.PossessionId, 53 56 content.Count*qty, ··· 55 58 ) 56 59 } 57 60 58 - s.applyContentEffects(user, shopItemId, qty, nowMillis) 61 + applyShopContentEffects(catalog, user, shopItemId, qty, nowMillis) 59 62 60 63 si := user.ShopItems[shopItemId] 61 64 si.ShopItemId = shopItemId ··· 76 79 func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) { 77 80 log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed) 78 81 82 + catalog := s.holder.Get().Shop 79 83 userId := CurrentUserId(ctx, s.users, s.sessions) 80 84 nowMillis := gametime.NowMillis() 81 85 82 86 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { 83 - if len(user.ShopReplaceableLineup) == 0 && len(s.catalog.ItemShopPool) > 0 { 84 - for i, itemId := range s.catalog.ItemShopPool { 87 + if len(user.ShopReplaceableLineup) == 0 && len(catalog.ItemShopPool) > 0 { 88 + for i, itemId := range catalog.ItemShopPool { 85 89 slot := int32(i + 1) 86 90 user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{ 87 91 SlotNumber: slot, ··· 93 97 if req.IsGemUsed { 94 98 user.ShopReplaceable.LineupUpdateCount++ 95 99 user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis 96 - for _, itemId := range s.catalog.ItemShopPool { 100 + for _, itemId := range catalog.ItemShopPool { 97 101 if si, ok := user.ShopItems[itemId]; ok { 98 102 si.BoughtCount = 0 99 103 si.LatestVersion = nowMillis ··· 120 124 log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s", 121 125 req.ShopId, req.ShopItemId, req.ProductId) 122 126 127 + cat := s.holder.Get() 128 + catalog := cat.Shop 129 + granter := cat.QuestHandler.Granter 123 130 userId := CurrentUserId(ctx, s.users, s.sessions) 124 131 nowMillis := gametime.NowMillis() 125 132 126 133 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { 127 - item, ok := s.catalog.Items[req.ShopItemId] 134 + item, ok := catalog.Items[req.ShopItemId] 128 135 if !ok { 129 136 log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId) 130 137 return ··· 134 141 log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err) 135 142 } 136 143 137 - for _, content := range s.catalog.Contents[req.ShopItemId] { 138 - s.granter.GrantFull(user, 144 + for _, content := range catalog.Contents[req.ShopItemId] { 145 + granter.GrantFull(user, 139 146 model.PossessionType(content.PossessionType), 140 147 content.PossessionId, 141 148 content.Count, ··· 143 150 ) 144 151 } 145 152 146 - s.applyContentEffects(user, req.ShopItemId, 1, nowMillis) 153 + applyShopContentEffects(catalog, user, req.ShopItemId, 1, nowMillis) 147 154 148 155 si := user.ShopItems[req.ShopItemId] 149 156 si.ShopItemId = req.ShopItemId 150 157 si.BoughtCount++ 151 158 if item.ShopItemLimitedStockId > 0 { 152 - if maxCount, ok := s.catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount { 159 + if maxCount, ok := catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount { 153 160 si.BoughtCount = 0 154 161 } 155 162 } ··· 182 189 }, nil 183 190 } 184 191 185 - func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemId, qty int32, nowMillis int64) { 186 - for _, effect := range s.catalog.Effects[shopItemId] { 192 + func applyShopContentEffects(catalog *masterdata.ShopCatalog, user *store.UserState, shopItemId, qty int32, nowMillis int64) { 193 + for _, effect := range catalog.Effects[shopItemId] { 187 194 switch effect.EffectTargetType { 188 195 case model.EffectTargetStaminaRecovery: 189 - maxMillis := s.catalog.MaxStaminaMillis[user.Status.Level] 190 - millis := s.resolveEffectMillis(effect.EffectValueType, effect.EffectValue, user.Status.Level) 196 + maxMillis := catalog.MaxStaminaMillis[user.Status.Level] 197 + millis := resolveShopEffectMillis(catalog, effect.EffectValueType, effect.EffectValue, user.Status.Level) 191 198 store.RecoverStamina(user, millis*qty, maxMillis, nowMillis) 192 199 default: 193 200 log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType) ··· 195 202 } 196 203 } 197 204 198 - func (s *ShopServiceServer) resolveEffectMillis(effectValueType, effectValue, userLevel int32) int32 { 205 + func resolveShopEffectMillis(catalog *masterdata.ShopCatalog, effectValueType, effectValue, userLevel int32) int32 { 199 206 switch effectValueType { 200 207 case model.EffectValueFixed: 201 208 return effectValue 202 209 case model.EffectValuePermil: 203 - maxMillis := s.catalog.MaxStaminaMillis[userLevel] 210 + maxMillis := catalog.MaxStaminaMillis[userLevel] 204 211 return effectValue * maxMillis / 1000 205 212 default: 206 213 return 0
+6 -4
server/internal/service/tutorial.go
··· 8 8 "lunar-tear/server/internal/gametime" 9 9 "lunar-tear/server/internal/model" 10 10 "lunar-tear/server/internal/questflow" 11 + "lunar-tear/server/internal/runtime" 11 12 "lunar-tear/server/internal/store" 12 13 ) 13 14 ··· 15 16 pb.UnimplementedTutorialServiceServer 16 17 users store.UserRepository 17 18 sessions store.SessionRepository 18 - engine *questflow.QuestHandler 19 + holder *runtime.Holder 19 20 } 20 21 21 - func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *TutorialServiceServer { 22 - return &TutorialServiceServer{users: users, sessions: sessions, engine: engine} 22 + func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *TutorialServiceServer { 23 + return &TutorialServiceServer{users: users, sessions: sessions, holder: holder} 23 24 } 24 25 25 26 func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb.SetTutorialProgressRequest) (*pb.SetTutorialProgressResponse, error) { 26 27 log.Printf("[TutorialService] SetTutorialProgress: type=%d phase=%d choice=%d", req.TutorialType, req.ProgressPhase, req.ChoiceId) 27 28 userId := CurrentUserId(ctx, s.users, s.sessions) 28 29 nowMillis := gametime.NowMillis() 30 + engine := s.holder.Get().QuestHandler 29 31 var grants []questflow.RewardGrant 30 32 s.users.UpdateUser(userId, func(user *store.UserState) { 31 33 existing, exists := user.Tutorials[req.TutorialType] ··· 36 38 ChoiceId: req.ChoiceId, 37 39 } 38 40 } 39 - grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis) 41 + grants = engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis) 40 42 if req.TutorialType == int32(model.TutorialTypeMenuFirst) && req.ProgressPhase == 20 { 41 43 store.EnsureDefaultDeck(user, nowMillis) 42 44 }
+91 -64
server/internal/service/weapon.go
··· 10 10 "lunar-tear/server/internal/gameutil" 11 11 "lunar-tear/server/internal/masterdata" 12 12 "lunar-tear/server/internal/model" 13 + "lunar-tear/server/internal/runtime" 13 14 "lunar-tear/server/internal/store" 14 15 ) 15 16 ··· 17 18 pb.UnimplementedWeaponServiceServer 18 19 users store.UserRepository 19 20 sessions store.SessionRepository 20 - catalog *masterdata.WeaponCatalog 21 - config *masterdata.GameConfig 21 + holder *runtime.Holder 22 22 } 23 23 24 - func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.WeaponCatalog, config *masterdata.GameConfig) *WeaponServiceServer { 25 - return &WeaponServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} 24 + func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *WeaponServiceServer { 25 + return &WeaponServiceServer{users: users, sessions: sessions, holder: holder} 26 26 } 27 27 28 28 func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectRequest) (*pb.ProtectResponse, error) { ··· 72 72 func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.EnhanceByMaterialRequest) (*pb.EnhanceByMaterialResponse, error) { 73 73 log.Printf("[WeaponService] EnhanceByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials) 74 74 75 + cat := s.holder.Get() 76 + catalog := cat.Weapon 77 + config := cat.GameConfig 75 78 userId := CurrentUserId(ctx, s.users, s.sessions) 76 79 nowMillis := gametime.NowMillis() 77 80 ··· 82 85 return 83 86 } 84 87 85 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 88 + wm, ok := catalog.Weapons[weapon.WeaponId] 86 89 if !ok { 87 90 log.Printf("[WeaponService] EnhanceByMaterial: weapon master id=%d not found", weapon.WeaponId) 88 91 return ··· 91 94 totalExp := int32(0) 92 95 totalMaterialCount := int32(0) 93 96 for materialId, count := range req.Materials { 94 - mat, ok := s.catalog.Materials[materialId] 97 + mat, ok := catalog.Materials[materialId] 95 98 if !ok { 96 99 log.Printf("[WeaponService] EnhanceByMaterial: material id=%d not found, skipping", materialId) 97 100 continue ··· 107 110 108 111 expPerUnit := mat.EffectValue 109 112 if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType { 110 - expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 113 + expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000 111 114 } 112 115 totalExp += expPerUnit * count 113 116 } 114 117 115 - if costFunc, ok := s.catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { 118 + if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { 116 119 goldCost := costFunc.Evaluate(totalMaterialCount) 117 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 120 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 118 121 log.Printf("[WeaponService] EnhanceByMaterial: gold cost=%d (materials=%d)", goldCost, totalMaterialCount) 119 122 } 120 123 121 124 weapon.Exp += totalExp 122 - if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 125 + if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 123 126 weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) 124 127 } 125 128 ··· 127 130 user.Weapons[req.UserWeaponUuid] = weapon 128 131 log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) 129 132 130 - s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) 133 + checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis) 131 134 }) 132 135 if err != nil { 133 136 return nil, fmt.Errorf("weapon enhance by material: %w", err) ··· 142 145 func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) { 143 146 log.Printf("[WeaponService] Sell: uuids=%v", req.UserWeaponUuid) 144 147 148 + cat := s.holder.Get() 149 + catalog := cat.Weapon 150 + config := cat.GameConfig 145 151 userId := CurrentUserId(ctx, s.users, s.sessions) 146 152 147 153 _, err := s.users.UpdateUser(userId, func(user *store.UserState) { ··· 153 159 continue 154 160 } 155 161 156 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 162 + wm, ok := catalog.Weapons[weapon.WeaponId] 157 163 if !ok { 158 164 log.Printf("[WeaponService] Sell: weapon master id=%d not found, skipping", weapon.WeaponId) 159 165 continue 160 166 } 161 167 162 - if sellFunc, ok := s.catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 168 + if sellFunc, ok := catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 163 169 totalGold += sellFunc.Evaluate(weapon.Level) 164 170 } 165 171 166 - if medals, ok := s.catalog.MedalsByWeaponId[weapon.WeaponId]; ok { 172 + if medals, ok := catalog.MedalsByWeaponId[weapon.WeaponId]; ok { 167 173 for itemId, count := range medals { 168 174 user.ConsumableItems[itemId] += count 169 175 } ··· 176 182 } 177 183 178 184 if totalGold > 0 { 179 - user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold 185 + user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold 180 186 log.Printf("[WeaponService] Sell: granted %d gold", totalGold) 181 187 } 182 188 }) ··· 190 196 func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) (*pb.EvolveResponse, error) { 191 197 log.Printf("[WeaponService] Evolve: uuid=%s", req.UserWeaponUuid) 192 198 199 + cat := s.holder.Get() 200 + catalog := cat.Weapon 201 + config := cat.GameConfig 193 202 userId := CurrentUserId(ctx, s.users, s.sessions) 194 203 nowMillis := gametime.NowMillis() 195 204 ··· 200 209 return 201 210 } 202 211 203 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 212 + wm, ok := catalog.Weapons[weapon.WeaponId] 204 213 if !ok { 205 214 log.Printf("[WeaponService] Evolve: weapon master id=%d not found", weapon.WeaponId) 206 215 return 207 216 } 208 217 209 - evolvedId, ok := s.catalog.EvolutionNextWeaponId[weapon.WeaponId] 218 + evolvedId, ok := catalog.EvolutionNextWeaponId[weapon.WeaponId] 210 219 if !ok { 211 220 log.Printf("[WeaponService] Evolve: no evolution for weaponId=%d", weapon.WeaponId) 212 221 return 213 222 } 214 223 215 224 totalMaterialCount := int32(0) 216 - mats := s.catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId] 225 + mats := catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId] 217 226 for _, mat := range mats { 218 227 cur := user.Materials[mat.MaterialId] 219 228 cost := mat.Count ··· 225 234 totalMaterialCount += cost 226 235 } 227 236 228 - if costFunc, ok := s.catalog.EvolutionCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { 237 + if costFunc, ok := catalog.EvolutionCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { 229 238 goldCost := costFunc.Evaluate(totalMaterialCount) 230 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 239 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 231 240 log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost) 232 241 } 233 242 ··· 235 244 weapon.LatestVersion = nowMillis 236 245 user.Weapons[req.UserWeaponUuid] = weapon 237 246 238 - evolvedMaster, ok := s.catalog.Weapons[evolvedId] 247 + evolvedMaster, ok := catalog.Weapons[evolvedId] 239 248 if ok { 240 - if slots, ok := s.catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok { 249 + if slots, ok := catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok { 241 250 abilities := make([]store.WeaponAbilityState, len(slots)) 242 251 for i, slot := range slots { 243 252 abilities[i] = store.WeaponAbilityState{ ··· 252 261 253 262 log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId) 254 263 255 - s.checkWeaponStoryUnlocks(user, evolvedId, weapon.Level, nowMillis) 264 + checkWeaponStoryUnlocks(catalog, user, evolvedId, weapon.Level, nowMillis) 256 265 }) 257 266 if err != nil { 258 267 return nil, fmt.Errorf("weapon evolve: %w", err) ··· 264 273 func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceSkillRequest) (*pb.EnhanceSkillResponse, error) { 265 274 log.Printf("[WeaponService] EnhanceSkill: uuid=%s skillId=%d addLevel=%d", req.UserWeaponUuid, req.SkillId, req.AddLevelCount) 266 275 276 + cat := s.holder.Get() 277 + catalog := cat.Weapon 278 + config := cat.GameConfig 267 279 userId := CurrentUserId(ctx, s.users, s.sessions) 268 280 nowMillis := gametime.NowMillis() 269 281 ··· 274 286 return 275 287 } 276 288 277 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 289 + wm, ok := catalog.Weapons[weapon.WeaponId] 278 290 if !ok { 279 291 log.Printf("[WeaponService] EnhanceSkill: weapon master id=%d not found", weapon.WeaponId) 280 292 return 281 293 } 282 294 283 - groupRows := s.catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId] 295 + groupRows := catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId] 284 296 var skillGroup *masterdata.EntityMWeaponSkillGroup 285 297 for i := range groupRows { 286 298 if groupRows[i].SkillId == req.SkillId { ··· 306 318 return 307 319 } 308 320 309 - maxLevelFunc, ok := s.catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] 321 + maxLevelFunc, ok := catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] 310 322 if !ok { 311 323 log.Printf("[WeaponService] EnhanceSkill: no max skill level func for enhanceId=%d", wm.WeaponSpecificEnhanceId) 312 324 return ··· 326 338 enhanceMatId := skillGroup.WeaponSkillEnhancementMaterialId 327 339 for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { 328 340 key := [2]int32{enhanceMatId, lvl} 329 - mats := s.catalog.SkillEnhanceMats[key] 341 + mats := catalog.SkillEnhanceMats[key] 330 342 for _, mat := range mats { 331 343 cur := user.Materials[mat.MaterialId] 332 344 cost := mat.Count ··· 337 349 user.Materials[mat.MaterialId] = cur - cost 338 350 } 339 351 340 - if costFunc, ok := s.catalog.SkillCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 352 + if costFunc, ok := catalog.SkillCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 341 353 goldCost := costFunc.Evaluate(lvl + 1) 342 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 354 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 343 355 } 344 356 } 345 357 ··· 360 372 func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.EnhanceAbilityRequest) (*pb.EnhanceAbilityResponse, error) { 361 373 log.Printf("[WeaponService] EnhanceAbility: uuid=%s abilityId=%d addLevel=%d", req.UserWeaponUuid, req.AbilityId, req.AddLevelCount) 362 374 375 + cat := s.holder.Get() 376 + catalog := cat.Weapon 377 + config := cat.GameConfig 363 378 userId := CurrentUserId(ctx, s.users, s.sessions) 364 379 nowMillis := gametime.NowMillis() 365 380 ··· 370 385 return 371 386 } 372 387 373 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 388 + wm, ok := catalog.Weapons[weapon.WeaponId] 374 389 if !ok { 375 390 log.Printf("[WeaponService] EnhanceAbility: weapon master id=%d not found", weapon.WeaponId) 376 391 return 377 392 } 378 393 379 - groupRows := s.catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId] 394 + groupRows := catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId] 380 395 var abilityGroup *masterdata.EntityMWeaponAbilityGroup 381 396 for i := range groupRows { 382 397 if groupRows[i].AbilityId == req.AbilityId { ··· 402 417 return 403 418 } 404 419 405 - maxLevelFunc, ok := s.catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] 420 + maxLevelFunc, ok := catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] 406 421 if !ok { 407 422 log.Printf("[WeaponService] EnhanceAbility: no max ability level func for enhanceId=%d", wm.WeaponSpecificEnhanceId) 408 423 return ··· 422 437 enhanceMatId := abilityGroup.WeaponAbilityEnhancementMaterialId 423 438 for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { 424 439 key := [2]int32{enhanceMatId, lvl} 425 - mats := s.catalog.AbilityEnhanceMats[key] 440 + mats := catalog.AbilityEnhanceMats[key] 426 441 for _, mat := range mats { 427 442 cur := user.Materials[mat.MaterialId] 428 443 cost := mat.Count ··· 433 448 user.Materials[mat.MaterialId] = cur - cost 434 449 } 435 450 436 - if costFunc, ok := s.catalog.AbilityCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 451 + if costFunc, ok := catalog.AbilityCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 437 452 goldCost := costFunc.Evaluate(lvl + 1) 438 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 453 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 439 454 } 440 455 } 441 456 ··· 456 471 func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.LimitBreakByMaterialRequest) (*pb.LimitBreakByMaterialResponse, error) { 457 472 log.Printf("[WeaponService] LimitBreakByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials) 458 473 474 + cat := s.holder.Get() 475 + catalog := cat.Weapon 476 + config := cat.GameConfig 459 477 userId := CurrentUserId(ctx, s.users, s.sessions) 460 478 nowMillis := gametime.NowMillis() 461 479 ··· 466 484 return 467 485 } 468 486 469 - if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount { 487 + if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount { 470 488 log.Printf("[WeaponService] LimitBreakByMaterial: already at max limit break %d", weapon.LimitBreakCount) 471 489 return 472 490 } 473 491 474 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 492 + wm, ok := catalog.Weapons[weapon.WeaponId] 475 493 if !ok { 476 494 log.Printf("[WeaponService] LimitBreakByMaterial: weapon master id=%d not found", weapon.WeaponId) 477 495 return 478 496 } 479 497 480 - remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount 498 + remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount 481 499 482 500 totalMaterialCount := int32(0) 483 501 for materialId, count := range req.Materials { ··· 496 514 totalMaterialCount += count 497 515 } 498 516 499 - if costFunc, ok := s.catalog.LimitBreakCostByMaterialByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { 517 + if costFunc, ok := catalog.LimitBreakCostByMaterialByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { 500 518 goldCost := costFunc.Evaluate(totalMaterialCount) 501 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 519 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 502 520 log.Printf("[WeaponService] LimitBreakByMaterial: gold cost=%d", goldCost) 503 521 } 504 522 ··· 525 543 func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.LimitBreakByWeaponRequest) (*pb.LimitBreakByWeaponResponse, error) { 526 544 log.Printf("[WeaponService] LimitBreakByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids) 527 545 546 + cat := s.holder.Get() 547 + catalog := cat.Weapon 548 + config := cat.GameConfig 528 549 userId := CurrentUserId(ctx, s.users, s.sessions) 529 550 nowMillis := gametime.NowMillis() 530 551 ··· 535 556 return 536 557 } 537 558 538 - if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount { 559 + if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount { 539 560 log.Printf("[WeaponService] LimitBreakByWeapon: already at max limit break %d", weapon.LimitBreakCount) 540 561 return 541 562 } 542 563 543 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 564 + wm, ok := catalog.Weapons[weapon.WeaponId] 544 565 if !ok { 545 566 log.Printf("[WeaponService] LimitBreakByWeapon: weapon master id=%d not found", weapon.WeaponId) 546 567 return 547 568 } 548 569 549 - remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount 570 + remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount 550 571 551 572 consumedCount := int32(0) 552 573 for _, uuid := range req.MaterialUserWeaponUuids { ··· 560 581 continue 561 582 } 562 583 563 - if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { 584 + if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { 564 585 for itemId, count := range medals { 565 586 user.ConsumableItems[itemId] += count 566 587 } ··· 573 594 consumedCount++ 574 595 } 575 596 576 - if costFunc, ok := s.catalog.LimitBreakCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 { 597 + if costFunc, ok := catalog.LimitBreakCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 { 577 598 goldCost := costFunc.Evaluate(consumedCount) 578 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 599 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 579 600 log.Printf("[WeaponService] LimitBreakByWeapon: gold cost=%d", goldCost) 580 601 } 581 602 ··· 602 623 func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.EnhanceByWeaponRequest) (*pb.EnhanceByWeaponResponse, error) { 603 624 log.Printf("[WeaponService] EnhanceByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids) 604 625 626 + cat := s.holder.Get() 627 + catalog := cat.Weapon 628 + config := cat.GameConfig 605 629 userId := CurrentUserId(ctx, s.users, s.sessions) 606 630 nowMillis := gametime.NowMillis() 607 631 ··· 612 636 return 613 637 } 614 638 615 - wm, ok := s.catalog.Weapons[weapon.WeaponId] 639 + wm, ok := catalog.Weapons[weapon.WeaponId] 616 640 if !ok { 617 641 log.Printf("[WeaponService] EnhanceByWeapon: weapon master id=%d not found", weapon.WeaponId) 618 642 return ··· 627 651 continue 628 652 } 629 653 630 - matMaster, ok := s.catalog.Weapons[matWeapon.WeaponId] 654 + matMaster, ok := catalog.Weapons[matWeapon.WeaponId] 631 655 if !ok { 632 656 log.Printf("[WeaponService] EnhanceByWeapon: material weapon master id=%d not found, skipping", matWeapon.WeaponId) 633 657 continue 634 658 } 635 659 636 - baseExp := s.catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId] 660 + baseExp := catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId] 637 661 if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType { 638 - baseExp = baseExp * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 662 + baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000 639 663 } 640 664 totalExp += baseExp 641 665 642 - if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { 666 + if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { 643 667 for itemId, count := range medals { 644 668 user.ConsumableItems[itemId] += count 645 669 } ··· 652 676 consumedCount++ 653 677 } 654 678 655 - if costFunc, ok := s.catalog.EnhanceCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 { 679 + if costFunc, ok := catalog.EnhanceCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 { 656 680 goldCost := costFunc.Evaluate(consumedCount) 657 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost 681 + user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost 658 682 log.Printf("[WeaponService] EnhanceByWeapon: gold cost=%d (weapons=%d)", goldCost, consumedCount) 659 683 } 660 684 661 685 weapon.Exp += totalExp 662 - if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 686 + if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 663 687 weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) 664 688 } 665 689 ··· 667 691 user.Weapons[req.UserWeaponUuid] = weapon 668 692 log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) 669 693 670 - s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) 694 + checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis) 671 695 }) 672 696 if err != nil { 673 697 return nil, fmt.Errorf("weapon enhance by weapon: %w", err) ··· 679 703 }, nil 680 704 } 681 705 682 - func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, weaponId, level int32, nowMillis int64) { 683 - wm, ok := s.catalog.Weapons[weaponId] 706 + func checkWeaponStoryUnlocks(catalog *masterdata.WeaponCatalog, user *store.UserState, weaponId, level int32, nowMillis int64) { 707 + wm, ok := catalog.Weapons[weaponId] 684 708 if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 { 685 709 return 686 710 } 687 - evoOrder, hasEvo := s.catalog.EvolutionOrder[weaponId] 688 - conditions := s.catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId] 711 + evoOrder, hasEvo := catalog.EvolutionOrder[weaponId] 712 + conditions := catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId] 689 713 690 714 for _, cond := range conditions { 691 715 switch model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) { ··· 696 720 store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) 697 721 } 698 722 case model.WeaponStoryReleaseConditionTypeReachInitialMaxLevel: 699 - if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 723 + if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 700 724 if level >= maxFunc.Evaluate(0) { 701 725 store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) 702 726 } 703 727 } 704 728 case model.WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel: 705 729 if hasEvo && evoOrder >= 1 { 706 - if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 730 + if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { 707 731 if level >= maxFunc.Evaluate(0) { 708 732 store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) 709 733 } ··· 720 744 func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) { 721 745 log.Printf("[WeaponService] Awaken: uuid=%s", req.UserWeaponUuid) 722 746 747 + cat := s.holder.Get() 748 + catalog := cat.Weapon 749 + config := cat.GameConfig 723 750 userId := CurrentUserId(ctx, s.users, s.sessions) 724 751 nowMillis := gametime.NowMillis() 725 752 ··· 730 757 return 731 758 } 732 759 733 - awakenRow, ok := s.catalog.AwakenByWeaponId[weapon.WeaponId] 760 + awakenRow, ok := catalog.AwakenByWeaponId[weapon.WeaponId] 734 761 if !ok { 735 762 log.Printf("[WeaponService] Awaken: no awaken data for weaponId=%d", weapon.WeaponId) 736 763 return ··· 741 768 return 742 769 } 743 770 744 - mats := s.catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId] 771 + mats := catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId] 745 772 for _, mat := range mats { 746 773 cur := user.Materials[mat.MaterialId] 747 774 cost := mat.Count ··· 753 780 } 754 781 755 782 if awakenRow.ConsumeGold > 0 { 756 - user.ConsumableItems[s.config.ConsumableItemIdForGold] -= awakenRow.ConsumeGold 783 + user.ConsumableItems[config.ConsumableItemIdForGold] -= awakenRow.ConsumeGold 757 784 log.Printf("[WeaponService] Awaken: gold cost=%d", awakenRow.ConsumeGold) 758 785 } 759 786