105 lines
4.1 KiB
Go
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
|
|
}
|