diff --git a/server/internal/questflow/rewards.go b/server/internal/questflow/rewards.go index 8c7734c..6d9ab90 100644 --- a/server/internal/questflow/rewards.go +++ b/server/internal/questflow/rewards.go @@ -166,6 +166,10 @@ func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, now return } + if questDef.CharacterExp == 0 && questDef.CostumeExp == 0 { + return + } + deckCostumeUuids, deckCharacterIds := h.resolveDeckUnits(user, questId) if deckCostumeUuids == nil { log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (deck not resolved)", questId) diff --git a/server/internal/service/deck.go b/server/internal/service/deck.go index 8187019..6212af9 100644 --- a/server/internal/service/deck.go +++ b/server/internal/service/deck.go @@ -191,11 +191,45 @@ func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.Repla } store.ApplyDeckReplacement(user, model.DeckType(detail.DeckType), detail.UserDeckNumber, deckSlotsFromProto(detail.Deck), nowMillis) } + + key := store.DeckKey{DeckType: model.DeckType(req.DeckType), UserDeckNumber: req.UserDeckNumber} + td := user.TripleDecks[key] + td.DeckType = model.DeckType(req.DeckType) + td.UserDeckNumber = req.UserDeckNumber + td.DeckNumber01 = innerDeckNumber(req.DeckDetail01) + td.DeckNumber02 = innerDeckNumber(req.DeckDetail02) + td.DeckNumber03 = innerDeckNumber(req.DeckDetail03) + td.LatestVersion = nowMillis + user.TripleDecks[key] = td }) return &pb.ReplaceTripleDeckResponse{}, nil } +func innerDeckNumber(d *pb.DeckDetail) int32 { + if d == nil { + return 0 + } + return d.UserDeckNumber +} + +func (s *DeckServiceServer) UpdateTripleDeckName(ctx context.Context, req *pb.UpdateTripleDeckNameRequest) (*pb.UpdateTripleDeckNameResponse, error) { + log.Printf("[DeckService] UpdateTripleDeckName: deckType=%d deckNumber=%d name=%q", req.DeckType, req.UserDeckNumber, req.Name) + userId := CurrentUserId(ctx, s.users, s.sessions) + + s.users.UpdateUser(userId, func(user *store.UserState) { + key := store.DeckKey{DeckType: model.DeckType(req.DeckType), UserDeckNumber: req.UserDeckNumber} + td := user.TripleDecks[key] + td.DeckType = model.DeckType(req.DeckType) + td.UserDeckNumber = req.UserDeckNumber + td.Name = req.Name + td.LatestVersion = gametime.NowMillis() + user.TripleDecks[key] = td + }) + + return &pb.UpdateTripleDeckNameResponse{}, nil +} + func (s *DeckServiceServer) ReplaceMultiDeck(ctx context.Context, req *pb.ReplaceMultiDeckRequest) (*pb.ReplaceMultiDeckResponse, error) { log.Printf("[DeckService] ReplaceMultiDeck: %d entries", len(req.DeckDetail)) userId := CurrentUserId(ctx, s.users, s.sessions) diff --git a/server/internal/store/clone.go b/server/internal/store/clone.go index 4c3f7e3..7015425 100644 --- a/server/internal/store/clone.go +++ b/server/internal/store/clone.go @@ -14,6 +14,7 @@ func CloneUserState(u UserState) UserState { out.DeckSubWeapons = cloneSliceMap(u.DeckSubWeapons) out.DeckParts = cloneSliceMap(u.DeckParts) out.Decks = maps.Clone(u.Decks) + out.TripleDecks = maps.Clone(u.TripleDecks) out.Quests = maps.Clone(u.Quests) out.QuestMissions = maps.Clone(u.QuestMissions) out.WeaponStories = maps.Clone(u.WeaponStories) diff --git a/server/internal/store/seed.go b/server/internal/store/seed.go index feafd7e..dfc6615 100644 --- a/server/internal/store/seed.go +++ b/server/internal/store/seed.go @@ -97,6 +97,7 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl Companions: make(map[string]CompanionState), DeckCharacters: make(map[string]DeckCharacterState), Decks: make(map[DeckKey]DeckState), + TripleDecks: make(map[DeckKey]TripleDeckState), DeckSubWeapons: make(map[string][]string), DeckParts: make(map[string][]string), Quests: make(map[int32]UserQuestState), diff --git a/server/internal/store/sqlite/load.go b/server/internal/store/sqlite/load.go index 8a193bf..35291b7 100644 --- a/server/internal/store/sqlite/load.go +++ b/server/internal/store/sqlite/load.go @@ -43,6 +43,7 @@ func initMaps(u *store.UserState) { u.Thoughts = make(map[string]store.ThoughtState) u.DeckCharacters = make(map[string]store.DeckCharacterState) u.Decks = make(map[store.DeckKey]store.DeckState) + u.TripleDecks = make(map[store.DeckKey]store.TripleDeckState) u.DeckSubWeapons = make(map[string][]string) u.DeckParts = make(map[string][]string) u.Quests = make(map[int32]store.UserQuestState) @@ -299,6 +300,16 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) { u.Decks[store.DeckKey{DeckType: v.DeckType, UserDeckNumber: v.UserDeckNumber}] = v }) + queryRows(db, `SELECT deck_type, user_deck_number, name, deck_number01, deck_number02, deck_number03, latest_version + FROM user_triple_decks WHERE user_id=?`, uid, + func(rows *sql.Rows) { + var v store.TripleDeckState + var dt int32 + rows.Scan(&dt, &v.UserDeckNumber, &v.Name, &v.DeckNumber01, &v.DeckNumber02, &v.DeckNumber03, &v.LatestVersion) + v.DeckType = model.DeckType(dt) + u.TripleDecks[store.DeckKey{DeckType: v.DeckType, UserDeckNumber: v.UserDeckNumber}] = v + }) + queryRows(db, `SELECT user_deck_character_uuid, ordinal, user_weapon_uuid FROM user_deck_sub_weapons WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid, func(rows *sql.Rows) { diff --git a/server/internal/store/sqlite/save.go b/server/internal/store/sqlite/save.go index 3617e63..c4d3e82 100644 --- a/server/internal/store/sqlite/save.go +++ b/server/internal/store/sqlite/save.go @@ -171,6 +171,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error { return err } } + for k, v := range u.TripleDecks { + if err := exec(`INSERT INTO user_triple_decks (user_id, deck_type, user_deck_number, name, deck_number01, deck_number02, deck_number03, latest_version) VALUES (?,?,?,?,?,?,?,?)`, + uid, int32(k.DeckType), k.UserDeckNumber, v.Name, v.DeckNumber01, v.DeckNumber02, v.DeckNumber03, v.LatestVersion); err != nil { + return err + } + } for key, uuids := range u.DeckSubWeapons { for i, uuid := range uuids { if err := exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`, @@ -713,6 +719,18 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error { } } + for k, v := range after.TripleDecks { + if old, ok := before.TripleDecks[k]; !ok || old != v { + exec(`INSERT OR REPLACE INTO user_triple_decks (user_id, deck_type, user_deck_number, name, deck_number01, deck_number02, deck_number03, latest_version) VALUES (?,?,?,?,?,?,?,?)`, + uid, int32(k.DeckType), k.UserDeckNumber, v.Name, v.DeckNumber01, v.DeckNumber02, v.DeckNumber03, v.LatestVersion) + } + } + for k := range before.TripleDecks { + if _, ok := after.TripleDecks[k]; !ok { + exec(`DELETE FROM user_triple_decks WHERE user_id=? AND deck_type=? AND user_deck_number=?`, uid, int32(k.DeckType), k.UserDeckNumber) + } + } + replaceSliceTable(tx, uid, "user_deck_sub_weapons", after.DeckSubWeapons, func(key string, uuids []string) { for i, uuid := range uuids { exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`, uid, key, i, uuid) diff --git a/server/internal/store/types.go b/server/internal/store/types.go index 2726906..7f04628 100644 --- a/server/internal/store/types.go +++ b/server/internal/store/types.go @@ -70,6 +70,7 @@ type UserState struct { Thoughts map[string]ThoughtState DeckCharacters map[string]DeckCharacterState Decks map[DeckKey]DeckState + TripleDecks map[DeckKey]TripleDeckState Quests map[int32]UserQuestState QuestMissions map[QuestMissionKey]UserQuestMissionState Missions map[int32]UserMissionState @@ -142,6 +143,9 @@ func (u *UserState) EnsureMaps() { if u.Decks == nil { u.Decks = make(map[DeckKey]DeckState) } + if u.TripleDecks == nil { + u.TripleDecks = make(map[DeckKey]TripleDeckState) + } if u.DeckSubWeapons == nil { u.DeckSubWeapons = make(map[string][]string) } @@ -455,6 +459,16 @@ type DeckState struct { LatestVersion int64 } +type TripleDeckState struct { + DeckType model.DeckType + UserDeckNumber int32 + Name string + DeckNumber01 int32 + DeckNumber02 int32 + DeckNumber03 int32 + LatestVersion int64 +} + type DeckCharacterInput struct { UserCostumeUuid string MainUserWeaponUuid string diff --git a/server/internal/userdata/changed_tables.go b/server/internal/userdata/changed_tables.go index 85a9609..700661f 100644 --- a/server/internal/userdata/changed_tables.go +++ b/server/internal/userdata/changed_tables.go @@ -188,6 +188,9 @@ func ChangedTables(before, after *store.UserState) []string { if !mapsEqualStruct(before.Decks, after.Decks) { add("IUserDeck") } + if !mapsEqualStruct(before.TripleDecks, after.TripleDecks) { + add("IUserTripleDeck") + } if !mapsEqualSliceValues(before.DeckSubWeapons, after.DeckSubWeapons) { add("IUserDeckSubWeaponGroup") } @@ -358,6 +361,8 @@ func keyFieldsForTable(table string) []string { return []string{"userId", "userDeckCharacterUuid"} case "IUserDeck": return []string{"userId", "deckType", "userDeckNumber"} + case "IUserTripleDeck": + return []string{"userId", "deckType", "userDeckNumber"} case "IUserDeckSubWeaponGroup": return []string{"userId", "userDeckCharacterUuid", "sortOrder"} case "IUserDeckPartsGroup": diff --git a/server/internal/userdata/proj_deck.go b/server/internal/userdata/proj_deck.go index 1b9f535..5431d2f 100644 --- a/server/internal/userdata/proj_deck.go +++ b/server/internal/userdata/proj_deck.go @@ -13,6 +13,10 @@ func init() { s, _ := utils.EncodeJSONMaps(sortedDeckRecords(user)...) return s }) + register("IUserTripleDeck", func(user store.UserState) string { + s, _ := utils.EncodeJSONMaps(sortedTripleDeckRecords(user)...) + return s + }) register("IUserDeckCharacter", func(user store.UserState) string { s, _ := utils.EncodeJSONMaps(sortedDeckCharacterRecords(user)...) return s @@ -68,6 +72,35 @@ func sortedDeckRecords(user store.UserState) []map[string]any { return records } +func sortedTripleDeckRecords(user store.UserState) []map[string]any { + keys := make([]store.DeckKey, 0, len(user.TripleDecks)) + for key := range user.TripleDecks { + 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.TripleDecks[key] + records = append(records, map[string]any{ + "userId": user.UserId, + "deckType": int32(row.DeckType), + "userDeckNumber": row.UserDeckNumber, + "name": row.Name, + "deckNumber01": row.DeckNumber01, + "deckNumber02": row.DeckNumber02, + "deckNumber03": row.DeckNumber03, + "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)) diff --git a/server/internal/userdata/state_projection.go b/server/internal/userdata/state_projection.go index 980b3b7..d398769 100644 --- a/server/internal/userdata/state_projection.go +++ b/server/internal/userdata/state_projection.go @@ -21,6 +21,7 @@ func FullClientTableMap(user store.UserState) map[string]string { "IUserThought": projectTable("IUserThought", user), "IUserDeckCharacter": projectTable("IUserDeckCharacter", user), "IUserDeck": projectTable("IUserDeck", user), + "IUserTripleDeck": projectTable("IUserTripleDeck", user), "IUserLogin": projectTable("IUserLogin", user), "IUserLoginBonus": projectTable("IUserLoginBonus", user), "IUserMission": projectTable("IUserMission", user), diff --git a/server/migrations/20260514064857_add_user_triple_decks.sql b/server/migrations/20260514064857_add_user_triple_decks.sql new file mode 100644 index 0000000..c75019a --- /dev/null +++ b/server/migrations/20260514064857_add_user_triple_decks.sql @@ -0,0 +1,20 @@ +-- +goose Up +CREATE TABLE user_triple_decks ( + user_id INTEGER NOT NULL REFERENCES users(user_id), + deck_type INTEGER NOT NULL, + user_deck_number INTEGER NOT NULL, + name TEXT NOT NULL DEFAULT '', + deck_number01 INTEGER NOT NULL DEFAULT 0, + deck_number02 INTEGER NOT NULL DEFAULT 0, + deck_number03 INTEGER NOT NULL DEFAULT 0, + latest_version INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (user_id, deck_type, user_deck_number) +); + +-- Legacy BigHunt wave-decks have no preset wrapper rows and aren't reachable +-- via the new IUserTripleDeck projection. Drop them so users start clean and +-- repopulate via ReplaceTripleDeck from the client. +DELETE FROM user_decks WHERE deck_type = 5; + +-- +goose Down +DROP TABLE IF EXISTS user_triple_decks;