package player // Code generated by profilegen; DO NOT EDIT. import ( "context" "encoding/json" "fmt" "time" ) type Profile struct { AccountID string `json:"account_id"` RoleID string `json:"role_id"` Name string `json:"name"` Level int `json:"level"` State string `json:"state"` LoadedAt time.Time `json:"loaded_at,omitempty"` SavedAt time.Time `json:"saved_at,omitempty"` UpdatedAt time.Time `json:"updated_at"` Version int64 `json:"version"` Currencies map[string]int64 `json:"currencies,omitempty"` Inventory map[string]int64 `json:"inventory,omitempty"` Quests map[string]string `json:"quests,omitempty"` Mailbox []MailRecord `json:"mailbox,omitempty"` } const ( ProfileFieldBase ProfileField = "base" ProfileFieldRuntime ProfileField = "runtime" ProfileFieldMeta ProfileField = "meta" ProfileFieldCurrency ProfileField = "currency" ProfileFieldInventory ProfileField = "inventory" ProfileFieldQuest ProfileField = "quest" ProfileFieldMail ProfileField = "mail" ) var orderedFields = []ProfileField{ ProfileFieldBase, ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldCurrency, ProfileFieldInventory, ProfileFieldQuest, ProfileFieldMail, } const ( ProfileModuleIdentity = "identity" ProfileModuleRuntime = "runtime" ProfileModuleMeta = "meta" ProfileModuleWallet = "wallet" ProfileModuleInventory = "inventory" ProfileModuleQuest = "quest" ProfileModuleMailbox = "mailbox" ) type profileBaseRecord struct { AccountID string `json:"account_id"` RoleID string `json:"role_id"` Name string `json:"name"` Level int `json:"level"` } type profileRuntimeRecord struct { State string `json:"state"` LoadedAt time.Time `json:"loaded_at,omitempty"` SavedAt time.Time `json:"saved_at,omitempty"` UpdatedAt time.Time `json:"updated_at"` } type profileMetaRecord struct { Version int64 `json:"version"` } type currencyRecord struct { Currencies map[string]int64 `json:"currencies,omitempty"` } type inventoryRecord struct { Inventory map[string]int64 `json:"inventory,omitempty"` } type profileQuestRecord struct { Quests map[string]string `json:"quests,omitempty"` } type profileMailRecord struct { Mailbox []MailRecord `json:"mailbox,omitempty"` } const ( redisBaseField = "base" redisRuntimeField = "runtime" redisMetaField = "meta" redisCurrencyField = "currency" redisInventoryField = "inventory" redisQuestField = "quest" redisMailField = "mail" ) var genFieldLookup = map[ProfileField]struct{}{ ProfileFieldBase: {}, ProfileFieldRuntime: {}, ProfileFieldMeta: {}, ProfileFieldCurrency: {}, ProfileFieldInventory: {}, ProfileFieldQuest: {}, ProfileFieldMail: {}, } var genModuleFields = map[string]ProfileField{ ProfileModuleIdentity: ProfileFieldBase, ProfileModuleRuntime: ProfileFieldRuntime, ProfileModuleMeta: ProfileFieldMeta, ProfileModuleWallet: ProfileFieldCurrency, ProfileModuleInventory: ProfileFieldInventory, ProfileModuleQuest: ProfileFieldQuest, ProfileModuleMailbox: ProfileFieldMail, } var genReqFields = []ProfileField{ ProfileFieldBase, } var genBackfillFields = []ProfileField{ ProfileFieldCurrency, ProfileFieldInventory, ProfileFieldQuest, ProfileFieldMail, } func AllProfileFields() []ProfileField { return append([]ProfileField(nil), orderedFields...) } func reqProfileFields() []ProfileField { return append([]ProfileField(nil), genReqFields...) } func backfillFields() []ProfileField { return append([]ProfileField(nil), genBackfillFields...) } func isProfileField(field ProfileField) bool { _, ok := genFieldLookup[field] return ok } func fieldByModule(module string) (ProfileField, bool) { field, ok := genModuleFields[module] return field, ok } func dirtyFieldsByModule(module string) []ProfileField { field, ok := fieldByModule(module) if !ok { return nil } return profileDirtyFields(field) } func normProfileFields(fields []ProfileField) []ProfileField { if len(fields) == 0 { return nil } seen := make(map[ProfileField]struct{}, len(fields)) for _, field := range fields { if isProfileField(field) { seen[field] = struct{}{} } } out := make([]ProfileField, 0, len(seen)) for _, field := range orderedFields { if _, ok := seen[field]; ok { out = append(out, field) } } return out } func mergeProfileFields(left, right []ProfileField) []ProfileField { if len(left) == 0 { return normProfileFields(right) } if len(right) == 0 { return normProfileFields(left) } merged := make([]ProfileField, 0, len(left)+len(right)) merged = append(merged, left...) merged = append(merged, right...) return normProfileFields(merged) } func profileDirtyFields(field ProfileField) []ProfileField { switch field { case ProfileFieldBase: return normProfileFields([]ProfileField{ ProfileFieldBase, }) case ProfileFieldRuntime: return normProfileFields([]ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, }) case ProfileFieldMeta: return normProfileFields([]ProfileField{ ProfileFieldMeta, }) case ProfileFieldCurrency: return normProfileFields([]ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldCurrency, }) case ProfileFieldInventory: return normProfileFields([]ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldInventory, }) case ProfileFieldQuest: return normProfileFields([]ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldQuest, }) case ProfileFieldMail: return normProfileFields([]ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldMail, }) default: return nil } } const ( ProfileOperationAddCurrency ProfileOperation = "add_currency" ProfileOperationSetInventoryItem ProfileOperation = "set_inventory_item" ProfileOperationSetQuestState ProfileOperation = "set_quest_state" ) var genProfileDescs = []ProfileFieldDescriptor{ {Field: ProfileFieldBase, Module: ProfileModuleIdentity, HashField: redisBaseField, Record: "profileBaseRecord", Required: true, Backfill: false, DirtyFields: []ProfileField{ ProfileFieldBase, }, Description: "Account, role, name and level identity data.", Columns: []ProfileRecordFieldDescriptor{ {Name: "AccountID", Type: "string", JSONName: "account_id", Profile: "AccountID"}, {Name: "RoleID", Type: "string", JSONName: "role_id", Profile: "RoleID"}, {Name: "Name", Type: "string", JSONName: "name", Profile: "Name"}, {Name: "Level", Type: "int", JSONName: "level", Profile: "Level"}, }}, {Field: ProfileFieldRuntime, Module: ProfileModuleRuntime, HashField: redisRuntimeField, Record: "profileRuntimeRecord", Required: false, Backfill: false, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, }, Description: "Online state and profile timestamps.", Columns: []ProfileRecordFieldDescriptor{ {Name: "State", Type: "string", JSONName: "state", Profile: "State"}, {Name: "LoadedAt", Type: "time.Time", JSONName: "loaded_at", Profile: "LoadedAt"}, {Name: "SavedAt", Type: "time.Time", JSONName: "saved_at", Profile: "SavedAt"}, {Name: "UpdatedAt", Type: "time.Time", JSONName: "updated_at", Profile: "UpdatedAt"}, }}, {Field: ProfileFieldMeta, Module: ProfileModuleMeta, HashField: redisMetaField, Record: "profileMetaRecord", Required: false, Backfill: false, DirtyFields: []ProfileField{ ProfileFieldMeta, }, Description: "Version and optimistic metadata.", Columns: []ProfileRecordFieldDescriptor{ {Name: "Version", Type: "int64", JSONName: "version", Profile: "Version"}, }}, {Field: ProfileFieldCurrency, Module: ProfileModuleWallet, HashField: redisCurrencyField, Record: "currencyRecord", Required: false, Backfill: true, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldCurrency, }, Description: "Wallet balances such as gold and diamond.", Columns: []ProfileRecordFieldDescriptor{ {Name: "Currencies", Type: "map[string]int64", JSONName: "currencies", Profile: "Currencies"}, }}, {Field: ProfileFieldInventory, Module: ProfileModuleInventory, HashField: redisInventoryField, Record: "inventoryRecord", Required: false, Backfill: true, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldInventory, }, Description: "Item quantities keyed by item id.", Columns: []ProfileRecordFieldDescriptor{ {Name: "Inventory", Type: "map[string]int64", JSONName: "inventory", Profile: "Inventory"}, }}, {Field: ProfileFieldQuest, Module: ProfileModuleQuest, HashField: redisQuestField, Record: "profileQuestRecord", Required: false, Backfill: true, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldQuest, }, Description: "Quest state keyed by quest id.", Columns: []ProfileRecordFieldDescriptor{ {Name: "Quests", Type: "map[string]string", JSONName: "quests", Profile: "Quests"}, }}, {Field: ProfileFieldMail, Module: ProfileModuleMailbox, HashField: redisMailField, Record: "profileMailRecord", Required: false, Backfill: true, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldMail, }, Description: "Mailbox records and read states.", Columns: []ProfileRecordFieldDescriptor{ {Name: "Mailbox", Type: "[]MailRecord", JSONName: "mailbox", Profile: "Mailbox"}, }}, } func cloneProfileDesc(descriptor ProfileFieldDescriptor) ProfileFieldDescriptor { descriptor.DirtyFields = append([]ProfileField(nil), descriptor.DirtyFields...) descriptor.Columns = append([]ProfileRecordFieldDescriptor(nil), descriptor.Columns...) return descriptor } func ProfileFieldDescriptors() []ProfileFieldDescriptor { out := make([]ProfileFieldDescriptor, len(genProfileDescs)) for i := range out { out[i] = cloneProfileDesc(genProfileDescs[i]) } return out } func profileDBDesc(field ProfileField) (ProfileFieldDescriptor, bool) { switch field { case ProfileFieldBase: return cloneProfileDesc(genProfileDescs[0]), true case ProfileFieldRuntime: return cloneProfileDesc(genProfileDescs[1]), true case ProfileFieldMeta: return cloneProfileDesc(genProfileDescs[2]), true case ProfileFieldCurrency: return cloneProfileDesc(genProfileDescs[3]), true case ProfileFieldInventory: return cloneProfileDesc(genProfileDescs[4]), true case ProfileFieldQuest: return cloneProfileDesc(genProfileDescs[5]), true case ProfileFieldMail: return cloneProfileDesc(genProfileDescs[6]), true default: return ProfileFieldDescriptor{}, false } } func profileDBDescByHash(hashField string) (ProfileFieldDescriptor, bool) { switch hashField { case redisBaseField: return cloneProfileDesc(genProfileDescs[0]), true case redisRuntimeField: return cloneProfileDesc(genProfileDescs[1]), true case redisMetaField: return cloneProfileDesc(genProfileDescs[2]), true case redisCurrencyField: return cloneProfileDesc(genProfileDescs[3]), true case redisInventoryField: return cloneProfileDesc(genProfileDescs[4]), true case redisQuestField: return cloneProfileDesc(genProfileDescs[5]), true case redisMailField: return cloneProfileDesc(genProfileDescs[6]), true default: return ProfileFieldDescriptor{}, false } } func profileDBHashField(field ProfileField) (string, bool) { switch field { case ProfileFieldBase: return redisBaseField, true case ProfileFieldRuntime: return redisRuntimeField, true case ProfileFieldMeta: return redisMetaField, true case ProfileFieldCurrency: return redisCurrencyField, true case ProfileFieldInventory: return redisInventoryField, true case ProfileFieldQuest: return redisQuestField, true case ProfileFieldMail: return redisMailField, true default: return "", false } } var genProfileOps = []ProfileOperationDescriptor{ {Operation: ProfileOperationAddCurrency, Module: ProfileModuleWallet, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldCurrency, }, Description: "Adds currency to wallet balances and marks wallet dirty."}, {Operation: ProfileOperationSetInventoryItem, Module: ProfileModuleInventory, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldInventory, }, Description: "Sets or removes one inventory item quantity."}, {Operation: ProfileOperationSetQuestState, Module: ProfileModuleQuest, DirtyFields: []ProfileField{ ProfileFieldRuntime, ProfileFieldMeta, ProfileFieldQuest, }, Description: "Updates one quest state entry."}, } func cloneProfileOp(descriptor ProfileOperationDescriptor) ProfileOperationDescriptor { descriptor.DirtyFields = append([]ProfileField(nil), descriptor.DirtyFields...) return descriptor } func ProfileOperationDescriptors() []ProfileOperationDescriptor { out := make([]ProfileOperationDescriptor, len(genProfileOps)) for i := range out { out[i] = cloneProfileOp(genProfileOps[i]) } return out } func opDesc(operation ProfileOperation) (ProfileOperationDescriptor, bool) { switch operation { case ProfileOperationAddCurrency: return cloneProfileOp(genProfileOps[0]), true case ProfileOperationSetInventoryItem: return cloneProfileOp(genProfileOps[1]), true case ProfileOperationSetQuestState: return cloneProfileOp(genProfileOps[2]), true default: return ProfileOperationDescriptor{}, false } } func dirtyFieldsByOp(operation ProfileOperation) []ProfileField { descriptor, ok := opDesc(operation) if !ok { return nil } return descriptor.DirtyFields } func mutateProfileOp(ctx context.Context, module *Module, accountID string, operation ProfileOperation, mutate func(*Profile)) (Profile, error) { if module == nil { return Profile{}, fmt.Errorf("profile module is nil") } if mutate == nil { return Profile{}, fmt.Errorf("profile operation %s mutation is required", operation) } dirtyFields := dirtyFieldsByOp(operation) if len(dirtyFields) == 0 { return Profile{}, fmt.Errorf("unsupported profile operation %s", operation) } return module.mutateLoadedProfile(ctx, accountID, func(profile *Profile) []ProfileField { mutate(profile) return dirtyFields }) } func mutateAddCurrency(ctx context.Context, module *Module, accountID string, mutate func(*Profile)) (Profile, error) { return mutateProfileOp(ctx, module, accountID, ProfileOperationAddCurrency, mutate) } func mutateSetItem(ctx context.Context, module *Module, accountID string, mutate func(*Profile)) (Profile, error) { return mutateProfileOp(ctx, module, accountID, ProfileOperationSetInventoryItem, mutate) } func mutateSetQuest(ctx context.Context, module *Module, accountID string, mutate func(*Profile)) (Profile, error) { return mutateProfileOp(ctx, module, accountID, ProfileOperationSetQuestState, mutate) } func encodeProfileField(profile Profile, field ProfileField) ([]byte, error) { switch field { case ProfileFieldBase: return json.Marshal(profileBaseRecord{ AccountID: profile.AccountID, RoleID: profile.RoleID, Name: profile.Name, Level: profile.Level, }) case ProfileFieldRuntime: return json.Marshal(profileRuntimeRecord{ State: profile.State, LoadedAt: profile.LoadedAt, SavedAt: profile.SavedAt, UpdatedAt: profile.UpdatedAt, }) case ProfileFieldMeta: return json.Marshal(profileMetaRecord{ Version: profile.Version, }) case ProfileFieldCurrency: return json.Marshal(currencyRecord{ Currencies: profile.Currencies, }) case ProfileFieldInventory: return json.Marshal(inventoryRecord{ Inventory: profile.Inventory, }) case ProfileFieldQuest: return json.Marshal(profileQuestRecord{ Quests: profile.Quests, }) case ProfileFieldMail: return json.Marshal(profileMailRecord{ Mailbox: profile.Mailbox, }) default: return nil, fmt.Errorf("unsupported profile field %q", field) } } func encodeProfileFields(profile Profile, fields []ProfileField) (map[string][]byte, error) { fields = normProfileFields(fields) if len(fields) == 0 { return nil, nil } out := make(map[string][]byte, len(fields)) for _, field := range fields { hashField, ok := profileDBHashField(field) if !ok { return nil, fmt.Errorf("unsupported profile field %q", field) } data, err := encodeProfileField(profile, field) if err != nil { return nil, err } out[hashField] = data } return out, nil } func decodeProfileField(profile *Profile, field ProfileField, data []byte) error { if profile == nil { return fmt.Errorf("profile is nil") } switch field { case ProfileFieldBase: if len(data) == 0 { return fmt.Errorf("profile field %s is empty", field) } var record profileBaseRecord if err := json.Unmarshal(data, &record); err != nil { return fmt.Errorf("decode profile field %s: %w", field, err) } profile.AccountID = record.AccountID profile.RoleID = record.RoleID profile.Name = record.Name profile.Level = record.Level return nil case ProfileFieldRuntime: if len(data) == 0 { return nil } var record profileRuntimeRecord if err := json.Unmarshal(data, &record); err != nil { return fmt.Errorf("decode profile field %s: %w", field, err) } profile.State = record.State profile.LoadedAt = record.LoadedAt profile.SavedAt = record.SavedAt profile.UpdatedAt = record.UpdatedAt return nil case ProfileFieldMeta: if len(data) == 0 { return nil } var record profileMetaRecord if err := json.Unmarshal(data, &record); err != nil { return fmt.Errorf("decode profile field %s: %w", field, err) } profile.Version = record.Version return nil case ProfileFieldCurrency: if len(data) == 0 { return nil } var record currencyRecord if err := json.Unmarshal(data, &record); err != nil { return fmt.Errorf("decode profile field %s: %w", field, err) } profile.Currencies = record.Currencies return nil case ProfileFieldInventory: if len(data) == 0 { return nil } var record inventoryRecord if err := json.Unmarshal(data, &record); err != nil { return fmt.Errorf("decode profile field %s: %w", field, err) } profile.Inventory = record.Inventory return nil case ProfileFieldQuest: if len(data) == 0 { return nil } var record profileQuestRecord if err := json.Unmarshal(data, &record); err != nil { return fmt.Errorf("decode profile field %s: %w", field, err) } profile.Quests = record.Quests return nil case ProfileFieldMail: if len(data) == 0 { return nil } var record profileMailRecord if err := json.Unmarshal(data, &record); err != nil { return fmt.Errorf("decode profile field %s: %w", field, err) } profile.Mailbox = record.Mailbox return nil default: return fmt.Errorf("unsupported profile field %q", field) } } func backfillProfileField(profile *Profile, field ProfileField, now time.Time) bool { if profile == nil { return false } switch field { case ProfileFieldCurrency: return backfillCurrency(profile, now) case ProfileFieldInventory: return backfillInventory(profile, now) case ProfileFieldQuest: return backfillProfileQuest(profile, now) case ProfileFieldMail: return backfillProfileMail(profile, now) default: return false } } func backfillModules(profile *Profile, now time.Time) []ProfileField { if profile == nil { return nil } dirty := make([]ProfileField, 0, len(genBackfillFields)) for _, field := range genBackfillFields { if backfillProfileField(profile, field, now) { dirty = append(dirty, field) } } return normProfileFields(dirty) } func redisModuleFields() []string { return []string{ redisBaseField, redisRuntimeField, redisMetaField, redisCurrencyField, redisInventoryField, redisQuestField, redisMailField, } } func profileToModules(profile Profile) (map[string][]byte, error) { out := make(map[string][]byte, len(genProfileDescs)) for _, descriptor := range genProfileDescs { data, err := encodeProfileField(profile, descriptor.Field) if err != nil { return nil, err } out[descriptor.HashField] = data } return out, nil } func profileFromRedis(accountID string, modules map[string][]byte) (Profile, error) { var profile Profile for _, descriptor := range genProfileDescs { if err := decodeProfileField(&profile, descriptor.Field, modules[descriptor.HashField]); err != nil { return Profile{}, fmt.Errorf("decode redis profile %s: %w", accountID, err) } } if profile.AccountID == "" { profile.AccountID = accountID } return profile, nil }