package common import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" ) // Role variants retain the native row JSON without converting FixPoint strings // or text hashes to float64. Only the captured star is available here; this // does not imply that every star or every hidden shop role has been captured. type CurrencyWarsRoleVariant struct { Config CurrencyWarsRoleConfig `json:"Config"` Price uint32 `json:"Price"` MaxStar uint32 `json:"MaxStar"` Star uint32 `json:"Star"` SellPrice uint32 `json:"SellPrice"` SaleQuoteKnown *bool `json:"SaleQuoteKnown,omitempty"` StarConfig json.RawMessage `json:"StarConfig"` ServantStarConfig json.RawMessage `json:"ServantStarConfig"` } func (c *CurrencyWarsCatalog) validateRoleVariants() error { c.variantByKey = make(map[[2]uint32]CurrencyWarsRoleVariant) if len(c.CapturedVariants) == 0 && c.Provenance.VariantsSHA256 == "" { return nil // Older catalogs have no variant capture; lookups reject it. } digest, err := hex.DecodeString(c.Provenance.VariantsSHA256) if err != nil || len(digest) != sha256.Size || len(c.CapturedVariants) == 0 { return fmt.Errorf("CW role variant capture or provenance is missing") } if c.Provenance.StarRowsSHA256 != "" { d, e := hex.DecodeString(c.Provenance.StarRowsSHA256) if e != nil || len(d) != sha256.Size { return fmt.Errorf("CW star row capture identity is invalid") } } for _, role := range c.CapturedVariants { key := [2]uint32{role.Config.ID, role.Star} if role.Config.ID == 0 || role.Config.AvatarID == 0 || role.Config.Rarity == 0 || role.Config.SeasonID != c.ActiveSeasonID || role.Star == 0 || role.Star > role.MaxStar { return fmt.Errorf("invalid CW captured role variant %v", key) } if _, exists := c.variantByKey[key]; exists { return fmt.Errorf("duplicate CW captured role variant %v", key) } var identity struct{ ID, Star uint32 } if err := json.Unmarshal(role.StarConfig, &identity); err != nil || identity.ID != key[0] || identity.Star != key[1] { return fmt.Errorf("CW role star row identity differs for %v", key) } c.variantByKey[key] = role } for id, role := range c.roleByID { variant, exists := c.variantByKey[[2]uint32{id, role.DefaultStar}] if !exists || variant.Price != role.Price || variant.SellPrice != role.DefaultSellPrice || variant.MaxStar != role.MaxStar || variant.Config.AvatarID != role.Config.AvatarID || variant.Config.SeasonID != role.Config.SeasonID { return fmt.Errorf("CW variant capture differs from default role %d", id) } } return nil } func (c *CurrencyWarsCatalog) CapturedRoleVariant(roleID, star uint32) (CurrencyWarsRoleVariant, error) { roleID = c.CurrentRoleID(roleID) role, exists := c.variantByKey[[2]uint32{roleID, star}] if !exists { return CurrencyWarsRoleVariant{}, fmt.Errorf("CW role %d star %d has not been captured", roleID, star) } role.Config.TraitList = append([]uint32(nil), role.Config.TraitList...) role.Config.BackendRankList = append([]uint32(nil), role.Config.BackendRankList...) role.Config.ChargeType = append([]uint32(nil), role.Config.ChargeType...) role.StarConfig = append(json.RawMessage(nil), role.StarConfig...) role.ServantStarConfig = append(json.RawMessage(nil), role.ServantStarConfig...) return role, nil } // Native special roles can have a sale quote different from the rarity array. // Return only an observed quote, with run modifiers/refunds left to the caller. func (c *CurrencyWarsCatalog) CapturedSalePrice(roleID, star uint32) (uint32, error) { role, err := c.CapturedRoleVariant(roleID, star) if err != nil { return 0, err } if role.SaleQuoteKnown != nil && !*role.SaleQuoteKnown { return 0, fmt.Errorf("CW role %d star %d sale quote has not been established", roleID, star) } return role.SellPrice, nil }