feat: add verified gacha flows and native client studio

This commit is contained in:
2026-09-30 23:49:00 +08:00
parent 78467e13ea
commit f445cbc9ac
66 changed files with 2387 additions and 2351 deletions
+29 -9
View File
@@ -39,10 +39,13 @@ type CollectionGrant struct {
Upgrades []CostumeUpgrade `json:"upgrades,omitempty"`
Exchanges []CostumeExchange `json:"exchanges,omitempty"`
ViewCostumeIDs []uint64 `json:"view_costume_ids"`
ViewCostumeSortIDs []uint64 `json:"view_costume_sort_ids,omitempty"`
GachaGroupID uint64 `json:"gacha_group_id,omitempty"`
GachaPoint uint64 `json:"gacha_point,omitempty"`
GachaFixed []GachaFixedState `json:"gacha_fixed,omitempty"`
SelectionApplySortIDs []uint64 `json:"selection_apply_sort_ids,omitempty"`
GachaRequestDigest string `json:"gacha_request_digest,omitempty"`
GachaResponse []byte `json:"gacha_response,omitempty"`
}
type GachaUserState struct {
@@ -79,6 +82,10 @@ type GachaPurchase struct {
SelectionApplySortIDs []uint64
StepUpGroupID uint64
StepUpStep uint64
RewardCount uint64
// RewardSortIDs preserves positions from mixed reward programs. Ordinary
// costume-only gachas leave it empty and use their slice positions.
RewardSortIDs []uint64
}
type GachaSelection struct {
@@ -245,9 +252,9 @@ func (s *CollectionStore) EnsurePersisted() error {
func emptyCollectionGrant(grant CollectionGrant) bool {
return len(grant.CharacterIndices) == 0 && len(grant.CostumeIndices) == 0 &&
len(grant.Upgrades) == 0 && len(grant.Exchanges) == 0 &&
len(grant.ViewCostumeIDs) == 0 && grant.GachaGroupID == 0 &&
len(grant.ViewCostumeIDs) == 0 && len(grant.ViewCostumeSortIDs) == 0 && grant.GachaGroupID == 0 &&
grant.GachaPoint == 0 && len(grant.GachaFixed) == 0 &&
len(grant.SelectionApplySortIDs) == 0
len(grant.SelectionApplySortIDs) == 0 && grant.GachaRequestDigest == "" && len(grant.GachaResponse) == 0
}
// BindBaseCharacters attaches the authoritative base roster and rejects a
@@ -513,7 +520,7 @@ func (s *CollectionStore) GrantRegularPurchase(identity string, costumeIDs []uin
if identity == "" || len(costumeIDs) == 0 || design == nil || purchase.Group.ID == 0 {
return CollectionGrant{}, errors.New("player: invalid regular gacha purchase")
}
return s.grantCostumes(identity, costumeIDs, design.Character, func(next *collectionSnapshot, grant *CollectionGrant) error {
return s.grantCostumesSorted(identity, costumeIDs, purchase.RewardSortIDs, design.Character, func(next *collectionSnapshot, grant *CollectionGrant) error {
if err := applyStepUpProgress(next, purchase.StepUpGroupID, purchase.StepUpStep); err != nil {
return err
}
@@ -579,15 +586,26 @@ func (s *CollectionStore) GrantEquipmentDraw(identity string) (CollectionGrant,
}
func (s *CollectionStore) grantCostumes(identity string, costumeIDs []uint64, character func(uint64) (gamedata.CharacterDesign, bool), mutate func(*collectionSnapshot, *CollectionGrant) error) (CollectionGrant, error) {
return s.grantCostumesSorted(identity, costumeIDs, nil, character, mutate)
}
func (s *CollectionStore) grantCostumesSorted(identity string, costumeIDs, sortIDs []uint64, character func(uint64) (gamedata.CharacterDesign, bool), mutate func(*collectionSnapshot, *CollectionGrant) error) (CollectionGrant, error) {
if len(sortIDs) != 0 && len(sortIDs) != len(costumeIDs) {
return CollectionGrant{}, errors.New("player: costume reward sort count mismatch")
}
s.mu.Lock()
defer s.mu.Unlock()
if grant, ok := s.data.Grants[identity]; ok {
return cloneGrant(grant), nil
}
next := cloneCollection(s.data)
grant := CollectionGrant{ViewCostumeIDs: append([]uint64(nil), costumeIDs...)}
grant := CollectionGrant{ViewCostumeIDs: append([]uint64(nil), costumeIDs...), ViewCostumeSortIDs: append([]uint64(nil), sortIDs...)}
now := uint64(time.Now().UnixMilli())
for sortIndex, costumeID := range costumeIDs {
sortID := uint64(sortIndex)
if len(sortIDs) != 0 {
sortID = sortIDs[sortIndex]
}
if costumeID == 0 {
return CollectionGrant{}, errors.New("player: preview has invalid costume")
}
@@ -603,9 +621,9 @@ func (s *CollectionStore) grantCostumes(identity string, costumeIDs []uint64, ch
before := next.Costumes[position].Level
if before < maxLevel {
next.Costumes[position].Level++
grant.Upgrades = append(grant.Upgrades, CostumeUpgrade{InvenIndex: next.Costumes[position].InvenIndex, CostumeID: costumeID, Before: before, After: next.Costumes[position].Level, SortID: uint64(sortIndex)})
grant.Upgrades = append(grant.Upgrades, CostumeUpgrade{InvenIndex: next.Costumes[position].InvenIndex, CostumeID: costumeID, Before: before, After: next.Costumes[position].Level, SortID: sortID})
} else {
exchange, err := costumeOverflowExchange(costumeID, next.Costumes[position].InvenIndex, uint64(sortIndex), characterDesign)
exchange, err := costumeOverflowExchange(costumeID, next.Costumes[position].InvenIndex, sortID, characterDesign)
if err != nil {
return CollectionGrant{}, err
}
@@ -622,9 +640,9 @@ func (s *CollectionStore) grantCostumes(identity string, costumeIDs []uint64, ch
}
if before < maxLevel {
next.BaseCostumeLevels[key] = before + 1
grant.Upgrades = append(grant.Upgrades, CostumeUpgrade{InvenIndex: s.base[base].InvenIndex, CostumeID: costumeID, Before: before, After: before + 1, SortID: uint64(sortIndex)})
grant.Upgrades = append(grant.Upgrades, CostumeUpgrade{InvenIndex: s.base[base].InvenIndex, CostumeID: costumeID, Before: before, After: before + 1, SortID: sortID})
} else {
exchange, err := costumeOverflowExchange(costumeID, s.base[base].InvenIndex, uint64(sortIndex), characterDesign)
exchange, err := costumeOverflowExchange(costumeID, s.base[base].InvenIndex, sortID, characterDesign)
if err != nil {
return CollectionGrant{}, err
}
@@ -645,7 +663,7 @@ func (s *CollectionStore) grantCostumes(identity string, costumeIDs []uint64, ch
next.Characters = append(next.Characters, ownedCharacter)
grant.CharacterIndices = append(grant.CharacterIndices, characterIndex)
}
costume := Costume{InvenIndex: costumeIndex, ID: costumeID, UseChar: characterIndex, SortID: uint64(sortIndex), TimeValue: now}
costume := Costume{InvenIndex: costumeIndex, ID: costumeID, UseChar: characterIndex, SortID: sortID, TimeValue: now}
next.Costumes = append(next.Costumes, costume)
grant.CostumeIndices = append(grant.CostumeIndices, costumeIndex)
}
@@ -912,11 +930,13 @@ func findCharacterByID(base, collection []Character, id uint64) (uint64, bool) {
}
func cloneGrant(grant CollectionGrant) CollectionGrant {
grant.GachaResponse = append([]byte(nil), grant.GachaResponse...)
grant.CharacterIndices = append([]uint64(nil), grant.CharacterIndices...)
grant.CostumeIndices = append([]uint64(nil), grant.CostumeIndices...)
grant.Upgrades = append([]CostumeUpgrade(nil), grant.Upgrades...)
grant.Exchanges = append([]CostumeExchange(nil), grant.Exchanges...)
grant.ViewCostumeIDs = append([]uint64(nil), grant.ViewCostumeIDs...)
grant.ViewCostumeSortIDs = append([]uint64(nil), grant.ViewCostumeSortIDs...)
grant.GachaFixed = append([]GachaFixedState(nil), grant.GachaFixed...)
grant.SelectionApplySortIDs = append([]uint64(nil), grant.SelectionApplySortIDs...)
return grant