From c9a192927956a640ccf893e31470055c983f4d99 Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Sat, 16 May 2026 19:05:22 +0300 Subject: [PATCH] Implement remaining Memoirs preset management RPCs --- server/internal/service/parts.go | 99 +++++++++++++++++++ server/internal/store/clone.go | 1 + server/internal/store/seed.go | 1 + server/internal/store/sqlite/load.go | 9 ++ server/internal/store/sqlite/save.go | 10 ++ server/internal/store/sqlite/user.go | 1 + server/internal/store/types.go | 10 ++ server/internal/userdata/changed_tables.go | 5 + server/internal/userdata/proj_inventory.go | 24 ++++- ...60516155206_add_user_parts_preset_tags.sql | 11 +++ 10 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 server/migrations/20260516155206_add_user_parts_preset_tags.sql diff --git a/server/internal/service/parts.go b/server/internal/service/parts.go index 4b03731..037bb23 100644 --- a/server/internal/service/parts.go +++ b/server/internal/service/parts.go @@ -270,3 +270,102 @@ func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsRep return &pb.PartsReplacePresetResponse{}, nil } + +func (s *PartsServiceServer) UpdatePresetName(ctx context.Context, req *pb.PartsUpdatePresetNameRequest) (*pb.PartsUpdatePresetNameResponse, error) { + log.Printf("[PartsService] UpdatePresetName: preset=%d name=%q", req.UserPartsPresetNumber, req.Name) + + userId := CurrentUserId(ctx, s.users, s.sessions) + nowMillis := gametime.NowMillis() + + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { + preset := user.PartsPresets[req.UserPartsPresetNumber] + preset.UserPartsPresetNumber = req.UserPartsPresetNumber + preset.Name = req.Name + preset.LatestVersion = nowMillis + user.PartsPresets[req.UserPartsPresetNumber] = preset + }) + if err != nil { + return nil, fmt.Errorf("parts update preset name: %w", err) + } + + return &pb.PartsUpdatePresetNameResponse{}, nil +} + +func (s *PartsServiceServer) UpdatePresetTagNumber(ctx context.Context, req *pb.PartsUpdatePresetTagNumberRequest) (*pb.PartsUpdatePresetTagNumberResponse, error) { + log.Printf("[PartsService] UpdatePresetTagNumber: preset=%d tag=%d", req.UserPartsPresetNumber, req.UserPartsPresetTagNumber) + + userId := CurrentUserId(ctx, s.users, s.sessions) + nowMillis := gametime.NowMillis() + + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { + preset := user.PartsPresets[req.UserPartsPresetNumber] + preset.UserPartsPresetNumber = req.UserPartsPresetNumber + preset.UserPartsPresetTagNumber = req.UserPartsPresetTagNumber + preset.LatestVersion = nowMillis + user.PartsPresets[req.UserPartsPresetNumber] = preset + }) + if err != nil { + return nil, fmt.Errorf("parts update preset tag number: %w", err) + } + + return &pb.PartsUpdatePresetTagNumberResponse{}, nil +} + +func (s *PartsServiceServer) UpdatePresetTagName(ctx context.Context, req *pb.PartsUpdatePresetTagNameRequest) (*pb.PartsUpdatePresetTagNameResponse, error) { + log.Printf("[PartsService] UpdatePresetTagName: tag=%d name=%q", req.UserPartsPresetTagNumber, req.Name) + + userId := CurrentUserId(ctx, s.users, s.sessions) + nowMillis := gametime.NowMillis() + + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { + tag := user.PartsPresetTags[req.UserPartsPresetTagNumber] + tag.UserPartsPresetTagNumber = req.UserPartsPresetTagNumber + tag.Name = req.Name + tag.LatestVersion = nowMillis + user.PartsPresetTags[req.UserPartsPresetTagNumber] = tag + }) + if err != nil { + return nil, fmt.Errorf("parts update preset tag name: %w", err) + } + + return &pb.PartsUpdatePresetTagNameResponse{}, nil +} + +func (s *PartsServiceServer) CopyPreset(ctx context.Context, req *pb.PartsCopyPresetRequest) (*pb.PartsCopyPresetResponse, error) { + log.Printf("[PartsService] CopyPreset: from=%d to=%d", req.FromUserPartsPresetNumber, req.ToUserPartsPresetNumber) + + userId := CurrentUserId(ctx, s.users, s.sessions) + nowMillis := gametime.NowMillis() + + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { + from, ok := user.PartsPresets[req.FromUserPartsPresetNumber] + if !ok { + log.Printf("[PartsService] CopyPreset: source preset=%d not found, skipping", req.FromUserPartsPresetNumber) + return + } + to := from + to.UserPartsPresetNumber = req.ToUserPartsPresetNumber + to.LatestVersion = nowMillis + user.PartsPresets[req.ToUserPartsPresetNumber] = to + }) + if err != nil { + return nil, fmt.Errorf("parts copy preset: %w", err) + } + + return &pb.PartsCopyPresetResponse{}, nil +} + +func (s *PartsServiceServer) RemovePreset(ctx context.Context, req *pb.PartsRemovePresetRequest) (*pb.PartsRemovePresetResponse, error) { + log.Printf("[PartsService] RemovePreset: preset=%d", req.UserPartsPresetNumber) + + userId := CurrentUserId(ctx, s.users, s.sessions) + + _, err := s.users.UpdateUser(userId, func(user *store.UserState) { + delete(user.PartsPresets, req.UserPartsPresetNumber) + }) + if err != nil { + return nil, fmt.Errorf("parts remove preset: %w", err) + } + + return &pb.PartsRemovePresetResponse{}, nil +} diff --git a/server/internal/store/clone.go b/server/internal/store/clone.go index 7015425..4611e20 100644 --- a/server/internal/store/clone.go +++ b/server/internal/store/clone.go @@ -31,6 +31,7 @@ func CloneUserState(u UserState) UserState { out.Parts = maps.Clone(u.Parts) out.PartsGroupNotes = maps.Clone(u.PartsGroupNotes) out.PartsPresets = maps.Clone(u.PartsPresets) + out.PartsPresetTags = maps.Clone(u.PartsPresetTags) out.PartsStatusSubs = maps.Clone(u.PartsStatusSubs) out.ImportantItems = maps.Clone(u.ImportantItems) out.CostumeActiveSkills = maps.Clone(u.CostumeActiveSkills) diff --git a/server/internal/store/seed.go b/server/internal/store/seed.go index dfc6615..49bb25e 100644 --- a/server/internal/store/seed.go +++ b/server/internal/store/seed.go @@ -130,6 +130,7 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl Parts: make(map[string]PartsState), PartsGroupNotes: make(map[int32]PartsGroupNoteState), PartsPresets: make(map[int32]PartsPresetState), + PartsPresetTags: make(map[int32]PartsPresetTagState), PartsStatusSubs: make(map[PartsStatusSubKey]PartsStatusSubState), ImportantItems: make(map[int32]int32), CostumeActiveSkills: make(map[string]CostumeActiveSkillState), diff --git a/server/internal/store/sqlite/load.go b/server/internal/store/sqlite/load.go index 35291b7..9ddf83a 100644 --- a/server/internal/store/sqlite/load.go +++ b/server/internal/store/sqlite/load.go @@ -61,6 +61,7 @@ func initMaps(u *store.UserState) { u.Parts = make(map[string]store.PartsState) u.PartsGroupNotes = make(map[int32]store.PartsGroupNoteState) u.PartsPresets = make(map[int32]store.PartsPresetState) + u.PartsPresetTags = make(map[int32]store.PartsPresetTagState) u.PartsStatusSubs = make(map[store.PartsStatusSubKey]store.PartsStatusSubState) u.DeckTypeNotes = make(map[model.DeckType]store.DeckTypeNoteState) u.ConsumableItems = make(map[int32]int32) @@ -492,6 +493,14 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) { u.PartsPresets[v.UserPartsPresetNumber] = v }) + queryRows(db, `SELECT user_parts_preset_tag_number, name, latest_version + FROM user_parts_preset_tags WHERE user_id=?`, uid, + func(rows *sql.Rows) { + var v store.PartsPresetTagState + rows.Scan(&v.UserPartsPresetTagNumber, &v.Name, &v.LatestVersion) + u.PartsPresetTags[v.UserPartsPresetTagNumber] = v + }) + queryRows(db, `SELECT user_parts_uuid, status_index, parts_status_sub_lottery_id, level, status_kind_type, status_calculation_type, status_change_value, latest_version FROM user_parts_status_subs WHERE user_id=?`, uid, diff --git a/server/internal/store/sqlite/save.go b/server/internal/store/sqlite/save.go index c4d3e82..b837ebe 100644 --- a/server/internal/store/sqlite/save.go +++ b/server/internal/store/sqlite/save.go @@ -312,6 +312,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error { return err } } + for _, v := range u.PartsPresetTags { + if err := exec(`INSERT INTO user_parts_preset_tags (user_id, user_parts_preset_tag_number, name, latest_version) VALUES (?,?,?,?)`, + uid, v.UserPartsPresetTagNumber, v.Name, v.LatestVersion); err != nil { + return err + } + } for _, v := range u.PartsStatusSubs { if err := exec(`INSERT INTO user_parts_status_subs (user_id, user_parts_uuid, status_index, parts_status_sub_lottery_id, level, status_kind_type, status_calculation_type, status_change_value, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`, uid, v.UserPartsUuid, v.StatusIndex, v.PartsStatusSubLotteryId, v.Level, v.StatusKindType, v.StatusCalculationType, v.StatusChangeValue, v.LatestVersion); err != nil { @@ -862,6 +868,10 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error { func(v store.PartsPresetState) []any { return []any{v.UserPartsPresetNumber, v.UserPartsUuid01, v.UserPartsUuid02, v.UserPartsUuid03, v.Name, v.UserPartsPresetTagNumber, v.LatestVersion} }, "user_parts_preset_number, user_parts_uuid01, user_parts_uuid02, user_parts_uuid03, name, user_parts_preset_tag_number, latest_version") + diffMapInt32(tx, uid, before.PartsPresetTags, after.PartsPresetTags, "user_parts_preset_tags", "user_parts_preset_tag_number", + func(v store.PartsPresetTagState) []any { + return []any{v.UserPartsPresetTagNumber, v.Name, v.LatestVersion} + }, "user_parts_preset_tag_number, name, latest_version") for k, v := range after.PartsStatusSubs { if old, ok := before.PartsStatusSubs[k]; !ok || old != v { diff --git a/server/internal/store/sqlite/user.go b/server/internal/store/sqlite/user.go index 499a6e4..4ed74ca 100644 --- a/server/internal/store/sqlite/user.go +++ b/server/internal/store/sqlite/user.go @@ -119,6 +119,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error { "user_decks", "user_deck_characters", "user_parts_status_subs", + "user_parts_preset_tags", "user_parts_presets", "user_parts_group_notes", "user_parts", diff --git a/server/internal/store/types.go b/server/internal/store/types.go index 7f04628..c2404fa 100644 --- a/server/internal/store/types.go +++ b/server/internal/store/types.go @@ -82,6 +82,7 @@ type UserState struct { Parts map[string]PartsState PartsGroupNotes map[int32]PartsGroupNoteState PartsPresets map[int32]PartsPresetState + PartsPresetTags map[int32]PartsPresetTagState PartsStatusSubs map[PartsStatusSubKey]PartsStatusSubState ImportantItems map[int32]int32 CostumeActiveSkills map[string]CostumeActiveSkillState @@ -206,6 +207,9 @@ func (u *UserState) EnsureMaps() { if u.PartsPresets == nil { u.PartsPresets = make(map[int32]PartsPresetState) } + if u.PartsPresetTags == nil { + u.PartsPresetTags = make(map[int32]PartsPresetTagState) + } if u.PartsStatusSubs == nil { u.PartsStatusSubs = make(map[PartsStatusSubKey]PartsStatusSubState) } @@ -890,6 +894,12 @@ type PartsPresetState struct { LatestVersion int64 } +type PartsPresetTagState struct { + UserPartsPresetTagNumber int32 + Name string + LatestVersion int64 +} + type PartsStatusSubKey struct { UserPartsUuid string StatusIndex int32 diff --git a/server/internal/userdata/changed_tables.go b/server/internal/userdata/changed_tables.go index 700661f..628b7f8 100644 --- a/server/internal/userdata/changed_tables.go +++ b/server/internal/userdata/changed_tables.go @@ -163,6 +163,9 @@ func ChangedTables(before, after *store.UserState) []string { if !mapsEqualStruct(before.PartsPresets, after.PartsPresets) { add("IUserPartsPreset") } + if !mapsEqualStruct(before.PartsPresetTags, after.PartsPresetTags) { + add("IUserPartsPresetTag") + } if !mapsEqualStruct(before.PartsStatusSubs, after.PartsStatusSubs) { add("IUserPartsStatusSub") } @@ -419,6 +422,8 @@ func keyFieldsForTable(table string) []string { return []string{"userId", "partsGroupId"} case "IUserPartsPreset": return []string{"userId", "userPartsPresetNumber"} + case "IUserPartsPresetTag": + return []string{"userId", "userPartsPresetTagNumber"} case "IUserCageOrnamentReward": return []string{"userId", "cageOrnamentId"} case "IUserAutoSaleSettingDetail": diff --git a/server/internal/userdata/proj_inventory.go b/server/internal/userdata/proj_inventory.go index 7927e02..e554360 100644 --- a/server/internal/userdata/proj_inventory.go +++ b/server/internal/userdata/proj_inventory.go @@ -86,6 +86,10 @@ func init() { s, _ := utils.EncodeJSONMaps(sortedPartsPresetRecords(user)...) return s }) + register("IUserPartsPresetTag", func(user store.UserState) string { + s, _ := utils.EncodeJSONMaps(sortedPartsPresetTagRecords(user)...) + return s + }) register("IUserCostumeAwakenStatusUp", func(user store.UserState) string { s, _ := utils.EncodeJSONMaps(sortedCostumeAwakenStatusUpRecords(user)...) return s @@ -122,7 +126,6 @@ func init() { "IUserCostumeLevelBonusReleaseStatus", "IUserCostumeLotteryEffectAbility", "IUserCostumeLotteryEffectStatusUp", - "IUserPartsPresetTag", ) } @@ -496,6 +499,25 @@ func sortedPartsPresetRecords(user store.UserState) []map[string]any { return records } +func sortedPartsPresetTagRecords(user store.UserState) []map[string]any { + ids := make([]int, 0, len(user.PartsPresetTags)) + for id := range user.PartsPresetTags { + ids = append(ids, int(id)) + } + sort.Ints(ids) + records := make([]map[string]any, 0, len(ids)) + for _, id := range ids { + row := user.PartsPresetTags[int32(id)] + records = append(records, map[string]any{ + "userId": user.UserId, + "userPartsPresetTagNumber": row.UserPartsPresetTagNumber, + "name": row.Name, + "latestVersion": row.LatestVersion, + }) + } + return records +} + func sortedPartsStatusSubRecords(user store.UserState) []map[string]any { keys := make([]store.PartsStatusSubKey, 0, len(user.PartsStatusSubs)) for k := range user.PartsStatusSubs { diff --git a/server/migrations/20260516155206_add_user_parts_preset_tags.sql b/server/migrations/20260516155206_add_user_parts_preset_tags.sql new file mode 100644 index 0000000..47b7df9 --- /dev/null +++ b/server/migrations/20260516155206_add_user_parts_preset_tags.sql @@ -0,0 +1,11 @@ +-- +goose Up +CREATE TABLE user_parts_preset_tags ( + user_id INTEGER NOT NULL REFERENCES users(user_id), + user_parts_preset_tag_number INTEGER NOT NULL, + name TEXT NOT NULL DEFAULT '', + latest_version INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (user_id, user_parts_preset_tag_number) +); + +-- +goose Down +DROP TABLE IF EXISTS user_parts_preset_tags;