Update GaCy-GO
This commit is contained in:
1 parent
da8a32f60d
commit
0cbe460e49
345 files changed
+98202
-77559
No files matched your search
@@ -0,0 +1,284 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type FeaturedGachaBanner struct {
|
||||
ID, AvatarID, SortID uint32
|
||||
Prefab string
|
||||
}
|
||||
type GachaSandboxPolicy struct {
|
||||
LimitedRules LimitedGachaRules `json:"limited_rules"`
|
||||
Source string `json:"source"`
|
||||
DrawCounts []uint32 `json:"allowed_draw_counts"`
|
||||
ChargeCurrency bool `json:"charge_currency"`
|
||||
DuplicateReward string `json:"duplicate_reward"`
|
||||
// A flat-table path does not prove that its bundle exists in this Client.
|
||||
VerifiedBannerPrefabs []string `json:"verified_banner_prefabs"`
|
||||
SandboxDrawPool struct {
|
||||
Source string `json:"source"`
|
||||
FirstAcquisition string `json:"first_acquisition"`
|
||||
} `json:"sandbox_draw_pool"`
|
||||
// MappedBannerPreviewCount caps how many mapped per-character banners the
|
||||
// GetGachaInfo response advertises. The current Client skips IDs absent
|
||||
// from its local config before creating a pool. Keep zero until a runtime
|
||||
// presentation adapter has been verified; filling a nil Row afterward
|
||||
// cannot create those missing pools.
|
||||
MappedBannerPreviewCount uint32 `json:"mapped_banner_preview_count"`
|
||||
NativePresentation struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TemplateGachaID uint32 `json:"template_gacha_id"`
|
||||
Source string `json:"source"`
|
||||
} `json:"native_presentation"`
|
||||
}
|
||||
|
||||
func LoadFeaturedGachaBanners() ([]FeaturedGachaBanner, GachaSandboxPolicy, error) {
|
||||
return loadGachaBanners(true)
|
||||
}
|
||||
|
||||
// Browse entries can include a verified standard panel without pretending it
|
||||
// represents one featured character. Such entries do not enable sandbox draws.
|
||||
func LoadBrowsableGachaBanners() ([]FeaturedGachaBanner, GachaSandboxPolicy, error) {
|
||||
return loadGachaBanners(false)
|
||||
}
|
||||
|
||||
func loadGachaBanners(featuredOnly bool) ([]FeaturedGachaBanner, GachaSandboxPolicy, error) {
|
||||
raw, err := ReadResourceFile("resources/PreviewGameplayCatalog.json")
|
||||
if err != nil {
|
||||
return nil, GachaSandboxPolicy{}, err
|
||||
}
|
||||
var catalog struct {
|
||||
Source string `json:"source"`
|
||||
Evidence string `json:"evidence_id"`
|
||||
Tables struct {
|
||||
Gacha []struct {
|
||||
ID uint32 `json:"GachaID"`
|
||||
SortID uint32
|
||||
Prefab string `json:"PrefabPath"`
|
||||
} `json:"gacha"`
|
||||
} `json:"tables"`
|
||||
}
|
||||
if err = json.Unmarshal(raw, &catalog); err != nil {
|
||||
return nil, GachaSandboxPolicy{}, err
|
||||
}
|
||||
if catalog.Source == "" || catalog.Evidence == "" {
|
||||
return nil, GachaSandboxPolicy{}, fmt.Errorf("gacha catalog provenance missing")
|
||||
}
|
||||
raw, err = ReadResourceFile("resources/GachaSandboxPolicy.json")
|
||||
if err != nil {
|
||||
return nil, GachaSandboxPolicy{}, err
|
||||
}
|
||||
var policy GachaSandboxPolicy
|
||||
if err = json.Unmarshal(raw, &policy); err != nil {
|
||||
return nil, policy, err
|
||||
}
|
||||
if policy.Source == "" || len(policy.DrawCounts) == 0 || policy.ChargeCurrency || policy.DuplicateReward != "configured_first_eidolon_cost" {
|
||||
return nil, policy, fmt.Errorf("unsupported gacha sandbox policy")
|
||||
}
|
||||
for _, n := range policy.DrawCounts {
|
||||
if n == 0 {
|
||||
return nil, policy, fmt.Errorf("invalid gacha draw count")
|
||||
}
|
||||
}
|
||||
// The actual captured avatar-prefab basename identifies the pictured avatar.
|
||||
// No banner-ID arithmetic or synthetic prefab paths are used.
|
||||
avatarPrefab := regexp.MustCompile(`(?:^|/)AvatarGacha_([0-9]+)\.prefab$`)
|
||||
verified := map[string]bool{}
|
||||
for _, prefab := range policy.VerifiedBannerPrefabs {
|
||||
verified[prefab] = true
|
||||
}
|
||||
var banners []FeaturedGachaBanner
|
||||
seen := map[uint32]bool{}
|
||||
for _, row := range catalog.Tables.Gacha {
|
||||
match := avatarPrefab.FindStringSubmatch(row.Prefab)
|
||||
if match == nil && featuredOnly {
|
||||
continue
|
||||
}
|
||||
if !verified[row.Prefab] {
|
||||
continue
|
||||
}
|
||||
var avatar uint64
|
||||
if match != nil {
|
||||
avatar, err = strconv.ParseUint(match[1], 10, 32)
|
||||
if err != nil || avatar == 0 {
|
||||
return nil, policy, fmt.Errorf("invalid captured character banner")
|
||||
}
|
||||
}
|
||||
if row.ID == 0 || seen[row.ID] {
|
||||
return nil, policy, fmt.Errorf("invalid captured character banner")
|
||||
}
|
||||
banners = append(banners, FeaturedGachaBanner{ID: row.ID, AvatarID: uint32(avatar), SortID: row.SortID, Prefab: row.Prefab})
|
||||
seen[row.ID] = true
|
||||
}
|
||||
// An empty verified set intentionally disables entry instead of advertising
|
||||
// missing prefabs and leaving the Client's UI transition incomplete.
|
||||
sort.Slice(banners, func(i, j int) bool { return banners[i].SortID < banners[j].SortID })
|
||||
return banners, policy, nil
|
||||
}
|
||||
|
||||
// SandboxDrawPool reports whether the policy enables sandbox draws over the
|
||||
// imported roster, and returns the drawable roster IDs (sorted). IDs are read
|
||||
// from the live import, never embedded: every draw resolves to a duplicate
|
||||
// grant on an avatar that actually exists in freesr-data.json.
|
||||
func LoadSandboxDrawPool() (bool, []uint32, GachaSandboxPolicy, error) {
|
||||
_, policy, err := LoadBrowsableGachaBanners()
|
||||
if err != nil {
|
||||
return false, nil, policy, err
|
||||
}
|
||||
pool := policy.SandboxDrawPool
|
||||
if pool.Source != "imported_roster" || pool.FirstAcquisition != "reject" {
|
||||
return false, nil, policy, nil
|
||||
}
|
||||
persistentWriteMu.Lock()
|
||||
defer persistentWriteMu.Unlock()
|
||||
unlock, err := lockPersistentFile()
|
||||
if err != nil {
|
||||
return false, nil, policy, err
|
||||
}
|
||||
defer unlock()
|
||||
raw, err := readResourceFile("freesr-data.json")
|
||||
if err != nil {
|
||||
return false, nil, policy, err
|
||||
}
|
||||
var root struct {
|
||||
Avatars map[uint32]json.RawMessage `json:"avatars"`
|
||||
}
|
||||
if err = json.Unmarshal(raw, &root); err != nil {
|
||||
return false, nil, policy, fmt.Errorf("invalid FreeSR import: %w", err)
|
||||
}
|
||||
ids := make([]uint32, 0, len(root.Avatars))
|
||||
for id := range root.Avatars {
|
||||
if id != 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
return len(ids) > 0, ids, policy, nil
|
||||
}
|
||||
|
||||
// MappedGachaBanner is one entry of the custom per-character banner mapping
|
||||
// (sandbox 900000+ namespace, generated from the imported roster).
|
||||
type MappedGachaBanner struct {
|
||||
GachaID uint32 `json:"gacha_id"`
|
||||
AvatarID uint32 `json:"avatar_id"`
|
||||
// nil accepts the original generated format. Explicit false reserves an
|
||||
// old ID without offering a banner for an avatar absent from the import.
|
||||
RosterAvailable *bool `json:"roster_available,omitempty"`
|
||||
}
|
||||
|
||||
// LoadGachaBannerMapping returns the generated per-character banner mapping.
|
||||
// The mapping alone does not render anything in the Client: the custom
|
||||
// presentation consumes it through DoGacha once its UI lands.
|
||||
func LoadGachaBannerMapping() ([]MappedGachaBanner, error) {
|
||||
raw, err := ReadResourceFile("resources/GachaBannerMapping.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc struct {
|
||||
Format uint32 `json:"format_version"`
|
||||
Source string `json:"source"`
|
||||
ClientAssembly string `json:"client_assembly_sha256"`
|
||||
Banners []MappedGachaBanner `json:"banners"`
|
||||
}
|
||||
if err = json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if doc.Format != 1 || doc.Source == "" || !regexp.MustCompile(`^[0-9a-f]{64}$`).MatchString(doc.ClientAssembly) {
|
||||
return nil, fmt.Errorf("gacha banner mapping provenance missing")
|
||||
}
|
||||
var captured struct {
|
||||
Assembly string `json:"client_assembly_sha256"`
|
||||
Tables struct {
|
||||
Gacha []struct {
|
||||
ID uint32 `json:"GachaID"`
|
||||
} `json:"gacha"`
|
||||
} `json:"tables"`
|
||||
}
|
||||
raw, err = ReadResourceFile("resources/PreviewGameplayCatalog.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = json.Unmarshal(raw, &captured); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if captured.Assembly != doc.ClientAssembly {
|
||||
return nil, fmt.Errorf("gacha banner mapping/captured catalog identity mismatch")
|
||||
}
|
||||
capturedIDs := map[uint32]bool{}
|
||||
for _, row := range captured.Tables.Gacha {
|
||||
capturedIDs[row.ID] = true
|
||||
}
|
||||
seen := map[uint32]bool{}
|
||||
seenAvatars := map[uint32]bool{}
|
||||
active := make([]MappedGachaBanner, 0, len(doc.Banners))
|
||||
for _, b := range doc.Banners {
|
||||
if b.GachaID < 900001 || b.AvatarID == 0 || seen[b.GachaID] || seenAvatars[b.AvatarID] || capturedIDs[b.GachaID] {
|
||||
return nil, fmt.Errorf("invalid gacha banner mapping entry %d", b.GachaID)
|
||||
}
|
||||
seen[b.GachaID] = true
|
||||
seenAvatars[b.AvatarID] = true
|
||||
if b.RosterAvailable == nil || *b.RosterAvailable {
|
||||
active = append(active, b)
|
||||
}
|
||||
}
|
||||
return active, nil
|
||||
}
|
||||
|
||||
// Add only duplicate materials to the latest import. This never replaces the
|
||||
// avatar, its rank/EX/Traces, the lineup, or unrelated inventory entries.
|
||||
func GrantGachaDuplicate(avatarID uint32, additions, generatedBaseline map[uint32]uint32) (map[uint32]uint32, error) {
|
||||
persistentWriteMu.Lock()
|
||||
defer persistentWriteMu.Unlock()
|
||||
unlock, err := lockPersistentFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unlock()
|
||||
raw, err := readResourceFile("freesr-data.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var root map[string]json.RawMessage
|
||||
if err = json.Unmarshal(raw, &root); err != nil || root == nil {
|
||||
return nil, fmt.Errorf("invalid FreeSR import")
|
||||
}
|
||||
var avatars map[uint32]json.RawMessage
|
||||
if err = json.Unmarshal(root["avatars"], &avatars); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(avatars[avatarID]) == 0 {
|
||||
return nil, fmt.Errorf("banner avatar %d is absent from the imported roster", avatarID)
|
||||
}
|
||||
materials := map[uint32]uint32{}
|
||||
if value, exists := root["materials"]; exists {
|
||||
if err = json.Unmarshal(value, &materials); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if materials == nil {
|
||||
materials = map[uint32]uint32{}
|
||||
}
|
||||
}
|
||||
updated := map[uint32]uint32{}
|
||||
for id, count := range additions {
|
||||
current, explicit := materials[id]
|
||||
if !explicit {
|
||||
current = generatedBaseline[id]
|
||||
}
|
||||
next := uint64(current) + uint64(count)
|
||||
if id == 0 || next > uint64(^uint32(0)) {
|
||||
return nil, fmt.Errorf("gacha material count overflow")
|
||||
}
|
||||
materials[id] = uint32(next)
|
||||
updated[id] = uint32(next)
|
||||
}
|
||||
root["materials"], err = json.Marshal(materials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updated, writeJSONAtomic("freesr-data.json", root)
|
||||
}
|
||||
Reference in new issue
Block a user