Files
bd2/go/internal/statebridge/snapshot.go
T
2026-09-21 09:53:46 +08:00

548 lines
23 KiB
Go

// Package statebridge adapts the legacy JSON persistence files to the frozen
// cross-language protobuf contract. It never mutates the state directory.
package statebridge
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
statev1 "bd2server/gen/state/v1"
)
const (
FormatVersion = 1
ClientVersion = "2.34.13"
GameDataVersion = "20260910162539"
)
var stateFiles = []string{
"characters.json", "collection.json", "deck.json", "equipment.json",
"items.json", "mail.json", "missions.json", "progress.json", "wallet.json",
}
func CompleteStateAvailable(dir string) (bool, error) {
dir = filepath.Clean(dir)
found := 0
for _, name := range stateFiles {
info, err := os.Stat(filepath.Join(dir, name))
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return false, err
}
if info.IsDir() {
return false, fmt.Errorf("statebridge: %s is a directory", name)
}
found++
}
if found != 0 && found != len(stateFiles) {
return false, fmt.Errorf("statebridge: incomplete state: %d of %d required files exist", found, len(stateFiles))
}
return found == len(stateFiles), nil
}
type pictorial struct {
ID uint64 `json:"id"`
GroupID uint64 `json:"group_id"`
}
type item struct {
InvenIndex uint64 `json:"inven_index"`
ID uint64 `json:"id"`
Type uint64 `json:"type"`
Count uint64 `json:"count"`
KeepFlag uint64 `json:"keep_flag"`
TimeValue uint64 `json:"time_value"`
Pictorial *pictorial `json:"pictorialbook"`
SortID uint64 `json:"sort_id"`
UseCount uint64 `json:"use_count"`
}
type character struct {
InvenIndex uint64 `json:"inven_index"`
ID uint64 `json:"id"`
HP uint64 `json:"hp"`
Level uint64 `json:"level"`
CostumeID uint64 `json:"costume_id"`
Exp uint64 `json:"exp"`
UseCostume uint64 `json:"use_costume"`
TalentLevel uint64 `json:"talent_level"`
TalentExp uint64 `json:"talent_exp"`
SolidarityReward uint64 `json:"solidarity_reward"`
ExpiryTime uint64 `json:"expiry_time"`
ConnectPotentialCostume uint64 `json:"connect_potential_costume"`
Pictorial []pictorial `json:"pictorialbook"`
}
type costume struct {
InvenIndex uint64 `json:"inven_index"`
ID uint64 `json:"id"`
Level uint64 `json:"level"`
UseChar uint64 `json:"use_char"`
SortID uint64 `json:"sort_id"`
PotentialID uint64 `json:"potential_id"`
DesignID uint64 `json:"design_id"`
BurstLevel uint64 `json:"burst_level"`
TimeValue uint64 `json:"time_value"`
Pictorial []pictorial `json:"pictorialbook"`
}
type progressDisk struct {
Version uint32 `json:"version"`
Position struct {
PackID uint32 `json:"PackID"`
Position struct {
MapID uint32 `json:"MapId"`
PlayerPosition struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Z float64 `json:"z"`
} `json:"PlayerPosition"`
ColleaguePositions json.RawMessage `json:"ColleaguePositions"`
} `json:"Position"`
RawJSON string `json:"RawJSON"`
} `json:"position"`
Tutorials []uint32 `json:"tutorials"`
Quests map[string]struct {
QuestID uint32 `json:"QuestID"`
PackID uint32 `json:"PackID"`
Values []uint32 `json:"Values"`
} `json:"quests"`
Cleared map[string]bool `json:"cleared_quests"`
}
type deckSlot struct {
Character uint64 `json:"character_inven_index"`
Costume uint64 `json:"costume_inven_index"`
Slot uint64 `json:"slot"`
}
type deckDisk struct {
Version string `json:"version"`
Deck []deckSlot `json:"deck"`
FieldDeck []deckSlot `json:"field_deck"`
FieldControl uint64 `json:"field_char_control_deck_type"`
Waypoints map[uint64]uint64 `json:"waypoints"`
Costumes map[uint64]uint64 `json:"costumes"`
Packs map[uint64]uint64 `json:"packs"`
HighestPower uint64 `json:"highest_total_battle_power"`
PortraitCostume uint64 `json:"portrait_costume_id"`
AutoRevive uint64 `json:"auto_revive_catalyst"`
}
type inventoryDisk struct {
Version string `json:"version"`
NextIndex uint64 `json:"next_index"`
Items []item `json:"items"`
Granted map[string]bool `json:"granted"`
GrantItems map[string][]uint64 `json:"grant_items"`
}
type equipmentOption struct {
GroupID uint64 `json:"group_id"`
ID uint64 `json:"id"`
}
type equipment struct {
InvenIndex uint64 `json:"inven_index"`
ID uint64 `json:"id"`
Level uint64 `json:"level"`
UseChar uint64 `json:"use_char"`
KeepFlag uint64 `json:"keep_flag"`
LockFlag uint64 `json:"lock_flag"`
SortID uint64 `json:"sort_id"`
MainOption []equipmentOption `json:"main_option"`
SubOption []equipmentOption `json:"sub_option"`
PrivateOption *equipmentOption `json:"private_option"`
Rank []uint64 `json:"rank"`
}
type equipmentDisk struct {
Version string `json:"version"`
NextIndex uint64 `json:"next_index"`
Equipment []equipment `json:"equipment"`
Granted map[string]uint64 `json:"granted"`
}
type charactersDisk struct {
Version string `json:"version"`
Characters []character `json:"characters"`
}
type gachaSelection struct {
GroupID uint64 `json:"group_id"`
Slot uint64 `json:"slot"`
ItemID uint64 `json:"item_id"`
}
type gachaUser struct {
GroupID uint64 `json:"group_id"`
Point uint64 `json:"point"`
TotalBuyCount uint64 `json:"total_buy_count"`
OneFreePickCount uint64 `json:"one_free_pick_count"`
OneCashPickCount uint64 `json:"one_cash_pick_count"`
TenFreePickCount uint64 `json:"ten_free_pick_count"`
TenCashPickCount uint64 `json:"ten_cash_pick_count"`
ExchangeItemCount uint64 `json:"exchange_item_count"`
ExchangeMileageCount uint64 `json:"exchange_mileage_count"`
}
type gachaFixed struct {
FixedID uint64 `json:"fixed_id"`
Type uint64 `json:"type"`
Count uint64 `json:"count"`
ApplySort int32 `json:"apply_sort_id"`
}
type pointExchange struct {
GroupID uint64 `json:"group_id"`
Count uint64 `json:"count"`
}
type costumeUpgrade struct {
InvenIndex uint64 `json:"inven_index"`
CostumeID uint64 `json:"costume_id"`
Before uint64 `json:"before"`
After uint64 `json:"after"`
SortID uint64 `json:"sort_id"`
}
type costumeExchange struct {
InvenIndex uint64 `json:"inven_index"`
OriginalItemType uint64 `json:"original_item_type"`
OriginalItemID uint64 `json:"original_item_id"`
OriginalCount uint64 `json:"original_count"`
ExchangeItemType uint64 `json:"exchange_item_type"`
ExchangeItemID uint64 `json:"exchange_item_id"`
ExchangeCount uint64 `json:"exchange_count"`
SortID uint64 `json:"sort_id"`
}
type collectionGrant struct {
CharacterIndices []uint64 `json:"character_indices"`
CostumeIndices []uint64 `json:"costume_indices"`
Upgrades []costumeUpgrade `json:"upgrades"`
Exchanges []costumeExchange `json:"exchanges"`
ViewCostumeIDs []uint64 `json:"view_costume_ids"`
GachaGroupID uint64 `json:"gacha_group_id"`
GachaPoint uint64 `json:"gacha_point"`
GachaFixed []gachaFixed `json:"gacha_fixed"`
SelectionApplySortIDs []uint64 `json:"selection_apply_sort_ids"`
}
type collectionDisk struct {
Version string `json:"version"`
NextCharacterIndex uint64 `json:"next_character_index"`
NextCostumeIndex uint64 `json:"next_costume_index"`
LatestPreview []uint64 `json:"latest_preview"`
PreviewEventIndex uint64 `json:"preview_event_index"`
PreviewLocked bool `json:"preview_locked"`
Characters []character `json:"characters"`
Costumes []costume `json:"costumes"`
BaseCostumeLevels map[string]uint64 `json:"base_costume_levels"`
GachaSelections map[string][]gachaSelection `json:"gacha_selections"`
StepUpProgress map[string]uint64 `json:"step_up_progress"`
GachaUsers map[string]gachaUser `json:"gacha_users"`
GachaFixed map[string]gachaFixed `json:"gacha_fixed"`
GachaApplied map[string]bool `json:"gacha_applied"`
GachaPointExchanges map[string]pointExchange `json:"gacha_point_exchanges"`
GachaCountCorrected bool `json:"gacha_count_corrected"`
Grants map[string]collectionGrant `json:"grants"`
}
type walletDisk struct {
Version string `json:"version"`
Gold uint64 `json:"gold"`
FreeJewelry uint64 `json:"free_jewelry"`
Jewelry uint64 `json:"jewelry"`
Mileage uint64 `json:"mileage"`
HopePowder uint64 `json:"hope_powder"`
Granted map[string]bool `json:"granted"`
Spent map[string]bool `json:"spent"`
}
type mailDisk struct {
Version string `json:"version"`
Opened []uint64 `json:"opened"`
}
type missionsDisk struct {
Version string `json:"version"`
Completed []string `json:"completed"`
Claimed []string `json:"claimed"`
Progress map[string]uint64 `json:"progress"`
}
// LoadSnapshot reads every state file before decoding any semantic DTO. Hash
// covers filenames, lengths, and original bytes in a fixed order.
func LoadSnapshot(dir string) (*statev1.Snapshot, [32]byte, error) {
dir = filepath.Clean(dir)
raw := make(map[string][]byte, len(stateFiles))
hasher := sha256.New()
for _, name := range stateFiles {
data, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
return nil, [32]byte{}, fmt.Errorf("statebridge: read %s: %w", name, err)
}
raw[name] = data
_ = binary.Write(hasher, binary.LittleEndian, uint32(len(name)))
_, _ = hasher.Write([]byte(name))
_ = binary.Write(hasher, binary.LittleEndian, uint64(len(data)))
_, _ = hasher.Write(data)
}
var sum [32]byte
copy(sum[:], hasher.Sum(nil))
var progress progressDisk
var deck deckDisk
var inventory inventoryDisk
var equipment equipmentDisk
var characters charactersDisk
var collection collectionDisk
var wallet walletDisk
var mail mailDisk
var missions missionsDisk
for name, target := range map[string]any{
"progress.json": &progress, "deck.json": &deck, "items.json": &inventory,
"equipment.json": &equipment, "characters.json": &characters,
"collection.json": &collection, "wallet.json": &wallet,
"mail.json": &mail, "missions.json": &missions,
} {
decoder := json.NewDecoder(bytes.NewReader(raw[name]))
if err := decoder.Decode(target); err != nil {
return nil, [32]byte{}, fmt.Errorf("statebridge: decode %s: %w", name, err)
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
return nil, [32]byte{}, fmt.Errorf("statebridge: trailing data in %s", name)
}
}
snapshot, err := buildSnapshot(progress, deck, inventory, equipment, characters, collection, wallet, mail, missions)
return snapshot, sum, err
}
func buildSnapshot(progress progressDisk, deck deckDisk, inventory inventoryDisk, equipment equipmentDisk, characters charactersDisk, collection collectionDisk, wallet walletDisk, mail mailDisk, missions missionsDisk) (*statev1.Snapshot, error) {
versions := []string{deck.Version, inventory.Version, equipment.Version, characters.Version, collection.Version, wallet.Version, mail.Version, missions.Version}
for _, version := range versions {
if version != ClientVersion {
return nil, fmt.Errorf("statebridge: incompatible client version %q", version)
}
}
progressPB, err := progressProto(progress)
if err != nil {
return nil, err
}
return &statev1.Snapshot{
FormatVersion: FormatVersion, ClientVersion: ClientVersion, GameDataVersion: GameDataVersion,
Progress: progressPB, Deck: deckProto(deck), Inventory: inventoryProto(inventory),
Equipment: equipmentProto(equipment), Characters: &statev1.CharacterState{ClientVersion: characters.Version, Characters: charactersProto(characters.Characters)},
Collection: collectionProto(collection), Wallet: walletProto(wallet),
Mail: &statev1.MailState{ClientVersion: mail.Version, OpenedMailIds: append([]uint64(nil), mail.Opened...)},
Missions: &statev1.MissionState{ClientVersion: missions.Version, Completed: append([]string(nil), missions.Completed...), Claimed: append([]string(nil), missions.Claimed...), Progress: namedCounts(missions.Progress)},
}, nil
}
func progressProto(in progressDisk) (*statev1.Progress, error) {
if in.Version != 2 {
return nil, fmt.Errorf("statebridge: expected progress format 2, got %d", in.Version)
}
out := &statev1.Progress{SourceFormatVersion: in.Version, ClearedTutorialIds: append([]uint32(nil), in.Tutorials...)}
out.Position = &statev1.Position{PackId: in.Position.PackID, MapId: in.Position.Position.MapID,
Player: &statev1.Vector3{X: in.Position.Position.PlayerPosition.X, Y: in.Position.Position.PlayerPosition.Y, Z: in.Position.Position.PlayerPosition.Z},
OriginalJson: []byte(in.Position.RawJSON), ColleaguePositionsJson: append([]byte(nil), in.Position.Position.ColleaguePositions...)}
for _, key := range sortedKeys(in.Quests) {
quest := in.Quests[key]
packID, questID, err := parseQuestKey(key)
if err != nil {
return nil, err
}
if packID != quest.PackID || questID != quest.QuestID {
return nil, fmt.Errorf("statebridge: quest key %q does not match value %d:%d", key, quest.PackID, quest.QuestID)
}
out.Quests = append(out.Quests, &statev1.QuestProgress{Key: &statev1.QuestKey{PackId: quest.PackID, QuestId: quest.QuestID}, Values: append([]uint32(nil), quest.Values...)})
}
for _, key := range sortedKeys(in.Cleared) {
if !in.Cleared[key] {
return nil, fmt.Errorf("statebridge: cleared quest %q must be true", key)
}
pack, quest, err := parseQuestKey(key)
if err != nil {
return nil, err
}
out.ClearedQuests = append(out.ClearedQuests, &statev1.QuestKey{PackId: pack, QuestId: quest})
}
return out, nil
}
func parseQuestKey(key string) (uint32, uint32, error) {
left, right, ok := strings.Cut(key, ":")
if !ok {
return 0, 0, fmt.Errorf("statebridge: malformed quest key %q", key)
}
pack, err1 := strconv.ParseUint(left, 10, 32)
quest, err2 := strconv.ParseUint(right, 10, 32)
if err1 != nil || err2 != nil || pack == 0 || quest == 0 {
return 0, 0, fmt.Errorf("statebridge: malformed quest key %q", key)
}
return uint32(pack), uint32(quest), nil
}
func deckProto(in deckDisk) *statev1.Deck {
return &statev1.Deck{ClientVersion: in.Version, Battle: deckSlots(in.Deck), Field: deckSlots(in.FieldDeck), FieldControlType: in.FieldControl,
Waypoints: pairs(in.Waypoints), SelectedCostumes: pairs(in.Costumes), BoughtPacks: pairs(in.Packs), HighestBattlePower: in.HighestPower,
PortraitCostumeId: in.PortraitCostume, AutoReviveCatalyst: in.AutoRevive}
}
func deckSlots(in []deckSlot) []*statev1.DeckSlot {
out := make([]*statev1.DeckSlot, 0, len(in))
for _, v := range in {
out = append(out, &statev1.DeckSlot{CharacterIndex: v.Character, CostumeIndex: v.Costume, Slot: v.Slot})
}
return out
}
func pairs(in map[uint64]uint64) []*statev1.Pair {
keys := make([]uint64, 0, len(in))
for k := range in {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
out := make([]*statev1.Pair, 0, len(keys))
for _, k := range keys {
out = append(out, &statev1.Pair{Key: k, Value: in[k]})
}
return out
}
func inventoryProto(in inventoryDisk) *statev1.Inventory {
out := &statev1.Inventory{ClientVersion: in.Version, NextIndex: in.NextIndex}
for _, v := range in.Items {
out.Items = append(out.Items, itemProto(v))
}
out.GrantedIdentities = sortedTrueKeys(in.Granted)
for _, identity := range sortedKeys(in.GrantItems) {
out.GrantItems = append(out.GrantItems, &statev1.IndexedGrant{Identity: identity, InventoryIndices: append([]uint64(nil), in.GrantItems[identity]...)})
}
return out
}
func itemProto(v item) *statev1.Item {
out := &statev1.Item{InventoryIndex: v.InvenIndex, Id: v.ID, Type: v.Type, Count: v.Count, KeepFlag: v.KeepFlag, TimeValue: v.TimeValue, SortId: v.SortID, UseCount: v.UseCount}
if v.Pictorial != nil {
out.Pictorial = &statev1.Pictorial{Id: v.Pictorial.ID, GroupId: v.Pictorial.GroupID}
}
return out
}
func equipmentProto(in equipmentDisk) *statev1.EquipmentInventory {
out := &statev1.EquipmentInventory{ClientVersion: in.Version, NextIndex: in.NextIndex}
for _, v := range in.Equipment {
out.Equipment = append(out.Equipment, equipmentItemProto(v))
}
for _, k := range sortedKeys(in.Granted) {
out.Grants = append(out.Grants, &statev1.NamedIndex{Identity: k, InventoryIndex: in.Granted[k]})
}
return out
}
func equipmentItemProto(v equipment) *statev1.Equipment {
out := &statev1.Equipment{InventoryIndex: v.InvenIndex, Id: v.ID, Level: v.Level, UseChar: v.UseChar, KeepFlag: v.KeepFlag, LockFlag: v.LockFlag, SortId: v.SortID, Ranks: append([]uint64(nil), v.Rank...)}
for _, o := range v.MainOption {
out.MainOptions = append(out.MainOptions, &statev1.EquipmentOption{GroupId: o.GroupID, Id: o.ID})
}
for _, o := range v.SubOption {
out.SubOptions = append(out.SubOptions, &statev1.EquipmentOption{GroupId: o.GroupID, Id: o.ID})
}
if v.PrivateOption != nil {
out.PrivateOption = &statev1.EquipmentOption{GroupId: v.PrivateOption.GroupID, Id: v.PrivateOption.ID}
}
return out
}
func charactersProto(in []character) []*statev1.Character {
out := make([]*statev1.Character, 0, len(in))
for _, v := range in {
p := &statev1.Character{InventoryIndex: v.InvenIndex, Id: v.ID, Hp: v.HP, Level: v.Level, CostumeId: v.CostumeID, Exp: v.Exp, UseCostume: v.UseCostume, TalentLevel: v.TalentLevel, TalentExp: v.TalentExp, SolidarityReward: v.SolidarityReward, ExpiryTime: v.ExpiryTime, ConnectPotentialCostume: v.ConnectPotentialCostume}
for _, book := range v.Pictorial {
p.Pictorial = append(p.Pictorial, &statev1.Pictorial{Id: book.ID, GroupId: book.GroupID})
}
out = append(out, p)
}
return out
}
func costumesProto(in []costume) []*statev1.Costume {
out := make([]*statev1.Costume, 0, len(in))
for _, v := range in {
p := &statev1.Costume{InventoryIndex: v.InvenIndex, Id: v.ID, Level: v.Level, UseChar: v.UseChar, SortId: v.SortID, PotentialId: v.PotentialID, DesignId: v.DesignID, BurstLevel: v.BurstLevel, TimeValue: v.TimeValue}
for _, book := range v.Pictorial {
p.Pictorial = append(p.Pictorial, &statev1.Pictorial{Id: book.ID, GroupId: book.GroupID})
}
out = append(out, p)
}
return out
}
func collectionProto(in collectionDisk) *statev1.Collection {
out := &statev1.Collection{ClientVersion: in.Version, NextCharacterIndex: in.NextCharacterIndex, NextCostumeIndex: in.NextCostumeIndex, LatestPreview: append([]uint64(nil), in.LatestPreview...), PreviewEventIndex: in.PreviewEventIndex, PreviewLocked: in.PreviewLocked, Characters: charactersProto(in.Characters), Costumes: costumesProto(in.Costumes), BaseCostumeLevels: namedCounts(in.BaseCostumeLevels), StepUpProgress: namedCounts(in.StepUpProgress), GachaApplied: sortedTrueKeys(in.GachaApplied), GachaCountCorrected: in.GachaCountCorrected}
for _, k := range sortedKeys(in.GachaSelections) {
entry := &statev1.NamedSelections{Identity: k}
for _, v := range in.GachaSelections[k] {
entry.Selections = append(entry.Selections, &statev1.GachaSelection{GroupId: v.GroupID, Slot: v.Slot, ItemId: v.ItemID})
}
out.GachaSelections = append(out.GachaSelections, entry)
}
for _, k := range sortedKeys(in.GachaUsers) {
v := in.GachaUsers[k]
out.GachaUsers = append(out.GachaUsers, &statev1.NamedGachaUser{Identity: k, User: &statev1.GachaUser{GroupId: v.GroupID, Point: v.Point, TotalBuyCount: v.TotalBuyCount, OneFreePickCount: v.OneFreePickCount, OneCashPickCount: v.OneCashPickCount, TenFreePickCount: v.TenFreePickCount, TenCashPickCount: v.TenCashPickCount, ExchangeItemCount: v.ExchangeItemCount, ExchangeMileageCount: v.ExchangeMileageCount}})
}
for _, k := range sortedKeys(in.GachaFixed) {
out.GachaFixed = append(out.GachaFixed, &statev1.NamedGachaFixed{Identity: k, Fixed: fixedProto(in.GachaFixed[k])})
}
for _, k := range sortedKeys(in.GachaPointExchanges) {
v := in.GachaPointExchanges[k]
out.GachaPointExchanges = append(out.GachaPointExchanges, &statev1.NamedGachaPointExchange{Identity: k, Exchange: &statev1.GachaPointExchange{GroupId: v.GroupID, Count: v.Count}})
}
for _, k := range sortedKeys(in.Grants) {
v := in.Grants[k]
grant := &statev1.CollectionGrant{Identity: k, CharacterIndices: append([]uint64(nil), v.CharacterIndices...), CostumeIndices: append([]uint64(nil), v.CostumeIndices...), ViewCostumeIds: append([]uint64(nil), v.ViewCostumeIDs...), GachaGroupId: v.GachaGroupID, GachaPoint: v.GachaPoint, SelectionApplySortIds: append([]uint64(nil), v.SelectionApplySortIDs...)}
for _, u := range v.Upgrades {
grant.Upgrades = append(grant.Upgrades, &statev1.CostumeUpgrade{InventoryIndex: u.InvenIndex, CostumeId: u.CostumeID, Before: u.Before, After: u.After, SortId: u.SortID})
}
for _, e := range v.Exchanges {
grant.Exchanges = append(grant.Exchanges, &statev1.CostumeExchange{InventoryIndex: e.InvenIndex, OriginalItemType: e.OriginalItemType, OriginalItemId: e.OriginalItemID, OriginalCount: e.OriginalCount, ExchangeItemType: e.ExchangeItemType, ExchangeItemId: e.ExchangeItemID, ExchangeCount: e.ExchangeCount, SortId: e.SortID})
}
for _, f := range v.GachaFixed {
grant.GachaFixed = append(grant.GachaFixed, fixedProto(f))
}
out.Grants = append(out.Grants, grant)
}
return out
}
func fixedProto(v gachaFixed) *statev1.GachaFixed {
return &statev1.GachaFixed{FixedId: v.FixedID, Type: v.Type, Count: v.Count, ApplySortId: v.ApplySort}
}
func walletProto(in walletDisk) *statev1.Wallet {
return &statev1.Wallet{ClientVersion: in.Version, Gold: in.Gold, FreeJewelry: in.FreeJewelry, Jewelry: in.Jewelry, Mileage: in.Mileage, HopePowder: in.HopePowder, GrantedIdentities: sortedTrueKeys(in.Granted), SpentIdentities: sortedTrueKeys(in.Spent)}
}
func namedCounts(in map[string]uint64) []*statev1.NamedCount {
out := make([]*statev1.NamedCount, 0, len(in))
for _, k := range sortedKeys(in) {
out = append(out, &statev1.NamedCount{Identity: k, Value: in[k]})
}
return out
}
func sortedTrueKeys(in map[string]bool) []string {
keys := make([]string, 0, len(in))
for k, v := range in {
if v {
keys = append(keys, k)
}
}
sort.Strings(keys)
return keys
}
func sortedKeys[V any](in map[string]V) []string {
keys := make([]string, 0, len(in))
for k := range in {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}