Initial commit

This commit is contained in:
Ilya Groshev
2026-04-14 09:28:26 +03:00
commit 02f511f40c
161 changed files with 21541 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
package userdata
import (
"encoding/json"
"fmt"
"strings"
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/store"
)
type DiffSet struct {
updates map[string]string
deletes map[string]string
}
func NewDiffSet(tables map[string]string) *DiffSet {
return &DiffSet{
updates: tables,
deletes: make(map[string]string),
}
}
func (ds *DiffSet) WithDeletes(table, deleteKeysJSON string) *DiffSet {
if deleteKeysJSON != "" && deleteKeysJSON != "[]" {
ds.deletes[table] = deleteKeysJSON
}
return ds
}
func (ds *DiffSet) Build() map[string]*pb.DiffData {
diff := make(map[string]*pb.DiffData, len(ds.updates))
for table, payload := range ds.updates {
diff[table] = ds.entry(table, payload)
}
return diff
}
func (ds *DiffSet) BuildOrdered(order []string) map[string]*pb.DiffData {
diff := make(map[string]*pb.DiffData, len(order))
for _, table := range order {
payload := ds.updates[table]
diff[table] = ds.entry(table, payload)
}
return diff
}
func (ds *DiffSet) entry(table, payload string) *pb.DiffData {
if payload == "" {
payload = "[]"
}
deleteKeys := "[]"
if dk, ok := ds.deletes[table]; ok {
deleteKeys = dk
}
return &pb.DiffData{
UpdateRecordsJson: payload,
DeleteKeysJson: deleteKeys,
}
}
type trackedTable struct {
tableName string
keyFields []string
oldRecords []map[string]any
recordsFn func(store.UserState) []map[string]any
}
type DeleteTracker struct {
entries []trackedTable
}
func NewDeleteTracker() *DeleteTracker {
return &DeleteTracker{}
}
func (dt *DeleteTracker) Track(tableName string, old store.UserState, recordsFn func(store.UserState) []map[string]any, keyFields []string) *DeleteTracker {
dt.entries = append(dt.entries, trackedTable{
tableName: tableName,
keyFields: keyFields,
oldRecords: recordsFn(old),
recordsFn: recordsFn,
})
return dt
}
func (dt *DeleteTracker) Apply(newState store.UserState, tables map[string]string) map[string]*pb.DiffData {
ds := NewDiffSet(tables)
for _, e := range dt.entries {
newRecords := e.recordsFn(newState)
ds.WithDeletes(e.tableName, ComputeDeleteKeys(e.oldRecords, newRecords, e.keyFields))
}
return ds.Build()
}
func ComputeDeleteKeys(oldRecords, newRecords []map[string]any, keyFields []string) string {
if len(oldRecords) == 0 {
return "[]"
}
newSet := make(map[string]struct{}, len(newRecords))
for _, r := range newRecords {
newSet[compositeKey(r, keyFields)] = struct{}{}
}
var deleted []map[string]any
for _, r := range oldRecords {
if _, exists := newSet[compositeKey(r, keyFields)]; !exists {
deleted = append(deleted, r)
}
}
if len(deleted) == 0 {
return "[]"
}
b, err := json.Marshal(deleted)
if err != nil {
return "[]"
}
return string(b)
}
func compositeKey(record map[string]any, fields []string) string {
var sb strings.Builder
for i, f := range fields {
if i > 0 {
sb.WriteByte('|')
}
sb.WriteString(fmt.Sprint(record[f]))
}
return sb.String()
}
+9
View File
@@ -0,0 +1,9 @@
package userdata
import (
pb "lunar-tear/server/gen/proto"
)
func EmptyDiff() map[string]*pb.DiffData {
return map[string]*pb.DiffData{}
}
+159
View File
@@ -0,0 +1,159 @@
package userdata
import (
"sort"
"lunar-tear/server/internal/store"
)
func init() {
register("IUserBigHuntProgressStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentBigHuntBossQuestId": user.BigHuntProgress.CurrentBigHuntBossQuestId,
"currentBigHuntQuestId": user.BigHuntProgress.CurrentBigHuntQuestId,
"currentQuestSceneId": user.BigHuntProgress.CurrentQuestSceneId,
"isDryRun": user.BigHuntProgress.IsDryRun,
"latestVersion": user.BigHuntProgress.LatestVersion,
})
return s
})
register("IUserBigHuntMaxScore", func(user store.UserState) string {
if len(user.BigHuntMaxScores) == 0 {
return "[]"
}
ids := make([]int, 0, len(user.BigHuntMaxScores))
for id := range user.BigHuntMaxScores {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
ms := user.BigHuntMaxScores[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"bigHuntBossId": int32(id),
"maxScore": ms.MaxScore,
"maxScoreUpdateDatetime": ms.MaxScoreUpdateDatetime,
"latestVersion": ms.LatestVersion,
})
}
s, _ := encodeJSONMaps(records...)
return s
})
register("IUserBigHuntStatus", func(user store.UserState) string {
if len(user.BigHuntStatuses) == 0 {
return "[]"
}
ids := make([]int, 0, len(user.BigHuntStatuses))
for id := range user.BigHuntStatuses {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
st := user.BigHuntStatuses[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"bigHuntBossQuestId": int32(id),
"dailyChallengeCount": st.DailyChallengeCount,
"latestChallengeDatetime": st.LatestChallengeDatetime,
"latestVersion": st.LatestVersion,
})
}
s, _ := encodeJSONMaps(records...)
return s
})
register("IUserBigHuntScheduleMaxScore", func(user store.UserState) string {
if len(user.BigHuntScheduleMaxScores) == 0 {
return "[]"
}
type sortableKey struct {
ScheduleId int32
BossId int32
}
keys := make([]sortableKey, 0, len(user.BigHuntScheduleMaxScores))
for k := range user.BigHuntScheduleMaxScores {
keys = append(keys, sortableKey{k.BigHuntScheduleId, k.BigHuntBossId})
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].ScheduleId != keys[j].ScheduleId {
return keys[i].ScheduleId < keys[j].ScheduleId
}
return keys[i].BossId < keys[j].BossId
})
records := make([]map[string]any, 0, len(keys))
for _, k := range keys {
ms := user.BigHuntScheduleMaxScores[store.BigHuntScheduleScoreKey{BigHuntScheduleId: k.ScheduleId, BigHuntBossId: k.BossId}]
records = append(records, map[string]any{
"userId": user.UserId,
"bigHuntScheduleId": k.ScheduleId,
"bigHuntBossId": k.BossId,
"maxScore": ms.MaxScore,
"maxScoreUpdateDatetime": ms.MaxScoreUpdateDatetime,
"latestVersion": ms.LatestVersion,
})
}
s, _ := encodeJSONMaps(records...)
return s
})
register("IUserBigHuntWeeklyMaxScore", func(user store.UserState) string {
if len(user.BigHuntWeeklyMaxScores) == 0 {
return "[]"
}
type sortableKey struct {
WeeklyVersion int64
AttributeType int32
}
keys := make([]sortableKey, 0, len(user.BigHuntWeeklyMaxScores))
for k := range user.BigHuntWeeklyMaxScores {
keys = append(keys, sortableKey{k.BigHuntWeeklyVersion, k.AttributeType})
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].WeeklyVersion != keys[j].WeeklyVersion {
return keys[i].WeeklyVersion < keys[j].WeeklyVersion
}
return keys[i].AttributeType < keys[j].AttributeType
})
records := make([]map[string]any, 0, len(keys))
for _, k := range keys {
ms := user.BigHuntWeeklyMaxScores[store.BigHuntWeeklyScoreKey{BigHuntWeeklyVersion: k.WeeklyVersion, AttributeType: k.AttributeType}]
records = append(records, map[string]any{
"userId": user.UserId,
"bigHuntWeeklyVersion": k.WeeklyVersion,
"attributeType": k.AttributeType,
"maxScore": ms.MaxScore,
"latestVersion": ms.LatestVersion,
})
}
s, _ := encodeJSONMaps(records...)
return s
})
register("IUserBigHuntWeeklyStatus", func(user store.UserState) string {
if len(user.BigHuntWeeklyStatuses) == 0 {
return "[]"
}
versions := make([]int64, 0, len(user.BigHuntWeeklyStatuses))
for v := range user.BigHuntWeeklyStatuses {
versions = append(versions, v)
}
sort.Slice(versions, func(i, j int) bool { return versions[i] < versions[j] })
records := make([]map[string]any, 0, len(versions))
for _, v := range versions {
ws := user.BigHuntWeeklyStatuses[v]
records = append(records, map[string]any{
"userId": user.UserId,
"bigHuntWeeklyVersion": v,
"isReceivedWeeklyReward": ws.IsReceivedWeeklyReward,
"latestVersion": ws.LatestVersion,
})
}
s, _ := encodeJSONMaps(records...)
return s
})
}
@@ -0,0 +1,109 @@
package userdata
import (
"sort"
"lunar-tear/server/internal/store"
)
func init() {
register("IUserCharacterBoard", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCharacterBoardRecords(user)...)
return s
})
register("IUserCharacterBoardAbility", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCharacterBoardAbilityRecords(user)...)
return s
})
register("IUserCharacterBoardStatusUp", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCharacterBoardStatusUpRecords(user)...)
return s
})
registerStatic("IUserCharacterBoardCompleteReward")
}
func sortedCharacterBoardRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.CharacterBoards))
for id := range user.CharacterBoards {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.CharacterBoards[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"characterBoardId": row.CharacterBoardId,
"panelReleaseBit1": row.PanelReleaseBit1,
"panelReleaseBit2": row.PanelReleaseBit2,
"panelReleaseBit3": row.PanelReleaseBit3,
"panelReleaseBit4": row.PanelReleaseBit4,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedCharacterBoardAbilityRecords(user store.UserState) []map[string]any {
type entry struct {
key store.CharacterBoardAbilityKey
state store.CharacterBoardAbilityState
}
entries := make([]entry, 0, len(user.CharacterBoardAbilities))
for k, v := range user.CharacterBoardAbilities {
entries = append(entries, entry{k, v})
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].key.CharacterId != entries[j].key.CharacterId {
return entries[i].key.CharacterId < entries[j].key.CharacterId
}
return entries[i].key.AbilityId < entries[j].key.AbilityId
})
records := make([]map[string]any, 0, len(entries))
for _, e := range entries {
records = append(records, map[string]any{
"userId": user.UserId,
"characterId": e.state.CharacterId,
"abilityId": e.state.AbilityId,
"level": e.state.Level,
"latestVersion": e.state.LatestVersion,
})
}
return records
}
func sortedCharacterBoardStatusUpRecords(user store.UserState) []map[string]any {
type entry struct {
key store.CharacterBoardStatusUpKey
state store.CharacterBoardStatusUpState
}
entries := make([]entry, 0, len(user.CharacterBoardStatusUps))
for k, v := range user.CharacterBoardStatusUps {
entries = append(entries, entry{k, v})
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].key.CharacterId != entries[j].key.CharacterId {
return entries[i].key.CharacterId < entries[j].key.CharacterId
}
return entries[i].key.StatusCalculationType < entries[j].key.StatusCalculationType
})
records := make([]map[string]any, 0, len(entries))
for _, e := range entries {
records = append(records, map[string]any{
"userId": user.UserId,
"characterId": e.state.CharacterId,
"statusCalculationType": e.state.StatusCalculationType,
"hp": e.state.Hp,
"attack": e.state.Attack,
"vitality": e.state.Vitality,
"agility": e.state.Agility,
"criticalRatio": e.state.CriticalRatio,
"criticalAttack": e.state.CriticalAttack,
"latestVersion": e.state.LatestVersion,
})
}
return records
}
+181
View File
@@ -0,0 +1,181 @@
package userdata
import (
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func init() {
register("IUserDeck", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedDeckRecords(user)...)
return s
})
register("IUserDeckCharacter", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedDeckCharacterRecords(user)...)
return s
})
register("IUserDeckSubWeaponGroup", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedDeckSubWeaponGroupRecords(user)...)
return s
})
register("IUserDeckTypeNote", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedDeckTypeNoteRecords(user)...)
return s
})
register("IUserDeckPartsGroup", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedDeckPartsGroupRecords(user)...)
return s
})
register("IUserDeckCharacterDressupCostume", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedDeckDressupCostumeRecords(user)...)
return s
})
registerStatic(
"IUserDeckLimitContentRestricted",
)
}
func sortedDeckRecords(user store.UserState) []map[string]any {
keys := make([]store.DeckKey, 0, len(user.Decks))
for key := range user.Decks {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].DeckType != keys[j].DeckType {
return keys[i].DeckType < keys[j].DeckType
}
return keys[i].UserDeckNumber < keys[j].UserDeckNumber
})
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Decks[key]
records = append(records, map[string]any{
"userId": user.UserId,
"deckType": row.DeckType,
"userDeckNumber": row.UserDeckNumber,
"userDeckCharacterUuid01": row.UserDeckCharacterUuid01,
"userDeckCharacterUuid02": row.UserDeckCharacterUuid02,
"userDeckCharacterUuid03": row.UserDeckCharacterUuid03,
"name": row.Name,
"power": row.Power,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedDeckCharacterRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.DeckCharacters)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.DeckCharacters[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userDeckCharacterUuid": row.UserDeckCharacterUuid,
"userCostumeUuid": row.UserCostumeUuid,
"mainUserWeaponUuid": row.MainUserWeaponUuid,
"userCompanionUuid": row.UserCompanionUuid,
"power": row.Power,
"userThoughtUuid": row.UserThoughtUuid,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedDeckSubWeaponGroupRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.DeckSubWeapons)
records := make([]map[string]any, 0)
for _, dcUuid := range keys {
weapons := user.DeckSubWeapons[dcUuid]
var lv int64
if dc, ok := user.DeckCharacters[dcUuid]; ok {
lv = dc.LatestVersion
}
for idx, weaponUuid := range weapons {
records = append(records, map[string]any{
"userId": user.UserId,
"userDeckCharacterUuid": dcUuid,
"userWeaponUuid": weaponUuid,
"sortOrder": int32(idx + 1),
"latestVersion": lv,
})
}
}
return records
}
func sortedDeckTypeNoteRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.DeckTypeNotes))
for id := range user.DeckTypeNotes {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.DeckTypeNotes[model.DeckType(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"deckType": row.DeckType,
"maxDeckPower": row.MaxDeckPower,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedDeckPartsGroupRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.DeckParts)
records := make([]map[string]any, 0)
for _, dcUuid := range keys {
parts := user.DeckParts[dcUuid]
var lv int64
if dc, ok := user.DeckCharacters[dcUuid]; ok {
lv = dc.LatestVersion
}
for idx, partsUuid := range parts {
records = append(records, map[string]any{
"userId": user.UserId,
"userDeckCharacterUuid": dcUuid,
"userPartsUuid": partsUuid,
"sortOrder": int32(idx + 1),
"latestVersion": lv,
})
}
}
return records
}
func DeckSubWeaponRecords(user store.UserState) []map[string]any {
return sortedDeckSubWeaponGroupRecords(user)
}
func DeckPartsGroupRecords(user store.UserState) []map[string]any {
return sortedDeckPartsGroupRecords(user)
}
func sortedDeckDressupCostumeRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.DeckCharacters)
records := make([]map[string]any, 0)
for _, key := range keys {
row := user.DeckCharacters[key]
if row.DressupCostumeId == 0 {
continue
}
records = append(records, map[string]any{
"userId": user.UserId,
"userDeckCharacterUuid": row.UserDeckCharacterUuid,
"dressupCostumeId": row.DressupCostumeId,
"latestVersion": row.LatestVersion,
})
}
return records
}
func DeckDressupCostumeRecords(user store.UserState) []map[string]any {
return sortedDeckDressupCostumeRecords(user)
}
+128
View File
@@ -0,0 +1,128 @@
package userdata
import (
"sort"
"lunar-tear/server/internal/store"
)
func init() {
register("IUserGimmick", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedGimmickRecords(user)...)
return s
})
register("IUserGimmickOrnamentProgress", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedGimmickOrnamentProgressRecords(user)...)
return s
})
register("IUserGimmickSequence", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedGimmickSequenceRecords(user)...)
return s
})
register("IUserGimmickUnlock", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedGimmickUnlockRecords(user)...)
return s
})
}
func sortedGimmickRecords(user store.UserState) []map[string]any {
keys := make([]store.GimmickKey, 0, len(user.Gimmick.Progress))
for key := range user.Gimmick.Progress {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
return compareGimmickKey(keys[i], keys[j]) < 0
})
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Gimmick.Progress[key]
records = append(records, map[string]any{
"userId": user.UserId,
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
"gimmickSequenceId": row.Key.GimmickSequenceId,
"gimmickId": row.Key.GimmickId,
"isGimmickCleared": row.IsGimmickCleared,
"startDatetime": row.StartDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedGimmickOrnamentProgressRecords(user store.UserState) []map[string]any {
keys := make([]store.GimmickOrnamentKey, 0, len(user.Gimmick.OrnamentProgress))
for key := range user.Gimmick.OrnamentProgress {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
return compareGimmickOrnamentKey(keys[i], keys[j]) < 0
})
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Gimmick.OrnamentProgress[key]
records = append(records, map[string]any{
"userId": user.UserId,
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
"gimmickSequenceId": row.Key.GimmickSequenceId,
"gimmickId": row.Key.GimmickId,
"gimmickOrnamentIndex": row.Key.GimmickOrnamentIndex,
"progressValueBit": row.ProgressValueBit,
"baseDatetime": row.BaseDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedGimmickSequenceRecords(user store.UserState) []map[string]any {
keys := make([]store.GimmickSequenceKey, 0, len(user.Gimmick.Sequences))
for key := range user.Gimmick.Sequences {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].GimmickSequenceScheduleId != keys[j].GimmickSequenceScheduleId {
return keys[i].GimmickSequenceScheduleId < keys[j].GimmickSequenceScheduleId
}
return keys[i].GimmickSequenceId < keys[j].GimmickSequenceId
})
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Gimmick.Sequences[key]
records = append(records, map[string]any{
"userId": user.UserId,
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
"gimmickSequenceId": row.Key.GimmickSequenceId,
"isGimmickSequenceCleared": row.IsGimmickSequenceCleared,
"clearDatetime": row.ClearDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedGimmickUnlockRecords(user store.UserState) []map[string]any {
keys := make([]store.GimmickKey, 0, len(user.Gimmick.Unlocks))
for key := range user.Gimmick.Unlocks {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
return compareGimmickKey(keys[i], keys[j]) < 0
})
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Gimmick.Unlocks[key]
records = append(records, map[string]any{
"userId": user.UserId,
"gimmickSequenceScheduleId": row.Key.GimmickSequenceScheduleId,
"gimmickSequenceId": row.Key.GimmickSequenceId,
"gimmickId": row.Key.GimmickId,
"isUnlocked": row.IsUnlocked,
"latestVersion": row.LatestVersion,
})
}
return records
}
+583
View File
@@ -0,0 +1,583 @@
package userdata
import (
"log"
"sort"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/store"
)
func init() {
register("IUserCharacter", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCharacterRecords(user)...)
return s
})
register("IUserCostume", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCostumeRecords(user)...)
return s
})
register("IUserWeapon", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedWeaponRecords(user)...)
return s
})
register("IUserWeaponStory", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedWeaponStoryRecords(user)...)
return s
})
register("IUserWeaponNote", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedWeaponNoteRecords(user)...)
return s
})
register("IUserCompanion", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCompanionRecords(user)...)
return s
})
register("IUserThought", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedThoughtRecords(user)...)
return s
})
register("IUserConsumableItem", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedConsumableItemRecords(user)...)
return s
})
register("IUserMaterial", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedMaterialRecords(user)...)
return s
})
register("IUserImportantItem", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedImportantItemRecords(user)...)
return s
})
register("IUserPremiumItem", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedPremiumItemRecords(user)...)
return s
})
register("IUserParts", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedPartsRecords(user)...)
return s
})
register("IUserCostumeActiveSkill", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCostumeActiveSkillRecords(user)...)
return s
})
register("IUserWeaponSkill", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedWeaponSkillRecords(user)...)
return s
})
register("IUserWeaponAbility", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedWeaponAbilityRecords(user)...)
return s
})
register("IUserExplore", func(user store.UserState) string {
s, _ := encodeJSONMaps(exploreRecord(user))
return s
})
register("IUserExploreScore", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedExploreScoreRecords(user)...)
return s
})
register("IUserPartsGroupNote", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedPartsGroupNoteRecords(user)...)
return s
})
register("IUserPartsPreset", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedPartsPresetRecords(user)...)
return s
})
register("IUserCostumeAwakenStatusUp", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCostumeAwakenStatusUpRecords(user)...)
return s
})
register("IUserAutoSaleSettingDetail", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedAutoSaleSettingRecords(user)...)
return s
})
register("IUserCharacterRebirth", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCharacterRebirthRecords(user)...)
return s
})
register("IUserCageOrnamentReward", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCageOrnamentRewardRecords(user)...)
return s
})
registerStatic(
"IUserCostumeLevelBonusReleaseStatus",
"IUserCostumeLotteryEffect",
"IUserCostumeLotteryEffectAbility",
"IUserCostumeLotteryEffectStatusUp",
"IUserCostumeLotteryEffectPending",
"IUserWeaponAwaken",
"IUserPartsPresetTag",
"IUserPartsStatusSub",
)
}
func sortedCharacterRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.Characters))
for id := range user.Characters {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.Characters[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"characterId": row.CharacterId,
"level": row.Level,
"exp": row.Exp,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedCostumeRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.Costumes)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Costumes[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"costumeId": row.CostumeId,
"limitBreakCount": row.LimitBreakCount,
"level": row.Level,
"exp": row.Exp,
"headupDisplayViewId": row.HeadupDisplayViewId,
"acquisitionDatetime": row.AcquisitionDatetime,
"awakenCount": row.AwakenCount,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedAutoSaleSettingRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.AutoSaleSettings))
for id := range user.AutoSaleSettings {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.AutoSaleSettings[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"possessionAutoSaleItemType": row.PossessionAutoSaleItemType,
"possessionAutoSaleItemValue": row.PossessionAutoSaleItemValue,
"latestVersion": gametime.NowMillis(),
})
}
return records
}
func sortedCostumeAwakenStatusUpRecords(user store.UserState) []map[string]any {
keys := make([]store.CostumeAwakenStatusKey, 0, len(user.CostumeAwakenStatusUps))
for k := range user.CostumeAwakenStatusUps {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].UserCostumeUuid != keys[j].UserCostumeUuid {
return keys[i].UserCostumeUuid < keys[j].UserCostumeUuid
}
return keys[i].StatusCalculationType < keys[j].StatusCalculationType
})
records := make([]map[string]any, 0, len(keys))
for _, k := range keys {
row := user.CostumeAwakenStatusUps[k]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"statusCalculationType": int32(row.StatusCalculationType),
"hp": row.Hp,
"attack": row.Attack,
"vitality": row.Vitality,
"agility": row.Agility,
"criticalRatio": row.CriticalRatio,
"criticalAttack": row.CriticalAttack,
"latestVersion": row.LatestVersion,
})
}
return records
}
func SortedWeaponRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.Weapons)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Weapons[key]
uuid := row.UserWeaponUuid
if uuid == "" {
log.Printf("[userdata] sortedWeaponRecords: using key as fallback for weapon key=%q (empty userWeaponUuid)", key)
uuid = key
}
records = append(records, map[string]any{
"userId": user.UserId,
"userWeaponUuid": uuid,
"weaponId": row.WeaponId,
"level": row.Level,
"exp": row.Exp,
"limitBreakCount": row.LimitBreakCount,
"isProtected": row.IsProtected,
"acquisitionDatetime": row.AcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedWeaponStoryRecords(user store.UserState) []map[string]any {
if user.WeaponStories == nil {
return []map[string]any{}
}
weaponIdsInWeapons := make(map[int32]bool)
for _, row := range user.Weapons {
weaponIdsInWeapons[row.WeaponId] = true
}
weaponIds := make([]int32, 0, len(user.WeaponStories))
for weaponId := range user.WeaponStories {
if weaponIdsInWeapons[weaponId] {
weaponIds = append(weaponIds, weaponId)
}
}
sort.Slice(weaponIds, func(i, j int) bool { return weaponIds[i] < weaponIds[j] })
records := make([]map[string]any, 0, len(weaponIds))
for _, weaponId := range weaponIds {
row := user.WeaponStories[weaponId]
records = append(records, map[string]any{
"userId": user.UserId,
"weaponId": row.WeaponId,
"releasedMaxStoryIndex": row.ReleasedMaxStoryIndex,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedWeaponNoteRecords(user store.UserState) []map[string]any {
weaponIds := make([]int32, 0, len(user.WeaponNotes))
for id := range user.WeaponNotes {
weaponIds = append(weaponIds, id)
}
sort.Slice(weaponIds, func(i, j int) bool { return weaponIds[i] < weaponIds[j] })
records := make([]map[string]any, 0, len(weaponIds))
for _, id := range weaponIds {
row := user.WeaponNotes[id]
records = append(records, map[string]any{
"userId": user.UserId,
"weaponId": row.WeaponId,
"maxLevel": row.MaxLevel,
"maxLimitBreakCount": row.MaxLimitBreakCount,
"firstAcquisitionDatetime": row.FirstAcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedCompanionRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.Companions)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Companions[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userCompanionUuid": row.UserCompanionUuid,
"companionId": row.CompanionId,
"headupDisplayViewId": row.HeadupDisplayViewId,
"level": row.Level,
"acquisitionDatetime": row.AcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedThoughtRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.Thoughts)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Thoughts[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userThoughtUuid": row.UserThoughtUuid,
"thoughtId": row.ThoughtId,
"acquisitionDatetime": row.AcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func SortedConsumableItemRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.ConsumableItems))
for id := range user.ConsumableItems {
ids = append(ids, int(id))
}
sort.Ints(ids)
nowMillis := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"consumableItemId": int32(id),
"count": user.ConsumableItems[int32(id)],
"firstAcquisitionDatetime": nowMillis,
"latestVersion": nowMillis,
})
}
return records
}
func SortedMaterialRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.Materials))
for id := range user.Materials {
ids = append(ids, int(id))
}
sort.Ints(ids)
nowMillis := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"materialId": int32(id),
"count": user.Materials[int32(id)],
"firstAcquisitionDatetime": nowMillis,
"latestVersion": nowMillis,
})
}
return records
}
func sortedImportantItemRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.ImportantItems))
for id := range user.ImportantItems {
ids = append(ids, int(id))
}
sort.Ints(ids)
nowMillis := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"importantItemId": int32(id),
"count": user.ImportantItems[int32(id)],
"firstAcquisitionDatetime": nowMillis,
"latestVersion": nowMillis,
})
}
return records
}
func sortedPremiumItemRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.PremiumItems))
for id := range user.PremiumItems {
ids = append(ids, int(id))
}
sort.Ints(ids)
nowMillis := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
acqTime := user.PremiumItems[int32(id)]
if acqTime == 0 {
acqTime = nowMillis
}
records = append(records, map[string]any{
"userId": user.UserId,
"premiumItemId": int32(id),
"acquisitionDatetime": acqTime,
"latestVersion": nowMillis,
})
}
return records
}
func SortedPartsRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.Parts)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.Parts[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userPartsUuid": row.UserPartsUuid,
"partsId": row.PartsId,
"level": row.Level,
"partsStatusMainId": row.PartsStatusMainId,
"isProtected": row.IsProtected,
"acquisitionDatetime": row.AcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedPartsGroupNoteRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.PartsGroupNotes))
for id := range user.PartsGroupNotes {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.PartsGroupNotes[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"partsGroupId": row.PartsGroupId,
"firstAcquisitionDatetime": row.FirstAcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedPartsPresetRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.PartsPresets))
for id := range user.PartsPresets {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.PartsPresets[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"userPartsPresetNumber": row.UserPartsPresetNumber,
"userPartsUuid01": row.UserPartsUuid01,
"userPartsUuid02": row.UserPartsUuid02,
"userPartsUuid03": row.UserPartsUuid03,
"name": row.Name,
"userPartsPresetTagNumber": row.UserPartsPresetTagNumber,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedCostumeActiveSkillRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.CostumeActiveSkills)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.CostumeActiveSkills[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"level": row.Level,
"acquisitionDatetime": row.AcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func SortedWeaponSkillRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.WeaponSkills)
records := make([]map[string]any, 0)
for _, key := range keys {
for _, row := range user.WeaponSkills[key] {
records = append(records, map[string]any{
"userId": user.UserId,
"userWeaponUuid": row.UserWeaponUuid,
"slotNumber": row.SlotNumber,
"level": row.Level,
"latestVersion": int64(0),
})
}
}
return records
}
func SortedWeaponAbilityRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.WeaponAbilities)
records := make([]map[string]any, 0)
for _, key := range keys {
for _, row := range user.WeaponAbilities[key] {
records = append(records, map[string]any{
"userId": user.UserId,
"userWeaponUuid": row.UserWeaponUuid,
"slotNumber": row.SlotNumber,
"level": row.Level,
"latestVersion": int64(0),
})
}
}
return records
}
func exploreRecord(user store.UserState) map[string]any {
return map[string]any{
"userId": user.UserId,
"isUseExploreTicket": user.Explore.IsUseExploreTicket,
"playingExploreId": user.Explore.PlayingExploreId,
"latestPlayDatetime": user.Explore.LatestPlayDatetime,
"latestVersion": user.Explore.LatestVersion,
}
}
func sortedCharacterRebirthRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.CharacterRebirths))
for id := range user.CharacterRebirths {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.CharacterRebirths[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"characterId": row.CharacterId,
"rebirthCount": row.RebirthCount,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedExploreScoreRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.ExploreScores))
for id := range user.ExploreScores {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.ExploreScores[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"exploreId": row.ExploreId,
"maxScore": row.MaxScore,
"maxScoreUpdateDatetime": row.MaxScoreUpdateDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedCageOrnamentRewardRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.CageOrnamentRewards))
for id := range user.CageOrnamentRewards {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.CageOrnamentRewards[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"cageOrnamentId": row.CageOrnamentId,
"acquisitionDatetime": row.AcquisitionDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
+203
View File
@@ -0,0 +1,203 @@
package userdata
import (
"sort"
"lunar-tear/server/internal/store"
)
func sortedQuestRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.Quests))
for id := range user.Quests {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.Quests[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"questId": row.QuestId,
"questStateType": row.QuestStateType,
"isBattleOnly": row.IsBattleOnly,
"latestStartDatetime": row.LatestStartDatetime,
"clearCount": row.ClearCount,
"dailyClearCount": row.DailyClearCount,
"lastClearDatetime": row.LastClearDatetime,
"shortestClearFrames": row.ShortestClearFrames,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedQuestMissionRecords(user store.UserState) []map[string]any {
keys := make([]store.QuestMissionKey, 0, len(user.QuestMissions))
for key := range user.QuestMissions {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].QuestId != keys[j].QuestId {
return keys[i].QuestId < keys[j].QuestId
}
return keys[i].QuestMissionId < keys[j].QuestMissionId
})
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.QuestMissions[key]
records = append(records, map[string]any{
"userId": user.UserId,
"questId": row.QuestId,
"questMissionId": row.QuestMissionId,
"progressValue": row.ProgressValue,
"isClear": row.IsClear,
"latestClearDatetime": row.LatestClearDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func init() {
register("IUserQuest", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedQuestRecords(user)...)
return s
})
register("IUserQuestMission", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedQuestMissionRecords(user)...)
return s
})
register("IUserMainQuestFlowStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentQuestFlowType": user.MainQuest.CurrentQuestFlowType,
"latestVersion": user.MainQuest.LatestVersion,
})
return s
})
register("IUserMainQuestMainFlowStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentMainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId,
"currentQuestSceneId": user.MainQuest.CurrentQuestSceneId,
"headQuestSceneId": user.MainQuest.HeadQuestSceneId,
"isReachedLastQuestScene": user.MainQuest.IsReachedLastQuestScene,
"latestVersion": user.MainQuest.LatestVersion,
})
return s
})
register("IUserMainQuestProgressStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentQuestSceneId": user.MainQuest.ProgressQuestSceneId,
"headQuestSceneId": user.MainQuest.ProgressHeadQuestSceneId,
"currentQuestFlowType": user.MainQuest.ProgressQuestFlowType,
"latestVersion": user.MainQuest.LatestVersion,
})
return s
})
register("IUserMainQuestSeasonRoute", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"mainQuestSeasonId": user.MainQuest.MainQuestSeasonId,
"mainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId,
"latestVersion": user.MainQuest.LatestVersion,
})
return s
})
register("IUserEventQuestProgressStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentEventQuestChapterId": user.EventQuest.CurrentEventQuestChapterId,
"currentQuestId": user.EventQuest.CurrentQuestId,
"currentQuestSceneId": user.EventQuest.CurrentQuestSceneId,
"headQuestSceneId": user.EventQuest.HeadQuestSceneId,
"latestVersion": user.EventQuest.LatestVersion,
})
return s
})
register("IUserExtraQuestProgressStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentQuestId": user.ExtraQuest.CurrentQuestId,
"currentQuestSceneId": user.ExtraQuest.CurrentQuestSceneId,
"headQuestSceneId": user.ExtraQuest.HeadQuestSceneId,
"latestVersion": user.ExtraQuest.LatestVersion,
})
return s
})
register("IUserMainQuestReplayFlowStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentHeadQuestSceneId": user.MainQuest.ReplayFlowHeadQuestSceneId,
"currentQuestSceneId": user.MainQuest.ReplayFlowCurrentQuestSceneId,
"latestVersion": user.MainQuest.LatestVersion,
})
return s
})
register("IUserSideStoryQuestSceneProgressStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"currentSideStoryQuestId": user.SideStoryActiveProgress.CurrentSideStoryQuestId,
"currentSideStoryQuestSceneId": user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId,
"latestVersion": user.SideStoryActiveProgress.LatestVersion,
})
return s
})
register("IUserSideStoryQuest", func(user store.UserState) string {
if len(user.SideStoryQuests) == 0 {
return "[]"
}
ids := make([]int, 0, len(user.SideStoryQuests))
for id := range user.SideStoryQuests {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
progress := user.SideStoryQuests[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"sideStoryQuestId": int32(id),
"headSideStoryQuestSceneId": progress.HeadSideStoryQuestSceneId,
"sideStoryQuestStateType": progress.SideStoryQuestStateType,
"latestVersion": progress.LatestVersion,
})
}
s, _ := encodeJSONMaps(records...)
return s
})
register("IUserQuestLimitContentStatus", func(user store.UserState) string {
if len(user.QuestLimitContentStatus) == 0 {
return "[]"
}
ids := make([]int, 0, len(user.QuestLimitContentStatus))
for id := range user.QuestLimitContentStatus {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
st := user.QuestLimitContentStatus[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"questId": int32(id),
"limitContentQuestStatusType": st.LimitContentQuestStatusType,
"eventQuestChapterId": st.EventQuestChapterId,
"latestVersion": st.LatestVersion,
})
}
s, _ := encodeJSONMaps(records...)
return s
})
registerStatic(
"IUserEventQuestDailyGroupCompleteReward",
"IUserEventQuestLabyrinthSeason",
"IUserEventQuestLabyrinthStage",
"IUserEventQuestTowerAccumulationReward",
"IUserQuestReplayFlowRewardGroup",
"IUserQuestAutoOrbit",
"IUserQuestSceneChoice",
"IUserQuestSceneChoiceHistory",
)
}
+333
View File
@@ -0,0 +1,333 @@
package userdata
import (
"sort"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/store"
)
func init() {
register("IUser", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"playerId": user.PlayerId,
"osType": user.OsType,
"platformType": user.PlatformType,
"userRestrictionType": user.UserRestrictionType,
"registerDatetime": user.RegisterDatetime,
"gameStartDatetime": user.GameStartDatetime,
"latestVersion": user.LatestVersion,
})
return s
})
register("IUserSetting", func(user store.UserState) string {
s, _ := encodeJSONRecords(&EntityIUserSetting{
UserId: user.UserId,
IsNotifyPurchaseAlert: user.Setting.IsNotifyPurchaseAlert,
LatestVersion: user.Setting.LatestVersion,
})
return s
})
register("IUserStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"level": user.Status.Level,
"exp": user.Status.Exp,
"staminaMilliValue": user.Status.StaminaMilliValue,
"staminaUpdateDatetime": user.Status.StaminaUpdateDatetime,
"latestVersion": user.Status.LatestVersion,
})
return s
})
register("IUserGem", func(user store.UserState) string {
s, _ := encodeJSONRecords(&EntityIUserGem{
UserId: user.UserId,
PaidGem: user.Gem.PaidGem,
FreeGem: user.Gem.FreeGem,
})
return s
})
register("IUserProfile", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"name": user.Profile.Name,
"nameUpdateDatetime": user.Profile.NameUpdateDatetime,
"message": user.Profile.Message,
"messageUpdateDatetime": user.Profile.MessageUpdateDatetime,
"favoriteCostumeId": user.Profile.FavoriteCostumeId,
"favoriteCostumeIdUpdateDatetime": user.Profile.FavoriteCostumeIdUpdateDatetime,
"latestVersion": user.Profile.LatestVersion,
})
return s
})
register("IUserLogin", func(user store.UserState) string {
s, _ := encodeJSONRecords(&EntityIUserLogin{
UserId: user.UserId,
TotalLoginCount: user.Login.TotalLoginCount,
ContinualLoginCount: user.Login.ContinualLoginCount,
MaxContinualLoginCount: user.Login.MaxContinualLoginCount,
LastLoginDatetime: user.Login.LastLoginDatetime,
LastComebackLoginDatetime: user.Login.LastComebackLoginDatetime,
LatestVersion: user.Login.LatestVersion,
})
return s
})
register("IUserLoginBonus", func(user store.UserState) string {
s, _ := encodeJSONRecords(&EntityIUserLoginBonus{
UserId: user.UserId,
LoginBonusId: user.LoginBonus.LoginBonusId,
CurrentPageNumber: user.LoginBonus.CurrentPageNumber,
CurrentStampNumber: user.LoginBonus.CurrentStampNumber,
LatestRewardReceiveDatetime: user.LoginBonus.LatestRewardReceiveDatetime,
LatestVersion: user.LoginBonus.LatestVersion,
})
return s
})
register("IUserTutorialProgress", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedTutorialRecords(user)...)
return s
})
register("IUserMission", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedMissionRecords(user)...)
return s
})
register("IUserNaviCutIn", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedNaviCutInRecords(user)...)
return s
})
register("IUserMovie", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedMovieRecords(user)...)
return s
})
register("IUserContentsStory", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedContentsStoryRecords(user)...)
return s
})
register("IUserOmikuji", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedOmikujiRecords(user)...)
return s
})
register("IUserDokan", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedDokanRecords(user)...)
return s
})
register("IUserPortalCageStatus", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"isCurrentProgress": user.PortalCageStatus.IsCurrentProgress,
"dropItemStartDatetime": user.PortalCageStatus.DropItemStartDatetime,
"currentDropItemCount": user.PortalCageStatus.CurrentDropItemCount,
"latestVersion": user.PortalCageStatus.LatestVersion,
})
return s
})
register("IUserEventQuestGuerrillaFreeOpen", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"startDatetime": user.GuerrillaFreeOpen.StartDatetime,
"openMinutes": user.GuerrillaFreeOpen.OpenMinutes,
"dailyOpenedCount": user.GuerrillaFreeOpen.DailyOpenedCount,
"latestVersion": user.GuerrillaFreeOpen.LatestVersion,
})
return s
})
register("IUserShopItem", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedShopItemRecords(user)...)
return s
})
register("IUserShopReplaceable", func(user store.UserState) string {
s, _ := encodeJSONMaps(map[string]any{
"userId": user.UserId,
"lineupUpdateCount": user.ShopReplaceable.LineupUpdateCount,
"latestLineupUpdateDatetime": user.ShopReplaceable.LatestLineupUpdateDatetime,
"latestVersion": user.ShopReplaceable.LatestVersion,
})
return s
})
register("IUserShopReplaceableLineup", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedShopReplaceableLineupRecords(user)...)
return s
})
registerStatic()
}
func sortedTutorialRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.Tutorials))
for id := range user.Tutorials {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.Tutorials[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"tutorialType": row.TutorialType,
"progressPhase": row.ProgressPhase,
"choiceId": row.ChoiceId,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedMissionRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.Missions))
for id := range user.Missions {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.Missions[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"missionId": row.MissionId,
"startDatetime": row.StartDatetime,
"progressValue": row.ProgressValue,
"missionProgressStatusType": row.MissionProgressStatusType,
"clearDatetime": row.ClearDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedNaviCutInRecords(user store.UserState) []map[string]any {
ids := make([]int32, 0, len(user.NaviCutInPlayed))
for id := range user.NaviCutInPlayed {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
now := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"naviCutInId": id,
"playDatetime": now,
"latestVersion": now,
})
}
return records
}
func sortedContentsStoryRecords(user store.UserState) []map[string]any {
ids := make([]int32, 0, len(user.ContentsStories))
for id := range user.ContentsStories {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
now := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"contentsStoryId": id,
"playDatetime": user.ContentsStories[id],
"latestVersion": now,
})
}
return records
}
func sortedMovieRecords(user store.UserState) []map[string]any {
ids := make([]int32, 0, len(user.ViewedMovies))
for id := range user.ViewedMovies {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
now := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"movieId": id,
"latestViewedDatetime": user.ViewedMovies[id],
"latestVersion": now,
})
}
return records
}
func sortedOmikujiRecords(user store.UserState) []map[string]any {
ids := make([]int32, 0, len(user.DrawnOmikuji))
for id := range user.DrawnOmikuji {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
now := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"omikujiId": id,
"latestDrawDatetime": user.DrawnOmikuji[id],
"latestVersion": now,
})
}
return records
}
func sortedDokanRecords(user store.UserState) []map[string]any {
ids := make([]int32, 0, len(user.DokanConfirmed))
for id := range user.DokanConfirmed {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
now := gametime.NowMillis()
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
records = append(records, map[string]any{
"userId": user.UserId,
"dokanId": id,
"displayDatetime": now,
"latestVersion": now,
})
}
return records
}
func sortedShopItemRecords(user store.UserState) []map[string]any {
ids := make([]int, 0, len(user.ShopItems))
for id := range user.ShopItems {
ids = append(ids, int(id))
}
sort.Ints(ids)
records := make([]map[string]any, 0, len(ids))
for _, id := range ids {
row := user.ShopItems[int32(id)]
records = append(records, map[string]any{
"userId": user.UserId,
"shopItemId": row.ShopItemId,
"boughtCount": row.BoughtCount,
"latestBoughtCountChangedDatetime": row.LatestBoughtCountChangedDatetime,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedShopReplaceableLineupRecords(user store.UserState) []map[string]any {
slots := make([]int, 0, len(user.ShopReplaceableLineup))
for slot := range user.ShopReplaceableLineup {
slots = append(slots, int(slot))
}
sort.Ints(slots)
records := make([]map[string]any, 0, len(slots))
for _, slot := range slots {
row := user.ShopReplaceableLineup[int32(slot)]
records = append(records, map[string]any{
"userId": user.UserId,
"slotNumber": row.SlotNumber,
"shopItemId": row.ShopItemId,
"latestVersion": row.LatestVersion,
})
}
return records
}
+88
View File
@@ -0,0 +1,88 @@
package userdata
import (
"sort"
"lunar-tear/server/internal/store"
)
type Projector func(user store.UserState) string
var projectors = make(map[string]Projector)
func register(tableName string, fn Projector) {
projectors[tableName] = fn
}
func registerStatic(tableNames ...string) {
for _, name := range tableNames {
projectors[name] = func(_ store.UserState) string { return "[]" }
}
}
func projectTable(tableName string, user store.UserState) string {
fn, ok := projectors[tableName]
if !ok {
return "[]"
}
s := fn(user)
if s == "" {
return "[]"
}
return s
}
func sortedStringKeys[T any](rows map[string]T) []string {
keys := make([]string, 0, len(rows))
for key := range rows {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func compareGimmickKey(a, b store.GimmickKey) int {
if a.GimmickSequenceScheduleId != b.GimmickSequenceScheduleId {
if a.GimmickSequenceScheduleId < b.GimmickSequenceScheduleId {
return -1
}
return 1
}
if a.GimmickSequenceId != b.GimmickSequenceId {
if a.GimmickSequenceId < b.GimmickSequenceId {
return -1
}
return 1
}
if a.GimmickId < b.GimmickId {
return -1
}
if a.GimmickId > b.GimmickId {
return 1
}
return 0
}
func compareGimmickOrnamentKey(a, b store.GimmickOrnamentKey) int {
if cmp := compareGimmickKey(
store.GimmickKey{
GimmickSequenceScheduleId: a.GimmickSequenceScheduleId,
GimmickSequenceId: a.GimmickSequenceId,
GimmickId: a.GimmickId,
},
store.GimmickKey{
GimmickSequenceScheduleId: b.GimmickSequenceScheduleId,
GimmickSequenceId: b.GimmickSequenceId,
GimmickId: b.GimmickId,
},
); cmp != 0 {
return cmp
}
if a.GimmickOrnamentIndex < b.GimmickOrnamentIndex {
return -1
}
if a.GimmickOrnamentIndex > b.GimmickOrnamentIndex {
return 1
}
return 0
}
@@ -0,0 +1,172 @@
package userdata
import (
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/store"
)
func FullClientTableMap(user store.UserState) map[string]string {
return map[string]string{
"IUser": projectTable("IUser", user),
"IUserSetting": projectTable("IUserSetting", user),
"IUserStatus": projectTable("IUserStatus", user),
"IUserGem": projectTable("IUserGem", user),
"IUserProfile": projectTable("IUserProfile", user),
"IUserCharacter": projectTable("IUserCharacter", user),
"IUserCostume": projectTable("IUserCostume", user),
"IUserWeapon": projectTable("IUserWeapon", user),
"IUserWeaponStory": projectTable("IUserWeaponStory", user),
"IUserCompanion": projectTable("IUserCompanion", user),
"IUserThought": projectTable("IUserThought", user),
"IUserDeckCharacter": projectTable("IUserDeckCharacter", user),
"IUserDeck": projectTable("IUserDeck", user),
"IUserLogin": projectTable("IUserLogin", user),
"IUserLoginBonus": projectTable("IUserLoginBonus", user),
"IUserMission": projectTable("IUserMission", user),
"IUserMainQuestFlowStatus": projectTable("IUserMainQuestFlowStatus", user),
"IUserMainQuestMainFlowStatus": projectTable("IUserMainQuestMainFlowStatus", user),
"IUserMainQuestProgressStatus": projectTable("IUserMainQuestProgressStatus", user),
"IUserMainQuestSeasonRoute": projectTable("IUserMainQuestSeasonRoute", user),
"IUserQuest": projectTable("IUserQuest", user),
"IUserQuestMission": projectTable("IUserQuestMission", user),
"IUserTutorialProgress": projectTable("IUserTutorialProgress", user),
"IUserGimmick": projectTable("IUserGimmick", user),
"IUserGimmickOrnamentProgress": projectTable("IUserGimmickOrnamentProgress", user),
"IUserGimmickSequence": projectTable("IUserGimmickSequence", user),
"IUserGimmickUnlock": projectTable("IUserGimmickUnlock", user),
"IUserMaterial": projectTable("IUserMaterial", user),
"IUserConsumableItem": projectTable("IUserConsumableItem", user),
"IUserParts": projectTable("IUserParts", user),
"IUserImportantItem": projectTable("IUserImportantItem", user),
"IUserPremiumItem": projectTable("IUserPremiumItem", user),
"IUserDeckPartsGroup": projectTable("IUserDeckPartsGroup", user),
"IUserDeckSubWeaponGroup": projectTable("IUserDeckSubWeaponGroup", user),
"IUserDeckCharacterDressupCostume": projectTable("IUserDeckCharacterDressupCostume", user),
"IUserDeckTypeNote": projectTable("IUserDeckTypeNote", user),
"IUserDeckLimitContentRestricted": projectTable("IUserDeckLimitContentRestricted", user),
"IUserCostumeActiveSkill": projectTable("IUserCostumeActiveSkill", user),
"IUserCostumeAwakenStatusUp": projectTable("IUserCostumeAwakenStatusUp", user),
"IUserCostumeLevelBonusReleaseStatus": projectTable("IUserCostumeLevelBonusReleaseStatus", user),
"IUserCostumeLotteryEffect": projectTable("IUserCostumeLotteryEffect", user),
"IUserCostumeLotteryEffectAbility": projectTable("IUserCostumeLotteryEffectAbility", user),
"IUserCostumeLotteryEffectStatusUp": projectTable("IUserCostumeLotteryEffectStatusUp", user),
"IUserCostumeLotteryEffectPending": projectTable("IUserCostumeLotteryEffectPending", user),
"IUserWeaponNote": projectTable("IUserWeaponNote", user),
"IUserWeaponAbility": projectTable("IUserWeaponAbility", user),
"IUserWeaponSkill": projectTable("IUserWeaponSkill", user),
"IUserWeaponAwaken": projectTable("IUserWeaponAwaken", user),
"IUserPartsGroupNote": projectTable("IUserPartsGroupNote", user),
"IUserPartsPreset": projectTable("IUserPartsPreset", user),
"IUserPartsPresetTag": projectTable("IUserPartsPresetTag", user),
"IUserPartsStatusSub": projectTable("IUserPartsStatusSub", user),
"IUserNaviCutIn": projectTable("IUserNaviCutIn", user),
"IUserMovie": projectTable("IUserMovie", user),
"IUserContentsStory": projectTable("IUserContentsStory", user),
"IUserOmikuji": projectTable("IUserOmikuji", user),
"IUserDokan": projectTable("IUserDokan", user),
"IUserPortalCageStatus": projectTable("IUserPortalCageStatus", user),
"IUserEventQuestGuerrillaFreeOpen": projectTable("IUserEventQuestGuerrillaFreeOpen", user),
"IUserEventQuestProgressStatus": projectTable("IUserEventQuestProgressStatus", user),
"IUserExtraQuestProgressStatus": projectTable("IUserExtraQuestProgressStatus", user),
"IUserEventQuestDailyGroupCompleteReward": projectTable("IUserEventQuestDailyGroupCompleteReward", user),
"IUserEventQuestLabyrinthSeason": projectTable("IUserEventQuestLabyrinthSeason", user),
"IUserEventQuestLabyrinthStage": projectTable("IUserEventQuestLabyrinthStage", user),
"IUserEventQuestTowerAccumulationReward": projectTable("IUserEventQuestTowerAccumulationReward", user),
"IUserMainQuestReplayFlowStatus": projectTable("IUserMainQuestReplayFlowStatus", user),
"IUserSideStoryQuest": projectTable("IUserSideStoryQuest", user),
"IUserSideStoryQuestSceneProgressStatus": projectTable("IUserSideStoryQuestSceneProgressStatus", user),
"IUserQuestLimitContentStatus": projectTable("IUserQuestLimitContentStatus", user),
"IUserQuestReplayFlowRewardGroup": projectTable("IUserQuestReplayFlowRewardGroup", user),
"IUserQuestAutoOrbit": projectTable("IUserQuestAutoOrbit", user),
"IUserQuestSceneChoice": projectTable("IUserQuestSceneChoice", user),
"IUserQuestSceneChoiceHistory": projectTable("IUserQuestSceneChoiceHistory", user),
"IUserShopItem": projectTable("IUserShopItem", user),
"IUserShopReplaceable": projectTable("IUserShopReplaceable", user),
"IUserShopReplaceableLineup": projectTable("IUserShopReplaceableLineup", user),
"IUserExplore": projectTable("IUserExplore", user),
"IUserExploreScore": projectTable("IUserExploreScore", user),
"IUserCharacterBoard": projectTable("IUserCharacterBoard", user),
"IUserCharacterBoardAbility": projectTable("IUserCharacterBoardAbility", user),
"IUserCharacterBoardStatusUp": projectTable("IUserCharacterBoardStatusUp", user),
"IUserCharacterBoardCompleteReward": projectTable("IUserCharacterBoardCompleteReward", user),
"IUserAutoSaleSettingDetail": projectTable("IUserAutoSaleSettingDetail", user),
"IUserCharacterRebirth": projectTable("IUserCharacterRebirth", user),
"IUserCageOrnamentReward": projectTable("IUserCageOrnamentReward", user),
"IUserBigHuntProgressStatus": projectTable("IUserBigHuntProgressStatus", user),
"IUserBigHuntMaxScore": projectTable("IUserBigHuntMaxScore", user),
"IUserBigHuntStatus": projectTable("IUserBigHuntStatus", user),
"IUserBigHuntScheduleMaxScore": projectTable("IUserBigHuntScheduleMaxScore", user),
"IUserBigHuntWeeklyMaxScore": projectTable("IUserBigHuntWeeklyMaxScore", user),
"IUserBigHuntWeeklyStatus": projectTable("IUserBigHuntWeeklyStatus", user),
}
}
func FirstEntranceClientTableMap(user store.UserState) map[string]string {
tables := FullClientTableMap(user)
for _, table := range []string{
"IUserCharacter",
"IUserCostume",
"IUserWeapon",
"IUserCompanion",
"IUserDeckCharacter",
"IUserDeck",
"IUserTutorialProgress",
"IUserParts",
"IUserWeaponNote",
"IUserWeaponStory",
"IUserCostumeActiveSkill",
"IUserDeckTypeNote",
} {
tables[table] = "[]"
}
return tables
}
func SelectTables(all map[string]string, requested []string) map[string]string {
selected := make(map[string]string, len(requested))
for _, table := range requested {
if payload, ok := all[table]; ok && payload != "" {
selected[table] = payload
continue
}
selected[table] = "[]"
}
return selected
}
func BuildDiffFromTables(tables map[string]string) map[string]*pb.DiffData {
diff := make(map[string]*pb.DiffData, len(tables))
for table, payload := range tables {
if payload == "" {
payload = "[]"
}
diff[table] = &pb.DiffData{
UpdateRecordsJson: payload,
DeleteKeysJson: "[]",
}
}
return diff
}
// BuildDiffFromTablesOrdered builds a diff map with tables in the given order.
// Use when client applies tables in received order and order matters (e.g. IUserWeapon before IUserWeaponStory).
// Protobuf map serialization order is implementation-defined; this at least ensures we only include
// the requested tables in the specified sequence when building the map.
func BuildDiffFromTablesOrdered(tables map[string]string, order []string) map[string]*pb.DiffData {
diff := make(map[string]*pb.DiffData, len(order))
for _, table := range order {
payload, ok := tables[table]
if !ok {
payload = "[]"
}
if payload == "" {
payload = "[]"
}
diff[table] = &pb.DiffData{
UpdateRecordsJson: payload,
DeleteKeysJson: "[]",
}
}
return diff
}
+301
View File
@@ -0,0 +1,301 @@
package userdata
import (
"encoding/base64"
"encoding/json"
"fmt"
"lunar-tear/server/internal/gametime"
"github.com/vmihailenco/msgpack/v5"
)
// EntityIUser mirrors the game's EntityIUser [MessagePackObject] with [Key(0..7)].
// Serialized as a MessagePack array of 8 elements.
type EntityIUser struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
PlayerId int64 // Key(1)
OsType int32 // Key(2) — 2 = Android
PlatformType int32 // Key(3) — 2 = GooglePlay
UserRestrictionType int32 // Key(4) — 0 = None
RegisterDatetime int64 // Key(5) — unix millis
GameStartDatetime int64 // Key(6) — unix millis
LatestVersion int64 // Key(7)
}
// EntityIUserSetting mirrors EntityIUserSetting [Key(0..2)].
type EntityIUserSetting struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 `json:"userId"` // Key(0)
IsNotifyPurchaseAlert bool `json:"isNotifyPurchaseAlert"` // Key(1)
LatestVersion int64 `json:"latestVersion"` // Key(2)
}
// EntityIUserTutorialProgress mirrors EntityIUserTutorialProgress [Key(0..4)].
type EntityIUserTutorialProgress struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
TutorialType int32 // Key(1)
ProgressPhase int32 // Key(2)
ChoiceId int32 // Key(3)
LatestVersion int64 // Key(4)
}
// EntityIUserQuest mirrors EntityIUserQuest [Key(0..9)].
type EntityIUserQuest struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
QuestId int32 // Key(1)
QuestStateType int32 // Key(2) — 2 = Cleared
IsBattleOnly bool // Key(3)
LatestStartDatetime int64 // Key(4) — unix millis
ClearCount int32 // Key(5)
DailyClearCount int32 // Key(6)
LastClearDatetime int64 // Key(7) — unix millis
ShortestClearFrames int32 // Key(8)
LatestVersion int64 // Key(9)
}
// EntityIUserMainQuestFlowStatus mirrors EntityIUserMainQuestFlowStatus [Key(0..2)].
type EntityIUserMainQuestFlowStatus struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
CurrentQuestFlowType int32 // Key(1) // QuestFlowType: 0=UNKNOWN, 1=MAIN_FLOW, 2=SUB_FLOW, 3=REPLAY_FLOW, 4=ANOTHER_ROUTE_REPLAY_FLOW
LatestVersion int64 // Key(2)
}
// EntityIUserMainQuestMainFlowStatus mirrors EntityIUserMainQuestMainFlowStatus [Key(0..5)].
type EntityIUserMainQuestMainFlowStatus struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
CurrentMainQuestRouteId int32 // Key(1)
CurrentQuestSceneId int32 // Key(2)
HeadQuestSceneId int32 // Key(3)
IsReachedLastQuestScene bool // Key(4)
LatestVersion int64 // Key(5)
}
// EntityIUserMainQuestProgressStatus mirrors EntityIUserMainQuestProgressStatus [Key(0..4)].
// This table is used by ActivePlayerToEntityPlayingMainQuestStatus (0x2AB4A48).
type EntityIUserMainQuestProgressStatus struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
CurrentQuestSceneId int32 // Key(1)
HeadQuestSceneId int32 // Key(2)
CurrentQuestFlowType int32 // Key(3) // QuestFlowType: 0=UNKNOWN, 1=MAIN_FLOW, 2=SUB_FLOW, 3=REPLAY_FLOW, 4=ANOTHER_ROUTE_REPLAY_FLOW
LatestVersion int64 // Key(4)
}
// EntityIUserMainQuestSeasonRoute mirrors EntityIUserMainQuestSeasonRoute [Key(0..3)].
type EntityIUserMainQuestSeasonRoute struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
MainQuestSeasonId int32 // Key(1)
MainQuestRouteId int32 // Key(2)
LatestVersion int64 // Key(3)
}
// EntityIUserStatus mirrors EntityIUserStatus [Key(0..5)].
type EntityIUserStatus struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
Level int32 // Key(1)
Exp int32 // Key(2)
StaminaMilliValue int32 // Key(3)
StaminaUpdateDatetime int64 // Key(4)
LatestVersion int64 // Key(5)
}
// EntityIUserGem mirrors EntityIUserGem [Key(0..2)].
type EntityIUserGem struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 `json:"userId"` // Key(0)
PaidGem int32 `json:"paidGem"` // Key(1)
FreeGem int32 `json:"freeGem"` // Key(2)
}
// EntityIUserProfile mirrors EntityIUserProfile [Key(0..7)].
type EntityIUserProfile struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
Name string // Key(1)
NameUpdateDatetime int64 // Key(2)
Message string // Key(3)
MessageUpdateDatetime int64 // Key(4)
FavoriteCostumeId int32 // Key(5)
FavoriteCostumeIdUpdateDatetime int64 // Key(6)
LatestVersion int64 // Key(7)
}
// EntityIUserCharacter mirrors EntityIUserCharacter [Key(0..4)].
type EntityIUserCharacter struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
CharacterId int32 // Key(1)
Level int32 // Key(2)
Exp int32 // Key(3)
LatestVersion int64 // Key(4)
}
// EntityIUserCostume mirrors EntityIUserCostume [Key(0..9)].
type EntityIUserCostume struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
UserCostumeUuid string // Key(1)
CostumeId int32 // Key(2)
LimitBreakCount int32 // Key(3)
Level int32 // Key(4)
Exp int32 // Key(5)
HeadupDisplayViewId int32 // Key(6)
AcquisitionDatetime int64 // Key(7)
AwakenCount int32 // Key(8)
LatestVersion int64 // Key(9)
}
// EntityIUserWeapon mirrors EntityIUserWeapon [Key(0..8)].
type EntityIUserWeapon struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
UserWeaponUuid string // Key(1)
WeaponId int32 // Key(2)
Level int32 // Key(3)
Exp int32 // Key(4)
LimitBreakCount int32 // Key(5)
IsProtected bool // Key(6)
AcquisitionDatetime int64 // Key(7)
LatestVersion int64 // Key(8)
}
// EntityIUserCompanion mirrors EntityIUserCompanion [Key(0..6)].
type EntityIUserCompanion struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
UserCompanionUuid string // Key(1)
CompanionId int32 // Key(2)
HeadupDisplayViewId int32 // Key(3)
Level int32 // Key(4)
AcquisitionDatetime int64 // Key(5)
LatestVersion int64 // Key(6)
}
// EntityIUserDeckCharacter mirrors EntityIUserDeckCharacter [Key(0..7)].
type EntityIUserDeckCharacter struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
UserDeckCharacterUuid string // Key(1)
UserCostumeUuid string // Key(2)
MainUserWeaponUuid string // Key(3)
UserCompanionUuid string // Key(4)
Power int32 // Key(5)
UserThoughtUuid string // Key(6)
LatestVersion int64 // Key(7)
}
// EntityIUserDeck mirrors EntityIUserDeck [Key(0..8)].
type EntityIUserDeck struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
DeckType int32 // Key(1)
UserDeckNumber int32 // Key(2)
UserDeckCharacterUuid01 string // Key(3)
UserDeckCharacterUuid02 string // Key(4)
UserDeckCharacterUuid03 string // Key(5)
Name string // Key(6)
Power int32 // Key(7)
LatestVersion int64 // Key(8)
}
// EntityIUserLogin mirrors EntityIUserLogin [Key(0..6)].
type EntityIUserLogin struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 `json:"userId"` // Key(0)
TotalLoginCount int32 `json:"totalLoginCount"` // Key(1)
ContinualLoginCount int32 `json:"continualLoginCount"` // Key(2)
MaxContinualLoginCount int32 `json:"maxContinualLoginCount"` // Key(3)
LastLoginDatetime int64 `json:"lastLoginDatetime"` // Key(4)
LastComebackLoginDatetime int64 `json:"lastComebackLoginDatetime"` // Key(5)
LatestVersion int64 `json:"latestVersion"` // Key(6)
}
// EntityIUserLoginBonus mirrors EntityIUserLoginBonus [Key(0..5)].
type EntityIUserLoginBonus struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 `json:"userId"` // Key(0)
LoginBonusId int32 `json:"loginBonusId"` // Key(1)
CurrentPageNumber int32 `json:"currentPageNumber"` // Key(2)
CurrentStampNumber int32 `json:"currentStampNumber"` // Key(3)
LatestRewardReceiveDatetime int64 `json:"latestRewardReceiveDatetime"` // Key(4)
LatestVersion int64 `json:"latestVersion"` // Key(5)
}
// EntityIUserMission mirrors EntityIUserMission [Key(0..6)].
type EntityIUserMission struct {
_msgpack struct{} `msgpack:",asArray"`
UserId int64 // Key(0)
MissionId int32 // Key(1)
StartDatetime int64 // Key(2)
ProgressValue int32 // Key(3)
MissionProgressStatusType int32 // Key(4)
ClearDatetime int64 // Key(5)
LatestVersion int64 // Key(6)
}
// EncodeRecords serializes a slice of entities to the client-expected format:
// a JSON array of base64-encoded MessagePack byte strings.
func EncodeRecords(entities ...any) (string, error) {
b64List := make([]string, 0, len(entities))
for _, e := range entities {
data, err := msgpack.Marshal(e)
if err != nil {
return "", fmt.Errorf("msgpack marshal: %w", err)
}
b64List = append(b64List, base64.StdEncoding.EncodeToString(data))
}
jsonBytes, err := json.Marshal(b64List)
if err != nil {
return "", fmt.Errorf("json marshal: %w", err)
}
return string(jsonBytes), nil
}
func encodeJSONRecords(entities ...any) (string, error) {
jsonBytes, err := json.Marshal(entities)
if err != nil {
return "", fmt.Errorf("json marshal records: %w", err)
}
return string(jsonBytes), nil
}
func encodeJSONMaps(records ...map[string]any) (string, error) {
jsonBytes, err := json.Marshal(records)
if err != nil {
return "", fmt.Errorf("json marshal maps: %w", err)
}
return string(jsonBytes), nil
}
// DefaultUserData returns pre-built user data tables for a fresh user.
// We provide BOTH msgpack-encoded (base64) and plain JSON variants.
// The server tries msgpack first; if the client doesn't accept it, switch to JSON.
func DefaultUserData(userId int64) map[string]string {
now := gametime.Now().Unix()
userRecord, _ := EncodeRecords(&EntityIUser{
UserId: userId,
PlayerId: userId,
OsType: 2,
PlatformType: 2,
RegisterDatetime: now,
})
settingRecord, _ := EncodeRecords(&EntityIUserSetting{
UserId: userId,
})
data := map[string]string{
"user": userRecord,
"user_setting": settingRecord,
}
return data
}