Files
2026-10-28 18:54:43 +07:00

105 lines
4.1 KiB
Go

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
}