Persist Subjugation Quests triple-deck presets

This commit is contained in:
Ilya Groshev
2026-05-14 11:39:03 +03:00
parent fa5d023f58
commit ae884b4060
11 changed files with 142 additions and 0 deletions
+4
View File
@@ -166,6 +166,10 @@ func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, now
return return
} }
if questDef.CharacterExp == 0 && questDef.CostumeExp == 0 {
return
}
deckCostumeUuids, deckCharacterIds := h.resolveDeckUnits(user, questId) deckCostumeUuids, deckCharacterIds := h.resolveDeckUnits(user, questId)
if deckCostumeUuids == nil { if deckCostumeUuids == nil {
log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (deck not resolved)", questId) log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (deck not resolved)", questId)
+34
View File
@@ -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) 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 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) { func (s *DeckServiceServer) ReplaceMultiDeck(ctx context.Context, req *pb.ReplaceMultiDeckRequest) (*pb.ReplaceMultiDeckResponse, error) {
log.Printf("[DeckService] ReplaceMultiDeck: %d entries", len(req.DeckDetail)) log.Printf("[DeckService] ReplaceMultiDeck: %d entries", len(req.DeckDetail))
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
+1
View File
@@ -14,6 +14,7 @@ func CloneUserState(u UserState) UserState {
out.DeckSubWeapons = cloneSliceMap(u.DeckSubWeapons) out.DeckSubWeapons = cloneSliceMap(u.DeckSubWeapons)
out.DeckParts = cloneSliceMap(u.DeckParts) out.DeckParts = cloneSliceMap(u.DeckParts)
out.Decks = maps.Clone(u.Decks) out.Decks = maps.Clone(u.Decks)
out.TripleDecks = maps.Clone(u.TripleDecks)
out.Quests = maps.Clone(u.Quests) out.Quests = maps.Clone(u.Quests)
out.QuestMissions = maps.Clone(u.QuestMissions) out.QuestMissions = maps.Clone(u.QuestMissions)
out.WeaponStories = maps.Clone(u.WeaponStories) out.WeaponStories = maps.Clone(u.WeaponStories)
+1
View File
@@ -97,6 +97,7 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
Companions: make(map[string]CompanionState), Companions: make(map[string]CompanionState),
DeckCharacters: make(map[string]DeckCharacterState), DeckCharacters: make(map[string]DeckCharacterState),
Decks: make(map[DeckKey]DeckState), Decks: make(map[DeckKey]DeckState),
TripleDecks: make(map[DeckKey]TripleDeckState),
DeckSubWeapons: make(map[string][]string), DeckSubWeapons: make(map[string][]string),
DeckParts: make(map[string][]string), DeckParts: make(map[string][]string),
Quests: make(map[int32]UserQuestState), Quests: make(map[int32]UserQuestState),
+11
View File
@@ -43,6 +43,7 @@ func initMaps(u *store.UserState) {
u.Thoughts = make(map[string]store.ThoughtState) u.Thoughts = make(map[string]store.ThoughtState)
u.DeckCharacters = make(map[string]store.DeckCharacterState) u.DeckCharacters = make(map[string]store.DeckCharacterState)
u.Decks = make(map[store.DeckKey]store.DeckState) u.Decks = make(map[store.DeckKey]store.DeckState)
u.TripleDecks = make(map[store.DeckKey]store.TripleDeckState)
u.DeckSubWeapons = make(map[string][]string) u.DeckSubWeapons = make(map[string][]string)
u.DeckParts = make(map[string][]string) u.DeckParts = make(map[string][]string)
u.Quests = make(map[int32]store.UserQuestState) 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 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 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, FROM user_deck_sub_weapons WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid,
func(rows *sql.Rows) { func(rows *sql.Rows) {
+18
View File
@@ -171,6 +171,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
return err 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 key, uuids := range u.DeckSubWeapons {
for i, uuid := range uuids { 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 (?,?,?,?)`, 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) { replaceSliceTable(tx, uid, "user_deck_sub_weapons", after.DeckSubWeapons, func(key string, uuids []string) {
for i, uuid := range uuids { 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) exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`, uid, key, i, uuid)
+14
View File
@@ -70,6 +70,7 @@ type UserState struct {
Thoughts map[string]ThoughtState Thoughts map[string]ThoughtState
DeckCharacters map[string]DeckCharacterState DeckCharacters map[string]DeckCharacterState
Decks map[DeckKey]DeckState Decks map[DeckKey]DeckState
TripleDecks map[DeckKey]TripleDeckState
Quests map[int32]UserQuestState Quests map[int32]UserQuestState
QuestMissions map[QuestMissionKey]UserQuestMissionState QuestMissions map[QuestMissionKey]UserQuestMissionState
Missions map[int32]UserMissionState Missions map[int32]UserMissionState
@@ -142,6 +143,9 @@ func (u *UserState) EnsureMaps() {
if u.Decks == nil { if u.Decks == nil {
u.Decks = make(map[DeckKey]DeckState) u.Decks = make(map[DeckKey]DeckState)
} }
if u.TripleDecks == nil {
u.TripleDecks = make(map[DeckKey]TripleDeckState)
}
if u.DeckSubWeapons == nil { if u.DeckSubWeapons == nil {
u.DeckSubWeapons = make(map[string][]string) u.DeckSubWeapons = make(map[string][]string)
} }
@@ -455,6 +459,16 @@ type DeckState struct {
LatestVersion int64 LatestVersion int64
} }
type TripleDeckState struct {
DeckType model.DeckType
UserDeckNumber int32
Name string
DeckNumber01 int32
DeckNumber02 int32
DeckNumber03 int32
LatestVersion int64
}
type DeckCharacterInput struct { type DeckCharacterInput struct {
UserCostumeUuid string UserCostumeUuid string
MainUserWeaponUuid string MainUserWeaponUuid string
@@ -188,6 +188,9 @@ func ChangedTables(before, after *store.UserState) []string {
if !mapsEqualStruct(before.Decks, after.Decks) { if !mapsEqualStruct(before.Decks, after.Decks) {
add("IUserDeck") add("IUserDeck")
} }
if !mapsEqualStruct(before.TripleDecks, after.TripleDecks) {
add("IUserTripleDeck")
}
if !mapsEqualSliceValues(before.DeckSubWeapons, after.DeckSubWeapons) { if !mapsEqualSliceValues(before.DeckSubWeapons, after.DeckSubWeapons) {
add("IUserDeckSubWeaponGroup") add("IUserDeckSubWeaponGroup")
} }
@@ -358,6 +361,8 @@ func keyFieldsForTable(table string) []string {
return []string{"userId", "userDeckCharacterUuid"} return []string{"userId", "userDeckCharacterUuid"}
case "IUserDeck": case "IUserDeck":
return []string{"userId", "deckType", "userDeckNumber"} return []string{"userId", "deckType", "userDeckNumber"}
case "IUserTripleDeck":
return []string{"userId", "deckType", "userDeckNumber"}
case "IUserDeckSubWeaponGroup": case "IUserDeckSubWeaponGroup":
return []string{"userId", "userDeckCharacterUuid", "sortOrder"} return []string{"userId", "userDeckCharacterUuid", "sortOrder"}
case "IUserDeckPartsGroup": case "IUserDeckPartsGroup":
+33
View File
@@ -13,6 +13,10 @@ func init() {
s, _ := utils.EncodeJSONMaps(sortedDeckRecords(user)...) s, _ := utils.EncodeJSONMaps(sortedDeckRecords(user)...)
return s return s
}) })
register("IUserTripleDeck", func(user store.UserState) string {
s, _ := utils.EncodeJSONMaps(sortedTripleDeckRecords(user)...)
return s
})
register("IUserDeckCharacter", func(user store.UserState) string { register("IUserDeckCharacter", func(user store.UserState) string {
s, _ := utils.EncodeJSONMaps(sortedDeckCharacterRecords(user)...) s, _ := utils.EncodeJSONMaps(sortedDeckCharacterRecords(user)...)
return s return s
@@ -68,6 +72,35 @@ func sortedDeckRecords(user store.UserState) []map[string]any {
return records 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 { func sortedDeckCharacterRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.DeckCharacters) keys := sortedStringKeys(user.DeckCharacters)
records := make([]map[string]any, 0, len(keys)) records := make([]map[string]any, 0, len(keys))
@@ -21,6 +21,7 @@ func FullClientTableMap(user store.UserState) map[string]string {
"IUserThought": projectTable("IUserThought", user), "IUserThought": projectTable("IUserThought", user),
"IUserDeckCharacter": projectTable("IUserDeckCharacter", user), "IUserDeckCharacter": projectTable("IUserDeckCharacter", user),
"IUserDeck": projectTable("IUserDeck", user), "IUserDeck": projectTable("IUserDeck", user),
"IUserTripleDeck": projectTable("IUserTripleDeck", user),
"IUserLogin": projectTable("IUserLogin", user), "IUserLogin": projectTable("IUserLogin", user),
"IUserLoginBonus": projectTable("IUserLoginBonus", user), "IUserLoginBonus": projectTable("IUserLoginBonus", user),
"IUserMission": projectTable("IUserMission", user), "IUserMission": projectTable("IUserMission", user),
@@ -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;