mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Add admin API for content reload
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user