Add admin API for content reload

This commit is contained in:
Ilya Groshev
2026-04-28 21:22:28 +03:00
parent 9be0df4c30
commit 3fe564cb1d
36 changed files with 992 additions and 638 deletions
+39 -3
View File
@@ -46,6 +46,7 @@ go run ./cmd/wizard --grpc-port 9003 --cdn-port 9080
| `--grpc-port` | `8003` | gRPC server port | | `--grpc-port` | `8003` | gRPC server port |
| `--cdn-port` | `8080` | CDN server port | | `--cdn-port` | `8080` | CDN server port |
| `--auth-port` | `3000` | Auth server port | | `--auth-port` | `3000` | Auth server port |
| `--admin-port` | `0` | Admin webhook port (`0` = disabled). Bound on `127.0.0.1`; only takes effect when `LUNAR_ADMIN_TOKEN` is set in the env. |
Custom ports are saved to `.wizard.json` alongside your other settings. On the next run the saved ports are reused automatically — no need to pass the flags again. If you later pass different port flags, the wizard warns you that the ports changed and asks for confirmation before continuing. Custom ports are saved to `.wizard.json` alongside your other settings. On the next run the saved ports are reused automatically — no need to pass the flags again. If you later pass different port flags, the wizard warns you that the ports changed and asks for confirmation before continuing.
@@ -172,6 +173,7 @@ make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000"
| `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr | | `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr |
| `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear | | `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear |
| `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear | | `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear |
| `--admin.listen` | *(empty)* | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. |
| `--no-color` | `false` | disable colored output | | `--no-color` | `false` | disable colored output |
### Ports ### Ports
@@ -180,16 +182,39 @@ make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000"
| -------- | ---- | ------------- | ----------------------------------------------------------- | | -------- | ---- | ------------- | ----------------------------------------------------------- |
| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) | | gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) |
| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages | | HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages |
| HTTP | 8082 | `lunar-tear` | admin webhook (`/api/admin/master-data/reload`); loopback by default, only binds when `LUNAR_ADMIN_TOKEN` is set |
| HTTP | 3000 | `auth-server` | account registration and login |
### Game Server Flags (`lunar-tear`) ### Game Server Flags (`lunar-tear`)
| Flag | Default | Description | | Flag | Default | Description |
| --------------- | ----------------- | ---------------------------------------------------- | | ---------------- | ----------------- | ---------------------------------------------------- |
| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | | `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) |
| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | | `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients |
| `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | | `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) |
| `--db` | `db/game.db` | SQLite database path | | `--db` | `db/game.db` | SQLite database path |
| `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | | `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) |
| `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. |
### Live Master Data Reload
The game server reads its master data from `assets/release/20240404193219.bin.e` at startup. To swap in updated content **without restarting** the server:
1. Replace `assets/release/20240404193219.bin.e` on disk with your edited copy.
2. POST to the admin webhook with a Bearer token matching `LUNAR_ADMIN_TOKEN`:
```bash
curl -X POST -H "Authorization: Bearer ${LUNAR_ADMIN_TOKEN}" \
http://127.0.0.1:8082/api/admin/master-data/reload
```
The server re-reads the file, atomically swaps every in-memory catalog and derived handler, and bumps the file's mtime. The mtime is folded into `GetLatestMasterDataVersion`, so connected clients see a new version string and re-download the file from the CDN on their next poll.
Security defaults are fail-closed:
- `LUNAR_ADMIN_TOKEN` **must** be set in the environment, or the webhook listener never binds.
- `--admin-listen` defaults to `127.0.0.1:8082` (loopback only). Bind to `0.0.0.0` only if you intend to expose it.
- Authentication uses constant-time Bearer-token comparison.
### CDN Flags (`octo-cdn`) ### CDN Flags (`octo-cdn`)
@@ -214,11 +239,22 @@ Each service has its own image and can be deployed independently:
| Service | Image | Default Port | Notes | | Service | Image | Default Port | Notes |
| -------- | --------------------------- | ------------ | ------------------------------ | | -------- | --------------------------- | ------------ | ------------------------------ |
| `server` | `kretts/lunar-tear:latest` | 8003 | gRPC game server | | `server` | `kretts/lunar-tear:latest` | 8003, 8082 | gRPC game server + admin webhook |
| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN | | `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN |
| `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login | | `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login |
The game server is configured via environment variables in the compose file: `LUNAR_LISTEN` (bind address), `LUNAR_PUBLIC_ADDR` (client-facing address), `LUNAR_OCTO_URL`, and `LUNAR_AUTH_URL`. Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. The game server is configured via environment variables in the compose file:
| Env var | Description |
| --------------------- | -------------------------------------------------------------------------------------------- |
| `LUNAR_LISTEN` | gRPC bind address |
| `LUNAR_PUBLIC_ADDR` | Client-facing address advertised to the game |
| `LUNAR_OCTO_URL` | CDN base URL the client uses for assets |
| `LUNAR_AUTH_URL` | Auth server base URL (optional) |
| `LUNAR_ADMIN_LISTEN` | Admin webhook bind address inside the container (compose default: `0.0.0.0:8082`) |
| `LUNAR_ADMIN_TOKEN` | Bearer token for the admin webhook. **The webhook does not bind unless this is set.** |
Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. The admin webhook is published to `127.0.0.1:8082` on the host so the master-data reload endpoint stays loopback-only by default; set `LUNAR_ADMIN_TOKEN` (e.g. via a `.env` file) before bringing the stack up.
### Makefile Targets ### Makefile Targets
+22 -5
View File
@@ -93,6 +93,10 @@ func main() {
grpcOctoURL := flag.String("grpc.octo-url", "", "Octo CDN base URL passed to lunar-tear (default: derived from cdn.public-addr)") grpcOctoURL := flag.String("grpc.octo-url", "", "Octo CDN base URL passed to lunar-tear (default: derived from cdn.public-addr)")
grpcAuthURL := flag.String("grpc.auth-url", "", "auth server base URL passed to lunar-tear (default: derived from auth.listen)") grpcAuthURL := flag.String("grpc.auth-url", "", "auth server base URL passed to lunar-tear (default: derived from auth.listen)")
// admin webhook is opt-in; empty leaves lunar-tear's own default in place
// (the listener still only binds if LUNAR_ADMIN_TOKEN is set in the env).
adminListen := flag.String("admin.listen", "", "lunar-tear admin webhook listen address (host:port). Empty = leave default; webhook only binds when LUNAR_ADMIN_TOKEN is set in the env.")
noColor := flag.Bool("no-color", false, "disable colored output") noColor := flag.Bool("no-color", false, "disable colored output")
flag.Parse() flag.Parse()
@@ -139,11 +143,7 @@ func main() {
label: "grpc", label: "grpc",
color: colorYellow, color: colorYellow,
cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext), cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext),
"--listen", *grpcListen, grpcArgs(*grpcListen, *grpcPublicAddr, *grpcDB, *grpcOctoURL, *grpcAuthURL, *adminListen)...,
"--public-addr", *grpcPublicAddr,
"--db", *grpcDB,
"--octo-url", *grpcOctoURL,
"--auth-url", *grpcAuthURL,
), ),
}, },
} }
@@ -200,3 +200,20 @@ func prefixLines(wg *sync.WaitGroup, prefix string, r io.Reader) {
fmt.Printf("%s%s\n", prefix, scanner.Text()) fmt.Printf("%s%s\n", prefix, scanner.Text())
} }
} }
// grpcArgs assembles the argv for the lunar-tear subprocess. The admin flag
// is appended only when --admin.listen was supplied so we don't override
// lunar-tear's own default when the operator hasn't opted in.
func grpcArgs(listen, publicAddr, db, octoURL, authURL, adminListen string) []string {
args := []string{
"--listen", listen,
"--public-addr", publicAddr,
"--db", db,
"--octo-url", octoURL,
"--auth-url", authURL,
}
if adminListen != "" {
args = append(args, "--admin-listen", adminListen)
}
return args
}
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"crypto/subtle"
"log"
"net/http"
"os"
"lunar-tear/server/internal/runtime"
)
// startAdmin spins up the admin webhook used by external content tools to
// trigger an in-place re-read of assets/release/20240404193219.bin.e.
//
// Authentication: Bearer token via the LUNAR_ADMIN_TOKEN environment variable.
// If LUNAR_ADMIN_TOKEN is unset or empty the listener does not bind at all
// (fail closed), so a fresh deploy never exposes an unauthenticated endpoint.
//
// The default --admin-listen is 127.0.0.1:8082 so the webhook is only
// reachable via loopback unless the operator opts in by binding to 0.0.0.0.
func startAdmin(listen string, holder *runtime.Holder) {
token := os.Getenv("LUNAR_ADMIN_TOKEN")
if token == "" {
log.Println("[admin] disabled (no LUNAR_ADMIN_TOKEN set)")
return
}
expected := []byte("Bearer " + token)
mux := http.NewServeMux()
mux.HandleFunc("/api/admin/master-data/reload", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
got := []byte(r.Header.Get("Authorization"))
if len(got) != len(expected) || subtle.ConstantTimeCompare(got, expected) != 1 {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if err := holder.Reload(); err != nil {
log.Printf("[admin] master-data reload failed: %v", err)
http.Error(w, "master-data reload failed", http.StatusInternalServerError)
return
}
log.Printf("[admin] master-data reloaded by %s", r.RemoteAddr)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
})
log.Printf("[admin] webhook listener on %s (token-gated)", listen)
go func() {
if err := http.ListenAndServe(listen, mux); err != nil {
log.Printf("[admin] webhook listener failed: %v", err)
}
}()
}
+26 -94
View File
@@ -6,10 +6,8 @@ import (
"strconv" "strconv"
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/interceptor" "lunar-tear/server/internal/interceptor"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/service" "lunar-tear/server/internal/service"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
@@ -40,27 +38,7 @@ func startGRPC(
store.UserRepository store.UserRepository
store.SessionRepository store.SessionRepository
}, },
questEngine *questflow.QuestHandler, holder *runtime.Holder,
gachaHandler *gacha.GachaHandler,
gachaEntries []store.GachaCatalogEntry,
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
loginBonusCatalog *masterdata.LoginBonusCatalog,
characterViewerCatalog *masterdata.CharacterViewerCatalog,
shopCatalog *masterdata.ShopCatalog,
costumeCatalog *masterdata.CostumeCatalog,
omikujiCatalog *masterdata.OmikujiCatalog,
weaponCatalog *masterdata.WeaponCatalog,
exploreCatalog *masterdata.ExploreCatalog,
gimmickCatalog *masterdata.GimmickCatalog,
characterBoardCatalog *masterdata.CharacterBoardCatalog,
partsCatalog *masterdata.PartsCatalog,
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
companionCatalog *masterdata.CompanionCatalog,
materialCatalog *masterdata.MaterialCatalog,
consumableItemCatalog *masterdata.ConsumableItemCatalog,
gameConfig *masterdata.GameConfig,
sideStoryCatalog *masterdata.SideStoryCatalog,
bigHuntCatalog *masterdata.BigHuntCatalog,
) *grpc.Server { ) *grpc.Server {
lis, err := net.Listen("tcp", listenAddr) lis, err := net.Listen("tcp", listenAddr)
if err != nil { if err != nil {
@@ -74,33 +52,7 @@ func startGRPC(
grpc.UnknownServiceHandler(interceptor.UnknownService), grpc.UnknownServiceHandler(interceptor.UnknownService),
) )
registerServices(grpcServer, registerServices(grpcServer, publicAddr, octoURL, authURL, userStore, holder)
publicAddr,
octoURL,
authURL,
userStore,
questEngine,
gachaHandler,
gachaEntries,
cageOrnamentCatalog,
loginBonusCatalog,
characterViewerCatalog,
shopCatalog,
costumeCatalog,
omikujiCatalog,
weaponCatalog,
exploreCatalog,
gimmickCatalog,
characterBoardCatalog,
partsCatalog,
characterRebirthCatalog,
companionCatalog,
materialCatalog,
consumableItemCatalog,
gameConfig,
sideStoryCatalog,
bigHuntCatalog,
)
reflection.Register(grpcServer) reflection.Register(grpcServer)
@@ -124,66 +76,46 @@ func registerServices(
store.UserRepository store.UserRepository
store.SessionRepository store.SessionRepository
}, },
questEngine *questflow.QuestHandler, holder *runtime.Holder,
gachaHandler *gacha.GachaHandler,
gachaEntries []store.GachaCatalogEntry,
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
loginBonusCatalog *masterdata.LoginBonusCatalog,
characterViewerCatalog *masterdata.CharacterViewerCatalog,
shopCatalog *masterdata.ShopCatalog,
costumeCatalog *masterdata.CostumeCatalog,
omikujiCatalog *masterdata.OmikujiCatalog,
weaponCatalog *masterdata.WeaponCatalog,
exploreCatalog *masterdata.ExploreCatalog,
gimmickCatalog *masterdata.GimmickCatalog,
characterBoardCatalog *masterdata.CharacterBoardCatalog,
partsCatalog *masterdata.PartsCatalog,
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
companionCatalog *masterdata.CompanionCatalog,
materialCatalog *masterdata.MaterialCatalog,
consumableItemCatalog *masterdata.ConsumableItemCatalog,
gameConfig *masterdata.GameConfig,
sideStoryCatalog *masterdata.SideStoryCatalog,
bigHuntCatalog *masterdata.BigHuntCatalog,
) { ) {
pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr) pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr)
pubPort, _ := strconv.Atoi(pubPortStr) pubPort, _ := strconv.Atoi(pubPortStr)
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(gachaEntries)) pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(holder))
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL)) pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL))
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore)) pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL)) pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL))
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore)) pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine)) pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, holder))
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, gachaEntries, gachaHandler)) pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, holder))
pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore)) pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore))
pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer()) pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer())
pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, gimmickCatalog)) pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, holder))
pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, questEngine)) pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, holder))
pb.RegisterNotificationServiceServer(srv, service.NewNotificationServiceServer(userStore, userStore)) pb.RegisterNotificationServiceServer(srv, service.NewNotificationServiceServer(userStore, userStore))
pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, cageOrnamentCatalog, questEngine.Granter)) pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, holder))
pb.RegisterDeckServiceServer(srv, service.NewDeckServiceServer(userStore, userStore)) pb.RegisterDeckServiceServer(srv, service.NewDeckServiceServer(userStore, userStore))
pb.RegisterFriendServiceServer(srv, service.NewFriendServiceServer(userStore, userStore)) pb.RegisterFriendServiceServer(srv, service.NewFriendServiceServer(userStore, userStore))
pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, loginBonusCatalog)) pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, holder))
pb.RegisterNaviCutInServiceServer(srv, service.NewNaviCutInServiceServer(userStore, userStore)) pb.RegisterNaviCutInServiceServer(srv, service.NewNaviCutInServiceServer(userStore, userStore))
pb.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore)) pb.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore))
pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(userStore, userStore)) pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(userStore, userStore))
pb.RegisterPortalCageServiceServer(srv, service.NewPortalCageServiceServer(userStore, userStore)) pb.RegisterPortalCageServiceServer(srv, service.NewPortalCageServiceServer(userStore, userStore))
pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, characterViewerCatalog)) pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, holder))
pb.RegisterMissionServiceServer(srv, service.NewMissionServiceServer(userStore, userStore)) pb.RegisterMissionServiceServer(srv, service.NewMissionServiceServer(userStore, userStore))
pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, shopCatalog, questEngine.Granter)) pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, holder))
pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, costumeCatalog, gameConfig)) pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, holder))
pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore)) pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore))
pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, omikujiCatalog)) pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, holder))
pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, weaponCatalog, gameConfig)) pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, holder))
pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, exploreCatalog)) pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, holder))
pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, characterBoardCatalog)) pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, holder))
pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, partsCatalog, gameConfig)) pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, holder))
pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, characterRebirthCatalog, gameConfig)) pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, holder))
pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, companionCatalog, gameConfig)) pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, holder))
pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, materialCatalog, gameConfig)) pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, holder))
pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, consumableItemCatalog, gameConfig)) pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, holder))
pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, sideStoryCatalog)) pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, holder))
pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine)) pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, holder))
pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter)) pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, holder))
} }
+9 -156
View File
@@ -9,30 +9,30 @@ import (
"syscall" "syscall"
"lunar-tear/server/internal/database" "lunar-tear/server/internal/database"
"lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/masterdata/memorydb"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/store/sqlite" "lunar-tear/server/internal/store/sqlite"
) )
const masterDataPath = "assets/release/20240404193219.bin.e"
func main() { func main() {
listen := flag.String("listen", "0.0.0.0:443", "gRPC listen address (host:port)") listen := flag.String("listen", "0.0.0.0:443", "gRPC listen address (host:port)")
publicAddr := flag.String("public-addr", "127.0.0.1:443", "externally-reachable host:port advertised to clients") publicAddr := flag.String("public-addr", "127.0.0.1:443", "externally-reachable host:port advertised to clients")
dbPath := flag.String("db", "db/game.db", "SQLite database path") dbPath := flag.String("db", "db/game.db", "SQLite database path")
octoURL := flag.String("octo-url", "", "Octo CDN base URL the client will use for assets (e.g. http://10.0.2.2:8080)") octoURL := flag.String("octo-url", "", "Octo CDN base URL the client will use for assets (e.g. http://10.0.2.2:8080)")
authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)") authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)")
adminListen := flag.String("admin-listen", "127.0.0.1:8082", "admin webhook listen address (host:port). Loopback by default; only binds when LUNAR_ADMIN_TOKEN is set.")
flag.Parse() flag.Parse()
if *octoURL == "" { if *octoURL == "" {
log.Fatalf("--octo-url is required (e.g. http://10.0.2.2:8080)") log.Fatalf("--octo-url is required (e.g. http://10.0.2.2:8080)")
} }
if err := memorydb.Init("assets/release/20240404193219.bin.e"); err != nil { holder, err := runtime.NewHolder(masterDataPath)
log.Fatalf("load master data: %v", err) if err != nil {
log.Fatalf("init master data: %v", err)
} }
log.Printf("master data loaded (%d tables)", memorydb.TableCount())
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() defer stop()
@@ -44,158 +44,11 @@ func main() {
defer db.Close() defer db.Close()
log.Printf("database opened: %s", *dbPath) log.Printf("database opened: %s", *dbPath)
gameConfig, err := masterdata.LoadGameConfig()
if err != nil {
log.Fatalf("load game config: %v", err)
}
log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)",
gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold)
partsCatalog, err := masterdata.LoadPartsCatalog()
if err != nil {
log.Fatalf("load parts catalog: %v", err)
}
log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType))
questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog)
if err != nil {
log.Fatalf("load quest catalog: %v", err)
}
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
userStore := sqlite.New(db, gametime.Now) userStore := sqlite.New(db, gametime.Now)
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog() grpcServer := startGRPC(*listen, *publicAddr, *octoURL, *authURL, userStore, holder)
if err != nil {
log.Fatalf("load gacha catalog: %v", err)
}
log.Printf("gacha catalog loaded: %d entries", len(gachaEntries))
gachaPool, err := masterdata.LoadGachaPool() startAdmin(*adminListen, holder)
if err != nil {
log.Fatalf("load gacha pool: %v", err)
}
log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d",
len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials))
shopCatalog, err := masterdata.LoadShopCatalog()
if err != nil {
log.Fatalf("load shop catalog: %v", err)
}
log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops",
len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells))
gachaPool.BuildShopFeatured(shopCatalog)
gachaPool.PruneUnpairedCostumes()
gachaPool.BuildFeaturedMapping(gachaEntries)
gachaPool.BuildBannerPools(gachaEntries)
masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool)
dupExchange, err := masterdata.LoadDupExchange()
if err != nil {
log.Fatalf("load dup exchange: %v", err)
}
dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool)
if err != nil {
log.Fatalf("enrich dup exchange: %v", err)
}
log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded)
gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange)
conditionResolver, err := masterdata.LoadConditionResolver()
if err != nil {
log.Fatalf("load condition resolver: %v", err)
}
cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog()
loginBonusCatalog := masterdata.LoadLoginBonusCatalog()
characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver)
omikujiCatalog := masterdata.LoadOmikujiCatalog()
materialCatalog, err := masterdata.LoadMaterialCatalog()
if err != nil {
log.Fatalf("load material catalog: %v", err)
}
log.Printf("material catalog loaded: %d materials", len(materialCatalog.All))
consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog()
if err != nil {
log.Fatalf("load consumable item catalog: %v", err)
}
log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All))
costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog)
if err != nil {
log.Fatalf("load costume catalog: %v", err)
}
log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity))
weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog)
if err != nil {
log.Fatalf("load weapon catalog: %v", err)
}
log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId))
exploreCatalog, err := masterdata.LoadExploreCatalog()
if err != nil {
log.Fatalf("load explore catalog: %v", err)
}
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver)
if err != nil {
log.Fatalf("load gimmick catalog: %v", err)
}
characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog()
if err != nil {
log.Fatalf("load character board catalog: %v", err)
}
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
if err != nil {
log.Fatalf("load character rebirth catalog: %v", err)
}
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
companionCatalog, err := masterdata.LoadCompanionCatalog()
if err != nil {
log.Fatalf("load companion catalog: %v", err)
}
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
grpcServer := startGRPC(
*listen,
*publicAddr,
*octoURL,
*authURL,
userStore,
questHandler,
gachaHandler,
gachaEntries,
cageOrnamentCatalog,
loginBonusCatalog,
characterViewerCatalog,
shopCatalog,
costumeCatalog,
omikujiCatalog,
weaponCatalog,
exploreCatalog,
gimmickCatalog,
characterBoardCatalog,
partsCatalog,
characterRebirthCatalog,
companionCatalog,
materialCatalog,
consumableItemCatalog,
gameConfig,
sideStoryCatalog,
bigHuntCatalog,
)
<-ctx.Done() <-ctx.Done()
log.Println("shutting down...") log.Println("shutting down...")
+48 -7
View File
@@ -39,6 +39,7 @@ type config struct {
GRPCPort int `json:"grpc_port,omitempty"` GRPCPort int `json:"grpc_port,omitempty"`
CDNPort int `json:"cdn_port,omitempty"` CDNPort int `json:"cdn_port,omitempty"`
AuthPort int `json:"auth_port,omitempty"` AuthPort int `json:"auth_port,omitempty"`
AdminPort int `json:"admin_port,omitempty"`
} }
const ( const (
@@ -47,10 +48,13 @@ const (
defaultAuthPort = 3000 defaultAuthPort = 3000
) )
// ports.Admin is opt-in: 0 means the admin webhook is not configured by the
// wizard at all. Other ports always get a default if unset.
type ports struct { type ports struct {
GRPC int GRPC int
CDN int CDN int
Auth int Auth int
Admin int
} }
func main() { func main() {
@@ -59,6 +63,7 @@ func main() {
grpcPort := flag.Int("grpc-port", defaultGRPCPort, "gRPC server port") grpcPort := flag.Int("grpc-port", defaultGRPCPort, "gRPC server port")
cdnPort := flag.Int("cdn-port", defaultCDNPort, "CDN server port") cdnPort := flag.Int("cdn-port", defaultCDNPort, "CDN server port")
authPort := flag.Int("auth-port", defaultAuthPort, "auth server port") authPort := flag.Int("auth-port", defaultAuthPort, "auth server port")
adminPort := flag.Int("admin-port", 0, "admin webhook port (0 = disabled). Bound on 127.0.0.1; only takes effect when LUNAR_ADMIN_TOKEN is set.")
flag.Parse() flag.Parse()
flagSet := map[string]bool{} flagSet := map[string]bool{}
@@ -80,10 +85,10 @@ func main() {
ip, cfg, firstRun := resolveIP(*preferSaved) ip, cfg, firstRun := resolveIP(*preferSaved)
p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, cfg) p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, *adminPort, cfg)
savedPorts := portsFromConfig(cfg) savedPorts := portsFromConfig(cfg)
if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth) { if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth || p.Admin != savedPorts.Admin) {
if !warnPortChange(savedPorts, p) { if !warnPortChange(savedPorts, p) {
os.Exit(0) os.Exit(0)
} }
@@ -92,6 +97,7 @@ func main() {
cfg.GRPCPort = p.GRPC cfg.GRPCPort = p.GRPC
cfg.CDNPort = p.CDN cfg.CDNPort = p.CDN
cfg.AuthPort = p.Auth cfg.AuthPort = p.Auth
cfg.AdminPort = p.Admin
saveConfig(cfg) saveConfig(cfg)
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14) labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14)
@@ -101,6 +107,9 @@ func main() {
fmt.Printf(" %s %s\n", labelStyle.Render("Game server:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.GRPC))) fmt.Printf(" %s %s\n", labelStyle.Render("Game server:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.GRPC)))
fmt.Printf(" %s %s\n", labelStyle.Render("CDN:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.CDN))) fmt.Printf(" %s %s\n", labelStyle.Render("CDN:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.CDN)))
fmt.Printf(" %s %s\n", labelStyle.Render("Auth:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.Auth))) fmt.Printf(" %s %s\n", labelStyle.Render("Auth:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.Auth)))
if p.Admin > 0 {
fmt.Printf(" %s %s\n", labelStyle.Render("Admin webhook:"), addrStyle.Render(fmt.Sprintf("127.0.0.1:%d", p.Admin)))
}
fmt.Println() fmt.Println()
if firstRun || *setupOnly { if firstRun || *setupOnly {
@@ -477,6 +486,22 @@ func warnPortChange(old, new ports) bool {
} }
return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", label+":", oldP, newP)) return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", label+":", oldP, newP))
} }
// Admin formatting handles the disabled (0) state since the port is
// opt-in and we don't want to display "0" to the user.
adminLine := func(oldP, newP int) (string, bool) {
switch {
case oldP == 0 && newP == 0:
return "", false
case oldP == 0 && newP != 0:
return hlStyle.Render(fmt.Sprintf(" %-7s disabled → %d", "Admin:", newP)), true
case oldP != 0 && newP == 0:
return hlStyle.Render(fmt.Sprintf(" %-7s %d → disabled", "Admin:", oldP)), true
case oldP == newP:
return dimStyle.Render(fmt.Sprintf(" %-7s %d (unchanged)", "Admin:", oldP)), true
default:
return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", "Admin:", oldP, newP)), true
}
}
var b strings.Builder var b strings.Builder
b.WriteString("\n") b.WriteString("\n")
@@ -487,7 +512,12 @@ func warnPortChange(old, new ports) bool {
b.WriteString(portLine("CDN", old.CDN, new.CDN)) b.WriteString(portLine("CDN", old.CDN, new.CDN))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(portLine("Auth", old.Auth, new.Auth)) b.WriteString(portLine("Auth", old.Auth, new.Auth))
b.WriteString("\n\n") b.WriteString("\n")
if line, show := adminLine(old.Admin, new.Admin); show {
b.WriteString(line)
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(dimStyle.Render(" Your APK was patched for the old ports. You may need to re-patch.")) b.WriteString(dimStyle.Render(" Your APK was patched for the old ports. You may need to re-patch."))
b.WriteString("\n\n") b.WriteString("\n\n")
fmt.Print(b.String()) fmt.Print(b.String())
@@ -821,7 +851,7 @@ func loadConfig() (config, error) {
} }
func portsFromConfig(cfg config) ports { func portsFromConfig(cfg config) ports {
p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort} p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort, Admin: cfg.AdminPort}
if p.GRPC == 0 { if p.GRPC == 0 {
p.GRPC = defaultGRPCPort p.GRPC = defaultGRPCPort
} }
@@ -831,10 +861,11 @@ func portsFromConfig(cfg config) ports {
if p.Auth == 0 { if p.Auth == 0 {
p.Auth = defaultAuthPort p.Auth = defaultAuthPort
} }
// Admin is opt-in: leave 0 = disabled.
return p return p
} }
func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, saved config) ports { func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag, adminFlag int, saved config) ports {
resolve := func(name string, flagVal, savedVal, defaultVal int) int { resolve := func(name string, flagVal, savedVal, defaultVal int) int {
if flagSet[name] { if flagSet[name] {
return flagVal return flagVal
@@ -848,6 +879,9 @@ func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, save
GRPC: resolve("grpc-port", grpcFlag, saved.GRPCPort, defaultGRPCPort), GRPC: resolve("grpc-port", grpcFlag, saved.GRPCPort, defaultGRPCPort),
CDN: resolve("cdn-port", cdnFlag, saved.CDNPort, defaultCDNPort), CDN: resolve("cdn-port", cdnFlag, saved.CDNPort, defaultCDNPort),
Auth: resolve("auth-port", authFlag, saved.AuthPort, defaultAuthPort), Auth: resolve("auth-port", authFlag, saved.AuthPort, defaultAuthPort),
// defaultVal=0 keeps admin opt-in: never enabled unless --admin-port
// is passed or a non-zero value was previously saved.
Admin: resolve("admin-port", adminFlag, saved.AdminPort, 0),
} }
} }
@@ -874,13 +908,20 @@ func launchDev(ip string, p ports) {
runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev") runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev")
}).Run() }).Run()
cmd := exec.Command(devBin, devArgs := []string{
"--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC), "--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC),
"--grpc.public-addr", fmt.Sprintf("%s:%d", ip, p.GRPC), "--grpc.public-addr", fmt.Sprintf("%s:%d", ip, p.GRPC),
"--cdn.listen", fmt.Sprintf("0.0.0.0:%d", p.CDN), "--cdn.listen", fmt.Sprintf("0.0.0.0:%d", p.CDN),
"--cdn.public-addr", fmt.Sprintf("%s:%d", ip, p.CDN), "--cdn.public-addr", fmt.Sprintf("%s:%d", ip, p.CDN),
"--auth.listen", fmt.Sprintf("0.0.0.0:%d", p.Auth), "--auth.listen", fmt.Sprintf("0.0.0.0:%d", p.Auth),
) }
// Bind admin on loopback only — the wizard is for local dev, and the
// webhook should never be exposed to the LAN by accident. Operators who
// want a different bind can run cmd/dev directly with --admin.listen.
if p.Admin > 0 {
devArgs = append(devArgs, "--admin.listen", fmt.Sprintf("127.0.0.1:%d", p.Admin))
}
cmd := exec.Command(devBin, devArgs...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
+3
View File
@@ -10,11 +10,14 @@ services:
LUNAR_PUBLIC_ADDR: 127.0.0.1:8003 LUNAR_PUBLIC_ADDR: 127.0.0.1:8003
LUNAR_OCTO_URL: http://cdn:8080 LUNAR_OCTO_URL: http://cdn:8080
LUNAR_AUTH_URL: http://auth:3000 LUNAR_AUTH_URL: http://auth:3000
LUNAR_ADMIN_LISTEN: 0.0.0.0:8082
LUNAR_ADMIN_TOKEN: ${LUNAR_ADMIN_TOKEN:-}
volumes: volumes:
- ./db:/opt/lunar-tear/db - ./db:/opt/lunar-tear/db
- ./assets:/opt/lunar-tear/assets - ./assets:/opt/lunar-tear/assets
ports: ports:
- 8003:8003 - 8003:8003
- 127.0.0.1:8082:8082
depends_on: depends_on:
- cdn - cdn
- auth - auth
+7 -1
View File
@@ -9,8 +9,14 @@ if [ -n "${LUNAR_AUTH_URL}" ]; then
AUTH_FLAG="--auth-url ${LUNAR_AUTH_URL}" AUTH_FLAG="--auth-url ${LUNAR_AUTH_URL}"
fi fi
ADMIN_FLAG=""
if [ -n "${LUNAR_ADMIN_LISTEN}" ]; then
ADMIN_FLAG="--admin-listen ${LUNAR_ADMIN_LISTEN}"
fi
exec ./lunar-tear \ exec ./lunar-tear \
--listen "${LUNAR_LISTEN:-0.0.0.0:443}" \ --listen "${LUNAR_LISTEN:-0.0.0.0:443}" \
--public-addr "${LUNAR_PUBLIC_ADDR}" \ --public-addr "${LUNAR_PUBLIC_ADDR}" \
--octo-url "${LUNAR_OCTO_URL}" \ --octo-url "${LUNAR_OCTO_URL}" \
${AUTH_FLAG} ${AUTH_FLAG} \
${ADMIN_FLAG}
+170
View File
@@ -0,0 +1,170 @@
package runtime
import (
"fmt"
"log"
"lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/masterdata/memorydb"
"lunar-tear/server/internal/questflow"
)
// buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever
// memorydb currently holds and returns a fully populated *Catalogs. Called
// once at startup and again on every reload.
func buildCatalogs() (*Catalogs, error) {
log.Printf("master data loaded (%d tables)", memorydb.TableCount())
gameConfig, err := masterdata.LoadGameConfig()
if err != nil {
return nil, fmt.Errorf("load game config: %w", err)
}
log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)",
gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold)
partsCatalog, err := masterdata.LoadPartsCatalog()
if err != nil {
return nil, fmt.Errorf("load parts catalog: %w", err)
}
log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType))
questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog)
if err != nil {
return nil, fmt.Errorf("load quest catalog: %w", err)
}
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
if err != nil {
return nil, fmt.Errorf("load gacha catalog: %w", err)
}
log.Printf("gacha catalog loaded: %d entries", len(gachaEntries))
gachaPool, err := masterdata.LoadGachaPool()
if err != nil {
return nil, fmt.Errorf("load gacha pool: %w", err)
}
log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d",
len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials))
shopCatalog, err := masterdata.LoadShopCatalog()
if err != nil {
return nil, fmt.Errorf("load shop catalog: %w", err)
}
log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops",
len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells))
gachaPool.BuildShopFeatured(shopCatalog)
gachaPool.PruneUnpairedCostumes()
gachaPool.BuildFeaturedMapping(gachaEntries)
gachaPool.BuildBannerPools(gachaEntries)
masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool)
dupExchange, err := masterdata.LoadDupExchange()
if err != nil {
return nil, fmt.Errorf("load dup exchange: %w", err)
}
dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool)
if err != nil {
return nil, fmt.Errorf("enrich dup exchange: %w", err)
}
log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded)
gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange)
conditionResolver, err := masterdata.LoadConditionResolver()
if err != nil {
return nil, fmt.Errorf("load condition resolver: %w", err)
}
cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog()
loginBonusCatalog := masterdata.LoadLoginBonusCatalog()
characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver)
omikujiCatalog := masterdata.LoadOmikujiCatalog()
materialCatalog, err := masterdata.LoadMaterialCatalog()
if err != nil {
return nil, fmt.Errorf("load material catalog: %w", err)
}
log.Printf("material catalog loaded: %d materials", len(materialCatalog.All))
consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog()
if err != nil {
return nil, fmt.Errorf("load consumable item catalog: %w", err)
}
log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All))
costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog)
if err != nil {
return nil, fmt.Errorf("load costume catalog: %w", err)
}
log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity))
weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog)
if err != nil {
return nil, fmt.Errorf("load weapon catalog: %w", err)
}
log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId))
exploreCatalog, err := masterdata.LoadExploreCatalog()
if err != nil {
return nil, fmt.Errorf("load explore catalog: %w", err)
}
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver)
if err != nil {
return nil, fmt.Errorf("load gimmick catalog: %w", err)
}
characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog()
if err != nil {
return nil, fmt.Errorf("load character board catalog: %w", err)
}
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
if err != nil {
return nil, fmt.Errorf("load character rebirth catalog: %w", err)
}
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
companionCatalog, err := masterdata.LoadCompanionCatalog()
if err != nil {
return nil, fmt.Errorf("load companion catalog: %w", err)
}
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
return &Catalogs{
GameConfig: gameConfig,
Parts: partsCatalog,
Quest: questCatalog,
GachaEntries: gachaEntries,
GachaMedals: medalInfo,
GachaPool: gachaPool,
Shop: shopCatalog,
DupExchange: dupExchange,
ConditionResolver: conditionResolver,
CageOrnament: cageOrnamentCatalog,
LoginBonus: loginBonusCatalog,
CharacterViewer: characterViewerCatalog,
Omikuji: omikujiCatalog,
Material: materialCatalog,
ConsumableItem: consumableItemCatalog,
Costume: costumeCatalog,
Weapon: weaponCatalog,
Explore: exploreCatalog,
Gimmick: gimmickCatalog,
CharacterBoard: characterBoardCatalog,
CharacterRebirth: characterRebirthCatalog,
Companion: companionCatalog,
SideStory: sideStoryCatalog,
BigHunt: bigHuntCatalog,
QuestHandler: questHandler,
GachaHandler: gachaHandler,
}, nil
}
+104
View File
@@ -0,0 +1,104 @@
// Package runtime owns the live, hot-swappable view of master data.
//
// The Holder atomically swaps a *Catalogs aggregate every time the operator
// asks the server to re-read assets/release/20240404193219.bin.e (typically via
// the admin webhook in cmd/lunar-tear/admin.go). gRPC services hold a *Holder
// and call Get() at the start of each RPC, so they always see a consistent
// snapshot.
package runtime
import (
"fmt"
"log"
"os"
"sync/atomic"
"time"
"lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/masterdata/memorydb"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/store"
)
// Catalogs is an immutable snapshot of every catalog and catalog-derived
// handler the server needs at runtime. A new *Catalogs is built from scratch
// on every reload and atomically published via Holder.
type Catalogs struct {
GameConfig *masterdata.GameConfig
Parts *masterdata.PartsCatalog
Quest *masterdata.QuestCatalog
GachaEntries []store.GachaCatalogEntry
GachaMedals map[int32]masterdata.GachaMedalInfo
GachaPool *masterdata.GachaCatalog
Shop *masterdata.ShopCatalog
DupExchange map[int32][]model.DupExchangeEntry
ConditionResolver *masterdata.ConditionResolver
CageOrnament *masterdata.CageOrnamentCatalog
LoginBonus *masterdata.LoginBonusCatalog
CharacterViewer *masterdata.CharacterViewerCatalog
Omikuji *masterdata.OmikujiCatalog
Material *masterdata.MaterialCatalog
ConsumableItem *masterdata.ConsumableItemCatalog
Costume *masterdata.CostumeCatalog
Weapon *masterdata.WeaponCatalog
Explore *masterdata.ExploreCatalog
Gimmick *masterdata.GimmickCatalog
CharacterBoard *masterdata.CharacterBoardCatalog
CharacterRebirth *masterdata.CharacterRebirthCatalog
Companion *masterdata.CompanionCatalog
SideStory *masterdata.SideStoryCatalog
BigHunt *masterdata.BigHuntCatalog
// Catalog-derived handlers must rebuild on every reload because they
// embed/cache pointers to specific catalog instances.
QuestHandler *questflow.QuestHandler
GachaHandler *gacha.GachaHandler
}
// Holder owns the current *Catalogs and the bin.e path. Concurrent readers
// call Get(); the single-writer Reload() rebuilds and atomically publishes.
type Holder struct {
binPath string
cur atomic.Pointer[Catalogs]
}
// NewHolder reads the binary at binPath, builds the initial catalogs, and
// returns a ready-to-use Holder. Subsequent calls to Reload() re-read the
// same path.
func NewHolder(binPath string) (*Holder, error) {
h := &Holder{binPath: binPath}
if err := h.Reload(); err != nil {
return nil, err
}
return h, nil
}
// Reload re-reads the bin.e from disk, rebuilds every catalog and handler,
// atomically publishes the new snapshot, and bumps the bin.e mtime so client
// caches invalidate (see service/data.go GetLatestMasterDataVersion).
func (h *Holder) Reload() error {
if err := memorydb.Init(h.binPath); err != nil {
return fmt.Errorf("memorydb.Init: %w", err)
}
c, err := buildCatalogs()
if err != nil {
return fmt.Errorf("buildCatalogs: %w", err)
}
h.cur.Store(c)
now := time.Now()
if err := os.Chtimes(h.binPath, now, now); err != nil {
// Non-fatal: the catalogs swapped fine in-memory; clients may take
// longer to invalidate their cached download but server-side state is
// already coherent.
log.Printf("[runtime] os.Chtimes(%s) failed (clients may not invalidate cache): %v", h.binPath, err)
}
return nil
}
// Get returns the current snapshot. Safe for concurrent callers; the returned
// pointer is stable for the duration of the caller's use.
func (h *Holder) Get() *Catalogs {
return h.cur.Load()
}
+10 -5
View File
@@ -4,24 +4,29 @@ import (
"context" "context"
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/runtime"
) )
type BannerServiceServer struct { type BannerServiceServer struct {
pb.UnimplementedBannerServiceServer pb.UnimplementedBannerServiceServer
catalog []store.GachaCatalogEntry holder *runtime.Holder
} }
func NewBannerServiceServer(catalog []store.GachaCatalogEntry) *BannerServiceServer { func NewBannerServiceServer(holder *runtime.Holder) *BannerServiceServer {
return &BannerServiceServer{catalog: catalog} return &BannerServiceServer{holder: holder}
} }
func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) { func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) {
catalog := s.catalog catalog := s.holder.Get().GachaEntries
nowMillis := gametime.NowMillis()
var termLimited []*pb.GachaBanner var termLimited []*pb.GachaBanner
var latestChapter *pb.GachaBanner var latestChapter *pb.GachaBanner
for _, entry := range catalog { for _, entry := range catalog {
if !gachaActiveAt(entry, nowMillis) {
continue
}
if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle { if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle {
continue continue
} }
+8 -7
View File
@@ -6,8 +6,8 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -15,21 +15,22 @@ type CageOrnamentServiceServer struct {
pb.UnimplementedCageOrnamentServiceServer pb.UnimplementedCageOrnamentServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.CageOrnamentCatalog holder *runtime.Holder
granter *store.PossessionGranter
} }
func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CageOrnamentCatalog, granter *store.PossessionGranter) *CageOrnamentServiceServer { func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CageOrnamentServiceServer {
return &CageOrnamentServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter} return &CageOrnamentServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.ReceiveRewardRequest) (*pb.ReceiveRewardResponse, error) { func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.ReceiveRewardRequest) (*pb.ReceiveRewardResponse, error) {
log.Printf("[CageOrnamentService] ReceiveReward: cageOrnamentId=%d", req.CageOrnamentId) log.Printf("[CageOrnamentService] ReceiveReward: cageOrnamentId=%d", req.CageOrnamentId)
reward, ok := s.catalog.LookupReward(req.CageOrnamentId) cat := s.holder.Get()
reward, ok := cat.CageOrnament.LookupReward(req.CageOrnamentId)
if !ok { if !ok {
log.Fatalf("[CageOrnamentService] ReceiveReward: no reward for cageOrnamentId=%d", req.CageOrnamentId) log.Fatalf("[CageOrnamentService] ReceiveReward: no reward for cageOrnamentId=%d", req.CageOrnamentId)
} }
granter := cat.QuestHandler.Granter
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -39,7 +40,7 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R
AcquisitionDatetime: nowMillis, AcquisitionDatetime: nowMillis,
LatestVersion: nowMillis, LatestVersion: nowMillis,
} }
s.granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis) granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
}) })
return &pb.ReceiveRewardResponse{ return &pb.ReceiveRewardResponse{
+13 -10
View File
@@ -7,6 +7,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -14,21 +15,23 @@ type CharacterServiceServer struct {
pb.UnimplementedCharacterServiceServer pb.UnimplementedCharacterServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.CharacterRebirthCatalog holder *runtime.Holder
config *masterdata.GameConfig
} }
func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterRebirthCatalog, config *masterdata.GameConfig) *CharacterServiceServer { func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterServiceServer {
return &CharacterServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} return &CharacterServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthRequest) (*pb.RebirthResponse, error) { func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthRequest) (*pb.RebirthResponse, error) {
log.Printf("[CharacterService] Rebirth: characterId=%d rebirthCount=%d", req.CharacterId, req.RebirthCount) log.Printf("[CharacterService] Rebirth: characterId=%d rebirthCount=%d", req.CharacterId, req.RebirthCount)
cat := s.holder.Get()
catalog := cat.CharacterRebirth
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
stepGroupId, ok := s.catalog.StepGroupByCharacterId[req.CharacterId] stepGroupId, ok := catalog.StepGroupByCharacterId[req.CharacterId]
if !ok { if !ok {
log.Printf("[CharacterService] Rebirth: no step group for characterId=%d", req.CharacterId) log.Printf("[CharacterService] Rebirth: no step group for characterId=%d", req.CharacterId)
return &pb.RebirthResponse{}, nil return &pb.RebirthResponse{}, nil
@@ -40,17 +43,17 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq
targetCount := currentCount + req.RebirthCount targetCount := currentCount + req.RebirthCount
for count := currentCount; count < targetCount; count++ { for count := currentCount; count < targetCount; count++ {
step, ok := s.catalog.StepByGroupAndCount[masterdata.StepKey{GroupId: stepGroupId, BeforeRebirthCount: count}] step, ok := catalog.StepByGroupAndCount[masterdata.StepKey{GroupId: stepGroupId, BeforeRebirthCount: count}]
if !ok { if !ok {
log.Printf("[CharacterService] Rebirth: no step row for groupId=%d beforeCount=%d", stepGroupId, count) log.Printf("[CharacterService] Rebirth: no step row for groupId=%d beforeCount=%d", stepGroupId, count)
return return
} }
goldId := s.config.ConsumableItemIdForGold goldId := config.ConsumableItemIdForGold
user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-s.config.CharacterRebirthConsumeGold, 0) user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-config.CharacterRebirthConsumeGold, 0)
log.Printf("[CharacterService] Rebirth: consumed gold=%d", s.config.CharacterRebirthConsumeGold) log.Printf("[CharacterService] Rebirth: consumed gold=%d", config.CharacterRebirthConsumeGold)
materials := s.catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId] materials := catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId]
for _, mat := range materials { for _, mat := range materials {
user.Materials[mat.MaterialId] -= mat.Count user.Materials[mat.MaterialId] -= mat.Count
if user.Materials[mat.MaterialId] <= 0 { if user.Materials[mat.MaterialId] <= 0 {
+25 -23
View File
@@ -7,6 +7,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -14,43 +15,44 @@ type CharacterBoardServiceServer struct {
pb.UnimplementedCharacterBoardServiceServer pb.UnimplementedCharacterBoardServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.CharacterBoardCatalog holder *runtime.Holder
} }
func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterBoardCatalog) *CharacterBoardServiceServer { func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterBoardServiceServer {
return &CharacterBoardServiceServer{users: users, sessions: sessions, catalog: catalog} return &CharacterBoardServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) { func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) {
log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId) log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId)
catalog := s.holder.Get().CharacterBoard
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
for _, panelId := range req.CharacterBoardPanelId { for _, panelId := range req.CharacterBoardPanelId {
panel, ok := s.catalog.PanelById[panelId] panel, ok := catalog.PanelById[panelId]
if !ok { if !ok {
log.Printf("[CharacterBoardService] unknown panelId=%d, skipping", panelId) log.Printf("[CharacterBoardService] unknown panelId=%d, skipping", panelId)
continue continue
} }
s.consumeCosts(user, panel) consumeBoardCosts(catalog, user, panel)
s.setReleaseBit(user, panel) setBoardReleaseBit(user, panel)
s.applyEffects(user, panel) applyBoardEffects(catalog, user, panel)
} }
}) })
return &pb.ReleasePanelResponse{}, nil return &pb.ReleasePanelResponse{}, nil
} }
func (s *CharacterBoardServiceServer) consumeCosts(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { func consumeBoardCosts(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
costs := s.catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId] costs := catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId]
for _, cost := range costs { for _, cost := range costs {
store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count) store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count)
} }
} }
func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { func setBoardReleaseBit(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
boardId := panel.CharacterBoardId boardId := panel.CharacterBoardId
board := user.CharacterBoards[boardId] board := user.CharacterBoards[boardId]
board.CharacterBoardId = boardId board.CharacterBoardId = boardId
@@ -73,26 +75,26 @@ func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel
user.CharacterBoards[boardId] = board user.CharacterBoards[boardId] = board
} }
func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) { func applyBoardEffects(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, panel masterdata.EntityMCharacterBoardPanel) {
effects := s.catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId] effects := catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId]
for _, eff := range effects { for _, eff := range effects {
switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) { switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) {
case model.CharacterBoardEffectTypeAbility: case model.CharacterBoardEffectTypeAbility:
s.applyAbilityEffect(user, eff) applyBoardAbilityEffect(catalog, user, eff)
case model.CharacterBoardEffectTypeStatusUp: case model.CharacterBoardEffectTypeStatusUp:
s.applyStatusUpEffect(user, eff) applyBoardStatusUpEffect(catalog, user, eff)
} }
} }
} }
func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { func applyBoardAbilityEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) {
ability, ok := s.catalog.AbilityById[eff.CharacterBoardEffectId] ability, ok := catalog.AbilityById[eff.CharacterBoardEffectId]
if !ok { if !ok {
log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId) log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId)
return return
} }
characterId := s.resolveCharacterId(ability.CharacterBoardEffectTargetGroupId) characterId := resolveBoardCharacterId(catalog, ability.CharacterBoardEffectTargetGroupId)
if characterId == 0 { if characterId == 0 {
return return
} }
@@ -103,21 +105,21 @@ func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState,
state.AbilityId = ability.AbilityId state.AbilityId = ability.AbilityId
state.Level += eff.EffectValue state.Level += eff.EffectValue
if maxLvl, ok := s.catalog.AbilityMaxLevel[key]; ok && state.Level > maxLvl { if maxLvl, ok := catalog.AbilityMaxLevel[key]; ok && state.Level > maxLvl {
state.Level = maxLvl state.Level = maxLvl
} }
user.CharacterBoardAbilities[key] = state user.CharacterBoardAbilities[key] = state
} }
func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) { func applyBoardStatusUpEffect(catalog *masterdata.CharacterBoardCatalog, user *store.UserState, eff masterdata.EntityMCharacterBoardPanelReleaseEffectGroup) {
statusUp, ok := s.catalog.StatusUpById[eff.CharacterBoardEffectId] statusUp, ok := catalog.StatusUpById[eff.CharacterBoardEffectId]
if !ok { if !ok {
log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId) log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId)
return return
} }
characterId := s.resolveCharacterId(statusUp.CharacterBoardEffectTargetGroupId) characterId := resolveBoardCharacterId(catalog, statusUp.CharacterBoardEffectTargetGroupId)
if characterId == 0 { if characterId == 0 {
return return
} }
@@ -151,8 +153,8 @@ func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState,
user.CharacterBoardStatusUps[key] = state user.CharacterBoardStatusUps[key] = state
} }
func (s *CharacterBoardServiceServer) resolveCharacterId(targetGroupId int32) int32 { func resolveBoardCharacterId(catalog *masterdata.CharacterBoardCatalog, targetGroupId int32) int32 {
targets := s.catalog.EffectTargetsByGroupId[targetGroupId] targets := catalog.EffectTargetsByGroupId[targetGroupId]
for _, t := range targets { for _, t := range targets {
if t.TargetValue != 0 { if t.TargetValue != 0 {
return t.TargetValue return t.TargetValue
+5 -5
View File
@@ -6,7 +6,7 @@ import (
"log" "log"
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
@@ -16,11 +16,11 @@ type CharacterViewerServiceServer struct {
pb.UnimplementedCharacterViewerServiceServer pb.UnimplementedCharacterViewerServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.CharacterViewerCatalog holder *runtime.Holder
} }
func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterViewerCatalog) *CharacterViewerServiceServer { func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CharacterViewerServiceServer {
return &CharacterViewerServiceServer{users: users, sessions: sessions, catalog: catalog} return &CharacterViewerServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ *emptypb.Empty) (*pb.CharacterViewerTopResponse, error) { func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ *emptypb.Empty) (*pb.CharacterViewerTopResponse, error) {
@@ -32,7 +32,7 @@ func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _
panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err)) panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err))
} }
released := s.catalog.ReleasedFieldIds(user) released := s.holder.Get().CharacterViewer.ReleasedFieldIds(user)
log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId) log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId)
return &pb.CharacterViewerTopResponse{ return &pb.CharacterViewerTopResponse{
+11 -8
View File
@@ -8,6 +8,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -17,17 +18,19 @@ type CompanionServiceServer struct {
pb.UnimplementedCompanionServiceServer pb.UnimplementedCompanionServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.CompanionCatalog holder *runtime.Holder
config *masterdata.GameConfig
} }
func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CompanionCatalog, config *masterdata.GameConfig) *CompanionServiceServer { func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CompanionServiceServer {
return &CompanionServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} return &CompanionServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionEnhanceRequest) (*pb.CompanionEnhanceResponse, error) { func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionEnhanceRequest) (*pb.CompanionEnhanceResponse, error) {
log.Printf("[CompanionService] Enhance: uuid=%s addLevel=%d", req.UserCompanionUuid, req.AddLevelCount) log.Printf("[CompanionService] Enhance: uuid=%s addLevel=%d", req.UserCompanionUuid, req.AddLevelCount)
cat := s.holder.Get()
catalog := cat.Companion
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -38,7 +41,7 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE
return return
} }
compDef, ok := s.catalog.CompanionById[companion.CompanionId] compDef, ok := catalog.CompanionById[companion.CompanionId]
if !ok { if !ok {
log.Printf("[CompanionService] Enhance: companion master id=%d not found", companion.CompanionId) log.Printf("[CompanionService] Enhance: companion master id=%d not found", companion.CompanionId)
return return
@@ -50,13 +53,13 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE
} }
for lvl := companion.Level; lvl < targetLevel; lvl++ { for lvl := companion.Level; lvl < targetLevel; lvl++ {
if costFunc, ok := s.catalog.GoldCostByCategory[compDef.CompanionCategoryType]; ok { if costFunc, ok := catalog.GoldCostByCategory[compDef.CompanionCategoryType]; ok {
goldCost := costFunc.Evaluate(lvl) goldCost := costFunc.Evaluate(lvl)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
} }
matKey := masterdata.CompanionLevelKey{CategoryType: compDef.CompanionCategoryType, Level: lvl} matKey := masterdata.CompanionLevelKey{CategoryType: compDef.CompanionCategoryType, Level: lvl}
if mat, ok := s.catalog.MaterialsByKey[matKey]; ok { if mat, ok := catalog.MaterialsByKey[matKey]; ok {
user.Materials[mat.MaterialId] -= mat.Count user.Materials[mat.MaterialId] -= mat.Count
} }
} }
+9 -7
View File
@@ -6,7 +6,7 @@ import (
"log" "log"
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -14,23 +14,25 @@ type ConsumableItemServiceServer struct {
pb.UnimplementedConsumableItemServiceServer pb.UnimplementedConsumableItemServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.ConsumableItemCatalog holder *runtime.Holder
config *masterdata.GameConfig
} }
func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ConsumableItemCatalog, config *masterdata.GameConfig) *ConsumableItemServiceServer { func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ConsumableItemServiceServer {
return &ConsumableItemServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} return &ConsumableItemServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) { func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) {
log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession)) log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession))
cat := s.holder.Get()
catalog := cat.ConsumableItem
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
totalGold := int32(0) totalGold := int32(0)
for _, item := range req.ConsumableItemPossession { for _, item := range req.ConsumableItemPossession {
row, ok := s.catalog.All[item.ConsumableItemId] row, ok := catalog.All[item.ConsumableItemId]
if !ok { if !ok {
log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId) log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId)
continue continue
@@ -53,7 +55,7 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab
} }
if totalGold > 0 { if totalGold > 0 {
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold) log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold)
} }
}) })
+55 -37
View File
@@ -13,6 +13,7 @@ import (
"lunar-tear/server/internal/gameutil" "lunar-tear/server/internal/gameutil"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -20,17 +21,19 @@ type CostumeServiceServer struct {
pb.UnimplementedCostumeServiceServer pb.UnimplementedCostumeServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.CostumeCatalog holder *runtime.Holder
config *masterdata.GameConfig
} }
func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CostumeCatalog, config *masterdata.GameConfig) *CostumeServiceServer { func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *CostumeServiceServer {
return &CostumeServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} return &CostumeServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceRequest) (*pb.EnhanceResponse, error) { func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceRequest) (*pb.EnhanceResponse, error) {
log.Printf("[CostumeService] Enhance: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) log.Printf("[CostumeService] Enhance: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
cat := s.holder.Get()
catalog := cat.Costume
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -41,7 +44,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
return return
} }
cm, ok := s.catalog.Costumes[costume.CostumeId] cm, ok := catalog.Costumes[costume.CostumeId]
if !ok { if !ok {
log.Printf("[CostumeService] Enhance: costume master id=%d not found", costume.CostumeId) log.Printf("[CostumeService] Enhance: costume master id=%d not found", costume.CostumeId)
return return
@@ -50,7 +53,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
totalExp := int32(0) totalExp := int32(0)
totalMaterialCount := int32(0) totalMaterialCount := int32(0)
for materialId, count := range req.Materials { for materialId, count := range req.Materials {
mat, ok := s.catalog.Materials[materialId] mat, ok := catalog.Materials[materialId]
if !ok { if !ok {
log.Printf("[CostumeService] Enhance: material id=%d not found, skipping", materialId) log.Printf("[CostumeService] Enhance: material id=%d not found, skipping", materialId)
continue continue
@@ -66,20 +69,20 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
expPerUnit := mat.EffectValue expPerUnit := mat.EffectValue
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType { if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
} }
totalExp += expPerUnit * count totalExp += expPerUnit * count
} }
if costFunc, ok := s.catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 { if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
goldCost := costFunc.Evaluate(totalMaterialCount) goldCost := costFunc.Evaluate(totalMaterialCount)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
log.Printf("[CostumeService] Enhance: gold cost=%d (materials=%d)", goldCost, totalMaterialCount) log.Printf("[CostumeService] Enhance: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
} }
costume.Exp += totalExp costume.Exp += totalExp
if thresholds, ok := s.catalog.ExpByRarity[cm.RarityType]; ok { if thresholds, ok := catalog.ExpByRarity[cm.RarityType]; ok {
costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds) costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds)
} }
@@ -100,6 +103,9 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest) (*pb.AwakenResponse, error) { func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest) (*pb.AwakenResponse, error) {
log.Printf("[CostumeService] Awaken: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) log.Printf("[CostumeService] Awaken: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
cat := s.holder.Get()
catalog := cat.Costume
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -110,7 +116,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
return return
} }
awakenRow, ok := s.catalog.AwakenByCostumeId[costume.CostumeId] awakenRow, ok := catalog.AwakenByCostumeId[costume.CostumeId]
if !ok { if !ok {
log.Printf("[CostumeService] Awaken: no awaken data for costumeId=%d", costume.CostumeId) log.Printf("[CostumeService] Awaken: no awaken data for costumeId=%d", costume.CostumeId)
return return
@@ -118,8 +124,8 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
nextStep := costume.AwakenCount + 1 nextStep := costume.AwakenCount + 1
if gold, ok := s.catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok { if gold, ok := catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok {
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= gold user.ConsumableItems[config.ConsumableItemIdForGold] -= gold
log.Printf("[CostumeService] Awaken: gold cost=%d", gold) log.Printf("[CostumeService] Awaken: gold cost=%d", gold)
} }
@@ -137,7 +143,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
user.Costumes[req.UserCostumeUuid] = costume user.Costumes[req.UserCostumeUuid] = costume
log.Printf("[CostumeService] Awaken: costumeId=%d awakenCount=%d", costume.CostumeId, nextStep) log.Printf("[CostumeService] Awaken: costumeId=%d awakenCount=%d", costume.CostumeId, nextStep)
effectSteps, ok := s.catalog.AwakenEffectsByGroupAndStep[awakenRow.CostumeAwakenEffectGroupId] effectSteps, ok := catalog.AwakenEffectsByGroupAndStep[awakenRow.CostumeAwakenEffectGroupId]
if !ok { if !ok {
return return
} }
@@ -148,11 +154,11 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
switch model.CostumeAwakenEffectType(effect.CostumeAwakenEffectType) { switch model.CostumeAwakenEffectType(effect.CostumeAwakenEffectType) {
case model.CostumeAwakenEffectTypeStatusUp: case model.CostumeAwakenEffectTypeStatusUp:
s.applyAwakenStatusUp(user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis) applyCostumeAwakenStatusUp(catalog, user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis)
case model.CostumeAwakenEffectTypeAbility: case model.CostumeAwakenEffectTypeAbility:
log.Printf("[CostumeService] Awaken: ability effect id=%d (client-resolved)", effect.CostumeAwakenEffectId) log.Printf("[CostumeService] Awaken: ability effect id=%d (client-resolved)", effect.CostumeAwakenEffectId)
case model.CostumeAwakenEffectTypeItemAcquire: case model.CostumeAwakenEffectTypeItemAcquire:
s.applyAwakenItemAcquire(user, effect.CostumeAwakenEffectId, nowMillis) applyCostumeAwakenItemAcquire(catalog, user, effect.CostumeAwakenEffectId, nowMillis)
default: default:
log.Printf("[CostumeService] Awaken: unknown effect type=%d", effect.CostumeAwakenEffectType) log.Printf("[CostumeService] Awaken: unknown effect type=%d", effect.CostumeAwakenEffectType)
} }
@@ -164,8 +170,8 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
return &pb.AwakenResponse{}, nil return &pb.AwakenResponse{}, nil
} }
func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) { func applyCostumeAwakenStatusUp(catalog *masterdata.CostumeCatalog, user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) {
rows, ok := s.catalog.AwakenStatusUpByGroup[statusUpGroupId] rows, ok := catalog.AwakenStatusUpByGroup[statusUpGroupId]
if !ok { if !ok {
log.Printf("[CostumeService] Awaken: status up group %d not found", statusUpGroupId) log.Printf("[CostumeService] Awaken: status up group %d not found", statusUpGroupId)
return return
@@ -201,8 +207,8 @@ func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costum
} }
} }
func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, itemAcquireId int32, nowMillis int64) { func applyCostumeAwakenItemAcquire(catalog *masterdata.CostumeCatalog, user *store.UserState, itemAcquireId int32, nowMillis int64) {
acq, ok := s.catalog.AwakenItemAcquireById[itemAcquireId] acq, ok := catalog.AwakenItemAcquireById[itemAcquireId]
if !ok { if !ok {
log.Printf("[CostumeService] Awaken: item acquire id=%d not found", itemAcquireId) log.Printf("[CostumeService] Awaken: item acquire id=%d not found", itemAcquireId)
return return
@@ -226,6 +232,9 @@ func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, ite
func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.EnhanceActiveSkillRequest) (*pb.EnhanceActiveSkillResponse, error) { func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.EnhanceActiveSkillRequest) (*pb.EnhanceActiveSkillResponse, error) {
log.Printf("[CostumeService] EnhanceActiveSkill: uuid=%s addLevel=%d", req.UserCostumeUuid, req.AddLevelCount) log.Printf("[CostumeService] EnhanceActiveSkill: uuid=%s addLevel=%d", req.UserCostumeUuid, req.AddLevelCount)
cat := s.holder.Get()
catalog := cat.Costume
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -236,13 +245,13 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
return return
} }
cm, ok := s.catalog.Costumes[costume.CostumeId] cm, ok := catalog.Costumes[costume.CostumeId]
if !ok { if !ok {
log.Printf("[CostumeService] EnhanceActiveSkill: costume master id=%d not found", costume.CostumeId) log.Printf("[CostumeService] EnhanceActiveSkill: costume master id=%d not found", costume.CostumeId)
return return
} }
groupRows := s.catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId] groupRows := catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId]
enhanceMatId := int32(-1) enhanceMatId := int32(-1)
for _, g := range groupRows { for _, g := range groupRows {
if g.CostumeLimitBreakCountLowerLimit <= costume.LimitBreakCount { if g.CostumeLimitBreakCountLowerLimit <= costume.LimitBreakCount {
@@ -259,7 +268,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
skill := user.CostumeActiveSkills[req.UserCostumeUuid] skill := user.CostumeActiveSkills[req.UserCostumeUuid]
currentLevel := skill.Level currentLevel := skill.Level
maxLevelFunc, ok := s.catalog.ActiveSkillMaxLevelByRarity[cm.RarityType] maxLevelFunc, ok := catalog.ActiveSkillMaxLevelByRarity[cm.RarityType]
if !ok { if !ok {
log.Printf("[CostumeService] EnhanceActiveSkill: no max level func for rarity=%d", cm.RarityType) log.Printf("[CostumeService] EnhanceActiveSkill: no max level func for rarity=%d", cm.RarityType)
return return
@@ -277,7 +286,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
key := [2]int32{enhanceMatId, lvl} key := [2]int32{enhanceMatId, lvl}
mats := s.catalog.ActiveSkillEnhanceMats[key] mats := catalog.ActiveSkillEnhanceMats[key]
for _, mat := range mats { for _, mat := range mats {
cur := user.Materials[mat.MaterialId] cur := user.Materials[mat.MaterialId]
cost := mat.Count cost := mat.Count
@@ -288,9 +297,9 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
user.Materials[mat.MaterialId] = cur - cost user.Materials[mat.MaterialId] = cur - cost
} }
if costFunc, ok := s.catalog.ActiveSkillCostByRarity[cm.RarityType]; ok { if costFunc, ok := catalog.ActiveSkillCostByRarity[cm.RarityType]; ok {
goldCost := costFunc.Evaluate(lvl + 1) goldCost := costFunc.Evaluate(lvl + 1)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
} }
} }
@@ -310,6 +319,9 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBreakRequest) (*pb.LimitBreakResponse, error) { func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBreakRequest) (*pb.LimitBreakResponse, error) {
log.Printf("[CostumeService] LimitBreak: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials) log.Printf("[CostumeService] LimitBreak: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
cat := s.holder.Get()
catalog := cat.Costume
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -320,12 +332,12 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
return return
} }
if costume.LimitBreakCount >= s.config.CostumeLimitBreakAvailableCount { if costume.LimitBreakCount >= config.CostumeLimitBreakAvailableCount {
log.Printf("[CostumeService] LimitBreak: already at max limit break %d", costume.LimitBreakCount) log.Printf("[CostumeService] LimitBreak: already at max limit break %d", costume.LimitBreakCount)
return return
} }
cm, ok := s.catalog.Costumes[costume.CostumeId] cm, ok := catalog.Costumes[costume.CostumeId]
if !ok { if !ok {
log.Printf("[CostumeService] LimitBreak: costume master id=%d not found", costume.CostumeId) log.Printf("[CostumeService] LimitBreak: costume master id=%d not found", costume.CostumeId)
return return
@@ -342,9 +354,9 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
totalMaterialCount += count totalMaterialCount += count
} }
if costFunc, ok := s.catalog.LimitBreakCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 { if costFunc, ok := catalog.LimitBreakCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
goldCost := costFunc.Evaluate(totalMaterialCount) goldCost := costFunc.Evaluate(totalMaterialCount)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
log.Printf("[CostumeService] LimitBreak: gold cost=%d", goldCost) log.Printf("[CostumeService] LimitBreak: gold cost=%d", goldCost)
} }
@@ -363,6 +375,9 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req *pb.UnlockLotteryEffectSlotRequest) (*pb.UnlockLotteryEffectSlotResponse, error) { func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req *pb.UnlockLotteryEffectSlotRequest) (*pb.UnlockLotteryEffectSlotResponse, error) {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber) log.Printf("[CostumeService] UnlockLotteryEffectSlot: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
cat := s.holder.Get()
catalog := cat.Costume
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -373,15 +388,15 @@ func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req
return return
} }
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
if !ok { if !ok {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber) log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
return return
} }
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectUnlockSlotConsumeGold user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectUnlockSlotConsumeGold
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId] mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId]
for _, mat := range mats { for _, mat := range mats {
cur := user.Materials[mat.MaterialId] cur := user.Materials[mat.MaterialId]
cost := mat.Count cost := mat.Count
@@ -418,6 +433,9 @@ func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req
func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.DrawLotteryEffectRequest) (*pb.DrawLotteryEffectResponse, error) { func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.DrawLotteryEffectRequest) (*pb.DrawLotteryEffectResponse, error) {
log.Printf("[CostumeService] DrawLotteryEffect: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber) log.Printf("[CostumeService] DrawLotteryEffect: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
cat := s.holder.Get()
catalog := cat.Costume
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -428,21 +446,21 @@ func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.Dr
return return
} }
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}] effectRow, ok := catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
if !ok { if !ok {
log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber) log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
return return
} }
oddsPool := s.catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId] oddsPool := catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId]
if len(oddsPool) == 0 { if len(oddsPool) == 0 {
log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId) log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId)
return return
} }
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectDrawSlotConsumeGold user.ConsumableItems[config.ConsumableItemIdForGold] -= config.CostumeLotteryEffectDrawSlotConsumeGold
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId] mats := catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId]
for _, mat := range mats { for _, mat := range mats {
cur := user.Materials[mat.MaterialId] cur := user.Materials[mat.MaterialId]
cost := mat.Count cost := mat.Count
+19 -2
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"os"
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
@@ -12,6 +13,16 @@ import (
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
) )
// masterDataBinPath is the canonical location of the encrypted master data
// file. The mtime of this file is folded into the version string so the
// client invalidates its cache as soon as an admin reload swaps it in.
const masterDataBinPath = "assets/release/20240404193219.bin.e"
// masterDataBaseVersion preserves the historical "yyyymmddHHMMSS" value the
// client has always seen; we suffix it with the file mtime to force a
// re-download when content changes.
const masterDataBaseVersion = "20240404193219"
type DataServiceServer struct { type DataServiceServer struct {
pb.UnimplementedDataServiceServer pb.UnimplementedDataServiceServer
users store.UserRepository users store.UserRepository
@@ -23,9 +34,15 @@ func NewDataServiceServer(users store.UserRepository, sessions store.SessionRepo
} }
func (s *DataServiceServer) GetLatestMasterDataVersion(ctx context.Context, _ *emptypb.Empty) (*pb.MasterDataGetLatestVersionResponse, error) { func (s *DataServiceServer) GetLatestMasterDataVersion(ctx context.Context, _ *emptypb.Empty) (*pb.MasterDataGetLatestVersionResponse, error) {
log.Printf("[DataService] GetLatestMasterDataVersion") version := masterDataBaseVersion
if info, err := os.Stat(masterDataBinPath); err == nil {
version = fmt.Sprintf("%s_%d", masterDataBaseVersion, info.ModTime().UnixMilli())
} else {
log.Printf("[DataService] stat %s: %v (falling back to base version)", masterDataBinPath, err)
}
log.Printf("[DataService] GetLatestMasterDataVersion -> %s", version)
return &pb.MasterDataGetLatestVersionResponse{ return &pb.MasterDataGetLatestVersionResponse{
LatestMasterDataVersion: "20240404193219", LatestMasterDataVersion: version,
}, nil }, nil
} }
+10 -8
View File
@@ -7,8 +7,8 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -22,17 +22,18 @@ type ExploreServiceServer struct {
pb.UnimplementedExploreServiceServer pb.UnimplementedExploreServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.ExploreCatalog holder *runtime.Holder
} }
func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ExploreCatalog) *ExploreServiceServer { func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ExploreServiceServer {
return &ExploreServiceServer{users: users, sessions: sessions, catalog: catalog} return &ExploreServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartExploreRequest) (*pb.StartExploreResponse, error) { func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartExploreRequest) (*pb.StartExploreResponse, error) {
log.Printf("[ExploreService] StartExplore: exploreId=%d useConsumableItemId=%d", req.ExploreId, req.UseConsumableItemId) log.Printf("[ExploreService] StartExplore: exploreId=%d useConsumableItemId=%d", req.ExploreId, req.UseConsumableItemId)
if _, ok := s.catalog.Explores[req.ExploreId]; !ok { catalog := s.holder.Get().Explore
if _, ok := catalog.Explores[req.ExploreId]; !ok {
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId) return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
} }
@@ -40,7 +41,7 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
explore := s.catalog.Explores[req.ExploreId] explore := catalog.Explores[req.ExploreId]
if req.UseConsumableItemId > 0 && explore.ConsumeItemCount > 0 { if req.UseConsumableItemId > 0 && explore.ConsumeItemCount > 0 {
cur := user.ConsumableItems[req.UseConsumableItemId] cur := user.ConsumableItems[req.UseConsumableItemId]
user.ConsumableItems[req.UseConsumableItemId] = cur - explore.ConsumeItemCount user.ConsumableItems[req.UseConsumableItemId] = cur - explore.ConsumeItemCount
@@ -64,12 +65,13 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx
func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.FinishExploreRequest) (*pb.FinishExploreResponse, error) { func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.FinishExploreRequest) (*pb.FinishExploreResponse, error) {
log.Printf("[ExploreService] FinishExplore: exploreId=%d score=%d", req.ExploreId, req.Score) log.Printf("[ExploreService] FinishExplore: exploreId=%d score=%d", req.ExploreId, req.Score)
explore, ok := s.catalog.Explores[req.ExploreId] catalog := s.holder.Get().Explore
explore, ok := catalog.Explores[req.ExploreId]
if !ok { if !ok {
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId) return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
} }
assetGradeIconId := s.catalog.GradeForScore(req.ExploreId, req.Score) assetGradeIconId := catalog.GradeForScore(req.ExploreId, req.Score)
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
+42 -19
View File
@@ -10,6 +10,7 @@ import (
"lunar-tear/server/internal/gacha" "lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
emptypb "google.golang.org/protobuf/types/known/emptypb" emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -20,34 +21,33 @@ type GachaServiceServer struct {
pb.UnimplementedGachaServiceServer pb.UnimplementedGachaServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog []store.GachaCatalogEntry holder *runtime.Holder
handler *gacha.GachaHandler
} }
func NewGachaServiceServer( func NewGachaServiceServer(
users store.UserRepository, users store.UserRepository,
sessions store.SessionRepository, sessions store.SessionRepository,
catalog []store.GachaCatalogEntry, holder *runtime.Holder,
handler *gacha.GachaHandler,
) *GachaServiceServer { ) *GachaServiceServer {
return &GachaServiceServer{ return &GachaServiceServer{
users: users, users: users,
sessions: sessions, sessions: sessions,
catalog: catalog, holder: holder,
handler: handler,
} }
} }
func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) { func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) {
log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType) log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType)
catalog := s.catalog cat := s.holder.Get()
catalog := cat.GachaEntries
handler := cat.GachaHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
user, err := s.users.UpdateUser(userId, func(user *store.UserState) { user, err := s.users.UpdateUser(userId, func(user *store.UserState) {
user.EnsureMaps() user.EnsureMaps()
s.autoConvertExpiredMedals(user, catalog, nowMillis) autoConvertExpiredMedals(user, catalog, handler, nowMillis)
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("update user: %w", err) return nil, fmt.Errorf("update user: %w", err)
@@ -55,6 +55,9 @@ func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaL
gachaList := make([]*pb.Gacha, 0, len(catalog)) gachaList := make([]*pb.Gacha, 0, len(catalog))
for _, entry := range catalog { for _, entry := range catalog {
if !gachaActiveAt(entry, nowMillis) {
continue
}
if !matchesGachaLabel(req.GachaLabelType, entry.GachaLabelType) { if !matchesGachaLabel(req.GachaLabelType, entry.GachaLabelType) {
continue continue
} }
@@ -71,7 +74,7 @@ func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaL
}, nil }, nil
} }
func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, catalog []store.GachaCatalogEntry, nowMillis int64) { func autoConvertExpiredMedals(user *store.UserState, catalog []store.GachaCatalogEntry, handler *gacha.GachaHandler, nowMillis int64) {
for _, entry := range catalog { for _, entry := range catalog {
if entry.GachaMedalId == 0 || entry.EndDatetime == 0 { if entry.GachaMedalId == 0 || entry.EndDatetime == 0 {
continue continue
@@ -84,7 +87,7 @@ func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, cat
continue continue
} }
medalInfo, ok := s.handler.MedalInfo[entry.GachaId] medalInfo, ok := handler.MedalInfo[entry.GachaId]
if !ok { if !ok {
continue continue
} }
@@ -117,7 +120,8 @@ func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, cat
func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) { func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) {
log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId) log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId)
catalog := s.catalog catalog := s.holder.Get().GachaEntries
nowMillis := gametime.NowMillis()
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
user, err := s.users.LoadUser(userId) user, err := s.users.LoadUser(userId)
@@ -128,13 +132,17 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque
byId := make(map[int32]*pb.Gacha, len(req.GachaId)) byId := make(map[int32]*pb.Gacha, len(req.GachaId))
for _, wantedId := range req.GachaId { for _, wantedId := range req.GachaId {
for _, entry := range catalog { for _, entry := range catalog {
if entry.GachaId == wantedId { if entry.GachaId != wantedId {
continue
}
if !gachaActiveAt(entry, nowMillis) {
break
}
bs := user.Gacha.BannerStates[entry.GachaId] bs := user.Gacha.BannerStates[entry.GachaId]
byId[wantedId] = toProtoGacha(entry, &bs) byId[wantedId] = toProtoGacha(entry, &bs)
break break
} }
} }
}
return &pb.GetGachaResponse{ return &pb.GetGachaResponse{
Gacha: byId, Gacha: byId,
@@ -144,10 +152,12 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque
func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) { func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) {
log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount) log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount)
entry := findCatalogEntry(s.catalog, req.GachaId) cat := s.holder.Get()
entry := findCatalogEntry(cat.GachaEntries, req.GachaId)
if entry == nil { if entry == nil {
return nil, fmt.Errorf("gacha %d not found", req.GachaId) return nil, fmt.Errorf("gacha %d not found", req.GachaId)
} }
handler := cat.GachaHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
execCount := req.ExecCount execCount := req.ExecCount
@@ -158,7 +168,7 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
var drawResult *gacha.DrawResult var drawResult *gacha.DrawResult
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
var drawErr error var drawErr error
drawResult, drawErr = s.handler.HandleDraw(user, *entry, req.GachaPricePhaseId, execCount) drawResult, drawErr = handler.HandleDraw(user, *entry, req.GachaPricePhaseId, execCount)
if drawErr != nil { if drawErr != nil {
log.Printf("[GachaService] Draw error: %v", drawErr) log.Printf("[GachaService] Draw error: %v", drawErr)
drawResult = &gacha.DrawResult{} drawResult = &gacha.DrawResult{}
@@ -285,14 +295,16 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) { func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) {
log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId) log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId)
entry := findCatalogEntry(s.catalog, req.GachaId) cat := s.holder.Get()
entry := findCatalogEntry(cat.GachaEntries, req.GachaId)
if entry == nil { if entry == nil {
return nil, fmt.Errorf("gacha %d not found", req.GachaId) return nil, fmt.Errorf("gacha %d not found", req.GachaId)
} }
handler := cat.GachaHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
if resetErr := s.handler.HandleResetBox(user, *entry); resetErr != nil { if resetErr := handler.HandleResetBox(user, *entry); resetErr != nil {
log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr) log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr)
} }
}) })
@@ -315,7 +327,7 @@ func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Em
return nil, fmt.Errorf("snapshot user: %w", err) return nil, fmt.Errorf("snapshot user: %w", err)
} }
maxCount := s.handler.Config.RewardGachaDailyMaxCount maxCount := s.holder.Get().GachaHandler.Config.RewardGachaDailyMaxCount
if maxCount <= 0 { if maxCount <= 0 {
maxCount = model.DefaultDailyDrawLimit maxCount = model.DefaultDailyDrawLimit
} }
@@ -337,11 +349,12 @@ func (s *GachaServiceServer) RewardDraw(ctx context.Context, req *pb.RewardDrawR
log.Printf("[GachaService] RewardDraw: placement=%q reward=%q amount=%q", req.PlacementName, req.RewardName, req.RewardAmount) log.Printf("[GachaService] RewardDraw: placement=%q reward=%q amount=%q", req.PlacementName, req.RewardName, req.RewardAmount)
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
handler := s.holder.Get().GachaHandler
var items []gacha.DrawnItem var items []gacha.DrawnItem
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) { updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
var drawErr error var drawErr error
items, drawErr = s.handler.HandleRewardDraw(user, 1) items, drawErr = handler.HandleRewardDraw(user, 1)
if drawErr != nil { if drawErr != nil {
log.Printf("[GachaService] RewardDraw error: %v", drawErr) log.Printf("[GachaService] RewardDraw error: %v", drawErr)
} }
@@ -395,6 +408,16 @@ func matchesGachaLabel(labels []int32, label int32) bool {
return false return false
} }
func gachaActiveAt(entry store.GachaCatalogEntry, nowMillis int64) bool {
if entry.StartDatetime != 0 && nowMillis < entry.StartDatetime {
return false
}
if entry.EndDatetime != 0 && nowMillis >= entry.EndDatetime {
return false
}
return true
}
func toProtoGacha(entry store.GachaCatalogEntry, bs *store.GachaBannerState) *pb.Gacha { func toProtoGacha(entry store.GachaCatalogEntry, bs *store.GachaBannerState) *pb.Gacha {
g := &pb.Gacha{ g := &pb.Gacha{
GachaId: entry.GachaId, GachaId: entry.GachaId,
+5 -5
View File
@@ -6,7 +6,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
emptypb "google.golang.org/protobuf/types/known/emptypb" emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -16,11 +16,11 @@ type GimmickServiceServer struct {
pb.UnimplementedGimmickServiceServer pb.UnimplementedGimmickServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
gimmickCatalog *masterdata.GimmickCatalog holder *runtime.Holder
} }
func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, gimmickCatalog *masterdata.GimmickCatalog) *GimmickServiceServer { func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *GimmickServiceServer {
return &GimmickServiceServer{users: users, sessions: sessions, gimmickCatalog: gimmickCatalog} return &GimmickServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) { func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) {
@@ -80,7 +80,7 @@ func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *empt
now := gametime.NowMillis() now := gametime.NowMillis()
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
added := 0 added := 0
for _, key := range s.gimmickCatalog.ActiveScheduleKeys(*user, now) { for _, key := range s.holder.Get().Gimmick.ActiveScheduleKeys(*user, now) {
if _, exists := user.Gimmick.Sequences[key]; !exists { if _, exists := user.Gimmick.Sequences[key]; !exists {
user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key} user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key}
added++ added++
+6 -5
View File
@@ -9,7 +9,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
emptypb "google.golang.org/protobuf/types/known/emptypb" emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -19,22 +19,23 @@ type LoginBonusServiceServer struct {
pb.UnimplementedLoginBonusServiceServer pb.UnimplementedLoginBonusServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.LoginBonusCatalog holder *runtime.Holder
} }
func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.LoginBonusCatalog) *LoginBonusServiceServer { func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *LoginBonusServiceServer {
return &LoginBonusServiceServer{users: users, sessions: sessions, catalog: catalog} return &LoginBonusServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) { func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) {
log.Printf("[LoginBonusService] ReceiveStamp") log.Printf("[LoginBonusService] ReceiveStamp")
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
catalog := s.holder.Get().LoginBonus
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
now := gametime.NowMillis() now := gametime.NowMillis()
nextStamp := user.LoginBonus.CurrentStampNumber + 1 nextStamp := user.LoginBonus.CurrentStampNumber + 1
reward, ok := s.catalog.LookupStampReward( reward, ok := catalog.LookupStampReward(
user.LoginBonus.LoginBonusId, user.LoginBonus.LoginBonusId,
user.LoginBonus.CurrentPageNumber, user.LoginBonus.CurrentPageNumber,
nextStamp, nextStamp,
+9 -7
View File
@@ -6,7 +6,7 @@ import (
"log" "log"
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -14,23 +14,25 @@ type MaterialServiceServer struct {
pb.UnimplementedMaterialServiceServer pb.UnimplementedMaterialServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.MaterialCatalog holder *runtime.Holder
config *masterdata.GameConfig
} }
func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.MaterialCatalog, config *masterdata.GameConfig) *MaterialServiceServer { func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *MaterialServiceServer {
return &MaterialServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} return &MaterialServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) { func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) {
log.Printf("[MaterialService] Sell: %d item(s)", len(req.MaterialPossession)) log.Printf("[MaterialService] Sell: %d item(s)", len(req.MaterialPossession))
cat := s.holder.Get()
catalog := cat.Material
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
totalGold := int32(0) totalGold := int32(0)
for _, item := range req.MaterialPossession { for _, item := range req.MaterialPossession {
mat, ok := s.catalog.All[item.MaterialId] mat, ok := catalog.All[item.MaterialId]
if !ok { if !ok {
log.Printf("[MaterialService] Sell: unknown materialId=%d, skipping", item.MaterialId) log.Printf("[MaterialService] Sell: unknown materialId=%d, skipping", item.MaterialId)
continue continue
@@ -53,7 +55,7 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe
} }
if totalGold > 0 { if totalGold > 0 {
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
log.Printf("[MaterialService] Sell: total gold +%d", totalGold) log.Printf("[MaterialService] Sell: total gold +%d", totalGold)
} }
}) })
+6 -18
View File
@@ -414,24 +414,12 @@ func (s *OctoHTTPServer) serveListBin(w http.ResponseWriter, filePath string) {
w.Write(data) w.Write(data)
} }
// serveDatabaseBinE serves MasterMemory database: /assets/release/{version}/database.bin.e // serveDatabaseBinE serves the master data binary. The URL's {version} segment
// -> assets/release/{version}.bin.e (or assets/release/database.bin.e fallback). // is a cache key (it changes whenever the file's mtime changes, see
func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, path string) { // DataService.GetLatestMasterDataVersion) but does not select a different file —
parts := strings.Split(path, "/") // there's only ever one bin.e on disk.
var version string func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, _ string) {
for i, p := range parts { filePath := filepath.Join(s.BaseDir, "assets", "release", "20240404193219.bin.e")
if p == "release" && i+1 < len(parts) {
version = parts[i+1]
break
}
}
filePath := filepath.Join(s.BaseDir, "assets", "release", "database.bin.e")
if version != "" {
vPath := filepath.Join(s.BaseDir, "assets", "release", version+".bin.e")
if _, err := os.Stat(vPath); err == nil {
filePath = vPath
}
}
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, filePath) http.ServeFile(w, r, filePath)
} }
+5 -5
View File
@@ -7,7 +7,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -15,11 +15,11 @@ type OmikujiServiceServer struct {
pb.UnimplementedOmikujiServiceServer pb.UnimplementedOmikujiServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.OmikujiCatalog holder *runtime.Holder
} }
func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.OmikujiCatalog) *OmikujiServiceServer { func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *OmikujiServiceServer {
return &OmikujiServiceServer{users: users, sessions: sessions, catalog: catalog} return &OmikujiServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiDrawRequest) (*pb.OmikujiDrawResponse, error) { func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiDrawRequest) (*pb.OmikujiDrawResponse, error) {
@@ -36,7 +36,7 @@ func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiD
} }
return &pb.OmikujiDrawResponse{ return &pb.OmikujiDrawResponse{
OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId), OmikujiResultAssetId: s.holder.Get().Omikuji.LookupAssetId(req.OmikujiId),
OmikujiItem: []*pb.OmikujiItem{}, OmikujiItem: []*pb.OmikujiItem{},
}, nil }, nil
} }
+25 -19
View File
@@ -9,6 +9,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -18,17 +19,19 @@ type PartsServiceServer struct {
pb.UnimplementedPartsServiceServer pb.UnimplementedPartsServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.PartsCatalog holder *runtime.Holder
config *masterdata.GameConfig
} }
func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.PartsCatalog, config *masterdata.GameConfig) *PartsServiceServer { func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *PartsServiceServer {
return &PartsServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} return &PartsServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) { func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) {
log.Printf("[PartsService] Sell: %d part(s)", len(req.UserPartsUuid)) log.Printf("[PartsService] Sell: %d part(s)", len(req.UserPartsUuid))
cat := s.holder.Get()
catalog := cat.Parts
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
@@ -44,13 +47,13 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
continue continue
} }
partDef, ok := s.catalog.PartsById[part.PartsId] partDef, ok := catalog.PartsById[part.PartsId]
if !ok { if !ok {
log.Printf("[PartsService] Sell: partsId=%d not in catalog, skipping", part.PartsId) log.Printf("[PartsService] Sell: partsId=%d not in catalog, skipping", part.PartsId)
continue continue
} }
sellFunc, ok := s.catalog.SellPriceByRarity[partDef.RarityType] sellFunc, ok := catalog.SellPriceByRarity[partDef.RarityType]
if !ok { if !ok {
log.Printf("[PartsService] Sell: no sell price func for rarity=%d, skipping", partDef.RarityType) log.Printf("[PartsService] Sell: no sell price func for rarity=%d, skipping", partDef.RarityType)
continue continue
@@ -68,7 +71,7 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
} }
if totalGold > 0 { if totalGold > 0 {
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
log.Printf("[PartsService] Sell: total gold +%d", totalGold) log.Printf("[PartsService] Sell: total gold +%d", totalGold)
} }
}) })
@@ -82,6 +85,9 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) { func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) {
log.Printf("[PartsService] Enhance: uuid=%s", req.UserPartsUuid) log.Printf("[PartsService] Enhance: uuid=%s", req.UserPartsUuid)
cat := s.holder.Get()
catalog := cat.Parts
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -99,33 +105,33 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
return return
} }
partDef, ok := s.catalog.PartsById[part.PartsId] partDef, ok := catalog.PartsById[part.PartsId]
if !ok { if !ok {
log.Printf("[PartsService] Enhance: part master id=%d not found", part.PartsId) log.Printf("[PartsService] Enhance: part master id=%d not found", part.PartsId)
return return
} }
rarity, ok := s.catalog.RarityByRarityType[partDef.RarityType] rarity, ok := catalog.RarityByRarityType[partDef.RarityType]
if !ok { if !ok {
log.Printf("[PartsService] Enhance: rarity type=%d not found", partDef.RarityType) log.Printf("[PartsService] Enhance: rarity type=%d not found", partDef.RarityType)
return return
} }
goldCost := int32(0) goldCost := int32(0)
if prices, ok := s.catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok { if prices, ok := catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok {
goldCost = prices[part.Level] goldCost = prices[part.Level]
} }
currentGold := user.ConsumableItems[s.config.ConsumableItemIdForGold] currentGold := user.ConsumableItems[config.ConsumableItemIdForGold]
if currentGold < goldCost { if currentGold < goldCost {
log.Printf("[PartsService] Enhance: insufficient gold have=%d need=%d", currentGold, goldCost) log.Printf("[PartsService] Enhance: insufficient gold have=%d need=%d", currentGold, goldCost)
return return
} }
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
successRate := int32(1000) successRate := int32(1000)
if rates, ok := s.catalog.RateByGroupAndLevel[rarity.PartsLevelUpRateGroupId]; ok { if rates, ok := catalog.RateByGroupAndLevel[rarity.PartsLevelUpRateGroupId]; ok {
if r, ok := rates[part.Level]; ok { if r, ok := rates[part.Level]; ok {
successRate = r successRate = r
} }
@@ -137,7 +143,7 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)", log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)",
part.PartsId, part.Level-1, part.Level, successRate, goldCost) part.PartsId, part.Level-1, part.Level, successRate, goldCost)
s.grantSubStatuses(user, req.UserPartsUuid, part, partDef, nowMillis) grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis)
} else { } else {
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)", log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)",
part.PartsId, part.Level, successRate, goldCost) part.PartsId, part.Level, successRate, goldCost)
@@ -155,9 +161,9 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
}, nil }, nil
} }
func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) { func grantPartsSubStatuses(catalog *masterdata.PartsCatalog, user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) {
unlockLevels := s.catalog.SubStatusUnlockLvls[partDef.RarityType] unlockLevels := catalog.SubStatusUnlockLvls[partDef.RarityType]
pool := s.catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId] pool := catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId]
if len(pool) == 0 { if len(pool) == 0 {
return return
} }
@@ -173,13 +179,13 @@ func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string
} }
pick := pool[rand.Intn(len(pool))] pick := pool[rand.Intn(len(pool))]
def, ok := s.catalog.PartsStatusMainById[pick] def, ok := catalog.PartsStatusMainById[pick]
if !ok { if !ok {
continue continue
} }
statusValue := def.StatusChangeInitialValue statusValue := def.StatusChangeInitialValue
if f, ok := s.catalog.FuncResolver.Resolve(def.StatusNumericalFunctionId); ok { if f, ok := catalog.FuncResolver.Resolve(def.StatusNumericalFunctionId); ok {
statusValue = f.Evaluate(part.Level) statusValue = f.Evaluate(part.Level)
} }
+36 -28
View File
@@ -8,7 +8,7 @@ import (
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/questflow" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
emptypb "google.golang.org/protobuf/types/known/emptypb" emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -18,34 +18,35 @@ type BigHuntServiceServer struct {
pb.UnimplementedBigHuntServiceServer pb.UnimplementedBigHuntServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.BigHuntCatalog holder *runtime.Holder
engine *questflow.QuestHandler
} }
func NewBigHuntServiceServer( func NewBigHuntServiceServer(
users store.UserRepository, users store.UserRepository,
sessions store.SessionRepository, sessions store.SessionRepository,
catalog *masterdata.BigHuntCatalog, holder *runtime.Holder,
engine *questflow.QuestHandler,
) *BigHuntServiceServer { ) *BigHuntServiceServer {
return &BigHuntServiceServer{users: users, sessions: sessions, catalog: catalog, engine: engine} return &BigHuntServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.StartBigHuntQuestRequest) (*pb.StartBigHuntQuestResponse, error) { func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.StartBigHuntQuestRequest) (*pb.StartBigHuntQuestResponse, error) {
log.Printf("[BigHuntService] StartBigHuntQuest: bossQuestId=%d questId=%d deckNumber=%d isDryRun=%v", log.Printf("[BigHuntService] StartBigHuntQuest: bossQuestId=%d questId=%d deckNumber=%d isDryRun=%v",
req.BigHuntBossQuestId, req.BigHuntQuestId, req.UserDeckNumber, req.IsDryRun) req.BigHuntBossQuestId, req.BigHuntQuestId, req.UserDeckNumber, req.IsDryRun)
cat := s.holder.Get()
catalog := cat.BigHunt
engine := cat.QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
bhQuest, ok := s.catalog.QuestById[req.BigHuntQuestId] bhQuest, ok := catalog.QuestById[req.BigHuntQuestId]
if !ok { if !ok {
log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId) log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId)
} }
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
if ok { if ok {
s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis) engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis)
} }
user.BigHuntProgress = store.BigHuntProgress{ user.BigHuntProgress = store.BigHuntProgress{
@@ -85,18 +86,21 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v", log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v",
req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired) req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired)
cat := s.holder.Get()
catalog := cat.BigHunt
engine := cat.QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
bhQuest := s.catalog.QuestById[req.BigHuntQuestId] bhQuest := catalog.QuestById[req.BigHuntQuestId]
bossQuest := s.catalog.BossQuestById[req.BigHuntBossQuestId] bossQuest := catalog.BossQuestById[req.BigHuntBossQuestId]
boss := s.catalog.BossByBossId[bossQuest.BigHuntBossId] boss := catalog.BossByBossId[bossQuest.BigHuntBossId]
var scoreInfo *pb.BigHuntScoreInfo var scoreInfo *pb.BigHuntScoreInfo
var scoreRewards []*pb.BigHuntReward var scoreRewards []*pb.BigHuntReward
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleBigHuntQuestFinish(user, bhQuest.QuestId, req.IsRetired, false, nowMillis) engine.HandleBigHuntQuestFinish(user, bhQuest.QuestId, req.IsRetired, false, nowMillis)
if req.IsRetired || user.BigHuntProgress.IsDryRun { if req.IsRetired || user.BigHuntProgress.IsDryRun {
user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis} user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis}
@@ -108,7 +112,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
baseScore := totalDamage baseScore := totalDamage
difficultyBonusPermil := int32(0) difficultyBonusPermil := int32(0)
if coeff, ok := s.catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok { if coeff, ok := catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok {
difficultyBonusPermil = coeff difficultyBonusPermil = coeff
} }
@@ -138,7 +142,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
} }
schedKey := store.BigHuntScheduleScoreKey{ schedKey := store.BigHuntScheduleScoreKey{
BigHuntScheduleId: s.catalog.ActiveScheduleId, BigHuntScheduleId: catalog.ActiveScheduleId,
BigHuntBossId: bossQuest.BigHuntBossId, BigHuntBossId: bossQuest.BigHuntBossId,
} }
oldSchedMax := user.BigHuntScheduleMaxScores[schedKey].MaxScore oldSchedMax := user.BigHuntScheduleMaxScores[schedKey].MaxScore
@@ -163,7 +167,7 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
} }
} }
assetGradeIconId := s.catalog.ResolveGradeIconId(bossQuest.BigHuntBossId, userScore) assetGradeIconId := catalog.ResolveGradeIconId(bossQuest.BigHuntBossId, userScore)
scoreInfo = &pb.BigHuntScoreInfo{ scoreInfo = &pb.BigHuntScoreInfo{
UserScore: userScore, UserScore: userScore,
@@ -177,12 +181,12 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
} }
if isHighScore { if isHighScore {
rewardGroupId := s.catalog.ResolveActiveScoreRewardGroupId( rewardGroupId := catalog.ResolveActiveScoreRewardGroupId(
bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis) bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis)
if rewardGroupId > 0 { if rewardGroupId > 0 {
newItems := s.catalog.CollectNewRewards(rewardGroupId, oldMax, userScore) newItems := catalog.CollectNewRewards(rewardGroupId, oldMax, userScore)
for _, item := range newItems { for _, item := range newItems {
s.engine.Granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis) engine.Granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
scoreRewards = append(scoreRewards, &pb.BigHuntReward{ scoreRewards = append(scoreRewards, &pb.BigHuntReward{
PossessionType: item.PossessionType, PossessionType: item.PossessionType,
PossessionId: item.PossessionId, PossessionId: item.PossessionId,
@@ -216,16 +220,19 @@ func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.F
func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb.RestartBigHuntQuestRequest) (*pb.RestartBigHuntQuestResponse, error) { func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb.RestartBigHuntQuestRequest) (*pb.RestartBigHuntQuestResponse, error) {
log.Printf("[BigHuntService] RestartBigHuntQuest: bossQuestId=%d questId=%d", req.BigHuntBossQuestId, req.BigHuntQuestId) log.Printf("[BigHuntService] RestartBigHuntQuest: bossQuestId=%d questId=%d", req.BigHuntBossQuestId, req.BigHuntQuestId)
cat := s.holder.Get()
catalog := cat.BigHunt
engine := cat.QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
bhQuest := s.catalog.QuestById[req.BigHuntQuestId] bhQuest := catalog.QuestById[req.BigHuntQuestId]
var battleBinary []byte var battleBinary []byte
var deckNumber int32 var deckNumber int32
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, user.BigHuntDeckNumber, nowMillis) engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, user.BigHuntDeckNumber, nowMillis)
user.BigHuntProgress.CurrentQuestSceneId = 0 user.BigHuntProgress.CurrentQuestSceneId = 0
user.BigHuntProgress.LatestVersion = nowMillis user.BigHuntProgress.LatestVersion = nowMillis
@@ -302,6 +309,7 @@ func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *p
func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) { func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) {
log.Printf("[BigHuntService] GetBigHuntTopData") log.Printf("[BigHuntService] GetBigHuntTopData")
catalog := s.holder.Get().BigHunt
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
user, _ := s.users.LoadUser(userId) user, _ := s.users.LoadUser(userId)
@@ -309,13 +317,13 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
weeklyVersion := gametime.WeeklyVersion(nowMillis) weeklyVersion := gametime.WeeklyVersion(nowMillis)
var weeklyScoreResults []*pb.WeeklyScoreResult var weeklyScoreResults []*pb.WeeklyScoreResult
for _, boss := range s.catalog.BossByBossId { for _, boss := range catalog.BossByBossId {
key := store.BigHuntWeeklyScoreKey{ key := store.BigHuntWeeklyScoreKey{
BigHuntWeeklyVersion: weeklyVersion, BigHuntWeeklyVersion: weeklyVersion,
AttributeType: boss.AttributeType, AttributeType: boss.AttributeType,
} }
ws := user.BigHuntWeeklyMaxScores[key] ws := user.BigHuntWeeklyMaxScores[key]
gradeIconId := s.catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore) gradeIconId := catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore)
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{ weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
AttributeType: boss.AttributeType, AttributeType: boss.AttributeType,
@@ -330,10 +338,10 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
ws := user.BigHuntWeeklyStatuses[weeklyVersion] ws := user.BigHuntWeeklyStatuses[weeklyVersion]
weeklyRewards := s.resolveWeeklyRewards(user, weeklyVersion, nowMillis) weeklyRewards := resolveBigHuntWeeklyRewards(catalog, user, weeklyVersion, nowMillis)
lastWeekVersion := weeklyVersion - 7*24*60*60*1000 lastWeekVersion := weeklyVersion - 7*24*60*60*1000
lastWeekRewards := s.resolveWeeklyRewards(user, lastWeekVersion, nowMillis) lastWeekRewards := resolveBigHuntWeeklyRewards(catalog, user, lastWeekVersion, nowMillis)
return &pb.GetBigHuntTopDataResponse{ return &pb.GetBigHuntTopDataResponse{
WeeklyScoreResult: weeklyScoreResults, WeeklyScoreResult: weeklyScoreResults,
@@ -343,14 +351,14 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
}, nil }, nil
} }
func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward { func resolveBigHuntWeeklyRewards(catalog *masterdata.BigHuntCatalog, user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward {
var rewards []*pb.BigHuntReward var rewards []*pb.BigHuntReward
for _, boss := range s.catalog.BossByBossId { for _, boss := range catalog.BossByBossId {
rewardKey := masterdata.BigHuntWeeklyRewardKey{ rewardKey := masterdata.BigHuntWeeklyRewardKey{
ScheduleId: 1, ScheduleId: 1,
AttributeType: boss.AttributeType, AttributeType: boss.AttributeType,
} }
rewardGroupId := s.catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) rewardGroupId := catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis)
if rewardGroupId == 0 { if rewardGroupId == 0 {
continue continue
} }
@@ -359,7 +367,7 @@ func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weekly
AttributeType: boss.AttributeType, AttributeType: boss.AttributeType,
} }
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
for _, item := range s.catalog.CollectNewRewards(rewardGroupId, 0, maxScore) { for _, item := range catalog.CollectNewRewards(rewardGroupId, 0, maxScore) {
rewards = append(rewards, &pb.BigHuntReward{ rewards = append(rewards, &pb.BigHuntReward{
PossessionType: item.PossessionType, PossessionType: item.PossessionType,
PossessionId: item.PossessionId, PossessionId: item.PossessionId,
+9 -5
View File
@@ -15,13 +15,14 @@ import (
func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) { func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) {
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly) log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
}) })
drops := s.engine.BattleDropRewards(req.QuestId) drops := engine.BattleDropRewards(req.QuestId)
pbDrops := make([]*pb.BattleDropReward, len(drops)) pbDrops := make([]*pb.BattleDropReward, len(drops))
for i, d := range drops { for i, d := range drops {
pbDrops[i] = &pb.BattleDropReward{ pbDrops[i] = &pb.BattleDropReward{
@@ -40,10 +41,11 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated) log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
var outcome questflow.FinishOutcome var outcome questflow.FinishOutcome
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
outcome = s.engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) outcome = engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
}) })
return &pb.FinishEventQuestResponse{ return &pb.FinishEventQuestResponse{
@@ -61,9 +63,10 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis
func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.RestartEventQuestRequest) (*pb.RestartEventQuestResponse, error) { func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.RestartEventQuestRequest) (*pb.RestartEventQuestResponse, error) {
log.Printf("[QuestService] RestartEventQuest: chapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId) log.Printf("[QuestService] RestartEventQuest: chapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis()) engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis())
}) })
return &pb.RestartEventQuestResponse{ return &pb.RestartEventQuestResponse{
@@ -74,9 +77,10 @@ func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.Rest
func (s *QuestServiceServer) UpdateEventQuestSceneProgress(ctx context.Context, req *pb.UpdateEventQuestSceneProgressRequest) (*pb.UpdateEventQuestSceneProgressResponse, error) { func (s *QuestServiceServer) UpdateEventQuestSceneProgress(ctx context.Context, req *pb.UpdateEventQuestSceneProgressRequest) (*pb.UpdateEventQuestSceneProgressResponse, error) {
log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId) log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
}) })
return &pb.UpdateEventQuestSceneProgressResponse{}, nil return &pb.UpdateEventQuestSceneProgressResponse{}, nil
+10 -6
View File
@@ -13,13 +13,14 @@ import (
func (s *QuestServiceServer) StartExtraQuest(ctx context.Context, req *pb.StartExtraQuestRequest) (*pb.StartExtraQuestResponse, error) { func (s *QuestServiceServer) StartExtraQuest(ctx context.Context, req *pb.StartExtraQuestRequest) (*pb.StartExtraQuestResponse, error) {
log.Printf("[QuestService] StartExtraQuest: questId=%d deckNumber=%d", req.QuestId, req.UserDeckNumber) log.Printf("[QuestService] StartExtraQuest: questId=%d deckNumber=%d", req.QuestId, req.UserDeckNumber)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleExtraQuestStart(user, req.QuestId, req.UserDeckNumber, nowMillis) engine.HandleExtraQuestStart(user, req.QuestId, req.UserDeckNumber, nowMillis)
}) })
drops := s.engine.BattleDropRewards(req.QuestId) drops := engine.BattleDropRewards(req.QuestId)
pbDrops := make([]*pb.BattleDropReward, len(drops)) pbDrops := make([]*pb.BattleDropReward, len(drops))
for i, d := range drops { for i, d := range drops {
pbDrops[i] = &pb.BattleDropReward{ pbDrops[i] = &pb.BattleDropReward{
@@ -38,10 +39,11 @@ func (s *QuestServiceServer) FinishExtraQuest(ctx context.Context, req *pb.Finis
log.Printf("[QuestService] FinishExtraQuest: questId=%d isRetired=%v isAnnihilated=%v", req.QuestId, req.IsRetired, req.IsAnnihilated) log.Printf("[QuestService] FinishExtraQuest: questId=%d isRetired=%v isAnnihilated=%v", req.QuestId, req.IsRetired, req.IsAnnihilated)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
var outcome questflow.FinishOutcome var outcome questflow.FinishOutcome
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
outcome = s.engine.HandleExtraQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) outcome = engine.HandleExtraQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
}) })
return &pb.FinishExtraQuestResponse{ return &pb.FinishExtraQuestResponse{
@@ -58,14 +60,15 @@ func (s *QuestServiceServer) FinishExtraQuest(ctx context.Context, req *pb.Finis
func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.RestartExtraQuestRequest) (*pb.RestartExtraQuestResponse, error) { func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.RestartExtraQuestRequest) (*pb.RestartExtraQuestResponse, error) {
log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId) log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
var deckNumber int32 var deckNumber int32
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis()) engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis())
deckNumber = user.Quests[req.QuestId].UserDeckNumber deckNumber = user.Quests[req.QuestId].UserDeckNumber
}) })
drops := s.engine.BattleDropRewards(req.QuestId) drops := engine.BattleDropRewards(req.QuestId)
pbDrops := make([]*pb.BattleDropReward, len(drops)) pbDrops := make([]*pb.BattleDropReward, len(drops))
for i, d := range drops { for i, d := range drops {
pbDrops[i] = &pb.BattleDropReward{ pbDrops[i] = &pb.BattleDropReward{
@@ -84,9 +87,10 @@ func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.Rest
func (s *QuestServiceServer) UpdateExtraQuestSceneProgress(ctx context.Context, req *pb.UpdateExtraQuestSceneProgressRequest) (*pb.UpdateExtraQuestSceneProgressResponse, error) { func (s *QuestServiceServer) UpdateExtraQuestSceneProgress(ctx context.Context, req *pb.UpdateExtraQuestSceneProgressRequest) (*pb.UpdateExtraQuestSceneProgressResponse, error) {
log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId) log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
}) })
return &pb.UpdateExtraQuestSceneProgressResponse{}, nil return &pb.UpdateExtraQuestSceneProgressResponse{}, nil
+25 -16
View File
@@ -8,6 +8,7 @@ import (
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/questflow" "lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
emptypb "google.golang.org/protobuf/types/known/emptypb" emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -17,22 +18,23 @@ type QuestServiceServer struct {
pb.UnimplementedQuestServiceServer pb.UnimplementedQuestServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
engine *questflow.QuestHandler holder *runtime.Holder
} }
func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *QuestServiceServer { func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *QuestServiceServer {
if engine == nil { if holder == nil {
panic("quest handler is required") panic("runtime holder is required")
} }
return &QuestServiceServer{users: users, sessions: sessions, engine: engine} return &QuestServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, req *pb.UpdateMainFlowSceneProgressRequest) (*pb.UpdateMainFlowSceneProgressResponse, error) { func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, req *pb.UpdateMainFlowSceneProgressRequest) (*pb.UpdateMainFlowSceneProgressResponse, error) {
log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId) log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
}) })
return &pb.UpdateMainFlowSceneProgressResponse{}, nil return &pb.UpdateMainFlowSceneProgressResponse{}, nil
@@ -41,9 +43,10 @@ func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, re
func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, req *pb.UpdateReplayFlowSceneProgressRequest) (*pb.UpdateReplayFlowSceneProgressResponse, error) { func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, req *pb.UpdateReplayFlowSceneProgressRequest) (*pb.UpdateReplayFlowSceneProgressResponse, error) {
log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId) log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis()) engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
}) })
return &pb.UpdateReplayFlowSceneProgressResponse{}, nil return &pb.UpdateReplayFlowSceneProgressResponse{}, nil
@@ -52,9 +55,10 @@ func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context,
func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) { func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) {
log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId) log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleMainQuestSceneProgress(user, req.QuestSceneId) engine.HandleMainQuestSceneProgress(user, req.QuestSceneId)
}) })
return &pb.UpdateMainQuestSceneProgressResponse{}, nil return &pb.UpdateMainQuestSceneProgressResponse{}, nil
@@ -63,17 +67,18 @@ func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, r
func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) { func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) {
log.Printf("[QuestService] StartMainQuest: %+v", req) log.Printf("[QuestService] StartMainQuest: %+v", req)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
if req.IsReplayFlow { if req.IsReplayFlow {
s.engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
} else { } else {
s.engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis) engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
} }
}) })
drops := s.engine.BattleDropRewards(req.QuestId) drops := engine.BattleDropRewards(req.QuestId)
pbDrops := make([]*pb.BattleDropReward, len(drops)) pbDrops := make([]*pb.BattleDropReward, len(drops))
for i, d := range drops { for i, d := range drops {
pbDrops[i] = &pb.BattleDropReward{ pbDrops[i] = &pb.BattleDropReward{
@@ -108,10 +113,11 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType) req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
var outcome questflow.FinishOutcome var outcome questflow.FinishOutcome
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
outcome = s.engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis) outcome = engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
}) })
return &pb.FinishMainQuestResponse{ return &pb.FinishMainQuestResponse{
@@ -130,14 +136,15 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish
func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.RestartMainQuestRequest) (*pb.RestartMainQuestResponse, error) { func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.RestartMainQuestRequest) (*pb.RestartMainQuestResponse, error) {
log.Printf("[QuestService] RestartMainQuest: questId=%d isMainFlow=%v", req.QuestId, req.IsMainFlow) log.Printf("[QuestService] RestartMainQuest: questId=%d isMainFlow=%v", req.QuestId, req.IsMainFlow)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
var deckNumber int32 var deckNumber int32
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
s.engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis()) engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis())
deckNumber = user.Quests[req.QuestId].UserDeckNumber deckNumber = user.Quests[req.QuestId].UserDeckNumber
}) })
drops := s.engine.BattleDropRewards(req.QuestId) drops := engine.BattleDropRewards(req.QuestId)
pbDrops := make([]*pb.BattleDropReward, len(drops)) pbDrops := make([]*pb.BattleDropReward, len(drops))
for i, d := range drops { for i, d := range drops {
pbDrops[i] = &pb.BattleDropReward{ pbDrops[i] = &pb.BattleDropReward{
@@ -162,6 +169,7 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq
log.Printf("[QuestService] SkipQuest: questId=%d skipCount=%d useEffectItems=%d", req.QuestId, req.SkipCount, len(req.UseEffectItem)) log.Printf("[QuestService] SkipQuest: questId=%d skipCount=%d useEffectItems=%d", req.QuestId, req.SkipCount, len(req.UseEffectItem))
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
var outcome questflow.FinishOutcome var outcome questflow.FinishOutcome
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
@@ -172,7 +180,7 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq
user.ConsumableItems[item.ConsumableItemId] = 0 user.ConsumableItems[item.ConsumableItemId] = 0
} }
} }
outcome = s.engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis) outcome = engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis)
}) })
return &pb.SkipQuestResponse{ return &pb.SkipQuestResponse{
@@ -184,10 +192,11 @@ func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestReq
func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) { func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) {
log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId) log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
if seasonId, ok := s.engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok { if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
user.MainQuest.MainQuestSeasonId = seasonId user.MainQuest.MainQuestSeasonId = seasonId
} }
now := gametime.NowMillis() now := gametime.NowMillis()
+5 -5
View File
@@ -6,8 +6,8 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -15,11 +15,11 @@ type SideStoryQuestServiceServer struct {
pb.UnimplementedSideStoryQuestServiceServer pb.UnimplementedSideStoryQuestServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.SideStoryCatalog holder *runtime.Holder
} }
func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.SideStoryCatalog) *SideStoryQuestServiceServer { func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *SideStoryQuestServiceServer {
return &SideStoryQuestServiceServer{users: users, sessions: sessions, catalog: catalog} return &SideStoryQuestServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) { func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) {
@@ -27,7 +27,7 @@ func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Con
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
firstSceneId := s.catalog.FirstSceneByQuestId[req.SideStoryQuestId] firstSceneId := s.holder.Get().SideStory.FirstSceneByQuestId[req.SideStoryQuestId]
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
existing, exists := user.SideStoryQuests[req.SideStoryQuestId] existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
+13 -11
View File
@@ -8,6 +8,7 @@ import (
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
emptypb "google.golang.org/protobuf/types/known/emptypb" emptypb "google.golang.org/protobuf/types/known/emptypb"
@@ -17,22 +18,23 @@ type RewardServiceServer struct {
pb.UnimplementedRewardServiceServer pb.UnimplementedRewardServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
bhCatalog *masterdata.BigHuntCatalog holder *runtime.Holder
granter *store.PossessionGranter
} }
func NewRewardServiceServer( func NewRewardServiceServer(
users store.UserRepository, users store.UserRepository,
sessions store.SessionRepository, sessions store.SessionRepository,
bhCatalog *masterdata.BigHuntCatalog, holder *runtime.Holder,
granter *store.PossessionGranter,
) *RewardServiceServer { ) *RewardServiceServer {
return &RewardServiceServer{users: users, sessions: sessions, bhCatalog: bhCatalog, granter: granter} return &RewardServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveBigHuntRewardResponse, error) { func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveBigHuntRewardResponse, error) {
log.Printf("[RewardService] ReceiveBigHuntReward") log.Printf("[RewardService] ReceiveBigHuntReward")
cat := s.holder.Get()
bhCatalog := cat.BigHunt
granter := cat.QuestHandler.Granter
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
weeklyVersion := gametime.WeeklyVersion(nowMillis) weeklyVersion := gametime.WeeklyVersion(nowMillis)
@@ -45,13 +47,13 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty
ws := user.BigHuntWeeklyStatuses[weeklyVersion] ws := user.BigHuntWeeklyStatuses[weeklyVersion]
isReceived = ws.IsReceivedWeeklyReward isReceived = ws.IsReceivedWeeklyReward
for _, boss := range s.bhCatalog.BossByBossId { for _, boss := range bhCatalog.BossByBossId {
key := store.BigHuntWeeklyScoreKey{ key := store.BigHuntWeeklyScoreKey{
BigHuntWeeklyVersion: weeklyVersion, BigHuntWeeklyVersion: weeklyVersion,
AttributeType: boss.AttributeType, AttributeType: boss.AttributeType,
} }
wms := user.BigHuntWeeklyMaxScores[key] wms := user.BigHuntWeeklyMaxScores[key]
gradeIcon := s.bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore) gradeIcon := bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore)
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{ weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
AttributeType: boss.AttributeType, AttributeType: boss.AttributeType,
BeforeMaxScore: wms.MaxScore, BeforeMaxScore: wms.MaxScore,
@@ -64,12 +66,12 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty
} }
if !isReceived { if !isReceived {
for _, boss := range s.bhCatalog.BossByBossId { for _, boss := range bhCatalog.BossByBossId {
rewardKey := masterdata.BigHuntWeeklyRewardKey{ rewardKey := masterdata.BigHuntWeeklyRewardKey{
ScheduleId: 1, ScheduleId: 1,
AttributeType: boss.AttributeType, AttributeType: boss.AttributeType,
} }
rewardGroupId := s.bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis) rewardGroupId := bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis)
if rewardGroupId == 0 { if rewardGroupId == 0 {
continue continue
} }
@@ -80,9 +82,9 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty
} }
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
items := s.bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore) items := bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore)
for _, item := range items { for _, item := range items {
s.granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis) granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
weeklyRewards = append(weeklyRewards, &pb.BigHuntReward{ weeklyRewards = append(weeklyRewards, &pb.BigHuntReward{
PossessionType: item.PossessionType, PossessionType: item.PossessionType,
PossessionId: item.PossessionId, PossessionId: item.PossessionId,
+29 -22
View File
@@ -9,6 +9,7 @@ import (
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
@@ -18,23 +19,25 @@ type ShopServiceServer struct {
pb.UnimplementedShopServiceServer pb.UnimplementedShopServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.ShopCatalog holder *runtime.Holder
granter *store.PossessionGranter
} }
func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ShopCatalog, granter *store.PossessionGranter) *ShopServiceServer { func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *ShopServiceServer {
return &ShopServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter} return &ShopServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) { func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) {
log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems) log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems)
cat := s.holder.Get()
catalog := cat.Shop
granter := cat.QuestHandler.Granter
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
for shopItemId, qty := range req.ShopItems { for shopItemId, qty := range req.ShopItems {
item, ok := s.catalog.Items[shopItemId] item, ok := catalog.Items[shopItemId]
if !ok { if !ok {
log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId) log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId)
continue continue
@@ -46,8 +49,8 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
continue continue
} }
for _, content := range s.catalog.Contents[shopItemId] { for _, content := range catalog.Contents[shopItemId] {
s.granter.GrantFull(user, granter.GrantFull(user,
model.PossessionType(content.PossessionType), model.PossessionType(content.PossessionType),
content.PossessionId, content.PossessionId,
content.Count*qty, content.Count*qty,
@@ -55,7 +58,7 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
) )
} }
s.applyContentEffects(user, shopItemId, qty, nowMillis) applyShopContentEffects(catalog, user, shopItemId, qty, nowMillis)
si := user.ShopItems[shopItemId] si := user.ShopItems[shopItemId]
si.ShopItemId = shopItemId si.ShopItemId = shopItemId
@@ -76,12 +79,13 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) { func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed) log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed)
catalog := s.holder.Get().Shop
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
if len(user.ShopReplaceableLineup) == 0 && len(s.catalog.ItemShopPool) > 0 { if len(user.ShopReplaceableLineup) == 0 && len(catalog.ItemShopPool) > 0 {
for i, itemId := range s.catalog.ItemShopPool { for i, itemId := range catalog.ItemShopPool {
slot := int32(i + 1) slot := int32(i + 1)
user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{ user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{
SlotNumber: slot, SlotNumber: slot,
@@ -93,7 +97,7 @@ func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.Refresh
if req.IsGemUsed { if req.IsGemUsed {
user.ShopReplaceable.LineupUpdateCount++ user.ShopReplaceable.LineupUpdateCount++
user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis
for _, itemId := range s.catalog.ItemShopPool { for _, itemId := range catalog.ItemShopPool {
if si, ok := user.ShopItems[itemId]; ok { if si, ok := user.ShopItems[itemId]; ok {
si.BoughtCount = 0 si.BoughtCount = 0
si.LatestVersion = nowMillis si.LatestVersion = nowMillis
@@ -120,11 +124,14 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s", log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s",
req.ShopId, req.ShopItemId, req.ProductId) req.ShopId, req.ShopItemId, req.ProductId)
cat := s.holder.Get()
catalog := cat.Shop
granter := cat.QuestHandler.Granter
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
item, ok := s.catalog.Items[req.ShopItemId] item, ok := catalog.Items[req.ShopItemId]
if !ok { if !ok {
log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId) log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId)
return return
@@ -134,8 +141,8 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err) log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err)
} }
for _, content := range s.catalog.Contents[req.ShopItemId] { for _, content := range catalog.Contents[req.ShopItemId] {
s.granter.GrantFull(user, granter.GrantFull(user,
model.PossessionType(content.PossessionType), model.PossessionType(content.PossessionType),
content.PossessionId, content.PossessionId,
content.Count, content.Count,
@@ -143,13 +150,13 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
) )
} }
s.applyContentEffects(user, req.ShopItemId, 1, nowMillis) applyShopContentEffects(catalog, user, req.ShopItemId, 1, nowMillis)
si := user.ShopItems[req.ShopItemId] si := user.ShopItems[req.ShopItemId]
si.ShopItemId = req.ShopItemId si.ShopItemId = req.ShopItemId
si.BoughtCount++ si.BoughtCount++
if item.ShopItemLimitedStockId > 0 { if item.ShopItemLimitedStockId > 0 {
if maxCount, ok := s.catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount { if maxCount, ok := catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount {
si.BoughtCount = 0 si.BoughtCount = 0
} }
} }
@@ -182,12 +189,12 @@ func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context,
}, nil }, nil
} }
func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemId, qty int32, nowMillis int64) { func applyShopContentEffects(catalog *masterdata.ShopCatalog, user *store.UserState, shopItemId, qty int32, nowMillis int64) {
for _, effect := range s.catalog.Effects[shopItemId] { for _, effect := range catalog.Effects[shopItemId] {
switch effect.EffectTargetType { switch effect.EffectTargetType {
case model.EffectTargetStaminaRecovery: case model.EffectTargetStaminaRecovery:
maxMillis := s.catalog.MaxStaminaMillis[user.Status.Level] maxMillis := catalog.MaxStaminaMillis[user.Status.Level]
millis := s.resolveEffectMillis(effect.EffectValueType, effect.EffectValue, user.Status.Level) millis := resolveShopEffectMillis(catalog, effect.EffectValueType, effect.EffectValue, user.Status.Level)
store.RecoverStamina(user, millis*qty, maxMillis, nowMillis) store.RecoverStamina(user, millis*qty, maxMillis, nowMillis)
default: default:
log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType) log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType)
@@ -195,12 +202,12 @@ func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemI
} }
} }
func (s *ShopServiceServer) resolveEffectMillis(effectValueType, effectValue, userLevel int32) int32 { func resolveShopEffectMillis(catalog *masterdata.ShopCatalog, effectValueType, effectValue, userLevel int32) int32 {
switch effectValueType { switch effectValueType {
case model.EffectValueFixed: case model.EffectValueFixed:
return effectValue return effectValue
case model.EffectValuePermil: case model.EffectValuePermil:
maxMillis := s.catalog.MaxStaminaMillis[userLevel] maxMillis := catalog.MaxStaminaMillis[userLevel]
return effectValue * maxMillis / 1000 return effectValue * maxMillis / 1000
default: default:
return 0 return 0
+6 -4
View File
@@ -8,6 +8,7 @@ import (
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/questflow" "lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -15,17 +16,18 @@ type TutorialServiceServer struct {
pb.UnimplementedTutorialServiceServer pb.UnimplementedTutorialServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
engine *questflow.QuestHandler holder *runtime.Holder
} }
func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *TutorialServiceServer { func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *TutorialServiceServer {
return &TutorialServiceServer{users: users, sessions: sessions, engine: engine} return &TutorialServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb.SetTutorialProgressRequest) (*pb.SetTutorialProgressResponse, error) { func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb.SetTutorialProgressRequest) (*pb.SetTutorialProgressResponse, error) {
log.Printf("[TutorialService] SetTutorialProgress: type=%d phase=%d choice=%d", req.TutorialType, req.ProgressPhase, req.ChoiceId) log.Printf("[TutorialService] SetTutorialProgress: type=%d phase=%d choice=%d", req.TutorialType, req.ProgressPhase, req.ChoiceId)
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
engine := s.holder.Get().QuestHandler
var grants []questflow.RewardGrant var grants []questflow.RewardGrant
s.users.UpdateUser(userId, func(user *store.UserState) { s.users.UpdateUser(userId, func(user *store.UserState) {
existing, exists := user.Tutorials[req.TutorialType] existing, exists := user.Tutorials[req.TutorialType]
@@ -36,7 +38,7 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb
ChoiceId: req.ChoiceId, ChoiceId: req.ChoiceId,
} }
} }
grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis) grants = engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
if req.TutorialType == int32(model.TutorialTypeMenuFirst) && req.ProgressPhase == 20 { if req.TutorialType == int32(model.TutorialTypeMenuFirst) && req.ProgressPhase == 20 {
store.EnsureDefaultDeck(user, nowMillis) store.EnsureDefaultDeck(user, nowMillis)
} }
+91 -64
View File
@@ -10,6 +10,7 @@ import (
"lunar-tear/server/internal/gameutil" "lunar-tear/server/internal/gameutil"
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -17,12 +18,11 @@ type WeaponServiceServer struct {
pb.UnimplementedWeaponServiceServer pb.UnimplementedWeaponServiceServer
users store.UserRepository users store.UserRepository
sessions store.SessionRepository sessions store.SessionRepository
catalog *masterdata.WeaponCatalog holder *runtime.Holder
config *masterdata.GameConfig
} }
func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.WeaponCatalog, config *masterdata.GameConfig) *WeaponServiceServer { func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder) *WeaponServiceServer {
return &WeaponServiceServer{users: users, sessions: sessions, catalog: catalog, config: config} return &WeaponServiceServer{users: users, sessions: sessions, holder: holder}
} }
func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectRequest) (*pb.ProtectResponse, error) { func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectRequest) (*pb.ProtectResponse, error) {
@@ -72,6 +72,9 @@ func (s *WeaponServiceServer) Unprotect(ctx context.Context, req *pb.UnprotectRe
func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.EnhanceByMaterialRequest) (*pb.EnhanceByMaterialResponse, error) { func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.EnhanceByMaterialRequest) (*pb.EnhanceByMaterialResponse, error) {
log.Printf("[WeaponService] EnhanceByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials) log.Printf("[WeaponService] EnhanceByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -82,7 +85,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
return return
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceByMaterial: weapon master id=%d not found", weapon.WeaponId) log.Printf("[WeaponService] EnhanceByMaterial: weapon master id=%d not found", weapon.WeaponId)
return return
@@ -91,7 +94,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
totalExp := int32(0) totalExp := int32(0)
totalMaterialCount := int32(0) totalMaterialCount := int32(0)
for materialId, count := range req.Materials { for materialId, count := range req.Materials {
mat, ok := s.catalog.Materials[materialId] mat, ok := catalog.Materials[materialId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceByMaterial: material id=%d not found, skipping", materialId) log.Printf("[WeaponService] EnhanceByMaterial: material id=%d not found, skipping", materialId)
continue continue
@@ -107,19 +110,19 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
expPerUnit := mat.EffectValue expPerUnit := mat.EffectValue
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType { if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
} }
totalExp += expPerUnit * count totalExp += expPerUnit * count
} }
if costFunc, ok := s.catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
goldCost := costFunc.Evaluate(totalMaterialCount) goldCost := costFunc.Evaluate(totalMaterialCount)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
log.Printf("[WeaponService] EnhanceByMaterial: gold cost=%d (materials=%d)", goldCost, totalMaterialCount) log.Printf("[WeaponService] EnhanceByMaterial: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
} }
weapon.Exp += totalExp weapon.Exp += totalExp
if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
} }
@@ -127,7 +130,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
user.Weapons[req.UserWeaponUuid] = weapon user.Weapons[req.UserWeaponUuid] = weapon
log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis)
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("weapon enhance by material: %w", err) return nil, fmt.Errorf("weapon enhance by material: %w", err)
@@ -142,6 +145,9 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) { func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) {
log.Printf("[WeaponService] Sell: uuids=%v", req.UserWeaponUuid) log.Printf("[WeaponService] Sell: uuids=%v", req.UserWeaponUuid)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
_, err := s.users.UpdateUser(userId, func(user *store.UserState) { _, err := s.users.UpdateUser(userId, func(user *store.UserState) {
@@ -153,17 +159,17 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
continue continue
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] Sell: weapon master id=%d not found, skipping", weapon.WeaponId) log.Printf("[WeaponService] Sell: weapon master id=%d not found, skipping", weapon.WeaponId)
continue continue
} }
if sellFunc, ok := s.catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if sellFunc, ok := catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
totalGold += sellFunc.Evaluate(weapon.Level) totalGold += sellFunc.Evaluate(weapon.Level)
} }
if medals, ok := s.catalog.MedalsByWeaponId[weapon.WeaponId]; ok { if medals, ok := catalog.MedalsByWeaponId[weapon.WeaponId]; ok {
for itemId, count := range medals { for itemId, count := range medals {
user.ConsumableItems[itemId] += count user.ConsumableItems[itemId] += count
} }
@@ -176,7 +182,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
} }
if totalGold > 0 { if totalGold > 0 {
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold user.ConsumableItems[config.ConsumableItemIdForGold] += totalGold
log.Printf("[WeaponService] Sell: granted %d gold", totalGold) log.Printf("[WeaponService] Sell: granted %d gold", totalGold)
} }
}) })
@@ -190,6 +196,9 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) (*pb.EvolveResponse, error) { func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) (*pb.EvolveResponse, error) {
log.Printf("[WeaponService] Evolve: uuid=%s", req.UserWeaponUuid) log.Printf("[WeaponService] Evolve: uuid=%s", req.UserWeaponUuid)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -200,20 +209,20 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
return return
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] Evolve: weapon master id=%d not found", weapon.WeaponId) log.Printf("[WeaponService] Evolve: weapon master id=%d not found", weapon.WeaponId)
return return
} }
evolvedId, ok := s.catalog.EvolutionNextWeaponId[weapon.WeaponId] evolvedId, ok := catalog.EvolutionNextWeaponId[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] Evolve: no evolution for weaponId=%d", weapon.WeaponId) log.Printf("[WeaponService] Evolve: no evolution for weaponId=%d", weapon.WeaponId)
return return
} }
totalMaterialCount := int32(0) totalMaterialCount := int32(0)
mats := s.catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId] mats := catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId]
for _, mat := range mats { for _, mat := range mats {
cur := user.Materials[mat.MaterialId] cur := user.Materials[mat.MaterialId]
cost := mat.Count cost := mat.Count
@@ -225,9 +234,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
totalMaterialCount += cost totalMaterialCount += cost
} }
if costFunc, ok := s.catalog.EvolutionCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { if costFunc, ok := catalog.EvolutionCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
goldCost := costFunc.Evaluate(totalMaterialCount) goldCost := costFunc.Evaluate(totalMaterialCount)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost) log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost)
} }
@@ -235,9 +244,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
weapon.LatestVersion = nowMillis weapon.LatestVersion = nowMillis
user.Weapons[req.UserWeaponUuid] = weapon user.Weapons[req.UserWeaponUuid] = weapon
evolvedMaster, ok := s.catalog.Weapons[evolvedId] evolvedMaster, ok := catalog.Weapons[evolvedId]
if ok { if ok {
if slots, ok := s.catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok { if slots, ok := catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok {
abilities := make([]store.WeaponAbilityState, len(slots)) abilities := make([]store.WeaponAbilityState, len(slots))
for i, slot := range slots { for i, slot := range slots {
abilities[i] = store.WeaponAbilityState{ abilities[i] = store.WeaponAbilityState{
@@ -252,7 +261,7 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId) log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId)
s.checkWeaponStoryUnlocks(user, evolvedId, weapon.Level, nowMillis) checkWeaponStoryUnlocks(catalog, user, evolvedId, weapon.Level, nowMillis)
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("weapon evolve: %w", err) return nil, fmt.Errorf("weapon evolve: %w", err)
@@ -264,6 +273,9 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceSkillRequest) (*pb.EnhanceSkillResponse, error) { func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceSkillRequest) (*pb.EnhanceSkillResponse, error) {
log.Printf("[WeaponService] EnhanceSkill: uuid=%s skillId=%d addLevel=%d", req.UserWeaponUuid, req.SkillId, req.AddLevelCount) log.Printf("[WeaponService] EnhanceSkill: uuid=%s skillId=%d addLevel=%d", req.UserWeaponUuid, req.SkillId, req.AddLevelCount)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -274,13 +286,13 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
return return
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceSkill: weapon master id=%d not found", weapon.WeaponId) log.Printf("[WeaponService] EnhanceSkill: weapon master id=%d not found", weapon.WeaponId)
return return
} }
groupRows := s.catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId] groupRows := catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId]
var skillGroup *masterdata.EntityMWeaponSkillGroup var skillGroup *masterdata.EntityMWeaponSkillGroup
for i := range groupRows { for i := range groupRows {
if groupRows[i].SkillId == req.SkillId { if groupRows[i].SkillId == req.SkillId {
@@ -306,7 +318,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
return return
} }
maxLevelFunc, ok := s.catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] maxLevelFunc, ok := catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceSkill: no max skill level func for enhanceId=%d", wm.WeaponSpecificEnhanceId) log.Printf("[WeaponService] EnhanceSkill: no max skill level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
return return
@@ -326,7 +338,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
enhanceMatId := skillGroup.WeaponSkillEnhancementMaterialId enhanceMatId := skillGroup.WeaponSkillEnhancementMaterialId
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
key := [2]int32{enhanceMatId, lvl} key := [2]int32{enhanceMatId, lvl}
mats := s.catalog.SkillEnhanceMats[key] mats := catalog.SkillEnhanceMats[key]
for _, mat := range mats { for _, mat := range mats {
cur := user.Materials[mat.MaterialId] cur := user.Materials[mat.MaterialId]
cost := mat.Count cost := mat.Count
@@ -337,9 +349,9 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
user.Materials[mat.MaterialId] = cur - cost user.Materials[mat.MaterialId] = cur - cost
} }
if costFunc, ok := s.catalog.SkillCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if costFunc, ok := catalog.SkillCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
goldCost := costFunc.Evaluate(lvl + 1) goldCost := costFunc.Evaluate(lvl + 1)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
} }
} }
@@ -360,6 +372,9 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.EnhanceAbilityRequest) (*pb.EnhanceAbilityResponse, error) { func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.EnhanceAbilityRequest) (*pb.EnhanceAbilityResponse, error) {
log.Printf("[WeaponService] EnhanceAbility: uuid=%s abilityId=%d addLevel=%d", req.UserWeaponUuid, req.AbilityId, req.AddLevelCount) log.Printf("[WeaponService] EnhanceAbility: uuid=%s abilityId=%d addLevel=%d", req.UserWeaponUuid, req.AbilityId, req.AddLevelCount)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -370,13 +385,13 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
return return
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceAbility: weapon master id=%d not found", weapon.WeaponId) log.Printf("[WeaponService] EnhanceAbility: weapon master id=%d not found", weapon.WeaponId)
return return
} }
groupRows := s.catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId] groupRows := catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId]
var abilityGroup *masterdata.EntityMWeaponAbilityGroup var abilityGroup *masterdata.EntityMWeaponAbilityGroup
for i := range groupRows { for i := range groupRows {
if groupRows[i].AbilityId == req.AbilityId { if groupRows[i].AbilityId == req.AbilityId {
@@ -402,7 +417,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
return return
} }
maxLevelFunc, ok := s.catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId] maxLevelFunc, ok := catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceAbility: no max ability level func for enhanceId=%d", wm.WeaponSpecificEnhanceId) log.Printf("[WeaponService] EnhanceAbility: no max ability level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
return return
@@ -422,7 +437,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
enhanceMatId := abilityGroup.WeaponAbilityEnhancementMaterialId enhanceMatId := abilityGroup.WeaponAbilityEnhancementMaterialId
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ { for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
key := [2]int32{enhanceMatId, lvl} key := [2]int32{enhanceMatId, lvl}
mats := s.catalog.AbilityEnhanceMats[key] mats := catalog.AbilityEnhanceMats[key]
for _, mat := range mats { for _, mat := range mats {
cur := user.Materials[mat.MaterialId] cur := user.Materials[mat.MaterialId]
cost := mat.Count cost := mat.Count
@@ -433,9 +448,9 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
user.Materials[mat.MaterialId] = cur - cost user.Materials[mat.MaterialId] = cur - cost
} }
if costFunc, ok := s.catalog.AbilityCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if costFunc, ok := catalog.AbilityCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
goldCost := costFunc.Evaluate(lvl + 1) goldCost := costFunc.Evaluate(lvl + 1)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
} }
} }
@@ -456,6 +471,9 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.LimitBreakByMaterialRequest) (*pb.LimitBreakByMaterialResponse, error) { func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.LimitBreakByMaterialRequest) (*pb.LimitBreakByMaterialResponse, error) {
log.Printf("[WeaponService] LimitBreakByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials) log.Printf("[WeaponService] LimitBreakByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -466,18 +484,18 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
return return
} }
if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount { if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount {
log.Printf("[WeaponService] LimitBreakByMaterial: already at max limit break %d", weapon.LimitBreakCount) log.Printf("[WeaponService] LimitBreakByMaterial: already at max limit break %d", weapon.LimitBreakCount)
return return
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] LimitBreakByMaterial: weapon master id=%d not found", weapon.WeaponId) log.Printf("[WeaponService] LimitBreakByMaterial: weapon master id=%d not found", weapon.WeaponId)
return return
} }
remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
totalMaterialCount := int32(0) totalMaterialCount := int32(0)
for materialId, count := range req.Materials { for materialId, count := range req.Materials {
@@ -496,9 +514,9 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
totalMaterialCount += count totalMaterialCount += count
} }
if costFunc, ok := s.catalog.LimitBreakCostByMaterialByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 { if costFunc, ok := catalog.LimitBreakCostByMaterialByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
goldCost := costFunc.Evaluate(totalMaterialCount) goldCost := costFunc.Evaluate(totalMaterialCount)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
log.Printf("[WeaponService] LimitBreakByMaterial: gold cost=%d", goldCost) log.Printf("[WeaponService] LimitBreakByMaterial: gold cost=%d", goldCost)
} }
@@ -525,6 +543,9 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.LimitBreakByWeaponRequest) (*pb.LimitBreakByWeaponResponse, error) { func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.LimitBreakByWeaponRequest) (*pb.LimitBreakByWeaponResponse, error) {
log.Printf("[WeaponService] LimitBreakByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids) log.Printf("[WeaponService] LimitBreakByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -535,18 +556,18 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
return return
} }
if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount { if weapon.LimitBreakCount >= config.WeaponLimitBreakAvailableCount {
log.Printf("[WeaponService] LimitBreakByWeapon: already at max limit break %d", weapon.LimitBreakCount) log.Printf("[WeaponService] LimitBreakByWeapon: already at max limit break %d", weapon.LimitBreakCount)
return return
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] LimitBreakByWeapon: weapon master id=%d not found", weapon.WeaponId) log.Printf("[WeaponService] LimitBreakByWeapon: weapon master id=%d not found", weapon.WeaponId)
return return
} }
remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount remaining := config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
consumedCount := int32(0) consumedCount := int32(0)
for _, uuid := range req.MaterialUserWeaponUuids { for _, uuid := range req.MaterialUserWeaponUuids {
@@ -560,7 +581,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
continue continue
} }
if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
for itemId, count := range medals { for itemId, count := range medals {
user.ConsumableItems[itemId] += count user.ConsumableItems[itemId] += count
} }
@@ -573,9 +594,9 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
consumedCount++ consumedCount++
} }
if costFunc, ok := s.catalog.LimitBreakCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 { if costFunc, ok := catalog.LimitBreakCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
goldCost := costFunc.Evaluate(consumedCount) goldCost := costFunc.Evaluate(consumedCount)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
log.Printf("[WeaponService] LimitBreakByWeapon: gold cost=%d", goldCost) log.Printf("[WeaponService] LimitBreakByWeapon: gold cost=%d", goldCost)
} }
@@ -602,6 +623,9 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.EnhanceByWeaponRequest) (*pb.EnhanceByWeaponResponse, error) { func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.EnhanceByWeaponRequest) (*pb.EnhanceByWeaponResponse, error) {
log.Printf("[WeaponService] EnhanceByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids) log.Printf("[WeaponService] EnhanceByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -612,7 +636,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
return return
} }
wm, ok := s.catalog.Weapons[weapon.WeaponId] wm, ok := catalog.Weapons[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceByWeapon: weapon master id=%d not found", weapon.WeaponId) log.Printf("[WeaponService] EnhanceByWeapon: weapon master id=%d not found", weapon.WeaponId)
return return
@@ -627,19 +651,19 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
continue continue
} }
matMaster, ok := s.catalog.Weapons[matWeapon.WeaponId] matMaster, ok := catalog.Weapons[matWeapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] EnhanceByWeapon: material weapon master id=%d not found, skipping", matWeapon.WeaponId) log.Printf("[WeaponService] EnhanceByWeapon: material weapon master id=%d not found, skipping", matWeapon.WeaponId)
continue continue
} }
baseExp := s.catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId] baseExp := catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId]
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType { if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
baseExp = baseExp * s.config.MaterialSameWeaponExpCoefficientPermil / 1000 baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000
} }
totalExp += baseExp totalExp += baseExp
if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok { if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
for itemId, count := range medals { for itemId, count := range medals {
user.ConsumableItems[itemId] += count user.ConsumableItems[itemId] += count
} }
@@ -652,14 +676,14 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
consumedCount++ consumedCount++
} }
if costFunc, ok := s.catalog.EnhanceCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 { if costFunc, ok := catalog.EnhanceCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
goldCost := costFunc.Evaluate(consumedCount) goldCost := costFunc.Evaluate(consumedCount)
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost user.ConsumableItems[config.ConsumableItemIdForGold] -= goldCost
log.Printf("[WeaponService] EnhanceByWeapon: gold cost=%d (weapons=%d)", goldCost, consumedCount) log.Printf("[WeaponService] EnhanceByWeapon: gold cost=%d (weapons=%d)", goldCost, consumedCount)
} }
weapon.Exp += totalExp weapon.Exp += totalExp
if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
} }
@@ -667,7 +691,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
user.Weapons[req.UserWeaponUuid] = weapon user.Weapons[req.UserWeaponUuid] = weapon
log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level) log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
s.checkWeaponStoryUnlocks(user, weapon.WeaponId, weapon.Level, nowMillis) checkWeaponStoryUnlocks(catalog, user, weapon.WeaponId, weapon.Level, nowMillis)
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("weapon enhance by weapon: %w", err) return nil, fmt.Errorf("weapon enhance by weapon: %w", err)
@@ -679,13 +703,13 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
}, nil }, nil
} }
func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, weaponId, level int32, nowMillis int64) { func checkWeaponStoryUnlocks(catalog *masterdata.WeaponCatalog, user *store.UserState, weaponId, level int32, nowMillis int64) {
wm, ok := s.catalog.Weapons[weaponId] wm, ok := catalog.Weapons[weaponId]
if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 { if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 {
return return
} }
evoOrder, hasEvo := s.catalog.EvolutionOrder[weaponId] evoOrder, hasEvo := catalog.EvolutionOrder[weaponId]
conditions := s.catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId] conditions := catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId]
for _, cond := range conditions { for _, cond := range conditions {
switch model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) { switch model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) {
@@ -696,14 +720,14 @@ func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, wea
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
} }
case model.WeaponStoryReleaseConditionTypeReachInitialMaxLevel: case model.WeaponStoryReleaseConditionTypeReachInitialMaxLevel:
if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
if level >= maxFunc.Evaluate(0) { if level >= maxFunc.Evaluate(0) {
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
} }
} }
case model.WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel: case model.WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel:
if hasEvo && evoOrder >= 1 { if hasEvo && evoOrder >= 1 {
if maxFunc, ok := s.catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
if level >= maxFunc.Evaluate(0) { if level >= maxFunc.Evaluate(0) {
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
} }
@@ -720,6 +744,9 @@ func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, wea
func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) { func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) {
log.Printf("[WeaponService] Awaken: uuid=%s", req.UserWeaponUuid) log.Printf("[WeaponService] Awaken: uuid=%s", req.UserWeaponUuid)
cat := s.holder.Get()
catalog := cat.Weapon
config := cat.GameConfig
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
@@ -730,7 +757,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
return return
} }
awakenRow, ok := s.catalog.AwakenByWeaponId[weapon.WeaponId] awakenRow, ok := catalog.AwakenByWeaponId[weapon.WeaponId]
if !ok { if !ok {
log.Printf("[WeaponService] Awaken: no awaken data for weaponId=%d", weapon.WeaponId) log.Printf("[WeaponService] Awaken: no awaken data for weaponId=%d", weapon.WeaponId)
return return
@@ -741,7 +768,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
return return
} }
mats := s.catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId] mats := catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId]
for _, mat := range mats { for _, mat := range mats {
cur := user.Materials[mat.MaterialId] cur := user.Materials[mat.MaterialId]
cost := mat.Count cost := mat.Count
@@ -753,7 +780,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
} }
if awakenRow.ConsumeGold > 0 { if awakenRow.ConsumeGold > 0 {
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= awakenRow.ConsumeGold user.ConsumableItems[config.ConsumableItemIdForGold] -= awakenRow.ConsumeGold
log.Printf("[WeaponService] Awaken: gold cost=%d", awakenRow.ConsumeGold) log.Printf("[WeaponService] Awaken: gold cost=%d", awakenRow.ConsumeGold)
} }