package gameserver import ( "crypto/sha256" _ "embed" "encoding/hex" "encoding/json" "fmt" "strings" "gac-hkrpg-go/internal/common" ) // The maintained adapter is embedded separately from scripts/: a disabled // presentation must not be executed merely because heartbeat lists Lua files. // //go:embed gacha_presentation.lua var gachaPresentationAdapter []byte // loadGachaPresentationLua supplies the same explicit banner/avatar mapping // accepted by DoGacha, plus resource-backed art paths. The Client performs its // own AssetMgr.Exists checks before registering each initialized native clone. // No user save is read or written here. func loadGachaPresentationLua() ([]byte, error) { verified, policy, err := common.LoadBrowsableGachaBanners() if err != nil { return nil, err } if !policy.NativePresentation.Enabled { return []byte("\n_G.__GAC_GACHA_PRESENTATION={enabled=false}\n"), nil } if policy.NativePresentation.Source == "" || policy.NativePresentation.TemplateGachaID == 0 { return nil, fmt.Errorf("native gacha presentation template provenance missing") } var template common.FeaturedGachaBanner for _, banner := range verified { if banner.ID == policy.NativePresentation.TemplateGachaID { template = banner break } } if template.ID == 0 || template.AvatarID != 0 { return nil, fmt.Errorf("native gacha presentation requires a verified generic template") } mapped, rules, err := common.LoadLimitedGachaBanners() if err != nil { return nil, err } var avatars struct { Rows []struct { ID uint32 `json:"AvatarID"` Art string `json:"AvatarCutinFrontImgPath"` Icon string `json:"DefaultAvatarHeadIconPath"` } `json:"avatar_config"` } raw, err := common.ReadResourceFile("resources/AvatarConfig.json") if err != nil { return nil, err } if err = json.Unmarshal(raw, &avatars); err != nil { return nil, err } byID := make(map[uint32]struct{ art, icon string }, len(avatars.Rows)) for _, row := range avatars.Rows { if _, duplicate := byID[row.ID]; duplicate || row.ID == 0 { return nil, fmt.Errorf("duplicate/invalid avatar presentation row %d", row.ID) } byID[row.ID] = struct{ art, icon string }{row.Art, row.Icon} } var payload strings.Builder fmt.Fprintf(&payload, "\n_G.__GAC_GACHA_PRESENTATION={enabled=true,template_id=%d,template_prefab=%s,rules={five_star_pity=%d,four_star_pity=%d},banners={\n", template.ID, luaNavigationString(template.Prefab), rules.FiveStarPity, rules.FourStarPity) for index, banner := range mapped { paths, exists := byID[banner.AvatarID] if !exists || paths.art == "" || paths.icon == "" { return nil, fmt.Errorf("mapped avatar %d has no captured presentation paths", banner.AvatarID) } fmt.Fprintf(&payload, "{gacha_id=%d,avatar_id=%d,sort_order=%d,art_path=%s,icon_path=%s,four_stars={", banner.GachaID, banner.AvatarID, index+1, luaNavigationString(paths.art), luaNavigationString(paths.icon)) for _, id := range banner.FourStarIDs { four, exists := byID[id] if !exists || four.icon == "" { return nil, fmt.Errorf("featured four-star %d has no captured portrait", id) } fmt.Fprintf(&payload, "{avatar_id=%d,portrait_path=%s},", id, luaNavigationString(four.icon)) } payload.WriteString("}},\n") } payload.WriteString("}}\n") // A source update must also replace the owned hooks on a long-lived Client. // Otherwise an unchanged mapping keeps the old closure after a new server // executable starts. Lua defers the replacement until Warp is closed. identity := sha256.New() identity.Write([]byte(payload.String())) identity.Write(gachaPresentationAdapter) fmt.Fprintf(&payload, "_G.__GAC_GACHA_PRESENTATION.digest=%s\n", luaNavigationString(hex.EncodeToString(identity.Sum(nil)))) // Counters change after draws; do not include them in the static adapter // digest or each draw would rebuild every tab and invalidate its selection. pity, err := common.ReadLimitedGachaPity() if err != nil { return nil, err } fmt.Fprintf(&payload, "_G.__GAC_GACHA_PRESENTATION.pity={since_five_star=%d,since_four_star=%d}\n", pity.SinceFiveStar, pity.SinceFourStar) payload.Write(gachaPresentationAdapter) return []byte(payload.String()), nil }